Class: CfnStatus

Inherits:
Object
  • Object
show all
Includes:
AwsService
Defined in:
lib/cfn_status.rb,
lib/cfn_status/version.rb,
lib/cfn_status/aws_service.rb,
lib/cfn_status/rollback_stack.rb
more...

Defined Under Namespace

Modules: AwsService Classes: Error, RollbackStack

Constant Summary collapse

VERSION =
"0.6.1"

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from AwsService

#aws_options, #cfn, #find_stack, #rollback_complete?, #stack_exists?

Constructor Details

#initialize(stack_name, options = {}) ⇒ CfnStatus

Returns a new instance of CfnStatus.

[View source]

11
12
13
14
15
16
17
18
# File 'lib/cfn_status.rb', line 11

def initialize(stack_name, options = {})
  @stack_name = stack_name
  @options = options
  @cfn = options[:cfn] # allow use of different cfn client. can be useful multiple cfn clients and with different regions
  resp = cfn.describe_stacks(stack_name: @stack_name)
  @stack = resp.stacks.first
  reset
end

Instance Attribute Details

#eventsObject (readonly)

Returns the value of attribute events.


10
11
12
# File 'lib/cfn_status.rb', line 10

def events
  @events
end

#stackObject (readonly)

Returns the value of attribute stack.


10
11
12
# File 'lib/cfn_status.rb', line 10

def stack
  @stack
end

Instance Method Details

#add_events_pages(resp, index_method) ⇒ Object

Examples:

add_events_pages(:start_index)
add_events_pages(:last_shown_index)

if index_method is start_index

loops add_events_pagess through describe_stack_events until "User Initiated" is found

if index_method is last_shown_index

loops add_events_pagess through describe_stack_events until last_shown_index is found
[View source]

186
187
188
189
190
191
192
193
# File 'lib/cfn_status.rb', line 186

def add_events_pages(resp, index_method)
  found = !!send(index_method)
  until found
    resp = cfn.describe_stack_events(stack_name: @stack_name, next_token: resp["next_token"])
    @events += resp["stack_events"]
    found = !!send(index_method)
  end
end

#completed?Boolean

Returns:

  • (Boolean)
[View source]

99
100
101
102
103
# File 'lib/cfn_status.rb', line 99

def completed?
  last_event_status =~ /(_COMPLETE|_FAILED)$/ &&
    @events.dig(0, "logical_resource_id") == @stack_name &&
    @events.dig(0, "resource_type") == "AWS::CloudFormation::Stack"
end

#event_time(timestamp) ⇒ Object

[View source]

147
148
149
# File 'lib/cfn_status.rb', line 147

def event_time(timestamp)
  Time.parse(timestamp.to_s).localtime.strftime("%I:%M:%S%p")
end

#find_update_failed_eventObject

[View source]

231
232
233
234
235
236
237
238
239
240
# File 'lib/cfn_status.rb', line 231

def find_update_failed_event
  i = @events.find_index do |event|
    event["resource_type"] == "AWS::CloudFormation::Stack" &&
      event["resource_status_reason"] == "User Initiated"
  end

  @events[0..i].reverse.find do |e|
    e["resource_status"] == "UPDATE_FAILED"
  end
end

#handle_rollback!Object

[View source]

277
278
279
# File 'lib/cfn_status.rb', line 277

def handle_rollback!
  CfnStatus::RollbackStack.handle!(@stack_name, cfn: cfn)
end

#in_progress?Boolean

Returns:

  • (Boolean)
[View source]

41
42
43
44
# File 'lib/cfn_status.rb', line 41

def in_progress?
  in_progress = stack.stack_status =~ /_IN_PROGRESS$/
  !!in_progress
end

#last_event_statusObject

[View source]

105
106
107
# File 'lib/cfn_status.rb', line 105

def last_event_status
  @events.dig(0, "resource_status")
end

#last_shown_indexObject

[View source]

