ufo 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +3 -0
  4. data/CHANGELOG.md +12 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +128 -0
  7. data/Guardfile +12 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +227 -0
  10. data/Rakefile +6 -0
  11. data/bin/ufo +10 -0
  12. data/lib/starter_project/.env.prod +3 -0
  13. data/lib/starter_project/Dockerfile +20 -0
  14. data/lib/starter_project/bin/deploy +12 -0
  15. data/lib/starter_project/ufo/settings.yml +7 -0
  16. data/lib/starter_project/ufo/task_definitions.rb +44 -0
  17. data/lib/starter_project/ufo/templates/main.json.erb +39 -0
  18. data/lib/ufo.rb +26 -0
  19. data/lib/ufo/aws_services.rb +21 -0
  20. data/lib/ufo/cli.rb +136 -0
  21. data/lib/ufo/cli/help.rb +164 -0
  22. data/lib/ufo/defaults.rb +46 -0
  23. data/lib/ufo/destroy.rb +60 -0
  24. data/lib/ufo/docker_builder.rb +120 -0
  25. data/lib/ufo/docker_cleaner.rb +52 -0
  26. data/lib/ufo/dockerfile_updater.rb +42 -0
  27. data/lib/ufo/dsl.rb +93 -0
  28. data/lib/ufo/dsl/helper.rb +47 -0
  29. data/lib/ufo/dsl/outputter.rb +40 -0
  30. data/lib/ufo/dsl/task_definition.rb +65 -0
  31. data/lib/ufo/ecr_auth.rb +35 -0
  32. data/lib/ufo/ecr_cleaner.rb +66 -0
  33. data/lib/ufo/execute.rb +19 -0
  34. data/lib/ufo/init.rb +83 -0
  35. data/lib/ufo/pretty_time.rb +14 -0
  36. data/lib/ufo/scale.rb +34 -0
  37. data/lib/ufo/settings.rb +33 -0
  38. data/lib/ufo/ship.rb +436 -0
  39. data/lib/ufo/tasks_builder.rb +30 -0
  40. data/lib/ufo/tasks_register.rb +51 -0
  41. data/lib/ufo/templates/default.json.erb +39 -0
  42. data/lib/ufo/version.rb +3 -0
  43. data/spec/fixtures/home_existing/.docker/config.json +10 -0
  44. data/spec/lib/cli_spec.rb +50 -0
  45. data/spec/lib/ecr_auth_spec.rb +39 -0
  46. data/spec/lib/ecr_cleaner_spec.rb +32 -0
  47. data/spec/lib/ship_spec.rb +77 -0
  48. data/spec/spec_helper.rb +28 -0
  49. data/ufo.gemspec +34 -0
  50. metadata +267 -0
@@ -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
@@ -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