Skip to content
Open
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
4 changes: 2 additions & 2 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ Metrics/CyclomaticComplexity:
- 'lib/stack_master/aws_driver/s3.rb'
- 'lib/stack_master/cli.rb'
- 'lib/stack_master/parameter_resolvers/latest_container.rb'
- 'lib/stack_master/stack.rb'
- 'lib/stack_master/stack_definition.rb'
- 'lib/stack_master/stack_events/streamer.rb'

Expand All @@ -276,8 +277,6 @@ Metrics/MethodLength:
- 'lib/stack_master/config.rb'
- 'lib/stack_master/paged_response_accumulator.rb'
- 'lib/stack_master/parameter_resolvers/latest_container.rb'
- 'lib/stack_master/parameter_resolvers/stack_output.rb'
- 'lib/stack_master/parameter_validator.rb'
- 'lib/stack_master/prompter.rb'
- 'lib/stack_master/security_group_finder.rb'
- 'lib/stack_master/sso_group_id_finder.rb'
Expand Down Expand Up @@ -306,6 +305,7 @@ Metrics/PerceivedComplexity:
Exclude:
- 'lib/stack_master/aws_driver/s3.rb'
- 'lib/stack_master/cli.rb'
- 'lib/stack_master/stack.rb'
- 'lib/stack_master/stack_definition.rb'

Naming/AccessorMethodName:
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ The format is based on [Keep a Changelog], and this project adheres to

### Changed

