logstash/ci/test_plugins.rb

379 lines
16 KiB
Ruby

# encoding: utf-8
# Test the
# - build (rspec unit testing),
# - packaging (gem build)
# - and deploy (bin/logstash-plugin install)
# of a plugins inside the current Logstash, using its JRuby
# Usage example:
# bin/ruby ci/test_supported_plugins.rb -p logstash-integration-jdbc
# bin/ruby ci/test_supported_plugins.rb -t tier1 -k input,codec,integration
# bin/ruby ci/test_supported_plugins.rb -t tier1 -k input,codec,integration --split 1/3
#
# The script uses OS's temp folder unless the environment variable LOGSTASH_PLUGINS_TMP is specified.
# The path of such variable should be absolute.
require "open3"
require "set"
require 'optparse'
require 'rake'
ENV['LOGSTASH_PATH'] = File.expand_path('..', __dir__)
ENV['LOGSTASH_SOURCE'] = '1'
logstash_plugin_cli = ENV['LOGSTASH_PATH'] + "/bin/logstash-plugin"
# it has to be out of logstash local close else plugins' Gradle script
# would interfere with Logstash's one
base_folder = ENV['LOGSTASH_PLUGINS_TMP'] || (require 'tmpdir'; Dir.tmpdir)
puts "Using #{base_folder} as temporary clone folder"
plugins_folder = File.join(base_folder, "plugin_clones")
unless File.directory?(plugins_folder)
Dir.mkdir(plugins_folder)
end
class Plugin
attr_reader :plugins_folder, :plugin_name, :plugin_base_folder, :plugin_repository
# params:
# plugin_definition
def initialize(plugins_folder, plugin_definition)
@plugin_name = plugin_definition.name
@plugins_folder = plugins_folder
@plugin_base_folder = "#{plugins_folder}/#{plugin_name}"
plugin_org = plugin_definition.organization || 'logstash-plugins'
@plugin_repository = "git@github.com:#{plugin_org}/#{plugin_name}.git"
end
def git_retrieve
if File.directory?(plugin_name)
puts "test plugins(git_retrieve)>> #{plugin_name} already cloned locally, proceed with updating... (at #{Time.new})"
Dir.chdir(plugin_name) do
system("git restore -- .")
puts "Cleaning following files"
system("git clean -n ")
puts "Proceed with cleaning"
system("git clean -Xf")
end
puts "test plugins(git_retrieve)>> #{plugin_name} updated"
return
end
puts "test plugins(git_retrieve)>> #{plugin_name} local clone doesn't exist, cloning... (at #{Time.new})"
unless system("git clone #{plugin_repository}")
puts "Can't clone #{plugin_repository}"
exit 1
end
puts "#{plugin_name} cloned"
end
# return true if successed
def execute_rspec
# setup JRUBY_OPTS env var to open access to expected modules
# the content is the same of the file https://github.com/logstash-plugins/.ci/blob/main/dockerjdk17.env
ENV['JRUBY_OPTS'] = "-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -J--add-opens=java.base/java.security=ALL-UNNAMED -J--add-opens=java.base/java.io=ALL-UNNAMED -J--add-opens=java.base/java.nio.channels=ALL-UNNAMED -J--add-opens=java.base/sun.nio.ch=ALL-UNNAMED -J--add-opens=java.management/sun.management=ALL-UNNAMED -Xregexp.interruptible=true -Xcompile.invokedynamic=true -Xjit.threshold=0 -J-XX:+PrintCommandLineFlags -v -W1"
ENV['USER'] = "logstash"
puts "test plugins(execute_rspec)>> bundle install"
return false unless system("#{ENV['LOGSTASH_PATH']}/bin/ruby -S bundle install")
unit_test_folders = Dir.glob('spec/**/*')
.select {|f| File.directory? f}
.select{|f| not f.include?('integration')}
.select{|f| not f.include?('benchmark')}
.join(" ")
puts "test plugins(execute_rspec)>> rake vendor (at #{Time.new})"
return false unless system("#{ENV['LOGSTASH_PATH']}/bin/ruby -S bundle exec rake vendor")
puts "test plugins(execute_rspec)>> exec rspec"
rspec_command = "#{ENV['LOGSTASH_PATH']}/bin/ruby -S bundle exec rspec #{unit_test_folders} --tag ~integration --tag ~secure_integration"
puts "\t\t executing: #{rspec_command}\n from #{Dir.pwd}"
stdout, stderr, status = Open3.capture3(rspec_command)
if status != 0
puts "Error executing rspec"
puts "Stderr ----------------------"
puts stderr
puts "Stdout ----------------------"
puts stdout
puts "OEFStdout--------------------"
return false
end
return true
end
# Return nil in case of error or the file name of the generated gem file
def build_gem
gem_command = "#{ENV['LOGSTASH_PATH']}/bin/ruby -S gem build #{plugin_name}.gemspec"
system(gem_command)
gem_name = Dir.glob("#{plugin_name}*.gem").first
unless gem_name
puts "**error** gem not generated for plugin #{plugin_name}"
return nil
end
gem_file = File.join(plugin_base_folder, gem_name)
puts "gem_file generated: #{gem_file}"
gem_file
end
def install_gem(gem_file)
logstash_plugin_cli = ENV['LOGSTASH_PATH'] + "/bin/logstash-plugin"
stdout, stderr, status = Open3.capture3("#{logstash_plugin_cli} install #{gem_file}")
reg_exp = /Installing .*\nInstallation successful$/
if status != 0 && !reg_exp.match(stdout)
puts "Failed to install plugins:\n stdout:\n #{stdout} \nstderr:\n #{stderr}"
return false
else
puts "plugin #{plugin_name} successfully installed"
#system("#{logstash_plugin_cli} remove #{gem_name}")
return true
end
end
end
# reason could be a symbol, describing the phase that broke:
# :unit_test, :gem_build, :gem_install
FailureDetail = Struct.new(:plugin_name, :reason)
# contains set of FailureDetail
failed_plugins = [].to_set
# Models plugin's metadata, organization is optional, if nil then it consider logstash-plugins as default.
PluginDefinition = Struct.new(:name, :support, :type, :organization) do
def initialize(name, support, type, organization = "logstash-plugins")
super(name, support, type, organization)
end
end
PLUGIN_DEFINITIONS = [
PluginDefinition.new('logstash-input-azure_event_hubs', :tier1, :input),
PluginDefinition.new('logstash-input-beats', :tier1, :input),
PluginDefinition.new('logstash-input-elasticsearch', :tier1, :input),
PluginDefinition.new('logstash-input-file', :tier1, :input),
PluginDefinition.new('logstash-input-generator', :tier1, :input),
PluginDefinition.new('logstash-input-heartbeat', :tier1, :input),
PluginDefinition.new('logstash-input-http', :tier1, :input),
PluginDefinition.new('logstash-input-http_poller', :tier1, :input),
PluginDefinition.new('logstash-input-redis', :tier1, :input),
PluginDefinition.new('logstash-input-stdin', :tier1, :input),
PluginDefinition.new('logstash-input-syslog', :tier1, :input),
PluginDefinition.new('logstash-input-udp', :tier1, :input),
PluginDefinition.new('logstash-codec-avro', :tier1, :codec),
PluginDefinition.new('logstash-codec-cef', :tier1, :codec),
PluginDefinition.new('logstash-codec-es_bulk', :tier1, :codec),
PluginDefinition.new('logstash-codec-json', :tier1, :codec),
PluginDefinition.new('logstash-codec-json_lines', :tier1, :codec),
PluginDefinition.new('logstash-codec-line', :tier1, :codec),
PluginDefinition.new('logstash-codec-multiline', :tier1, :codec),
PluginDefinition.new('logstash-codec-plain', :tier1, :codec),
PluginDefinition.new('logstash-codec-rubydebug', :tier1, :codec),
PluginDefinition.new('logstash-filter-cidr', :tier1, :filter),
PluginDefinition.new('logstash-filter-clone', :tier1, :filter),
PluginDefinition.new('logstash-filter-csv', :tier1, :filter),
PluginDefinition.new('logstash-filter-date', :tier1, :filter),
PluginDefinition.new('logstash-filter-dissect', :tier1, :filter),
PluginDefinition.new('logstash-filter-dns', :tier1, :filter),
PluginDefinition.new('logstash-filter-drop', :tier1, :filter),
PluginDefinition.new('logstash-filter-elasticsearch', :tier1, :filter),
PluginDefinition.new('logstash-filter-fingerprint', :tier1, :filter),
PluginDefinition.new('logstash-filter-geoip', :tier1, :filter),
PluginDefinition.new('logstash-filter-grok', :tier1, :filter),
PluginDefinition.new('logstash-filter-http', :tier1, :filter),
PluginDefinition.new('logstash-filter-json', :tier1, :filter),
PluginDefinition.new('logstash-filter-kv', :tier1, :filter),
PluginDefinition.new('logstash-filter-memcached', :tier1, :filter),
PluginDefinition.new('logstash-filter-mutate', :tier1, :filter),
PluginDefinition.new('logstash-filter-prune', :tier1, :filter),
PluginDefinition.new('logstash-filter-ruby', :tier1, :filter),
PluginDefinition.new('logstash-filter-sleep', :tier1, :filter),
PluginDefinition.new('logstash-filter-split', :tier1, :filter),
PluginDefinition.new('logstash-filter-syslog_pri', :tier1, :filter),
PluginDefinition.new('logstash-filter-translate', :tier1, :filter),
PluginDefinition.new('logstash-filter-truncate', :tier1, :filter),
PluginDefinition.new('logstash-filter-urldecode', :tier1, :filter),
PluginDefinition.new('logstash-filter-useragent', :tier1, :filter),
PluginDefinition.new('logstash-filter-uuid', :tier1, :filter),
PluginDefinition.new('logstash-filter-xml', :tier1, :filter),
PluginDefinition.new('logstash-filter-elastic_integration', :tier1, :filter, 'elastic'),
PluginDefinition.new('logstash-output-elasticsearch', :tier1, :output),
PluginDefinition.new('logstash-output-email', :tier1, :output),
PluginDefinition.new('logstash-output-file', :tier1, :output),
PluginDefinition.new('logstash-output-http', :tier1, :output),
PluginDefinition.new('logstash-output-redis', :tier1, :output),
PluginDefinition.new('logstash-output-stdout', :tier1, :output),
PluginDefinition.new('logstash-output-tcp', :tier1, :output),
PluginDefinition.new('logstash-output-udp', :tier1, :output),
PluginDefinition.new('logstash-integration-jdbc', :tier1, :integration),
PluginDefinition.new('logstash-integration-kafka', :tier1, :integration),
PluginDefinition.new('logstash-integration-rabbitmq', :tier1, :integration),
PluginDefinition.new('logstash-integration-elastic_enterprise_search', :tier1, :integration),
PluginDefinition.new('logstash-integration-aws', :tier1, :integration),
# tier2
# Removed because of https://github.com/logstash-plugins/logstash-input-couchdb_changes/issues/51
#PluginDefinition.new('logstash-input-couchdb_changes', :tier2, :input),
PluginDefinition.new('logstash-input-gelf', :tier2, :input),
PluginDefinition.new('logstash-input-graphite', :tier2, :input),
PluginDefinition.new('logstash-input-jms', :tier2, :input),
PluginDefinition.new('logstash-input-snmp', :tier2, :input),
PluginDefinition.new('logstash-input-sqs', :tier2, :input),
PluginDefinition.new('logstash-input-twitter', :tier2, :input),
PluginDefinition.new('logstash-codec-collectd', :tier2, :codec),
PluginDefinition.new('logstash-codec-dots', :tier2, :codec),
PluginDefinition.new('logstash-codec-fluent', :tier2, :codec),
PluginDefinition.new('logstash-codec-graphite', :tier2, :codec),
PluginDefinition.new('logstash-codec-msgpack', :tier2, :codec),
PluginDefinition.new('logstash-codec-netflow', :tier2, :codec),
PluginDefinition.new('logstash-filter-aggregate', :tier2, :filter),
PluginDefinition.new('logstash-filter-de_dot', :tier2, :filter),
PluginDefinition.new('logstash-filter-throttle', :tier2, :filter),
PluginDefinition.new('logstash-output-csv', :tier2, :output),
PluginDefinition.new('logstash-output-graphite', :tier2, :output),
# unsupported
PluginDefinition.new('logstash-input-rss', :unsupported, :input),
]
def validate_options!(options)
raise "plugin and tiers or kinds can't be specified at the same time" if (options[:tiers] || options[:kinds]) && options[:plugin]
options[:tiers].map! { |v| v.to_sym } if options[:tiers]
options[:kinds].map! { |v| v.to_sym } if options[:kinds]
raise "Invalid tier name expected tier1, tier2 or unsupported" if options[:tiers] && !(options[:tiers] - [:tier1, :tier2, :unsupported]).empty?
raise "Invalid kind name expected input, codec, filter, output, integration" if options[:kinds] && !(options[:kinds] - [:input, :codec, :filter, :output, :integration]).empty?
end
# @param tiers array of labels
# @param kinds array of labels
def select_by_tiers_and_kinds(tiers, kinds)
PLUGIN_DEFINITIONS.select { |plugin| tiers.include?(plugin.support) }
.select { |plugin| kinds.include?(plugin.type) }
end
# Return of PluginDefinitions given a list of plugin names
def list_plugins_definitions(plugins)
PLUGIN_DEFINITIONS.select { |plugin| plugins.include?(plugin.name) }
end
def select_plugins_by_opts(options)
if options[:plugins]
return list_plugins_definitions(options[:plugins])
end
selected_tiers = options.fetch(:tiers, [:tier1, :tier2, :unsupported])
selected_kinds = options.fetch(:kinds, [:input, :codec, :filter, :output, :integration])
selected_partition = options.fetch(:split, "1/1")
select_plugins = select_by_tiers_and_kinds(selected_tiers, selected_kinds)
return split_by_partition(select_plugins, selected_partition)
end
# Return the partition corresponding to the definition of the given list
def split_by_partition(list, partition_definition)
slice = partition_definition.split('/')[0].to_i
num_slices = partition_definition.split('/')[1].to_i
slice_size = list.size / num_slices
slice_start = (slice - 1) * slice_size
slice_end = slice == num_slices ? -1 : slice * slice_size - 1
return list[slice_start..slice_end]
end
def snapshot_logstash_artifacts!
stdout, stderr, status = Open3.capture3("git add --force -- Gemfile Gemfile.lock vendor/bundle")
if status != 0
puts "Error snapshotting Logstash on path: #{Dir.pwd}"
puts stderr
exit 1
end
end
def cleanup_logstash_snapshot
system("git restore --staged -- Gemfile Gemfile.lock vendor/bundle")
end
def restore_logstash_from_snapshot
system("git restore -- Gemfile Gemfile.lock vendor/bundle")
system("git clean -Xf -- Gemfile Gemfile.lock vendor/bundle")
end
def setup_logstash_for_development
system("./gradlew installDevelopmentGems")
end
option_parser = OptionParser.new do |opts|
opts.on '-t', '--tiers tier1,tier2,unsupported', Array, 'Use to select which tier to test. If no provided mean "all"'
opts.on '-k', '--kinds input,codec,filter,output', Array, 'Use to select which kind of plugin to test. If no provided mean "all"'
opts.on '-p', '--plugins plugin1,plugin2', Array, 'Use to select a specific set of plugins, conflict with either -t and -k'
opts.on '-sPARTITION', '--split=PARTITION', String, 'Use to partition the set of plugins to execute, for example -s 1/3 means "execute the first third of the selected plugin list"'
opts.on '-h', '--halt', 'Halt immediately on first error'
end
options = {}
option_parser.parse!(into: options)
validate_options!(options)
plugins = select_plugins_by_opts(options)
puts "test plugins(start)>> start at #{Time.new}"
setup_logstash_for_development
# save to local git for test isolation
snapshot_logstash_artifacts!
plugins.each do |plugin_definition|
restore_logstash_from_snapshot
plugin_name = plugin_definition.name
status = Dir.chdir(plugins_folder) do
plugin = Plugin.new(plugins_folder, plugin_definition)
plugin.git_retrieve
status = Dir.chdir(plugin_name) do
unless plugin.execute_rspec
failed_plugins << FailureDetail.new(plugin_name, :unit_test)
break :error
end
# build the gem and install into Logstash
gem_file = plugin.build_gem
unless gem_file
#puts "inserted into failed, because no gem file exists"
failed_plugins << FailureDetail.new(plugin_name, :gem_build)
break :error
end
# install the plugin
unless plugin.install_gem(gem_file)
#puts "inserted into failed, because the gem can't be installed"
failed_plugins << FailureDetail.new(plugin_name, :gem_install)
break :error
end
:success
end
status
end
# any of the verification subtask terminated with error
if status == :error
# break looping on plugins if immediate halt
break if options[:halt]
end
end
# restore original git status to avoid to accidentally commit build artifacts
cleanup_logstash_snapshot
if !failed_plugins.empty?
puts "########################################"
puts " Failed plugins:"
puts "----------------------------------------"
failed_plugins.each {|failure| puts "- #{failure}"}
puts "########################################"
exit 1
else
puts "NO ERROR ON PLUGINS!"
end