ufo 0.0.6
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 +7 -0
- data/.gitignore +5 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +128 -0
- data/Guardfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +227 -0
- data/Rakefile +6 -0
- data/bin/ufo +10 -0
- data/lib/starter_project/.env.prod +3 -0
- data/lib/starter_project/Dockerfile +20 -0
- data/lib/starter_project/bin/deploy +12 -0
- data/lib/starter_project/ufo/settings.yml +7 -0
- data/lib/starter_project/ufo/task_definitions.rb +44 -0
- data/lib/starter_project/ufo/templates/main.json.erb +39 -0
- data/lib/ufo.rb +26 -0
- data/lib/ufo/aws_services.rb +21 -0
- data/lib/ufo/cli.rb +136 -0
- data/lib/ufo/cli/help.rb +164 -0
- data/lib/ufo/defaults.rb +46 -0
- data/lib/ufo/destroy.rb +60 -0
- data/lib/ufo/docker_builder.rb +120 -0
- data/lib/ufo/docker_cleaner.rb +52 -0
- data/lib/ufo/dockerfile_updater.rb +42 -0
- data/lib/ufo/dsl.rb +93 -0
- data/lib/ufo/dsl/helper.rb +47 -0
- data/lib/ufo/dsl/outputter.rb +40 -0
- data/lib/ufo/dsl/task_definition.rb +65 -0
- data/lib/ufo/ecr_auth.rb +35 -0
- data/lib/ufo/ecr_cleaner.rb +66 -0
- data/lib/ufo/execute.rb +19 -0
- data/lib/ufo/init.rb +83 -0
- data/lib/ufo/pretty_time.rb +14 -0
- data/lib/ufo/scale.rb +34 -0
- data/lib/ufo/settings.rb +33 -0
- data/lib/ufo/ship.rb +436 -0
- data/lib/ufo/tasks_builder.rb +30 -0
- data/lib/ufo/tasks_register.rb +51 -0
- data/lib/ufo/templates/default.json.erb +39 -0
- data/lib/ufo/version.rb +3 -0
- data/spec/fixtures/home_existing/.docker/config.json +10 -0
- data/spec/lib/cli_spec.rb +50 -0
- data/spec/lib/ecr_auth_spec.rb +39 -0
- data/spec/lib/ecr_cleaner_spec.rb +32 -0
- data/spec/lib/ship_spec.rb +77 -0
- data/spec/spec_helper.rb +28 -0
- data/ufo.gemspec +34 -0
- metadata +267 -0
data/lib/ufo/defaults.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Ufo
|
2
|
+
# To include this module must have this in initialize:
|
3
|
+
#
|
4
|
+
# def initialize(optiions, ...)
|
5
|
+
# @options = options
|
6
|
+
# ...
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# So @options must be set
|
10
|
+
module Defaults
|
11
|
+
# image: 123456789.dkr.ecr.us-east-1.amazonaws.com/sinatra
|
12
|
+
# # service to cluster mapping, overrides default cluster cli overrides this
|
13
|
+
# service_cluster:
|
14
|
+
# default: prod-lo
|
15
|
+
# hi-web-prod: prod-hi
|
16
|
+
# hi-clock-prod: prod-lo
|
17
|
+
# hi-worker-prod: prod-lo
|
18
|
+
#
|
19
|
+
# Assumes that @service is set in the class that the Defaults module is included in.
|
20
|
+
def default_cluster
|
21
|
+
service_cluster = settings.data["service_cluster"]
|
22
|
+
service_cluster[@service] || service_cluster["default"]
|
23
|
+
end
|
24
|
+
|
25
|
+
# These default service values only are used when a service is created by `ufo`
|
26
|
+
def default_maximum_percent
|
27
|
+
Integer(new_service_settings["maximum_percent"] || 200)
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_minimum_healthy_percent
|
31
|
+
Integer(new_service_settings["minimum_healthy_percent"] || 100)
|
32
|
+
end
|
33
|
+
|
34
|
+
def default_desired_count
|
35
|
+
Integer(new_service_settings["desired_count"] || 1)
|
36
|
+
end
|
37
|
+
|
38
|
+
def new_service_settings
|
39
|
+
settings.data["new_service"] || {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def settings
|
43
|
+
@settings ||= Settings.new(@options[:project_root])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/ufo/destroy.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module Ufo
|
2
|
+
class Destroy
|
3
|
+
include Defaults
|
4
|
+
include AwsServices
|
5
|
+
|
6
|
+
def initialize(service, options={})
|
7
|
+
@service = service
|
8
|
+
@options = options
|
9
|
+
@cluster = @options[:cluster] || default_cluster
|
10
|
+
end
|
11
|
+
|
12
|
+
def bye
|
13
|
+
unless are_you_sure?
|
14
|
+
puts "Phew, that was close"
|
15
|
+
exit
|
16
|
+
end
|
17
|
+
|
18
|
+
clusters = ecs.describe_clusters(clusters: [@cluster]).clusters
|
19
|
+
if clusters.size < 1
|
20
|
+
puts "The #{@cluster} cluster does not exist so there can be no service on that cluster to delete."
|
21
|
+
exit
|
22
|
+
end
|
23
|
+
|
24
|
+
services = ecs.describe_services(cluster: @cluster, services: [@service]).services
|
25
|
+
service = services.first
|
26
|
+
if service.nil?
|
27
|
+
puts "Unable to find #{@service} service to delete it."
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
if service.status != "ACTIVE"
|
31
|
+
puts "The #{@service} service is not ACTIVE so no need to delete it."
|
32
|
+
exit
|
33
|
+
end
|
34
|
+
|
35
|
+
# changes desired size to 0
|
36
|
+
ecs.update_service(
|
37
|
+
desired_count: 0,
|
38
|
+
cluster: @cluster,
|
39
|
+
service: @service
|
40
|
+
)
|
41
|
+
# Cannot find all tasks scoped to a service. Only scoped to a cluster.
|
42
|
+
# So will not try to stop the tasks.
|
43
|
+
# ask to stop them
|
44
|
+
#
|
45
|
+
resp = ecs.delete_service(
|
46
|
+
cluster: @cluster,
|
47
|
+
service: @service
|
48
|
+
)
|
49
|
+
puts "#{@service} service has been scaled down to 0 and destroyed." unless @options[:mute]
|
50
|
+
end
|
51
|
+
|
52
|
+
def are_you_sure?
|
53
|
+
return true if @options[:force]
|
54
|
+
puts "You are about to destroy #{@service} service on #{@cluster} cluster."
|
55
|
+
print "Are you sure you want to do this? (y/n) "
|
56
|
+
answer = $stdin.gets.strip
|
57
|
+
answer =~ /^y/
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Ufo
|
2
|
+
class DockerBuilder
|
3
|
+
include PrettyTime
|
4
|
+
include Execute
|
5
|
+
|
6
|
+
def initialize(options={})
|
7
|
+
@options = options
|
8
|
+
@project_root = options[:project_root] || '.'
|
9
|
+
@dockerfile = options[:dockerfile] || 'Dockerfile'
|
10
|
+
@image_namespace = options[:image_namespace] || 'ufo'
|
11
|
+
end
|
12
|
+
|
13
|
+
def build
|
14
|
+
start_time = Time.now
|
15
|
+
store_full_image_name
|
16
|
+
update_auth_token # call after store_full_image_name
|
17
|
+
|
18
|
+
command = "docker build -t #{full_image_name} -f #{@dockerfile} ."
|
19
|
+
say "Building docker image with:".green
|
20
|
+
say " #{command}".green
|
21
|
+
check_dockerfile_exists
|
22
|
+
command = "cd #{@project_root} && #{command}"
|
23
|
+
success = execute(command, use_system: true)
|
24
|
+
unless success
|
25
|
+
puts "The docker image fail to build. Are you sure the docker daemon is available? Try running: docker version"
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
29
|
+
took = Time.now - start_time
|
30
|
+
say "Docker image #{full_image_name} built. " + "Took #{pretty_time(took)}.".green
|
31
|
+
end
|
32
|
+
|
33
|
+
def push
|
34
|
+
update_auth_token
|
35
|
+
start_time = Time.now
|
36
|
+
message = "Pushed #{full_image_name} docker image."
|
37
|
+
if @options[:noop]
|
38
|
+
message = "NOOP #{message}"
|
39
|
+
else
|
40
|
+
execute("docker push #{full_image_name}", use_system: true)
|
41
|
+
end
|
42
|
+
took = Time.now - start_time
|
43
|
+
message << " Took #{pretty_time(took)}.".green
|
44
|
+
puts message unless @options[:mute]
|
45
|
+
end
|
46
|
+
|
47
|
+
def check_dockerfile_exists
|
48
|
+
unless File.exist?("#{@project_root}/#{@dockerfile}")
|
49
|
+
puts "#{@dockerfile} does not exist. Are you sure it exists?"
|
50
|
+
exit 1
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def update_auth_token
|
55
|
+
return unless ecr_image?
|
56
|
+
repo_domain = "https://#{image_name.split('/').first}"
|
57
|
+
auth = EcrAuth.new(repo_domain)
|
58
|
+
auth.update
|
59
|
+
end
|
60
|
+
|
61
|
+
def ecr_image?
|
62
|
+
full_image_name =~ /\.amazonaws\.com/
|
63
|
+
end
|
64
|
+
|
65
|
+
# full_image - does not include the tag
|
66
|
+
def image_name
|
67
|
+
settings.data["image"]
|
68
|
+
end
|
69
|
+
|
70
|
+
# full_image - includes the tag
|
71
|
+
def full_image_name
|
72
|
+
unless File.exist?(docker_name_path)
|
73
|
+
puts "Unable to find #{docker_name_path} which contains the last docker image name that was used as a part of `ufo docker build`. Please run `ufo docker build` first."
|
74
|
+
exit 1
|
75
|
+
end
|
76
|
+
IO.read(docker_name_path).strip
|
77
|
+
end
|
78
|
+
|
79
|
+
# Store this in a file because this name gets reference in other tasks later
|
80
|
+
# and we want the image name to stay the same when the commands are run separate
|
81
|
+
# in different processes. So we store the state in a file.
|
82
|
+
# Only when a new docker build command gets run will the image name state be updated.
|
83
|
+
def store_full_image_name
|
84
|
+
dirname = File.dirname(docker_name_path)
|
85
|
+
FileUtils.mkdir_p(dirname) unless File.exist?(dirname)
|
86
|
+
full_image_name = "#{image_name}:#{@image_namespace}-#{timestamp}-#{git_sha}"
|
87
|
+
IO.write(docker_name_path, full_image_name)
|
88
|
+
IO.write("#{@project_root}/ufo/version", full_image_name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def docker_name_path
|
92
|
+
# output gets entirely wiped by tasks builder so dotn use that folder
|
93
|
+
"#{@project_root}/ufo/docker_image_name_#{@image_namespace}.txt"
|
94
|
+
end
|
95
|
+
|
96
|
+
def timestamp
|
97
|
+
@timestamp ||= Time.now.strftime('%Y-%m-%dT%H-%M-%S')
|
98
|
+
end
|
99
|
+
|
100
|
+
def git_sha
|
101
|
+
return @git_sha if @git_sha
|
102
|
+
# always call this and dont use the execute method because of the noop option
|
103
|
+
@git_sha = `cd #{@project_root} && git rev-parse --short HEAD`
|
104
|
+
@git_sha.strip!
|
105
|
+
end
|
106
|
+
|
107
|
+
def settings
|
108
|
+
@settings ||= Settings.new(@project_root)
|
109
|
+
end
|
110
|
+
|
111
|
+
def update_dockerfile
|
112
|
+
updater = DockerfileUpdater.new(full_image_name, @options)
|
113
|
+
updater.update
|
114
|
+
end
|
115
|
+
|
116
|
+
def say(msg)
|
117
|
+
puts msg unless @options[:mute]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Ufo
|
2
|
+
class DockerCleaner
|
3
|
+
include Execute
|
4
|
+
|
5
|
+
def initialize(docker_image_name, options)
|
6
|
+
# docker_image_name does not containg the tag
|
7
|
+
# Example: 123456789.dkr.ecr.us-east-1.amazonaws.com/image
|
8
|
+
@docker_image_name = docker_image_name
|
9
|
+
@options = options
|
10
|
+
@keep = options[:keep] || 3
|
11
|
+
@tag_prefix = options[:tag_prefix] || "ufo"
|
12
|
+
end
|
13
|
+
|
14
|
+
def cleanup
|
15
|
+
return if delete_list.empty?
|
16
|
+
command = "docker rmi #{delete_list}"
|
17
|
+
say "Cleaning up docker images...".green
|
18
|
+
say "Running: #{"docker rmi #{delete_list}"}"
|
19
|
+
return if @options[:noop]
|
20
|
+
execute(command, use_system: false) # to use_system: false silence output
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete_list
|
24
|
+
return @delete_list if @delete_list
|
25
|
+
|
26
|
+
out = execute("docker images", live: true) # live to override the noop cli options
|
27
|
+
name_regexp = Regexp.new(@docker_image_name)
|
28
|
+
# Example tag: ufo-2016-10-19T00-36-47-211b63a
|
29
|
+
tag_string = "#{@tag_prefix}-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-.{7}"
|
30
|
+
tag_regexp = Regexp.new(tag_string)
|
31
|
+
filtered_out = out.split("\n").select do |line|
|
32
|
+
name,tag = line.split(' ')
|
33
|
+
name =~ name_regexp && tag =~ tag_regexp
|
34
|
+
end
|
35
|
+
|
36
|
+
tags = filtered_out.map { |l| l.split(' ')[1] } # 2nd column is tag
|
37
|
+
tags = tags.sort.reverse # ordered by most recent images first
|
38
|
+
delete_tags = tags[@keep..-1]
|
39
|
+
if delete_tags.nil?
|
40
|
+
say "No images found that matched #{@docker_image_name}:#{tag_string}"
|
41
|
+
@delete_list = []
|
42
|
+
else
|
43
|
+
@delete_list = delete_tags.map { |t| "#{@docker_image_name}:#{t}" }.join(' ')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def say(msg)
|
48
|
+
msg = "NOOP #{msg}" if @options[:noop]
|
49
|
+
puts msg unless @options[:mute]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Ufo
|
2
|
+
class DockerfileUpdater
|
3
|
+
def initialize(full_image_name, options={})
|
4
|
+
@full_image_name = full_image_name
|
5
|
+
@options = options
|
6
|
+
@project_root = options[:project_root] || '.'
|
7
|
+
end
|
8
|
+
|
9
|
+
def update
|
10
|
+
write_new_dockerfile
|
11
|
+
end
|
12
|
+
|
13
|
+
def current_dockerfile
|
14
|
+
@current_dockerfile ||= IO.read(dockerfile_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def dockerfile_path
|
18
|
+
"#{@project_root}/Dockerfile"
|
19
|
+
end
|
20
|
+
|
21
|
+
def new_dockerfile
|
22
|
+
lines = current_dockerfile.split("\n")
|
23
|
+
# replace FROM line
|
24
|
+
new_lines = lines.map do |line|
|
25
|
+
if line =~ /^FROM /
|
26
|
+
"FROM #{@full_image_name}"
|
27
|
+
else
|
28
|
+
line
|
29
|
+
end
|
30
|
+
end
|
31
|
+
new_lines.join("\n") + "\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
def write_new_dockerfile
|
35
|
+
IO.write(dockerfile_path, new_dockerfile)
|
36
|
+
unless @options[:mute]
|
37
|
+
puts "The Dockerfile FROM statement has been updated with the latest base image:".green
|
38
|
+
puts " #{@full_image_name}".green
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/ufo/dsl.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Ufo
|
4
|
+
autoload :TaskDefinition, 'ufo/dsl/task_definition'
|
5
|
+
autoload :Outputter, 'ufo/dsl/outputter'
|
6
|
+
autoload :Helper, 'ufo/dsl/helper'
|
7
|
+
|
8
|
+
class DSL
|
9
|
+
def initialize(template_definitions_path, options={})
|
10
|
+
@template_definitions_path = template_definitions_path
|
11
|
+
@options = options
|
12
|
+
@project_root = options[:project_root] || '.'
|
13
|
+
@task_definitions = []
|
14
|
+
@outputters = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
evaluate_template_definitions
|
19
|
+
build_task_definitions
|
20
|
+
write_outputs
|
21
|
+
end
|
22
|
+
|
23
|
+
# All we're doing at this point is saving blocks of code into memory
|
24
|
+
# The instance_eval provides the task_definition and helper methods as they are part
|
25
|
+
# of this class.
|
26
|
+
def evaluate_template_definitions
|
27
|
+
source_code = IO.read(@template_definitions_path)
|
28
|
+
instance_eval(source_code, @template_definitions_path)
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_task_definitions
|
32
|
+
puts "Generating Task Definitions:" unless @options[:quiet]
|
33
|
+
clean_existing_task_definitions
|
34
|
+
@task_definitions.each do |task|
|
35
|
+
erb_result = task.build
|
36
|
+
@outputters << Outputter.new(task.task_definition_name, erb_result, @options)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def clean_existing_task_definitions
|
41
|
+
# removing 1 file a a time instead of recursing removing the directory to be safe
|
42
|
+
Dir.glob("#{@options[:project_root]}/ufo/output/*").each do |path|
|
43
|
+
FileUtils.rm_f(path)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def write_outputs
|
48
|
+
@outputters.each do |outputter|
|
49
|
+
outputter.write
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# methods available in task_definitionintions
|
54
|
+
def task_definition(name, &block)
|
55
|
+
@task_definitions << TaskDefinition.new(self, name, @options, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def env_vars(text)
|
59
|
+
lines = filtered_lines(text)
|
60
|
+
lines.map do |line|
|
61
|
+
key,value = line.strip.split("=").map {|x| x.strip}
|
62
|
+
{
|
63
|
+
name: key,
|
64
|
+
value: value,
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def env_file(path)
|
70
|
+
full_path = "#{@project_root}/#{path}"
|
71
|
+
unless File.exist?(full_path)
|
72
|
+
puts "The #{full_path} env file could not be found. Are you sure it exists?"
|
73
|
+
exit 1
|
74
|
+
end
|
75
|
+
text = IO.read(full_path)
|
76
|
+
env_vars(text)
|
77
|
+
end
|
78
|
+
|
79
|
+
def filtered_lines(text)
|
80
|
+
lines = text.split("\n")
|
81
|
+
# remove comment at the end of the line
|
82
|
+
lines.map! { |l| l.sub(/#.*/,'').strip }
|
83
|
+
# filter out commented lines
|
84
|
+
lines = lines.reject { |l| l =~ /(^|\s)#/i }
|
85
|
+
# filter out empty lines
|
86
|
+
lines = lines.reject { |l| l.strip.empty? }
|
87
|
+
end
|
88
|
+
|
89
|
+
def helper
|
90
|
+
Helper.new(@options)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# Some of variables are from the Dockerfile and some are from other places.
|
2
|
+
#
|
3
|
+
# * helper.full_image_name - Docker image name to be used when a the docker image is build. This is defined in ufo/settings.yml.
|
4
|
+
# * helper.dockerfile_port - Expose port in the Dockerfile. Only supports one exposed port, the first one that is encountered.
|
5
|
+
|
6
|
+
# Simply aggregates a bunch of variables that is useful for the task_definition.
|
7
|
+
module Ufo
|
8
|
+
class DSL
|
9
|
+
# provides some helperally context variables
|
10
|
+
class Helper
|
11
|
+
def initialize(options={})
|
12
|
+
@options = options
|
13
|
+
@project_root = options[:project_root] || '.'
|
14
|
+
end
|
15
|
+
|
16
|
+
##############
|
17
|
+
# helper variables
|
18
|
+
def dockerfile_port
|
19
|
+
dockerfile_path = "#{@project_root}/Dockerfile"
|
20
|
+
if File.exist?(dockerfile_path)
|
21
|
+
parse_for_dockerfile_port(dockerfile_path)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def full_image_name
|
26
|
+
DockerBuilder.new(@options).full_image_name
|
27
|
+
end
|
28
|
+
|
29
|
+
#############
|
30
|
+
# helper methods
|
31
|
+
def settings
|
32
|
+
@settings ||= Settings.new(@project_root)
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_for_dockerfile_port(dockerfile_path)
|
36
|
+
lines = IO.read(dockerfile_path).split("\n")
|
37
|
+
expose_line = lines.find { |l| l =~ /^EXPOSE / }
|
38
|
+
if expose_line
|
39
|
+
md = expose_line.match(/EXPOSE (\d+)/)
|
40
|
+
port = md[1] if md
|
41
|
+
end
|
42
|
+
port.to_i if port
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|