ufo 6.1.5 → 6.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.cody/acceptance/bin/build.sh +1 -1
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +84 -0
  4. data/.github/ISSUE_TEMPLATE/documentation.md +12 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +64 -0
  6. data/.github/ISSUE_TEMPLATE/question.md +14 -0
  7. data/.github/ISSUE_TEMPLATE.md +7 -0
  8. data/.github/PULL_REQUEST_TEMPLATE.md +50 -0
  9. data/CHANGELOG.md +11 -0
  10. data/lib/ufo/aws_services/concerns.rb +55 -0
  11. data/lib/ufo/aws_services.rb +9 -40
  12. data/lib/ufo/cfn/stack/builder.rb +2 -1
  13. data/lib/ufo/cfn/stack/params.rb +2 -1
  14. data/lib/ufo/cfn/stack/status.rb +1 -1
  15. data/lib/ufo/cfn/stack.rb +4 -4
  16. data/lib/ufo/cli/destroy.rb +1 -1
  17. data/lib/ufo/cli/ps/errors.rb +40 -0
  18. data/lib/ufo/command.rb +17 -7
  19. data/lib/ufo/config.rb +47 -3
  20. data/lib/ufo/docker/builder.rb +1 -1
  21. data/lib/ufo/docker/compiler.rb +3 -3
  22. data/lib/ufo/docker/state/base.rb +14 -0
  23. data/lib/ufo/docker/state/bucket.rb +2 -0
  24. data/lib/ufo/docker/state/file.rb +52 -0
  25. data/lib/ufo/docker/state/s3.rb +80 -0
  26. data/lib/ufo/docker/state.rb +16 -50
  27. data/lib/ufo/info.rb +1 -1
  28. data/lib/ufo/layering/layer.rb +27 -37
  29. data/lib/ufo/s3/aws_setup.rb +17 -0
  30. data/lib/ufo/s3/bucket.rb +174 -0
  31. data/lib/ufo/s3/rollback.rb +52 -0
  32. data/lib/ufo/task_definition/erb.rb +2 -2
  33. data/lib/ufo/task_definition/helpers/{core.rb → docker.rb} +9 -24
  34. data/lib/ufo/task_definition/helpers/{aws_helper.rb → vars/aws_helper.rb} +2 -1
  35. data/lib/ufo/task_definition/helpers/vars/builder.rb +124 -0
  36. data/lib/ufo/task_definition/helpers/vars.rb +11 -114
  37. data/lib/ufo/upgrade/upgrade4.rb +0 -9
  38. data/lib/ufo/version.rb +1 -1
  39. data/ufo.gemspec +1 -0
  40. metadata +33 -4
data/lib/ufo/config.rb CHANGED
@@ -85,6 +85,9 @@ module Ufo
85
85
  config.exec.command = "/bin/bash" # aws ecs execute-command cli
86
86
  config.exec.enabled = true # EcsService EnableExecuteCommand
87
87
 
88
+ config.layering = ActiveSupport::OrderedOptions.new
89
+ config.layering.show = show_layers?
90
+
88
91
  config.log = ActiveSupport::OrderedOptions.new
89
92
  config.log.root = Ufo.log_root
90
93
  config.logger = ufo_logger
@@ -114,7 +117,10 @@ module Ufo
114
117
  config.ship.docker.quiet = false # only affects ufo ship docker commands output
115
118
 
116
119
  config.state = ActiveSupport::OrderedOptions.new
120
+ config.state.bucket = nil # Set to use existing bucket. When not set ufo creates a managed s3 bucket
121
+ config.state.managed = true # false will disable creation of managed bucket entirely
117
122
  config.state.reminder = true
123
+ config.state.storage = "s3" # s3 or file
118
124
 
119
125
  config.waf = ActiveSupport::OrderedOptions.new
120
126
  config.waf.web_acl_arn = nil
@@ -157,15 +163,52 @@ module Ufo
157
163
  role = layer_levels(".ufo/config/#{Ufo.app}/#{Ufo.role}")
158
164
  layers += root + env + role
159
165
  end
