ufo 6.1.5 → 6.2.2
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/.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
|