208
209
210
211
212
# File 'lib/cfn_status.rb', line 208

def last_shown_index
  @events.find_index do |event|
    event["event_id"] == @last_shown_event_id
  end
end

#messages_mapObject

[View source]

258
259
260
261
262
263
264
# File 'lib/cfn_status.rb', line 258

def messages_map
  {
    /CloudFormation cannot update a stack when a custom-named resource requires replacing/ => "A workaround is to run ufo again with STATIC_NAME=0 and to switch to dynamic names for resources. Then run ufo again with STATIC_NAME=1 to get back to statically name resources. Note, there are caveats with the workaround.",
    /cannot be associated with more than one load balancer/ => "There's was an issue updating the stack. Target groups can only be associated with one load balancer at a time. The workaround for this is to use UFO_FORCE_TARGET_GROUP=1 and run the command again. This will force the recreation of the target group resource.",
    /SetSubnets is not supported for load balancers of type/ => "Changing subnets for Network Load Balancers is currently not supported. You can try workarouding this with UFO_FORCE_ELB=1 and run the command again. This will force the recreation of the elb resource."
  }
end

#pretty_time(total_seconds) ⇒ Object

[View source]

267
268
269
270
271
272
273
274
275
# File 'lib/cfn_status.rb', line 267

def pretty_time(total_seconds)
  minutes = (total_seconds / 60) % 60
  seconds = total_seconds % 60
  if total_seconds < 60
    "#{seconds.to_i}s"
  else
    "#{minutes.to_i}m #{seconds.to_i}s"
  end
end
[View source]

134
135
136
137
138
139
140
141
142
143
144
# File 'lib/cfn_status.rb', line 134

def print_event(e)
  message = [
    event_time(e["timestamp"]),
    e["resource_status"],
    e["resource_type"],
    e["logical_resource_id"],
    e["resource_status_reason"]
  ].join(" ")
  message = message.color(:red) if /_FAILED/.match?(e["resource_status"])
  puts message
end
[View source]

125
126
127
128
129
130
131
132
# File 'lib/cfn_status.rb', line 125

def print_events(i)
  @events[0..i].reverse_each do |e|
    print_event(e)
  end

  @last_shown_event_id = @events.dig(0, "event_id")
  # puts "@last_shown_event_id #{@last_shown_event_id.inspect}"
end

#refresh_eventsObject

Refreshes the @events in memory.

[View source]

153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/cfn_status.rb', line 153

def refresh_events
  resp = cfn.describe_stack_events(stack_name: @stack_name)
  @events = resp["stack_events"]

  # refresh_events uses add_events_pages and resp["next_token"] to load all events until:
  #
  #     1. @last_shown_event_id found - if @last_shown_event_id is set
  #     2. User Initiated Event found - fallback when @last_shown_event_id is not set
  #
  if @last_shown_event_id
    add_events_pages(resp, :last_shown_index)
  else
    add_events_pages(resp, :start_index)
  end
rescue Aws::CloudFormation::Errors::ValidationError => e
  if /Stack .* does not exis/.match?(e.message)
    @stack_deletion_completed = true
  else
    raise
  end
end

#resetObject

[View source]

46
47
48
49
50
# File 'lib/cfn_status.rb', line 46

def reset
  @events = [] # constantly replaced with recent events
  @last_shown_event_id = nil
  @stack_deletion_completed = nil
end

#rollback_error_messageObject

[View source]

242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/cfn_status.rb', line 242

def rollback_error_message
  return unless update_rollback?

  event = find_update_failed_event
  return unless event

  reason = event["resource_status_reason"]
  messages_map.each do |pattern, message|
    if reason&.match?(pattern)
      return message
    end
  end

  reason # default message is original reason if not found in messages map
end

#runObject

[View source]

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/cfn_status.rb', line 20

