ufo 6.1.5 → 6.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.cody/acceptance/bin/build.sh +1 -1
- data/.github/ISSUE_TEMPLATE/bug_report.md +84 -0
- data/.github/ISSUE_TEMPLATE/documentation.md +12 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +64 -0
- data/.github/ISSUE_TEMPLATE/question.md +14 -0
- data/.github/ISSUE_TEMPLATE.md +7 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +50 -0
- data/CHANGELOG.md +11 -0
- data/lib/ufo/aws_services/concerns.rb +55 -0
- data/lib/ufo/aws_services.rb +9 -40
- data/lib/ufo/cfn/stack/builder.rb +2 -1
- data/lib/ufo/cfn/stack/params.rb +2 -1
- data/lib/ufo/cfn/stack/status.rb +1 -1
- data/lib/ufo/cfn/stack.rb +4 -4
- data/lib/ufo/cli/destroy.rb +1 -1
- data/lib/ufo/cli/ps/errors.rb +40 -0
- data/lib/ufo/command.rb +17 -7
- data/lib/ufo/config.rb +47 -3
- data/lib/ufo/docker/builder.rb +1 -1
- data/lib/ufo/docker/compiler.rb +3 -3
- data/lib/ufo/docker/state/base.rb +14 -0
- data/lib/ufo/docker/state/bucket.rb +2 -0
- data/lib/ufo/docker/state/file.rb +52 -0
- data/lib/ufo/docker/state/s3.rb +80 -0
- data/lib/ufo/docker/state.rb +16 -50
- data/lib/ufo/info.rb +1 -1
- data/lib/ufo/layering/layer.rb +27 -37
- data/lib/ufo/s3/aws_setup.rb +17 -0
- data/lib/ufo/s3/bucket.rb +174 -0
- data/lib/ufo/s3/rollback.rb +52 -0
- data/lib/ufo/task_definition/erb.rb +2 -2
- data/lib/ufo/task_definition/helpers/{core.rb → docker.rb} +9 -24
- data/lib/ufo/task_definition/helpers/{aws_helper.rb → vars/aws_helper.rb} +2 -1
- data/lib/ufo/task_definition/helpers/vars/builder.rb +124 -0
- data/lib/ufo/task_definition/helpers/vars.rb +11 -114
- data/lib/ufo/upgrade/upgrade4.rb +0 -9
- data/lib/ufo/version.rb +1 -1
- data/ufo.gemspec +1 -0
- 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.
|
161
|
-
|
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
|
-
|
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('/')
|
data/lib/ufo/docker/builder.rb
CHANGED
@@ -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(
|
165
|
+
State.new(@options.merge(base_image: docker_image))
|
166
166
|
else
|
167
167
|
Dockerfile.new(docker_image, @options)
|
168
168
|
end
|
data/lib/ufo/docker/compiler.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
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,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
|
data/lib/ufo/docker/state.rb
CHANGED
@@ -1,63 +1,29 @@
|
|
1
1
|
module Ufo::Docker
|
2
2
|
class State
|
3
|
-
|
4
|
-
include Ufo::Utils::Pretty
|
3
|
+
extend Memoist
|
5
4
|
|
6
|
-
def initialize(
|
7
|
-
@
|
5
|
+
def initialize(options={})
|
6
|
+
@options = options
|
8
7
|
end
|
9
8
|
|
10
9
|
def update
|
11
|
-
|
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
|
23
|
-
|
13
|
+
def read
|
14
|
+
storage.read
|
24
15
|
end
|
25
16
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
data/lib/ufo/layering/layer.rb
CHANGED
@@ -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
|
-
|
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.
|
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
|
-
|
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 @@
|
87
|
-
logger.
|
73
|
+
return if @@shown
|
74
|
+
logger.debug "Layers:"
|
88
75
|
paths.each do |path|
|
89
|
-
|
90
|
-
|
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
|
-
@@
|
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
|