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 +3 -0
- data/README.md +14 -0
- data/Rakefile +1 -0
- data/dockly.gemspec +1 -1
- data/lib/dockly.rb +2 -0
- data/lib/dockly/aws.rb +2 -0
- data/lib/dockly/aws/s3_writer.rb +59 -0
- data/lib/dockly/bash_builder.rb +29 -0
- data/lib/dockly/build_cache.rb +4 -4
- data/lib/dockly/build_cache/docker.rb +2 -2
- data/lib/dockly/cli.rb +36 -1
- data/lib/dockly/deb.rb +79 -23
- data/lib/dockly/docker.rb +83 -12
- data/lib/dockly/docker/registry.rb +1 -1
- data/lib/dockly/rake_task.rb +31 -2
- data/lib/dockly/tar_diff.rb +150 -0
- data/lib/dockly/version.rb +1 -1
- data/snippets/docker_import.erb +1 -0
- data/snippets/file_diff_docker_import.erb +4 -0
- data/snippets/file_docker_import.erb +1 -0
- data/snippets/get_and_install_deb.erb +2 -0
- data/snippets/get_from_s3.erb +11 -0
- data/snippets/install_package.erb +1 -0
- data/snippets/normalize_for_dockly.erb +12 -0
- data/snippets/registry_import.erb +1 -0
- data/snippets/s3_diff_docker_import.erb +14 -0
- data/snippets/s3_docker_import.erb +4 -0
- data/spec/dockly/aws/s3_writer_spec.rb +154 -0
- data/spec/dockly/bash_builder_spec.rb +138 -0
- data/spec/dockly/build_cache/local_spec.rb +1 -1
- data/spec/dockly/deb_spec.rb +16 -8
- data/spec/dockly/docker_spec.rb +104 -3
- data/spec/dockly/tar_diff_spec.rb +85 -0
- data/spec/fixtures/test-1.tar +0 -0
- data/spec/fixtures/test-3.tar +0 -0
- metadata +25 -4
@@ -7,7 +7,7 @@ class Dockly::Docker::Registry
|
|
7
7
|
logger_prefix '[dockly docker registry]'
|
8
8
|
|
9
9
|
dsl_attribute :name, :server_address, :email, :username, :password,
|
10
|
-
:authentication_required
|
10
|
+
:authentication_required, :auth_config_file
|
11
11
|
|
12
12
|
default_value :server_address, DEFAULT_SERVER_ADDRESS
|
13
13
|
default_value :authentication_required, true
|
data/lib/dockly/rake_task.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
require 'rake'
|
2
2
|
require 'dockly'
|
3
3
|
|
4
|
-
$rake_task_logger = Dockly::Util::Logger.new('[dockly rake_task]', STDOUT, false)
|
5
|
-
|
6
4
|
class Rake::DebTask < Rake::Task
|
7
5
|
def needed?
|
8
6
|
raise "Package does not exist" if package.nil?
|
@@ -14,10 +12,25 @@ class Rake::DebTask < Rake::Task
|
|
14
12
|
end
|
15
13
|
end
|
16
14
|
|
15
|
+
class Rake::DockerTask < Rake::Task
|
16
|
+
def needed?
|
17
|
+
raise "Docker does not exist" if docker.nil?
|
18
|
+
!docker.exists?
|
19
|
+
end
|
20
|
+
|
21
|
+
def docker
|
22
|
+
Dockly::Docker[name.split(':').last.to_sym]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
17
26
|
module Rake::DSL
|
18
27
|
def deb(*args, &block)
|
19
28
|
Rake::DebTask.define_task(*args, &block)
|
20
29
|
end
|
30
|
+
|
31
|
+
def docker(*args, &block)
|
32
|
+
Rake::DockerTask.define_task(*args, &block)
|
33
|
+
end
|
21
34
|
end
|
22
35
|
|
23
36
|
namespace :dockly do
|
@@ -33,4 +46,20 @@ namespace :dockly do
|
|
33
46
|
end
|
34
47
|
end
|
35
48
|
end
|
49
|
+
|
50
|
+
namespace :docker do
|
51
|
+
Dockly.dockers.values.each do |inst|
|
52
|
+
docker inst.name => 'dockly:load' do
|
53
|
+
Thread.current[:rake_task] = inst.name
|
54
|
+
inst.generate!
|
55
|
+
end
|
56
|
+
|
57
|
+
namespace :noexport do
|
58
|
+
task inst.name => 'dockly:load' do
|
59
|
+
Thread.current[:rake_task] = inst.name
|
60
|
+
inst.generate_build
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
36
65
|
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
class Dockly::TarDiff
|
2
|
+
include Dockly::Util::Logger::Mixin
|
3
|
+
|
4
|
+
# Tar header format for a ustar tar
|
5
|
+
HEADER_UNPACK_FORMAT = "Z100A8A8A8A12A12A8aZ100A6A2Z32Z32A8A8Z155"
|
6
|
+
PAX_FILE_FORMAT_REGEX = /\d+ path=(.*)/
|
7
|
+
|
8
|
+
logger_prefix '[dockly tar_diff]'
|
9
|
+
|
10
|
+
attr_reader :base, :output, :target, :base_enum, :target_enum
|
11
|
+
|
12
|
+
def initialize(base, target, output)
|
13
|
+
@base, @target, @output = base, target, output
|
14
|
+
|
15
|
+
@base_enum = to_enum(:read_header, base)
|
16
|
+
@target_enum = to_enum(:read_header, target)
|
17
|
+
end
|
18
|
+
|
19
|
+
def write_tar_section(header, data, remainder)
|
20
|
+
output.write(header)
|
21
|
+
output.write(data)
|
22
|
+
output.write("\0" * remainder)
|
23
|
+
end
|
24
|
+
|
25
|
+
def quick_write(size)
|
26
|
+
while size > 0
|
27
|
+
bread = target.read([size, 4096].min)
|
28
|
+
output.write(bread)
|
29
|
+
size -= bread.to_s.size
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_header(io)
|
34
|
+
loop do
|
35
|
+
return if io.eof?
|
36
|
+
# Tar header is 512 bytes large
|
37
|
+
data = io.read(512)
|
38
|
+
fields = data.unpack(HEADER_UNPACK_FORMAT)
|
39
|
+
name = fields[0]
|
40
|
+
size = fields[4].oct
|
41
|
+
mtime = fields[5].oct
|
42
|
+
typeflag = fields[7]
|
43
|
+
prefix = fields[15]
|
44
|
+
|
45
|
+
empty = (data == "\0" * 512)
|
46
|
+
remainder = (512 - (size % 512)) % 512
|
47
|
+
|
48
|
+
yield data, name, prefix, mtime, typeflag, size, remainder, empty
|
49
|
+
|
50
|
+
io.read(remainder)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def process
|
55
|
+
debug "Started processing tar diff"
|
56
|
+
target_data = nil
|
57
|
+
base_data = nil
|
58
|
+
loop do
|
59
|
+
begin
|
60
|
+
|
61
|
+
target_header, target_name, \
|
62
|
+
target_prefix, target_mtime, \
|
63
|
+
target_typeflag, \
|
64
|
+
target_size, target_remainder, \
|
65
|
+
target_empty = target_enum.peek
|
66
|
+
rescue StopIteration
|
67
|
+
debug "Finished target file"
|
68
|
+
break
|
69
|
+
end
|
70
|
+
|
71
|
+
if target_empty
|
72
|
+
debug "End of target file/Empty"
|
73
|
+
break
|
74
|
+
end
|
75
|
+
|
76
|
+
begin
|
77
|
+
_, base_name, base_prefix, base_mtime, base_typeflag, base_size, _, base_empty = base_enum.peek
|
78
|
+
rescue StopIteration
|
79
|
+
target_data ||= target.read(target_size)
|
80
|
+
write_tar_section(target_header, target_data, target_remainder)
|
81
|
+
target_data = nil
|
82
|
+
target_enum.next
|
83
|
+
next
|
84
|
+
end
|
85
|
+
|
86
|
+
if base_empty
|
87
|
+
target_data ||= target.read(target_size)
|
88
|
+
write_tar_section(target_header, target_data, target_remainder)
|
89
|
+
target_data = nil
|
90
|
+
target_enum.next
|
91
|
+
next
|
92
|
+
end
|
93
|
+
|
94
|
+
target_full_name = File.join(target_prefix, target_name)
|
95
|
+
base_full_name = File.join(base_prefix, base_name)
|
96
|
+
|
97
|
+
target_full_name = target_full_name[1..-1] if target_full_name[0] == '/'
|
98
|
+
base_full_name = base_full_name[1..-1] if base_full_name[0] == '/'
|
99
|
+
|
100
|
+
if target_typeflag == 'x'
|
101
|
+
target_file = File.basename(target_full_name)
|
102
|
+
target_dir = File.dirname(File.dirname(target_full_name))
|
103
|
+
target_full_name = File.join(target_dir, target_file)
|
104
|
+
end
|
105
|
+
|
106
|
+
if base_typeflag == 'x'
|
107
|
+
base_file = File.basename(base_full_name)
|
108
|
+
base_dir = File.dirname(File.dirname(base_full_name))
|
109
|
+
base_full_name = File.join(base_dir, base_file)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Remove the PaxHeader.PID from the file
|
113
|
+
# Format: /base/directory/PaxHeader.1234/file.ext
|
114
|
+
# After: /base/directory/file.ext
|
115
|
+
if (target_typeflag == 'x' && base_typeflag == 'x')
|
116
|
+
target_data = target.read(target_size)
|
117
|
+
base_data = base.read(base_size)
|
118
|
+
|
119
|
+
if target_match = target_data.match(PAX_FILE_FORMAT_REGEX) && \
|
120
|
+
base_match = base_data.match(PAX_FILE_FORMAT_REGEX)
|
121
|
+
target_full_name = target_match[1]
|
122
|
+
base_full_name = base_match[1]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
if (target_full_name < base_full_name)
|
127
|
+
target_data ||= target.read(target_size)
|
128
|
+
write_tar_section(target_header, target_data, target_remainder)
|
129
|
+
target_data = nil
|
130
|
+
target_enum.next
|
131
|
+
elsif (base_full_name < target_full_name)
|
132
|
+
base.read(base_size) unless base_data
|
133
|
+
base_data = nil
|
134
|
+
base_enum.next
|
135
|
+
elsif (target_mtime != base_mtime) || (target_size != base_size)
|
136
|
+
target_data ||= target.read(target_size)
|
137
|
+
write_tar_section(target_header, target_data, target_remainder)
|
138
|
+
target_data = nil
|
139
|
+
target_enum.next
|
140
|
+
else
|
141
|
+
target.read(target_size) unless target_data
|
142
|
+
target_data = nil
|
143
|
+
target_enum.next
|
144
|
+
base.read(base_size) unless base_data
|
145
|
+
base_data = nil
|
146
|
+
base_enum.next
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/dockly/version.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
docker import - <% if data[:repo] %><%= data[:repo] %>:<%= data[:tag] %><% end %>
|
@@ -0,0 +1 @@
|
|
1
|
+
cat <%= data[:path] %> | gunzip -c | <%= docker_import(data[:repo], data[:tag]) %>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
log "fetch: starting to fetch deb"
|
2
|
+
worked=1
|
3
|
+
s3_path="<%= data[:s3_url] %>"
|
4
|
+
output_path="<%= data[:output_path] %>"
|
5
|
+
for attempt in {1..200}; do
|
6
|
+
[[ $worked != 0 ]] || break
|
7
|
+
log "fetch: attempt ${attempt} to get $s3_path ..."
|
8
|
+
s3cmd -f get $s3_path $output_path && worked=0 || (log "fetch: attempt failed, sleeping 30"; sleep 30)
|
9
|
+
done
|
10
|
+
[[ $worked != 0 ]] && fatal "fetch: failed to pull deb from S3"
|
11
|
+
log "fetch: successfully fetched deb"
|
@@ -0,0 +1 @@
|
|
1
|
+
dpkg -i "<%= data[:path] %>"
|
@@ -0,0 +1 @@
|
|
1
|
+
docker pull <%= data[:repo] %>:<%= data[:tag] %>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<%
|
2
|
+
base_image = "/opt/dockly/base_image.tar"
|
3
|
+
%>
|
4
|
+
|
5
|
+
s3_diff_docker_import_base_fn() {
|
6
|
+
<%= get_from_s3(data[:base_image]) %>
|
7
|
+
}
|
8
|
+
s3_diff_docker_import_diff_fn() {
|
9
|
+
<%= get_from_s3(data[:diff_image]) %>
|
10
|
+
}
|
11
|
+
s3_diff_docker_import_base_fn | gunzip -c > "<%= base_image %>"
|
12
|
+
size=$(stat --format "%s" "<%= base_image %>")
|
13
|
+
head_size=$(($size - 1024))
|
14
|
+
(head -c $head_size "<%= base_image %>"; s3_diff_docker_import_diff_fn | gunzip -c) | <%= docker_import(data[:repo], data[:tag]) %>
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Dockly::AWS::S3Writer do
|
4
|
+
let(:connection) { double(:connection) }
|
5
|
+
let(:bucket) { 'test_bucket' }
|
6
|
+
let(:object) { 'object_name.tar' }
|
7
|
+
let(:initiate_response) { double(:initiate_response) }
|
8
|
+
let(:upload_id) { 'test_id' }
|
9
|
+
|
10
|
+
subject { described_class.new(connection, bucket, object) }
|
11
|
+
|
12
|
+
before do
|
13
|
+
connection.should_receive(:initiate_multipart_upload) { initiate_response }
|
14
|
+
initiate_response.stub(:body) { { 'UploadId' => upload_id } }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe ".new" do
|
18
|
+
|
19
|
+
it "sets the connection, s3_bucket, s3_object, and upload_id" do
|
20
|
+
expect(subject.connection).to eq(connection)
|
21
|
+
expect(subject.s3_bucket).to eq(bucket)
|
22
|
+
expect(subject.s3_object).to eq(object)
|
23
|
+
expect(subject.upload_id).to eq(upload_id)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#upload_buffer" do
|
28
|
+
let(:message) { "message" }
|
29
|
+
let(:upload_response) { double(:upload_response) }
|
30
|
+
let(:etag) { "test" }
|
31
|
+
|
32
|
+
before do
|
33
|
+
connection.should_receive(:upload_part).with(bucket, object, upload_id, 1, message) do
|
34
|
+
upload_response
|
35
|
+
end
|
36
|
+
upload_response.stub(:headers) { { "ETag" => etag } }
|
37
|
+
subject.instance_variable_set(:"@buffer", message)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "connects to S3" do
|
41
|
+
subject.upload_buffer
|
42
|
+
expect(subject.instance_variable_get(:"@parts")).to include(etag)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#write" do
|
47
|
+
let(:message) { "a" * chunk_length }
|
48
|
+
|
49
|
+
context "with a buffer of less than 5 MB" do
|
50
|
+
let(:chunk_length) { 100 }
|
51
|
+
|
52
|
+
before do
|
53
|
+
subject.should_not_receive(:upload_buffer)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "adds it to the buffer and returns the chunk length" do
|
57
|
+
expect(subject.write(message)).to eq(chunk_length)
|
58
|
+
expect(subject.instance_variable_get(:"@buffer")).to eq(message)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "with a buffer of greater than 5 MB" do
|
63
|
+
let(:chunk_length) { 1 + 5 * 1024 * 1024 }
|
64
|
+
|
65
|
+
before do
|
66
|
+
subject.should_receive(:upload_buffer)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "adds it to the buffer, writes to S3 and returns the chunk length" do
|
70
|
+
expect(subject.write(message)).to eq(chunk_length)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#close" do
|
76
|
+
let(:complete_response) { double(:complete_response) }
|
77
|
+
|
78
|
+
before do
|
79
|
+
connection.should_receive(:complete_multipart_upload).with(bucket, object, upload_id, []) do
|
80
|
+
complete_response
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "when it passes" do
|
85
|
+
before do
|
86
|
+
complete_response.stub(:body) { {} }
|
87
|
+
end
|
88
|
+
|
89
|
+
context "when the buffer is not empty" do
|
90
|
+
before do
|
91
|
+
subject.instance_variable_set(:"@buffer", "text")
|
92
|
+
subject.should_receive(:upload_buffer)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "uploads the rest of the buffer and closes the connection" do
|
96
|
+
expect(subject.close).to be_true
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context "when the buffer is empty" do
|
101
|
+
before do
|
102
|
+
subject.should_not_receive(:upload_buffer)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "closes the connection" do
|
106
|
+
expect(subject.close).to be_true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context "when it fails" do
|
112
|
+
before do
|
113
|
+
complete_response.stub(:body) { { 'Code' => 20, 'Message' => 'Msggg' } }
|
114
|
+
end
|
115
|
+
|
116
|
+
it "raises an error" do
|
117
|
+
expect { subject.close }.to raise_error("Failed to upload to S3: 20: Msggg")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "#abort" do
|
123
|
+
before do
|
124
|
+
connection.should_receive(:abort_multipart_upload).with(bucket, object, upload_id)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "aborts the upload" do
|
128
|
+
subject.abort
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "#abort_unless_closed" do
|
133
|
+
context "when the upload is closed" do
|
134
|
+
before do
|
135
|
+
subject.should_not_receive(:abort)
|
136
|
+
subject.instance_variable_set(:"@closed", true)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "does not abort" do
|
140
|
+
subject.abort_unless_closed
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context "when the upload is open" do
|
145
|
+
before do
|
146
|
+
subject.should_receive(:abort)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "aborts the upload" do
|
150
|
+
subject.abort_unless_closed
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|