miasma-aws 0.3.12 → 0.3.14

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: 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