dockly 1.5.8 → 1.5.9

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -4,3 +4,6 @@ source 'https://rubygems.org'
4
4
  gem 'foreman', :git => 'https://github.com/adamjt/foreman'
5
5
 
6
6
  gemspec
7
+
8
+ gem 'fog-core'
9
+ gem 'fog-json'
data/README.md CHANGED
@@ -16,6 +16,8 @@ Usage
16
16
  -----
17
17
 
18
18
  Once a `deb` block has been defined by the DSL below, dockly is invoked by either `bundle exec dockly build #{deb block name}` or `bundle exec rake dockly:deb:#{deb block name}`.
19
+ If looking to just build a `docker` block, run either `bundle exec dockly docker #{docker block name}` or `bundle exec rake dockly:docker:#{docker block name}`.
20
+ To build without exporting, run add either `--no-export` or `:noexport` to the CLI program or Rake task
19
21
 
20
22
  The DSL
21
23
  -------
@@ -142,6 +144,18 @@ The `docker` DSL is used to define Docker containers. It has the following attri
142
144
  - required: `false`
143
145
  - default: `[]`
144
146
  - description: a listing of references to build caches to run
147
+ - `s3_bucket`
148
+ - required: `false`
149
+ - default: `nil`
150
+ - description: S3 bucket for where the docker image is exported
151
+ - `s3_object_prefix`
152
+ - required: `false`
153
+ - default: `nil`
154
+ - description: prefix to be added to S3 exported docker images
155
+ - `tar_diff`
156
+ - required: `false`
157
+ - default: `false`
158
+ - description: after docker export, performs a diff between the base image and the new image
145
159
 
146
160
  In addition to the above attributes, `docker` has the following references:
147
161
 
data/Rakefile CHANGED
@@ -16,4 +16,5 @@ end
16
16
 
17
17
  Cane::RakeTask.new(:quality) do |cane|
18
18
  cane.canefile = '.cane'
