lib/choria/colt/cli.rb in choria-colt-0.2.0 vs lib/choria/colt/cli.rb in choria-colt-0.3.0

- old
+ new

@@ -1,42 +1,52 @@ require 'choria/colt' +require 'choria/colt/cli/formatter' require 'choria/colt/cli/thor' require 'json' -require 'logger' +require 'tty/logger' module Choria class Colt class CLI < Thor class Tasks < Thor + class_option :log_level, + desc: 'Set log level for CLI', + default: 'info' # BOLT: desc 'run <task name> [parameters] {--targets TARGETS | --query QUERY | --rerun FILTER} [options]', 'Run a Bolt task' desc 'run <task name> [parameters] --targets TARGETS [options]', 'Run a Bolt task' long_desc <<~DESC Run a task on the specified targets. Parameters take the form parameter=value. DESC option :targets, aliases: ['--target', '-t'], - desc: 'Identifies the targets of the command.', - required: true - def run(*args) + desc: 'Identifies the targets of the command.' + option :targets_with_classes, + aliases: ['--targets-with-class', '-C'], + desc: 'Select the targets which have the specified Puppet classes.' + def run(*args) # rubocop:disable Metrics/AbcSize input = extract_task_parameters_from_args(args) raise Thor::Error, 'Task name is required' if args.empty? raise Thor::Error, "Too many arguments: #{args}" unless args.count == 1 + raise Thor::Error, 'Flag --targets or --targets-with-class is required' if options['targets'].nil? && options['targets_with_classes'].nil? + task_name = args.shift - targets = options['targets'].split ',' + targets = options['targets']&.split(',') targets = nil if options['targets'] == 'all' - results = colt.run_bolt_task task_name, input: input, targets: targets + targets_with_classes = options['targets_with_classes']&.split(',') - File.write 'last_run.json', JSON.pretty_generate(results) + results = colt.run_bolt_task task_name, input: input, targets: targets, targets_with_classes: targets_with_classes do |result| + $stdout.puts formatter.process_result(result) + end - show_results(results) + File.write 'last_run.json', JSON.pretty_generate(results) rescue Choria::Orchestrator::Error => e raise Thor::Error, "#{e.class}: #{e}" end desc 'show [task name] [options]', 'Show available tasks and task documentation' @@ -66,60 +76,86 @@ else tasks_names.each { |task_name| show_task_details(task_name, tasks) } end end - no_commands do + no_commands do # rubocop:disable Metrics/BlockLength def colt - @colt ||= Choria::Colt.new logger: Logger.new($stdout) + @colt ||= Choria::Colt.new logger: logger end + def logger + @logger ||= TTY::Logger.new do |config| + config.handlers = [ + [:console, { output: $stderr, level: options['log_level'].to_sym }], + [:stream, { output: File.open('colt-debug.log', 'a'), level: :debug }], + ] + config.metadata = %i[date time] + end + end + + def formatter + @formatter ||= Formatter.new(colored: $stdout.tty?) + end + def extract_task_parameters_from_args(args) parameters = args.grep(/^\w+=/) args.reject! { |arg| arg =~ /^\w+=/ } parameters.map do |parameter| - key, value = parameter.split('=') + key, value = parameter.split('=', 2) + + # TODO: Convert to boolean only if the expected type of parameter is boolean + # TODO: Support String to integer convertion + # TODO: Support @notation from parameter and/or whole input + value = true if value == 'true' + value = false if value == 'false' + [key, value] end.to_h end def show_tasks_summary(tasks) tasks.reject! { |_task, metadata| metadata['metadata']['private'] } puts <<~OUTPUT - Tasks + #{pastel.title 'Tasks'} #{tasks.map { |task, metadata| "#{task}#{' ' * (60 - task.size)}#{metadata['metadata']['description']}" }.join("\n").gsub(/^/, ' ')} OUTPUT end def show_task_details(task_name, tasks) metadata = tasks[task_name] puts <<~OUTPUT - Task: '#{task_name}' + #{pastel.title "Task: #{task_name}"} #{metadata['metadata']['description']} - Parameters: - #{JSON.pretty_generate(metadata['metadata']['parameters']).gsub(/^/, ' ')} + #{pastel.title 'Parameters'} + #{format_task_parameters(metadata['metadata']['parameters']).gsub(/^/, ' ')} OUTPUT end - def show_results(results) - results.each { |result| show_result(result) } + def format_task_parameters(parameters) + parameters.map do |parameter, metadata| + output = <<~OUTPUT + #{pastel.parameter(parameter)} #{pastel.parameter_type metadata['type']} + #{metadata['description']} + OUTPUT + output += " Default: #{metadata['default']}" unless metadata['default'].nil? + output + end.join "\n" end - def show_result(result) - return show_generic_output(result) unless result.dig(:result, '_output').nil? || (result.dig(:result, 'exit_code') != 0) - - $stdout.puts JSON.pretty_generate(result) + def pastel + @pastel ||= _pastel end - def show_generic_output(result) - target = result[:sender] - - output = result.dig(:result, '_output') - $stdout.puts "'#{target}':" - output.split("\n").each { |line| $stdout.puts(" #{line}") } + def _pastel + pastel = Pastel.new(enabled: $stdout.tty?) + pastel.alias_color(:title, :cyan) + pastel.alias_color(:parameter, :yellow) + pastel.alias_color(:parameter_type, :bright_white) + pastel end end end desc 'tasks', 'Show and run Bolt tasks.'