mirror of
https://github.com/elastic/logstash.git
synced 2025-04-25 07:07:54 -04:00
Allow the installation of a plugin from a .gem
, refactor the plugin manager classes.
do not change the gemfile or the .lock Fixes #2946
This commit is contained in:
parent
55871247d5
commit
96b6893ebc
13 changed files with 310 additions and 151 deletions
|
@ -23,6 +23,28 @@ end
|
||||||
|
|
||||||
module LogStash
|
module LogStash
|
||||||
module Bundler
|
module Bundler
|
||||||
|
# Take a gem package and extract it to a specific target
|
||||||
|
# @param [String] Gem file, this must be a path
|
||||||
|
# @param [String, String] Return a Gem::Package and the installed path
|
||||||
|
def self.unpack(file, path)
|
||||||
|
require "rubygems/package"
|
||||||
|
require "securerandom"
|
||||||
|
|
||||||
|
# We are creating a random directory per extract,
|
||||||
|
# if we dont do this bundler will not trigger download of the dependencies.
|
||||||
|
# Use case is:
|
||||||
|
# - User build his own gem with a fix
|
||||||
|
# - User doesnt increment the version
|
||||||
|
# - User install the same version but different code or dependencies multiple times..
|
||||||
|
basename = ::File.basename(file, '.gem')
|
||||||
|
unique = SecureRandom.hex(4)
|
||||||
|
target_path = ::File.expand_path(::File.join(path, unique, basename))
|
||||||
|
|
||||||
|
package = ::Gem::Package.new(file)
|
||||||
|
package.extract_files(target_path)
|
||||||
|
|
||||||
|
return [package, target_path]
|
||||||
|
end
|
||||||
|
|
||||||
# capture any $stdout from the passed block. also trap any exception in that block, in which case the trapped exception will be returned
|
# capture any $stdout from the passed block. also trap any exception in that block, in which case the trapped exception will be returned
|
||||||
# @param [Proc] the code block to execute
|
# @param [Proc] the code block to execute
|
||||||
|
@ -53,6 +75,9 @@ module LogStash
|
||||||
|
|
||||||
ENV["GEM_PATH"] = LogStash::Environment.logstash_gem_home
|
ENV["GEM_PATH"] = LogStash::Environment.logstash_gem_home
|
||||||
|
|
||||||
|
# force Rubygems sources to our Gemfile sources
|
||||||
|
::Gem.sources = options[:rubygems_source] if options[:rubygems_source]
|
||||||
|
|
||||||
::Bundler.settings[:path] = LogStash::Environment::BUNDLE_DIR
|
::Bundler.settings[:path] = LogStash::Environment::BUNDLE_DIR
|
||||||
::Bundler.settings[:gemfile] = LogStash::Environment::GEMFILE_PATH
|
::Bundler.settings[:gemfile] = LogStash::Environment::GEMFILE_PATH
|
||||||
::Bundler.settings[:without] = options[:without].join(":")
|
::Bundler.settings[:without] = options[:without].join(":")
|
||||||
|
@ -60,7 +85,7 @@ module LogStash
|
||||||
try = 0
|
try = 0
|
||||||
|
|
||||||
# capture_stdout also traps any raised exception and pass them back as the function return [output, exception]
|
# capture_stdout also traps any raised exception and pass them back as the function return [output, exception]
|
||||||
capture_stdout do
|
output, exception = capture_stdout do
|
||||||
loop do
|
loop do
|
||||||
begin
|
begin
|
||||||
::Bundler.reset!
|
::Bundler.reset!
|
||||||
|
@ -81,11 +106,15 @@ module LogStash
|
||||||
|
|
||||||
try += 1
|
try += 1
|
||||||
$stderr.puts("Error #{e.class}, retrying #{try}/#{options[:max_tries]}")
|
$stderr.puts("Error #{e.class}, retrying #{try}/#{options[:max_tries]}")
|
||||||
$stderr.puts(e.message) if ENV["DEBUG"]
|
$stderr.puts(e.message)
|
||||||
sleep(0.5)
|
sleep(0.5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
raise exception if exception
|
||||||
|
|
||||||
|
return output
|
||||||
end
|
end
|
||||||
|
|
||||||
# build Bundler::CLI.start arguments array from the given options hash
|
# build Bundler::CLI.start arguments array from the given options hash
|
||||||
|
|
|
@ -50,6 +50,7 @@ module LogStash
|
||||||
GEMFILE_PATH = ::File.join(LOGSTASH_HOME, "Gemfile")
|
GEMFILE_PATH = ::File.join(LOGSTASH_HOME, "Gemfile")
|
||||||
BUNDLE_CONFIG_PATH = ::File.join(LOGSTASH_HOME, ".bundle", "config")
|
BUNDLE_CONFIG_PATH = ::File.join(LOGSTASH_HOME, ".bundle", "config")
|
||||||
BOOTSTRAP_GEM_PATH = ::File.join(LOGSTASH_HOME, 'build', 'bootstrap')
|
BOOTSTRAP_GEM_PATH = ::File.join(LOGSTASH_HOME, 'build', 'bootstrap')
|
||||||
|
LOCAL_GEM_PATH = ::File.join(LOGSTASH_HOME, 'vendor', 'locally_installed_gem')
|
||||||
|
|
||||||
LOGSTASH_ENV = (ENV["LS_ENV"] || 'production').to_s.freeze
|
LOGSTASH_ENV = (ENV["LS_ENV"] || 'production').to_s.freeze
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
require "logstash/util"
|
||||||
module LogStash
|
module LogStash
|
||||||
|
|
||||||
class GemfileError < StandardError; end
|
class GemfileError < StandardError; end
|
||||||
|
@ -17,6 +18,7 @@ module LogStash
|
||||||
|
|
||||||
def load
|
def load
|
||||||
@gemset ||= DSL.parse(@io.read)
|
@gemset ||= DSL.parse(@io.read)
|
||||||
|
backup
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -51,6 +53,23 @@ module LogStash
|
||||||
def remove(name)
|
def remove(name)
|
||||||
@gemset.remove_gem(name)
|
@gemset.remove_gem(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def backup
|
||||||
|
@orignal_backup = @gemset.copy
|
||||||
|
end
|
||||||
|
|
||||||
|
def restore
|
||||||
|
@gemset = @orignal_backup
|
||||||
|
end
|
||||||
|
|
||||||
|
def restore!
|
||||||
|
restore
|
||||||
|
save
|
||||||
|
end
|
||||||
|
|
||||||
|
def locally_installed_gems
|
||||||
|
@gemset.gems.select { |gem| gem.options.include?(:path) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Gemset
|
class Gemset
|
||||||
|
@ -101,7 +120,6 @@ module LogStash
|
||||||
def copy
|
def copy
|
||||||
Marshal.load(Marshal.dump(self))
|
Marshal.load(Marshal.dump(self))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def sources_to_s
|
def sources_to_s
|
||||||
|
|
37
lib/logstash/pluginmanager/base.rb
Normal file
37
lib/logstash/pluginmanager/base.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
class LogStash::PluginManager::Base < Clamp::Command
|
||||||
|
def gemfile
|
||||||
|
@gemfile ||= LogStash::Gemfile.new(File.new(LogStash::Environment::GEMFILE_PATH, 'r+')).load
|
||||||
|
end
|
||||||
|
|
||||||
|
# If set in debug mode we will raise an exception and display the stacktrace
|
||||||
|
def report_exception(readable_message, exception)
|
||||||
|
if ENV["DEBUG"]
|
||||||
|
raise exception
|
||||||
|
else
|
||||||
|
signal_error("#{readable_message}, message: #{exception.message}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def display_bundler_output(output)
|
||||||
|
if ENV['DEBUG'] && output
|
||||||
|
# Display what bundler did in the last run
|
||||||
|
$stderr.puts("Bundler output")
|
||||||
|
$stderr.puts(output)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Each plugin install for a gemfile create a path with a unique id.
|
||||||
|
# we must clear what is not currently used in the
|
||||||
|
def remove_unused_locally_installed_gems!
|
||||||
|
used_path = gemfile.locally_installed_gems.collect { |gem| gem.options[:path] }
|
||||||
|
|
||||||
|
Dir.glob(File.join(LogStash::Environment::LOCAL_GEM_PATH, '*')) do |path|
|
||||||
|
FileUtils.rm_rf(relative_path(path)) if used_path.none? { |p| p.start_with?(relative_path(path)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def relative_path(path)
|
||||||
|
Pathname.new(path).relative_path_from(Pathname.new(LogStash::Environment::LOGSTASH_HOME)).to_s
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,15 +1,16 @@
|
||||||
require 'clamp'
|
require "clamp"
|
||||||
require 'logstash/namespace'
|
require "logstash/namespace"
|
||||||
require 'logstash/environment'
|
require "logstash/environment"
|
||||||
require 'logstash/pluginmanager/util'
|
require "logstash/pluginmanager/util"
|
||||||
require 'jar-dependencies'
|
require "logstash/pluginmanager/base"
|
||||||
require 'jar_install_post_install_hook'
|
require "jar-dependencies"
|
||||||
require 'file-dependencies/gem'
|
require "jar_install_post_install_hook"
|
||||||
|
require "file-dependencies/gem"
|
||||||
require "logstash/gemfile"
|
require "logstash/gemfile"
|
||||||
require "logstash/bundler"
|
require "logstash/bundler"
|
||||||
|
require "fileutils"
|
||||||
|
|
||||||
class LogStash::PluginManager::Install < Clamp::Command
|
class LogStash::PluginManager::Install < LogStash::PluginManager::Base
|
||||||
parameter "[PLUGIN] ...", "plugin name(s) or file"
|
parameter "[PLUGIN] ...", "plugin name(s) or file"
|
||||||
option "--version", "VERSION", "version of the plugin to install"
|
option "--version", "VERSION", "version of the plugin to install"
|
||||||
option "--[no-]verify", :flag, "verify plugin validity before installation", :default => true
|
option "--[no-]verify", :flag, "verify plugin validity before installation", :default => true
|
||||||
|
@ -18,95 +19,119 @@ class LogStash::PluginManager::Install < Clamp::Command
|
||||||
# the install logic below support installing multiple plugins with each a version specification
|
# the install logic below support installing multiple plugins with each a version specification
|
||||||
# but the argument parsing does not support it for now so currently if specifying --version only
|
# but the argument parsing does not support it for now so currently if specifying --version only
|
||||||
# one plugin name can be also specified.
|
# one plugin name can be also specified.
|
||||||
#
|
|
||||||
# TODO: find right syntax to allow specifying list of plugins with optional version specification for each
|
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
if development?
|
validate_cli_options!
|
||||||
raise(LogStash::PluginManager::Error, "Cannot specify plugin(s) with --development, it will add the development dependencies of the currently installed plugins") unless plugin_list.empty?
|
|
||||||
|
if local_gems?
|
||||||
|
gems = extract_local_gems_plugins
|
||||||
|
elsif development?
|
||||||
|
gems = plugins_development_gems
|
||||||
else
|
else
|
||||||
raise(LogStash::PluginManager::Error, "No plugin specified") if plugin_list.empty? && verify?
|
gems = plugins_gems
|
||||||
|
verify!(gems)
|
||||||
# temporary until we fullfil TODO ^^
|
|
||||||
raise(LogStash::PluginManager::Error, "Only 1 plugin name can be specified with --version") if version && plugin_list.size > 1
|
|
||||||
end
|
end
|
||||||
raise(LogStash::PluginManager::Error, "File #{LogStash::Environment::GEMFILE_PATH} does not exist or is not writable, aborting") unless File.writable?(LogStash::Environment::GEMFILE_PATH)
|
|
||||||
|
|
||||||
gemfile = LogStash::Gemfile.new(File.new(LogStash::Environment::GEMFILE_PATH, "r+")).load
|
install_gems_list!(gems)
|
||||||
# keep a copy of the gemset to revert on error
|
remove_unused_locally_installed_gems!
|
||||||
original_gemset = gemfile.gemset.copy
|
end
|
||||||
|
|
||||||
# force Rubygems sources to our Gemfile sources
|
|
||||||
Gem.sources = gemfile.gemset.sources
|
|
||||||
|
|
||||||
# install_list will be an array of [plugin name, version] tuples, version can be nil
|
|
||||||
install_list = []
|
|
||||||
|
|
||||||
|
private
|
||||||
|
def validate_cli_options!
|
||||||
if development?
|
if development?
|
||||||
specs = LogStash::PluginManager.all_installed_plugins_gem_specs(gemfile)
|
signal_usage_error("Cannot specify plugin(s) with --development, it will add the development dependencies of the currently installed plugins") unless plugin_list.empty?
|
||||||
install_list = specs.inject([]) do |result, spec|
|
|
||||||
result = result + spec.dependencies.select{|dep| dep.type == :development}.map{|dep| [dep.name] + dep.requirement.as_list + [{:group => :development}]}
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
# at this point we know that plugin_list is not empty and if the --version is specified there is only one plugin in plugin_list
|
signal_usage_error("No plugin specified") if plugin_list.empty? && verify?
|
||||||
|
# TODO: find right syntax to allow specifying list of plugins with optional version specification for each
|
||||||
|
signal_usage_error("Only 1 plugin name can be specified with --version") if version && plugin_list.size > 1
|
||||||
|
end
|
||||||
|
signal_error("File #{LogStash::Environment::GEMFILE_PATH} does not exist or is not writable, aborting") unless ::File.writable?(LogStash::Environment::GEMFILE_PATH)
|
||||||
|
end
|
||||||
|
|
||||||
install_list = version ? [plugin_list << version] : plugin_list.map{|plugin| [plugin, nil]}
|
# Check if the specified gems contains
|
||||||
|
# the logstash `metadata`
|
||||||
install_list.each do |plugin, version|
|
def verify!(gems)
|
||||||
|
if verify?
|
||||||
|
gems.each do |plugin, version|
|
||||||
puts("Validating #{[plugin, version].compact.join("-")}")
|
puts("Validating #{[plugin, version].compact.join("-")}")
|
||||||
raise(LogStash::PluginManager::Error, "Installation aborted") unless LogStash::PluginManager.logstash_plugin?(plugin, version)
|
signal_error("Installation aborted, verification failed for #{plugin} #{version}") unless LogStash::PluginManager.logstash_plugin?(plugin, version)
|
||||||
end if verify?
|
end
|
||||||
|
end
|
||||||
# at this point we know that we either have a valid gem name & version or a valid .gem file path
|
|
||||||
|
|
||||||
# if LogStash::PluginManager.plugin_file?(plugin)
|
|
||||||
# raise(LogStash::PluginManager::Error) unless cache_gem_file(plugin)
|
|
||||||
# spec = LogStash::PluginManager.plugin_file_spec(plugin)
|
|
||||||
# gemfile.update(spec.name, spec.version.to_s)
|
|
||||||
# else
|
|
||||||
# plugins.each{|tuple| gemfile.update(*tuple)}
|
|
||||||
# end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def plugins_development_gems
|
||||||
|
# Get currently defined gems and their dev dependencies
|
||||||
|
specs = []
|
||||||
|
|
||||||
|
specs = LogStash::PluginManager.all_installed_plugins_gem_specs(gemfile)
|
||||||
|
|
||||||
|
# Construct the list of dependencies to add to the current gemfile
|
||||||
|
specs.each_with_object([]) do |spec, install_list|
|
||||||
|
dependencies = spec.dependencies
|
||||||
|
.select { |dep| dep.type == :development }
|
||||||
|
.map { |dep| [dep.name] + dep.requirement.as_list }
|
||||||
|
|
||||||
|
install_list.concat(dependencies)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def plugins_gems
|
||||||
|
version ? [plugin_list << version] : plugin_list.map { |plugin| [plugin, nil] }
|
||||||
|
end
|
||||||
|
|
||||||
|
# install_list will be an array of [plugin name, version, options] tuples, version it
|
||||||
|
# can be nil at this point we know that plugin_list is not empty and if the
|
||||||
|
# --version is specified there is only one plugin in plugin_list
|
||||||
|
#
|
||||||
|
def install_gems_list!(install_list)
|
||||||
|
# If something goes wrong during the installation `LogStash::Gemfile` will restore a backup version.
|
||||||
install_list = LogStash::PluginManager.merge_duplicates(install_list)
|
install_list = LogStash::PluginManager.merge_duplicates(install_list)
|
||||||
install_list.each{|plugin, version| gemfile.update(plugin, version)}
|
|
||||||
gemfile.save
|
|
||||||
|
|
||||||
puts("Installing" + (install_list.empty? ? "..." : " " + install_list.map{|plugin, version| plugin}.join(", ")))
|
# Add plugins/gems to the current gemfile
|
||||||
|
puts("Installing" + (install_list.empty? ? "..." : " " + install_list.collect(&:first).join(", ")))
|
||||||
|
install_list.each { |plugin, version, options| gemfile.update(plugin, version, options) }
|
||||||
|
|
||||||
|
# Sync gemfiles changes to disk to make them available to the `bundler install`'s API
|
||||||
|
gemfile.save
|
||||||
|
|
||||||
bundler_options = {:install => true}
|
bundler_options = {:install => true}
|
||||||
bundler_options[:without] = [] if development?
|
bundler_options[:without] = [] if development?
|
||||||
|
bundler_options[:rubygems_source] = gemfile.gemset.sources
|
||||||
|
|
||||||
# any errors will be logged to $stderr by invoke_bundler!
|
output = LogStash::Bundler.invoke_bundler!(bundler_options)
|
||||||
output, exception = LogStash::Bundler.invoke_bundler!(bundler_options)
|
|
||||||
|
|
||||||
if ENV["DEBUG"]
|
|
||||||
$stderr.puts(output)
|
|
||||||
$stderr.puts("Error: #{exception.class}, #{exception.message}") if exception
|
|
||||||
end
|
|
||||||
|
|
||||||
if exception
|
|
||||||
# revert to original Gemfile content
|
|
||||||
gemfile.gemset = original_gemset
|
|
||||||
gemfile.save
|
|
||||||
raise(LogStash::PluginManager::Error, "Installation aborted")
|
|
||||||
end
|
|
||||||
|
|
||||||
puts("Installation successful")
|
puts("Installation successful")
|
||||||
|
rescue => exception
|
||||||
|
gemfile.restore!
|
||||||
|
report_exception("Installation Aborded", exception)
|
||||||
|
ensure
|
||||||
|
display_bundler_output(output)
|
||||||
end
|
end
|
||||||
|
|
||||||
# copy .gem file into bundler cache directory, log any error to $stderr
|
# Extract the specified local gems in a predefined local path
|
||||||
# @param path [String] the source .gem file to copy
|
# Update the gemfile to use a relative path to this plugin and run
|
||||||
# @return [Boolean] true if successful
|
# Bundler, this will mark the gem not updatable by `bin/plugin update`
|
||||||
def cache_gem_file(path)
|
# This is the most reliable way to make it work in bundler without
|
||||||
dest = ::File.join(LogStash::Environment.logstash_gem_home, "cache")
|
# hacking with `how bundler works`
|
||||||
begin
|
#
|
||||||
FileUtils.cp(path, dest)
|
# Bundler 2.0, will have support for plugins source we could create a .gem source
|
||||||
rescue => e
|
# to support it.
|
||||||
$stderr.puts("Error copying #{plugin} to #{dest}, caused by #{e.class}")
|
def extract_local_gems_plugins
|
||||||
return false
|
plugin_list.collect do |plugin|
|
||||||
|
package, path = LogStash::Bundler.unpack(plugin, LogStash::Environment::LOCAL_GEM_PATH)
|
||||||
|
[package.spec.name, package.spec.version, { :path => relative_path(path) }]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# We cannot install both .gem and normal plugin in one call of `plugin install`
|
||||||
|
def local_gems?
|
||||||
|
return false if plugin_list.empty?
|
||||||
|
|
||||||
|
local_gem = plugin_list.collect { |plugin| ::File.extname(plugin) == ".gem" }.uniq
|
||||||
|
|
||||||
|
if local_gem.size == 1
|
||||||
|
return local_gem.first
|
||||||
|
else
|
||||||
|
signal_usage_error("Mixed source of plugins, you can't mix local `.gem` and remote gems")
|
||||||
end
|
end
|
||||||
true
|
|
||||||
end
|
end
|
||||||
end # class Logstash::PluginManager
|
end # class Logstash::PluginManager
|
||||||
|
|
|
@ -18,10 +18,17 @@ class LogStash::PluginManager::List < Clamp::Command
|
||||||
require 'logstash/environment'
|
require 'logstash/environment'
|
||||||
LogStash::Environment.bundler_setup!
|
LogStash::Environment.bundler_setup!
|
||||||
|
|
||||||
Gem.configuration.verbose = false
|
signal_error("No plugins found") if filtered_specs.empty?
|
||||||
|
|
||||||
gemfile = LogStash::Gemfile.new(File.new(LogStash::Environment::GEMFILE_PATH, "r+")).load
|
filtered_specs.sort_by{|spec| spec.name}.each do |spec|
|
||||||
|
line = "#{spec.name}"
|
||||||
|
line += " (#{spec.version})" if verbose?
|
||||||
|
puts(line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered_specs
|
||||||
|
@filtered_specs ||= begin
|
||||||
# start with all locally installed plugin gems regardless of the Gemfile content
|
# start with all locally installed plugin gems regardless of the Gemfile content
|
||||||
specs = LogStash::PluginManager.find_plugins_gem_specs
|
specs = LogStash::PluginManager.find_plugins_gem_specs
|
||||||
|
|
||||||
|
@ -29,13 +36,6 @@ class LogStash::PluginManager::List < Clamp::Command
|
||||||
specs = specs.select{|spec| gemfile.find(spec.name)} if installed?
|
specs = specs.select{|spec| gemfile.find(spec.name)} if installed?
|
||||||
specs = specs.select{|spec| spec.name =~ /#{plugin}/i} if plugin
|
specs = specs.select{|spec| spec.name =~ /#{plugin}/i} if plugin
|
||||||
specs = specs.select{|spec| spec.metadata['logstash_group'] == group} if group
|
specs = specs.select{|spec| spec.metadata['logstash_group'] == group} if group
|
||||||
|
|
||||||
raise(LogStash::PluginManager::Error, "No plugins found") if specs.empty?
|
|
||||||
|
|
||||||
specs.sort_by{|spec| spec.name}.each do |spec|
|
|
||||||
line = "#{spec.name}"
|
|
||||||
line += " (#{spec.version})" if verbose?
|
|
||||||
puts(line)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end # class Logstash::PluginManager
|
end # class Logstash::PluginManager
|
||||||
|
|
|
@ -5,7 +5,7 @@ require "logstash/pluginmanager/uninstall"
|
||||||
require "logstash/pluginmanager/list"
|
require "logstash/pluginmanager/list"
|
||||||
require "logstash/pluginmanager/update"
|
require "logstash/pluginmanager/update"
|
||||||
require "logstash/pluginmanager/util"
|
require "logstash/pluginmanager/util"
|
||||||
require "logstash/pluginmanager/maven_tools_patch"
|
require "logstash/patches/maven_tools_patch"
|
||||||
require "clamp"
|
require "clamp"
|
||||||
|
|
||||||
module LogStash
|
module LogStash
|
||||||
|
|
|
@ -3,25 +3,23 @@ require "logstash/logging"
|
||||||
require "logstash/errors"
|
require "logstash/errors"
|
||||||
require "logstash/environment"
|
require "logstash/environment"
|
||||||
require "logstash/pluginmanager/util"
|
require "logstash/pluginmanager/util"
|
||||||
|
require "logstash/pluginmanager/base"
|
||||||
require "clamp"
|
require "clamp"
|
||||||
|
|
||||||
require "logstash/gemfile"
|
require "logstash/gemfile"
|
||||||
require "logstash/bundler"
|
require "logstash/bundler"
|
||||||
|
|
||||||
class LogStash::PluginManager::Uninstall < Clamp::Command
|
class LogStash::PluginManager::Uninstall < LogStash::PluginManager::Base
|
||||||
parameter "PLUGIN", "plugin name"
|
parameter "PLUGIN", "plugin name"
|
||||||
|
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
raise(LogStash::PluginManager::Error, "File #{LogStash::Environment::GEMFILE_PATH} does not exist or is not writable, aborting") unless File.writable?(LogStash::Environment::GEMFILE_PATH)
|
LogStash::Environment.bundler_setup!
|
||||||
|
|
||||||
gemfile = LogStash::Gemfile.new(File.new(LogStash::Environment::GEMFILE_PATH, "r+")).load
|
signal_error("File #{LogStash::Environment::GEMFILE_PATH} does not exist or is not writable, aborting") unless File.writable?(LogStash::Environment::GEMFILE_PATH)
|
||||||
# keep a copy of the gemset to revert on error
|
|
||||||
original_gemset = gemfile.gemset.copy
|
|
||||||
|
|
||||||
# make sure this is an installed plugin and present in Gemfile.
|
# make sure this is an installed plugin and present in Gemfile.
|
||||||
# it is not possible to uninstall a dependency not listed in the Gemfile, for example a dependent codec
|
# it is not possible to uninstall a dependency not listed in the Gemfile, for example a dependent codec
|
||||||
raise(LogStash::PluginManager::Error, "This plugin has not been previously installed, aborting") unless LogStash::PluginManager.installed_plugin?(plugin, gemfile)
|
signal_error("This plugin has not been previously installed, aborting") unless LogStash::PluginManager.installed_plugin?(plugin, gemfile)
|
||||||
|
|
||||||
# since we previously did a gemfile.find(plugin) there is no reason why
|
# since we previously did a gemfile.find(plugin) there is no reason why
|
||||||
# remove would not work (return nil) here
|
# remove would not work (return nil) here
|
||||||
|
@ -31,19 +29,15 @@ class LogStash::PluginManager::Uninstall < Clamp::Command
|
||||||
puts("Uninstalling #{plugin}")
|
puts("Uninstalling #{plugin}")
|
||||||
|
|
||||||
# any errors will be logged to $stderr by invoke_bundler!
|
# any errors will be logged to $stderr by invoke_bundler!
|
||||||
output, exception = LogStash::Bundler.invoke_bundler!(:install => true, :clean => true)
|
# output, exception = LogStash::Bundler.invoke_bundler!(:install => true, :clean => true)
|
||||||
|
output = LogStash::Bundler.invoke_bundler!(:install => true)
|
||||||
|
|
||||||
if ENV["DEBUG"]
|
remove_unused_locally_installed_gems!
|
||||||
$stderr.puts(output)
|
|
||||||
$stderr.puts("Error: #{exception.class}, #{exception.message}") if exception
|
|
||||||
end
|
|
||||||
|
|
||||||
if exception
|
|
||||||
# revert to original Gemfile content
|
|
||||||
gemfile.gemset = original_gemset
|
|
||||||
gemfile.save
|
|
||||||
raise(LogStash::PluginManager::Error, "Uninstall aborted")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
rescue => exception
|
||||||
|
gemfile.restore!
|
||||||
|
report_exception("Uninstall aborded", exception)
|
||||||
|
ensure
|
||||||
|
display_bundler_output(output)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,51 +1,80 @@
|
||||||
require 'clamp'
|
require 'clamp'
|
||||||
require 'logstash/namespace'
|
require 'logstash/namespace'
|
||||||
require 'logstash/pluginmanager/util'
|
require 'logstash/pluginmanager/util'
|
||||||
|
require 'logstash/pluginmanager/base'
|
||||||
require 'jar-dependencies'
|
require 'jar-dependencies'
|
||||||
require 'jar_install_post_install_hook'
|
require 'jar_install_post_install_hook'
|
||||||
require 'file-dependencies/gem'
|
require 'file-dependencies/gem'
|
||||||
|
|
||||||
require "logstash/gemfile"
|
require "logstash/gemfile"
|
||||||
require "logstash/bundler"
|
require "logstash/bundler"
|
||||||
|
|
||||||
class LogStash::PluginManager::Update < Clamp::Command
|
class LogStash::PluginManager::Update < LogStash::PluginManager::Base
|
||||||
parameter "[PLUGIN] ...", "Plugin name(s) to upgrade to latest version"
|
parameter "[PLUGIN] ...", "Plugin name(s) to upgrade to latest version"
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
gemfile = LogStash::Gemfile.new(File.new(LogStash::Environment::GEMFILE_PATH, "r+")).load
|
local_gems = gemfile.locally_installed_gems
|
||||||
# keep a copy of the gemset to revert on error
|
|
||||||
original_gemset = gemfile.gemset.copy
|
|
||||||
|
|
||||||
previous_gem_specs_map = find_latest_gem_specs
|
if update_all? || !local_gems.empty?
|
||||||
|
error_plugin_that_use_path!(local_gems)
|
||||||
# create list of plugins to update
|
|
||||||
plugins = unless plugin_list.empty?
|
|
||||||
not_installed = plugin_list.select{|plugin| !previous_gem_specs_map.has_key?(plugin.downcase)}
|
|
||||||
raise(LogStash::PluginManager::Error, "Plugin #{not_installed.join(', ')} is not installed so it cannot be updated, aborting") unless not_installed.empty?
|
|
||||||
plugin_list
|
|
||||||
else
|
else
|
||||||
previous_gem_specs_map.values.map{|spec| spec.name}
|
plugins_with_path = plugin_list & local_gems
|
||||||
|
error_plugin_that_use_path!(plugins_with_path) if plugins_with_path.size > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
update_gems!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def error_plugin_that_use_path!(plugins)
|
||||||
|
signal_error("You have installed plugins from a .gem or you have manually defined a plugin in the Gemfile, we cannot update all or update this specific plugin, problematic plugins: #{plugins.collect(&:name).join(",")}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_all?
|
||||||
|
plugin_list.size == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_gems!
|
||||||
|
# If any error is raise inside the block the Gemfile will restore a backup of the Gemfile
|
||||||
|
previous_gem_specs_map = find_latest_gem_specs
|
||||||
|
|
||||||
# remove any version constrain from the Gemfile so the plugin(s) can be updated to latest version
|
# remove any version constrain from the Gemfile so the plugin(s) can be updated to latest version
|
||||||
# calling update without requiremend will remove any previous requirements
|
# calling update without requiremend will remove any previous requirements
|
||||||
plugins.select{|plugin| gemfile.find(plugin)}.each{|plugin| gemfile.update(plugin)}
|
plugins = plugins_to_update(previous_gem_specs_map)
|
||||||
|
plugins
|
||||||
|
.select { |plugin| gemfile.find(plugin) }
|
||||||
|
.each { |plugin| gemfile.update(plugin) }
|
||||||
|
|
||||||
|
# force a disk sync before running bundler
|
||||||
gemfile.save
|
gemfile.save
|
||||||
|
|
||||||
puts("Updating " + plugins.join(", "))
|
puts("Updating " + plugins.join(", "))
|
||||||
|
|
||||||
# any errors will be logged to $stderr by invoke_bundler!
|
# any errors will be logged to $stderr by invoke_bundler!
|
||||||
output, exception = LogStash::Bundler.invoke_bundler!(:update => plugins)
|
# Bundler cannot update and clean gems in one operation so we have to call the CLI twice.
|
||||||
output, exception = LogStash::Bundler.invoke_bundler!(:clean => true) unless exception
|
output = LogStash::Bundler.invoke_bundler!(:update => plugins)
|
||||||
|
output = LogStash::Bundler.invoke_bundler!(:clean => true)
|
||||||
|
|
||||||
if exception
|
display_updated_plugins(previous_gem_specs_map)
|
||||||
# revert to original Gemfile content
|
rescue => exception
|
||||||
gemfile.gemset = original_gemset
|
gemfile.restore!
|
||||||
gemfile.save
|
report_exception("Updated Aborded", exception)
|
||||||
|
ensure
|
||||||
report_exception(output, exception)
|
display_bundler_output(output)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# create list of plugins to update
|
||||||
|
def plugins_to_update(previous_gem_specs_map)
|
||||||
|
unless plugin_list.empty?
|
||||||
|
not_installed = plugin_list.select{|plugin| !previous_gem_specs_map.has_key?(plugin.downcase)}
|
||||||
|
signal_error("Plugin #{not_installed.join(', ')} is not installed so it cannot be updated, aborting") unless not_installed.empty?
|
||||||
|
plugin_list
|
||||||
|
else
|
||||||
|
previous_gem_specs_map.values.map{|spec| spec.name}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# We compare the before the update and after the update
|
||||||
|
def display_updated_plugins(previous_gem_specs_map)
|
||||||
update_count = 0
|
update_count = 0
|
||||||
find_latest_gem_specs.values.each do |spec|
|
find_latest_gem_specs.values.each do |spec|
|
||||||
name = spec.name.downcase
|
name = spec.name.downcase
|
||||||
|
@ -59,11 +88,10 @@ class LogStash::PluginManager::Update < Clamp::Command
|
||||||
update_count += 1
|
update_count += 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
puts("No plugin updated") if update_count.zero?
|
puts("No plugin updated") if update_count.zero?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# retrieve only the latest spec for all locally installed plugins
|
# retrieve only the latest spec for all locally installed plugins
|
||||||
# @return [Hash] result hash {plugin_name.downcase => plugin_spec}
|
# @return [Hash] result hash {plugin_name.downcase => plugin_spec}
|
||||||
def find_latest_gem_specs
|
def find_latest_gem_specs
|
||||||
|
@ -73,13 +101,4 @@ class LogStash::PluginManager::Update < Clamp::Command
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_exception(output, exception)
|
|
||||||
if ENV["DEBUG"]
|
|
||||||
$stderr.puts(output)
|
|
||||||
$stderr.puts("Error: #{exception.class}, #{exception.message}") if exception
|
|
||||||
end
|
|
||||||
|
|
||||||
raise(LogStash::PluginManager::Error, "Update aborted")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
module LogStash::PluginManager
|
module LogStash::PluginManager
|
||||||
|
|
||||||
# check for valid logstash plugin gem name & version or .gem file, logs errors to $stdout
|
# check for valid logstash plugin gem name & version or .gem file, logs errors to $stdout
|
||||||
# uses Rubygems API and will remotely validated agains the current Gem.sources
|
# uses Rubygems API and will remotely validated agains the current Gem.sources
|
||||||
# @param plugin [String] plugin name or .gem file path
|
# @param plugin [String] plugin name or .gem file path
|
||||||
|
|
|
@ -148,5 +148,4 @@ module LogStash::Util
|
||||||
o
|
o
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end # module LogStash::Util
|
end # module LogStash::Util
|
||||||
|
|
|
@ -134,6 +134,44 @@ describe "logstash Gemfile Manager" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "Locally installed gems" do
|
||||||
|
subject { LogStash::Gemfile.new(StringIO.new(file)).load.locally_installed_gems }
|
||||||
|
|
||||||
|
context "has gems defined with a path" do
|
||||||
|
let(:file) {
|
||||||
|
%Q[
|
||||||
|
source "https://rubygems.org"
|
||||||
|
gemspec :a => "a", "b" => 1
|
||||||
|
gem "foo", "> 1.0", :path => "/tmp/foo"
|
||||||
|
gem "bar", :path => "/tmp/bar"
|
||||||
|
gem "no-fun"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "returns the list of gems" do
|
||||||
|
expect(subject.collect(&:name)).to eq(["foo", "bar"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "no gems defined with a path" do
|
||||||
|
let(:file) {
|
||||||
|
%Q[
|
||||||
|
source "https://rubygems.org"
|
||||||
|
gemspec :a => "a", "b" => 1
|
||||||
|
gem "no-fun"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "return an empty list" do
|
||||||
|
expect(subject.size).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "keep a backup of the original file" do
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "save" do
|
context "save" do
|
||||||
it "should save" do
|
it "should save" do
|
||||||
file = <<-END
|
file = <<-END
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue