miasma-aws 0.3.12 → 0.3.14

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: 136015ad26078a11d453df779c7116887ca4198d5f1c7901d5f3f20a9dcb1bf3
4
- data.tar.gz: 149a662fb85e2dacd67427b92192732af6031a7ab23f893f351a093360403d30
3
+ metadata.gz: d0662f315f3d9820571a2dd60fee0f737bd98aaef9594c1c957b93b3523dd7c3
4
+ data.tar.gz: 2a16d55b282bb0deeba025bebdcd82d635fd4a2ef28e81977be9d07223cbc076
5
5
  SHA512:
6
- metadata.gz: 300f29b1dacc60e70f6da191d32844a539e000e7fe9754dd07424b4b832dd1201ee2750768b40efd0aca6dbfe072fe94fcc0442b19492e98f6a777d5fd65eb17
7
- data.tar.gz: e7b1c007d3d54b4147d83c3ac0975f2c26fe1593067e4c7d1a8a52ea697644f1351de7e6fc10583cecc8d651b59cb130804246ebd1a2e95fc81d317c7991787d
6
+ metadata.gz: 8238b361d641eb8618e1897586e653089021997c85c89f40ca4fe1c722f41e0acf64d92df414add3bf091e2f65a06bb56f3982db31f8b3ccde286df29a3ac6a0
7
+ data.tar.gz: 2fd7f3cc11114d0c83fee457aeebb93085849f8976583cf4f9e1d081289363d006e976cc2dc4f84b5dd659541c3664ca16764ec6855f99e3ab9ad1d883820c63
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ # v0.3.14
2
+ * [feature] Add orchestration stack planning support (#46)
3
+
1
4
  # v0.3.12
2
5
  * [enhancement] Add support for defaulting to `AWS_` env vars
3
6
  * [fix] Properly parse AWS configuration/credentials file
@@ -1,4 +1,4 @@
1
1
  module MiasmaAws
2
2
  # Current library version
3
- VERSION = Gem::Version.new("0.3.12")
3
+ VERSION = Gem::Version.new("0.3.14")
4
4
  end
@@ -419,21 +419,35 @@ module Miasma
419
419
  def custom_setup(creds)
420
420
  cred_file = load_aws_file(aws_credentials_file)
421
421
  config_file = load_aws_file(aws_config_file)
422
- file_creds = Smash.new.tap do |fc|
423
- (config_file.keys + cred_file.keys).uniq.reverse.each do |k|
424
- fc[k] = config_file.fetch(k, Smash.new).merge(
425
- cred_file.fetch(k, Smash.new)
426
- )
427
- end
422
+ profile = creds[:aws_profile_name]
423
+ profile_list = [profile].compact
424
+ new_config_creds = Smash.new
425
+ while profile
426
+ new_config_creds = config_file.fetch(profile, Smash.new).merge(
427
+ new_config_creds
428
+ )
429
+ profile = new_config_creds.delete(:source_profile)
430
+ profile_list << profile
428
431
  end
432
+ new_config_creds = config_file.fetch(:default, Smash.new).merge(
433
+ new_config_creds
434
+ )
429
435
  profile = creds[:aws_profile_name]
430
436
  new_creds = Smash.new
431
- while profile
432
- new_creds = file_creds.fetch(profile, Smash.new).merge(new_creds)
437
+ profile_list.each do |profile|
438
+ new_creds = cred_file.fetch(profile, Smash.new).merge(
439
+ new_creds
440
+ )
433
441
  profile = new_creds.delete(:source_profile)
434
442
  end
435
- new_creds = file_creds.fetch(:default, Smash.new).merge(new_creds)
436
- creds.replace(new_creds.merge(creds))
443
+ new_creds = cred_file.fetch(:default, Smash.new).merge(
444
+ new_creds
445
+ )
446
+ new_creds = new_creds.merge(new_config_creds)
447
+ new_creds.each_pair do |key, value|
448
+ creds[key] = value if creds[key].nil?
449
+ end
450
+ creds.replace(new_creds)
437
451
  if creds[:aws_iam_instance_profile]
438
452
  self.class.const_get(:ECS_TASK_PROFILE_PATH).nil? ?
439
453
  load_instance_credentials!(creds) :
@@ -24,7 +24,6 @@ module Miasma
24
24
  "stopped" => :stopped,
25
25
  ).to_smash(:freeze)
26
26
 
27
- # @todo catch bad lookup and clear model
28
27
  def server_reload(server)
29
28
  result = request(
30
29
  :method => :post,
@@ -68,7 +67,7 @@ module Miasma
68
67
  },
69
68
  )
70
69
  else
71
- raise "this doesn't even exist"
70
+ raise Error::ModelPersistError.new("Server is not persisted")
72
71
  end
73
72
  end
74
73
 
@@ -101,7 +100,7 @@ module Miasma
101
100
  },
102
101
  )
103
102
  else
104
- raise "WAT DO I DO!?"
103
+ raise Error::ModelPersistError.new("Server is not persisted")
105
104
  end
106
105
  end
107
106
 
@@ -148,30 +148,7 @@ module Miasma
148
148
  # @param stack [Models::Orchestration::Stack]
149
149
  # @return [Models::Orchestration::Stack]
150
150
  def stack_save(stack)
151
- params = Smash.new("StackName" => stack.name)
152
- if stack.dirty?(:parameters)
153
- initial_parameters = stack.data[:parameters] || {}
154
- else
155
- initial_parameters = {}
156
- end
157
- (stack.parameters || {}).each_with_index do |pair, idx|
158
- params["Parameters.member.#{idx + 1}.ParameterKey"] = pair.first
159
- if initial_parameters[pair.first] == pair.last
160
- params["Parameters.member.#{idx + 1}.UsePreviousValue"] = true
161
- else
162
- params["Parameters.member.#{idx + 1}.ParameterValue"] = pair.last
163
- end
164
- end
165
- (stack.capabilities || []).each_with_index do |cap, idx|
166
- params["Capabilities.member.#{idx + 1}"] = cap
167
- end
168
- (stack.notification_topics || []).each_with_index do |topic, idx|
169
- params["NotificationARNs.member.#{idx + 1}"] = topic
170
- end
171
- (stack.tags || {}).each_with_index do |tag, idx|
172
- params["Tags.member.#{idx + 1}.Key"] = tag.first
173
- params["Tags.member.#{idx + 1}.Value"] = tag.last
174
- end
151
+ params = common_stack_params(stack)
175
152
  if stack.custom[:stack_policy_body]
176
153
  params["StackPolicyBody"] = MultiJson.dump(stack.custom[:stack_policy_body])
177
154
  end
@@ -184,13 +161,6 @@ module Miasma
184
161
  if stack.on_failure
185
162
  params["OnFailure"] = stack.on_failure == "nothing" ? "DO_NOTHING" : stack.on_failure.upcase
186
163
  end
187
- if stack.template_url
188
- params["TemplateURL"] = stack.template_url
189
- elsif !stack.dirty?(:template) && stack.persisted?
190
- params["UsePreviousTemplate"] = true
191
- else
192
- params["TemplateBody"] = MultiJson.dump(stack.template)
193
- end
194
164
  if stack.persisted?
195
165
  result = request(
196
166
  :path => "/",
@@ -216,6 +186,252 @@ module Miasma
216
186
  end
217
187
  end
218
188
 
189
+ # Generate a new stack plan from the API
190
+ #
191
+ # @param stack [Models::Orchestration::Stack]
192
+ # @return [Models::Orchestration::Stack]
193
+ # @todo Needs to include the rolearn and resourcetypes
194
+ # at some point but more thought on how to integrate
195
+ def stack_plan(stack)
196
+ params = common_stack_params(stack)
197
+ plan_name = changeset_name(stack)
198
+ result = request(
199
+ :path => "/",
200
+ :method => :post,
201
+ :form => params.merge(Smash.new(
202
+ "Action" => "CreateChangeSet",
203
+ "ChangeSetName" => plan_name,
204
+ "StackName" => stack.name,
205
+ "ChangeSetType" => stack.persisted? ? "UPDATE" : "CREATE",
206
+ )),
207
+ )
208
+ stack.reload
209
+ # Ensure we have the same plan name in use after reload
210
+ stack.custom = stack.custom.dup
211
+ stack.custom[:plan_name] = plan_name
212
+ stack.plan
213
+ end
214
+
215
+ # Load the plan for the stack
216
+ #
217
+ # @param stack [Models::Orchestration::Stack]
218
+ # @return [Models::Orchestration::Stack::Plan]
219
+ def stack_plan_load(stack)
220
+ if stack.attributes[:plan]
221
+ plan = stack.attributes[:plan]
222
+ else
223
+ plan = Stack::Plan.new(stack, name: changeset_name(stack))
224
+ end
225
+ if stack.custom[:plan_name]
226
+ if stack.custom[:plan_name] != plan.name
227
+ plan.name = stack.custom[:plan_name]
228
+ else
229
+ plan.name = changeset_name(stack)
230
+ end
231
+ end
232
+ result = nil
233
+ Bogo::Retry.build(:linear, max_attempts: 10, wait_interval: 5, ui: Bogo::Ui.new) do
234
+ begin
235
+ result = request(
236
+ :path => "/",
237
+ :method => :post,
238
+ :form => Smash.new(
239
+ "Action" => "DescribeChangeSet",
240
+ "ChangeSetName" => plan.name,
241
+ "StackName" => stack.name,
242
+ ),
243
+ )
244
+ rescue Error::ApiError::RequestError => e
245
+ # Plan does not exist
246
+ if e.response.code == 404
247
+ return nil
248
+ end
249
+ # Stack does not exist
250
+ if e.response.code == 400 && e.message.include?("ValidationError: Stack")
251
+ return nil
252
+ end
253
+ raise
254
+ end
255
+ status = result.get(:body, "DescribeChangeSetResponse", "DescribeChangeSetResult", "ExecutionStatus")
256
+ if status != "AVAILABLE"
257
+ raise "Plan execution is not yet available"
258
+ end
259
+ end.run!
260
+ res = result.get(:body, "DescribeChangeSetResponse", "DescribeChangeSetResult")
261
+ plan.id = res["ChangeSetId"]
262
+ plan.name = res["ChangeSetName"]
263
+ plan.custom = {
264
+ :execution_status => res["ExecutionStatus"],
265
+ :stack_name => res["StackName"],
266
+ :stack_id => res["StackId"],
267
+ :status => res["Status"],
268
+ }
269
+ plan.state = res["ExecutionStatus"].downcase.to_sym
270
+ plan.parameters = Smash[
271
+ [res.get("Parameters", "member")].compact.flatten.map { |param|
272
+ [param["ParameterKey"], param["ParameterValue"]]
273
+ }
274
+ ]
275
+ plan.created_at = res["CreationTime"]
276
+ plan.template = stack_plan_template(plan, :processed)
277
+ items = {:add => [], :replace => [], :remove => [], :unknown => [], :interrupt => []}
278
+ [res.get("Changes", "member")].compact.flatten.each do |chng|
279
+ if chng["Type"] == "Resource"
280
+ item_diffs = []
281
+ [chng.get("ResourceChange", "Details", "member")].compact.flatten.each do |d|
282
+ item_path = [
283
+ d.get("Target", "Attribute"),
284
+ d.get("Target", "Name"),
285
+ ].compact
286
+ original_value = stack.template.get("Resources", chng.get("ResourceChange", "LogicalResourceId"), *item_path)
287
+ if original_value.is_a?(Hash) && (stack.parameters || {}).key?(original_value["Ref"])
288
+ original_value = stack.parameters[original_value["Ref"]]
289
+ end
290
+ new_value = plan.template.get("Resources", chng.get("ResourceChange", "LogicalResourceId"), *item_path)
291
+ if new_value.is_a?(Hash) && plan.parameters.key?(new_value["Ref"])
292
+ new_value = plan.parameters[new_value["Ref"]]
293
+ end
294
+ diff = Stack::Plan::Diff.new(
295
+ :name => item_path.join("."),
296
+ :current => original_value.inspect,
297
+ :proposed => new_value.inspect,
298
+ )
299
+
300
+ unless item_diffs.detect { |d| d.name == diff.name && d.current == diff.current && d.proposed == diff.proposed }
301
+ item_diffs << diff
302
+ end
303
+ end
304
+ type = case chng.get("ResourceChange", "Action").to_s.downcase
305
+ when "add"
306
+ :add
307
+ when "modify"
308
+ chng.get("ResourceChange", "Replacement") == "True" ?
309
+ :replace : :interrupt
310
+ when "remove"
311
+ :remove
312
+ else
313
+ :unknown
314
+ end
315
+ items[type] << Stack::Plan::Item.new(
316
+ :name => chng.get("ResourceChange", "LogicalResourceId"),
317
+ :type => chng.get("ResourceChange", "ResourceType"),
318
+ :diffs => item_diffs.sort_by(&:name),
319
+ )
320
+ end
321
+ end.compact
322
+ items.each do |type, list|
323
+ plan.send("#{type}=", list.sort_by(&:name))
324
+ end
325
+ if plan.custom[:stack_id]
326
+ stack.id = plan.custom[:stack_id]
327
+ stack.valid_state
328
+ end
329
+ stack.plan = plan.valid_state
330
+ end
331
+
332
+ def stack_plan_template(plan, state)
333
+ result = request(
334
+ :path => "/",
335
+ :method => :post,
336
+ :form => Smash.new(
337
+ "Action" => "GetTemplate",
338
+ "ChangeSetName" => plan.id,
339
+ "TemplateStage" => state.to_s.capitalize,
340
+ ),
341
+ )
342
+ MultiJson.load(result.get(:body, "GetTemplateResponse", "GetTemplateResult", "TemplateBody")).to_smash
343
+ end
344
+
345
+ # Delete the plan attached to the stack
346
+ #
347
+ # @param stack [Models::Orchestration::Stack]
348
+ # @return [Models::Orchestration::Stack]
349
+ def stack_plan_destroy(stack)
350
+ request(
351
+ :path => "/",
352
+ :method => :post,
353
+ :form => Smash.new(
354
+ "Action" => "DeleteChangeSet",
355
+ "ChangeSetName" => stack.plan.id,
356
+ "StackName" => stack.name,
357
+ ),
358
+ )
359
+ stack.plan = nil
360
+ stack.valid_state
361
+ end
362
+
363
+ # Apply the plan attached to the stack
364
+ #
365
+ # @param stack [Model::Orchestration::Stack]
366
+ # @return [Model::Orchestration::Stack]
367
+ def stack_plan_execute(stack)
368
+ request(
369
+ :path => "/",
370
+ :method => :post,
371
+ :form => Smash.new(
372
+ "Action" => "ExecuteChangeSet",
373
+ "ChangeSetName" => stack.plan.id,
374
+ "StackName" => stack.name,
375
+ ),
376
+ )
377
+ stack.reload
378
+ end
379
+
380
+ # Reload the plan
381
+ #
382
+ # @param plan [Model::Orchestration::Stack::Plan]
383
+ # @return [Model::Orchestration::Stack::Plan]
384
+ def stack_plan_reload(plan)
385
+ if plan.stack.plan == plan
386
+ stack_plan_load(plan.stack)
387
+ else
388
+ stack = Stack.new(self,
389
+ id: plan.custom[:stack_id],
390
+ name: plan.custom[:stack_name])
391
+ stack.dirty[:plan] = plan
392
+ stack_plan_load(stack)
393
+ end
394
+ end
395
+
396
+ # Load all plans associated to given stack
397
+ #
398
+ # @param stack [Models::Orchestration::Stack]
399
+ # @return [Array<Models::Orchestration::Stack::Plan>]
400
+ def stack_plan_all(stack)
401
+ all_result_pages(nil, :body,
402
+ "ListChangeSetsResponse", "ListChangeSetsResult",
403
+ "Summaries", "member") do |options|
404
+ request(
405
+ :method => :post,
406
+ :path => "/",
407
+ :form => options.merge(
408
+ Smash.new(
409
+ "Action" => "ListChangeSets",
410
+ "StackName" => stack.id || stack.name,
411
+ )
412
+ ),
413
+ )
414
+ end.map do |res|
415
+ stack = Stack.new(self,
416
+ id: res["StackId"],
417
+ name: res["StackName"])
418
+ stack.custom = {:plan_name => res["ChangeSetName"],
419
+ :plan_id => res["ChangeSetId"]}
420
+ stack.plan
421
+ end
422
+ end
423
+
424
+ # Generate changeset name given stack. This
425
+ # is a unique name for miasma and ensures only
426
+ # one changeset is used/persisted for miasma
427
+ # interactions.
428
+ #
429
+ # @param stack [Models::Orchestration::Stack]
430
+ # @return [String]
431
+ def changeset_name(stack)
432
+ stack.custom.fetch(:plan_name, "miasma-changeset-#{stack.name}")
433
+ end
434
+
219
435
  # Reload the stack data from the API
220
436
  #
221
437
  # @param stack [Models::Orchestration::Stack]
@@ -270,9 +486,8 @@ module Miasma
270
486
  "StackName" => stack.id,
271
487
  ),
