lono 4.1.0 → 4.2.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +3 -1
  4. data/exe/lono +1 -1
  5. data/lib/lono.rb +17 -15
  6. data/lib/lono/cfn.rb +54 -24
  7. data/lib/lono/cfn/base.rb +95 -17
  8. data/lib/lono/cfn/create.rb +5 -29
  9. data/lib/lono/cfn/current.rb +95 -0
  10. data/lib/lono/cfn/delete.rb +15 -1
  11. data/lib/lono/cfn/preview.rb +5 -3
  12. data/lib/lono/cfn/status.rb +212 -0
  13. data/lib/lono/cfn/update.rb +6 -12
  14. data/lib/lono/cfn/util.rb +2 -2
  15. data/lib/lono/cli.rb +4 -6
  16. data/lib/lono/core.rb +23 -5
  17. data/lib/lono/default/settings.yml +1 -1
  18. data/lib/lono/file_uploader.rb +119 -0
  19. data/lib/lono/help/cfn/current.md +18 -0
  20. data/lib/lono/help/cfn/status.md +19 -0
  21. data/lib/lono/script/build.rb +2 -1
  22. data/lib/lono/script/upload.rb +1 -1
  23. data/lib/lono/template/helper.rb +8 -0
  24. data/lib/lono/template/upload.rb +13 -1
  25. data/lib/lono/upgrade.rb +16 -0
  26. data/lib/lono/{upgrade4.rb → upgrade/upgrade4.rb} +1 -1
  27. data/lib/lono/upgrade/upgrade42.rb +36 -0
  28. data/lib/lono/version.rb +1 -1
  29. data/lib/starter_projects/autoscaling/.gitignore +1 -0
  30. data/lib/starter_projects/autoscaling/README.md +1 -1
  31. data/lib/starter_projects/autoscaling/config/settings.yml +1 -1
  32. data/lib/starter_projects/ec2/.gitignore +1 -0
  33. data/lib/starter_projects/ec2/config/settings.yml +1 -1
  34. data/lib/starter_projects/skeleton/.gitignore +1 -0
  35. data/lib/starter_projects/skeleton/config/settings.yml +1 -1
  36. data/lono.gemspec +1 -0
  37. data/spec/fixtures/cfn/stack-events-complete.json +1080 -0
  38. data/spec/fixtures/cfn/stack-events-in-progress.json +1080 -0
  39. data/spec/fixtures/cfn/stack-events-update-rollback-complete.json +1086 -0
  40. data/spec/fixtures/lono_project/config/settings.yml +1 -1
  41. data/spec/lib/lono/cfn/status_spec.rb +77 -0
  42. metadata +33 -4
@@ -0,0 +1,95 @@
1
+ require "yaml"
2
+
3
+ class Lono::Cfn
4
+ class Current
5
+ def initialize(options={})
6
+ Lono::ProjectChecker.check_lono_project
7
+ @options = options
8
+ @file = ".lono/current"
9
+ @path = "#{Lono.root}/#{@file}"
10
+ end
11
+
12
+ def run
13
+ @options[:rm] ? rm : set
14
+ end
15
+
16
+ def rm
17
+ FileUtils.rm_f(@path)
18
+ puts "Current settings have been removed. Removed #{@file}"
19
+ end
20
+
21
+ def set
22
+ if @options.empty?
23
+ show
24
+ else
25
+ d = data # assign data to d to create local variable for merge to work
26
+ d = d.merge(@options).delete_if do |k,v|
27
+ v&.empty? || v == ['']
28
+ end
29
+ text = YAML.dump(d)
30
+ FileUtils.mkdir_p(File.dirname(@path))
31
+ IO.write(@path, text)
32
+ puts "Current settings saved in .lono/current"
33
+ show
34
+ end
35
+ end
36
+
37
+ def show
38
+ if data.empty?
39
+ puts <<-EOL
40
+ There are no current settings. To set a current stack run:
41
+
42
+ lono cfn current --name mystack
43
+ lono cfn current -h # for more examples
44
+ EOL
45
+ return
46
+ end
47
+
48
+ data.each do |key, value|
49
+ puts "Current #{key}: #{value}"
50
+ end
51
+ end
52
+
53
+ def data
54
+ YAML.load(IO.read(@path)) rescue {}
55
+ end
56
+
57
+ def suffix
58
+ current = data["suffix"]
59
+ return current unless current&.empty?
60
+ end
61
+
62
+ # reads suffix, returns nil if not set
63
+ def self.suffix
64
+ Current.new.suffix
65
+ end
66
+
67
+ def name
68
+ current = data["name"]
69
+ return current unless current&.empty?
70
+ end
71
+
72
+ # reads name, returns nil if not set
73
+ def self.name
74
+ Current.new.name
75
+ end
76
+
77
+ # reads name, will exit if current name not set
78
+ def self.name!(name=:current)
79
+ return name if name != :current
80
+
81
+ name = Current.name
82
+ return name if name
83
+
84
+ # TODO: don't think it is possible to get here...
85
+ puts "ERROR: stack name must be specified.".colorize(:red)
86
+ puts <<-EOL
87
+ Example:
88
+ lono cfn #{ARGV[1]} STACK
89
+ You can also set a current stack name to be remembered with:
90
+ lono cfn current --name STACK
91
+ EOL
92
+ exit 1
93
+ end
94
+ end
95
+ end
@@ -3,7 +3,7 @@ class Lono::Cfn::Delete
3
3
  include Lono::Cfn::Util
