cloudformation_wrapper 0.1.2 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20f9a4efe8adbabb2c6d7d28b9ea9e43563fd622029fe83e6c0d719060aae5ed
4
- data.tar.gz: bb5b22a94052890e4f1a634ded06598657c0cbf24923c0578e187595cc4b2f78
3
+ metadata.gz: f47bebe1f3736722886538c27e2accf2ec8443ca3b0948e9650cafe78868d50e
4
+ data.tar.gz: fc7824704acde353e69c2e5a43fc27782f513891e187b7eabf178f6c64666f4e
5
5
  SHA512:
6
- metadata.gz: 554bd6b6abd082f1395fd03a2d47568c4d2bb70cdb77f2fc19ea6e49563c6eb2b59dfda4d993bda5a0213287971f0bb6880d84ac61e6251eb452d164697a9a68
7
- data.tar.gz: 2ada52243c7cb5d439f140a6fc6f87876547230ad4122209e3ec9cf653c57ea0f63f47af62c3866687a8459a7d5ccd29c3b613a1ea1a0aaaf48f7e196df2855d
6
+ metadata.gz: 63819c959001cbb13e3fc989ef12a66aee9504cdef4d2bc6add26dfbdcd5c42065deff4716d7be18654010ffbbab49c68c8a5fbda44064997d1b5cbffa35e471
7
+ data.tar.gz: 9a419c5daefdd69cf5c5712db8e721f1decf18d11da07224c97aebf0b1994ed63aa4a3c14c3f6c495f8ef13b763744976d8515ad56858226645b88e2fc32987b
@@ -1,227 +1,227 @@
1
- module CloudFormationWrapper
2
- # Stack Manager Class
3
- # Class containing static convenience methods for deploying and managing CloudFormation Stacks.
4
- # @since 1.0
5
- class StackManager
6
- def self.deploy(options)
7
- unless options[:client]
8
- access_key_id = options[:access_key_id] || ENV['AWS_ACCESS_KEY_ID'] || ENV['ACCESS_KEY'] ||
9
- raise(ArgumentError, 'Cannot find AWS Access Key ID.')
10
-
11
- secret_access_key = options[:secret_access_key] || ENV['AWS_SECRET_ACCESS_KEY'] || ENV['SECRET_KEY'] ||
12
- raise(ArgumentError, 'Cannot find AWS Secret Key.')
13
-
14
- credentials = Aws::Credentials.new(access_key_id, secret_access_key)
15
- end
16
-
17
- region = options[:region] || ENV['AWS_REGION'] || ENV['AMAZON_REGION'] || ENV['AWS_DEFAULT_REGION'] ||
18
- raise(ArgumentError, 'Cannot find AWS Region.')
19
-
20
- verified_options = verify_options(options)
21
-
22
- cf_client = verified_options[:client] || Aws::CloudFormation::Client.new(credentials: credentials, region: region)
23
-
24
- deploy_stack(
25
- verified_options[:parameters],
26
- verified_options[:name],
27
- verified_options[:template_path],
28
- verified_options[:wait_for_stack],
29
- cf_client
30
- )
31
- end
32
-
33
- private
34
-
35
- def verify_options(options)
36
- defaults = {
37
- description: 'Deployed with CloudFormation Wrapper.', parameters: {}, wait_for_stack: true
38
- }
39
-
40
- options_with_defaults = options.reverse_merge(defaults)
41
-
42
- unless options_with_defaults[:template_path] && (options_with_defaults[:template_path].is_a? String)
43
- raise ArgumentError, 'template_path must be provided (String)'
44
- end
45
-
46
- unless options_with_defaults[:parameters] && (options_with_defaults[:parameters].is_a? Hash)
47
- raise ArgumentError, 'parameters must be provided (Hash)'
48
- end
49
-
50
- unless options_with_defaults[:client] && (options_with_defaults[:client].is_a? Aws::CloudFormation::Client)
51
- raise ArgumentError, 'If you\'re providing a client, it must be an Aws::CloudFormation::Client.'
52
- end
53
-
54
- return if options_with_defaults[:name] && (options_with_defaults[:name].is_a? String)
55
- raise ArgumentError, 'name must be provided (String)'
56
- end
57
-
58
- def deploy_stack(parameters, stack_name, template_path, cf_client, _wait)
59
- template_parameters = construct_template_parameters(parameters)
60
- client_token = ENV.fetch('BUILD_NUMBER', SecureRandom.uuid.delete('-'))
61
- change_set_type = describe_stack(stack_name, cf_client) ? 'UPDATE' : 'CREATE'
62
-
63
- create_change_set_params = {
64
- stack_name: stack_name,
65
- template_body: File.read(template_path),
66
- parameters: template_parameters,
67
- change_set_name: "ChangeSet-#{client_token}",
68
- client_token: client_token,
69
- description: ENV.fetch('BUILD_TAG', 'Stack Updates.'),
70
- change_set_type: change_set_type
71
- }
72
-
73
- change_set_id = cf_client.create_change_set(create_change_set_params).id
74
-
75
- unless wait_for_stack_change_set_creation(change_set_id, cf_client)
76
- puts "No changes required for #{stack_name}"
77
- delete_change_set(change_set_id, cf_client)
78
- return nil
79
- end
80
-
81
- list_changes(change_set_id, cf_client)
82
- time_change_set_executed = Time.now
83
- execute_change_set(change_set_id, cf_client)
84
- updated_stack = wait_for_stack_to_complete(stack_name, time_change_set_executed, cf_client)
85
- if updated_stack.stack_status == 'CREATE_COMPLETE' || updated_stack.stack_status == 'UPDATE_COMPLETE'
86
- puts "Stack finished updating: #{updated_stack.stack_status}"
87
- else
88
- puts "Stack failed to update: #{updated_stack.stack_status} (#{updated_stack.stack_status_reason})"
89
- return false
90
- end
91
- true
92
- end
93
-
94
- def construct_template_parameters(parameters)
95
- template_parameters = []
96
- parameters.each do |k, v|
97
- template_parameters.push(
98
- parameter_key: k.to_s,
99
- parameter_value: v.to_s
100
- )
101
- end
102
- template_parameters
103
- end
104
-
105
- def describe_stack(stack_name, cf_client)
106
- response = cf_client.describe_stacks(stack_name: stack_name)
107
- return false if response.stacks.length != 1
108
- return response.stacks[0]
109
- rescue Aws::CloudFormation::Errors::ServiceError
110
- return false
111
- end
112
-
113
- def wait_for_stack_change_set_creation(change_set_id, cf_client)
114
- polling_period = 1 # second
115
-
116
- puts "Waiting for the Change Set (#{change_set_id}) to be reviewed..."
117
-
118
- loop do
119
- sleep(polling_period)
120
- response = cf_client.describe_change_set(change_set_name: change_set_id)
121
- if response.status == 'CREATE_COMPLETE'
122
- puts "Change Set (#{change_set_id}) created."
123
- return true
124
- end
125
- if response.status == 'FAILED'
126
- puts "Change Set (#{change_set_id}) creation failed: #{response.status_reason}"
127
- return false
128
- end
129
- puts '...'
130
- end
131
- end
132
-
133
- def list_changes(change_set_id, cf_client)
134
- response = cf_client.describe_change_set(change_set_name: change_set_id)
135
- puts
136
- puts 'Stack Set Changes:'
137
- response.changes.each do |change|
138
- resource_change = change.resource_change
139
- puts "\t#{resource_change.action} - " \
140
- "#{resource_change.logical_resource_id} " \
141
- "aka #{resource_change.physical_resource_id} " \
142
- "(#{resource_change.resource_type})"
143
- puts "\t\tScope: #{resource_change.scope}"
144
- puts "\t\tReplacment: #{resource_change.replacement}"
145
- puts "\t\tDetails:"
146
- resource_change.details.each do |detail|
147
- puts "\t\t\tTarget: #{detail.target.attribute} - " \
148
- "#{detail.target.name} - " \
149
- "recreate:#{detail.target.requires_recreation}"
150
- puts "\t\t\tCaused By: #{detail.causing_entity}"
151
- puts "\t\t\tChange Source: #{detail.change_source}"
152
- end
153
- end
154
- puts
155
- end
156
-
157
- def execute_change_set(change_set_id, cf_client)
158
- puts 'Executing Change Set...'
159
-
160
- client_token = ENV.fetch('BUILD_NUMBER', SecureRandom.uuid.delete('-'))
161
-
162
- cf_client.execute_change_set(change_set_name: change_set_id, client_request_token: client_token)
163
- end
164
-
165
- def wait_for_stack_to_complete(stack_name, minimum_timestamp_for_events, cf_client)
166
- timestamp_width = 30
167
- logical_resource_width = 40
168
- resource_status_width = 40
169
- polling_period = 3 # seconds
170
- most_recent_event_id = ''
171
-
172
- puts
173
- puts "#{'Timestamp'.ljust(timestamp_width)} " \
174
- "#{'Logical Resource Id'.ljust(logical_resource_width)} " \
175
- "#{'Status'.ljust(resource_status_width)} "
176
-
177
- puts "#{'-'.center(timestamp_width, '-')} " \
178
- "#{'-'.center(logical_resource_width, '-')} " \
179
- "#{'-'.center(resource_status_width, '-')}"
180
-
181
- stack = {}
182
- loop do
183
- sleep(polling_period)
184
- stack = describe_stack(stack_name, cf_client)
185
- events = get_latest_events(stack_name, minimum_timestamp_for_events, most_recent_event_id, cf_client)
186
- most_recent_event_id = events[0].event_id unless events.empty?
187
- events.reverse_each do |event|
188
- line = "#{event.timestamp.to_s.ljust(timestamp_width)} " \
189
- "#{event.logical_resource_id.ljust(logical_resource_width)} " \
190
- "#{event.resource_status.ljust(resource_status_width)} "
191
- if !event.resource_status.end_with?('IN_PROGRESS') && !event.resource_status_reason.nil?
192
- line << event.resource_status_reason
193
- end
194
- puts line
195
- end
196
- break unless stack.stack_status.end_with?('IN_PROGRESS')
197
- end
198
- stack
199
- end
200
-
201
- def get_latest_events(stack_name, minimum_timestamp_for_events, most_recent_event_id, cf_client)
202
- no_new_events = false
203
- response = nil
204
- events = []
205
- loop do
206
- params = {
207
- stack_name: stack_name
208
- }
209
-
210
- params[:next_token] = response.next_token unless response.nil?
211
-
212
- response = cf_client.describe_stack_events(params)
213
-
214
- response.stack_events.each do |event|
215
- if (event.event_id == most_recent_event_id) || (event.timestamp < minimum_timestamp_for_events)
216
- no_new_events = true
217
- break
218
- end
219
- events << event
220
- end
221
-
222
- break if no_new_events || !response.next_token
223
- end
224
- events
225
- end
226
- end
227
- end
1
+ module CloudFormationWrapper
2
+ # Stack Manager Class
3
+ # Class containing static convenience methods for deploying and managing CloudFormation Stacks.
4
+ # @since 1.0
5
+ class StackManager
6
+ def self.deploy(options)
7
+ unless options[:client]
8
+ access_key_id = options[:access_key_id] || ENV['AWS_ACCESS_KEY_ID'] || ENV['ACCESS_KEY'] ||
9
+ raise(ArgumentError, 'Cannot find AWS Access Key ID.')
10
+
11
+ secret_access_key = options[:secret_access_key] || ENV['AWS_SECRET_ACCESS_KEY'] || ENV['SECRET_KEY'] ||
12
+ raise(ArgumentError, 'Cannot find AWS Secret Key.')
13
+
14
+ credentials = Aws::Credentials.new(access_key_id, secret_access_key)
15
+ end
16
+
17
+ region = options[:region] || ENV['AWS_REGION'] || ENV['AMAZON_REGION'] || ENV['AWS_DEFAULT_REGION'] ||
18
+ raise(ArgumentError, 'Cannot find AWS Region.')
19
+
20
+ verified_options = verify_options(options)
21
+
22
+ cf_client = verified_options[:client] || Aws::CloudFormation::Client.new(credentials: credentials, region: region)
23
+
24
+ deploy_stack(
25
+ verified_options[:parameters],
26
+ verified_options[:name],
27
+ verified_options[:template_path],
28
+ verified_options[:wait_for_stack],
29
+ cf_client
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ def verify_options(options)
36
+ defaults = {
37
+ description: 'Deployed with CloudFormation Wrapper.', parameters: {}, wait_for_stack: true
38
+ }
39
+
40
+ options_with_defaults = options.reverse_merge(defaults)
41
+
42
+ unless options_with_defaults[:template_path] && (options_with_defaults[:template_path].is_a? String)
43
+ raise ArgumentError, 'template_path must be provided (String)'
44
+ end
45
+
46
+ unless options_with_defaults[:parameters] && (options_with_defaults[:parameters].is_a? Hash)
47
+ raise ArgumentError, 'parameters must be provided (Hash)'
48
+ end
49
+
50
+ unless options_with_defaults[:client] && (options_with_defaults[:client].is_a? Aws::CloudFormation::Client)
51
+ raise ArgumentError, 'If you\'re providing a client, it must be an Aws::CloudFormation::Client.'
52
+ end
53
+
54
+ return if options_with_defaults[:name] && (options_with_defaults[:name].is_a? String)
55
+ raise ArgumentError, 'name must be provided (String)'
56
+ end
57
+
58
+ def deploy_stack(parameters, stack_name, template_path, cf_client, _wait)
59
+ template_parameters = construct_template_parameters(parameters)
60
+ client_token = ENV.fetch('BUILD_NUMBER', SecureRandom.uuid.delete('-'))
61
+ change_set_type = describe_stack(stack_name, cf_client) ? 'UPDATE' : 'CREATE'
62
+
63
+ create_change_set_params = {
64
+ stack_name: stack_name,
65
+ template_body: File.read(template_path),
66
+ parameters: template_parameters,
67
+ change_set_name: "ChangeSet-#{client_token}",
68
+ client_token: client_token,
69
+ description: ENV.fetch('BUILD_TAG', 'Stack Updates.'),
70
+ change_set_type: change_set_type
71
+ }
72
+
73
+ change_set_id = cf_client.create_change_set(create_change_set_params).id
74
+
75
+ unless wait_for_stack_change_set_creation(change_set_id, cf_client)
76
+ puts "No changes required for #{stack_name}"
77
+ delete_change_set(change_set_id, cf_client)
78
+ return nil
79
+ end
80
+
81
+ list_changes(change_set_id, cf_client)
82
+ time_change_set_executed = Time.now
83
+ execute_change_set(change_set_id, cf_client)
84
+ updated_stack = wait_for_stack_to_complete(stack_name, time_change_set_executed, cf_client)
85
+ if updated_stack.stack_status == 'CREATE_COMPLETE' || updated_stack.stack_status == 'UPDATE_COMPLETE'
86
+ puts "Stack finished updating: #{updated_stack.stack_status}"
87
+ else
88
+ puts "Stack failed to update: #{updated_stack.stack_status} (#{updated_stack.stack_status_reason})"
89
+ return false
90
+ end
91
+ true
92
+ end
93
+
94
+ def construct_template_parameters(parameters)
95
+ template_parameters = []
96
+ parameters.each do |k, v|
97
+ template_parameters.push(
98
+ parameter_key: k.to_s,
99
+ parameter_value: v.to_s
100
+ )
101
+ end
102
+ template_parameters
103
+ end
104
+
105
+ def describe_stack(stack_name, cf_client)
106
+ response = cf_client.describe_stacks(stack_name: stack_name)
107
+ return false if response.stacks.length != 1
108
+ return response.stacks[0]
109
+ rescue Aws::CloudFormation::Errors::ServiceError
110
+ return false
111
+ end
112
+
113
+ def wait_for_stack_change_set_creation(change_set_id, cf_client)
114
+ polling_period = 1 # second
115
+
116
+ puts "Waiting for the Change Set (#{change_set_id}) to be reviewed..."
117
+
118
+ loop do
119
+ sleep(polling_period)
120
+ response = cf_client.describe_change_set(change_set_name: change_set_id)
121
+ if response.status == 'CREATE_COMPLETE'
122
+ puts "Change Set (#{change_set_id}) created."
123
+ return true
124
+ end
125
+ if response.status == 'FAILED'
126
+ puts "Change Set (#{change_set_id}) creation failed: #{response.status_reason}"
127
+ return false
128
+ end
129
+ puts '...'
130
+ end
131
+ end
132
+
133
+ def list_changes(change_set_id, cf_client)
134
+ response = cf_client.describe_change_set(change_set_name: change_set_id)
135
+ puts
136
+ puts 'Stack Set Changes:'
137
+ response.changes.each do |change|
138
+ resource_change = change.resource_change
139
+ puts "\t#{resource_change.action} - " \
140
+ "#{resource_change.logical_resource_id} " \
141
+ "aka #{resource_change.physical_resource_id} " \
142
+ "(#{resource_change.resource_type})"
143
+ puts "\t\tScope: #{resource_change.scope}"
144
+ puts "\t\tReplacment: #{resource_change.replacement}"
145
+ puts "\t\tDetails:"
146
+ resource_change.details.each do |detail|
147
+ puts "\t\t\tTarget: #{detail.target.attribute} - " \
148
+ "#{detail.target.name} - " \
149
+ "recreate:#{detail.target.requires_recreation}"
150
+ puts "\t\t\tCaused By: #{detail.causing_entity}"
151
+ puts "\t\t\tChange Source: #{detail.change_source}"
152
+ end
153
+ end
154
+ puts
155
+ end
156
+
157
+ def execute_change_set(change_set_id, cf_client)
158
+ puts 'Executing Change Set...'
159
+
160
+ client_token = ENV.fetch('BUILD_NUMBER', SecureRandom.uuid.delete('-'))
161
+
162
+ cf_client.execute_change_set(change_set_name: change_set_id, client_request_token: client_token)
163
+ end
164
+
165
+ def wait_for_stack_to_complete(stack_name, minimum_timestamp_for_events, cf_client)
166
+ timestamp_width = 30
167
+ logical_resource_width = 40
168
+ resource_status_width = 40
169
+ polling_period = 3 # seconds
170
+ most_recent_event_id = ''
171
+
172
+ puts
173
+ puts "#{'Timestamp'.ljust(timestamp_width)} " \
174
+ "#{'Logical Resource Id'.ljust(logical_resource_width)} " \
175
+ "#{'Status'.ljust(resource_status_width)} "
176
+
177
+ puts "#{'-'.center(timestamp_width, '-')} " \
178
+ "#{'-'.center(logical_resource_width, '-')} " \
179
+ "#{'-'.center(resource_status_width, '-')}"
180
+
181
+ stack = {}
182
+ loop do
183
+ sleep(polling_period)
184
+ stack = describe_stack(stack_name, cf_client)
185
+ events = get_latest_events(stack_name, minimum_timestamp_for_events, most_recent_event_id, cf_client)
186
+ most_recent_event_id = events[0].event_id unless events.empty?
187
+ events.reverse_each do |event|
188
+ line = "#{event.timestamp.to_s.ljust(timestamp_width)} " \
189
+ "#{event.logical_resource_id.ljust(logical_resource_width)} " \
190
+ "#{event.resource_status.ljust(resource_status_width)} "
191
+ if !event.resource_status.end_with?('IN_PROGRESS') && !event.resource_status_reason.nil?
192
+ line << event.resource_status_reason
193
+ end
194
+ puts line
195
+ end
196
+ break unless stack.stack_status.end_with?('IN_PROGRESS')
197
+ end
198
+ stack
199
+ end
200
+
201
+ def get_latest_events(stack_name, minimum_timestamp_for_events, most_recent_event_id, cf_client)
202
+ no_new_events = false
203
+ response = nil
204
+ events = []
205
+ loop do
206
+ params = {
207
+ stack_name: stack_name
208
+ }
209
+
210
+ params[:next_token] = response.next_token unless response.nil?
211
+
212
+ response = cf_client.describe_stack_events(params)
213
+
214
+ response.stack_events.each do |event|
215
+ if (event.event_id == most_recent_event_id) || (event.timestamp < minimum_timestamp_for_events)
216
+ no_new_events = true
217
+ break
218
+ end
219
+ events << event
220
+ end
221
+
222
+ break if no_new_events || !response.next_token
223
+ end
224
+ events
225
+ end
226
+ end
227
+ end
@@ -1,4 +1,4 @@
1
- module CloudFormationWrapper
2
- # @!visibility private
3
- VERSION = '0.1.2'.freeze
4
- end
1
+ module CloudFormationWrapper
2
+ # @!visibility private
3
+ VERSION = '0.1.4'.freeze
4
+ end
@@ -1,9 +1,9 @@
1
- require 'cloudformation_wrapper/version'
2
-
3
- require 'aws-sdk-cloudformation'
4
- require 'active_support/core_ext/hash'
5
-
6
- require 'cloudformation_wrapper/stack_manager'
7
-
8
- STDOUT.sync
9
- STDERR.sync
1
+ require 'cloudformation_wrapper/version'
2
+
3
+ require 'aws-sdk-cloudformation'
4
+ require 'active_support/core_ext/hash'
5
+
6
+ require 'cloudformation_wrapper/stack_manager'
7
+
8
+ STDOUT.sync
9
+ STDERR.sync
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudformation_wrapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ted Armstrong
@@ -54,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
54
54
  version: '0'
55
55
  requirements: []
56
56
  rubyforge_project:
57
- rubygems_version: 2.7.1
57
+ rubygems_version: 2.7.4
58
58
  signing_key:
59
59
  specification_version: 4
60
60
  summary: Easy deployment of AWS CloudFormation stacks