272
488
  )
273
- MultiJson.load(
274
- result.get(:body, "GetTemplateResponse", "GetTemplateResult", "TemplateBody")
275
- ).to_smash
489
+ template = result.get(:body, "GetTemplateResponse", "GetTemplateResult", "TemplateBody")
490
+ template.nil? ? Smash.new : MultiJson.load(template)
276
491
  else
277
492
  Smash.new
278
493
  end
@@ -436,6 +651,47 @@ module Miasma
436
651
  event.stack.events.reload
437
652
  event.stack.events.get(event.id)
438
653
  end
654
+
655
+ # Common parameters used for stack creation/update
656
+ # requests. This is currently shared between stack
657
+ # creation and plan creation
658
+ #
659
+ # @param stack [Model::Orchestration::Stack]
660
+ # @return [Smash]
661
+ def common_stack_params(stack)
662
+ params = Smash.new("StackName" => stack.name)
663
+ if stack.dirty?(:parameters)
664
+ initial_parameters = stack.data[:parameters] || {}
665
+ else
666
+ initial_parameters = {}
667
+ end
668
+ (stack.parameters || {}).each_with_index do |pair, idx|
669
+ params["Parameters.member.#{idx + 1}.ParameterKey"] = pair.first
670
+ if initial_parameters[pair.first] == pair.last
671
+ params["Parameters.member.#{idx + 1}.UsePreviousValue"] = true
672
+ else
673
+ params["Parameters.member.#{idx + 1}.ParameterValue"] = pair.last
674
+ end
675
+ end
676
+ (stack.capabilities || []).each_with_index do |cap, idx|
677
+ params["Capabilities.member.#{idx + 1}"] = cap
678
+ end
679
+ (stack.notification_topics || []).each_with_index do |topic, idx|
680
+ params["NotificationARNs.member.#{idx + 1}"] = topic
681
+ end
682
+ (stack.tags || {}).each_with_index do |tag, idx|
683
+ params["Tags.member.#{idx + 1}.Key"] = tag.first
684
+ params["Tags.member.#{idx + 1}.Value"] = tag.last
685
+ end
686
+ if stack.template_url
687
+ params["TemplateURL"] = stack.template_url
688
+ elsif !stack.dirty?(:template) && stack.persisted?
689
+ params["UsePreviousTemplate"] = true
690
+ else
691
+ params["TemplateBody"] = MultiJson.dump(stack.template)
692
+ end
693
+ params
694
+ end
439
695
  end
