stax 0.0.3 → 0.0.4
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 +5 -5
- data/README.md +205 -13
- data/lib/stax.rb +6 -1
- data/lib/stax/aws/asg.rb +1 -0
- data/lib/stax/aws/cfn.rb +64 -3
- data/lib/stax/aws/codebuild.rb +41 -0
- data/lib/stax/aws/codepipeline.rb +44 -0
- data/lib/stax/aws/dynamodb.rb +10 -0
- data/lib/stax/aws/ec2.rb +11 -0
- data/lib/stax/aws/ecr.rb +17 -0
- data/lib/stax/aws/ecs.rb +5 -5
- data/lib/stax/aws/route53.rb +51 -0
- data/lib/stax/base.rb +18 -0
- data/lib/stax/cfer.rb +43 -33
- data/lib/stax/cli.rb +3 -5
- data/lib/stax/meta.rb +18 -0
- data/lib/stax/mixin/asg.rb +1 -1
- data/lib/stax/mixin/codebuild.rb +98 -0
- data/lib/stax/mixin/codepipeline.rb +125 -0
- data/lib/stax/mixin/dynamodb.rb +17 -1
- data/lib/stax/mixin/ec2.rb +6 -1
- data/lib/stax/mixin/ecr.rb +68 -0
- data/lib/stax/mixin/ecs.rb +98 -33
- data/lib/stax/mixin/ecs/deploy.rb +49 -0
- data/lib/stax/mixin/logs.rb +73 -2
- data/lib/stax/stack.rb +8 -6
- data/lib/stax/stack/cfn.rb +49 -8
- data/lib/stax/stack/changeset.rb +88 -0
- data/lib/stax/stack/crud.rb +143 -26
- data/lib/stax/stack/imports.rb +34 -0
- data/lib/stax/stack/outputs.rb +4 -2
- data/lib/stax/stack/parameters.rb +1 -1
- data/lib/stax/stack/resources.rb +3 -3
- data/lib/stax/staxfile.rb +14 -4
- data/lib/stax/version.rb +1 -1
- metadata +12 -4
- data/lib/stax/asg.rb +0 -140
data/lib/stax/stack.rb
CHANGED
@@ -1,9 +1,6 @@
|
|
1
1
|
module Stax
|
2
2
|
class Stack < Base
|
3
3
|
|
4
|
-
class_option :resources, type: :array, default: nil, desc: 'resources IDs to allow updates'
|
5
|
-
class_option :all, type: :boolean, default: false, desc: 'DANGER: allow updates to all resources'
|
6
|
-
|
7
4
|
no_commands do
|
8
5
|
def class_name
|
9
6
|
@_class_name ||= self.class.to_s.split('::').last.downcase
|
@@ -13,16 +10,21 @@ module Stax
|
|
13
10
|
@_stack_name ||= stack_prefix + class_name
|
14
11
|
end
|
15
12
|
|
13
|
+
## list of other stacks we need to reference
|
14
|
+
def stack_imports
|
15
|
+
self.class.instance_variable_get(:@imports)
|
16
|
+
end
|
17
|
+
|
16
18
|
def exists?
|
17
|
-
Cfn.exists?(stack_name)
|
19
|
+
Aws::Cfn.exists?(stack_name)
|
18
20
|
end
|
19
21
|
|
20
22
|
def stack_status
|
21
|
-
Cfn.describe(stack_name).stack_status
|
23
|
+
Aws::Cfn.describe(stack_name).stack_status
|
22
24
|
end
|
23
25
|
|
24
26
|
def stack_notification_arns
|
25
|
-
Cfn.describe(stack_name).notification_arns
|
27
|
+
Aws::Cfn.describe(stack_name).notification_arns
|
26
28
|
end
|
27
29
|
|
28
30
|
def resource(id)
|
data/lib/stax/stack/cfn.rb
CHANGED
@@ -1,23 +1,64 @@
|
|
1
1
|
module Stax
|
2
2
|
class Stack < Base
|
3
|
-
|
3
|
+
|
4
|
+
no_commands do
|
5
|
+
|
6
|
+
def event_fields(e)
|
7
|
+
[e.timestamp, color(e.resource_status, Aws::Cfn::COLORS), e.resource_type, e.logical_resource_id, e.resource_status_reason]
|
8
|
+
end
|
9
|
+
|
10
|
+
def print_events(events)
|
11
|
+
events.reverse.each do |e|
|
12
|
+
puts "%s %-44s %-40s %-20s %s" % event_fields(e)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
4
17
|
|
5
18
|
desc 'template', 'get template of existing stack from cloudformation'
|
6
19
|
method_option :pretty, type: :boolean, default: true, desc: 'format json output'
|
7
20
|
def template
|
8
|
-
Cfn.template(stack_name).tap { |t|
|
21
|
+
Aws::Cfn.template(stack_name).tap { |t|
|
9
22
|
puts options[:pretty] ? JSON.pretty_generate(JSON.parse(t)) : t
|
10
23
|
}
|
11
24
|
end
|
12
25
|
|
13
26
|
desc 'events', 'show all events for stack'
|
14
|
-
method_option :number, aliases: '-n', type: :numeric, default:
|
27
|
+
method_option :number, aliases: '-n', type: :numeric, default: 0, desc: 'show n most recent events'
|
15
28
|
def events
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
29
|
+
print_events(Aws::Cfn.events(stack_name)[0..options[:number]-1])
|
30
|
+
rescue ::Aws::CloudFormation::Errors::ValidationError => e
|
31
|
+
puts e.message
|
32
|
+
end
|
33
|
+
|
34
|
+
desc 'tail', 'tail stack events'
|
35
|
+
method_option :number, aliases: '-n', type: :numeric, default: nil, desc: 'number of historic events to show'
|
36
|
+
def tail
|
37
|
+
trap('SIGINT', 'EXIT') # clean exit with ctrl-c
|
38
|
+
|
39
|
+
## print some historical events
|
40
|
+
events = Aws::Cfn.events(stack_name).first(options[:number] || 1)
|
41
|
+
return unless events
|
42
|
+
print_events(events)
|
43
|
+
last_seen = events&.first&.event_id
|
44
|
+
|
45
|
+
loop do
|
46
|
+
sleep(1)
|
47
|
+
events = []
|
48
|
+
|
49
|
+
Aws::Cfn.events(stack_name).each do |e|
|
50
|
+
(last_seen == e.event_id) ? break : events << e
|
51
|
+
end
|
52
|
+
|
53
|
+
unless events.empty?
|
54
|
+
print_events(events)
|
55
|
+
last_seen = events.first.event_id
|
56
|
+
end
|
57
|
+
|
58
|
+
break if Aws::Cfn.describe(stack_name).stack_status.end_with?('COMPLETE', 'FAILED')
|
59
|
+
end
|
60
|
+
rescue ::Aws::CloudFormation::Errors::ValidationError => e
|
61
|
+
puts e.message
|
21
62
|
end
|
22
63
|
|
23
64
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Stax
|
2
|
+
class Stack < Base
|
3
|
+
|
4
|
+
no_commands do
|
5
|
+
|
6
|
+
## set this in stack to force changesets on update
|
7
|
+
def stack_force_changeset
|
8
|
+
false
|
9
|
+
end
|
10
|
+
|
11
|
+
## can be anything unique
|
12
|
+
def change_set_name
|
13
|
+
stack_name + '-' + Time.now.strftime('%Y%m%d%H%M%S')
|
14
|
+
end
|
15
|
+
|
16
|
+
## create a change set to update existing stack
|
17
|
+
def change_set_update
|
18
|
+
Aws::Cfn.changeset(
|
19
|
+
stack_name: stack_name,
|
20
|
+
template_body: cfn_template_body,
|
21
|
+
template_url: cfn_template_url,
|
22
|
+
parameters: cfn_parameters_update,
|
23
|
+
capabilities: cfn_capabilities,
|
24
|
+
notification_arns: cfer_notification_arns,
|
25
|
+
change_set_name: change_set_name,
|
26
|
+
change_set_type: :UPDATE,
|
27
|
+
).id
|
28
|
+
rescue ::Aws::CloudFormation::Errors::ValidationError => e
|
29
|
+
fail_task(e.message)
|
30
|
+
end
|
31
|
+
|
32
|
+
## wait and return true if changeset ready for execute
|
33
|
+
def change_set_complete?(id)
|
34
|
+
begin
|
35
|
+
Aws::Cfn.client.wait_until(:change_set_create_complete, stack_name: stack_name, change_set_name: id) { |w| w.delay = 1 }
|
36
|
+
rescue ::Aws::Waiters::Errors::FailureStateError => e
|
37
|
+
false # no changes to apply
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
## string to print for replacement flag
|
42
|
+
def change_set_replacement(string)
|
43
|
+
case string
|
44
|
+
when 'True' then 'Replace'
|
45
|
+
when 'Conditional' then 'May replace'
|
46
|
+
else ''
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
## display planned changes
|
51
|
+
def change_set_changes(id)
|
52
|
+
debug("Changes to #{stack_name}")
|
53
|
+
print_table Aws::Cfn.changes(stack_name: stack_name, change_set_name: id).map { |c|
|
54
|
+
r = c.resource_change
|
55
|
+
replacement = set_color(change_set_replacement(r.replacement), :red)
|
56
|
+
[color(r.action, Aws::Cfn::COLORS), r.logical_resource_id, r.physical_resource_id, r.resource_type, replacement]
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
## confirm and execute the change set
|
61
|
+
def change_set_execute(id)
|
62
|
+
if yes?("Apply these changes to stack #{stack_name}?", :yellow)
|
63
|
+
Aws::Cfn.execute(stack_name: stack_name, change_set_name: id)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def change_set_unlock
|
68
|
+
Aws::Cfn.set_policy(stack_name: stack_name, stack_policy_body: stack_policy_during_update)
|
69
|
+
end
|
70
|
+
|
71
|
+
def change_set_lock
|
72
|
+
Aws::Cfn.set_policy(stack_name: stack_name, stack_policy_body: stack_policy)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
desc 'change', 'create and execute a changeset'
|
77
|
+
def change
|
78
|
+
id = change_set_update
|
79
|
+
change_set_complete?(id) || fail_task('No changes')
|
80
|
+
change_set_changes(id)
|
81
|
+
change_set_unlock
|
82
|
+
change_set_execute(id) && tail && update_warn_imports
|
83
|
+
ensure
|
84
|
+
change_set_lock
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
data/lib/stax/stack/crud.rb
CHANGED
@@ -1,10 +1,16 @@
|
|
1
1
|
module Stax
|
2
2
|
class Stack < Base
|
3
3
|
|
4
|
-
class_option :resources, type: :array, default: nil, desc: 'resources IDs to allow updates'
|
5
|
-
class_option :all, type: :boolean, default: false, desc: 'DANGER: allow updates to all resources'
|
6
|
-
|
7
4
|
no_commands do
|
5
|
+
|
6
|
+
## by default we pass names of imported stacks;
|
7
|
+
## you are encouraged to override or extend this method
|
8
|
+
def cfn_parameters
|
9
|
+
stack_imports.each_with_object({}) do |i, h|
|
10
|
+
h[i.to_sym] = stack(i).stack_name
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
8
14
|
## policy to lock the stack to all updates
|
9
15
|
def stack_policy
|
10
16
|
{
|
@@ -14,26 +20,19 @@ module Stax
|
|
14
20
|
Principal: '*',
|
15
21
|
Resource: '*'
|
16
22
|
]
|
17
|
-
}
|
23
|
+
}.to_json
|
18
24
|
end
|
19
25
|
|
20
|
-
## temporary policy during updates
|
26
|
+
## temporary policy during updates; modify this to restrict resources
|
21
27
|
def stack_policy_during_update
|
22
28
|
{
|
23
29
|
Statement: [
|
24
30
|
Effect: 'Allow',
|
25
31
|
Action: 'Update:*',
|
26
32
|
Principal: '*',
|
27
|
-
Resource:
|
33
|
+
Resource: '*'
|
28
34
|
]
|
29
|
-
}
|
30
|
-
end
|
31
|
-
|
32
|
-
## resources to unlock during update
|
33
|
-
def stack_update_resources
|
34
|
-
(options[:all] ? ['*'] : options[:resources]).map do |r|
|
35
|
-
"LogicalResourceId/#{r}"
|
36
|
-
end
|
35
|
+
}.to_json
|
37
36
|
end
|
38
37
|
|
39
38
|
## cleanup sometimes needs to wait
|
@@ -45,39 +44,148 @@ module Stax
|
|
45
44
|
break unless exists?
|
46
45
|
end
|
47
46
|
end
|
47
|
+
|
48
|
+
def cfn_parameters_create
|
49
|
+
cfn_parameters.map do |k,v|
|
50
|
+
{ parameter_key: k, parameter_value: v }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def cfn_parameters_update
|
55
|
+
cfn_parameters.map do |k,v|
|
56
|
+
if options[:use_previous_value].include?(k.to_s)
|
57
|
+
{ parameter_key: k, use_previous_value: true }
|
58
|
+
else
|
59
|
+
{ parameter_key: k, parameter_value: v }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
## memoized template
|
65
|
+
def cfn_template
|
66
|
+
@_cfn_template ||= cfer_generate
|
67
|
+
end
|
68
|
+
|
69
|
+
## set this to always do an S3 upload of template
|
70
|
+
def cfn_force_s3?
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
## decide if we are uploading template to S3
|
75
|
+
def cfn_use_s3?
|
76
|
+
cfn_force_s3? || (cfn_template.bytesize > 51200)
|
77
|
+
end
|
78
|
+
|
79
|
+
## set this for template uploads as needed, e.g. s3://bucket-name/stax/#{stack_name}"
|
80
|
+
def cfn_s3_path
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
## upload template to S3 and return public url of new object
|
85
|
+
def cfn_s3_upload
|
86
|
+
fail_task('No S3 bucket set for template upload: please set cfn_s3_path') unless cfn_s3_path
|
87
|
+
uri = URI(cfn_s3_path)
|
88
|
+
obj = ::Aws::S3::Object.new(bucket_name: uri.host, key: uri.path.sub(/^\//, ''))
|
89
|
+
obj.put(body: cfn_template)
|
90
|
+
obj.public_url + ((v = obj.version_id) ? "?versionId=#{v}" : '')
|
91
|
+
end
|
92
|
+
|
93
|
+
## template body, or nil if uploading to S3
|
94
|
+
def cfn_template_body
|
95
|
+
@_cfn_template_body ||= cfn_use_s3? ? nil : cfn_template
|
96
|
+
end
|
97
|
+
|
98
|
+
## template S3 URL, or nil if not uploading to S3
|
99
|
+
def cfn_template_url
|
100
|
+
@_cfn_template_url ||= cfn_use_s3? ? cfn_s3_upload : nil
|
101
|
+
end
|
102
|
+
|
103
|
+
## validate template, and return list of require capabilities
|
104
|
+
def cfn_capabilities
|
105
|
+
validate.capabilities
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
desc 'validate', 'validate template'
|
111
|
+
def validate
|
112
|
+
Aws::Cfn.validate(
|
113
|
+
template_body: cfn_template_body,
|
114
|
+
template_url: cfn_template_url,
|
115
|
+
)
|
116
|
+
rescue ::Aws::CloudFormation::Errors::ValidationError => e
|
117
|
+
fail_task(e.message)
|
48
118
|
end
|
49
119
|
|
50
120
|
desc 'create', 'create stack'
|
51
121
|
def create
|
52
|
-
fail_task("Stack #{stack_name} already exists") if exists?
|
53
122
|
debug("Creating stack #{stack_name}")
|
54
|
-
|
123
|
+
|
124
|
+
## ensure stacks we import exist
|
125
|
+
ensure_stack(*stack_imports)
|
126
|
+
|
127
|
+
## create the stack
|
128
|
+
Aws::Cfn.create(
|
129
|
+
stack_name: stack_name,
|
130
|
+
template_body: cfn_template_body,
|
131
|
+
template_url: cfn_template_url,
|
132
|
+
parameters: cfn_parameters_create,
|
133
|
+
capabilities: cfn_capabilities,
|
134
|
+
stack_policy_body: stack_policy,
|
135
|
+
notification_arns: cfer_notification_arns,
|
136
|
+
enable_termination_protection: cfer_termination_protection,
|
137
|
+
)
|
138
|
+
|
139
|
+
## show stack events
|
140
|
+
tail
|
141
|
+
rescue ::Aws::CloudFormation::Errors::AlreadyExistsException => e
|
142
|
+
fail_task(e.message)
|
143
|
+
rescue ::Aws::CloudFormation::Errors::ValidationError => e
|
144
|
+
warn(e.message)
|
55
145
|
end
|
56
146
|
|
57
147
|
desc 'update', 'update stack'
|
58
148
|
def update
|
59
|
-
|
149
|
+
return change if stack_force_changeset
|
60
150
|
debug("Updating stack #{stack_name}")
|
61
|
-
|
151
|
+
Aws::Cfn.update(
|
152
|
+
stack_name: stack_name,
|
153
|
+
template_body: cfn_template_body,
|
154
|
+
template_url: cfn_template_url,
|
155
|
+
parameters: cfn_parameters_update,
|
156
|
+
capabilities: cfn_capabilities,
|
157
|
+
stack_policy_during_update_body: stack_policy_during_update,
|
158
|
+
notification_arns: cfer_notification_arns,
|
159
|
+
)
|
160
|
+
tail
|
161
|
+
update_warn_imports
|
162
|
+
rescue ::Aws::CloudFormation::Errors::ValidationError => e
|
163
|
+
warn(e.message)
|
62
164
|
end
|
63
165
|
|
64
166
|
desc 'delete', 'delete stack'
|
65
167
|
def delete
|
168
|
+
delete_warn_imports
|
66
169
|
if yes? "Really delete stack #{stack_name}?", :yellow
|
67
|
-
Cfn.delete(stack_name)
|
170
|
+
Aws::Cfn.delete(stack_name)
|
171
|
+
tail
|
68
172
|
end
|
69
173
|
rescue ::Aws::CloudFormation::Errors::ValidationError => e
|
70
174
|
fail_task(e.message)
|
71
175
|
end
|
72
176
|
|
73
|
-
desc '
|
74
|
-
def
|
75
|
-
|
177
|
+
desc 'cancel', 'cancel update_in_progress'
|
178
|
+
def cancel
|
179
|
+
debug("Cancelling update for #{stack_name}")
|
180
|
+
Aws::Cfn.cancel(stack_name)
|
181
|
+
tail
|
182
|
+
rescue ::Aws::CloudFormation::Errors::ValidationError => e
|
183
|
+
fail_task(e.message)
|
76
184
|
end
|
77
185
|
|
78
186
|
desc 'generate', 'generate cloudformation template'
|
79
187
|
def generate
|
80
|
-
cfer_generate
|
188
|
+
puts cfer_generate
|
81
189
|
end
|
82
190
|
|
83
191
|
desc 'protection', 'show/set termination protection for stack'
|
@@ -85,12 +193,21 @@ module Stax
|
|
85
193
|
method_option :disable, aliases: '-d', type: :boolean, default: nil, desc: 'disable termination protection'
|
86
194
|
def protection
|
87
195
|
if options[:enable]
|
88
|
-
Cfn.protection(stack_name, true)
|
196
|
+
Aws::Cfn.protection(stack_name, true)
|
89
197
|
elsif options[:disable]
|
90
|
-
Cfn.protection(stack_name, false)
|
198
|
+
Aws::Cfn.protection(stack_name, false)
|
91
199
|
end
|
92
200
|
debug("Termination protection for #{stack_name}")
|
93
|
-
puts Cfn.describe(stack_name)&.enable_termination_protection
|
201
|
+
puts Aws::Cfn.describe(stack_name)&.enable_termination_protection
|
202
|
+
end
|
203
|
+
|
204
|
+
desc 'policy [JSON]', 'get/set stack policy'
|
205
|
+
def policy(json = nil)
|
206
|
+
if json
|
207
|
+
Aws::Cfn.set_policy(stack_name: stack_name, stack_policy_body: json)
|
208
|
+
else
|
209
|
+
puts Aws::Cfn.get_policy(stack_name: stack_name)
|
210
|
+
end
|
94
211
|
end
|
95
212
|
|
96
213
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Stax
|
2
|
+
class Stack < Base
|
3
|
+
|
4
|
+
no_commands do
|
5
|
+
def import_stacks
|
6
|
+
@_import_stacks ||= Aws::Cfn.exports(stack_name).map do |e|
|
7
|
+
Aws::Cfn.imports(e.export_name)
|
8
|
+
end.flatten.uniq
|
9
|
+
end
|
10
|
+
|
11
|
+
def update_warn_imports
|
12
|
+
unless import_stacks.empty?
|
13
|
+
warn("You may also need to update stacks that import from this one: #{import_stacks.join(',')}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete_warn_imports
|
18
|
+
unless import_stacks.empty?
|
19
|
+
warn("The following stacks import from this one: #{import_stacks.join(',')}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'imports', 'list imports from this stack'
|
25
|
+
def imports
|
26
|
+
debug("Stacks that import from #{stack_name}")
|
27
|
+
print_table Aws::Cfn.exports(stack_name).map { |e|
|
28
|
+
imports = (i = Aws::Cfn.imports(e.export_name)).empty? ? '-' : i.join(',')
|
29
|
+
[e.output_key, imports]
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|