diff --git a/lib/moonshot/commands/create.rb b/lib/moonshot/commands/create.rb index c33b807a..08cfd3fc 100644 --- a/lib/moonshot/commands/create.rb +++ b/lib/moonshot/commands/create.rb @@ -1,7 +1,15 @@ +# frozen_string_literal: true + +require_relative 'parameter_arguments' +require_relative 'tag_arguments' +require_relative 'show_all_events_option' +require_relative 'parent_stack_option' + module Moonshot module Commands class Create < Moonshot::Command include ParameterArguments + include TagArguments include ShowAllEventsOption include ParentStackOption @@ -21,6 +29,10 @@ def parser parser.on('--version VERSION_NAME', 'Version for initial deployment. If unset, a new development build is created from the local directory') do |v| # rubocop:disable LineLength @version = v end + + parser.on('--template-file=FILE', 'Override the path to the CloudFormation template.') do |v| + Moonshot.config.template_file = v + end end def execute diff --git a/lib/moonshot/commands/tag_arguments.rb b/lib/moonshot/commands/tag_arguments.rb new file mode 100644 index 00000000..a09abe1a --- /dev/null +++ b/lib/moonshot/commands/tag_arguments.rb @@ -0,0 +1,18 @@ +module Moonshot + module Commands + module TagArguments + def parser + parser = super + + parser.on('--tag KEY=VALUE', '-TKEY=VALUE', 'Specify Stack Tag on the command line') do |v| + data = v.split('=', 2) + unless data.size == 2 + raise "Invalid tag format '#{v}', expected KEY=VALUE (e.g. MyStackTag=12)" + end + + Moonshot.config.extra_tags << { key: data[0], value: data[1] } + end + end + end + end +end diff --git a/lib/moonshot/commands/update.rb b/lib/moonshot/commands/update.rb index 4b2d98fb..533e164b 100644 --- a/lib/moonshot/commands/update.rb +++ b/lib/moonshot/commands/update.rb @@ -2,6 +2,7 @@ module Moonshot module Commands class Update < Moonshot::Command include ParameterArguments + include TagArguments include ShowAllEventsOption include ParentStackOption diff --git a/lib/moonshot/controller_config.rb b/lib/moonshot/controller_config.rb index ea38c5ec..3fb29706 100644 --- a/lib/moonshot/controller_config.rb +++ b/lib/moonshot/controller_config.rb @@ -27,6 +27,7 @@ class ControllerConfig attr_accessor :ssh_config attr_accessor :ssh_instance attr_accessor :template_s3_bucket + attr_accessor :extra_tags def initialize @default_parameter_source = AskUserSource.new @@ -42,6 +43,7 @@ def initialize @project_root = Dir.pwd @show_all_stack_events = false @ssh_config = SSHConfig.new + @extra_tags = [] @dev_build_name_proc = lambda do |c| ['dev', c.app_name, c.environment_name, Time.now.to_i].join('/') diff --git a/lib/moonshot/stack.rb b/lib/moonshot/stack.rb index a0d21ed2..6d45765a 100644 --- a/lib/moonshot/stack.rb +++ b/lib/moonshot/stack.rb @@ -12,10 +12,30 @@ class Stack # rubocop:disable ClassLength attr_reader :app_name attr_reader :name + class << self + def generate_name(config) + [config.app_name, config.environment_name].join('-') + end + + def make_tags(config) + default_tags = [ + { key: 'moonshot_application', value: config.app_name }, + { key: 'moonshot_environment', value: config.environment_name }, + ] + name = generate_name(config) + + if config.additional_tag + default_tags << { key: config.additional_tag, value: name } + end + + default_tags + config.extra_tags + end + end + def initialize(config) @config = config @ilog = config.interactive_logger - @name = [@config.app_name, @config.environment_name].join('-') + @name = self.class.generate_name(@config) yield @config if block_given? end @@ -225,7 +245,8 @@ def new_change_set description: "Moonshot update command for application '#{Moonshot.config.app_name}'", stack_name: @name, capabilities: %w(CAPABILITY_IAM CAPABILITY_NAMED_IAM), - parameters: @config.parameters.values.map(&:to_cf) + parameters: @config.parameters.values.map(&:to_cf), + tags: make_tags } if @config.template_s3_bucket parameters[:template_url] = upload_template_to_s3 @@ -284,16 +305,7 @@ def wait_for_stack_state(wait_target, past_tense_verb) end def make_tags - default_tags = [ - { key: 'moonshot_application', value: @config.app_name }, - { key: 'moonshot_environment', value: @config.environment_name } - ] - - if @config.additional_tag - default_tags << { key: @config.additional_tag, value: @name } - end - - default_tags + self.class.make_tags(@config) end def format_event(event) diff --git a/lib/plugins/encrypted_parameters.rb b/lib/plugins/encrypted_parameters.rb index 705676a7..e8ce381f 100644 --- a/lib/plugins/encrypted_parameters.rb +++ b/lib/plugins/encrypted_parameters.rb @@ -102,7 +102,9 @@ def find_or_create_kms_key Moonshot.config.parameters[@kms_key_parameter_name].set(key_arn) s.success "Created a new KMS Key for #{@kms_key_parameter_name.blue}!" else - key_arn = KmsKey.new(Moonshot.config.parameters[@kms_key_parameter_name].value).arn + kms=KmsKey.new(Moonshot.config.parameters[@kms_key_parameter_name].value) + key_arn = kms.arn + kms.update s.success "Using existing KMS Key for #{@kms_key_parameter_name.blue}!" end end diff --git a/lib/plugins/encrypted_parameters/kms_key.rb b/lib/plugins/encrypted_parameters/kms_key.rb index 77f613f1..38134910 100644 --- a/lib/plugins/encrypted_parameters/kms_key.rb +++ b/lib/plugins/encrypted_parameters/kms_key.rb @@ -1,20 +1,39 @@ +# frozen_string_literal: true +require_relative '../../moonshot/stack.rb' + module Moonshot module Plugins class EncryptedParameters # Class that manages KMS keys in AWS. class KmsKey attr_reader :arn + class << self + def create + standard_tags = stack_tags + resp = Aws::KMS::Client.new.create_key({ + tags: standard_tags, # An array of tags. + }) + arn = resp.key_metadata.arn + new(arn) + end + + def stack_tags + tags = Moonshot::Stack.make_tags(Moonshot.config) + tags.map { |tag| { tag_key: tag[:key], tag_value: tag[:value] } } + end + end def initialize(arn) @arn = arn @kms_client = Aws::KMS::Client.new end - def self.create - resp = Aws::KMS::Client.new.create_key - arn = resp.key_metadata.arn - - new(arn) + def update + standard_tags = self.class.stack_tags + @kms_client.tag_resource({ + key_id: @arn, # arn of the CMK being tagged + tags: standard_tags, # An array of tags. + }) end def delete diff --git a/spec/moonshot/commands/create_spec.rb b/spec/moonshot/commands/create_spec.rb index dce390cc..bbce9544 100644 --- a/spec/moonshot/commands/create_spec.rb +++ b/spec/moonshot/commands/create_spec.rb @@ -16,6 +16,22 @@ end end + extra_tags = { + %w(-T Key=Value -T OtherKey=OtherValue) => + [{ key: 'Key', value: 'Value' }, { key: 'OtherKey', value: 'OtherValue' }], + %w(-TKey=ValueWith=Equals) => [{ key: 'Key', value: 'ValueWith=Equals' }], + %w(--tag Key=Value --tag OtherKey=OtherValue) => + [{ key: 'Key', value: 'Value' }, { key: 'OtherKey', value: 'OtherValue' }] + } + + extra_tags.each do |input, expected| + it "Should process #{input} correctly" do + op = subject.parser + op.parse(input) + expect(Moonshot.config.extra_tags).to match(expected) + end + end + it 'should handle version and deploy correctly' do op = subject.parser op.parse(%w(--version 1.2.3 --no-deploy -P Key=Value)) diff --git a/spec/moonshot/commands/update_spec.rb b/spec/moonshot/commands/update_spec.rb index aba7ee12..1660aab7 100644 --- a/spec/moonshot/commands/update_spec.rb +++ b/spec/moonshot/commands/update_spec.rb @@ -15,4 +15,20 @@ expect(Moonshot.config.parameter_overrides).to match(expected) end end + + extra_tags = { + %w(-T Key=Value -T OtherKey=OtherValue) => + [{ key: 'Key', value: 'Value' }, { key: 'OtherKey', value: 'OtherValue' }], + %w(-TKey=ValueWith=Equals) => [{ key: 'Key', value: 'ValueWith=Equals' }], + %w(--tag Key=Value --tag OtherKey=OtherValue) => + [{ key: 'Key', value: 'Value' }, { key: 'OtherKey', value: 'OtherValue' }] + } + + extra_tags.each do |input, expected| + it "Should process #{input} correctly" do + op = subject.parser + op.parse(input) + expect(Moonshot.config.extra_tags).to match(expected) + end + end end diff --git a/spec/moonshot/stack_spec.rb b/spec/moonshot/stack_spec.rb index f5e01e12..8074e8b5 100644 --- a/spec/moonshot/stack_spec.rb +++ b/spec/moonshot/stack_spec.rb @@ -124,6 +124,31 @@ subject.create end end + + context 'when has extra tags' do + let(:expected_create_stack_options) do + { + tags: [ + { key: 'moonshot_application', value: 'rspec-app' }, + { key: 'moonshot_environment', value: 'staging' }, + { key: 'tag1', value: 'A' }, + { key: 'tag2', value: 'B' } + ] + } + end + + before do + config.extra_tags << { key: 'tag1', value: 'A' } + config.extra_tags << { key: 'tag2', value: 'B' } + end + + it 'should call CreateStack, then wait for completion' do + expect(s3_client).not_to receive(:put_object) + expect(cf_client).to receive(:create_stack) + .with(hash_including(expected_create_stack_options)) + subject.create + end + end end end