docker-api 2.0.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +633 -0
- data/lib/docker-api.rb +1 -0
- data/lib/docker.rb +136 -0
- data/lib/docker/base.rb +25 -0
- data/lib/docker/connection.rb +93 -0
- data/lib/docker/container.rb +360 -0
- data/lib/docker/error.rb +40 -0
- data/lib/docker/event.rb +126 -0
- data/lib/docker/exec.rb +107 -0
- data/lib/docker/image.rb +356 -0
- data/lib/docker/messages.rb +67 -0
- data/lib/docker/messages_stack.rb +25 -0
- data/lib/docker/network.rb +82 -0
- data/lib/docker/rake_task.rb +39 -0
- data/lib/docker/util.rb +279 -0
- data/lib/docker/version.rb +4 -0
- data/lib/docker/volume.rb +44 -0
- data/lib/excon/middlewares/hijack.rb +49 -0
- metadata +201 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
# This class represents all the messages either received by chunks from attach
|
2
|
+
class Docker::Messages
|
3
|
+
|
4
|
+
attr_accessor :buffer, :stdout_messages, :stderr_messages, :all_messages
|
5
|
+
|
6
|
+
def initialize(stdout_messages=[],
|
7
|
+
stderr_messages=[],
|
8
|
+
all_messages=[],
|
9
|
+
buffer="")
|
10
|
+
@stdout_messages = stdout_messages
|
11
|
+
@stderr_messages = stderr_messages
|
12
|
+
@all_messages = all_messages
|
13
|
+
@buffer = buffer
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_message(source, message)
|
17
|
+
case source
|
18
|
+
when 1
|
19
|
+
stdout_messages << message
|
20
|
+
when 2
|
21
|
+
stderr_messages << message
|
22
|
+
end
|
23
|
+
all_messages << message
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_message(raw_text)
|
27
|
+
header = raw_text.slice!(0,8)
|
28
|
+
if header.length < 8
|
29
|
+
@buffer = header
|
30
|
+
return
|
31
|
+
end
|
32
|
+
type, length = header.unpack("CxxxN")
|
33
|
+
|
34
|
+
message = raw_text.slice!(0,length)
|
35
|
+
if message.length < length
|
36
|
+
@buffer = header + message
|
37
|
+
else
|
38
|
+
add_message(type, message)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def append(messages)
|
43
|
+
@stdout_messages += messages.stdout_messages
|
44
|
+
@stderr_messages += messages.stderr_messages
|
45
|
+
@all_messages += messages.all_messages
|
46
|
+
messages.clear
|
47
|
+
|
48
|
+
@all_messages
|
49
|
+
end
|
50
|
+
|
51
|
+
def clear
|
52
|
+
stdout_messages.clear
|
53
|
+
stderr_messages.clear
|
54
|
+
all_messages.clear
|
55
|
+
end
|
56
|
+
|
57
|
+
# Method to break apart application/vnd.docker.raw-stream headers
|
58
|
+
def decipher_messages(body)
|
59
|
+
raw_text = buffer + body.dup
|
60
|
+
messages = Docker::Messages.new
|
61
|
+
while !raw_text.empty?
|
62
|
+
messages.get_message(raw_text)
|
63
|
+
end
|
64
|
+
|
65
|
+
messages
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# This class represents a messages stack
|
2
|
+
class Docker::MessagesStack
|
3
|
+
|
4
|
+
attr_accessor :messages
|
5
|
+
|
6
|
+
# Initialize stack with optional size
|
7
|
+
#
|
8
|
+
# @param size [Integer]
|
9
|
+
def initialize(size = -1)
|
10
|
+
@messages = []
|
11
|
+
@size = size
|
12
|
+
end
|
13
|
+
|
14
|
+
# Append messages to stack
|
15
|
+
#
|
16
|
+
# @param messages [Docker::Messages]
|
17
|
+
def append(messages)
|
18
|
+
return if @size == 0
|
19
|
+
|
20
|
+
messages.all_messages.each do |msg|
|
21
|
+
@messages << msg
|
22
|
+
@messages.shift if @size > -1 && @messages.size > @size
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# This class represents a Docker Network.
|
2
|
+
class Docker::Network
|
3
|
+
include Docker::Base
|
4
|
+
|
5
|
+
def connect(container, opts = {}, body_opts = {})
|
6
|
+
body = MultiJson.dump({ container: container }.merge(body_opts))
|
7
|
+
Docker::Util.parse_json(
|
8
|
+
connection.post(path_for('connect'), opts, body: body)
|
9
|
+
)
|
10
|
+
reload
|
11
|
+
end
|
12
|
+
|
13
|
+
def disconnect(container, opts = {})
|
14
|
+
body = MultiJson.dump(container: container)
|
15
|
+
Docker::Util.parse_json(
|
16
|
+
connection.post(path_for('disconnect'), opts, body: body)
|
17
|
+
)
|
18
|
+
reload
|
19
|
+
end
|
20
|
+
|
21
|
+
def remove(opts = {})
|
22
|
+
connection.delete(path_for, opts)
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
alias_method :delete, :remove
|
26
|
+
|
27
|
+
def json(opts = {})
|
28
|
+
Docker::Util.parse_json(connection.get(path_for, opts))
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
"Docker::Network { :id => #{id}, :info => #{info.inspect}, "\
|
33
|
+
":connection => #{connection} }"
|
34
|
+
end
|
35
|
+
|
36
|
+
def reload
|
37
|
+
network_json = @connection.get("/networks/#{@id}")
|
38
|
+
hash = Docker::Util.parse_json(network_json) || {}
|
39
|
+
@info = hash
|
40
|
+
end
|
41
|
+
|
42
|
+
class << self
|
43
|
+
def create(name, opts = {}, conn = Docker.connection)
|
44
|
+
default_opts = MultiJson.dump({
|
45
|
+
'Name' => name,
|
46
|
+
'CheckDuplicate' => true
|
47
|
+
}.merge(opts))
|
48
|
+
resp = conn.post('/networks/create', {}, body: default_opts)
|
49
|
+
response_hash = Docker::Util.parse_json(resp) || {}
|
50
|
+
get(response_hash['Id'], {}, conn) || {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def get(id, opts = {}, conn = Docker.connection)
|
54
|
+
network_json = conn.get("/networks/#{id}", opts)
|
55
|
+
hash = Docker::Util.parse_json(network_json) || {}
|
56
|
+
new(conn, hash)
|
57
|
+
end
|
58
|
+
|
59
|
+
def all(opts = {}, conn = Docker.connection)
|
60
|
+
hashes = Docker::Util.parse_json(conn.get('/networks', opts)) || []
|
61
|
+
hashes.map { |hash| new(conn, hash) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def remove(id, opts = {}, conn = Docker.connection)
|
65
|
+
conn.delete("/networks/#{id}", opts)
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
alias_method :delete, :remove
|
69
|
+
|
70
|
+
def prune(conn = Docker.connection)
|
71
|
+
conn.post("/networks/prune", {})
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Convenience method to return the path for a particular resource.
|
77
|
+
def path_for(resource = nil)
|
78
|
+
["/networks/#{id}", resource].compact.join('/')
|
79
|
+
end
|
80
|
+
|
81
|
+
private :path_for
|
82
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# This class allows image-based tasks to be created.
|
2
|
+
class Docker::ImageTask < Rake::Task
|
3
|
+
def self.scope_name(_scope, task_name)
|
4
|
+
task_name
|
5
|
+
end
|
6
|
+
|
7
|
+
def needed?
|
8
|
+
!has_repo_tag?
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def has_repo_tag?
|
14
|
+
images.any? { |image| image.info['RepoTags'].include?(repo_tag) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def images
|
18
|
+
@images ||= Docker::Image.all(:all => true)
|
19
|
+
end
|
20
|
+
|
21
|
+
def repo
|
22
|
+
name.split(':')[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
def tag
|
26
|
+
name.split(':')[1] || 'latest'
|
27
|
+
end
|
28
|
+
|
29
|
+
def repo_tag
|
30
|
+
"#{repo}:#{tag}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Monkeypatch Rake to add the `image` task.
|
35
|
+
module Rake::DSL
|
36
|
+
def image(*args, &block)
|
37
|
+
Docker::ImageTask.define_task(*args, &block)
|
38
|
+
end
|
39
|
+
end
|
data/lib/docker/util.rb
ADDED
@@ -0,0 +1,279 @@
|
|
1
|
+
# This module holds shared logic that doesn't really belong anywhere else in the
|
2
|
+
# gem.
|
3
|
+
module Docker::Util
|
4
|
+
# http://www.tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm#STANDARD-WILDCARDS
|
5
|
+
GLOB_WILDCARDS = /[\?\*\[\{]/
|
6
|
+
|
7
|
+
include Docker::Error
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# Attaches to a HTTP stream
|
12
|
+
#
|
13
|
+
# @param block
|
14
|
+
# @param msg_stack [Docker::Messages]
|
15
|
+
# @param tty [boolean]
|
16
|
+
def attach_for(block, msg_stack, tty = false)
|
17
|
+
# If TTY is enabled expect raw data and append to stdout
|
18
|
+
if tty
|
19
|
+
attach_for_tty(block, msg_stack)
|
20
|
+
else
|
21
|
+
attach_for_multiplex(block, msg_stack)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def attach_for_tty(block, msg_stack)
|
26
|
+
messages = Docker::Messages.new
|
27
|
+
lambda do |c,r,t|
|
28
|
+
messages.stdout_messages << c
|
29
|
+
messages.all_messages << c
|
30
|
+
msg_stack.append(messages)
|
31
|
+
|
32
|
+
block.call c if block
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def attach_for_multiplex(block, msg_stack)
|
37
|
+
messages = Docker::Messages.new
|
38
|
+
lambda do |c,r,t|
|
39
|
+
messages = messages.decipher_messages(c)
|
40
|
+
|
41
|
+
unless block.nil?
|
42
|
+
messages.stdout_messages.each do |msg|
|
43
|
+
block.call(:stdout, msg)
|
44
|
+
end
|
45
|
+
messages.stderr_messages.each do |msg|
|
46
|
+
block.call(:stderr, msg)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
msg_stack.append(messages)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def debug(msg)
|
55
|
+
Docker.logger.debug(msg) if Docker.logger
|
56
|
+
end
|
57
|
+
|
58
|
+
def hijack_for(stdin, block, msg_stack, tty)
|
59
|
+
attach_block = attach_for(block, msg_stack, tty)
|
60
|
+
|
61
|
+
lambda do |socket|
|
62
|
+
debug "hijack: hijacking the HTTP socket"
|
63
|
+
threads = []
|
64
|
+
|
65
|
+
debug "hijack: starting stdin copy thread"
|
66
|
+
threads << Thread.start do
|
67
|
+
debug "hijack: copying stdin => socket"
|
68
|
+
IO.copy_stream stdin, socket
|
69
|
+
|
70
|
+
debug "hijack: closing write end of hijacked socket"
|
71
|
+
close_write(socket)
|
72
|
+
end
|
73
|
+
|
74
|
+
debug "hijack: starting hijacked socket read thread"
|
75
|
+
threads << Thread.start do
|
76
|
+
debug "hijack: reading from hijacked socket"
|
77
|
+
|
78
|
+
begin
|
79
|
+
while chunk = socket.readpartial(512)
|
80
|
+
debug "hijack: got #{chunk.bytesize} bytes from hijacked socket"
|
81
|
+
attach_block.call chunk, nil, nil
|
82
|
+
end
|
83
|
+
rescue EOFError
|
84
|
+
end
|
85
|
+
|
86
|
+
debug "hijack: killing stdin copy thread"
|
87
|
+
threads.first.kill
|
88
|
+
end
|
89
|
+
|
90
|
+
threads.each(&:join)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def close_write(socket)
|
95
|
+
if socket.respond_to?(:close_write)
|
96
|
+
socket.close_write
|
97
|
+
elsif socket.respond_to?(:io)
|
98
|
+
socket.io.close_write
|
99
|
+
else
|
100
|
+
raise IOError, 'Cannot close socket'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def parse_json(body)
|
105
|
+
MultiJson.load(body) unless body.nil? || body.empty? || (body == 'null')
|
106
|
+
rescue MultiJson::ParseError => ex
|
107
|
+
raise UnexpectedResponseError, ex.message
|
108
|
+
end
|
109
|
+
|
110
|
+
def parse_repo_tag(str)
|
111
|
+
if match = str.match(/\A(.*):([^:]*)\z/)
|
112
|
+
match.captures
|
113
|
+
else
|
114
|
+
[str, '']
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def fix_json(body)
|
119
|
+
parse_json("[#{body.gsub(/}\s*{/, '},{')}]")
|
120
|
+
end
|
121
|
+
|
122
|
+
def create_tar(hash = {})
|
123
|
+
output = StringIO.new
|
124
|
+
Gem::Package::TarWriter.new(output) do |tar|
|
125
|
+
hash.each do |file_name, file_details|
|
126
|
+
permissions = file_details.is_a?(Hash) ? file_details[:permissions] : 0640
|
127
|
+
tar.add_file(file_name, permissions) do |tar_file|
|
128
|
+
content = file_details.is_a?(Hash) ? file_details[:content] : file_details
|
129
|
+
tar_file.write(content)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
output.tap(&:rewind).string
|
134
|
+
end
|
135
|
+
|
136
|
+
def create_dir_tar(directory)
|
137
|
+
tempfile = create_temp_file
|
138
|
+
directory += '/' unless directory.end_with?('/')
|
139
|
+
|
140
|
+
create_relative_dir_tar(directory, tempfile)
|
141
|
+
|
142
|
+
File.new(tempfile.path, 'r')
|
143
|
+
end
|
144
|
+
|
145
|
+
def create_relative_dir_tar(directory, output)
|
146
|
+
Gem::Package::TarWriter.new(output) do |tar|
|
147
|
+
files = glob_all_files(File.join(directory, "**/*"))
|
148
|
+
remove_ignored_files!(directory, files)
|
149
|
+
|
150
|
+
files.each do |prefixed_file_name|
|
151
|
+
stat = File.stat(prefixed_file_name)
|
152
|
+
next unless stat.file?
|
153
|
+
|
154
|
+
unprefixed_file_name = prefixed_file_name[directory.length..-1]
|
155
|
+
add_file_to_tar(
|
156
|
+
tar, unprefixed_file_name, stat.mode, stat.size, stat.mtime
|
157
|
+
) do |tar_file|
|
158
|
+
IO.copy_stream(File.open(prefixed_file_name, 'rb'), tar_file)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def add_file_to_tar(tar, name, mode, size, mtime)
|
165
|
+
tar.check_closed
|
166
|
+
|
167
|
+
io = tar.instance_variable_get(:@io)
|
168
|
+
|
169
|
+
name, prefix = tar.split_name(name)
|
170
|
+
|
171
|
+
header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
|
172
|
+
:size => size, :prefix => prefix,
|
173
|
+
:mtime => mtime).to_s
|
174
|
+
|
175
|
+
io.write header
|
176
|
+
os = Gem::Package::TarWriter::BoundedStream.new io, size
|
177
|
+
|
178
|
+
yield os if block_given?
|
179
|
+
|
180
|
+
min_padding = size - os.written
|
181
|
+
io.write("\0" * min_padding)
|
182
|
+
|
183
|
+
remainder = (512 - (size % 512)) % 512
|
184
|
+
io.write("\0" * remainder)
|
185
|
+
|
186
|
+
tar
|
187
|
+
end
|
188
|
+
|
189
|
+
def create_temp_file
|
190
|
+
tempfile_name = Dir::Tmpname.create('out') {}
|
191
|
+
File.open(tempfile_name, 'wb+')
|
192
|
+
end
|
193
|
+
|
194
|
+
def extract_id(body)
|
195
|
+
body.lines.reverse_each do |line|
|
196
|
+
if (id = line.match(/Successfully built ([a-f0-9]+)/)) && !id[1].empty?
|
197
|
+
return id[1]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
raise UnexpectedResponseError, "Couldn't find id: #{body}"
|
201
|
+
end
|
202
|
+
|
203
|
+
# Convenience method to get the file hash corresponding to an array of
|
204
|
+
# local paths.
|
205
|
+
def file_hash_from_paths(local_paths)
|
206
|
+
local_paths.each_with_object({}) do |local_path, file_hash|
|
207
|
+
unless File.exist?(local_path)
|
208
|
+
raise ArgumentError, "#{local_path} does not exist."
|
209
|
+
end
|
210
|
+
|
211
|
+
basename = File.basename(local_path)
|
212
|
+
if File.directory?(local_path)
|
213
|
+
tar = create_dir_tar(local_path)
|
214
|
+
file_hash[basename] = {
|
215
|
+
content: tar.read,
|
216
|
+
permissions: filesystem_permissions(local_path)
|
217
|
+
}
|
218
|
+
tar.close
|
219
|
+
FileUtils.rm(tar.path)
|
220
|
+
else
|
221
|
+
file_hash[basename] = {
|
222
|
+
content: File.read(local_path, mode: 'rb'),
|
223
|
+
permissions: filesystem_permissions(local_path)
|
224
|
+
}
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def filesystem_permissions(path)
|
230
|
+
mode = sprintf("%o", File.stat(path).mode)
|
231
|
+
mode[(mode.length - 3)...mode.length].to_i(8)
|
232
|
+
end
|
233
|
+
|
234
|
+
def build_auth_header(credentials)
|
235
|
+
credentials = MultiJson.dump(credentials) if credentials.is_a?(Hash)
|
236
|
+
encoded_creds = Base64.urlsafe_encode64(credentials)
|
237
|
+
{
|
238
|
+
'X-Registry-Auth' => encoded_creds
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
def build_config_header(credentials)
|
243
|
+
if credentials.is_a?(String)
|
244
|
+
credentials = MultiJson.load(credentials, symbolize_keys: true)
|
245
|
+
end
|
246
|
+
|
247
|
+
header = MultiJson.dump(
|
248
|
+
credentials[:serveraddress].to_s => {
|
249
|
+
'username' => credentials[:username].to_s,
|
250
|
+
'password' => credentials[:password].to_s,
|
251
|
+
'email' => credentials[:email].to_s
|
252
|
+
}
|
253
|
+
)
|
254
|
+
|
255
|
+
encoded_header = Base64.urlsafe_encode64(header)
|
256
|
+
|
257
|
+
{
|
258
|
+
'X-Registry-Config' => encoded_header
|
259
|
+
}
|
260
|
+
end
|
261
|
+
|
262
|
+
def glob_all_files(pattern)
|
263
|
+
Dir.glob(pattern, File::FNM_DOTMATCH) - ['..', '.']
|
264
|
+
end
|
265
|
+
|
266
|
+
def remove_ignored_files!(directory, files)
|
267
|
+
ignore = File.join(directory, '.dockerignore')
|
268
|
+
return unless files.include?(ignore)
|
269
|
+
ignored_files(directory, ignore).each { |f| files.delete(f) }
|
270
|
+
end
|
271
|
+
|
272
|
+
def ignored_files(directory, ignore_file)
|
273
|
+
patterns = File.read(ignore_file).split("\n").each(&:strip!)
|
274
|
+
patterns.reject! { |p| p.empty? || p.start_with?('#') }
|
275
|
+
patterns.map! { |p| File.join(directory, p) }
|
276
|
+
patterns.map! { |p| File.directory?(p) ? "#{p}/**/*" : p }
|
277
|
+
patterns.flat_map { |p| p =~ GLOB_WILDCARDS ? glob_all_files(p) : p }
|
278
|
+
end
|
279
|
+
end
|