shipitron 0.1.0 → 0.2.0

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +4 -0
  3. data/.gitignore +4 -0
  4. data/.rspec +3 -0
  5. data/Dockerfile +60 -0
  6. data/Dockerfile.release +49 -0
  7. data/README.md +44 -10
  8. data/build_dev.sh +39 -0
  9. data/exe/shipitron +5 -0
  10. data/lib/shipitron/cli.rb +132 -0
  11. data/lib/shipitron/client/bootstrap_application.rb +35 -0
  12. data/lib/shipitron/client/create_ecs_services.rb +81 -0
  13. data/lib/shipitron/client/deploy_application.rb +34 -0
  14. data/lib/shipitron/client/ensure_deploy_not_running.rb +54 -0
  15. data/lib/shipitron/client/load_application_config.rb +41 -0
  16. data/lib/shipitron/client/load_templates.rb +45 -0
  17. data/lib/shipitron/client/register_ecs_task_definitions.rb +75 -0
  18. data/lib/shipitron/client/run_ecs_tasks.rb +141 -0
  19. data/lib/shipitron/consul_keys.rb +34 -0
  20. data/lib/shipitron/consul_lock.rb +28 -0
  21. data/lib/shipitron/docker_image.rb +23 -0
  22. data/lib/shipitron/ecs_client.rb +22 -0
  23. data/lib/shipitron/ecs_task_def.rb +17 -0
  24. data/lib/shipitron/fetch_bucket.rb +26 -0
  25. data/lib/shipitron/logger.rb +33 -0
  26. data/lib/shipitron/mustache_yaml_parser.rb +22 -0
  27. data/lib/shipitron/parse_templates.rb +32 -0
  28. data/lib/shipitron/post_build.rb +32 -0
  29. data/lib/shipitron/server/deploy_application.rb +75 -0
  30. data/lib/shipitron/server/docker/build_image.rb +25 -0
  31. data/lib/shipitron/server/docker/configure.rb +35 -0
  32. data/lib/shipitron/server/docker/push_image.rb +37 -0
  33. data/lib/shipitron/server/docker/run_build_script.rb +53 -0
  34. data/lib/shipitron/server/download_build_cache.rb +44 -0
  35. data/lib/shipitron/server/ecs_task_defs/map_parsed_templates.rb +31 -0
  36. data/lib/shipitron/server/ecs_task_defs/update_from_params.rb +42 -0
  37. data/lib/shipitron/server/ecs_task_defs/update_in_place.rb +68 -0
  38. data/lib/shipitron/server/git/clone_local_copy.rb +46 -0
  39. data/lib/shipitron/server/git/configure.rb +40 -0
  40. data/lib/shipitron/server/git/download_cache.rb +56 -0
  41. data/lib/shipitron/server/git/pull_repo.rb +30 -0
  42. data/lib/shipitron/server/git/update_cache.rb +52 -0
  43. data/lib/shipitron/server/git/upload_cache.rb +61 -0
  44. data/lib/shipitron/server/run_post_build.rb +79 -0
  45. data/lib/shipitron/server/transform_cli_args.rb +85 -0
  46. data/lib/shipitron/server/update_ecs_services.rb +105 -0
  47. data/lib/shipitron/server/update_ecs_task_definitions.rb +47 -0
  48. data/lib/shipitron/server/upload_build_cache.rb +43 -0
  49. data/lib/shipitron/smash.rb +10 -0
  50. data/lib/shipitron/version.rb +1 -1
  51. data/lib/shipitron.rb +38 -0
  52. data/scripts/docker-entrypoint.sh +18 -0
  53. data/scripts/fetch-bundler-data.sh +16 -0
  54. data/shipitron.gemspec +14 -0
  55. metadata +236 -4