def run
  unless stack_exists?(@stack_name)
    puts "The stack #{@stack_name.color(:green)} does not exist."
    return true
  end

  puts "Stack #{@stack_name.color(:green)} Status: #{stack.stack_status.color(:green)}"
  if in_progress?
    puts "Stack events (tailing):"
    # tail all events until done
    @hide_time_took = true
    wait
  else
    puts "Stack events:"
    # show the last events that was user initiated
    refresh_events
    show_events(final: true)
  end
  success?
end

#show_events(final: false, quiet: false) ⇒ Object

Only shows new events

[View source]

110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/cfn_status.rb', line 110

def show_events(final: false, quiet: false)
  if @last_shown_event_id.nil?
    i = start_index
    print_events(i) unless quiet
  else
    i = last_shown_index
    # puts "last_shown index #{i}"
    print_events(i - 1) unless i == 0 || quiet
  end

  return if final
  sleep 5 unless ENV["TEST"]
  refresh_events
end

#show_took(start_time) ⇒ Object

[View source]

91
92
93
94
95
96
97
# File 'lib/cfn_status.rb', line 91

def show_took(start_time)
  # @hide_time_took set in run
  return if @hide_time_took
  return unless @options[:show_took]
  took = Time.now - start_time
  puts "Time took: #{pretty_time(took).color(:green)}"
end

#start_indexObject

Should always find a “User Initiated” stack event when @last_shown_index is not set

[View source]

196
197
198
199
200
201
202
203
204
205
206
# File 'lib/cfn_status.rb', line 196

def start_index
  start_index_before_delete = @options[:start_index_before_delete]

  @events.find_index do |event|
    skip = start_index_before_delete && event["resource_status"] == "DELETE_IN_PROGRESS"

    event["resource_type"] == "AWS::CloudFormation::Stack" &&
      event["resource_status_reason"] == "User Initiated" &&
      !skip
  end
end

#success?Boolean

Returns:

  • (Boolean)
[View source]

214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/cfn_status.rb', line 214

def success?
  resource_status = @events.dig(0, "resource_status")
  if resource_status.nil? # not called as a part of wait
    resp = cfn.describe_stacks(stack_name: @stack_name)
    status = resp.stacks.first.stack_status
  else
    status = resource_status
  end

  successes = %w[CREATE_COMPLETE UPDATE_COMPLETE]
  successes.include?(status)
end

#update_rollback?Boolean

Returns:

  • (Boolean)
[View source]

227
228
229
# File 'lib/cfn_status.rb', line 227

def update_rollback?
  @events.dig(0, "resource_status") == "UPDATE_ROLLBACK_COMPLETE"
end

#wait(quiet: false) ⇒ Object

check for /(_COMPLETE|_FAILED)$/ status

[View source]

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/cfn_status.rb', line 53

def wait(quiet: false)
  # Check for in progress again in case .wait is called from other libraries like s3-antivirus
  # Showing the event messages when will show old messages which can be confusing.
  return success? unless in_progress?

  puts "Waiting for stack to complete" unless quiet
  start_time = Time.now

  refresh_events
  until completed? || @stack_deletion_completed
    show_events(final: false, quiet: quiet)
  end
  show_events(final: true, quiet: quiet) # show the final event

  if @stack_deletion_completed
    puts "Stack #{@stack_name} deleted." unless quiet
    show_took(start_time)
    # Cant use success? because the stack is deleted and the describe stack errors
    # For deletion, always return true once describe_stack fails to return the stack
    return true
  end

  # Never gets beyond here when deleting a stack because the describe stack returns nothing
  # once the stack is deleted. Gets here for stack create and update though.

  if /_FAILED/.match?(last_event_status)
    puts "Stack failed: #{last_event_status}".color(:red)
    puts "Stack reason #{@events.dig(0, "resource_status_reason")}".color(:red)
  elsif /_ROLLBACK_/.match?(last_event_status)
    puts "Stack rolled back: #{last_event_status}".color(:red)
  else # success
    puts "Stack success status: #{last_event_status}" unless quiet
  end

  show_took(start_time)
  success?
end