19
+ cane.abc_exclude = %w(TarDiff#process)
19
20
  end
data/dockly.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = %w{lib}
16
16
  gem.version = Dockly::VERSION
17
17
  gem.add_dependency 'clamp', '~> 0.6'
18
- gem.add_dependency 'docker-api', '>= 1.8.4'
18
+ gem.add_dependency 'docker-api', '>= 1.13.1'
19
19
  gem.add_dependency 'dockly-util', '~> 0.0.8'
20
20
  gem.add_dependency 'excon'
21
21
  gem.add_dependency 'fog', '~> 1.21.0'
data/lib/dockly.rb CHANGED
@@ -11,9 +11,11 @@ module Dockly
11
11
 
12
12
  autoload :AWS, 'dockly/aws'
13
13
  autoload :Foreman, 'dockly/foreman'
14
+ autoload :BashBuilder, 'dockly/bash_builder'
14
15
  autoload :BuildCache, 'dockly/build_cache'
15
16
  autoload :Docker, 'dockly/docker'
16
17
  autoload :Deb, 'dockly/deb'
18
+ autoload :TarDiff, 'dockly/tar_diff'
17
19
 
18
20
  LOAD_FILE = 'dockly.rb'
19
21
 
data/lib/dockly/aws.rb CHANGED
@@ -4,6 +4,8 @@ require 'fog/aws'
4
4
  module Dockly::AWS
5
5
  extend self
6
6
 
7
+ autoload :S3Writer, 'dockly/aws/s3_writer'
8
+
7
9
  def service(name, klass)
8
10
  define_method name do
9
11
  if val = instance_variable_get(:"@#{name}")
@@ -0,0 +1,59 @@
1
+ module Dockly
2
+ module AWS
3
+ class S3Writer
4
+ include Dockly::Util::Logger::Mixin
5
+
6
+ logger_prefix '[dockly s3writer]'
7
+
8
+ attr_accessor :buffer
9
+ attr_reader :connection, :s3_bucket, :s3_object, :upload_id
10
+
11
+ def initialize(connection, s3_bucket, s3_object)
12
+ @connection = connection
13
+ @s3_bucket = s3_bucket
14
+ @s3_object = s3_object
15
+ @parts = []
16
+ @closed = false
17
+ @buffer = ""
18
+
19
+ init_upload_res = connection.initiate_multipart_upload(s3_bucket, s3_object)
20
+ @upload_id = init_upload_res.body['UploadId']
21
+ end
22
+
23
+ def upload_buffer
24
+ res = connection.upload_part(s3_bucket, s3_object, upload_id, @parts.size + 1, buffer)
25
+ @parts << res.headers["ETag"]
26
+ debug "Writing a chunk"
27
+ @buffer = ""
28
+ end
29
+
30
+ def write(chunk)
31
+ self.buffer << chunk
32
+
33
+ upload_buffer if buffer.bytesize > 5242880
34
+
35
+ chunk.length
36
+ end
37
+
38
+ def close
39
+ return if @closed
40
+ upload_buffer unless buffer.empty?
41
+
42
+ res = connection.complete_multipart_upload(s3_bucket, s3_object, upload_id, @parts)
43
+ if res.body['Code'] || res.body['Message']
44
+ raise "Failed to upload to S3: #{res.body['Code']}: #{res.body['Message']}"
45
+ end
46
+ @closed = true
47
+ end
48
+
49
+ def abort
50
+ connection.abort_multipart_upload(s3_bucket, s3_object, upload_id)
51
+ end
52
+
53
+ def abort_unless_closed
54
+ abort unless @closed
55
+ @closed = true
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,29 @@
1
+ require 'erb'
2
+ require 'ostruct'
3
+
4
+ module Dockly
5
+ class BashBuilder
6
+ SNIPPET_PATH = File.expand_path("../../../snippets", __FILE__)
7
+
8
+ def self.generate_snippet_for(name, opts, defaults={})
9
+ define_method(name) do |*args|
10
+ zipped_array = opts.zip(args).flatten
11
+ snippet = File.read(File.join(SNIPPET_PATH, "#{name}.erb"))
12
+ hash = Hash[*zipped_array].delete_if { |_,v| v.nil? }
13
+ data = defaults.merge(hash)
14
+ ERB.new(snippet).result(binding)
15
+ end
16
+ end
17
+
18
+ generate_snippet_for :normalize_for_dockly, []
19
+ generate_snippet_for :get_from_s3, [:s3_url, :output_path], { :output_path => "-" }
20
+ generate_snippet_for :install_package, [:path]
21
+ generate_snippet_for :get_and_install_deb, [:s3_url, :deb_path]
22
+ generate_snippet_for :docker_import, [:repo, :tag], { :tag => "latest" }
23
+ generate_snippet_for :file_docker_import, [:path, :repo, :tag]
24
+ generate_snippet_for :file_diff_docker_import, [:base_image, :diff_image, :repo, :tag]
25
+ generate_snippet_for :s3_docker_import, [:s3_url, :repo, :tag]
26
+ generate_snippet_for :s3_diff_docker_import, [:base_image, :diff_image, :repo, :tag]
27
+ generate_snippet_for :registry_import, [:repo, :tag], { :tag => "latest" }
28
+ end
29
+ end
@@ -1,11 +1,11 @@
1
1
  module Dockly::BuildCache
2
2
  end
3
3
 
4
- require 'dockly/build_cache/base'
5
- require 'dockly/build_cache/docker'
6
- require 'dockly/build_cache/local'
7
-
8
4
  module Dockly::BuildCache
5
+ autoload :Base, 'dockly/build_cache/base'
6
+ autoload :Docker, 'dockly/build_cache/docker'
7
+ autoload :Local, 'dockly/build_cache/local'
8
+
9
9
  class << self
10
10
  attr_writer :model
11
11
 
@@ -59,7 +59,7 @@ class Dockly::BuildCache::Docker < Dockly::BuildCache::Base
59
59
  container.wait(3600) # 1 hour max timeout
60
60
  debug 'Restarting the container to copy the cache\'s output'
61
61
  # Restart the container so we can copy its output
62
- container = container.commit.run('sleep 3600').tap(&:start)
62
+ container = container.commit.run('sleep 3600')
63
63
  container.copy(output_directory) { |chunk| file.write(chunk.to_s) }
64
64
  container.kill
65
65
  file.tap(&:rewind)
@@ -88,7 +88,7 @@ class Dockly::BuildCache::Docker < Dockly::BuildCache::Base
88
88
  resp = ""
89
89
  debug "running command `#{command}` on image #{image.id}"
90
90
  container = image.run(["/bin/bash", "-lc", "cd #{command_directory} && #{command}"])
91
- container.attach { |source,chunk| resp += chunk }
91
+ container.attach(logs: true) { |source,chunk| resp += chunk }
92
92
  status = container.wait['StatusCode']
93
93
  debug "`#{command}` returned the following output:"
94
94
  debug resp.strip
data/lib/dockly/cli.rb CHANGED
@@ -30,10 +30,44 @@ class Dockly::BuildCommand < Dockly::AbstractCommand
30
30
  end
31
31
  end
32
32
 
33
+ class Dockly::DockerCommand < Dockly::AbstractCommand
34
+ parameter 'DOCKER', 'the name to generate the docker image for', :attribute_name => :docker_name
35
+ option ['-f', '--force'], :flag, 'force the package build', :default => false, :attribute_name => :force
36
+ option ['-n', '--no-export'], :flag, 'do not export', :default => false, :attribute_name => :noexport
37
+
38
+ def execute
39
+ super
40
+ if docker = Dockly.dockers[docker_name.to_sym]
41
+ if force? || !docker.exists?
42
+ if noexport?
43
+ docker.generate_build
44
+ else
45
+ docker.generate!
46
+ end
47
+ else
48
+ puts "Package already exists!"
49
+ end
50
+ end
51
+ end
52
+ end
53
+
33
54
  class Dockly::ListCommand < Dockly::AbstractCommand
34
55
  def execute
35
56
  super
36
- Dockly.debs.each_with_index do |(name, package), index|
57
+ dockers = Dockly.dockers.dup
58
+ debs = Dockly.debs
59
+
60
+ puts "Debs" unless debs.empty?
61
+ debs.each_with_index do |(name, package), index|
62
+ puts "#{index + 1}. #{name}"
63
+ if package.docker
64
+ dockers.delete(package.docker.name)
65
+ puts " - Docker: #{package.docker.name}"
66
+ end
67
+ end
68
+
69
+ puts "Dockers" unless dockers.empty?
70
+ dockers.each_with_index do |(name, docker), index|
37
71
  puts "#{index + 1}. #{name}"
38
72
  end
39
73
  end
@@ -68,6 +102,7 @@ end
68
102
 
69
103
  class Dockly::Cli < Dockly::AbstractCommand
70
104
  subcommand ['build', 'b'], 'Create package', Dockly::BuildCommand
105
+ subcommand ['docker', 'd'], 'Generate docker image', Dockly::DockerCommand
71
106
  subcommand ['list', 'l'], 'List packages', Dockly::ListCommand
72
107
  subcommand ['build_cache', 'bc'], 'Build Cache commands', Dockly::BuildCacheCommand
73
108
  end
data/lib/dockly/deb.rb CHANGED
@@ -6,8 +6,8 @@ class Dockly::Deb
6
6
 
7
7
  logger_prefix '[dockly deb]'
8
8
  dsl_attribute :package_name, :version, :release, :arch, :build_dir,
9
- :pre_install, :post_install, :pre_uninstall, :post_uninstall,
10
- :s3_bucket, :files, :app_user
9
+ :deb_build_dir, :pre_install, :post_install, :pre_uninstall,
10
+ :post_uninstall, :s3_bucket, :files, :app_user
11
11
 
12
12
  dsl_class_attribute :docker, Dockly::Docker
13
13
  dsl_class_attribute :foreman, Dockly::Foreman
@@ -15,7 +15,8 @@ class Dockly::Deb
15
15
  default_value :version, '0.0'
16
16
  default_value :release, '0'
17
17
  default_value :arch, 'x86_64'
18
- default_value :build_dir, 'build/deb'
18
+ default_value :build_dir, 'build'
19
+ default_value :deb_build_dir, 'deb'
19
20
  default_value :files, []
20
21
  default_value :app_user, 'nobody'
21
22
 
@@ -24,8 +25,8 @@ class Dockly::Deb
24
25
  end
25
26
 
26
27
  def create_package!
27
- ensure_present! :build_dir
28
- FileUtils.mkdir_p(build_dir)
28
+ ensure_present! :build_dir, :deb_build_dir
29
+ FileUtils.mkdir_p(File.join(build_dir, deb_build_dir))
29
30
  FileUtils.rm(build_path) if File.exist?(build_path)
30
31
  debug "exporting #{package_name} to #{build_path}"
31
32
  build_package
@@ -46,8 +47,8 @@ class Dockly::Deb
46
47
  end
47
48
 
48
49
  def build_path
49
- ensure_present! :build_dir
50
- "#{build_dir}/#{output_filename}"
50
+ ensure_present! :build_dir, :deb_build_dir
51
+ File.join(build_dir, deb_build_dir, output_filename)
51
52
  end
52
53
 
53
54
  def exists?
@@ -63,7 +64,7 @@ class Dockly::Deb
63
64
 
64
65
  def upload_to_s3
65
66
  return if s3_bucket.nil?
66
- create_package! unless File.exist?(build_path)
67
+ raise "Package wasn't created!" unless File.exist?(build_path)
67
68
  info "uploading package to s3"
68
69
  Dockly::AWS.s3.put_bucket(s3_bucket) rescue nil
69
70
  Dockly::AWS.s3.put_object(s3_bucket, s3_object_name, File.new(build_path))
@@ -81,16 +82,26 @@ class Dockly::Deb
81
82
  "#{package_name}_#{version}.#{release}_#{arch}.deb"
82
83
  end
83
84
 
85
+ def startup_script
86
+ scripts = []
87
+ bb = Dockly::BashBuilder.new
88
+ scripts << bb.normalize_for_dockly
89
+ scripts << bb.get_and_install_deb(s3_url, "/opt/dockly/#{File.basename(s3_url)}")
90
+
91
+ scripts.join("\n")
92
+ end
93
+
84
94
  private
85
95
  def build_package
86
96
  ensure_present! :package_name, :version, :release, :arch
87
97
 
88
98
  info "building #{package_name}"
89
99
  @dir_package = FPM::Package::Dir.new
90
- add_docker(@dir_package)
91
100
  add_foreman(@dir_package)
92
101
  add_files(@dir_package)
93
102
  add_docker_auth_config(@dir_package)
103
+ add_docker(@dir_package)
104
+ add_startup_script(@dir_package)
94
105
 
95
106
  debug "converting to deb"
96
107
  @deb_package = @dir_package.convert(FPM::Package::Deb)
@@ -119,18 +130,6 @@ private
119
130
  package.attributes[:prefix] = nil
120
131
  end
121
132
 
122
- def add_docker(package)
123
- return if docker.nil?
124
- info "adding docker image"
125
- docker.generate!
126
- return unless docker.registry.nil?
127
- package.attributes[:prefix] = docker.package_dir
128
- Dir.chdir(File.dirname(docker.tar_path)) do
129
- package.input(File.basename(docker.tar_path))
130
- end
131
- package.attributes[:prefix] = nil
132
- end
133
-
134
133
  def add_files(package)
135
134
  return if files.empty?
136
135
  info "adding files to package"
@@ -144,14 +143,71 @@ private
144
143
  end
145
144
 
146
145
  def add_docker_auth_config(package)
147
- return if docker.nil? || (registry = docker.registry).nil? || !registry.authentication_required?
146
+ return if (registry = get_registry).nil? || !registry.authentication_required?
148
147
  info "adding docker config file"
149
148
  registry.generate_config_file!
150
149
 
151
- package.attributes[:prefix] = docker.auth_config_file || "~#{app_user}/.dockercfg"
150
+ package.attributes[:prefix] = registry.auth_config_file || "~#{app_user}"
152
151
  Dir.chdir(File.dirname(registry.config_file)) do
153
152
  package.input(File.basename(registry.config_file))
154
153
  end
155
154
  package.attributes[:prefix] = nil
156
155
  end
156
+
157
+ def add_docker(package)
158
+ return if docker.nil? || docker.s3_bucket
159
+ info "adding docker image"
160
+ docker.generate!
161
+ return unless docker.registry.nil?
162
+ package.attributes[:prefix] = docker.package_dir
163
+ Dir.chdir(File.dirname(docker.tar_path)) do
164
+ package.input(File.basename(docker.tar_path))
165
+ end
166
+ package.attributes[:prefix] = nil
167
+ end
168
+
169
+ def get_registry
170
+ if docker && registry = docker.registry
171
+ registry
172
+ end
173
+ end
174
+
175
+ def post_startup_script
176
+ scripts = ["#!/bin/bash"]
177
+ bb = Dockly::BashBuilder.new
178
+ scripts << bb.normalize_for_dockly
179
+ if get_registry
180
+ scripts << bb.registry_import(docker.repo, docker.tag)
181
+ elsif docker
182
+ if docker.s3_bucket.nil?
183
+ docker_output = File.join(docker.package_dir, docker.export_filename)
184
+ if docker.tar_diff
185
+ scripts << bb.file_diff_docker_import(docker.import, docker_output, docker.name, docker.tag)
186
+ else
187
+ scripts << bb.file_docker_import(docker_output, docker.name, docker.tag)
188
+ end
189
+ else
190
+ if docker.tar_diff
191
+ scripts << bb.s3_diff_docker_import(docker.import, docker.s3_url, docker.name, docker.tag)
192
+ else
193
+ scripts << bb.s3_docker_import(docker.s3_url, docker.name, docker.tag)
194
+ end
195
+ end
196
+ end
197
+ scripts.join("\n")
198
+ end
199
+
200
+ def add_startup_script(package)
201
+ ensure_present! :build_dir
202
+ startup_script_path = File.join(build_dir, "dockly-startup.sh")
203
+ File.open(startup_script_path, 'w+') do |f|
204
+ f.write(post_startup_script)
205
+ f.chmod(0755)
206
+ end
207
+ package.attributes[:prefix] = "/opt/dockly"
208
+ Dir.chdir(build_dir) do
209
+ package.input("dockly-startup.sh")
210
+ end
211
+ package.attributes[:prefix] = nil
212
+ end
157
213
  end
data/lib/dockly/docker.rb CHANGED
@@ -17,13 +17,16 @@ class Dockly::Docker
17
17
  dsl_class_attribute :registry, Dockly::Docker::Registry
18
18
 
19
19
  dsl_attribute :name, :import, :git_archive, :build, :tag, :build_dir, :package_dir,
20
- :timeout, :cleanup_images, :auth_config_file
20
+ :timeout, :cleanup_images, :tar_diff, :s3_bucket, :s3_object_prefix
21
21
 
22
22
  default_value :tag, nil
23
23
  default_value :build_dir, 'build/docker'
24
24
  default_value :package_dir, '/opt/docker'
25
25
  default_value :cleanup_images, false
26
26
  default_value :timeout, 60
27
+ default_value :tar_diff, false
28
+ default_value :s3_bucket, nil
29
+ default_value :s3_object_prefix, ""
27
30
 
28
31
  def generate!
29
32
  image = generate_build
@@ -171,6 +174,10 @@ class Dockly::Docker
171
174
  end
172
175
  end
173
176
 
177
+ def s3_url
178
+ "s3://#{s3_bucket}/#{s3_object}"
179
+ end
180
+
174
181
  def export_image(image)
175
182
  ensure_present! :name
176
183
  if registry.nil?
@@ -178,22 +185,74 @@ class Dockly::Docker
178
185
  info "Exporting the image with id #{image.id} to file #{File.expand_path(tar_path)}"
179
186
  container = image.run('true')
180
187
  info "created the container: #{container.id}"
181
- if ENV['SHELL_EXPORT']
182
- command = "docker export #{container.id} | gzip > #{tar_path}"
183
- info "running '#{command}' to export #{container.id}"
184
- system(command)
185
- raise "Could not export image, exit code: #{$?}" unless $?.success?
188
+
189
+ unless s3_bucket.nil?
190
+ output = Dockly::AWS::S3Writer.new(connection, s3_bucket, s3_object)
186
191
  else
187
- Zlib::GzipWriter.open(tar_path) do |file|
188
- container.export do |chunk, remaining, total|
189
- file.write(chunk)
190
- end
191
- end
192
+ output = File.open(tar_path, 'wb')
193
+ end
194
+
195
+ gzip_output = Zlib::GzipWriter.new(output)
196
+
197
+ if tar_diff
198
+ export_image_diff(container, gzip_output)
199
+ else
200
+ export_image_whole(container, gzip_output)
192
201
  end
193
- info "done writing the docker tar: #{export_filename}"
194
202
  else
195
203
  push_to_registry(image)
196
204
  end
205
+ rescue
206
+ if output && !s3_bucket.nil?
207
+ output.abort_unless_closed
208
+ end
209
+ raise
210
+ ensure
211
+ gzip_output.close if gzip_output
212
+ end
213
+
214
+ def export_image_whole(container, output)
215
+ container.export do |chunk, remaining, total|
216
+ output.write(chunk)
217
+ end
218
+ end
219
+
220
+ def export_image_diff(container, output)
221
+ rd, wr = IO.pipe(Encoding::ASCII_8BIT)
222
+
223
+ rd.binmode
224
+ wr.binmode
225
+
226
+ thread = Thread.new do
227
+ begin
228
+ if Dockly::Util::Tar.is_tar?(fetch_import)
229
+ base = File.open(fetch_import, 'rb')
230
+ else
231
+ base = Zlib::GzipReader.new(File.open(fetch_import, 'rb'))
232
+ end
233
+ td = Dockly::TarDiff.new(base, rd, output)
234
+ td.process
235
+ info "done writing the docker tar: #{export_filename}"
236
+ ensure
237
+ base.close if base
238
+ rd.close
239
+ end
240
+ end
241
+
242
+ begin
243
+ container.export do |chunk, remaining, total|
244
+ wr.write(chunk)
245
+ end
246
+ ensure
247
+ wr.close
248
+ thread.join
249
+ end
250
+ end
251
+
252
+ def s3_object
253
+ output = "#{s3_object_prefix}"
254
+ output << "#{Dockly::Util::Git.git_sha}/"
255
+ output << "#{export_filename}"
197
256
  end
198
257
 
199
258
  def push_to_registry(image)
@@ -235,6 +294,18 @@ class Dockly::Docker
235
294
  path
236
295
  end
237
296
 
297
+ def exists?
298
+ return false unless s3_bucket
299
+ debug "#{name}: checking for package: #{s3_url}"
300
+ Dockly::AWS.s3.head_object(s3_bucket, s3_object)
301
+ info "#{name}: found package: #{s3_url}"
302
+ true
303
+ rescue
304
+ info "#{name}: could not find package: " +
305
+ "#{s3_url}"
306
+ false
307
+ end
308
+
238
309
  def repository(value = nil)
239
310
  name(value)
240
311
  end