class StackMaster::CLI
Public Class Methods
new(argv, stdin=STDIN, stdout=STDOUT, stderr=STDERR, kernel=Kernel)
click to toggle source
# File lib/stack_master/cli.rb, line 8 def initialize(argv, stdin=STDIN, stdout=STDOUT, stderr=STDERR, kernel=Kernel) @argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel Commander::Runner.instance_variable_set('@singleton', Commander::Runner.new(argv)) StackMaster.stdout = @stdout StackMaster.stderr = @stderr TablePrint::Config.io = StackMaster.stdout end
Public Instance Methods
execute!()
click to toggle source
# File lib/stack_master/cli.rb, line 16 def execute! program :name, 'StackMaster' program :version, StackMaster::VERSION program :description, 'AWS Stack Management' global_option '-c', '--config FILE', String, 'Config file to use' global_option '--changed', 'filter stack selection to only ones that have changed' global_option '-y', '--yes', 'Run in non-interactive mode answering yes to any prompts' do StackMaster.non_interactive! StackMaster.non_interactive_answer = 'y' end global_option '-n', '--no', 'Run in non-interactive mode answering no to any prompts' do StackMaster.non_interactive! StackMaster.non_interactive_answer = 'n' end global_option '-d', '--debug', 'Run in debug mode' do StackMaster.debug! end global_option '-q', '--quiet', 'Do not output the resulting Stack Events, just return immediately' do StackMaster.quiet! end global_option '--skip-account-check', 'Do not check if command is allowed to execute in account' do StackMaster.skip_account_check! end command :apply do |c| c.syntax = 'stack_master apply [region_or_alias] [stack_name]' c.summary = 'Creates or updates a stack' c.description = "Creates or updates a stack. Shows a diff of the proposed stack's template and parameters. Tails stack events until CloudFormation has completed." c.example 'update a stack named myapp-vpc in us-east-1', 'stack_master apply us-east-1 myapp-vpc' c.option '--on-failure ACTION', String, "Action to take on CREATE_FAILURE. Valid Values: [ DO_NOTHING | ROLLBACK | DELETE ]. Default: ROLLBACK\nNote: You cannot use this option with Serverless Application Model (SAM) templates." c.option '--yes-param PARAM_NAME', String, "Auto-approve stack updates when only parameter PARAM_NAME changes" c.action do |args, options| options.default config: default_config_file execute_stacks_command(StackMaster::Commands::Apply, args, options) end end command :outputs do |c| c.syntax = 'stack_master outputs [region_or_alias] [stack_name]' c.summary = 'Displays outputs for a stack' c.description = "Displays outputs for a stack" c.action do |args, options| options.default config: default_config_file execute_stacks_command(StackMaster::Commands::Outputs, args, options) end end command :init do |c| c.syntax = 'stack_master init [region_or_alias] [stack_name]' c.summary = 'Initialises the expected directory structure and stack_master.yml file' c.description = 'Initialises the expected directory structure and stack_master.yml file' c.option('--overwrite', 'Overwrite existing files') c.action do |args, options| options.default config: default_config_file unless args.size == 2 say "Invalid arguments. stack_master init [region] [stack_name]" else StackMaster::Commands::Init.perform(options, *args) end end end command :diff do |c| c.syntax = 'stack_master diff [region_or_alias] [stack_name]' c.summary = "Shows a diff of the proposed stack's template and parameters" c.description = "Shows a diff of the proposed stack's template and parameters" c.example 'diff a stack named myapp-vpc in us-east-1', 'stack_master diff us-east-1 myapp-vpc' c.action do |args, options| options.default config: default_config_file execute_stacks_command(StackMaster::Commands::Diff, args, options) end end command :events do |c| c.syntax = 'stack_master events [region_or_alias] [stack_name]' c.summary = "Shows events for a stack" c.description = "Shows events for a stack" c.example 'show events for myapp-vpc in us-east-1', 'stack_master events us-east-1 myapp-vpc' c.option '--number Integer', Integer, 'Number of recent events to show' c.option '--all', 'Show all events' c.option '--tail', 'Tail events' c.action do |args, options| options.default config: default_config_file execute_stacks_command(StackMaster::Commands::Events, args, options) end end command :resources do |c| c.syntax = 'stack_master resources [region] [stack_name]' c.summary = "Shows stack resources" c.description = "Shows stack resources" c.action do |args, options| options.default config: default_config_file execute_stacks_command(StackMaster::Commands::Resources, args, options) end end command :list do |c| c.syntax = 'stack_master list' c.summary = 'List stack definitions' c.description = 'List stack definitions' c.action do |args, options| options.default config: default_config_file say "Invalid arguments." if args.size > 0 config = load_config(options.config) StackMaster::Commands::ListStacks.perform(config, nil, options) end end command :validate do |c| c.syntax = 'stack_master validate [region_or_alias] [stack_name]' c.summary = 'Validate a template' c.description = 'Validate a template' c.example 'validate a stack named myapp-vpc in us-east-1', 'stack_master validate us-east-1 myapp-vpc' c.option '--[no-]validate-template-parameters', 'Validate template parameters. Default: validate' c.action do |args, options| options.default config: default_config_file, validate_template_parameters: true execute_stacks_command(StackMaster::Commands::Validate, args, options) end end command :lint do |c| c.syntax = 'stack_master lint [region_or_alias] [stack_name]' c.summary = "Check the stack definition locally" c.description = "Runs cfn-lint on the template which would be sent to AWS on apply" c.example 'run cfn-lint on stack myapp-vpc with us-east-1 settings', 'stack_master lint us-east-1 myapp-vpc' c.action do |args, options| options.default config: default_config_file execute_stacks_command(StackMaster::Commands::Lint, args, options) end end command :nag do |c| c.syntax = 'stack_master nag [region_or_alias] [stack_name]' c.summary = "Check this stack's template with cfn_nag" c.description = "Runs SAST scan cfn_nag on the template" c.example 'run cfn_nag on stack myapp-vpc with us-east-1 settings', 'stack_master nag us-east-1 myapp-vpc' c.action do |args, options| options.default config: default_config_file execute_stacks_command(StackMaster::Commands::Nag, args, options) end end command :compile do |c| c.syntax = 'stack_master compile [region_or_alias] [stack_name]' c.summary = "Print the compiled version of a given stack" c.description = "Processes the stack and prints out a compiled version - same we'd send to AWS" c.example 'print compiled stack myapp-vpc with us-east-1 settings', 'stack_master compile us-east-1 myapp-vpc' c.action do |args, options| options.default config: default_config_file execute_stacks_command(StackMaster::Commands::Compile, args, options) end end command :status do |c| c.syntax = 'stack_master status' c.summary = 'Check the current status stacks.' c.description = 'Checks the status of all stacks defined in the stack_master.yml file. Warning this operation can be somewhat slow.' c.example 'description', 'Check the status of all stack definitions' c.action do |args, options| options.default config: default_config_file say "Invalid arguments. stack_master status" and return unless args.size == 0 config = load_config(options.config) StackMaster::Commands::Status.perform(config, nil, options) end end command :tidy do |c| c.syntax = 'stack_master tidy' c.summary = 'Try to identify extra & missing files.' c.description = 'Cross references stack_master.yml with the template and parameter directories to identify extra or missing files.' c.example 'description', 'Check for missing or extra files' c.action do |args, options| options.default config: default_config_file say "Invalid arguments. stack_master tidy" and return unless args.size == 0 config = load_config(options.config) StackMaster::Commands::Tidy.perform(config, nil, options) end end command :delete do |c| c.syntax = 'stack_master delete [region] [stack_name]' c.summary = 'Delete an existing stack' c.description = 'Deletes a stack. The stack does not necessarily have to appear in the stack_master.yml file.' c.example 'description', 'Delete a stack' c.action do |args, options| options.default config: default_config_file unless args.size == 2 say "Invalid arguments. stack_master delete [region] [stack_name]" return end stack_name = Utils.underscore_to_hyphen(args[1]) allowed_accounts = [] # Because delete can work without a stack_master.yml if options.config and File.file?(options.config) config = load_config(options.config) region = Utils.underscore_to_hyphen(config.unalias_region(args[0])) allowed_accounts = config.find_stack(region, stack_name)&.allowed_accounts else region = args[0] end success = execute_if_allowed_account(allowed_accounts) do StackMaster.cloud_formation_driver.set_region(region) StackMaster::Commands::Delete.perform(region, stack_name, options).success? end @kernel.exit false unless success end end command :drift do |c| c.syntax = 'stack_master drift [region_or_alias] [stack_name]' c.summary = 'Detects and displays stack drift using the CloudFormation Drift API' c.description = 'Detects and displays stack drift' c.option '--timeout SECONDS', Integer, "The number of seconds to wait for drift detection to complete" c.example 'view stack drift for a stack named myapp-vpc in us-east-1', 'stack_master drift us-east-1 myapp-vpc' c.action do |args, options| options.default config: default_config_file, timeout: 120 execute_stacks_command(StackMaster::Commands::Drift, args, options) end end run! end
Private Instance Methods
default_config_file()
click to toggle source
# File lib/stack_master/cli.rb, line 246 def default_config_file "stack_master.yml" end
execute_if_allowed_account(allowed_accounts, &block)
click to toggle source
# File lib/stack_master/cli.rb, line 292 def execute_if_allowed_account(allowed_accounts, &block) raise ArgumentError, "Block required to execute this method" unless block_given? if running_in_allowed_account?(allowed_accounts) block.call else account_text = "'#{identity.account}'" account_text << " (#{identity.account_aliases.join(', ')})" if identity.account_aliases.any? StackMaster.stdout.puts "Account #{account_text} is not an allowed account. Allowed accounts are #{allowed_accounts}." false end end
execute_stacks_command(command, args, options)
click to toggle source
# File lib/stack_master/cli.rb, line 258 def execute_stacks_command(command, args, options) success = true config = load_config(options.config) args = [nil, nil] if args.size == 0 args.each_slice(2) do |aliased_region, stack_name| region = Utils.underscore_to_hyphen(config.unalias_region(aliased_region)) stack_name = Utils.underscore_to_hyphen(stack_name) stack_definitions = config.filter(region, stack_name) if stack_definitions.empty? StackMaster.stdout.puts "Could not find stack definition #{stack_name} in region #{region}" show_other_region_candidates(config, stack_name) success = false end stack_definitions = stack_definitions.select do |stack_definition| running_in_allowed_account?(stack_definition.allowed_accounts) && StackStatus.new(config, stack_definition).changed? end if options.changed stack_definitions.each do |stack_definition| StackMaster.cloud_formation_driver.set_region(stack_definition.region) StackMaster.stdout.puts "Executing #{command.command_name} on #{stack_definition.stack_name} in #{stack_definition.region}" success = execute_if_allowed_account(stack_definition.allowed_accounts) do command.perform(config, stack_definition, options).success? end end end @kernel.exit false unless success end
identity()
click to toggle source
# File lib/stack_master/cli.rb, line 308 def identity @identity ||= StackMaster::Identity.new end
load_config(file)
click to toggle source
# File lib/stack_master/cli.rb, line 250 def load_config(file) stack_file = file || default_config_file StackMaster::Config.load!(stack_file) rescue Errno::ENOENT => e say "Failed to load config file #{stack_file}" @kernel.exit false end
running_in_allowed_account?(allowed_accounts)
click to toggle source
# File lib/stack_master/cli.rb, line 304 def running_in_allowed_account?(allowed_accounts) StackMaster.skip_account_check? || identity.running_in_account?(allowed_accounts) end
show_other_region_candidates(config, stack_name)
click to toggle source
# File lib/stack_master/cli.rb, line 285 def show_other_region_candidates(config, stack_name) candidates = config.filter(region="", stack_name=stack_name) return if candidates.empty? StackMaster.stdout.puts "Stack name #{stack_name} exists in regions: #{candidates.map(&:region).join(', ')}" end