bora 1.6.0 → 1.7.0
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 +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
|