stack_master 2.17.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -6
- data/bin/stack_master +3 -5
- data/lib/stack_master/aws_driver/cloud_formation.rb +25 -23
- data/lib/stack_master/aws_driver/s3.rb +24 -17
- data/lib/stack_master/change_set.rb +11 -15
- data/lib/stack_master/cli.rb +53 -32
- data/lib/stack_master/command.rb +1 -5
- data/lib/stack_master/commands/apply.rb +39 -20
- data/lib/stack_master/commands/delete.rb +6 -7
- data/lib/stack_master/commands/drift.rb +11 -12
- data/lib/stack_master/commands/events.rb +4 -6
- data/lib/stack_master/commands/init.rb +21 -20
- data/lib/stack_master/commands/lint.rb +1 -1
- data/lib/stack_master/commands/list_stacks.rb +1 -1
- data/lib/stack_master/commands/nag.rb +0 -1
- data/lib/stack_master/commands/outputs.rb +1 -1
- data/lib/stack_master/commands/resources.rb +9 -1
- data/lib/stack_master/commands/status.rb +4 -4
- data/lib/stack_master/commands/terminal_helper.rb +3 -3
- data/lib/stack_master/commands/tidy.rb +14 -13
- data/lib/stack_master/config.rb +23 -21
- data/lib/stack_master/diff.rb +2 -2
- data/lib/stack_master/identity.rb +2 -1
- data/lib/stack_master/parameter_loader.rb +3 -5
- data/lib/stack_master/parameter_resolver.rb +18 -18
- data/lib/stack_master/parameter_resolvers/acm_certificate.rb +4 -1
- data/lib/stack_master/parameter_resolvers/ami_finder.rb +2 -3
- data/lib/stack_master/parameter_resolvers/ejson.rb +9 -6
- data/lib/stack_master/parameter_resolvers/env.rb +1 -2
- data/lib/stack_master/parameter_resolvers/latest_ami_by_tags.rb +1 -1
- data/lib/stack_master/parameter_resolvers/latest_container.rb +9 -7
- data/lib/stack_master/parameter_resolvers/one_password.rb +11 -7
- data/lib/stack_master/parameter_resolvers/parameter_store.rb +1 -5
- data/lib/stack_master/parameter_resolvers/security_group.rb +1 -1
- data/lib/stack_master/parameter_resolvers/sso_group_id.rb +3 -2
- data/lib/stack_master/parameter_resolvers/stack_output.rb +7 -9
- data/lib/stack_master/parameter_validator.rb +2 -5
- data/lib/stack_master/prompter.rb +11 -10
- data/lib/stack_master/resolver_array.rb +2 -3
- data/lib/stack_master/role_assumer.rb +7 -5
- data/lib/stack_master/security_group_finder.rb +7 -5
- data/lib/stack_master/sns_topic_finder.rb +4 -3
- data/lib/stack_master/sparkle_formation/compile_time/allowed_pattern_validator.rb +2 -3
- data/lib/stack_master/sparkle_formation/compile_time/allowed_values_validator.rb +2 -3
- data/lib/stack_master/sparkle_formation/compile_time/definitions_validator.rb +6 -6
- data/lib/stack_master/sparkle_formation/compile_time/empty_validator.rb +0 -2
- data/lib/stack_master/sparkle_formation/compile_time/max_length_validator.rb +1 -2
- data/lib/stack_master/sparkle_formation/compile_time/max_size_validator.rb +1 -2
- data/lib/stack_master/sparkle_formation/compile_time/min_length_validator.rb +2 -3
- data/lib/stack_master/sparkle_formation/compile_time/min_size_validator.rb +2 -3
- data/lib/stack_master/sparkle_formation/compile_time/number_validator.rb +1 -2
- data/lib/stack_master/sparkle_formation/compile_time/parameters_validator.rb +0 -1
- data/lib/stack_master/sparkle_formation/compile_time/state_builder.rb +0 -2
- data/lib/stack_master/sparkle_formation/compile_time/string_validator.rb +2 -3
- data/lib/stack_master/sparkle_formation/compile_time/value_builder.rb +7 -9
- data/lib/stack_master/sparkle_formation/compile_time/value_validator.rb +3 -6
- data/lib/stack_master/sparkle_formation/compile_time/value_validator_factory.rb +11 -13
- data/lib/stack_master/sparkle_formation/template_file.rb +2 -4
- data/lib/stack_master/sso_group_id_finder.rb +15 -12
- data/lib/stack_master/stack.rb +51 -18
- data/lib/stack_master/stack_definition.rb +6 -5
- data/lib/stack_master/stack_differ.rb +36 -9
- data/lib/stack_master/stack_events/fetcher.rb +3 -1
- data/lib/stack_master/stack_events/presenter.rb +6 -1
- data/lib/stack_master/stack_events/streamer.rb +3 -1
- data/lib/stack_master/stack_status.rb +1 -0
- data/lib/stack_master/template_compilers/json.rb +1 -1
- data/lib/stack_master/template_compilers/sparkle_formation.rb +12 -9
- data/lib/stack_master/template_utils.rb +8 -4
- data/lib/stack_master/test_driver/cloud_formation.rb +34 -9
- data/lib/stack_master/test_driver/s3.rb +2 -3
- data/lib/stack_master/utils.rb +4 -6
- data/lib/stack_master/validator.rb +7 -6
- data/lib/stack_master/version.rb +1 -1
- data/lib/stack_master.rb +3 -1
- metadata +21 -10
- data/lib/stack_master/parameter_resolvers/accounts_by_tags.rb +0 -60
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ffa37da4e3a6d6d80804a60be1b868180a67470009472b02a0c838354028efb6
|
|
4
|
+
data.tar.gz: e04ddf265d427425f0a1a60973eec74e2e6cc42e3cc47f00b8cbf584f1cdac93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb887f7ebe035ec7ffd808af93ad5cd69329a161dfcef8f6986e1eb79d975ce81c8b7d344403d21ca9bcaa3faafa9e91d1ef50b17cbdf79d35f88fd4cf69926f
|
|
7
|
+
data.tar.gz: d59f604ca4b6881c928525ea5c8c88f3d07a8dde8899b27ed427223f6102d78c27a59e1cbcb51588e39653221c3c90fceaa758b809ab2d161df8d6bfed3da036
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/envato/stack_master/blob/master/LICENSE.md)
|
|
4
4
|
[](https://badge.fury.io/rb/stack_master)
|
|
5
|
-
[](https://github.com/envato/stack_master/actions/workflows/test.yml)
|
|
6
6
|
|
|
7
7
|
StackMaster is a CLI tool to manage [CloudFormation](https://aws.amazon.com/cloudformation/) stacks, with the following features:
|
|
8
8
|
|
|
@@ -794,11 +794,6 @@ CLI directly rather than via a stack update.
|
|
|
794
794
|
applied in CloudFormation. This can happen if the template or computed parameters have changed in code and the change
|
|
795
795
|
hasn't been applied to this stack.
|
|
796
796
|
|
|
797
|
-
## Maintainers
|
|
798
|
-
|
|
799
|
-
- [Steve Hodgkiss](https://github.com/stevehodgkiss)
|
|
800
|
-
- [Glen Stampoultzis](https://github.com/gstamp)
|
|
801
|
-
|
|
802
797
|
## License
|
|
803
798
|
|
|
804
799
|
StackMaster uses the MIT license. See [LICENSE.txt](https://github.com/envato/stack_master/blob/master/LICENSE.txt) for details.
|
data/bin/stack_master
CHANGED
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require 'stack_master'
|
|
4
4
|
|
|
5
|
-
if ENV['STUB_AWS'] == 'true'
|
|
6
|
-
require 'stack_master/testing'
|
|
7
|
-
end
|
|
5
|
+
require 'stack_master/testing' if ENV['STUB_AWS'] == 'true'
|
|
8
6
|
|
|
9
|
-
trap(
|
|
7
|
+
trap('SIGINT') { raise StackMaster::CtrlC }
|
|
10
8
|
|
|
11
9
|
begin
|
|
12
10
|
StackMaster::CLI.new(ARGV.dup).execute!
|
|
13
11
|
rescue StackMaster::CtrlC
|
|
14
|
-
StackMaster.stdout.puts
|
|
12
|
+
StackMaster.stdout.puts 'Exiting...'
|
|
15
13
|
end
|
|
@@ -8,37 +8,39 @@ module StackMaster
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def set_region(value)
|
|
11
|
-
if region
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
return if region == value
|
|
12
|
+
|
|
13
|
+
@region = value
|
|
14
|
+
@cf = nil
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def_delegators
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
17
|
+
def_delegators(
|
|
18
|
+
:cf,
|
|
19
|
+
:create_change_set,
|
|
20
|
+
:describe_change_set,
|
|
21
|
+
:execute_change_set,
|
|
22
|
+
:delete_change_set,
|
|
23
|
+
:delete_stack,
|
|
24
|
+
:cancel_update_stack,
|
|
25
|
+
:describe_stack_resources,
|
|
26
|
+
:get_template,
|
|
27
|
+
:get_stack_policy,
|
|
28
|
+
:set_stack_policy,
|
|
29
|
+
:describe_stack_events,
|
|
30
|
+
:update_stack,
|
|
31
|
+
:create_stack,
|
|
32
|
+
:validate_template,
|
|
33
|
+
:describe_stacks,
|
|
34
|
+
:detect_stack_drift,
|
|
35
|
+
:describe_stack_drift_detection_status,
|
|
36
|
+
:describe_stack_resource_drifts
|
|
37
|
+
)
|
|
35
38
|
|
|
36
39
|
private
|
|
37
40
|
|
|
38
41
|
def cf
|
|
39
42
|
@cf ||= Aws::CloudFormation::Client.new({ region: region, retry_limit: 10 })
|
|
40
43
|
end
|
|
41
|
-
|
|
42
44
|
end
|
|
43
45
|
end
|
|
44
46
|
end
|
|
@@ -11,20 +11,24 @@ module StackMaster
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def upload_files(bucket: nil, prefix: nil, region: nil, files: {})
|
|
14
|
-
|
|
14
|
+
unless bucket
|
|
15
|
+
raise StackMaster::AwsDriver::S3ConfigurationError, 'A bucket must be specified in order to use S3'
|
|
16
|
+
end
|
|
15
17
|
|
|
16
18
|
return if files.empty?
|
|
17
19
|
|
|
18
20
|
s3 = new_s3_client(region: region)
|
|
19
21
|
|
|
20
|
-
current_objects = s3.list_objects(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
current_objects = s3.list_objects(
|
|
23
|
+
{
|
|
24
|
+
prefix: prefix,
|
|
25
|
+
bucket: bucket
|
|
26
|
+
}
|
|
27
|
+
).map(&:contents).flatten.inject({}) do |h, obj|
|
|
24
28
|
h.merge(obj.key => obj)
|
|
25
|
-
|
|
29
|
+
end
|
|
26
30
|
|
|
27
|
-
StackMaster.stdout.puts
|
|
31
|
+
StackMaster.stdout.puts 'Uploading files to S3:'
|
|
28
32
|
|
|
29
33
|
files.each do |template, file|
|
|
30
34
|
body = file.fetch(:body)
|
|
@@ -32,26 +36,29 @@ module StackMaster
|
|
|
32
36
|
object_key = template.dup
|
|
33
37
|
object_key.prepend("#{prefix}/") if prefix
|
|
34
38
|
compiled_template_md5 = Digest::MD5.hexdigest(body).to_s
|
|
35
|
-
s3_md5 = current_objects[object_key] ? current_objects[object_key].etag.gsub("
|
|
39
|
+
s3_md5 = current_objects[object_key] ? current_objects[object_key].etag.gsub('"', '') : nil
|
|
36
40
|
|
|
37
41
|
next if compiled_template_md5 == s3_md5
|
|
42
|
+
|
|
38
43
|
s3_uri = "s3://#{bucket}/#{object_key}"
|
|
39
44
|
StackMaster.stdout.print "- #{File.basename(path)} => #{s3_uri} "
|
|
40
45
|
|
|
41
|
-
s3.put_object(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
s3.put_object(
|
|
47
|
+
{
|
|
48
|
+
bucket: bucket,
|
|
49
|
+
key: object_key,
|
|
50
|
+
body: body,
|
|
51
|
+
metadata: { md5: compiled_template_md5 }
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
StackMaster.stdout.puts 'done.'
|
|
48
55
|
end
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
def url(bucket:, prefix:, region:, template:)
|
|
52
59
|
if region == 'us-east-1'
|
|
53
|
-
[
|
|
54
|
-
elsif region.start_with?
|
|
60
|
+
['https://s3.amazonaws.com', bucket, prefix, template].compact.join('/')
|
|
61
|
+
elsif region.start_with? 'cn-'
|
|
55
62
|
["https://s3.#{region}.amazonaws.com.cn", bucket, prefix, template].compact.join('/')
|
|
56
63
|
else
|
|
57
64
|
["https://s3-#{region}.amazonaws.com", bucket, prefix, template].compact.join('/')
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module StackMaster
|
|
2
2
|
class ChangeSet
|
|
3
|
-
END_STATES = [
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
END_STATES = %w[
|
|
4
|
+
CREATE_COMPLETE
|
|
5
|
+
DELETE_COMPLETE
|
|
6
|
+
FAILED
|
|
7
7
|
]
|
|
8
8
|
|
|
9
9
|
def self.generate_change_set_name(stack_name)
|
|
@@ -42,15 +42,15 @@ module StackMaster
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def display(io)
|
|
45
|
-
io.puts
|
|
45
|
+
io.puts <<~EOL
|
|
46
46
|
|
|
47
|
-
========================================
|
|
48
|
-
Proposed change set:
|
|
49
|
-
EOL
|
|
47
|
+
========================================
|
|
48
|
+
Proposed change set:
|
|
49
|
+
EOL
|
|
50
50
|
@response.changes.each do |change|
|
|
51
51
|
display_resource_change(io, change.resource_change)
|
|
52
52
|
end
|
|
53
|
-
io.puts
|
|
53
|
+
io.puts '========================================'
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def failed?
|
|
@@ -84,13 +84,9 @@ io.puts "========================================"
|
|
|
84
84
|
def display_resource_change_detail(io, action_name, color, detail)
|
|
85
85
|
target_name = [detail.target.attribute, detail.target.name].compact.join('.')
|
|
86
86
|
detail_messages = [target_name]
|
|
87
|
-
if action_name == 'Replace'
|
|
88
|
-
detail_messages << "#{detail.target.requires_recreation} requires recreation"
|
|
89
|
-
end
|
|
87
|
+
detail_messages << "#{detail.target.requires_recreation} requires recreation" if action_name == 'Replace'
|
|
90
88
|
triggered_by = [detail.change_source, detail.causing_entity].compact.join('.')
|
|
91
|
-
if detail.evaluation != 'Static'
|
|
92
|
-
triggered_by << "(#{detail.evaluation})"
|
|
93
|
-
end
|
|
89
|
+
triggered_by << "(#{detail.evaluation})" if detail.evaluation != 'Static'
|
|
94
90
|
detail_messages << "Triggered by: #{triggered_by}"
|
|
95
91
|
io.puts Rainbow("- #{detail_messages.join('. ')}. ").color(color)
|
|
96
92
|
end
|
data/lib/stack_master/cli.rb
CHANGED
|
@@ -5,8 +5,12 @@ module StackMaster
|
|
|
5
5
|
class CLI
|
|
6
6
|
include Commander::Methods
|
|
7
7
|
|
|
8
|
-
def initialize(argv, stdin=STDIN, stdout=STDOUT, stderr=STDERR, kernel=Kernel)
|
|
9
|
-
@argv
|
|
8
|
+
def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = Kernel)
|
|
9
|
+
@argv = argv
|
|
10
|
+
@stdin = stdin
|
|
11
|
+
@stdout = stdout
|
|
12
|
+
@stderr = stderr
|
|
13
|
+
@kernel = kernel
|
|
10
14
|
Commander::Runner.instance_variable_set('@instance', Commander::Runner.new(argv))
|
|
11
15
|
StackMaster.stdout = @stdout
|
|
12
16
|
StackMaster.stderr = @stderr
|
|
@@ -41,10 +45,15 @@ module StackMaster
|
|
|
41
45
|
command :apply do |c|
|
|
42
46
|
c.syntax = 'stack_master apply [region_or_alias] [stack_name]'
|
|
43
47
|
c.summary = 'Creates or updates a stack'
|
|
44
|
-
c.description = "Creates or updates a stack. Shows a diff of the proposed stack's template and parameters.
|
|
48
|
+
c.description = "Creates or updates a stack. Shows a diff of the proposed stack's template and parameters. " \
|
|
49
|
+
'Tails stack events until CloudFormation has completed.'
|
|
45
50
|
c.example 'update a stack named myapp-vpc in us-east-1', 'stack_master apply us-east-1 myapp-vpc'
|
|
46
|
-
c.option '--on-failure ACTION', String,
|
|
47
|
-
|
|
51
|
+
c.option '--on-failure ACTION', String,
|
|
52
|
+
'Action to take on CREATE_FAILURE. ' \
|
|
53
|
+
'Valid Values: [ DO_NOTHING | ROLLBACK | DELETE ]. ' \
|
|
54
|
+
"Default: ROLLBACK\n" \
|
|
55
|
+
'Note: You cannot use this option with Serverless Application Model (SAM) templates.'
|
|
56
|
+
c.option '--yes-param PARAM_NAME', String, 'Auto-approve stack updates when only parameter PARAM_NAME changes'
|
|
48
57
|
c.action do |args, options|
|
|
49
58
|
options.default config: default_config_file
|
|
50
59
|
execute_stacks_command(StackMaster::Commands::Apply, args, options)
|
|
@@ -54,7 +63,7 @@ module StackMaster
|
|
|
54
63
|
command :outputs do |c|
|
|
55
64
|
c.syntax = 'stack_master outputs [region_or_alias] [stack_name]'
|
|
56
65
|
c.summary = 'Displays outputs for a stack'
|
|
57
|
-
c.description =
|
|
66
|
+
c.description = 'Displays outputs for a stack'
|
|
58
67
|
c.action do |args, options|
|
|
59
68
|
options.default config: default_config_file
|
|
60
69
|
execute_stacks_command(StackMaster::Commands::Outputs, args, options)
|
|
@@ -68,10 +77,10 @@ module StackMaster
|
|
|
68
77
|
c.option('--overwrite', 'Overwrite existing files')
|
|
69
78
|
c.action do |args, options|
|
|
70
79
|
options.default config: default_config_file
|
|
71
|
-
|
|
72
|
-
say "Invalid arguments. stack_master init [region] [stack_name]"
|
|
73
|
-
else
|
|
80
|
+
if args.size == 2
|
|
74
81
|
StackMaster::Commands::Init.perform(options, *args)
|
|
82
|
+
else
|
|
83
|
+
say 'Invalid arguments. stack_master init [region] [stack_name]'
|
|
75
84
|
end
|
|
76
85
|
end
|
|
77
86
|
end
|
|
@@ -89,8 +98,8 @@ module StackMaster
|
|
|
89
98
|
|
|
90
99
|
command :events do |c|
|
|
91
100
|
c.syntax = 'stack_master events [region_or_alias] [stack_name]'
|
|
92
|
-
c.summary =
|
|
93
|
-
c.description =
|
|
101
|
+
c.summary = 'Shows events for a stack'
|
|
102
|
+
c.description = 'Shows events for a stack'
|
|
94
103
|
c.example 'show events for myapp-vpc in us-east-1', 'stack_master events us-east-1 myapp-vpc'
|
|
95
104
|
c.option '--number Integer', Integer, 'Number of recent events to show'
|
|
96
105
|
c.option '--all', 'Show all events'
|
|
@@ -103,8 +112,8 @@ module StackMaster
|
|
|
103
112
|
|
|
104
113
|
command :resources do |c|
|
|
105
114
|
c.syntax = 'stack_master resources [region] [stack_name]'
|
|
106
|
-
c.summary =
|
|
107
|
-
c.description =
|
|
115
|
+
c.summary = 'Shows stack resources'
|
|
116
|
+
c.description = 'Shows stack resources'
|
|
108
117
|
c.action do |args, options|
|
|
109
118
|
options.default config: default_config_file
|
|
110
119
|
execute_stacks_command(StackMaster::Commands::Resources, args, options)
|
|
@@ -117,7 +126,7 @@ module StackMaster
|
|
|
117
126
|
c.description = 'List stack definitions'
|
|
118
127
|
c.action do |args, options|
|
|
119
128
|
options.default config: default_config_file
|
|
120
|
-
say
|
|
129
|
+
say 'Invalid arguments.' if args.size > 0
|
|
121
130
|
config = load_config(options.config)
|
|
122
131
|
StackMaster::Commands::ListStacks.perform(config, nil, options)
|
|
123
132
|
end
|
|
@@ -137,8 +146,8 @@ module StackMaster
|
|
|
137
146
|
|
|
138
147
|
command :lint do |c|
|
|
139
148
|
c.syntax = 'stack_master lint [region_or_alias] [stack_name]'
|
|
140
|
-
c.summary =
|
|
141
|
-
c.description =
|
|
149
|
+
c.summary = 'Check the stack definition locally'
|
|
150
|
+
c.description = 'Runs cfn-lint on the template which would be sent to AWS on apply'
|
|
142
151
|
c.example 'run cfn-lint on stack myapp-vpc with us-east-1 settings', 'stack_master lint us-east-1 myapp-vpc'
|
|
143
152
|
c.action do |args, options|
|
|
144
153
|
options.default config: default_config_file
|
|
@@ -149,7 +158,7 @@ module StackMaster
|
|
|
149
158
|
command :nag do |c|
|
|
150
159
|
c.syntax = 'stack_master nag [region_or_alias] [stack_name]'
|
|
151
160
|
c.summary = "Check this stack's template with cfn_nag"
|
|
152
|
-
c.description =
|
|
161
|
+
c.description = 'Runs SAST scan cfn_nag on the template'
|
|
153
162
|
c.example 'run cfn_nag on stack myapp-vpc with us-east-1 settings', 'stack_master nag us-east-1 myapp-vpc'
|
|
154
163
|
c.action do |args, options|
|
|
155
164
|
options.default config: default_config_file
|
|
@@ -159,7 +168,7 @@ module StackMaster
|
|
|
159
168
|
|
|
160
169
|
command :compile do |c|
|
|
161
170
|
c.syntax = 'stack_master compile [region_or_alias] [stack_name]'
|
|
162
|
-
c.summary =
|
|
171
|
+
c.summary = 'Print the compiled version of a given stack'
|
|
163
172
|
c.description = "Processes the stack and prints out a compiled version - same we'd send to AWS"
|
|
164
173
|
c.example 'print compiled stack myapp-vpc with us-east-1 settings', 'stack_master compile us-east-1 myapp-vpc'
|
|
165
174
|
c.action do |args, options|
|
|
@@ -171,11 +180,13 @@ module StackMaster
|
|
|
171
180
|
command :status do |c|
|
|
172
181
|
c.syntax = 'stack_master status'
|
|
173
182
|
c.summary = 'Check the current status stacks.'
|
|
174
|
-
c.description = 'Checks the status of all stacks defined in the stack_master.yml file.
|
|
183
|
+
c.description = 'Checks the status of all stacks defined in the stack_master.yml file. ' \
|
|
184
|
+
'Warning this operation can be somewhat slow.'
|
|
175
185
|
c.example 'description', 'Check the status of all stack definitions'
|
|
176
186
|
c.action do |args, options|
|
|
177
187
|
options.default config: default_config_file
|
|
178
|
-
say
|
|
188
|
+
say 'Invalid arguments. stack_master status' and return unless args.size == 0
|
|
189
|
+
|
|
179
190
|
config = load_config(options.config)
|
|
180
191
|
StackMaster::Commands::Status.perform(config, nil, options)
|
|
181
192
|
end
|
|
@@ -184,11 +195,13 @@ module StackMaster
|
|
|
184
195
|
command :tidy do |c|
|
|
185
196
|
c.syntax = 'stack_master tidy'
|
|
186
197
|
c.summary = 'Try to identify extra & missing files.'
|
|
187
|
-
c.description = 'Cross references stack_master.yml with the template
|
|
198
|
+
c.description = 'Cross references stack_master.yml with the template ' \
|
|
199
|
+
'and parameter directories to identify extra or missing files.'
|
|
188
200
|
c.example 'description', 'Check for missing or extra files'
|
|
189
201
|
c.action do |args, options|
|
|
190
202
|
options.default config: default_config_file
|
|
191
|
-
say
|
|
203
|
+
say 'Invalid arguments. stack_master tidy' and return unless args.size == 0
|
|
204
|
+
|
|
192
205
|
config = load_config(options.config)
|
|
193
206
|
StackMaster::Commands::Tidy.perform(config, nil, options)
|
|
194
207
|
end
|
|
@@ -202,7 +215,7 @@ module StackMaster
|
|
|
202
215
|
c.action do |args, options|
|
|
203
216
|
options.default config: default_config_file
|
|
204
217
|
unless args.size == 2
|
|
205
|
-
say
|
|
218
|
+
say 'Invalid arguments. stack_master delete [region] [stack_name]'
|
|
206
219
|
return
|
|
207
220
|
end
|
|
208
221
|
|
|
@@ -230,7 +243,7 @@ module StackMaster
|
|
|
230
243
|
c.syntax = 'stack_master drift [region_or_alias] [stack_name]'
|
|
231
244
|
c.summary = 'Detects and displays stack drift using the CloudFormation Drift API'
|
|
232
245
|
c.description = 'Detects and displays stack drift'
|
|
233
|
-
c.option '--timeout SECONDS', Integer,
|
|
246
|
+
c.option '--timeout SECONDS', Integer, 'The number of seconds to wait for drift detection to complete'
|
|
234
247
|
c.example 'view stack drift for a stack named myapp-vpc in us-east-1', 'stack_master drift us-east-1 myapp-vpc'
|
|
235
248
|
c.action do |args, options|
|
|
236
249
|
options.default config: default_config_file, timeout: 120
|
|
@@ -244,7 +257,7 @@ module StackMaster
|
|
|
244
257
|
private
|
|
245
258
|
|
|
246
259
|
def default_config_file
|
|
247
|
-
|
|
260
|
+
'stack_master.yml'
|
|
248
261
|
end
|
|
249
262
|
|
|
250
263
|
def load_config(file)
|
|
@@ -268,12 +281,17 @@ module StackMaster
|
|
|
268
281
|
show_other_region_candidates(config, stack_name)
|
|
269
282
|
success = false
|
|
270
283
|
end
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
284
|
+
if options.changed
|
|
285
|
+
stack_definitions = stack_definitions.select do |stack_definition|
|
|
286
|
+
running_in_allowed_account?(stack_definition.allowed_accounts) &&
|
|
287
|
+
StackStatus.new(config, stack_definition).changed?
|
|
288
|
+
end
|
|
289
|
+
end
|
|
274
290
|
stack_definitions.each do |stack_definition|
|
|
275
291
|
StackMaster.cloud_formation_driver.set_region(stack_definition.region)
|
|
276
|
-
StackMaster.stdout.puts
|
|
292
|
+
StackMaster.stdout.puts(
|
|
293
|
+
"Executing #{command.command_name} on #{stack_definition.stack_name} in #{stack_definition.region}"
|
|
294
|
+
)
|
|
277
295
|
success = execute_if_allowed_account(stack_definition.allowed_accounts) do
|
|
278
296
|
command.perform(config, stack_definition, options).success?
|
|
279
297
|
end
|
|
@@ -283,20 +301,23 @@ module StackMaster
|
|
|
283
301
|
end
|
|
284
302
|
|
|
285
303
|
def show_other_region_candidates(config, stack_name)
|
|
286
|
-
candidates = config.filter(region=
|
|
304
|
+
candidates = config.filter(region = '', stack_name = stack_name)
|
|
287
305
|
return if candidates.empty?
|
|
288
306
|
|
|
289
307
|
StackMaster.stdout.puts "Stack name #{stack_name} exists in regions: #{candidates.map(&:region).join(', ')}"
|
|
290
308
|
end
|
|
291
309
|
|
|
292
310
|
def execute_if_allowed_account(allowed_accounts, &block)
|
|
293
|
-
raise ArgumentError,
|
|
311
|
+
raise ArgumentError, 'Block required to execute this method' unless block_given?
|
|
312
|
+
|
|
294
313
|
if running_in_allowed_account?(allowed_accounts)
|
|
295
314
|
block.call
|
|
296
315
|
else
|
|
297
316
|
account_text = "'#{identity.account}'"
|
|
298
317
|
account_text << " (#{identity.account_aliases.join(', ')})" if identity.account_aliases.any?
|
|
299
|
-
StackMaster.stdout.puts
|
|
318
|
+
StackMaster.stdout.puts(
|
|
319
|
+
"Account #{account_text} is not an allowed account. Allowed accounts are #{allowed_accounts}."
|
|
320
|
+
)
|
|
300
321
|
false
|
|
301
322
|
end
|
|
302
323
|
end
|
data/lib/stack_master/command.rb
CHANGED
|
@@ -43,11 +43,7 @@ module StackMaster
|
|
|
43
43
|
msg = "#{e.class} #{e.message}"
|
|
44
44
|
msg << "\n Caused by: #{e.cause.class} #{e.cause.message}" if e.cause
|
|
45
45
|
msg << "\n at #{e.cause.backtrace[0..3].join("\n ")}\n ..." if e.cause && !options.trace
|
|
46
|
-
|
|
47
|
-
msg << "\n#{backtrace(e)}"
|
|
48
|
-
else
|
|
49
|
-
msg << "\n Use --trace to view backtrace"
|
|
50
|
-
end
|
|
46
|
+
msg << (options.trace ? "\n#{backtrace(e)}" : "\n Use --trace to view backtrace")
|
|
51
47
|
msg
|
|
52
48
|
end
|
|
53
49
|
|
|
@@ -44,10 +44,10 @@ module StackMaster
|
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def abort_if_review_in_progress
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
return unless stack_exists? && stack.stack_status == 'REVIEW_IN_PROGRESS'
|
|
48
|
+
|
|
49
|
+
StackMaster.stderr.puts "Stack currently exists and is in #{stack.stack_status}"
|
|
50
|
+
failed! "You will need to delete the stack (#{stack.stack_name}) before continuing"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def use_s3?
|
|
@@ -89,7 +89,7 @@ module StackMaster
|
|
|
89
89
|
@change_set = ChangeSet.create(stack_options.merge(change_set_type: 'CREATE'))
|
|
90
90
|
if @change_set.failed?
|
|
91
91
|
ChangeSet.delete(@change_set.id)
|
|
92
|
-
halt!(@change_set.status_reason)
|
|
92
|
+
halt!(user_friendly_changeset_error(@change_set.status_reason))
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
@change_set.display(StackMaster.stdout)
|
|
@@ -111,11 +111,11 @@ module StackMaster
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def ask_to_cancel_stack_update
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
return unless ask?('Cancel stack update?')
|
|
115
|
+
|
|
116
|
+
StackMaster.stdout.puts 'Attempting to cancel stack update'
|
|
117
|
+
cf.cancel_update_stack(stack_name: stack_name)
|
|
118
|
+
tail_stack_events
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
def update_stack
|
|
@@ -123,7 +123,7 @@ module StackMaster
|
|
|
123
123
|
@change_set = ChangeSet.create(stack_options)
|
|
124
124
|
if @change_set.failed?
|
|
125
125
|
ChangeSet.delete(@change_set.id)
|
|
126
|
-
halt!(@change_set.status_reason)
|
|
126
|
+
halt!(user_friendly_changeset_error(@change_set.status_reason))
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
@change_set.display(StackMaster.stdout)
|
|
@@ -136,14 +136,15 @@ module StackMaster
|
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def ask_update_confirmation!
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
return if ask?('Apply change set (y/n)? ')
|
|
140
|
+
|
|
141
|
+
ChangeSet.delete(@change_set.id)
|
|
142
|
+
halt! 'Stack update aborted'
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
def upload_files
|
|
146
146
|
return unless use_s3?
|
|
147
|
+
|
|
147
148
|
s3.upload_files(**s3_options)
|
|
148
149
|
end
|
|
149
150
|
|
|
@@ -153,7 +154,8 @@ module StackMaster
|
|
|
153
154
|
|
|
154
155
|
def template_value
|
|
155
156
|
if use_s3?
|
|
156
|
-
s3.url(bucket: @s3_config['bucket'], prefix: @s3_config['prefix'], region: @s3_config['region'],
|
|
157
|
+
s3.url(bucket: @s3_config['bucket'], prefix: @s3_config['prefix'], region: @s3_config['region'],
|
|
158
|
+
template: @stack_definition.s3_template_file_name)
|
|
157
159
|
else
|
|
158
160
|
proposed_stack.template
|
|
159
161
|
end
|
|
@@ -161,6 +163,7 @@ module StackMaster
|
|
|
161
163
|
|
|
162
164
|
def files_to_upload
|
|
163
165
|
return {} unless use_s3?
|
|
166
|
+
|
|
164
167
|
@stack_definition.s3_files.tap do |files|
|
|
165
168
|
files[@stack_definition.s3_template_file_name] = {
|
|
166
169
|
path: @stack_definition.template_file_path,
|
|
@@ -174,7 +177,7 @@ module StackMaster
|
|
|
174
177
|
stack_name: stack_name,
|
|
175
178
|
parameters: proposed_stack.aws_parameters,
|
|
176
179
|
tags: proposed_stack.aws_tags,
|
|
177
|
-
capabilities: [
|
|
180
|
+
capabilities: %w[CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND],
|
|
178
181
|
role_arn: proposed_stack.role_arn,
|
|
179
182
|
notification_arns: proposed_stack.notification_arns,
|
|
180
183
|
template_method => template_value
|
|
@@ -208,9 +211,9 @@ module StackMaster
|
|
|
208
211
|
end
|
|
209
212
|
|
|
210
213
|
def ensure_valid_template_body_size!
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
return unless proposed_stack.too_big?(use_s3?)
|
|
215
|
+
|
|
216
|
+
failed! TEMPLATE_TOO_LARGE_ERROR_MESSAGE
|
|
214
217
|
end
|
|
215
218
|
|
|
216
219
|
def set_stack_policy
|
|
@@ -218,6 +221,7 @@ module StackMaster
|
|
|
218
221
|
proposed_policy = proposed_stack.stack_policy_body
|
|
219
222
|
# No need to reset a stack policy if it's nil or not changed
|
|
220
223
|
return if proposed_policy.nil? || proposed_policy == current_policy
|
|
224
|
+
|
|
221
225
|
StackMaster.stdout.print 'Setting a stack policy...'
|
|
222
226
|
cf.set_stack_policy(
|
|
223
227
|
stack_name: stack_name,
|
|
@@ -226,6 +230,21 @@ module StackMaster
|
|
|
226
230
|
StackMaster.stdout.puts 'done.'
|
|
227
231
|
end
|
|
228
232
|
|
|
233
|
+
def user_friendly_changeset_error(status_reason)
|
|
234
|
+
# CloudFormation returns various messages when there are no changes to apply
|
|
235
|
+
if status_reason =~ /didn'?t contain changes|no changes|no updates are to be performed/i
|
|
236
|
+
<<~MESSAGE.chomp
|
|
237
|
+
#{status_reason}
|
|
238
|
+
|
|
239
|
+
While there may be differences in the template file (e.g., whitespace, comments, or
|
|
240
|
+
formatting), CloudFormation has determined that no actual resource changes are needed.
|
|
241
|
+
The stack is already in the desired state.
|
|
242
|
+
MESSAGE
|
|
243
|
+
else
|
|
244
|
+
status_reason
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
229
248
|
extend Forwardable
|
|
230
249
|
def_delegators :@stack_definition, :stack_name, :region
|
|
231
250
|
end
|
|
@@ -12,11 +12,10 @@ module StackMaster
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def perform
|
|
15
|
-
|
|
16
15
|
return unless check_exists
|
|
17
16
|
|
|
18
17
|
unless ask?("Really delete stack #{@stack_name} (y/n)? ")
|
|
19
|
-
StackMaster.stdout.puts
|
|
18
|
+
StackMaster.stdout.puts 'Stack update aborted'
|
|
20
19
|
return
|
|
21
20
|
end
|
|
22
21
|
|
|
@@ -27,14 +26,14 @@ module StackMaster
|
|
|
27
26
|
private
|
|
28
27
|
|
|
29
28
|
def delete_stack
|
|
30
|
-
cf.delete_stack({stack_name: @stack_name})
|
|
29
|
+
cf.delete_stack({ stack_name: @stack_name })
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
def check_exists
|
|
34
|
-
cf.describe_stacks({stack_name: @stack_name})
|
|
33
|
+
cf.describe_stacks({ stack_name: @stack_name })
|
|
35
34
|
true
|
|
36
35
|
rescue Aws::CloudFormation::Errors::ValidationError
|
|
37
|
-
failed(
|
|
36
|
+
failed('Stack does not exist')
|
|
38
37
|
false
|
|
39
38
|
end
|
|
40
39
|
|
|
@@ -44,10 +43,10 @@ module StackMaster
|
|
|
44
43
|
|
|
45
44
|
def tail_stack_events
|
|
46
45
|
StackEvents::Streamer.stream(@stack_name, @region, io: StackMaster.stdout, from: @from_time)
|
|
47
|
-
StackMaster.stdout.puts
|
|
46
|
+
StackMaster.stdout.puts 'Stack deleted'
|
|
48
47
|
rescue Aws::CloudFormation::Errors::ValidationError
|
|
49
48
|
# Unfortunately the stack as a tendency of going away before we get the final delete event.
|
|
50
|
-
StackMaster.stdout.puts
|
|
49
|
+
StackMaster.stdout.puts 'Stack deleted'
|
|
51
50
|
end
|
|
52
51
|
end
|
|
53
52
|
end
|