dockly 1.5.8 → 1.5.9

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