# frozen_string_literal: true module Terradactyl # rubocop:disable Metrics/ClassLength class CLI < Thor include Common def self.exit_on_failure? true end def initialize(*args) # Hook ensures abort on stack errors at_exit { abort if Stacks.error? } super end no_commands do # Monkey-patch Thor internal method to break out of nested calls def invoke_command(command, *args) catch(:error) { super } end def validate_smartplan(stacks) if stacks.empty? print_message 'No Stacks Modified ...' print_line 'Did you forget to `git add` your selected changes?' end stacks end def validate_planpr(stacks) if stacks.empty? print_message 'No Stacks Modified ...' print_line 'Skipping plan ...' end stacks end def generate_report(report) data_file = "#{config.base_folder}.audit.json" print_warning "Writing Report: #{data_file} ..." report[:error] = Stacks.error.map { |s| "#{config.base_folder}/#{s.name}" }.sort File.write data_file, JSON.pretty_generate(report) print_ok 'Done!' end end ################################################################# # GENERIC TASKS # * These tasks are used regularly against stacks, by name. ################################################################# desc 'defaults', 'Print the compiled configuration' def defaults puts config.to_h.to_yaml end desc 'stacks', 'List the stacks' def stacks print_ok 'Stacks:' Stacks.load.each do |name| print_dot name.to_s end end desc 'version', 'Print version' def version print_message format('version: %s', semver: Terradactyl::VERSION) end ################################################################# # SPECIAL TASKS # * These tasks are related to Git state and PR planning ops. # * Some are useful only in pipelines. These are hidden. ################################################################# desc 'planpr', 'Plan stacks against origin/HEAD (used for PRs)', hide: true def planpr print_header 'SmartPlanning PR ...' stacks = Stacks.load(filter: StacksPlanFilterGitDiffOriginBranch.new) validate_planpr(stacks).each do |name| clean(name) init(name) plan(name) @stack = nil end end desc 'smartplan', 'Plan any stacks that differ from Git HEAD' def smartplan print_header 'SmartPlanning Stacks ...' stacks = Stacks.load(filter: StacksPlanFilterGitDiffHead.new) validate_smartplan(stacks).each do |name| clean(name) init(name) plan(name) @stack = nil end end desc 'smartapply', 'Apply any stacks that contain plan files', hide: true def smartapply print_header 'SmartApplying Stacks ...' stacks = Stacks.load(filter: StacksApplyFilterPrePlanned.new) print_warning 'No stacks contain plan files ...' unless stacks.any? stacks.each do |name| apply(name) @stack = nil end print_message "Total Stacks Modified: #{stacks.size}" end desc 'smartrefresh', 'Refresh any stacks that contain plan files', hide: true def smartrefresh print_header 'SmartRefreshing Stacks ...' stacks = Stacks.load(filter: StacksApplyFilterPrePlanned.new) print_warning 'No stacks contain plan files ...' unless stacks.any? stacks.each do |name| refresh(name) @stack = nil end print_message "Total Stacks Refreshed: #{stacks.size}" end ################################################################# # META-STACK TASKS # * These tasks are used regularly against groups of stacks, but # the `quickplan` task is an exception to this rule. ################################################################# desc 'quickplan NAME', 'Clean, init and plan a stack, by name' def quickplan(name) print_header "Quick planning #{name} ..." clean(name) init(name) plan(name) end desc 'clean-all', 'Clean all stacks' def clean_all print_header 'Cleaning ALL Stacks ...' Stacks.load.each do |name| clean(name) @stack = nil end end desc 'plan-all', 'Plan all stacks' def plan_all print_header 'Planning ALL Stacks ...' Stacks.load.each do |name| catch(:error) do clean(name) init(name) plan(name) end @stack = nil end end desc 'audit-all', 'Audit all stacks' options report: :optional method_option :report, type: :boolean # rubocop:disable Metrics/AbcSize def audit_all report = { start: Time.now.to_json } print_header 'Auditing ALL Stacks ...' Stacks.load.each do |name| catch(:error) do clean(name) init(name) audit(name) end @stack = nil end report[:finish] = Time.now.to_json if options[:report] print_header 'Audit Report ...' generate_report(report) end end # rubocop:enable Metrics/AbcSize desc 'validate-all', 'Validate all stacks' def validate_all print_header 'Validating ALL Stacks ...' Stacks.load.each do |name| catch(:error) do clean(name) init(name) validate(name) end @stack = nil end end ################################################################# # TARGETED STACK TASKS # * These tasks are used regularly against stacks, by name. ################################################################# desc 'lint NAME', 'Lint an individual stack, by name' def lint(name) @stack ||= Stack.new(name) print_ok "Linting: #{@stack.name}" if @stack.lint.zero? print_ok "Formatting OK: #{@stack.name}" else Stacks.error!(@stack) print_warning "Bad Formatting: #{@stack.name}" end end desc 'fmt NAME', 'Format an individual stack, by name' def fmt(name) @stack ||= Stack.new(name) print_warning "Formatting: #{@stack.name}" if @stack.fmt.zero? print_ok "Formatted: #{@stack.name}" else Stacks.error!(@stack) print_crit "Formatting failed: #{@stack.name}" end end desc 'init NAME', 'Init an individual stack, by name' def init(name) @stack ||= Stack.new(name) print_ok "Initializing: #{@stack.name}" if @stack.init.zero? print_ok "Initialized: #{@stack.name}" else Stacks.error!(@stack) print_crit "Initialization failed: #{@stack.name}" throw :error end end desc 'plan NAME', 'Plan an individual stack, by name' # rubocop:disable Metrics/AbcSize def plan(name) @stack ||= Stack.new(name) print_ok "Planning: #{@stack.name}" case @stack.plan when 0 print_ok "No changes: #{@stack.name}" when 1 Stacks.error!(@stack) print_crit "Plan failed: #{@stack.name}" @stack.print_plan throw :error when 2 Stacks.dirty!(@stack) print_warning "Changes detected: #{@stack.name}" @stack.print_plan else raise end end # rubocop:enable Metrics/AbcSize desc 'audit NAME', 'Audit an individual stack, by name' def audit(name) plan(name) if (@stack = Stacks.dirty?(name)) Stacks.error!(@stack) print_crit "Dirty stack: #{@stack.name}" end end desc 'validate NAME', 'Validate an individual stack, by name' def validate(name) @stack ||= Stack.new(name) print_ok "Validating: #{@stack.name}" if @stack.validate.zero? print_ok "Validated: #{@stack.name}" else Stacks.error!(@stack) print_crit "Validation failed: #{@stack.name}" throw :error end end desc 'clean NAME', 'Clean an individual stack, by name' def clean(name) @stack ||= Stack.new(name) print_warning "Cleaning: #{@stack.name}" @stack.clean print_ok "Cleaned: #{@stack.name}" end ################################################################# # HIDDEN TARGETED STACK TASKS # * These tasks are destructive in nature and do not require # regular use. ################################################################# desc 'apply NAME', 'Apply an individual stack, by name', hide: true def apply(name) @stack ||= Stack.new(name) print_warning "Applying: #{@stack.name}" if @stack.apply.zero? print_ok "Applied: #{@stack.name}" else Stacks.error!(@stack) print_crit "Failed to apply changes: #{@stack.name}" end end desc 'refresh NAME', 'Refresh state on an individual stack, by name', hide: true def refresh(name) @stack ||= Stack.new(name) print_crit "Refreshing: #{@stack.name}" if @stack.refresh.zero? print_warning "Refreshed: #{@stack.name}" else Stacks.error!(@stack) print_crit "Failed to refresh stack: #{@stack.name}" end end desc 'destroy NAME', 'Destroy an individual stack, by name', hide: true def destroy(name) @stack ||= Stack.new(name) print_crit "Destroying: #{@stack.name}" if @stack.destroy.zero? print_warning "Destroyed: #{@stack.name}" else Stacks.error!(@stack) print_crit "Failed to apply changes: #{@stack.name}" end end end # rubocop:enable Metrics/ClassLength end