4
4
 
5
5
  def initialize(stack_name, options={})
6
- @stack_name = stack_name
6
+ @stack_name = switch_current(stack_name)
7
7
  @options = options
8
8
  end
9
9
 
@@ -21,5 +21,19 @@ class Lono::Cfn::Delete
21
21
  puts "#{@stack_name.inspect} stack does not exist".colorize(:red)
22
22
  end
23
23
  end
24
+
25
+ return unless @options[:wait]
26
+ start_time = Time.now
27
+ status.wait
28
+ took = Time.now - start_time
29
+ puts "Time took for stack deletion: #{status.pretty_time(took).green}."
30
+ end
31
+
32
+ def status
33
+ @status ||= Lono::Cfn::Status.new(@stack_name)
34
+ end
35
+
36
+ def switch_current(stack_name)
37
+ Lono::Cfn::Current.name!(stack_name)
24
38
  end
25
39
  end
@@ -22,14 +22,13 @@ class Lono::Cfn::Preview < Lono::Cfn::Base
22
22
  end
23
23
  exit_unless_updatable!(stack_status(@stack_name))
24
24
 
25
- template_body = IO.read(@template_path)
26
25
  params = {
27
26
  change_set_name: change_set_name,
28
27
  stack_name: @stack_name,
29
- template_body: template_body,
30
28
  parameters: params,
31
29
  capabilities: capabilities, # ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"]
32
30
  }
31
+ set_template_body!(params)
33
32
  show_parameters(params, "cfn.create_change_set")
34
33
  begin
35
34
  cfn.create_change_set(params)
@@ -67,7 +66,10 @@ class Lono::Cfn::Preview < Lono::Cfn::Base
67
66
  case change_set.status
68
67
  when "CREATE_COMPLETE"
69
68
  puts "CloudFormation preview for '#{@stack_name}' stack update. Changes:"
70
- change_set.changes.each do |change|
69
+ changes = change_set.changes.sort_by do |change|
70
+ change["resource_change"]["action"]
71
+ end
72
+ changes.each do |change|
71
73
  display_change(change)
72
74
  end
73
75
  when "FAILED"
