stax 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|