Skip to content

Commit fde903c

Browse files
authored
Backport 17203 and 17267 8.x (#17270)
* Pluginmanager clean after mutate (#17203) * pluginmanager: always clean after mutate * pluginmanager: don't skip updating plugins installed with --version * pr feedback (cherry picked from commit 8c96913) * Pluginmanager install preserve (#17267) * tests: integration tests for pluginmanager install --preserve * fix regression where pluginmanager's install --preserve flag didn't
1 parent fcdda83 commit fde903c

File tree

11 files changed

+436
-13
lines changed

11 files changed

+436
-13
lines changed

lib/pluginmanager/command.rb

+25
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,31 @@ def remove_unused_locally_installed_gems!
4747
end
4848
end
4949

50+
def remove_orphan_dependencies!
51+
locked_gem_names = ::Bundler::LockfileParser.new(File.read(LogStash::Environment::LOCKFILE)).specs.map(&:full_name).to_set
52+
orphan_gem_specs = ::Gem::Specification.each
53+
.reject(&:stubbed?) # skipped stubbed (uninstalled) gems
54+
.reject(&:default_gem?) # don't touch jruby-included default gems
55+
.reject{ |spec| locked_gem_names.include?(spec.full_name) }
56+
.sort
57+
58+
inactive_plugins, orphaned_dependencies = orphan_gem_specs.partition { |spec| LogStash::PluginManager.logstash_plugin_gem_spec?(spec) }
59+
60+
# uninstall plugins first, to limit damage should one fail to uninstall
61+
inactive_plugins.each { |plugin| uninstall_gem!("inactive plugin", plugin) }
62+
orphaned_dependencies.each { |dep| uninstall_gem!("orphaned dependency", dep) }
63+
end
64+
65+
def uninstall_gem!(desc, spec)
66+
require "rubygems/uninstaller"
67+
Gem::DefaultUserInteraction.use_ui(debug? ? Gem::DefaultUserInteraction.ui : Gem::SilentUI.new) do
68+
Gem::Uninstaller.new(spec.name, version: spec.version, force: true, executables: true).uninstall
69+
end
70+
puts "cleaned #{desc} #{spec.name} (#{spec.version})"
71+
rescue Gem::InstallError => e
72+
report_exception("Failed to uninstall `#{spec.full_name}`", e)
73+
end
74+
5075
def relative_path(path)
5176
require "pathname"
5277
::Pathname.new(path).relative_path_from(::Pathname.new(LogStash::Environment::LOGSTASH_HOME)).to_s

lib/pluginmanager/gemfile.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ def add_gem(_gem)
133133
# update existing or add new
134134
def update_gem(_gem)
135135
if old = find_gem(_gem.name)
136-
# always overwrite requirements if specified
137-
old.requirements = _gem.requirements unless no_constrains?(_gem.requirements)
136+
# always overwrite requirements
137+
old.requirements = _gem.requirements
138138
# but merge options
139139
old.options = old.options.merge(_gem.options)
140140
else

lib/pluginmanager/install.rb

+4-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def execute
7979
install_gems_list!(gems)
8080
remove_unused_locally_installed_gems!
8181
remove_unused_integration_overlaps!
82+
remove_orphan_dependencies!
8283
end
8384

8485
private
@@ -213,7 +214,9 @@ def install_gems_list!(install_list)
213214
plugin_gem = gemfile.find(plugin)
214215
if preserve?
215216
puts("Preserving Gemfile gem options for plugin #{plugin}") if plugin_gem && !plugin_gem.options.empty?
216-
gemfile.update(plugin, version, options)
217+
# if the plugin exists and no version was specified, keep the existing requirements
218+
requirements = (plugin_gem && version.nil? ? plugin_gem.requirements : [version]).compact
219+
gemfile.update(plugin, *requirements, options)
217220
else
218221
gemfile.overwrite(plugin, version, options)
219222
end

lib/pluginmanager/remove.rb

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def execute
6767
exit(1) unless ::Bundler::LogstashUninstall.uninstall!(plugin_list)
6868
LogStash::Bundler.genericize_platform
6969
remove_unused_locally_installed_gems!
70+
remove_orphan_dependencies!
7071
rescue => exception
7172
report_exception("Operation aborted, cannot remove plugin", exception)
7273
end

lib/pluginmanager/update.rb

+1-3
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,8 @@ def update_gems!
9595
output << LogStash::Bundler.genericize_platform unless output.nil?
9696
end
9797

98-
# We currently dont removed unused gems from the logstash installation
99-
# see: https://github.com/elastic/logstash/issues/6339
100-
# output = LogStash::Bundler.invoke!(:clean => true)
10198
display_updated_plugins(previous_gem_specs_map)
99+
remove_orphan_dependencies!
102100
rescue => exception
103101
gemfile.restore!
104102
report_exception("Updated Aborted", exception)
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
services:
3+
- logstash

qa/integration/services/logstash_service.rb

+46-4
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ def run_cmd(cmd_args, change_dir = true, environment = {})
308308
if ENV.key?("BUILD_JAVA_HOME") && !process.environment.key?("LS_JAVA_HOME")
309309
process.environment["LS_JAVA_HOME"] = ENV["BUILD_JAVA_HOME"]
310310
end
311-
process.io.stdout = process.io.stderr = out
311+
process.io.stdout = process.io.stderr = SynchronizedDelegate.new(out)
312312

313313
Bundler.with_unbundled_env do
314314
if change_dir
@@ -329,6 +329,31 @@ def run(*args)
329329
run_cmd [@logstash_bin, *args]
330330
end
331331

332+
##
333+
# A `SynchronizedDelegate` wraps any object and ensures that exactly one
334+
# calling thread is invoking methods on it at a time. This is useful for our
335+
# clumsy setting of process io STDOUT and STDERR to the same IO object, which
336+
# can cause interleaved writes.
337+
class SynchronizedDelegate
338+
def initialize(obj)
339+
require "monitor"
340+
@mon = Monitor.new
341+
@obj = obj
342+
end
343+
344+
def respond_to_missing?(method_name, include_private = false)
345+
@obj.respond_to?(method_name, include_private) || super
346+
end
347+
348+
def method_missing(method_name, *args, &block)
349+
return super unless @obj.respond_to?(method_name)
350+
351+
@mon.synchronize do
352+
@obj.public_send(method_name, *args, &block)
353+
end
354+
end
355+
end
356+
332357
class PluginCli
333358

334359
LOGSTASH_PLUGIN = File.join("bin", "logstash-plugin")
@@ -362,9 +387,26 @@ def list(*plugins, verbose: false)
362387
run(command)
363388
end
364389

365-
def install(plugin_name, *additional_plugins)
366-
plugin_list = ([plugin_name]+additional_plugins).flatten
367-
run("install #{Shellwords.shelljoin(plugin_list)}")
390+
def install(plugin_name, *additional_plugins, version: nil, verify: true, preserve: false, local: false)
391+
args = []
392+
args << "--no-verify" unless verify
393+
args << "--preserve" if preserve
394+
args << "--local" if local
395+
args << "--version" << version unless version.nil?
396+
args.concat(([plugin_name]+additional_plugins).flatten)
397+
398+
run("install #{Shellwords.shelljoin(args)}")
399+
end
400+
401+
def update(*plugin_list, level: nil, local: nil, verify: nil, conservative: nil)
402+
args = []
403+
args << (verify ? "--verify" : "--no-verify") unless verify.nil?
404+
args << "--level" << "#{level}" unless level.nil?
405+
args << "--local" if local
406+
args << (conservative ? "--conservative" : "--no-conservative") unless conservative.nil?
407+
args.concat(plugin_list)
408+
409+
run("update #{Shellwords.shelljoin(args)}")
368410
end
369411

370412
def run(command)

qa/integration/specs/cli/install_spec.rb

+155-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
require_relative "../../framework/settings"
2020
require_relative "../../services/logstash_service"
2121
require_relative "../../framework/helpers"
22+
require_relative "pluginmanager_spec_helper"
2223
require "logstash/devutils/rspec/spec_helper"
2324
require "stud/temporary"
2425
require "fileutils"
@@ -29,23 +30,32 @@ def gem_in_lock_file?(pattern, lock_file)
2930
content.match(pattern)
3031
end
3132

33+
def plugin_filename_re(name, version)
34+
%Q(\b#{Regexp.escape name}-#{Regexp.escape version}(-java)?\b)
35+
end
36+
3237
# Bundler can mess up installation successful output: https://github.com/elastic/logstash/issues/15801
3338
INSTALL_SUCCESS_RE = /IB?nstall successful/
3439
INSTALLATION_SUCCESS_RE = /IB?nstallation successful/
3540

41+
INSTALLATION_ABORTED_RE = /Installation aborted/
42+
3643
describe "CLI > logstash-plugin install" do
37-
before(:all) do
44+
before(:each) do
3845
@fixture = Fixture.new(__FILE__)
3946
@logstash = @fixture.get_service("logstash")
4047
@logstash_plugin = @logstash.plugin_cli
41-
@pack_directory = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "fixtures", "logstash-dummy-pack"))
4248
end
4349

4450
shared_examples "install from a pack" do
4551
let(:pack) { "file://#{File.join(@pack_directory, "logstash-dummy-pack.zip")}" }
4652
let(:install_command) { "bin/logstash-plugin install" }
4753
let(:change_dir) { true }
4854

55+
before(:all) do
56+
@pack_directory = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "fixtures", "logstash-dummy-pack"))
57+
end
58+
4959
# When you are on anything by linux we won't disable the internet with seccomp
5060
if RbConfig::CONFIG["host_os"] == "linux"
5161
context "without internet connection (linux seccomp wrapper)" do
@@ -152,4 +162,147 @@ def gem_in_lock_file?(pattern, lock_file)
152162
end
153163
end
154164
end
165+
166+
context "rubygems hosted plugin" do
167+
include_context "pluginmanager validation helpers"
168+
shared_context("install over existing") do
169+
before(:each) do
170+
aggregate_failures("precheck") do
171+
expect("#{plugin_name}-#{existing_plugin_version}").to_not be_installed_gem
172+
expect("#{plugin_name}").to_not be_installed_gem
173+
end
174+
aggregate_failures("setup") do
175+
execute = @logstash_plugin.install(plugin_name, version: existing_plugin_version)
176+
177+
expect(execute.stderr_and_stdout).to match(INSTALLATION_SUCCESS_RE)
178+
expect(execute.exit_code).to eq(0)
179+
180+
expect("#{plugin_name}-#{existing_plugin_version}").to be_installed_gem
181+
expect(plugin_name).to be_in_gemfile.with_requirements(existing_plugin_version)
182+
end
183+
end
184+
end
185+
shared_examples("overwriting existing with explicit version") do
186+
include_context "install over existing"
187+
it "installs the specified version and removes the pre-existing one" do
188+
execute = @logstash_plugin.install(plugin_name, version: specified_plugin_version)
189+
190+
aggregate_failures("command execution") do
191+
expect(execute.stderr_and_stdout).to match(INSTALLATION_SUCCESS_RE)
192+
expect(execute.exit_code).to eq(0)
193+
end
194+
195+
installed = @logstash_plugin.list(plugin_name, verbose: true)
196+
expect(installed.stderr_and_stdout).to match(/#{Regexp.escape plugin_name} [(]#{Regexp.escape(specified_plugin_version)}[)]/)
197+
198+
expect("#{plugin_name}-#{existing_plugin_version}").to_not be_installed_gem
199+
expect("#{plugin_name}-#{specified_plugin_version}").to be_installed_gem
200+
end
201+
end
202+
203+
context "when installing over an older version using --version" do
204+
let(:plugin_name) { "logstash-filter-qatest" }
205+
let(:existing_plugin_version) { "0.1.0" }
206+
let(:specified_plugin_version) { "0.1.1" }
207+
208+
include_examples "overwriting existing with explicit version"
209+
end
210+
211+
context "when installing over a newer version using --version" do
212+
let(:plugin_name) { "logstash-filter-qatest" }
213+
let(:existing_plugin_version) { "0.1.0" }
214+
let(:specified_plugin_version) { "0.1.1" }
215+
216+
include_examples "overwriting existing with explicit version"
217+
end
218+
219+
context "when installing over existing without --version" do
220+
let(:plugin_name) { "logstash-filter-qatest" }
221+
let(:existing_plugin_version) { "0.1.0" }
222+
223+
include_context "install over existing"
224+
225+
context "with --preserve" do
226+
it "succeeds without changing the requirements in the Gemfile" do
227+
execute = @logstash_plugin.install(plugin_name, preserve: true)
228+
229+
aggregate_failures("command execution") do
230+
expect(execute.stderr_and_stdout).to match(INSTALLATION_SUCCESS_RE)
231+
expect(execute.exit_code).to eq(0)
232+
end
233+
234+
installed = @logstash_plugin.list(verbose: true)
235+
expect(installed.stderr_and_stdout).to match(/#{Regexp.escape plugin_name}/)
236+
237+
# we want to ensure that the act of installing an already-installed plugin
238+
# does not change its requirements in the gemfile, and leaves the previously-installed
239+
# version in-tact.
240+
expect(plugin_name).to be_in_gemfile.with_requirements(existing_plugin_version)
241+
expect("#{plugin_name}-#{existing_plugin_version}").to be_installed_gem
242+
end
243+
end
244+
245+
context "without --preserve" do
246+
# this spec is OBSERVED behaviour, which I believe to be undesirable.
247+
it "succeeds and removes the version requirement from the Gemfile" do
248+
execute = @logstash_plugin.install(plugin_name)
249+
250+
aggregate_failures("command execution") do
251+
expect(execute.stderr_and_stdout).to match(INSTALLATION_SUCCESS_RE)
252+
expect(execute.exit_code).to eq(0)
253+
end
254+
255+
installed = @logstash_plugin.list(plugin_name, verbose: true)
256+
expect(installed.stderr_and_stdout).to match(/#{Regexp.escape plugin_name}/)
257+
258+
# This is the potentially-undesirable surprising behaviour, specified here
259+
# as a means of documentation, not a promise of future behavior.
260+
expect(plugin_name).to be_in_gemfile.without_requirements
261+
262+
# we expect _a_ version of the plugin to be installed, but cannot be opinionated
263+
# about which version was installed because bundler won't necessarily re-resolve
264+
# the dependency graph to get us an upgrade since the no-requirements dependency
265+
# is still met (but it MAY do so if also installing plugins that are not present).
266+
expect("#{plugin_name}").to be_installed_gem
267+
end
268+
end
269+
end
270+
271+
context "installing plugin that isn't present" do
272+
it "installs the plugin" do
273+
aggregate_failures("prevalidation") do
274+
expect("logstash-filter-qatest").to_not be_installed_gem
275+
end
276+
277+
execute = @logstash_plugin.install("logstash-filter-qatest")
278+
279+
expect(execute.stderr_and_stdout).to match(INSTALLATION_SUCCESS_RE)
280+
expect(execute.exit_code).to eq(0)
281+
282+
installed = @logstash_plugin.list("logstash-filter-qatest")
283+
expect(installed.stderr_and_stdout).to match(/logstash-filter-qatest/)
284+
expect(installed.exit_code).to eq(0)
285+
286+
expect(gem_in_lock_file?(/logstash-filter-qatest/, @logstash.lock_file)).to be_truthy
287+
288+
expect("logstash-filter-qatest").to be_installed_gem
289+
end
290+
end
291+
context "installing plugin that doesn't exist on rubygems" do
292+
it "doesn't install anything" do
293+
execute = @logstash_plugin.install("logstash-filter-404-no-exist")
294+
295+
expect(execute.stderr_and_stdout).to match(INSTALLATION_ABORTED_RE)
296+
expect(execute.exit_code).to eq(1)
297+
end
298+
end
299+
context "installing gem that isn't a plugin" do
300+
it "doesn't install anything" do
301+
execute = @logstash_plugin.install("dummy_gem")
302+
303+
expect(execute.stderr_and_stdout).to match(INSTALLATION_ABORTED_RE)
304+
expect(execute.exit_code).to eq(1)
305+
end
306+
end
307+
end
155308
end

0 commit comments

Comments
 (0)