stax 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)
@@ -1,23 +1,64 @@
1
1
  module Stax
2
2
  class Stack < Base
3
- include Aws
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: nil, desc: 'show n most recent events'
27
+ method_option :number, aliases: '-n', type: :numeric, default: 0, desc: 'show n most recent events'
15
28
  def events
16
- print_table Cfn.events(stack_name).tap { |events|
17
- events.replace(events.first(options[:number])) if options[:number]
18
- }.reverse.map { |e|
19
- [e.timestamp, color(e.resource_status, Cfn::COLORS), e.resource_type, e.logical_resource_id, e.resource_status_reason]
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
@@ -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: stack_update_resources
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
- cfer_converge(stack_policy: stack_policy)
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
- fail_task("Stack #{stack_name} does not exist") unless exists?
149
+ return change if stack_force_changeset
60
150
  debug("Updating stack #{stack_name}")
61
- cfer_converge(stack_policy_during_update: stack_policy_during_update)
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 'tail', 'tail stack events'
74
- def tail
75
- cfer_tail
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