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.
@@ -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