lib/pdk/validate/external_command_validator.rb in pdk-2.3.0 vs lib/pdk/validate/external_command_validator.rb in pdk-2.4.0

- old
+ new

@@ -1,208 +1,208 @@ -require 'pdk' - -module PDK - module Validate - # An abstract validator that runs external commands within a Ruby Bundled environment - # e.g. `puppet-lint`, or `puppet validate` - # - # At a a minimum child classes should implment the `name`, `cmd`, `pattern` and `parse_output` methods - # - # An example concrete implementation could look like: - # - # module PDK - # module Validate - # module Ruby - # class RubyRubocopValidator < ExternalCommandValidator - # def name - # 'rubocop' - # end - # - # def cmd - # 'rubocop' - # end - # - # def pattern - # '**/**.rb' - # end - # - # def parse_options(targets) - # ['--format', 'json'] - # end - # - # def parse_output(report, result, _targets) - # ... ruby code ... - # report.add_event( - # line: offense['location']['line'], - # column: offense['location']['column'], - # message: offense['message'], - # severity: offense['corrected'] ? 'corrected' : offense['severity'], - # test: offense['cop_name'], - # state: :failure, - # ) - # end - # end - # end - # end - # end - # - # @see PDK::Validate::InvokableValidator - class ExternalCommandValidator < InvokableValidator - # @return Array[PDK::CLI::Exec::Command] This is a private implementation attribute used for unit testing - # @api private - attr_reader :commands - - # @see PDK::Validate::Validator.spinner - def spinner - # The validator has sub-commands with their own spinners. - nil - end - - # Calculates the text of the spinner based on the target list - # @return [String] - # @abstract - def spinner_text_for_targets(targets); end - - # The name of the command to be run for validation - # @return [String] - # @abstract - def cmd; end - - # Alternate paths which the command (cmd) may exist in. Typically other ruby gem caches, - # or packaged installation bin directories. - # @return [Array[String]] - # @api private - def alternate_bin_paths - [ - PDK::Util::RubyVersion.bin_path, - File.join(PDK::Util::RubyVersion.gem_home, 'bin'), - PDK::Util::RubyVersion.gem_paths_raw.map { |gem_path_raw| File.join(gem_path_raw, 'bin') }, - PDK::Util.package_install? ? File.join(PDK::Util.pdk_package_basedir, 'bin') : nil, - ].flatten.compact - end - - # The full path to the command (cmd) - # Can be overridden in child classes to a non-default path - # @return [String] - # @api private - def cmd_path - return @cmd_path unless @cmd_path.nil? - @cmd_path = File.join(context.root_path, 'bin', cmd) - # Return the path to the command if it exists on disk, or we have a gemfile (i.e. Bundled install) - # The Bundle may be created after the prepare_invoke so if the file doesn't exist, it may not be an error - return @cmd_path if PDK::Util::Filesystem.exist?(@cmd_path) || !PDK::Util::Bundler::BundleHelper.new.gemfile.nil? - # But if there is no Gemfile AND cmd doesn't exist in the default path, we need to go searching... - @cmd_path = alternate_bin_paths.map { |alternate_path| File.join(alternate_path, cmd) } - .find { |path| PDK::Util::Filesystem.exist?(path) } - return @cmd_path unless @cmd_path.nil? - # If we can't find it anywhere, just let the OS find it - @cmd_path = cmd - end - - # An array of command line arguments to pass to the command for validation - # @return Array[String] - # @abstract - def parse_options(_targets) - [] - end - - # Parses the output from the command and appends formatted events to the report. - # This is called for each command, which is a group of targets - # - # @param report [PDK::Report] The report to add events to - # @param result [Hash[Symbol => Object]] The result of validation command process - # @param targets [Array[String]] The targets for this command result - # @api private - # @see PDK::CLI::Exec::Command.execute! - # @abstract - def parse_output(_report, _result, _targets); end - - # Prepares for invokation by parsing targets and creating the needed commands. - # @api private - # @see PDK::Validate::Validator.prepare_invoke! - def prepare_invoke! - return if @prepared - super - - @targets, @skipped, @invalid = parse_targets - @targets = [] if @targets.nil? - - target_groups = if @targets.empty? && allow_empty_targets? - # If we have no targets and we allow empty targets, create an empty target group list - [[]] - elsif invoke_style == :per_target - # If invoking :per_target, split the targets array into an array of - # single element arrays (one per target). - @targets.combination(1).to_a.compact - else - # Else we're invoking :once, wrap the targets array in another array. This is so we - # can loop through the invokes with the same logic, regardless of which invoke style - # is needed. - @targets.each_slice(1000).to_a.compact - end - - # Register all of the commands for all of the targets - @commands = [] - target_groups.each do |invokation_targets| - next if invokation_targets.empty? && !allow_empty_targets? - cmd_argv = parse_options(invokation_targets).unshift(cmd_path).compact - cmd_argv.unshift(File.join(PDK::Util::RubyVersion.bin_path, 'ruby.exe'), '-W0') if Gem.win_platform? - - command = PDK::CLI::Exec::Command.new(*cmd_argv).tap do |c| - c.context = :module - c.environment = { 'PUPPET_GEM_VERSION' => options[:puppet] } if options[:puppet] - - if spinners_enabled? - parent_validator = options[:parent_validator] - if parent_validator.nil? || parent_validator.spinner.nil? || !parent_validator.spinner.is_a?(TTY::Spinner::Multi) - c.add_spinner(spinner_text_for_targets(invokation_targets)) - else - spinner = TTY::Spinner.new("[:spinner] #{spinner_text_for_targets(invokation_targets)}", PDK::CLI::Util.spinner_opts_for_platform) - parent_validator.spinner.register(spinner) - c.register_spinner(spinner, PDK::CLI::Util.spinner_opts_for_platform) - end - end - end - - @commands << { command: command, invokation_targets: invokation_targets } - end - nil - end - - # Invokes the prepared commands as an ExecGroup - # @see PDK::Validate::Validator.invoke - def invoke(report) - prepare_invoke! - - process_skipped(report, @skipped) - process_invalid(report, @invalid) - - # Nothing to execute so return success - return 0 if @commands.empty? - - # If there's no Gemfile, then we can't ensure the binstubs are correct - PDK::Util::Bundler.ensure_binstubs!(cmd) unless PDK::Util::Bundler::BundleHelper.new.gemfile.nil? - - exec_group = PDK::CLI::ExecGroup.create(name, { parallel: false }, options) - - # Register all of the commands for all of the targets - @commands.each do |item| - command = item[:command] - invokation_targets = item[:invokation_targets] - - exec_group.register do - result = command.execute! - begin - parse_output(report, result, invokation_targets.compact) - rescue PDK::Validate::ParseOutputError => e - $stderr.puts e.message - end - result[:exit_code] - end - end - - # Now execute and get the return code - exec_group.exit_code - end - end - end -end +require 'pdk' + +module PDK + module Validate + # An abstract validator that runs external commands within a Ruby Bundled environment + # e.g. `puppet-lint`, or `puppet validate` + # + # At a a minimum child classes should implment the `name`, `cmd`, `pattern` and `parse_output` methods + # + # An example concrete implementation could look like: + # + # module PDK + # module Validate + # module Ruby + # class RubyRubocopValidator < ExternalCommandValidator + # def name + # 'rubocop' + # end + # + # def cmd + # 'rubocop' + # end + # + # def pattern + # '**/**.rb' + # end + # + # def parse_options(targets) + # ['--format', 'json'] + # end + # + # def parse_output(report, result, _targets) + # ... ruby code ... + # report.add_event( + # line: offense['location']['line'], + # column: offense['location']['column'], + # message: offense['message'], + # severity: offense['corrected'] ? 'corrected' : offense['severity'], + # test: offense['cop_name'], + # state: :failure, + # ) + # end + # end + # end + # end + # end + # + # @see PDK::Validate::InvokableValidator + class ExternalCommandValidator < InvokableValidator + # @return Array[PDK::CLI::Exec::Command] This is a private implementation attribute used for unit testing + # @api private + attr_reader :commands + + # @see PDK::Validate::Validator.spinner + def spinner + # The validator has sub-commands with their own spinners. + nil + end + + # Calculates the text of the spinner based on the target list + # @return [String] + # @abstract + def spinner_text_for_targets(targets); end + + # The name of the command to be run for validation + # @return [String] + # @abstract + def cmd; end + + # Alternate paths which the command (cmd) may exist in. Typically other ruby gem caches, + # or packaged installation bin directories. + # @return [Array[String]] + # @api private + def alternate_bin_paths + [ + PDK::Util::RubyVersion.bin_path, + File.join(PDK::Util::RubyVersion.gem_home, 'bin'), + PDK::Util::RubyVersion.gem_paths_raw.map { |gem_path_raw| File.join(gem_path_raw, 'bin') }, + PDK::Util.package_install? ? File.join(PDK::Util.pdk_package_basedir, 'bin') : nil, + ].flatten.compact + end + + # The full path to the command (cmd) + # Can be overridden in child classes to a non-default path + # @return [String] + # @api private + def cmd_path + return @cmd_path unless @cmd_path.nil? + @cmd_path = File.join(context.root_path, 'bin', cmd) + # Return the path to the command if it exists on disk, or we have a gemfile (i.e. Bundled install) + # The Bundle may be created after the prepare_invoke so if the file doesn't exist, it may not be an error + return @cmd_path if PDK::Util::Filesystem.exist?(@cmd_path) || !PDK::Util::Bundler::BundleHelper.new.gemfile.nil? + # But if there is no Gemfile AND cmd doesn't exist in the default path, we need to go searching... + @cmd_path = alternate_bin_paths.map { |alternate_path| File.join(alternate_path, cmd) } + .find { |path| PDK::Util::Filesystem.exist?(path) } + return @cmd_path unless @cmd_path.nil? + # If we can't find it anywhere, just let the OS find it + @cmd_path = cmd + end + + # An array of command line arguments to pass to the command for validation + # @return Array[String] + # @abstract + def parse_options(_targets) + [] + end + + # Parses the output from the command and appends formatted events to the report. + # This is called for each command, which is a group of targets + # + # @param report [PDK::Report] The report to add events to + # @param result [Hash[Symbol => Object]] The result of validation command process + # @param targets [Array[String]] The targets for this command result + # @api private + # @see PDK::CLI::Exec::Command.execute! + # @abstract + def parse_output(_report, _result, _targets); end + + # Prepares for invokation by parsing targets and creating the needed commands. + # @api private + # @see PDK::Validate::Validator.prepare_invoke! + def prepare_invoke! + return if @prepared + super + + @targets, @skipped, @invalid = parse_targets + @targets = [] if @targets.nil? + + target_groups = if @targets.empty? && allow_empty_targets? + # If we have no targets and we allow empty targets, create an empty target group list + [[]] + elsif invoke_style == :per_target + # If invoking :per_target, split the targets array into an array of + # single element arrays (one per target). + @targets.combination(1).to_a.compact + else + # Else we're invoking :once, wrap the targets array in another array. This is so we + # can loop through the invokes with the same logic, regardless of which invoke style + # is needed. + @targets.each_slice(1000).to_a.compact + end + + # Register all of the commands for all of the targets + @commands = [] + target_groups.each do |invokation_targets| + next if invokation_targets.empty? && !allow_empty_targets? + cmd_argv = parse_options(invokation_targets).unshift(cmd_path).compact + cmd_argv.unshift(File.join(PDK::Util::RubyVersion.bin_path, 'ruby.exe'), '-W0') if Gem.win_platform? + + command = PDK::CLI::Exec::Command.new(*cmd_argv).tap do |c| + c.context = :module + c.environment = { 'PUPPET_GEM_VERSION' => options[:puppet] } if options[:puppet] + + if spinners_enabled? + parent_validator = options[:parent_validator] + if parent_validator.nil? || parent_validator.spinner.nil? || !parent_validator.spinner.is_a?(TTY::Spinner::Multi) + c.add_spinner(spinner_text_for_targets(invokation_targets)) + else + spinner = TTY::Spinner.new("[:spinner] #{spinner_text_for_targets(invokation_targets)}", PDK::CLI::Util.spinner_opts_for_platform) + parent_validator.spinner.register(spinner) + c.register_spinner(spinner, PDK::CLI::Util.spinner_opts_for_platform) + end + end + end + + @commands << { command: command, invokation_targets: invokation_targets } + end + nil + end + + # Invokes the prepared commands as an ExecGroup + # @see PDK::Validate::Validator.invoke + def invoke(report) + prepare_invoke! + + process_skipped(report, @skipped) + process_invalid(report, @invalid) + + # Nothing to execute so return success + return 0 if @commands.empty? + + # If there's no Gemfile, then we can't ensure the binstubs are correct + PDK::Util::Bundler.ensure_binstubs!(cmd) unless PDK::Util::Bundler::BundleHelper.new.gemfile.nil? + + exec_group = PDK::CLI::ExecGroup.create(name, { parallel: false }, options) + + # Register all of the commands for all of the targets + @commands.each do |item| + command = item[:command] + invokation_targets = item[:invokation_targets] + + exec_group.register do + result = command.execute! + begin + parse_output(report, result, invokation_targets.compact) + rescue PDK::Validate::ParseOutputError => e + $stderr.puts e.message + end + result[:exit_code] + end + end + + # Now execute and get the return code + exec_group.exit_code + end + end + end +end