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.
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