- Resolve style issues identified by RuboCop ([#396])
- Display Tags diff (stack tags) in `stack_master diff` and `stack_master apply` commands. ([#397])
- Resolve style issues identified by RuboCop. ([#396])

[Unreleased]: https://github.com/envato/stack_master/compare/v2.17.1...HEAD
[#396]: https://github.com/envato/stack_master/pull/396
[#397]: https://github.com/envato/stack_master/pull/397

## [2.17.1] - 2025-12-19

Expand Down
74 changes: 74 additions & 0 deletions features/diff.feature
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,77 @@ Feature: Diff command
| + "GroupDescription": "Test SG 2", |
Then the exit status should be 0

Scenario: Run diff showing tags added when current stack has no tags
Given a file named "stack_master.yml" with:
"""
stacks:
us_east_1:
myapp_vpc:
template: myapp_vpc.json
tags:
Application: myapp
Environment: staging
"""
And a directory named "parameters"
And a file named "parameters/myapp_vpc.yml" with:
"""
KeyName: my-key
"""
And a directory named "templates"
And a file named "templates/myapp_vpc.json" with:
"""
{
"Description": "Test template",
"AWSTemplateFormatVersion": "2010-09-09",
"Parameters": {
"KeyName": {
"Description": "Key Name",
"Type": "String"
}
},
"Resources": {
"TestSg": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "Test SG",
"VpcId": {
"Ref": "VpcId"
}
}
}
}
}
"""
And I stub the following stacks:
| stack_id | stack_name | parameters | region |
| 1 | myapp-vpc | KeyName=changed-key | us-east-1 |
And I stub a template for the stack "myapp-vpc":
"""
{
"Description": "Test template",
"AWSTemplateFormatVersion": "2010-09-09",
"Parameters": {
"KeyName": {
"Description": "Key Name",
"Type": "String"
}
},
"Resources": {
"TestSg": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "Test SG",
"VpcId": {
"Ref": "VpcId"
}
}
}
}
}
"""
When I run `stack_master diff us-east-1 myapp-vpc --trace`
Then the output should contain all of these lines:
| Tags diff: |
| +Application: myapp |
| +Environment: staging |
And the exit status should be 0
1 change: 1 addition & 0 deletions features/step_definitions/stack_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def extract_hash_from_kv_string(string)
table.hashes.each do |row|
row.symbolize_keys!
row[:parameters] = StackMaster::Utils.hash_to_aws_parameters(extract_hash_from_kv_string(row[:parameters]))
row[:tags] = StackMaster::Utils.hash_to_aws_tags(extract_hash_from_kv_string(row[:tags])) if row.key?(:tags)
outputs = extract_hash_from_kv_string(row[:outputs]).each_with_object([]) do |(k, v), array|
array << OpenStruct.new(output_key: k, output_value: v)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/stack_master/stack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ def self.find(region, stack_name)
template_format = TemplateUtils.identify_template_format(template_body)
stack_policy_body ||= cf.get_stack_policy({ stack_name: stack_name }).stack_policy_body
outputs = cf_stack.outputs
tags = cf_stack.tags&.each_with_object({}) do |tag_struct, tags_hash|
tags_hash[tag_struct.key] = tag_struct.value
end || {}

new(region: region,
stack_name: stack_name,
stack_id: cf_stack.stack_id,
parameters: parameters,
tags: tags,
template_body: template_body,
template_format: template_format,
outputs: outputs,
Expand Down
25 changes: 25 additions & 0 deletions lib/stack_master/stack_differ.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,34 @@ def parameters_diff
after: proposed_parameters)
end

def tags_different?
tags_diff.different?
end

def tags_diff
@tags_diff ||= Diff.new(name: 'Tags',
before: current_tags,
after: proposed_tags)
end

def current_tags
tags_hash = @current_stack&.tags
return '' if tags_hash.nil? || tags_hash.empty?

YAML.dump(sort_params(tags_hash))
end

def proposed_tags
tags_hash = @proposed_stack.tags
return '' if tags_hash.nil? || tags_hash.empty?

YAML.dump(sort_params(tags_hash))
end

def output_diff
body_diff.display
parameters_diff.display
tags_diff.display

StackMaster.stdout.puts ' * can not tell if NoEcho parameters are different.' unless noecho_keys.empty?
StackMaster.stdout.puts 'No stack found' if @current_stack.nil?
Expand Down
8 changes: 8 additions & 0 deletions lib/stack_master/test_driver/cloud_formation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ def parameters
parameter_value: hash[:parameter_value])
end
end

def tags
return [] if @tags.nil?

@tags.map do |hash|
OpenStruct.new(key: hash[:key], value: hash[:value])
end
end
end

class StackEvent
Expand Down
88 changes: 88 additions & 0 deletions spec/stack_master/stack_differ_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,94 @@
expect { differ.output_diff }.to_not output(/No stack found/).to_stdout
end
end

context 'tags diff' do
context 'when tags are added on a new proposal' do
let(:stack) do
StackMaster::Stack.new(
stack_name: stack_name,
region: region,
template_body: '{}',
template_format: :json,
parameters: {},
tags: {}
)
end
let(:proposed_stack) do
StackMaster::Stack.new(
stack_name: stack_name,
region: region,
template_body: '{}',
template_format: :json,
parameters: {},
tags: { 'Application' => 'myapp', 'Environment' => 'staging' }
)
end

it 'prints a tags diff header and one-line additions for each tag' do
expect { differ.output_diff }.to output(/Tags diff:/).to_stdout
expect { differ.output_diff }.to output(/\+Application: myapp/).to_stdout
expect { differ.output_diff }.to output(/\+Environment: staging/).to_stdout
end
end

context 'when tags are unchanged and empty' do
let(:stack) do
StackMaster::Stack.new(
stack_name: stack_name,
region: region,
template_body: '{}',
template_format: :json,
parameters: {},
tags: {}
)
end
let(:proposed_stack) do
StackMaster::Stack.new(
stack_name: stack_name,
region: region,
template_body: '{}',
template_format: :json,
parameters: {},
tags: {}
)
end

it 'prints Tags diff: No changes' do
expect { differ.output_diff }.to output(/Tags diff: No changes/).to_stdout
end
end

context 'when tags are modified with additions and removals' do
let(:stack) do
StackMaster::Stack.new(
stack_name: stack_name,
region: region,
template_body: '{}',
template_format: :json,
parameters: {},
tags: { 'Application' => 'old', 'Environment' => 'staging' }
)
end
let(:proposed_stack) do
StackMaster::Stack.new(
stack_name: stack_name,
region: region,
template_body: '{}',
template_format: :json,
parameters: {},
tags: { 'Application' => 'new', 'Owner' => 'team' }
)
end

it 'prints +/- lines for changed/added/removed tags, one per line' do
expect { differ.output_diff }.to output(/-Application: old/).to_stdout
expect { differ.output_diff }.to output(/\+Application: new/).to_stdout
expect { differ.output_diff }.to output(/-Environment: staging/).to_stdout
expect { differ.output_diff }.to output(/\+Owner: team/).to_stdout
end
end
end
end

describe '#single_param_update?' do
Expand Down
8 changes: 8 additions & 0 deletions spec/stack_master/stack_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
creation_time: Time.now,
stack_status: 'UPDATE_COMPLETE',
parameters: parameters,
tags: [
{ key: 'Environment', value: 'staging' },
{ key: 'Application', value: 'myapp' }
],
notification_arns: ['test_arn'],
role_arn: 'test_service_role_arn'
}
Expand Down Expand Up @@ -62,6 +66,10 @@
it 'sets the stack policy' do
expect(stack.stack_policy_body).to eq stack_policy_body
end

it 'sets tags' do
expect(stack.tags).to eq({ 'Environment' => 'staging', 'Application' => 'myapp' })
end
end

context 'when the stack does not exist in AWS' do
Expand Down