docker-api 2.0.0.pre.1
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.
- 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
|