diff --git a/features/apply_with_explicit_parameter_files.feature b/features/apply_with_explicit_parameter_files.feature new file mode 100644 index 00000000..088d47b1 --- /dev/null +++ b/features/apply_with_explicit_parameter_files.feature @@ -0,0 +1,54 @@ +Feature: Apply command with explicit parameter files + + Background: + Given a file named "stack_master.yml" with: + """ + stack_defaults: + tags: + Application: myapp + region: us-east-1 + """ + And a file named "myapp-web.yml" with: + """ + template: myapp.rb + parameter_files: + - myapp-web-parameters.yml + """ + And a file named "myapp-web-parameters.yml" with: + """ + KeyName: my-key + """ + And a directory named "templates" + And a file named "templates/myapp.rb" with: + """ + SparkleFormation.new(:myapp) do + description "Test template" + + parameters.key_name do + description 'Key name' + type 'String' + end + + resources.instance do + type 'AWS::EC2::Instance' + properties do + image_id 'ami-0080e4c5bc078760e' + instance_type 't2.micro' + end + end + end + """ + + Scenario: Run apply with stack-name.yml and explicit parameter files + Given I stub the following stack events: + | stack_id | event_id | stack_name | logical_resource_id | resource_status | resource_type | timestamp | + | 1 | 1 | myapp-web | myapp-web | CREATE_COMPLETE | AWS::CloudFormation::Stack | 2020-10-29 00:00:00 | + When I run `stack_master apply myapp-web.yml --trace` + Then the output should match /2020-10-29 00:00:00 (\+|\-)[0-9]{4} myapp-web AWS::CloudFormation::Stack CREATE_COMPLETE/ + And the output should contain all of these lines: + | Stack diff: | + | + "Instance": { | + | Parameters diff: | + | KeyName: my-key | + | Proposed change set: | + And the exit status should be 0 diff --git a/features/apply_with_stack_definition_parameters.feature b/features/apply_with_stack_definition_parameters.feature new file mode 100644 index 00000000..32d9f9e8 --- /dev/null +++ b/features/apply_with_stack_definition_parameters.feature @@ -0,0 +1,46 @@ +Feature: Apply command with stack definition parameters + + Background: + Given a file named "stack_master.yml" with: + """ + stacks: + us-east-1: + myapp_web: + template: myapp.rb + parameters: + KeyName: my-key + """ + And a directory named "templates" + And a file named "templates/myapp.rb" with: + """ + SparkleFormation.new(:myapp) do + description "Test template" + + parameters.key_name do + description 'Key name' + type 'String' + end + + resources.instance do + type 'AWS::EC2::Instance' + properties do + image_id 'ami-0080e4c5bc078760e' + instance_type 't2.micro' + end + end + end + """ + + Scenario: Run apply with parameters contained in + Given I stub the following stack events: + | stack_id | event_id | stack_name | logical_resource_id | resource_status | resource_type | timestamp | + | 1 | 1 | myapp-web | myapp-web | CREATE_COMPLETE | AWS::CloudFormation::Stack | 2020-10-29 00:00:00 | + When I run `stack_master apply us-east-1 myapp-web --trace` + Then the output should match /2020-10-29 00:00:00 (\+|\-)[0-9]{4} myapp-web AWS::CloudFormation::Stack CREATE_COMPLETE/ + And the output should contain all of these lines: + | Stack diff: | + | + "Instance": { | + | Parameters diff: | + | KeyName: my-key | + | Proposed change set: | + And the exit status should be 0 diff --git a/features/apply_with_stack_yml_file.feature b/features/apply_with_stack_yml_file.feature new file mode 100644 index 00000000..86e80ceb --- /dev/null +++ b/features/apply_with_stack_yml_file.feature @@ -0,0 +1,50 @@ +Feature: Apply command with stack.yml file + + Background: + Given a file named "stack_master.yml" with: + """ + stack_defaults: + tags: + Application: myapp + region: us-east-1 + """ + And a file named "myapp-web.yml" with: + """ + template: myapp.rb + parameters: + KeyName: my-key + """ + And a directory named "templates" + And a file named "templates/myapp.rb" with: + """ + SparkleFormation.new(:myapp) do + description "Test template" + + parameters.key_name do + description 'Key name' + type 'String' + end + + resources.instance do + type 'AWS::EC2::Instance' + properties do + image_id 'ami-0080e4c5bc078760e' + instance_type 't2.micro' + end + end + end + """ + + Scenario: Run apply with stack-name.yml argument + Given I stub the following stack events: + | stack_id | event_id | stack_name | logical_resource_id | resource_status | resource_type | timestamp | + | 1 | 1 | myapp-web | myapp-web | CREATE_COMPLETE | AWS::CloudFormation::Stack | 2020-10-29 00:00:00 | + When I run `stack_master apply myapp-web.yml --trace` + Then the output should match /2020-10-29 00:00:00 (\+|\-)[0-9]{4} myapp-web AWS::CloudFormation::Stack CREATE_COMPLETE/ + And the output should contain all of these lines: + | Stack diff: | + | + "Instance": { | + | Parameters diff: | + | KeyName: my-key | + | Proposed change set: | + And the exit status should be 0 diff --git a/lib/stack_master/cli.rb b/lib/stack_master/cli.rb index f0c5c23b..e36ad066 100644 --- a/lib/stack_master/cli.rb +++ b/lib/stack_master/cli.rb @@ -234,27 +234,39 @@ def load_config(file) 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}" - 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? + if args.size == 1 && (args.first.end_with?('.yml') || args.first.end_with?('.yaml')) + yaml_file_name = args.first + stack_definition = config.build_stack_definition(yaml_file_name) + success = run_command_with_stack_definition(command, stack_definition, config, options) + @kernel.exit false unless success + else + 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}" + 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| + success = run_command_with_stack_definition(command, stack_definition, config, options) end end + @kernel.exit false unless success + end + end + + def run_command_with_stack_definition(command, stack_definition, config, options) + 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}" + execute_if_allowed_account(stack_definition.allowed_accounts) do + command.perform(config, stack_definition, options).success? end - @kernel.exit false unless success end def execute_if_allowed_account(allowed_accounts, &block) diff --git a/lib/stack_master/commands/tidy.rb b/lib/stack_master/commands/tidy.rb index d7fc63b1..24b440e8 100644 --- a/lib/stack_master/commands/tidy.rb +++ b/lib/stack_master/commands/tidy.rb @@ -12,7 +12,7 @@ def perform parameter_files = Set.new(find_parameter_files()) status = @config.stacks.each do |stack_definition| - parameter_files.subtract(stack_definition.parameter_files) + parameter_files.subtract(stack_definition.parameter_files_from_globs) template = File.absolute_path(stack_definition.template_file_path) if template diff --git a/lib/stack_master/config.rb b/lib/stack_master/config.rb index f85e797f..6c54780a 100644 --- a/lib/stack_master/config.rb +++ b/lib/stack_master/config.rb @@ -67,7 +67,15 @@ def unalias_region(region) @region_aliases.fetch(region) { region } end + def build_stack_definition(yaml_file) + stack_name = yaml_file.sub('.yml', '').sub('.yaml', '') + attributes = YAML.load(File.read(yaml_file)) + stack_attributes = merge_stack_attributes(nil, stack_name, attributes) + StackDefinition.new(stack_attributes) + end + private + def load_template_compilers(config) @template_compilers = {} populate_template_compilers(config.fetch('template_compilers', {})) @@ -94,7 +102,7 @@ def default_template_compilers end def load_config - unaliased_stacks = resolve_region_aliases(@config.fetch('stacks')) + unaliased_stacks = resolve_region_aliases(@config.fetch('stacks', {})) load_stacks(unaliased_stacks) end @@ -110,18 +118,25 @@ def load_stacks(stacks) region = Utils.underscore_to_hyphen(region) stacks_for_region.each do |stack_name, attributes| stack_name = Utils.underscore_to_hyphen(stack_name) - stack_attributes = build_stack_defaults(region).deeper_merge!(attributes).merge( - 'region' => region, - 'stack_name' => stack_name, - 'base_dir' => @base_dir, - 'template_dir' => @template_dir, - 'additional_parameter_lookup_dirs' => @region_to_aliases[region]) - stack_attributes['allowed_accounts'] = attributes['allowed_accounts'] if attributes['allowed_accounts'] + stack_attributes = merge_stack_attributes(region, stack_name, attributes) @stacks << StackDefinition.new(stack_attributes) end end end + def merge_stack_attributes(region, stack_name, attributes) + stack_attributes = build_stack_defaults(region) + .deeper_merge!(attributes) + .merge( + 'region' => region, + 'stack_name' => stack_name, + 'base_dir' => @base_dir, + 'template_dir' => @template_dir, + 'additional_parameter_lookup_dirs' => @region_to_aliases[region]) + stack_attributes['allowed_accounts'] = attributes['allowed_accounts'] if attributes['allowed_accounts'] + stack_attributes + end + def build_stack_defaults(region) region_defaults = @region_defaults.fetch(region, {}).deep_dup @stack_defaults.deep_dup.deeper_merge(region_defaults) diff --git a/lib/stack_master/parameter_loader.rb b/lib/stack_master/parameter_loader.rb index 452be95a..64dacfce 100644 --- a/lib/stack_master/parameter_loader.rb +++ b/lib/stack_master/parameter_loader.rb @@ -5,10 +5,10 @@ class ParameterLoader COMPILE_TIME_PARAMETERS_KEY = 'compile_time_parameters' - def self.load(parameter_files) + def self.load(parameter_files: [], parameters: {}) StackMaster.debug 'Searching for parameter files...' - parameter_files.reduce({template_parameters: {}, compile_time_parameters: {}}) do |hash, file_name| - parameters = load_parameters(file_name) + all_parameters = parameter_files.map { |file_name| load_parameters(file_name) } + [parameters] + all_parameters.reduce({template_parameters: {}, compile_time_parameters: {}}) do |hash, parameters| template_parameters = create_template_parameters(parameters) compile_time_parameters = create_compile_time_parameters(parameters) @@ -16,7 +16,6 @@ def self.load(parameter_files) merge_and_camelize(hash[:compile_time_parameters], compile_time_parameters) hash end - end private diff --git a/lib/stack_master/stack.rb b/lib/stack_master/stack.rb index ebb6a283..8c07ef2c 100644 --- a/lib/stack_master/stack.rb +++ b/lib/stack_master/stack.rb @@ -56,7 +56,7 @@ def self.find(region, stack_name) end def self.generate(stack_definition, config) - parameter_hash = ParameterLoader.load(stack_definition.parameter_files) + parameter_hash = ParameterLoader.load(parameter_files: stack_definition.all_parameter_files, parameters: stack_definition.parameters) template_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:template_parameters]) compile_time_parameters = ParameterResolver.resolve(config, stack_definition, parameter_hash[:compile_time_parameters]) template_body = TemplateCompiler.compile(config, stack_definition.compiler, stack_definition.template_dir, stack_definition.template, compile_time_parameters, stack_definition.compiler_options) diff --git a/lib/stack_master/stack_definition.rb b/lib/stack_master/stack_definition.rb index 587afb77..56bc0295 100644 --- a/lib/stack_master/stack_definition.rb +++ b/lib/stack_master/stack_definition.rb @@ -16,7 +16,9 @@ class StackDefinition :additional_parameter_lookup_dirs, :s3, :files, - :compiler_options + :compiler_options, + :parameters, + :parameter_files attr_reader :compiler @@ -34,6 +36,8 @@ def initialize(attributes = {}) @additional_parameter_lookup_dirs ||= [] @template_dir ||= File.join(@base_dir, 'templates') @allowed_accounts = Array(@allowed_accounts) + @parameters ||= {} + @parameter_files ||= [] end def ==(other) @@ -85,7 +89,11 @@ def s3_template_file_name Utils.change_extension(template, 'json') end - def parameter_files + def all_parameter_files + parameter_files_from_globs + parameter_files + end + + def parameter_files_from_globs parameter_file_globs.map(&Dir.method(:glob)).flatten end diff --git a/spec/stack_master/parameter_loader_spec.rb b/spec/stack_master/parameter_loader_spec.rb index 9d6015b1..65739e9e 100644 --- a/spec/stack_master/parameter_loader_spec.rb +++ b/spec/stack_master/parameter_loader_spec.rb @@ -2,7 +2,7 @@ let(:stack_file_name) { '/base_dir/parameters/stack_name.yml' } let(:region_file_name) { '/base_dir/parameters/us-east-1/stack_name.yml' } - subject(:parameters) { StackMaster::ParameterLoader.load([stack_file_name, region_file_name]) } + subject(:parameters) { StackMaster::ParameterLoader.load(parameter_files: [stack_file_name, region_file_name]) } before do file_mock(stack_file_name, **stack_file_returns) @@ -60,7 +60,7 @@ let(:region_yaml_file_returns) { {exists: true, read: "Param1: value1\nParam2: valueX"} } let(:region_yaml_file_name) { "/base_dir/parameters/us-east-1/stack_name.yaml" } - subject(:parameters) { StackMaster::ParameterLoader.load([stack_file_name, region_yaml_file_name, region_file_name]) } + subject(:parameters) { StackMaster::ParameterLoader.load(parameter_files: [stack_file_name, region_yaml_file_name, region_file_name]) } before do file_mock(region_yaml_file_name, **region_yaml_file_returns) diff --git a/spec/stack_master/stack_definition_spec.rb b/spec/stack_master/stack_definition_spec.rb index b9deb16e..a01886b0 100644 --- a/spec/stack_master/stack_definition_spec.rb +++ b/spec/stack_master/stack_definition_spec.rb @@ -35,7 +35,7 @@ end it 'has default and region specific parameter file locations' do - expect(stack_definition.parameter_files).to eq([ + expect(stack_definition.all_parameter_files).to eq([ "/base_dir/parameters/#{stack_name}.yaml", "/base_dir/parameters/#{stack_name}.yml", "/base_dir/parameters/#{region}/#{stack_name}.yaml", @@ -75,7 +75,7 @@ end it 'includes a parameter lookup dir for it' do - expect(stack_definition.parameter_files).to eq([ + expect(stack_definition.all_parameter_files).to eq([ "/base_dir/parameters/#{stack_name}.yaml", "/base_dir/parameters/#{stack_name}.yml", "/base_dir/parameters/#{region}/#{stack_name}.yaml", diff --git a/spec/stack_master/template_compiler_spec.rb b/spec/stack_master/template_compiler_spec.rb index 403de7f7..5b018c9b 100644 --- a/spec/stack_master/template_compiler_spec.rb +++ b/spec/stack_master/template_compiler_spec.rb @@ -13,7 +13,7 @@ def self.compile(template_dir, template, compile_time_parameters, compile_option context 'when a template compiler is explicitly specified' do it 'uses it' do expect(StackMaster::TemplateCompilers::SparkleFormation).to receive(:compile).with('/base_dir/templates', 'template', compile_time_parameters, anything) - StackMaster::TemplateCompiler.compile(config, :sparkle_formation, '/base_dir/templates', 'template', compile_time_parameters, compile_time_parameters) + StackMaster::TemplateCompiler.compile(config, :sparkle_formation, '/base_dir/templates', 'template', compile_time_parameters, {}) end end