bora 1.6.0 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +21 -0
- data/README.md +15 -9
- data/Rakefile +5 -3
- data/bin/console +3 -3
- data/bora.gemspec +19 -18
- data/lib/bora.rb +11 -14
- data/lib/bora/cfn/change.rb +1 -1
- data/lib/bora/cfn/change_set.rb +8 -8
- data/lib/bora/cfn/change_set_action.rb +9 -10
- data/lib/bora/cfn/event.rb +12 -5
- data/lib/bora/cfn/output.rb +1 -3
- data/lib/bora/cfn/parameter.rb +0 -1
- data/lib/bora/cfn/stack.rb +28 -29
- data/lib/bora/cfn/stack_status.rb +3 -7
- data/lib/bora/cfn/status.rb +6 -9
- data/lib/bora/cli.rb +34 -32
- data/lib/bora/cli_base.rb +8 -8
- data/lib/bora/cli_change_set.rb +9 -10
- data/lib/bora/parameter_resolver.rb +17 -10
- data/lib/bora/parameter_resolver_loader.rb +1 -4
- data/lib/bora/resolver/ami.rb +0 -1
- data/lib/bora/resolver/cfn.rb +4 -4
- data/lib/bora/resolver/credstash.rb +8 -8
- data/lib/bora/resolver/hostedzone.rb +5 -7
- data/lib/bora/stack.rb +63 -70
- data/lib/bora/stack_tasks.rb +2 -7
- data/lib/bora/tasks.rb +20 -28
- data/lib/bora/template.rb +3 -2
- data/lib/bora/version.rb +1 -1
- metadata +17 -3
- data/.travis.yml +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af9562b301a731f8fee40274923e356d3541ca98
|
4
|
+
data.tar.gz: 3a2cc26919d1502e0e272e16a47331426b38e2d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64fb26a2e2d65fe271346fdfbdb2f0f81f100135d149314d469b7d9b6b8f006cf25d13bfeefaaaa350110e2a9cb1a5d540919080b841c36532ccd41747c9f61f
|
7
|
+
data.tar.gz: c9de80ce38c97800dd0e03e84aa5f51be532350219915a61634fa6a4cdfbbda535a798afc6eee20997add492766e50c40c47e28377c38340300c0eb1c8c556ba
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.3
|
3
|
+
Metrics/AbcSize:
|
4
|
+
Enabled: false
|
5
|
+
Metrics/CyclomaticComplexity:
|
6
|
+
Enabled: false
|
7
|
+
Metrics/BlockLength:
|
8
|
+
Enabled: false
|
9
|
+
Metrics/ClassLength:
|
10
|
+
Enabled: false
|
11
|
+
Metrics/LineLength:
|
12
|
+
Enabled: false
|
13
|
+
Metrics/MethodLength:
|
14
|
+
CountComments: false
|
15
|
+
Max: 30
|
16
|
+
Metrics/PerceivedComplexity:
|
17
|
+
Enabled: false
|
18
|
+
Style/Documentation:
|
19
|
+
Enabled: false
|
20
|
+
Style/FrozenStringLiteralComment:
|
21
|
+
Enabled: false
|
data/README.md
CHANGED
@@ -76,11 +76,14 @@ templates:
|
|
76
76
|
app:
|
77
77
|
# This template is a plain old CloudFormation JSON file
|
78
78
|
template_file: app.json
|
79
|
-
|
80
|
-
#
|
81
|
-
|
82
|
-
|
83
|
-
|
79
|
+
# Tag Key / Value pairs for the Cloudformation stack
|
80
|
+
# Tags are inherited by stack resources
|
81
|
+
tags:
|
82
|
+
Name: my-app
|
83
|
+
# Optional create stack parameters
|
84
|
+
# See - http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudFormation/Client.html#create_stack-instance_method
|
85
|
+
capabilities: [CAPABILITY_IAM] # An array of "capabilities" to be passed to the CloudFormation API
|
86
|
+
on_failure: DO_NOTHING # See CloudFormation API docs for valid values and their meanings; "disable_rollback" is also supported
|
84
87
|
# Optional. The default region for all stacks in this template.
|
85
88
|
# Overrides "default_region" at the global level.
|
86
89
|
# See below for further information.
|
@@ -91,24 +94,27 @@ templates:
|
|
91
94
|
stacks:
|
92
95
|
# The "uat" stack
|
93
96
|
uat:
|
94
|
-
|
97
|
+
|
95
98
|
params:
|
96
99
|
InstanceType: t2.micro
|
97
100
|
AMI: ami-11032472
|
98
|
-
|
99
101
|
# The "prod" stack
|
100
102
|
prod:
|
103
|
+
# Overrides template level
|
104
|
+
on_failure: DELETE
|
105
|
+
# Tags here are merged with the template
|
106
|
+
tags:
|
107
|
+
Environment: uat
|
108
|
+
# The CloudFormation parameters to pass into the stack
|
101
109
|
# Optional. The stack name to use in CloudFormation
|
102
110
|
# If you don't supply this, the name will be the template
|
103
111
|
# name concatenated with the stack name as defined in this file,
|
104
112
|
# eg: "app-prod".
|
105
113
|
cfn_stack_name: prod-application-stack
|
106
|
-
|
107
114
|
# Optional. Default region for this stack.
|
108
115
|
# Overrides "default_region" at the template level.
|
109
116
|
# See below for further information.
|
110
117
|
default_region: ap-southeast-2
|
111
|
-
|
112
118
|
params:
|
113
119
|
InstanceType: m4.xlarge
|
114
120
|
AMI: ami-11032472
|
data/Rakefile
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rubocop/rake_task'
|
3
4
|
|
4
5
|
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
RuboCop::RakeTask.new
|
5
7
|
|
6
|
-
task :
|
8
|
+
task default: :spec
|
data/bin/console
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'bora'
|
5
5
|
|
6
6
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
7
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +10,5 @@ require "bora"
|
|
10
10
|
# require "pry"
|
11
11
|
# Pry.start
|
12
12
|
|
13
|
-
require
|
13
|
+
require 'irb'
|
14
14
|
IRB.start
|
data/bora.gemspec
CHANGED
@@ -4,30 +4,31 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'bora/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'bora'
|
8
8
|
spec.version = Bora::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
9
|
+
spec.authors = ['Charles Blaxland']
|
10
|
+
spec.email = ['charles.blaxland@gmail.com']
|
11
11
|
|
12
|
-
spec.summary =
|
13
|
-
spec.homepage =
|
12
|
+
spec.summary = 'A tool (including rake tasks) for working with cloudformation stacks'
|
13
|
+
spec.homepage = 'https://github.com/ampedandwired/bora'
|
14
14
|
|
15
15
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
-
spec.bindir =
|
16
|
+
spec.bindir = 'exe'
|
17
17
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
-
spec.require_paths = [
|
18
|
+
spec.require_paths = ['lib']
|
19
19
|
spec.required_ruby_version = '>= 2.1.0'
|
20
20
|
|
21
|
-
spec.add_dependency
|
22
|
-
spec.add_dependency
|
23
|
-
spec.add_dependency
|
24
|
-
spec.add_dependency
|
25
|
-
spec.add_dependency
|
26
|
-
spec.add_dependency
|
27
|
-
spec.add_dependency
|
21
|
+
spec.add_dependency 'aws-sdk', '~> 2.0'
|
22
|
+
spec.add_dependency 'cfndsl', '~> 0.4'
|
23
|
+
spec.add_dependency 'colorize', '~> 0.7'
|
24
|
+
spec.add_dependency 'diffy', '~> 3.0'
|
25
|
+
spec.add_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_dependency 'thor', '~> 0.19'
|
27
|
+
spec.add_dependency 'hashie', '~> 3.4.6'
|
28
28
|
|
29
|
-
spec.add_development_dependency
|
30
|
-
spec.add_development_dependency
|
31
|
-
spec.add_development_dependency
|
32
|
-
spec.add_development_dependency
|
29
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
31
|
+
spec.add_development_dependency 'simplecov', '~> 0.12'
|
32
|
+
spec.add_development_dependency 'rubocop'
|
33
|
+
spec.add_development_dependency 'pry'
|
33
34
|
end
|
data/lib/bora.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
1
|
+
require 'yaml'
|
2
|
+
require 'colorize'
|
3
|
+
require 'bora/version'
|
4
|
+
require 'bora/template'
|
5
|
+
require 'bora/tasks'
|
6
6
|
|
7
7
|
class Bora
|
8
|
-
DEFAULT_CONFIG_FILE =
|
9
|
-
INHERITABLE_PROPERTIES = [
|
8
|
+
DEFAULT_CONFIG_FILE = 'bora.yml'.freeze
|
9
|
+
INHERITABLE_PROPERTIES = ['default_region'].freeze
|
10
10
|
|
11
11
|
def initialize(config_file_or_hash: DEFAULT_CONFIG_FILE, override_config: {}, colorize: true)
|
12
12
|
@templates = {}
|
13
13
|
config = load_config(config_file_or_hash)
|
14
14
|
String.disable_colorization = !colorize
|
15
|
-
raise
|
15
|
+
raise 'No templates defined' unless config['templates']
|
16
16
|
config['templates'].each do |template_name, template_config|
|
17
17
|
resolved_config = resolve_template_config(config, template_config, override_config)
|
18
18
|
@templates[template_name] = Template.new(template_name, resolved_config, override_config)
|
@@ -28,7 +28,7 @@ class Bora
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def stack(stack_name)
|
31
|
-
t = @templates.find { |_, template| template.stack(stack_name)
|
31
|
+
t = @templates.find { |_, template| !template.stack(stack_name).nil? }
|
32
32
|
t ? t[1].stack(stack_name) : nil
|
33
33
|
end
|
34
34
|
|
@@ -36,18 +36,16 @@ class Bora
|
|
36
36
|
@templates.each { |_, t| t.rake_tasks }
|
37
37
|
end
|
38
38
|
|
39
|
-
|
40
39
|
protected
|
41
40
|
|
42
41
|
def load_config(config)
|
43
42
|
if config.class == String
|
44
|
-
|
43
|
+
YAML.load_file(config)
|
45
44
|
elsif config.class == Hash
|
46
|
-
|
45
|
+
config
|
47
46
|
end
|
48
47
|
end
|
49
48
|
|
50
|
-
|
51
49
|
private
|
52
50
|
|
53
51
|
def resolve_template_config(bora_config, template_config, override_config)
|
@@ -57,5 +55,4 @@ class Bora
|
|
57
55
|
def inheritable_properties(config)
|
58
56
|
config.select { |k| INHERITABLE_PROPERTIES.include?(k) }
|
59
57
|
end
|
60
|
-
|
61
58
|
end
|
data/lib/bora/cfn/change.rb
CHANGED
data/lib/bora/cfn/change_set.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'bora/cfn/change'
|
2
|
+
require 'bora/cfn/status'
|
3
3
|
|
4
4
|
class Bora
|
5
5
|
module Cfn
|
@@ -24,19 +24,19 @@ class Bora
|
|
24
24
|
status_success? || status_failure?
|
25
25
|
end
|
26
26
|
|
27
|
-
def
|
28
|
-
@status.success? &&
|
27
|
+
def changes?
|
28
|
+
@status.success? && !@changes.empty?
|
29
29
|
end
|
30
30
|
|
31
31
|
def to_s(changes_only: false)
|
32
|
-
reason = @change_set.status_reason ? " (#{@change_set.status_reason})" :
|
33
|
-
description = @change_set.description ? " - #{@change_set.description}" :
|
34
|
-
changes_str = !@is_summary ? @changes.map(&:to_s).join("\n") :
|
32
|
+
reason = @change_set.status_reason ? " (#{@change_set.status_reason})" : ''
|
33
|
+
description = @change_set.description ? " - #{@change_set.description}" : ''
|
34
|
+
changes_str = !@is_summary ? @changes.map(&:to_s).join("\n") : ''
|
35
35
|
if changes_only
|
36
36
|
s = changes_str
|
37
37
|
else
|
38
38
|
s = "#{@change_set.change_set_name.bold} - #{@change_set.creation_time.getlocal} - #{@status}#{reason} - #{@execution_status}#{description}"
|
39
|
-
s += "\n#{changes_str}"
|
39
|
+
s += "\n#{changes_str}" unless changes_str.empty?
|
40
40
|
end
|
41
41
|
s
|
42
42
|
end
|
@@ -10,27 +10,26 @@ class Bora
|
|
10
10
|
|
11
11
|
def to_s
|
12
12
|
action_str = @action
|
13
|
-
if @action ==
|
14
|
-
action_str =
|
15
|
-
|
16
|
-
when
|
13
|
+
if @action == 'Modify'
|
14
|
+
action_str =
|
15
|
+
case @replacement
|
16
|
+
when 'True' then 'Replace'
|
17
|
+
when 'Conditional' then 'Replace (conditional)'
|
17
18
|
else action_str
|
18
|
-
|
19
|
+
end
|
19
20
|
end
|
20
21
|
action_str.colorize(color)
|
21
22
|
end
|
22
23
|
|
23
|
-
|
24
24
|
private
|
25
25
|
|
26
26
|
def color
|
27
27
|
case @action
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
when 'Add' then :green
|
29
|
+
when 'Remove' then :red
|
30
|
+
else :yellow
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
34
|
-
|
35
34
|
end
|
36
35
|
end
|
data/lib/bora/cfn/event.rb
CHANGED
@@ -2,15 +2,23 @@ require 'bora/cfn/status'
|
|
2
2
|
|
3
3
|
class Bora
|
4
4
|
module Cfn
|
5
|
-
|
6
5
|
class Event
|
7
6
|
def initialize(event)
|
8
7
|
@event = event
|
9
8
|
@status = Status.new(@event.resource_status)
|
10
9
|
end
|
11
10
|
|
12
|
-
def
|
13
|
-
|
11
|
+
def respond_to_missing?(method_name, include_private = false)
|
12
|
+
return false if method_name == :to_ary
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(method_name, *args, &block)
|
17
|
+
if method_name.to_s =~ /(.*)/
|
18
|
+
@event.send(Regexp.last_match[1], *args, &block)
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
14
22
|
end
|
15
23
|
|
16
24
|
def status_success?
|
@@ -26,10 +34,9 @@ class Bora
|
|
26
34
|
end
|
27
35
|
|
28
36
|
def to_s
|
29
|
-
status_reason = @event.resource_status_reason ? " - #{@event.resource_status_reason}" :
|
37
|
+
status_reason = @event.resource_status_reason ? " - #{@event.resource_status_reason}" : ''
|
30
38
|
"#{@event.timestamp.getlocal} - #{@event.resource_type} - #{@event.logical_resource_id} - #{@status}#{status_reason}"
|
31
39
|
end
|
32
40
|
end
|
33
|
-
|
34
41
|
end
|
35
42
|
end
|
data/lib/bora/cfn/output.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
class Bora
|
2
2
|
module Cfn
|
3
|
-
|
4
3
|
class Output
|
5
4
|
def initialize(output)
|
6
5
|
@output = output
|
@@ -15,10 +14,9 @@ class Bora
|
|
15
14
|
end
|
16
15
|
|
17
16
|
def to_s
|
18
|
-
desc = @output.description ? " (#{@output.description})" :
|
17
|
+
desc = @output.description ? " (#{@output.description})" : ''
|
19
18
|
"#{@output.output_key} - #{@output.output_value} #{desc}"
|
20
19
|
end
|
21
20
|
end
|
22
|
-
|
23
21
|
end
|
24
22
|
end
|
data/lib/bora/cfn/parameter.rb
CHANGED
data/lib/bora/cfn/stack.rb
CHANGED
@@ -9,9 +9,8 @@ require 'bora/cfn/parameter'
|
|
9
9
|
|
10
10
|
class Bora
|
11
11
|
module Cfn
|
12
|
-
|
13
12
|
class Stack
|
14
|
-
NO_UPDATE_MESSAGE =
|
13
|
+
NO_UPDATE_MESSAGE = 'No updates are to be performed'.freeze
|
15
14
|
|
16
15
|
def initialize(stack_name, region = nil)
|
17
16
|
@stack_name = stack_name
|
@@ -24,7 +23,10 @@ class Bora
|
|
24
23
|
end
|
25
24
|
|
26
25
|
def update(options, &block)
|
27
|
-
|
26
|
+
# Parameters that are not valid for the update_stack api
|
27
|
+
invalid_update_stack_options = %i(on_failure disable_rollback)
|
28
|
+
update_options = options.select { |key| !invalid_update_stack_options.include?(key) }
|
29
|
+
call_cfn_action(:update_stack, update_options, &block)
|
28
30
|
end
|
29
31
|
|
30
32
|
def create_or_update(options, &block)
|
@@ -33,7 +35,7 @@ class Bora
|
|
33
35
|
|
34
36
|
def recreate(options, &block)
|
35
37
|
delete(&block) if exists?
|
36
|
-
create(options, &block)
|
38
|
+
create(options, &block) unless exists?
|
37
39
|
end
|
38
40
|
|
39
41
|
def delete(&block)
|
@@ -41,24 +43,24 @@ class Bora
|
|
41
43
|
end
|
42
44
|
|
43
45
|
def events
|
44
|
-
return
|
45
|
-
events = cloudformation.describe_stack_events(
|
46
|
+
return unless exists?
|
47
|
+
events = cloudformation.describe_stack_events(stack_name: underlying_stack.stack_id).stack_events
|
46
48
|
events.reverse.map { |e| Event.new(e) }
|
47
49
|
end
|
48
50
|
|
49
51
|
def outputs
|
50
|
-
return
|
52
|
+
return unless exists?
|
51
53
|
underlying_stack.outputs.map { |output| Output.new(output) }
|
52
54
|
end
|
53
55
|
|
54
56
|
def parameters
|
55
|
-
return
|
57
|
+
return unless exists?
|
56
58
|
underlying_stack.parameters.map { |parameter| Parameter.new(parameter) }
|
57
59
|
end
|
58
60
|
|
59
61
|
def template
|
60
|
-
return
|
61
|
-
cloudformation.get_template(
|
62
|
+
return unless exists?
|
63
|
+
cloudformation.get_template(stack_name: @stack_name).template_body
|
62
64
|
end
|
63
65
|
|
64
66
|
def validate(options)
|
@@ -79,11 +81,11 @@ class Bora
|
|
79
81
|
change_set_name: change_set_name
|
80
82
|
}
|
81
83
|
cloudformation.create_change_set(change_set_options.merge(options))
|
82
|
-
|
84
|
+
loop do
|
83
85
|
change_set = ChangeSet.new(cloudformation.describe_change_set(change_set_options))
|
84
|
-
|
85
|
-
|
86
|
-
|
86
|
+
return change_set if change_set.status_complete?
|
87
|
+
sleep 5
|
88
|
+
end
|
87
89
|
end
|
88
90
|
|
89
91
|
def list_change_sets
|
@@ -100,10 +102,9 @@ class Bora
|
|
100
102
|
end
|
101
103
|
|
102
104
|
def execute_change_set(change_set_name, &block)
|
103
|
-
call_cfn_action(:execute_change_set, {change_set_name: change_set_name}, &block)
|
105
|
+
call_cfn_action(:execute_change_set, { change_set_name: change_set_name }, &block)
|
104
106
|
end
|
105
107
|
|
106
|
-
|
107
108
|
# =============================================================================================
|
108
109
|
private
|
109
110
|
|
@@ -118,7 +119,7 @@ class Bora
|
|
118
119
|
return true if action == :delete_stack && !exists?
|
119
120
|
@previous_event_time = last_event_time
|
120
121
|
begin
|
121
|
-
action_options = {stack_name: @stack_name}.merge(options)
|
122
|
+
action_options = { stack_name: @stack_name }.merge(options)
|
122
123
|
cloudformation.method(action.to_s.downcase).call(action_options)
|
123
124
|
wait_for_completion(&block)
|
124
125
|
rescue Aws::CloudFormation::Errors::ValidationError => e
|
@@ -129,21 +130,22 @@ class Bora
|
|
129
130
|
end
|
130
131
|
|
131
132
|
def wait_for_completion
|
132
|
-
|
133
|
+
loop do
|
133
134
|
events = unprocessed_events
|
134
135
|
events.each { |e| yield e } if block_given?
|
135
136
|
finished = events.find do |e|
|
136
137
|
e.resource_type == 'AWS::CloudFormation::Stack' && e.logical_resource_id == @stack_name && e.status_complete?
|
137
138
|
end
|
138
|
-
|
139
|
-
|
139
|
+
break if finished
|
140
|
+
sleep 10
|
141
|
+
end
|
140
142
|
underlying_stack(refresh: true)
|
141
143
|
end
|
142
144
|
|
143
145
|
def underlying_stack(refresh: false)
|
144
146
|
if !@_stack || refresh
|
145
147
|
begin
|
146
|
-
response = cloudformation.describe_stacks(
|
148
|
+
response = cloudformation.describe_stacks(stack_name: @stack_name)
|
147
149
|
@_stack = response.stacks[0]
|
148
150
|
rescue Aws::CloudFormation::Errors::ValidationError
|
149
151
|
@_stack = nil
|
@@ -153,8 +155,8 @@ class Bora
|
|
153
155
|
end
|
154
156
|
|
155
157
|
def unprocessed_events
|
156
|
-
return []
|
157
|
-
events = cloudformation.describe_stack_events(
|
158
|
+
return [] unless underlying_stack
|
159
|
+
events = cloudformation.describe_stack_events(stack_name: underlying_stack.stack_id).stack_events
|
158
160
|
unprocessed_events = events.select do |event|
|
159
161
|
!@processed_events.include?(event.event_id) && @previous_event_time < event.timestamp
|
160
162
|
end
|
@@ -163,13 +165,10 @@ class Bora
|
|
163
165
|
end
|
164
166
|
|
165
167
|
def last_event_time
|
166
|
-
return Time.at(0)
|
167
|
-
events = cloudformation.describe_stack_events(
|
168
|
-
events.
|
168
|
+
return Time.at(0) unless underlying_stack
|
169
|
+
events = cloudformation.describe_stack_events(stack_name: @stack_name).stack_events
|
170
|
+
!events.empty? ? events[0].timestamp : Time.at(0)
|
169
171
|
end
|
170
|
-
|
171
172
|
end
|
172
|
-
|
173
|
-
|
174
173
|
end
|
175
174
|
end
|