@@ -0,0 +1,212 @@
1
+ class Lono::Cfn
2
+ class Status
3
+ include AwsService
4
+
5
+ attr_reader :events
6
+ def initialize(stack_name, options={})
7
+ @stack_name = switch_current(stack_name)
8
+ @options = options
9
+ reset
10
+ end
11
+
12
+ # used for the lono cfn status command
13
+ def run
14
+ unless stack_exists?(@stack_name)
15
+ puts "The stack #{@stack_name.colorize(:green)} does not exist."
16
+ return
17
+ end
18
+
19
+ resp = cfn.describe_stacks(stack_name: @stack_name)
20
+ stack = resp.stacks.first
21
+
22
+ puts "The current status for the stack #{@stack_name.colorize(:green)} is #{stack.stack_status.colorize(:green)}"
23
+ if stack.stack_status =~ /_IN_PROGRESS$/
24
+ puts "Stack events (tailing):"
25
+ # tail all events until done
26
+ @hide_time_took = true
27
+ wait
28
+ else
29
+ puts "Stack events:"
30
+ # show the last events that was user initiated
31
+ refresh_events
32
+ show_events(true)
33
+ end
34
+ end
35
+
36
+ def switch_current(stack_name)
37
+ Lono::Cfn::Current.name!(stack_name)
38
+ end
39
+
40
+ def reset
41
+ @events = [] # constantly replaced with recent events
42
+ @last_shown_event_id = nil
43
+ @stack_deletion_completed = nil
44
+ end
45
+
46
+ # check for /(_COMPLETE|_FAILED)$/ status
47
+ def wait
48
+ puts "Waiting for stack to complete"
49
+ start_time = Time.now
50
+
51
+ refresh_events
52
+ until completed || @stack_deletion_completed
53
+ show_events
54
+ end
55
+ show_events(true) # show the final event
56
+
57
+ if @stack_deletion_completed
58
+ puts "Stack #{@stack_name} deleted."
59
+ return
60
+ end
61
+
62
+ if last_event_status =~ /_FAILED/
63
+ puts "Stack failed: #{last_event_status}".colorize(:red)
64
+ puts "Stack reason #{@events[0]["resource_status_reason"]}".colorize(:red)
65
+ elsif last_event_status =~ /_ROLLBACK_/
66
+ puts "Stack rolled back: #{last_event_status}".colorize(:red)
67
+ else # success
68
+ puts "Stack success status: #{last_event_status}".colorize(:green)
69
+ end
70
+
71
+ # Never gets here when deleting a stack because the describe stack returns nothing
72
+ # once the stack is deleted. Gets here for stack create and update though.
73
+ return if @hide_time_took # set in run
74
+ took = Time.now - start_time
75
+ puts "Time took for stack deployment: #{pretty_time(took).green}."
76
+ end
77
+
78
+ def completed
79
+ last_event_status =~ /(_COMPLETE|_FAILED)$/ &&
80
+ @events[0]["resource_type"] == "AWS::CloudFormation::Stack"
81
+ end
82
+
83
+ def last_event_status
84
+ @events[0]["resource_status"]
85
+ end
86
+
87
+ # Only shows new events
88
+ def show_events(final=false)
89
+ if @last_shown_event_id.nil?
90
+ i = find_index(:start)
91
+ print_events(i)
92
+ else
93
+ i = find_index(:last_shown)
94
+ # puts "last_shown index #{i}"
95
+ print_events(i-1) unless i == 0
96
+ end
97
+
98
+ return if final
99
+ sleep 5 unless ENV['TEST']
100
+ refresh_events
101
+ end
102
+
103
+ def print_events(i)
104
+ @events[0..i].reverse.each do |e|
105
+ print_event(e)
106
+ end
107
+ @last_shown_event_id = @events[0]["event_id"]
108
+ # puts "@last_shown_event_id #{@last_shown_event_id.inspect}"
109
+ end
110
+
111
+ def print_event(e)
112
+ message = [
113
+ event_time(e["timestamp"]),
114
+ e["resource_status"],
115
+ e["resource_type"],
116
+ e["logical_resource_id"],
117
+ e["resource_status_reason"]
118
+ ].join(" ")
119
+ message = message.colorize(:red) if e["resource_status"] =~ /_FAILED/
120
+ puts message
121
+ end
122
+
123
+ # https://stackoverflow.com/questions/18000432/rails-12-hour-am-pm-range-for-a-day
124
+ def event_time(timestamp)
125
+ Time.parse(timestamp.to_s).localtime.strftime("%I:%M:%S%p")
126
+ end
127
+
128
+ # refreshes the loaded events in memory
129
+ def refresh_events
130
+ resp = cfn.describe_stack_events(stack_name: @stack_name)
131
+ @events = resp["stack_events"]
132
+ rescue Aws::CloudFormation::Errors::ValidationError => e
133
+ if e.message =~ /Stack .* does not exis/
134
+ @stack_deletion_completed = true
135
+ else
136
+ raise
137
+ end
138
+ end
139
+
140
+ def find_index(name)
141
+ send("#{name}_index")
142
+ end
143
+
144
+ def start_index
145
+ @events.find_index do |event|
146
+ event["resource_type"] == "AWS::CloudFormation::Stack" &&
147
+ event["resource_status_reason"] == "User Initiated"
148
+ end
149
+ end
150
+
151
+ def last_shown_index
152
+ @events.find_index do |event|
153
+ event["event_id"] == @last_shown_event_id
154
+ end
155
+ end
156
+
157
+ def success?
158
+ resource_status = @events[0]["resource_status"]
159
+ %w[CREATE_COMPLETE UPDATE_COMPLETE].include?(resource_status)
160
+ end
161
+
162
+ def update_rollback?
163
+ @events[0]["resource_status"] == "UPDATE_ROLLBACK_COMPLETE"
164
+ end
165
+
166
+ def find_update_failed_event
167
+ i = @events.find_index do |event|
168
+ event["resource_type"] == "AWS::CloudFormation::Stack" &&
169
+ event["resource_status_reason"] == "User Initiated"
170
+ end
171
+
172
+ @events[0..i].reverse.find do |e|
173
+ e["resource_status"] == "UPDATE_FAILED"
174
+ end
175
+ end
176
+
177
+ def rollback_error_message
178
+ return unless update_rollback?
179
+
180
+ event = find_update_failed_event
181
+ return unless event
182
+
183
+ reason = event["resource_status_reason"]
184
+ messages_map.each do |pattern, message|
185
+ if reason =~ pattern
186
+ return message
187
+ end
188
+ end
189
+
190
+ reason # default message is original reason if not found in messages map
191
+ end
192
+
193
+ def messages_map
194
+ {
195
+ /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.",
196
+ /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.",
197
+ /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."
198
+ }
199
+ end
200
+
201
+ # http://stackoverflow.com/questions/4175733/convert-duration-to-hoursminutesseconds-or-similar-in-rails-3-or-ruby
202
+ def pretty_time(total_seconds)
203
+ minutes = (total_seconds / 60) % 60
204
+ seconds = total_seconds % 60
205
+ if total_seconds < 60
206
+ "#{seconds.to_i}s"
207
+ else
208
+ "#{minutes.to_i}m #{seconds.to_i}s"
209
+ end
210
+ end
211
+ end
212
+ end
@@ -18,6 +18,10 @@ class Lono::Cfn::Update < Lono::Cfn::Base
18
18
  end
