Skip to content

Commit 4872d06

Browse files
yaauiemergify[bot]
authored andcommitted
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)
1 parent 93f84ca commit 4872d06

File tree

11 files changed

+345
-12
lines changed

11 files changed

+345
-12
lines changed

lib/pluginmanager/command.rb

Lines changed: 25 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 1 addition & 0 deletions
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

lib/pluginmanager/remove.rb

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 3 deletions
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)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
services:
3+
- logstash

qa/integration/services/logstash_service.rb

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ def run_cmd(cmd_args, change_dir = true, environment = {})
306306
if ENV.key?("BUILD_JAVA_HOME") && !process.environment.key?("LS_JAVA_HOME")
307307
process.environment["LS_JAVA_HOME"] = ENV["BUILD_JAVA_HOME"]
308308
end
309-
process.io.stdout = process.io.stderr = out
309+
process.io.stdout = process.io.stderr = SynchronizedDelegate.new(out)
310310

311311
Bundler.with_unbundled_env do
312312
if change_dir
@@ -327,6 +327,31 @@ def run(*args)
327327
run_cmd [@logstash_bin, *args]
328328
end
329329

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

332357
LOGSTASH_PLUGIN = File.join("bin", "logstash-plugin")
@@ -360,9 +385,26 @@ def list(*plugins, verbose: false)
360385
run(command)
361386
end
362387

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

368410
def run(command)

qa/integration/specs/cli/install_spec.rb

Lines changed: 100 additions & 2 deletions
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,92 @@ 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_examples("overwriting 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}-#{specified_plugin_version}").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}-#{specified_plugin_version}").to_not be_installed_gem
182+
end
183+
end
184+
it "installs the specified version and removes the pre-existing one" do
185+
execute = @logstash_plugin.install(plugin_name, version: specified_plugin_version)
186+
187+
aggregate_failures("command execution") do
188+
expect(execute.stderr_and_stdout).to match(INSTALLATION_SUCCESS_RE)
189+
expect(execute.exit_code).to eq(0)
190+
end
191+
192+
installed = @logstash_plugin.list(plugin_name, verbose: true)
193+
expect(installed.stderr_and_stdout).to match(/#{Regexp.escape plugin_name} [(]#{Regexp.escape(specified_plugin_version)}[)]/)
194+
195+
expect("#{plugin_name}-#{existing_plugin_version}").to_not be_installed_gem
196+
expect("#{plugin_name}-#{specified_plugin_version}").to be_installed_gem
197+
end
198+
end
199+
200+
context "when installing over an older version" do
201+
let(:plugin_name) { "logstash-filter-qatest" }
202+
let(:existing_plugin_version) { "0.1.0" }
203+
let(:specified_plugin_version) { "0.1.1" }
204+
205+
include_examples "overwriting existing"
206+
end
207+
208+
context "when installing over a newer version" do
209+
let(:plugin_name) { "logstash-filter-qatest" }
210+
let(:existing_plugin_version) { "0.1.0" }
211+
let(:specified_plugin_version) { "0.1.1" }
212+
213+
include_examples "overwriting existing"
214+
end
215+
216+
context "installing plugin that isn't present" do
217+
it "installs the plugin" do
218+
aggregate_failures("prevalidation") do
219+
expect("logstash-filter-qatest").to_not be_installed_gem
220+
end
221+
222+
execute = @logstash_plugin.install("logstash-filter-qatest")
223+
224+
expect(execute.stderr_and_stdout).to match(INSTALLATION_SUCCESS_RE)
225+
expect(execute.exit_code).to eq(0)
226+
227+
installed = @logstash_plugin.list("logstash-filter-qatest")
228+
expect(installed.stderr_and_stdout).to match(/logstash-filter-qatest/)
229+
expect(installed.exit_code).to eq(0)
230+
231+
expect(gem_in_lock_file?(/logstash-filter-qatest/, @logstash.lock_file)).to be_truthy
232+
233+
expect("logstash-filter-qatest").to be_installed_gem
234+
end
235+
end
236+
context "installing plugin that doesn't exist on rubygems" do
237+
it "doesn't install anything" do
238+
execute = @logstash_plugin.install("logstash-filter-404-no-exist")
239+
240+
expect(execute.stderr_and_stdout).to match(INSTALLATION_ABORTED_RE)
241+
expect(execute.exit_code).to eq(1)
242+
end
243+
end
244+
context "installing gem that isn't a plugin" do
245+
it "doesn't install anything" do
246+
execute = @logstash_plugin.install("dummy_gem")
247+
248+
expect(execute.stderr_and_stdout).to match(INSTALLATION_ABORTED_RE)
249+
expect(execute.exit_code).to eq(1)
250+
end
251+
end
252+
end
155253
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
require 'pathname'
2+
3+
shared_context "pluginmanager validation helpers" do
4+
5+
matcher :be_installed_gem do
6+
match do |actual|
7+
common(actual)
8+
@gemspec_present && @gem_installed
9+
end
10+
11+
match_when_negated do |actual|
12+
common(actual)
13+
!@gemspec_present && !@gem_installed
14+
end
15+
16+
define_method :common do |actual|
17+
version_suffix = /-[0-9.]+(-java)?$/
18+
filename_matcher = actual.match?(version_suffix) ? actual : /^#{Regexp.escape(actual)}#{version_suffix}/
19+
20+
@gems = (logstash_gemdir / "gems").glob("*-*")
21+
@gemspecs = (logstash_gemdir / "specifications").glob("*-*.gemspec")
22+
23+
@gem_installed = @gems.find { |gem| gem.basename.to_s.match?(filename_matcher) }
24+
@gemspec_present = @gemspecs.find { |gemspec| gemspec.basename(".gemspec").to_s.match?(filename_matcher) }
25+
end
26+
27+
failure_message do |actual|
28+
reasons = []
29+
reasons << "the gem dir could not be found (#{@gems})" unless @gem_installed
30+
reasons << "the gemspec could not be found (#{@gemspecs})" unless @gemspec_present
31+
32+
"expected that #{actual} would be installed, but #{reasons.join(' and ')}"
33+
end
34+
failure_message_when_negated do |actual|
35+
reasons = []
36+
reasons << "the gem dir is present (#{@gem_installed})" if @gem_installed
37+
reasons << "the gemspec is present (#{@gemspec_present})" if @gemspec_present
38+
39+
"expected that #{actual} would not be installed, but #{reasons.join(' and ')}"
40+
end
41+
end
42+
43+
def logstash_home
44+
return super() if defined?(super)
45+
return @logstash.logstash_home if @logstash
46+
fail("no @logstash, so we can't get logstash_home")
47+
end
48+
49+
def logstash_gemdir
50+
pathname_base = (Pathname.new(logstash_home) / "vendor" / "bundle" / "jruby")
51+
candidate_dirs = pathname_base.glob("[0-9]*")
52+
case candidate_dirs.size
53+
when 0 then fail("no version dir found in #{pathname_base}")
54+
when 1 then candidate_dirs.first
55+
else
56+
fail("multiple version dirs found in #{pathname_base} (#{candidate_dirs.map(&:basename)}")
57+
end
58+
end
59+
end

0 commit comments

Comments
 (0)