160
- # load_project_config gets called so early that logger is not yet configured. use puts
161
- puts "Config layers:" if ENV['UFO_SHOW_ALL_LAYERS']
166
+ # load_project_config gets called so early that logger is not yet configured.
167
+ # Cannot use Ufo.config yet and cannot use logger which relies on Ufo.config
168
+ # Use puts and use show_layers? which parses for the config
169
+ show = show_layers?
170
+ puts "Config Layers" if show
162
171
  layers.each do |layer|
163
172
  path = "#{Ufo.root}/#{layer}"
164
- puts " #{layer}" if ENV['UFO_SHOW_ALL_LAYERS']
173
+ if ENV['UFO_LAYERS_ALL']
174
+ puts " #{pretty_path(path)}"
175
+ elsif show
176
+ puts " #{pretty_path(path)}" if File.exist?(path)
177
+ end
165
178
  evaluate_file(path)
166
179
  end
167
180
  end
168
181
 
182
+ def show_layers?
183
+ ENV['UFO_LAYERS'] || parse_for_layering_show
184
+ end
185
+ private :show_layers?
186
+
187
+ # Some limitations:
188
+ #
189
+ # * Only parsing one file: .ufo/config.rb
190
+ # * If user is using Ruby code that cannot be parse will fallback to default
191
+ #
192
+ # Think it's worth it so user only has to configure
193
+ #
194
+ # config.layering.show = true
195
+ #
196
+ def parse_for_layering_show
197
+ lines = IO.readlines("#{Ufo.root}/.ufo/config.rb")
198
+ config_line = lines.find { |l| l =~ /config\.layering.show.*=/ && l !~ /^\s+#/ }
199
+ return false unless config_line # default is false
200
+ config_value = config_line.gsub(/.*=/,'').strip.gsub(/["']/,'')
201
+ config_value != "false" && config_value != "nil"
202
+ rescue Exception => e
203
+ if ENV['UFO_DEBUG']
204
+ puts "#{e.class} #{e.message}".color(:yellow)
205
+ puts "WARN: Unable to parse for config.layering.show".color(:yellow)
206
+ puts "Using default: config.layering.show = false"
207
+ end
208
+ false
209
+ end
210
+ memoize :parse_for_layering_show
211
+
169
212
  # Works similiar to Layering::Layer. Consider combining the logic and usin Layering::Layer
170
213
  #
171
214
  # Examples:
@@ -180,6 +223,7 @@ module Ufo
180
223
  #
181
224
  def layer_levels(prefix=nil)
182
225
  levels = ["", "base", Ufo.env]
226
+ levels << "#{Ufo.env}-#{Ufo.extra}" if Ufo.extra
183
227
  paths = levels.map do |i|
184
228
  # base layer has prefix of '', reject with blank so it doesnt produce '//'
185
229
  [prefix, i].join('/')
@@ -162,7 +162,7 @@ module Ufo::Docker
162
162
 
163
163
  def update_dockerfile
164
164
  updater = if File.exist?("#{Ufo.root}/Dockerfile.erb") # dont use @dockerfile on purpose
165
- State.new(docker_image, @options)
165
+ State.new(@options.merge(base_image: docker_image))
166
166
  else
167
167
  Dockerfile.new(docker_image, @options)
168
168
  end
@@ -9,9 +9,9 @@ module Ufo::Docker
9
9
  return unless File.exist?(@erb_file)
10
10
 
11
11
  puts "Compiled #{File.basename(@erb_file).color(:green)} to #{File.basename(@dockerfile).color(:green)}"
12
- path = "#{Ufo.root}/.ufo/state/data.yml"
13
- vars = YAML.load_file(path)[Ufo.env] if File.exist?(path)
14
- vars ||= {}
12
+
13
+ state = State.new
14
+ vars = state.read
15
15
  result = RenderMePretty.result(@erb_file, vars)
16
16
  comment =<<~EOL.chop # remove the trailing newline
17
17
  # IMPORTANT: This file was generated from #{File.basename(@erb_file)} as a part of running:
@@ -0,0 +1,14 @@
1
+ class Ufo::Docker::State
2
+ class Base
3
+ include Ufo::Utils::Logging
4
+ include Ufo::Utils::Pretty
5
+
6
+ def initialize(options={})
7
+ @options = options
8
+ # base_image only passed in with: ufo docker base
9
+ # State#update uses it.
10
+ # State#read wont have access to it and gets it from stored state
11
+ @base_image = options[:base_image]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,2 @@
1
+ class Ufo::Docker::State
2
+ class S3 < Base
@@ -0,0 +1,52 @@
1
+ class Ufo::Docker::State
2
+ class File < Base
3
+ def read
4
+ current_data
5
+ end
6
+
7
+ def update
8
+ data = current_data
9
+ data["base_image"] = @base_image
10
+
11
+ pretty_path = state_path.sub("#{Ufo.root}/", "")
12
+ FileUtils.mkdir_p(::File.dirname(state_path))
13
+ IO.write(state_path, YAML.dump(data))
14
+
15
+ logger.info "The #{pretty_path} base_image has been updated with the latest base image:".color(:green)
16
+ logger.info " #{@base_image}".color(:green)
17
+ reminder_message
18
+ end
19
+
20
+ def current_data
21
+ ::File.exist?(state_path) ? YAML.load_file(state_path) : {}
22
+ end
23
+
24
+ def state_path
25
+ "#{Ufo.root}/.ufo/state/#{Ufo.app}/#{Ufo.env}/data.yml"
26
+ end
27
+
28
+ def reminder_message
29
+ return unless Ufo.config.state.reminder
30
+ repo = ENV['UFO_CENTRAL_REPO']
31
+ return unless repo
32
+ logger.info "It looks like you're using a central deployer pattern".color(:yellow)
33
+ logger.info <<~EOL
34
+ Remember to commit the state file:
35
+
36
+ state file: #{pretty_path(state_path)}
37
+ repo: #{repo}
38
+
39
+ EOL
40
+
41
+ logger.info <<~EOL
42
+ You can disable these reminder messages with:
43
+
44
+ .ufo/config.rb
45
+
46
+ Ufo.configure do |config|
47
+ config.state.reminder = false
48
+ end
49
+ EOL
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,80 @@
1
+ class Ufo::Docker::State
2
+ class S3 < Base
3
+ extend Memoist
4
+ include Ufo::AwsServices
5
+
6
+ def read
7
+ current_data
8
+ end
9
+
10
+ def update
11
+ data = current_data
12
+ data["base_image"] = @base_image
13
+
14
+ # write data to s3
15
+ body = YAML.dump(data)
16
+ s3.put_object(
17
+ body: body,
18
+ bucket: s3_bucket,
19
+ key: s3_key,
20
+ )
21
+ logger.info "Updated base image in s3://#{s3_bucket}/#{s3_key}"
22
+ logger.info " #{@base_image}".color(:green)
23
+ end
24
+
25
+ # TODO: edge cases: no bucket, no permission
26
+ def current_data
27
+ resp = s3.get_object(bucket: s3_bucket, key: s3_key)
28
+ YAML.load(resp.body)
29
+ rescue Aws::S3::Errors::NoSuchKey
30
+ logger.debug "WARN: s3 key does not exist: #{s3_key}"
31
+ {}
32
+ rescue Aws::S3::Errors::NoSuchBucket
33
+ logger.error "ERROR: S3 bucket does not exist to store state: #{s3_bucket}".color(:red)
34
+ logger.error <<~EOL
35
+ Please double check the config.
36
+
37
+ See: http://ufoships.com/docs/config/state/
38
+
39
+ EOL
40
+ exit 1
41
+ end
42
+
43
+ def s3_key
44
+ "ufo/state/#{app}/#{Ufo.env}/data.yml"
45
+ end
46
+
47
+ # ufo docker base is called before Ufo.config is loaded. This ensures it is loaded
48
+ def app
49
+ Ufo.config
50
+ Ufo.app
51
+ end
52
+
53
+ def s3_bucket
54
+ state = Ufo.config.state
55
+ if state.bucket
56
+ state.bucket
57
+ elsif state.managed
58
+ ensure_s3_bucket_exist
59
+ Ufo::S3::Bucket.name
60
+ else
61
+ logger.error "ERROR: No s3 bucket to store state".color(:red)
62
+ logger.error <<~EOL
63
+ UFO needs a bucket to store the built docker base image.
64
+
65
+ Configure an existing bucket or enable UFO to create a bucket.
66
+
67
+ See: http://ufoships.com/docs/config/state/
68
+ EOL
69
+ exit 1
70
+ end
71
+ end
72
+
73
+ def ensure_s3_bucket_exist
74
+ bucket = Ufo::S3::Bucket.new
75
+ return if bucket.exist?
76
+ bucket.deploy
77
+ end
78
+ memoize :ensure_s3_bucket_exist
79
+ end
80
+ end
@@ -1,63 +1,29 @@
1
1
  module Ufo::Docker
2
2
  class State
3
- include Ufo::Utils::Logging
4
- include Ufo::Utils::Pretty
3
+ extend Memoist
5
4
 
6
- def initialize(docker_image, options={})
7
- @docker_image, @options = docker_image, options
5
+ def initialize(options={})
6
+ @options = options
8
7
  end
9
8
 
10
9
  def update
11
- data = current_data
12
- data[Ufo.env] ||= {}
13
- data[Ufo.env]["base_image"] = @docker_image
14
- pretty_path = state_path.sub("#{Ufo.root}/", "")
15
- FileUtils.mkdir_p(File.dirname(state_path))
16
- IO.write(state_path, YAML.dump(data))
17
- logger.info "The #{pretty_path} base_image has been updated with the latest base image:".color(:green)
18
- logger.info " #{@docker_image}".color(:green)
19
- reminder_message
10
+ storage.update
20
11
  end
21
12
 
22
- def current_data
23
- File.exist?(state_path) ? YAML.load_file(state_path) : {}
13
+ def read
14
+ storage.read
24
15
  end
25
16
 
26
- def state_path
27
- path = "#{Ufo.root}/.ufo/state"
28
- if ENV['UFO_APP'] # env var activates app path
29
- path = "#{Ufo.root}/.ufo/state/#{Ufo.app}"
30
- end
31
- "#{path}/data.yml"
32
- end
33
-
34
- def reminder_message
35
- return unless Ufo.config.state.reminder
36
- repo = ENV['UFO_CENTRAL_REPO']
37
- return unless repo
38
- logger.info "It looks like you're using a central deployer pattern".color(:yellow)
39
- logger.info <<~EOL
40
- Remember to commit the state file:
41
-
42
- state file: #{pretty_path(state_path)}
43
- repo: #{repo}
44
-
45
- EOL
46
-
47
- unless ENV['UFO_APP']
48
- logger.info "WARN: It also doesnt look like UFO_ENV is set".color(:yellow)
49
- logger.info "UFO_ENV should be set when you're using ufo in a central manner"
50
- end
51
-
52
- logger.info <<~EOL
53
- You can disable these reminder messages with:
54
-
55
- .ufo/config.rb
56
-
57
- Ufo.configure do |config|
58
- config.state.reminder = false
59
- end
60
- EOL
17
+ private
18
+ # Examples:
19
+ # File.new(@docker_image, @options)
20
+ # S3.new(@docker_image, @options)
21
+ def storage
22
+ storage = Ufo.config.state.storage
23
+ class_name = "Ufo::Docker::State::#{storage.camelize}"
24
+ klass = class_name.constantize
25
+ klass.new(@options)
61
26
  end
27
+ memoize :storage
62
28
  end
63
29
  end
data/lib/ufo/info.rb CHANGED
@@ -73,7 +73,7 @@ module Ufo
73
73
  end
74
74
 
75
75
  def stack_resources
76
- resp = cloudformation.describe_stack_resources(stack_name: @stack_name)
76
+ resp = cfn.describe_stack_resources(stack_name: @stack_name)
77
77
  resp.stack_resources
78
78
  end
79
79
  memoize :stack_resources
@@ -23,46 +23,44 @@ module Ufo::Layering
23
23
  end
24
24
 
25
25
  def paths
26
- core = full_layers(".ufo/vars")
27
- app = full_layers(".ufo/vars/#{Ufo.app}")
28
- paths = core + app
26
+ # core = full_layers(".ufo/vars")
27
+ # app = full_layers(".ufo/vars/#{Ufo.app}")
28
+
29
+ core = layer_levels(".ufo/vars")
30
+ role = layer_levels(".ufo/vars/#{@task_definition.role}")
31
+ app = layer_levels(".ufo/vars/#{Ufo.app}")
32
+ app_role = layer_levels(".ufo/vars/#{Ufo.app}/#{@task_definition.role}")
33
+
34
+ paths = core + role + app + app_role
29
35
  add_ext!(paths)
30
36
  paths.map! { |p| "#{Ufo.root}/#{p}" }
31
37
  show_layers(paths)
32
38
  paths
33
39
  end
34
40
 
35
- def full_layering
36
- # layers defined in Lono::Layering module
37
- all = layers.map { |layer| layer.sub(/\/$/,'') } # strip trailing slash
38
- all.inject([]) do |sum, layer|
39
- sum += layer_levels(layer) unless layer.nil?
40
- sum
41
- end
42
- end
43
-
44
- # interface method
45
- def main_layers
46
- ['']
47
- end
48
-
49
41
  # adds prefix and to each layer pair that has base and Ufo.env. IE:
50
42
  #
51
43
  # "#{prefix}/base"
52
44
  # "#{prefix}/#{Ufo.env}"
53
45
  #
54
46
  def layer_levels(prefix=nil)
55
- levels = ["base", Ufo.env]
47
+ levels = ["", "base", Ufo.env]
48
+ levels << "#{Ufo.env}-#{Ufo.extra}" if Ufo.extra
56
49
  levels.map! do |i|
57
50
  # base layer has prefix of '', reject with blank so it doesnt produce '//'
58
51
  [prefix, i].reject(&:blank?).join('/')
59
52
  end
60
- levels.unshift(prefix) # unless prefix.blank? # IE: params/us-west-2.txt
53
+ levels.map! { |level| level.sub(/\/$/,'') } # strip trailing slash
54
+ # levels.unshift(prefix) # unless prefix.blank? # IE: params/us-west-2.txt
61
55
  levels
62
56
  end
63
57
 
58
+ # interface method
59
+ def main_layers
60
+ ['']
61
+ end
62
+
64
63
  def add_ext!(paths)
65
- ext = "rb"
66
64
  paths.map! do |path|
67
65
  path = path.sub(/\/$/,'') if path.ends_with?('/')
68
66
  "#{path}.rb"
@@ -70,26 +68,18 @@ module Ufo::Layering
70
68
  paths
71
69
  end
72
70
 
73
- def full_layers(dir)
74
- layers = full_layering.map do |layer|
75
- "#{dir}/#{layer}"
76
- end
77
- role_layers = full_layering.map do |layer|
78
- "#{dir}/#{@task_definition.role}/#{layer}" # Note: layer can be '' will clean up
79
- end
80
- layers += role_layers
81
- layers.map { |l| l.gsub('//','/') } # cleanup // if layer is ''
82
- end
83
-
84
- @@shown_layers = false
71
+ @@shown = false
85
72
  def show_layers(paths)
86
- return if @@shown_layers
87
- logger.info "Vars Layers:" if ENV['UFO_SHOW_ALL_LAYERS']
73
+ return if @@shown
74
+ logger.debug "Layers:"
88
75
  paths.each do |path|
89
- show_layer = File.exist?(path) && logger.level <= Logger::DEBUG
90
- logger.info " #{pretty_path(path)}" if show_layer || ENV['UFO_SHOW_ALL_LAYERS']
76
+ if ENV['UFO_LAYERS_ALL']
77
+ logger.info " #{pretty_path(path)}"
78
+ elsif Ufo.config.layering.show
79
+ logger.info " #{pretty_path(path)}" if File.exist?(path)
80
+ end
91
81
  end
92
- @@shown_layers = true
82
+ @@shown = true
93
83
  end
94
84
  end
95
85
  end
@@ -0,0 +1,17 @@
1
+ module Ufo::S3
2
+ class AwsSetup
3
+ include Ufo::AwsServices
4
+ include Ufo::Utils::Logging
5
+
6
+ def check!
7
+ s3.config.region
8
+ rescue Aws::Errors::MissingRegionError => e
9
+ logger.info "ERROR: #{e.class}: #{e.message}".color(:red)
10
+ logger.info <<~EOL
11
+ Unable to detect the AWS_REGION to make AWS API calls. This is might be because the AWS access
12
+ has not been set up yet. Please either your ~/.aws files.
13
+ EOL
14
+ exit 1
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,174 @@
1
+ module Ufo::S3
2
+ class Bucket
3
+ extend Memoist
4
+ extend Ufo::AwsServices
5
+ include Ufo::AwsServices
6
+ include Ufo::Utils::Logging
7
+
8
+ STACK_NAME = ENV['UFO_STACK_NAME'] || "ufo"
9
+ def initialize(options={})
10
+ @options = options
11
+ end
12
+
13
+ def deploy
14
+ stack = find_stack
15
+ if rollback.complete?
16
+ logger.info "Existing '#{STACK_NAME}' stack in ROLLBACK_COMPLETE state. Deleting stack before continuing."
17
+ disable_termination_protection
18
+ cfn.delete_stack(stack_name: STACK_NAME)
19
+ status.wait
20
+ stack = nil
21
+ end
22
+
23
+ if stack
24
+ update
25
+ else
26
+ create
27
+ end
28
+ end
29
+
30
+ def exist?
31
+ !!bucket_name
32
+ end
33
+
34
+ def bucket_name
35
+ self.class.name
36
+ end
37
+
38
+ def show
39
+ if bucket_name
40
+ logger.info "UFO bucket name: #{bucket_name}"
41
+ else
42
+ logger.info "UFO bucket does not exist yet."
43
+ end
44
+ end
45
+
46
+ # Launches a cloudformation to create an s3 bucket
47
+ def create
48
+ logger.info "Creating #{STACK_NAME} stack for s3 bucket to store state"
49
+ cfn.create_stack(
50
+ stack_name: STACK_NAME,
51
+ template_body: template_body,
52
+ enable_termination_protection: true,
53
+ )
54
+ success = status.wait
55
+ unless success
56
+ logger.info "ERROR: Unable to create UFO stack with managed s3 bucket".color(:red)
57
+ exit 1
58
+ end
59
+ end
60
+
61
+ def update
62
+ logger.info "Updating #{STACK_NAME} stack with the s3 bucket"
63
+ cfn.update_stack(stack_name: STACK_NAME, template_body: template_body)
64
+ rescue Aws::CloudFormation::Errors::ValidationError => e
65
+ raise unless e.message.include?("No updates are to be performed")
66
+ end
67
+
68
+ def delete
69
+ are_you_sure?
70
+
71
+ logger.info "Deleting #{STACK_NAME} stack with the s3 bucket"
72
+ disable_termination_protection
73
+ empty_bucket!
74
+ cfn.delete_stack(stack_name: STACK_NAME)
75
+ end
76
+
77
+ def disable_termination_protection
78
+ cfn.update_termination_protection(
79
+ stack_name: STACK_NAME,
80
+ enable_termination_protection: false,
81
+ )
82
+ end
83
+
84
+ def find_stack
85
+ resp = cfn.describe_stacks(stack_name: STACK_NAME)
86
+ resp.stacks.first
87
+ rescue Aws::CloudFormation::Errors::ValidationError
88
+ nil
89
+ end
90
+
91
+ def status
92
+ CfnStatus.new(STACK_NAME)
93
+ end
94
+
95
+ private
96
+
97
+ def empty_bucket!
98
+ return unless bucket_name # in case of UFO stack ROLLBACK_COMPLETE from failed bucket creation
99
+
100
+ resp = s3.list_objects(bucket: bucket_name)
101
+ if resp.contents.size > 0
102
+ # IE: objects = [{key: "objectkey1"}, {key: "objectkey2"}]
103
+ objects = resp.contents.map { |item| {key: item.key} }
104
+ s3.delete_objects(
105
+ bucket: bucket_name,
106
+ delete: {
107
+ objects: objects,
108
+ quiet: false,
109
+ }
110
+ )
111
+ empty_bucket! # keep deleting objects until bucket is empty
112
+ end
113
+ end
114
+
115
+
116
+ def are_you_sure?
117
+ return true if @options[:yes]
118
+
119
+ if bucket_name.nil?
120
+ logger.info "The UFO stack and s3 bucket does not exist."
121
+ exit
122
+ end
123
+
124
+ logger.info "Are you yes you want the UFO bucket #{bucket_name.color(:green)} to be emptied and deleted? (y/N)"
125
+ yes = $stdin.gets.strip
126
+ confirmed = yes =~ /^Y/i
127
+ unless confirmed
128
+ logger.info "Phew that was close."
129
+ exit
130
+ end
131
+ end
132
+
133
+ def template_body
134
+ <<~YAML
135
+ Description: UFO managed s3 bucket
136
+ Resources:
137
+ Bucket:
138
+ Type: AWS::S3::Bucket
139
+ Properties:
140
+ BucketEncryption:
141
+ ServerSideEncryptionConfiguration:
142
+ - ServerSideEncryptionByDefault:
143
+ SSEAlgorithm: AES256
144
+ Tags:
145
+ - Key: Name
146
+ Value: UFO
147
+ Outputs:
148
+ Bucket:
149
+ Value:
150
+ Ref: Bucket
151
+ YAML
152
+ end
153
+
154
+ def rollback
155
+ Rollback.new(STACK_NAME)
156
+ end
157
+
158
+ class << self
159
+ @@name = nil
160
+ def name
161
+ return @@name if @@name # only memoize once bucket has been created
162
+
163
+ AwsSetup.new.check!
164
+
165
+ stack = new.find_stack
166
+ return unless stack
167
+
168
+ stack_resources = find_stack_resources(STACK_NAME)
169
+ bucket = stack_resources.find { |r| r.logical_resource_id == "Bucket" }
170
+ @@name = bucket.physical_resource_id # actual bucket name
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,52 @@
1
+ module Ufo::S3
2
+ class Rollback
3
+ extend Memoist
4
+ include Ufo::AwsServices
5
+ include Ufo::Utils::Logging
6
+
7
+ def initialize(stack)
8
+ @stack = stack
9
+ end
10
+
11
+ def delete_stack
12
+ return unless complete?
13
+ logger.info "Existing stack in ROLLBACK_COMPLETE state. Deleting stack before continuing."
14
+ cfn.delete_stack(stack_name: @stack)
15
+ status.wait
16
+ status.reset
17
+ true
18
+ end
19
+
20
+ def continue_update
21
+ continue_update?
22
+ begin
23
+ cfn.continue_update_rollback(stack_name: @stack)
24
+ rescue Aws::CloudFormation::Errors::ValidationError => e
25
+ logger.info "ERROR: Continue update: #{e.message}".color(:red)
26
+ quit 1
27
+ end
28
+ end
29
+
30
+ def continue_update?
31
+ logger.info <<~EOL
32
+ The stack is in the UPDATE_ROLLBACK_FAILED state. More info here: https://amzn.to/2IiEjc5
33
+ Would you like to try to continue the update rollback? (y/N)
34
+ EOL
35
+
36
+ yes = @options[:yes] ? "y" : $stdin.gets
37
+ unless yes =~ /^y/
38
+ logger.info "Exiting without continuing the update rollback."
39
+ quit 0
40
+ end
41
+ end
42
+
43
+ def complete?
44
+ stack&.stack_status == 'ROLLBACK_COMPLETE'
45
+ end
46
+
47
+ def stack
48
+ find_stack(@stack)
49
+ end
50
+ memoize :stack
51
+ end
52
+ end