Skip to content

Commit 38d1005

Browse files
committed
Display tags diff during diff and apply commands
1 parent a95bc37 commit 38d1005

File tree

7 files changed

+203
-1
lines changed

7 files changed

+203
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ The format is based on [Keep a Changelog], and this project adheres to
1212

1313
### Changed
1414

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

1718
[Unreleased]: https://github.com/envato/stack_master/compare/v2.17.1...HEAD
1819
[#396]: https://github.com/envato/stack_master/pull/396
20+
[#397]: https://github.com/envato/stack_master/pull/397
1921

2022
## [2.17.1] - 2025-12-19
2123

features/diff.feature

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,77 @@ Feature: Diff command
177177
| + "GroupDescription": "Test SG 2", |
178178
Then the exit status should be 0
179179

180+
Scenario: Run diff showing tags added when current stack has no tags
181+
Given a file named "stack_master.yml" with:
182+
"""
183+
stacks:
184+
us_east_1:
185+
myapp_vpc:
186+
template: myapp_vpc.json
187+
tags:
188+
Application: myapp
189+
Environment: staging
190+
"""
191+
And a directory named "parameters"
192+
And a file named "parameters/myapp_vpc.yml" with:
193+
"""
194+
KeyName: my-key
195+
"""
196+
And a directory named "templates"
197+
And a file named "templates/myapp_vpc.json" with:
198+
"""
199+
{
200+
"Description": "Test template",
201+
"AWSTemplateFormatVersion": "2010-09-09",
202+
"Parameters": {
203+
"KeyName": {
204+
"Description": "Key Name",
205+
"Type": "String"
206+
}
207+
},
208+
"Resources": {
209+
"TestSg": {
210+
"Type": "AWS::EC2::SecurityGroup",
211+
"Properties": {
212+
"GroupDescription": "Test SG",
213+
"VpcId": {
214+
"Ref": "VpcId"
215+
}
216+
}
217+
}
218+
}
219+
}
220+
"""
221+
And I stub the following stacks:
222+
| stack_id | stack_name | parameters | region |
223+
| 1 | myapp-vpc | KeyName=changed-key | us-east-1 |
224+
And I stub a template for the stack "myapp-vpc":
225+
"""
226+
{
227+
"Description": "Test template",
228+
"AWSTemplateFormatVersion": "2010-09-09",
229+
"Parameters": {
230+
"KeyName": {
231+
"Description": "Key Name",
232+
"Type": "String"
233+
}
234+
},
235+
"Resources": {
236+
"TestSg": {
237+
"Type": "AWS::EC2::SecurityGroup",
238+
"Properties": {
239+
"GroupDescription": "Test SG",
240+
"VpcId": {
241+
"Ref": "VpcId"
242+
}
243+
}
244+
}
245+
}
246+
}
247+
"""
248+
When I run `stack_master diff us-east-1 myapp-vpc --trace`
249+
Then the output should contain all of these lines:
250+
| Tags diff: |
251+
| +Application: myapp |
252+
| +Environment: staging |
253+
And the exit status should be 0

features/step_definitions/stack_steps.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def extract_hash_from_kv_string(string)
3131
table.hashes.each do |row|
3232
row.symbolize_keys!
3333
row[:parameters] = StackMaster::Utils.hash_to_aws_parameters(extract_hash_from_kv_string(row[:parameters]))
34+
row[:tags] = StackMaster::Utils.hash_to_aws_tags(extract_hash_from_kv_string(row[:tags])) if row.key?(:tags)
3435
outputs = extract_hash_from_kv_string(row[:outputs]).each_with_object([]) do |(k, v), array|
3536
array << OpenStruct.new(output_key: k, output_value: v)
3637
end

lib/stack_master/stack.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ def self.find(region, stack_name)
4141
template_format = TemplateUtils.identify_template_format(template_body)
4242
stack_policy_body ||= cf.get_stack_policy({ stack_name: stack_name }).stack_policy_body
4343
outputs = cf_stack.outputs
44+
tags = cf_stack.tags&.each_with_object({}) do |tag_struct, tags_hash|
45+
tags_hash[tag_struct.key] = tag_struct.value
46+
end || {}
4447

4548
new(region: region,
4649
stack_name: stack_name,
4750
stack_id: cf_stack.stack_id,
4851
parameters: parameters,
52+
tags: tags,
4953
template_body: template_body,
5054
template_format: template_format,
5155
outputs: outputs,

lib/stack_master/stack_differ.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,34 @@ def parameters_diff
6161
after: proposed_parameters)
6262
end
6363

64+
def tags_different?
65+
tags_diff.different?
66+
end
67+
68+
def tags_diff
69+
@tags_diff ||= Diff.new(name: 'Tags',
70+
before: current_tags,
71+
after: proposed_tags)
72+
end
73+
74+
def current_tags
75+
tags_hash = @current_stack&.tags
76+
return '' if tags_hash.nil? || tags_hash.empty?
77+
78+
YAML.dump(sort_params(tags_hash))
79+
end
80+
81+
def proposed_tags
82+
tags_hash = @proposed_stack.tags
83+
return '' if tags_hash.nil? || tags_hash.empty?
84+
85+
YAML.dump(sort_params(tags_hash))
86+
end
87+
6488
def output_diff
6589
body_diff.display
6690
parameters_diff.display
91+
tags_diff.display
6792

6893
StackMaster.stdout.puts ' * can not tell if NoEcho parameters are different.' unless noecho_keys.empty?
6994
StackMaster.stdout.puts 'No stack found' if @current_stack.nil?

spec/stack_master/stack_differ_spec.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,94 @@
5656
expect { differ.output_diff }.to_not output(/No stack found/).to_stdout
5757
end
5858
end
59+
60+
context 'tags diff' do
61+
context 'when tags are added on a new proposal' do
62+
let(:stack) do
63+
StackMaster::Stack.new(
64+
stack_name: stack_name,
65+
region: region,
66+
template_body: "{}",
67+
template_format: :json,
68+
parameters: {},
69+
tags: {}
70+
)
71+
end
72+
let(:proposed_stack) do
73+
StackMaster::Stack.new(
74+
stack_name: stack_name,
75+
region: region,
76+
template_body: "{}",
77+
template_format: :json,
78+
parameters: {},
79+
tags: { 'Application' => 'myapp', 'Environment' => 'staging' }
80+
)
81+
end
82+
83+
it 'prints a tags diff header and one-line additions for each tag' do
84+
expect { differ.output_diff }.to output(/Tags diff:/).to_stdout
85+
expect { differ.output_diff }.to output(/\+Application: myapp/).to_stdout
86+
expect { differ.output_diff }.to output(/\+Environment: staging/).to_stdout
87+
end
88+
end
89+
90+
context 'when tags are unchanged and empty' do
91+
let(:stack) do
92+
StackMaster::Stack.new(
93+
stack_name: stack_name,
94+
region: region,
95+
template_body: "{}",
96+
template_format: :json,
97+
parameters: {},
98+
tags: {}
99+
)
100+
end
101+
let(:proposed_stack) do
102+
StackMaster::Stack.new(
103+
stack_name: stack_name,
104+
region: region,
105+
template_body: "{}",
106+
template_format: :json,
107+
parameters: {},
108+
tags: {}
109+
)
110+
end
111+
112+
it 'prints Tags diff: No changes' do
113+
expect { differ.output_diff }.to output(/Tags diff: No changes/).to_stdout
114+
end
115+
end
116+
117+
context 'when tags are modified with additions and removals' do
118+
let(:stack) do
119+
StackMaster::Stack.new(
120+
stack_name: stack_name,
121+
region: region,
122+
template_body: "{}",
123+
template_format: :json,
124+
parameters: {},
125+
tags: { 'Application' => 'old', 'Environment' => 'staging' }
126+
)
127+
end
128+
let(:proposed_stack) do
129+
StackMaster::Stack.new(
130+
stack_name: stack_name,
131+
region: region,
132+
template_body: "{}",
133+
template_format: :json,
134+
parameters: {},
135+
tags: { 'Application' => 'new', 'Owner' => 'team' }
136+
)
137+
end
138+
139+
it 'prints +/- lines for changed/added/removed tags, one per line' do
140+
expect { differ.output_diff }.to output(/-Application: old/).to_stdout
141+
expect { differ.output_diff }.to output(/\+Application: new/).to_stdout
142+
expect { differ.output_diff }.to output(/-Environment: staging/).to_stdout
143+
expect { differ.output_diff }.to output(/\+Owner: team/).to_stdout
144+
end
145+
end
146+
end
59147
end
60148

61149
describe '#single_param_update?' do

spec/stack_master/stack_spec.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
creation_time: Time.now,
3030
stack_status: 'UPDATE_COMPLETE',
3131
parameters: parameters,
32+
tags: [
33+
{ key: 'Environment', value: 'staging' },
34+
{ key: 'Application', value: 'myapp' }
35+
],
3236
notification_arns: ['test_arn'],
3337
role_arn: 'test_service_role_arn'
3438
}
@@ -62,6 +66,10 @@
6266
it 'sets the stack policy' do
6367
expect(stack.stack_policy_body).to eq stack_policy_body
6468
end
69+
70+
it 'sets tags' do
71+
expect(stack.tags).to eq({ 'Environment' => 'staging', 'Application' => 'myapp' })
72+
end
6573
end
6674

6775
context 'when the stack does not exist in AWS' do

0 commit comments

Comments
 (0)