@@ -0,0 +1,53 @@
1
+ require 'shipitron'
2
+
3
+ module Shipitron
4
+ module Server
5
+ module Docker
6
+ class RunBuildScript
7
+ include Metaractor
8
+
9
+ required :application
10
+ required :docker_image
11
+ required :git_sha
12
+ optional :build_script
13
+
14
+ before do
15
+ context.build_script ||= 'shipitron/build.sh'
16
+ end
17
+
18
+ def call
19
+ Logger.info 'Building docker image'
20
+
21
+ docker_image.tag = git_sha
22
+
23
+ FileUtils.cd("/home/shipitron/#{application}") do
24
+ unless Pathname.new(build_script).exist?
25
+ fail_with_error!(message: "#{build_script} does not exist")
26
+ end
27
+ Logger.info `#{build_script} #{docker_image}`
28
+ if $? != 0
29
+ fail_with_error!(message: "build script exited with non-zero code: #{$?}")
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+ def application
36
+ context.application
37
+ end
38
+
39
+ def docker_image
40
+ context.docker_image
41
+ end
42
+
43
+ def git_sha
44
+ context.git_sha
45
+ end
46
+
47
+ def build_script
48
+ context.build_script
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,44 @@
1
+ require 'shipitron'
2
+ require 'shipitron/fetch_bucket'
3
+
4
+ module Shipitron
5
+ module Server
6
+ class DownloadBuildCache
7
+ include Metaractor
8
+
9
+ required :application
10
+ required :s3_cache_bucket
11
+
12
+ def call
13
+ Logger.info "Downloading build cache from bucket #{s3_cache_bucket}"
14
+
15
+ s3_file = bucket.files.get("#{application}.build-cache.tar.gz")
16
+ if s3_file.nil?
17
+ Logger.warn 'Build cache not found.'
18
+ return
19
+ end
20
+
21
+ build_cache = Pathname.new("/home/shipitron/#{application}/tmp/build-cache.tar.gz")
22
+ build_cache.parent.mkpath
23
+ build_cache.open('wb') do |local_file|
24
+ local_file.write(s3_file.body)
25
+ end
26
+
27
+ Logger.info 'Download complete.'
28
+ end
29
+
30
+ private
31
+ def application
32
+ context.application
33
+ end
34
+
35
+ def s3_cache_bucket
36
+ context.s3_cache_bucket
37
+ end
38
+
39
+ def bucket
40
+ @bucket ||= FetchBucket.call!(name: s3_cache_bucket).bucket
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ require 'shipitron'
2
+
3
+ module Shipitron
4
+ module Server
5
+ module EcsTaskDefs
6
+ class MapParsedTemplates
7
+ include Metaractor
8
+
9
+ required :ecs_task_defs
10
+ required :parsed_templates
11
+
12
+ def call
13
+ parsed_templates.each do |parsed|
14
+ task_def = ecs_task_defs.find {|t| t.name == parsed.family }
15
+ next if task_def.nil?
16
+ task_def.params = parsed
17
+ end
18
+ end
19
+
20
+ private
21
+ def ecs_task_defs
22
+ context.ecs_task_defs
23
+ end
24
+
25
+ def parsed_templates
26
+ context.parsed_templates
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,42 @@
1
+ require 'shipitron'
2
+ require 'shipitron/ecs_client'
3
+
4
+ module Shipitron
5
+ module Server
6
+ module EcsTaskDefs
7
+ class UpdateFromParams
8
+ include Metaractor
9
+ include EcsClient
10
+
11
+ required :region
12
+ required :ecs_task_defs
13
+
14
+ def call
15
+ ecs_task_defs.each do |ecs_task_def|
16
+ next if ecs_task_def.params.nil?
17
+
18
+ ecs_task_def.revision = ecs_client(region: region).register_task_definition(
19
+ ecs_task_def.params.to_h
20
+ ).task_definition.revision
21
+
22
+ Logger.info "Created task definition #{ecs_task_def}"
23
+ end
24
+ rescue Aws::ECS::Errors::ServiceError => e
25
+ fail_with_errors!(messages: [
26
+ "Error: #{e.message}",
27
+ e.backtrace.join("\n")
28
+ ])
29
+ end
30
+
31
+ private
32
+ def region
33
+ context.region
34
+ end
35
+
36
+ def ecs_task_defs
37
+ context.ecs_task_defs
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ require 'shipitron'
2
+ require 'shipitron/ecs_client'
3
+
4
+ module Shipitron
5
+ module Server
6
+ module EcsTaskDefs
7
+ class UpdateInPlace
8
+ include Metaractor
9
+ include EcsClient
10
+
11
+ required :region
12
+ required :ecs_task_defs
13
+
14
+ def call
15
+ ecs_task_defs.each do |ecs_task_def|
16
+ next if ecs_task_def.params != nil
17
+
18
+ existing_task = ecs_client(region: region).describe_task_definition(
19
+ task_definition: ecs_task_def.name
20
+ ).task_definition
21
+
22
+ updated_image = false
23
+ existing_task.container_definitions.each do |container_def|
24
+ container_def.image.match(/([^:]+)(?::.+)?/) do |m|
25
+ if m[1] == docker_image.name
26
+ container_def.image = docker_image.name_with_tag
27
+ updated_image = true
28
+ end
29
+ end
30
+ end
31
+
32
+ unless updated_image
33
+ fail_with_error!(
34
+ message: "Unable to update ECS task definition; #{docker_image.name} not found in task family #{ecs_task_def.name}."
35
+ )
36
+ end
37
+
38
+ existing_task = existing_task.to_h
39
+
40
+ ecs_task_def.revision = ecs_client(region: region).register_task_definition(
41
+ [
42
+ :family,
43
+ :container_definitions,
44
+ :volumes
45
+ ].each_with_object({}) { |k, hash| hash[k] = existing_task[k] if existing_task.has_key?(k) }
46
+ ).task_definition.revision
47
+
48
+ Logger.info "Created task definition #{ecs_task_def}"
49
+ end
50
+ rescue Aws::ECS::Errors::ServiceError => e
51
+ fail_with_errors!(messages: [
52
+ "Error: #{e.message}",
53
+ e.backtrace.join("\n")
54
+ ])
55
+ end
56
+
57
+ private
58
+ def region
59
+ context.region
60
+ end
61
+
62
+ def ecs_task_defs
63
+ context.ecs_task_defs
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,46 @@
1
+ require 'shipitron'
2
+ require 'shellwords'
3
+
4
+ module Shipitron
5
+ module Server
6
+ module Git
7
+ class CloneLocalCopy
8
+ include Metaractor
9
+
10
+ required :application
11
+ required :repository_url
12
+ optional :repository_branch
13
+
14
+ before do
15
+ context.repository_branch ||= 'master'
16
+ end
17
+
18
+ def call
19
+ Logger.info "Using this branch: #{repository_branch}"
20
+ FileUtils.cd('/home/shipitron') do
21
+ `git clone git-cache #{Shellwords.escape application} --recursive --branch #{Shellwords.escape repository_branch}`
22
+ end
23
+
24
+ Logger.info 'Using this git commit:'
25
+ FileUtils.cd("/home/shipitron/#{application}") do
26
+ context.git_sha = `git rev-parse --short=12 HEAD`.chomp
27
+ Logger.info `git --no-pager log --format='%aN (%h): %s' -n 1`.chomp
28
+ end
29
+ end
30
+
31
+ private
32
+ def application
33
+ context.application
34
+ end
35
+
36
+ def repository_url
37
+ context.repository_url
38
+ end
39
+
40
+ def repository_branch
41
+ context.repository_branch
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,40 @@
1
+ require 'shipitron'
2
+ require 'shipitron/consul_keys'
3
+
4
+ module Shipitron
5
+ module Server
6
+ module Git
7
+ class Configure
8
+ include Metaractor
9
+ include ConsulKeys
10
+
11
+ required :application
12
+ required :repository_url
13
+ required :s3_cache_bucket
14
+
15
+ before do
16
+ configure_consul_client!
17
+ end
18
+
19
+ def call
20
+ host_key = fetch_key!(key: "shipitron/#{application}/git_host_key")
21
+ Pathname.new('/home/shipitron/.ssh/known_hosts').open('a') do |file|
22
+ file.puts(host_key.to_s)
23
+ end
24
+
25
+ deploy_key = fetch_key!(key: "shipitron/#{application}/git_deploy_key")
26
+ Pathname.new('/home/shipitron/.ssh/id_rsa').open('w') do |file|
27
+ file.puts(deploy_key.to_s)
28
+ file.chmod(0600)
29
+ end
30
+ end
31
+
32
+ private
33
+ def application
34
+ context.application
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,56 @@
1
+ require 'shipitron'
2
+ require 'shipitron/fetch_bucket'
3
+ require 'archive/tar/minitar'
4
+
5
+ module Shipitron
6
+ module Server
7
+ module Git
8
+ class DownloadCache
9
+ include Metaractor
10
+
11
+ required :application
12
+ required :s3_cache_bucket
13
+
14
+ def call
15
+ Logger.info "Downloading git cache from bucket #{s3_cache_bucket}"
16
+
17
+ s3_file = bucket.files.get("#{application}.git.tar.gz")
18
+ if s3_file.nil?
19
+ Logger.warn 'Git cache not found.'
20
+ return
21
+ end
22
+
23
+ Pathname.new("/tmp/#{application}.git.tar.gz").open('wb') do |local_file|
24
+ local_file.write(s3_file.body)
25
+ end
26
+
27
+ extract_tarball(filename: "/tmp/#{application}.git.tar.gz", directory: '/home/shipitron')
28
+
29
+ Logger.info 'Download complete.'
30
+ end
31
+
32
+ private
33
+ def application
34
+ context.application
35
+ end
36
+
37
+ def s3_cache_bucket
38
+ context.s3_cache_bucket
39
+ end
40
+
41
+ def bucket
42
+ @bucket ||= FetchBucket.call!(name: s3_cache_bucket).bucket
43
+ end
44
+
45
+ def extract_tarball(filename:, directory:)
46
+ Pathname.new(filename).open('rb') do |tarball|
47
+ Zlib::GzipReader.wrap(tarball) do |gz|
48
+ Archive::Tar::Minitar.unpack(gz, directory)
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,30 @@
1
+ require 'shipitron'
2
+ require 'shipitron/server/git/configure'
3
+ require 'shipitron/server/git/download_cache'
4
+ require 'shipitron/server/git/update_cache'
5
+ require 'shipitron/server/git/upload_cache'
6
+ require 'shipitron/server/git/clone_local_copy'
7
+
8
+ module Shipitron
9
+ module Server
10
+ module Git
11
+ class PullRepo
12
+ include Metaractor
13
+ include Interactor::Organizer
14
+
15
+ required :application
16
+ required :repository_url
17
+ required :s3_cache_bucket
18
+ optional :repository_branch
19
+
20
+ organize [
21
+ Configure,
22
+ DownloadCache,
23
+ UpdateCache,
24
+ UploadCache,
25
+ CloneLocalCopy
26
+ ]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,52 @@
1
+ require 'shipitron'
2
+ require 'shellwords'
3
+
4
+ module Shipitron
5
+ module Server
6
+ module Git
7
+ class UpdateCache
8
+ include Metaractor
9
+
10
+ required :application
11
+ required :repository_url
12
+ required :s3_cache_bucket
13
+ optional :repository_branch
14
+
15
+ before do
16
+ context.repository_branch ||= 'master'
17
+ end
18
+
19
+ def call
20
+ if !Pathname.new('/home/shipitron/git-cache/objects').directory?
21
+ Logger.info 'Cloning the git repository'
22
+ FileUtils.cd('/home/shipitron') do
23
+ `git clone --bare #{Shellwords.escape repository_url} git-cache`
24
+ end
25
+ else
26
+ Logger.info 'Fetching new git commits'
27
+ FileUtils.cd('/home/shipitron/git-cache') do
28
+ `git fetch #{Shellwords.escape repository_url} #{Shellwords.escape repository_branch}:#{Shellwords.escape repository_branch}`
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+ def application
35
+ context.application
36
+ end
37
+
38
+ def repository_url
39
+ context.repository_url
40
+ end
41
+
42
+ def s3_cache_bucket
43
+ context.s3_cache_bucket
44
+ end
45
+
46
+ def repository_branch
47
+ context.repository_branch
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,61 @@
1
+ require 'shipitron'
2
+ require 'shipitron/fetch_bucket'
3
+ require 'find'
4
+ require 'archive/tar/minitar'
5
+
6
+ module Shipitron
7
+ module Server
8
+ module Git
9
+ class UploadCache
10
+ include Metaractor
11
+
12
+ required :application
13
+ required :s3_cache_bucket
14
+
15
+ def call
16
+ Logger.info "Uploading git cache to bucket #{s3_cache_bucket}"
17
+ create_tarball(filename: "/tmp/#{application}.git.tar.gz", directory: '/home/shipitron/git-cache')
18
+
19
+ Pathname.new("/tmp/#{application}.git.tar.gz").open('rb') do |local_file|
20
+ bucket.files.create(
21
+ key: "#{application}.git.tar.gz",
22
+ body: local_file.read
23
+ )
24
+ end
25
+ end
26
+
27
+ private
28
+ def application
29
+ context.application
30
+ end
31
+
32
+ def s3_cache_bucket
33
+ context.s3_cache_bucket
34
+ end
35
+
36
+ def create_tarball(filename:, directory:)
37
+ base_dir = Pathname.new(directory).parent
38
+ Logger.debug 'Creating tarball'
39
+ FileUtils.cd(base_dir) do
40
+ Pathname.new(filename).open('wb') do |tarball|
41
+ Zlib::GzipWriter.wrap(tarball) do |gz|
42
+ Archive::Tar::Minitar::Output.open(gz) do |tar|
43
+ Find.find(directory) do |path|
44
+ pn = Pathname.new(path)
45
+ name = pn.relative_path_from(base_dir)
46
+ Logger.debug(name)
47
+ Archive::Tar::Minitar.pack_file(name.to_s, tar)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def bucket
56
+ @bucket ||= FetchBucket.call!(name: s3_cache_bucket).bucket
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,79 @@
1
+ require 'shipitron'
2
+ require 'shipitron/ecs_client'
3
+
4
+ module Shipitron
5
+ module Server
6
+ class RunPostBuild
7
+ include Metaractor
8
+ include EcsClient
9
+
10
+ required :region
11
+ required :cluster_name
12
+ optional :post_builds
13
+
14
+ def call
15
+ return if post_builds.nil? || post_builds.empty?
16
+
17
+ Logger.info 'Running post build commands'
18
+
19
+ begin
20
+ post_builds.each do |post_build|
21
+ Logger.info "Running #{post_build.command}"
22
+ response = ecs_client(region: region).run_task(
23
+ cluster: cluster_name,
24
+ task_definition: post_build.ecs_task,
25
+ overrides: {
26
+ container_overrides: [
27
+ {
28
+ name: post_build.container_name,
29
+ command: [post_build.command]
30
+ }
31
+ ]
32
+ },
33
+ count: 1,
34
+ started_by: 'shipitron'
35
+ )
36
+
37
+ if !response.failures.empty?
38
+ response.failures.each do |failure|
39
+ fail_with_error! message: "ECS run_task failure: #{failure.arn}: #{failure.reason}"
40
+ end
41
+ end
42
+
43
+ task_arn = response.tasks.first.task_arn
44
+
45
+ Logger.info 'Waiting for task to finish'
46
+ loop do
47
+ response = ecs_client(region: region).describe_tasks(
48
+ cluster: cluster_name,
49
+ tasks: [task_arn]
50
+ )
51
+ Logger.info "Task status: #{response.tasks.first.last_status}"
52
+ break if response.tasks.first.last_status == 'STOPPED'.freeze
53
+ sleep 1
54
+ end
55
+ end
56
+
57
+ rescue Aws::ECS::Errors::ServiceError => e
58
+ fail_with_errors!(messages: [
59
+ "Error: #{e.message}",
60
+ e.backtrace.join("\n")
61
+ ])
62
+ end
63
+ end
64
+
65
+ private
66
+ def post_builds
67
+ context.post_builds
68
+ end
69
+
70
+ def region
71
+ context.region
72
+ end
73
+
74
+ def cluster_name
75
+ context.cluster_name
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,85 @@
1
+ require 'shipitron'
2
+ require 'shipitron/docker_image'
3
+ require 'shipitron/ecs_task_def'
4
+ require 'shipitron/post_build'
5
+ require 'base64'
6
+
7
+ module Shipitron
8
+ module Server
9
+ class TransformCliArgs
10
+ include Metaractor
11
+
12
+ required :application
13
+ required :repository_url
14
+ optional :repository_branch
15
+ required :s3_cache_bucket
16
+ required :image_name
17
+ required :region
18
+ required :cluster_name
19
+ required :ecs_task_defs
20
+ optional :ecs_task_def_templates
21
+ required :ecs_services
22
+ optional :ecs_service_templates
23
+ optional :build_script
24
+ optional :post_builds
25
+
26
+ before do
27
+ context.ecs_task_def_templates ||= []
28
+ context.ecs_service_templates ||= []
29
+ end
30
+
31
+ def call
32
+ cli_args = Smash.new
33
+
34
+ %i[
35
+ application
36
+ repository_url
37
+ repository_branch
38
+ s3_cache_bucket
39
+ region
40
+ cluster_name
41
+ ecs_services
42
+ build_script
43
+ ].each_with_object(cli_args) { |k, args| args[k] = context[k] }
44
+
45
+ cli_args.docker_image = DockerImage.new(name: context.image_name)
46
+
47
+ cli_args.ecs_task_defs = []
48
+ ecs_task_defs.each do |task_def|
49
+ cli_args.ecs_task_defs << EcsTaskDef.new(name: task_def)
50
+ end
51
+
52
+ cli_args.ecs_task_def_templates = []
53
+ context.ecs_task_def_templates.each do |template|
54
+ cli_args.ecs_task_def_templates << Base64.urlsafe_decode64(template)
55
+ end
56
+
57
+ cli_args.ecs_service_templates = []
58
+ context.ecs_service_templates.each do |template|
59
+ cli_args.ecs_service_templates << Base64.urlsafe_decode64(template)
60
+ end
61
+
62
+ Logger.debug "task_def_templates: #{cli_args.ecs_task_def_templates}"
63
+ Logger.debug "service_templates: #{cli_args.ecs_service_templates}"
64
+
65
+ if post_builds != nil && !post_builds.empty?
66
+ cli_args.post_builds = []
67
+ post_builds.each do |post_build_str|
68
+ cli_args.post_builds << PostBuild.parse(post_build_str)
69
+ end
70
+ end
71
+
72
+ context.cli_args = cli_args
73
+ end
74
+
75
+ private
76
+ def ecs_task_defs
77
+ context.ecs_task_defs
78
+ end
79
+
80
+ def post_builds
81
+ context.post_builds
82
+ end
83
+ end
84
+ end
85
+ end