19
19
  exit_unless_updatable!(stack_status(@stack_name))
20
20
 
21
+ options = @options.merge(lono: false, mute_params: true, mute_using: true, keep: true)
22
+ # create new copy of preview when update_stack is called because of IAM retry logic
23
+ preview = Lono::Cfn::Preview.new(@stack_name, options)
24
+
21
25
  error = nil
22
26
  diff.run if @options[:diff]
23
27
  preview.run if @options[:preview]
@@ -25,7 +29,7 @@ class Lono::Cfn::Update < Lono::Cfn::Base
25
29
 
26
30
  if @options[:change_set] # defaults to this
27
31
  message << " via change set: #{preview.change_set_name}"
28
- change_set_update
32
+ preview.execute_change_set
29
33
  else
30
34
  standard_update(params)
31
35
  end
@@ -33,14 +37,13 @@ class Lono::Cfn::Update < Lono::Cfn::Base
33
37
  end
34
38
 
35
39
  def standard_update(params)
36
- template_body = IO.read(@template_path)
37
40
  params = {
38
41
  stack_name: @stack_name,
39
- template_body: template_body,
40
42
  parameters: params,
41
43
  capabilities: capabilities, # ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"]
42
44
  disable_rollback: !@options[:rollback],
43
45
  }
46
+ set_template_body!(params)
44
47
  show_parameters(params, "cfn.update_stack")
45
48
  begin
46
49
  cfn.update_stack(params)
@@ -50,16 +53,7 @@ class Lono::Cfn::Update < Lono::Cfn::Base
50
53
  end
51
54
  end
52
55
 
53
- def preview
54
- options = @options.merge(lono: false, mute_params: true, mute_using: true, keep: true)
55
- @preview ||= Lono::Cfn::Preview.new(@stack_name, options)
56
- end
57
-
58
56
  def diff
59
57
  @diff ||= Lono::Cfn::Diff.new(@stack_name, @options.merge(lono: false, mute_params: true, mute_using: true))
60
58
  end
61
-
62
- def change_set_update
63
- preview.execute_change_set
64
- end
65
59
  end
@@ -5,9 +5,9 @@ module Lono::Cfn::Util
5
5
  else
6
6
  message = case action
7
7
  when :update
8
- "Are you sure you want to want to update the '#{stack_name}' stack with the changes? (y/N)"
8
+ "Are you sure you want to want to update the #{stack_name.colorize(:green)} stack with the changes? (y/N)"
9
9
  when :delete
10
- "Are you sure you want to want to delete the '#{stack_name}' stack? (y/N)"
10
+ "Are you sure you want to want to delete the #{stack_name.colorize(:green)} stack? (y/N)"
11
11
  end
12
12
  puts message
13
13
  sure = $stdin.gets
@@ -66,12 +66,6 @@ module Lono
66
66
  Completer::Script.generate
67
67
  end
68
68
 
69
- desc "upgrade4", "Upgrade from version 3 to 4."
70
- long_desc Help.text("upgrade4")
71
- def upgrade4
72
- Upgrade4.new(options).run
73
- end
74
-
75
69
  desc "version", "Prints version"
76
70
  def version
77
71
  puts VERSION
@@ -92,5 +86,9 @@ module Lono
92
86
  desc "script SUBCOMMAND", "script subcommands"
93
87
  long_desc Help.text(:script)
94
88
  subcommand "script", Script
89
+
90
+ desc "upgrade SUBCOMMAND", "upgrade subcommands"
91
+ long_desc Help.text(:upgrade)
92
+ subcommand "upgrade", Upgrade
95
93
  end
96
94
  end