440
696
  end
441
697
  end
data/miasma-aws.gemspec CHANGED
@@ -1,24 +1,24 @@
1
- $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
2
- require 'miasma-aws/version'
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + "/lib/"
2
+ require "miasma-aws/version"
3
3
  Gem::Specification.new do |s|
4
- s.name = 'miasma-aws'
4
+ s.name = "miasma-aws"
5
5
  s.version = MiasmaAws::VERSION.version
6
- s.summary = 'Smoggy AWS API'
7
- s.author = 'Chris Roberts'
8
- s.email = 'code@chrisroberts.org'
9
- s.homepage = 'https://github.com/miasma-rb/miasma-aws'
10
- s.description = 'Smoggy AWS API'
11
- s.license = 'Apache 2.0'
12
- s.require_path = 'lib'
13
- s.add_runtime_dependency 'miasma', '>= 0.3.1', '< 0.5'
14
- s.add_development_dependency 'rake', '~> 10'
15
- s.add_development_dependency 'pry'
16
- s.add_development_dependency 'vcr'
17
- s.add_development_dependency 'mocha'
18
- s.add_development_dependency 'webmock', '~> 1.23.0'
19
- s.add_development_dependency 'minitest'
20
- s.add_development_dependency 'minitest-vcr'
21
- s.add_development_dependency 'rufo'
22
- s.add_development_dependency 'rspec', '~> 3.5'
23
- s.files = Dir['lib/**/*'] + %w(miasma-aws.gemspec README.md CHANGELOG.md LICENSE)
6
+ s.summary = "Smoggy AWS API"
7
+ s.author = "Chris Roberts"
8
+ s.email = "code@chrisroberts.org"
9
+ s.homepage = "https://github.com/miasma-rb/miasma-aws"
10
+ s.description = "Smoggy AWS API"
11
+ s.license = "Apache 2.0"
12
+ s.require_path = "lib"
13
+ s.add_runtime_dependency "miasma", ">= 0.3.3", "< 0.5"
14
+ s.add_development_dependency "rake", "~> 10"
15
+ s.add_development_dependency "pry"
16
+ s.add_development_dependency "vcr"
17
+ s.add_development_dependency "mocha"
18
+ s.add_development_dependency "webmock", "~> 1.23.0"
19
+ s.add_development_dependency "minitest"
20
+ s.add_development_dependency "minitest-vcr"
21
+ s.add_development_dependency "rufo", "~> 0.3.0"
22
+ s.add_development_dependency "rspec", "~> 3.5"
23
+ s.files = Dir["lib/**/*"] + %w(miasma-aws.gemspec README.md CHANGELOG.md LICENSE)
24
24
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: miasma-aws
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.12
4
+ version: 0.3.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Roberts
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-15 00:00:00.000000000 Z
11
+ date: 2018-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: miasma
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.3.1
19
+ version: 0.3.3
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '0.5'
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 0.3.1
29
+ version: 0.3.3
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '0.5'
@@ -132,16 +132,16 @@ dependencies:
132
132
  name: rufo
133
133
  requirement: !ruby/object:Gem::Requirement
134
134
  requirements:
135
- - - ">="
135
+ - - "~>"
136
136
  - !ruby/object:Gem::Version
137
- version: '0'
137
+ version: 0.3.0
138
138
  type: :development
139
139
  prerelease: false
140
140
  version_requirements: !ruby/object:Gem::Requirement
141
141
  requirements:
142
- - - ">="
142
+ - - "~>"
143
143
  - !ruby/object:Gem::Version
144
- version: '0'
144
+ version: 0.3.0
145
145
  - !ruby/object:Gem::Dependency
146
146
  name: rspec
147
147
  requirement: !ruby/object:Gem::Requirement