cloudformation_wrapper 0.1.2 → 0.1.4

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