Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
636 changes: 6 additions & 630 deletions .rubocop_todo.yml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ The format is based on [Keep a Changelog], and this project adheres to
### Changed

- Always treat default parameter values from CloudFormation templates as strings. Avoids erroneous diffs being presented. ([#394])
- Resolve layout issues identified by RuboCop ([#393])

[Unreleased]: https://github.com/envato/stack_master/compare/v2.17.0...HEAD
[#394]: https://github.com/envato/stack_master/pull/394
[#393]: https://github.com/envato/stack_master/pull/393

## [2.17.0] - 2025-07-11

Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ end
begin
require 'cucumber/rake/task'
Cucumber::Rake::Task.new(:features) do |t|
t.cucumber_opts = "features --format pretty"
t.cucumber_opts = %w[features --format pretty]
end

require 'rspec/core/rake_task'
Expand Down
14 changes: 9 additions & 5 deletions features/step_definitions/asume_role_steps.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
Then(/^I expect the role "([^"]*)" is assumed in account "([^"]*)"$/) do |role, account|
expect(Aws::AssumeRoleCredentials).to receive(:new).with({
region: instance_of(String),
role_arn: "arn:aws:iam::#{account}:role/#{role}",
role_session_name: instance_of(String)
})
expect(Aws::AssumeRoleCredentials)
.to receive(:new)
.with(
{
region: instance_of(String),
role_arn: "arn:aws:iam::#{account}:role/#{role}",
role_session_name: instance_of(String)
}
)
end
3 changes: 2 additions & 1 deletion features/step_definitions/parameter_store_steps.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Given(/^(?:a|the) SSM parameter(?: named)? "([^"]*)" with value "([^"]*)" in region "([^"]*)"$/) do |parameter_name, parameter_value, parameter_region|
Given(/^(?:a|the)\ SSM\ parameter(?:\ named)?\ "([^"]*)"
\ with\ value\ "([^"]*)"\ in\ region\ "([^"]*)"$/x) do |parameter_name, parameter_value, parameter_region|
Aws.config[:ssm] = {
stub_responses: {
get_parameter: {
Expand Down
5 changes: 3 additions & 2 deletions features/step_definitions/stack_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def extract_hash_from_kv_string(string)
end
end


Given(/^I stub the following stacks:$/) do |table|
table.hashes.each do |row|
row.symbolize_keys!
Expand Down Expand Up @@ -62,7 +61,9 @@ def extract_hash_from_kv_string(string)
end

Given(/^I stub CloudFormation validate calls to fail validation with message "([^"]*)"$/) do |message|
allow(StackMaster.cloud_formation_driver).to receive(:validate_template).and_raise(Aws::CloudFormation::Errors::ValidationError.new('', message))
allow(StackMaster.cloud_formation_driver)
.to receive(:validate_template)
.and_raise(Aws::CloudFormation::Errors::ValidationError.new('', message))
end

Given(/^I stub the CloudFormation driver$/) do
Expand Down
2 changes: 2 additions & 0 deletions lib/stack_master.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def debug?

def debug(message)
return unless debug?

stderr.puts Rainbow("[DEBUG] #{message}").color(:green)
end

Expand Down Expand Up @@ -167,6 +168,7 @@ def skip_account_check?
end

attr_accessor :non_interactive_answer

@non_interactive_answer = 'y'

def base_dir
Expand Down
40 changes: 21 additions & 19 deletions lib/stack_master/aws_driver/cloud_formation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,33 @@ def set_region(value)
end
end

def_delegators :cf, :create_change_set,
:describe_change_set,
:execute_change_set,
:delete_change_set,
:delete_stack,
:cancel_update_stack,
:describe_stack_resources,
:get_template,
:get_stack_policy,
:set_stack_policy,
:describe_stack_events,
:update_stack,
:create_stack,
:validate_template,
:describe_stacks,
:detect_stack_drift,
:describe_stack_drift_detection_status,
:describe_stack_resource_drifts
def_delegators(
:cf,
:create_change_set,
:describe_change_set,
:execute_change_set,
:delete_change_set,
:delete_stack,
:cancel_update_stack,
:describe_stack_resources,
:get_template,
:get_stack_policy,
:set_stack_policy,
:describe_stack_events,
:update_stack,
:create_stack,
:validate_template,
:describe_stacks,
:detect_stack_drift,
:describe_stack_drift_detection_status,
:describe_stack_resource_drifts
)

private

def cf
@cf ||= Aws::CloudFormation::Client.new({ region: region, retry_limit: 10 })
end

end
end
end
29 changes: 18 additions & 11 deletions lib/stack_master/aws_driver/s3.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ def set_region(region)
end

def upload_files(bucket: nil, prefix: nil, region: nil, files: {})
raise StackMaster::AwsDriver::S3ConfigurationError, 'A bucket must be specified in order to use S3' unless bucket
unless bucket
raise StackMaster::AwsDriver::S3ConfigurationError, 'A bucket must be specified in order to use S3'
end

return if files.empty?

s3 = new_s3_client(region: region)

current_objects = s3.list_objects({
prefix: prefix,
bucket: bucket
}).map(&:contents).flatten.inject({}){|h,obj|
current_objects = s3.list_objects(
{
prefix: prefix,
bucket: bucket
}
).map(&:contents).flatten.inject({}) { |h, obj|
h.merge(obj.key => obj)
}

Expand All @@ -35,15 +39,18 @@ def upload_files(bucket: nil, prefix: nil, region: nil, files: {})
s3_md5 = current_objects[object_key] ? current_objects[object_key].etag.gsub("\"", '') : nil

next if compiled_template_md5 == s3_md5

s3_uri = "s3://#{bucket}/#{object_key}"
StackMaster.stdout.print "- #{File.basename(path)} => #{s3_uri} "

s3.put_object({
bucket: bucket,
key: object_key,
body: body,
metadata: { md5: compiled_template_md5 }
})
s3.put_object(
{
bucket: bucket,
key: object_key,
body: body,
metadata: { md5: compiled_template_md5 }
}
)
StackMaster.stdout.puts "done."
end
end
Expand Down
10 changes: 5 additions & 5 deletions lib/stack_master/change_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ def initialize(describe_change_set_response)
end

def display(io)
io.puts <<-EOL
io.puts <<~EOL

========================================
Proposed change set:
EOL
========================================
Proposed change set:
EOL
@response.changes.each do |change|
display_resource_change(io, change.resource_change)
end
io.puts "========================================"
io.puts "========================================"
end

def failed?
Expand Down
33 changes: 24 additions & 9 deletions lib/stack_master/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module StackMaster
class CLI
include Commander::Methods

def initialize(argv, stdin=STDIN, stdout=STDOUT, stderr=STDERR, kernel=Kernel)
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('@instance', Commander::Runner.new(argv))
StackMaster.stdout = @stdout
Expand Down Expand Up @@ -41,9 +41,14 @@ def execute!
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.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 '--on-failure ACTION', String,
'Action to take on CREATE_FAILURE. ' \
'Valid Values: [ DO_NOTHING | ROLLBACK | DELETE ]. ' \
"Default: ROLLBACK\n" \
'Note: 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
Expand Down Expand Up @@ -171,11 +176,13 @@ def execute!
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.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
Expand All @@ -184,11 +191,13 @@ def execute!
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.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
Expand Down Expand Up @@ -269,11 +278,14 @@ def execute_stacks_command(command, args, options)
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?
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}"
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
Expand All @@ -283,20 +295,23 @@ def execute_stacks_command(command, args, options)
end

def show_other_region_candidates(config, stack_name)
candidates = config.filter(region="", stack_name=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

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}."
StackMaster.stdout.puts(
"Account #{account_text} is not an allowed account. Allowed accounts are #{allowed_accounts}."
)
false
end
end
Expand Down
6 changes: 5 additions & 1 deletion lib/stack_master/commands/apply.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def ask_update_confirmation!

def upload_files
return unless use_s3?

s3.upload_files(**s3_options)
end

Expand All @@ -153,14 +154,16 @@ def template_method

def template_value
if use_s3?
s3.url(bucket: @s3_config['bucket'], prefix: @s3_config['prefix'], region: @s3_config['region'], template: @stack_definition.s3_template_file_name)
s3.url(bucket: @s3_config['bucket'], prefix: @s3_config['prefix'], region: @s3_config['region'],
template: @stack_definition.s3_template_file_name)
else
proposed_stack.template
end
end

def files_to_upload
return {} unless use_s3?

@stack_definition.s3_files.tap do |files|
files[@stack_definition.s3_template_file_name] = {
path: @stack_definition.template_file_path,
Expand Down Expand Up @@ -218,6 +221,7 @@ def set_stack_policy
proposed_policy = proposed_stack.stack_policy_body
# No need to reset a stack policy if it's nil or not changed
return if proposed_policy.nil? || proposed_policy == current_policy

StackMaster.stdout.print 'Setting a stack policy...'
cf.set_stack_policy(
stack_name: stack_name,
Expand Down
5 changes: 2 additions & 3 deletions lib/stack_master/commands/delete.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ def initialize(region, stack_name, options)
end

def perform

return unless check_exists

unless ask?("Really delete stack #{@stack_name} (y/n)? ")
Expand All @@ -27,11 +26,11 @@ def perform
private

def delete_stack
cf.delete_stack({stack_name: @stack_name})
cf.delete_stack({ stack_name: @stack_name })
end

def check_exists
cf.describe_stacks({stack_name: @stack_name})
cf.describe_stacks({ stack_name: @stack_name })
true
rescue Aws::CloudFormation::Errors::ValidationError
failed("Stack does not exist")
Expand Down
9 changes: 6 additions & 3 deletions lib/stack_master/commands/drift.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ def perform
detect_stack_drift_result = cf.detect_stack_drift(stack_name: stack_name)
drift_results = wait_for_drift_results(detect_stack_drift_result.stack_drift_detection_id)

puts colorize("Drift Status: #{drift_results.stack_drift_status}", stack_drift_status_color(drift_results.stack_drift_status))
puts colorize("Drift Status: #{drift_results.stack_drift_status}",
stack_drift_status_color(drift_results.stack_drift_status))
return if drift_results.stack_drift_status == 'IN_SYNC'

failed
Expand Down Expand Up @@ -51,8 +52,10 @@ def display_drift(drift)
end

def display_resource_drift(drift)
diff = ::StackMaster::Diff.new(before: prettify_json(drift.expected_properties),
after: prettify_json(drift.actual_properties))
diff = ::StackMaster::Diff.new(
before: prettify_json(drift.expected_properties),
after: prettify_json(drift.actual_properties)
)
diff.display_colorized_diff
end

Expand Down
Loading