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.
@@ -0,0 +1 @@
1
+ require 'docker'
@@ -0,0 +1,136 @@
1
+ require 'cgi'
2
+ require 'multi_json'
3
+ require 'excon'
4
+ require 'tempfile'
5
+ require 'base64'
6
+ require 'find'
7
+ require 'rubygems/package'
8
+ require 'uri'
9
+ require 'open-uri'
10
+
11
+ # Add the Hijack middleware at the top of the middleware stack so it can
12
+ # potentially hijack HTTP sockets (when attaching to stdin) before other
13
+ # middlewares try and parse the response.
14
+ require 'excon/middlewares/hijack'
15
+ Excon.defaults[:middlewares].unshift Excon::Middleware::Hijack
16
+
17
+ Excon.defaults[:middlewares] << Excon::Middleware::RedirectFollower
18
+
19
+ # The top-level module for this gem. Its purpose is to hold global
20
+ # configuration variables that are used as defaults in other classes.
21
+ module Docker
22
+ attr_accessor :creds, :logger
23
+
24
+ require 'docker/error'
25
+ require 'docker/connection'
26
+ require 'docker/base'
27
+ require 'docker/container'
28
+ require 'docker/network'
29
+ require 'docker/event'
30
+ require 'docker/exec'
31
+ require 'docker/image'
32
+ require 'docker/messages_stack'
33
+ require 'docker/messages'
34
+ require 'docker/util'
35
+ require 'docker/version'
36
+ require 'docker/volume'
37
+ require 'docker/rake_task' if defined?(Rake::Task)
38
+
39
+ def default_socket_url
40
+ 'unix:///var/run/docker.sock'
41
+ end
42
+
43
+ def env_url
44
+ ENV['DOCKER_URL'] || ENV['DOCKER_HOST']
45
+ end
46
+
47
+ def env_options
48
+ if cert_path = ENV['DOCKER_CERT_PATH']
49
+ {
50
+ client_cert: File.join(cert_path, 'cert.pem'),
51
+ client_key: File.join(cert_path, 'key.pem'),
52
+ ssl_ca_file: File.join(cert_path, 'ca.pem'),
53
+ scheme: 'https'
54
+ }.merge(ssl_options)
55
+ else
56
+ {}
57
+ end
58
+ end
59
+
60
+ def ssl_options
61
+ if ENV['DOCKER_SSL_VERIFY'] == 'false'
62
+ {
63
+ ssl_verify_peer: false
64
+ }
65
+ else
66
+ {}
67
+ end
68
+ end
69
+
70
+ def url
71
+ @url ||= env_url || default_socket_url
72
+ # docker uses a default notation tcp:// which means tcp://localhost:2375
73
+ if @url == 'tcp://'
74
+ @url = 'tcp://localhost:2375'
75
+ end
76
+ @url
77
+ end
78
+
79
+ def options
80
+ @options ||= env_options
81
+ end
82
+
83
+ def url=(new_url)
84
+ @url = new_url
85
+ reset_connection!
86
+ end
87
+
88
+ def options=(new_options)
89
+ @options = env_options.merge(new_options || {})
90
+ reset_connection!
91
+ end
92
+
93
+ def connection
94
+ @connection ||= Connection.new(url, options)
95
+ end
96
+
97
+ def reset!
98
+ @url = nil
99
+ @options = nil
100
+ reset_connection!
101
+ end
102
+
103
+ def reset_connection!
104
+ @connection = nil
105
+ end
106
+
107
+ # Get the version of Go, Docker, and optionally the Git commit.
108
+ def version(connection = self.connection)
109
+ Util.parse_json(connection.get('/version'))
110
+ end
111
+
112
+ # Get more information about the Docker server.
113
+ def info(connection = self.connection)
114
+ Util.parse_json(connection.get('/info'))
115
+ end
116
+
117
+ # Ping the Docker server.
118
+ def ping(connection = self.connection)
119
+ connection.get('/_ping')
120
+ end
121
+
122
+ # Login to the Docker registry.
123
+ def authenticate!(options = {}, connection = self.connection)
124
+ creds = MultiJson.dump(options)
125
+ connection.post('/auth', {}, body: creds)
126
+ @creds = creds
127
+ true
128
+ rescue Docker::Error::ServerError, Docker::Error::UnauthorizedError
129
+ raise Docker::Error::AuthenticationError
130
+ end
131
+
132
+ module_function :default_socket_url, :env_url, :url, :url=, :env_options,
133
+ :options, :options=, :creds, :creds=, :logger, :logger=,
134
+ :connection, :reset!, :reset_connection!, :version, :info,
135
+ :ping, :authenticate!, :ssl_options
136
+ end
@@ -0,0 +1,25 @@
1
+ # This class is a base class for Docker Container and Image.
2
+ # It is implementing accessor methods for the models attributes.
3
+ module Docker::Base
4
+ include Docker::Error
5
+
6
+ attr_accessor :connection, :info
7
+ attr_reader :id
8
+
9
+ # The private new method accepts a connection and a hash of options that must include an id.
10
+ def initialize(connection, hash={})
11
+ unless connection.is_a?(Docker::Connection)
12
+ raise ArgumentError, "Expected a Docker::Connection, got: #{connection}."
13
+ end
14
+ normalize_hash(hash)
15
+ @connection, @info, @id = connection, hash, hash['id']
16
+ raise ArgumentError, "Must have id, got: #{hash}" unless @id
17
+ end
18
+
19
+ # The docker-api will some time return "ID" other times it will return "Id"
20
+ # and other times it will return "id". This method normalize it to "id"
21
+ # The volumes endpoint returns Name instead of ID, added in the normalize function
22
+ def normalize_hash(hash)
23
+ hash["id"] ||= hash.delete("ID") || hash.delete("Id")
24
+ end
25
+ end
@@ -0,0 +1,93 @@
1
+ # This class represents a Connection to a Docker server. The Connection is
2
+ # immutable in that once the url and options is set they cannot be changed.
3
+ class Docker::Connection
4
+ include Docker::Error
5
+
6
+ attr_reader :url, :options
7
+
8
+ # Create a new Connection. This method takes a url (String) and options
9
+ # (Hash). These are passed to Excon, so any options valid for `Excon.new`
10
+ # can be passed here.
11
+ def initialize(url, opts)
12
+ case
13
+ when !url.is_a?(String)
14
+ raise ArgumentError, "Expected a String, got: '#{url}'"
15
+ when !opts.is_a?(Hash)
16
+ raise ArgumentError, "Expected a Hash, got: '#{opts}'"
17
+ else
18
+ uri = URI.parse(url)
19
+ if uri.scheme == "unix"
20
+ @url, @options = 'unix:///', {:socket => uri.path}.merge(opts)
21
+ elsif uri.scheme =~ /^(https?|tcp)$/
22
+ @url, @options = url, opts
23
+ else
24
+ @url, @options = "http://#{uri}", opts
25
+ end
26
+ end
27
+ end
28
+
29
+ # The actual client that sends HTTP methods to the Docker server. This value
30
+ # is not cached, since doing so may cause socket errors after bad requests.
31
+ def resource
32
+ Excon.new(url, options)
33
+ end
34
+ private :resource
35
+
36
+ # Send a request to the server with the `
37
+ def request(*args, &block)
38
+ request = compile_request_params(*args, &block)
39
+ log_request(request)
40
+ resource.request(request).body
41
+ rescue Excon::Errors::BadRequest => ex
42
+ raise ClientError, ex.response.body
43
+ rescue Excon::Errors::Unauthorized => ex
44
+ raise UnauthorizedError, ex.response.body
45
+ rescue Excon::Errors::NotFound => ex
46
+ raise NotFoundError, ex.response.body
47
+ rescue Excon::Errors::Conflict => ex
48
+ raise ConflictError, ex.response.body
49
+ rescue Excon::Errors::InternalServerError => ex
50
+ raise ServerError, ex.response.body
51
+ rescue Excon::Errors::Timeout => ex
52
+ raise TimeoutError, ex.message
53
+ end
54
+
55
+ def log_request(request)
56
+ if Docker.logger
57
+ Docker.logger.debug(
58
+ [request[:method], request[:path], request[:query], request[:body]]
59
+ )
60
+ end
61
+ end
62
+
63
+ # Delegate all HTTP methods to the #request.
64
+ [:get, :put, :post, :delete].each do |method|
65
+ define_method(method) { |*args, &block| request(method, *args, &block) }
66
+ end
67
+
68
+ def to_s
69
+ "Docker::Connection { :url => #{url}, :options => #{options} }"
70
+ end
71
+
72
+ private
73
+ # Given an HTTP method, path, optional query, extra options, and block,
74
+ # compiles a request.
75
+ def compile_request_params(http_method, path, query = nil, opts = nil, &block)
76
+ query ||= {}
77
+ opts ||= {}
78
+ headers = opts.delete(:headers) || {}
79
+ content_type = opts[:body].nil? ? 'text/plain' : 'application/json'
80
+ user_agent = "Swipely/Docker-API #{Docker::VERSION}"
81
+ {
82
+ :method => http_method,
83
+ :path => path,
84
+ :query => query,
85
+ :headers => { 'Content-Type' => content_type,
86
+ 'User-Agent' => user_agent,
87
+ }.merge(headers),
88
+ :expects => (200..204).to_a << 301 << 304,
89
+ :idempotent => http_method == :get,
90
+ :request_block => block,
91
+ }.merge(opts).reject { |_, v| v.nil? }
92
+ end
93
+ end
@@ -0,0 +1,360 @@
1
+ # This class represents a Docker Container. It's important to note that nothing
2
+ # is cached so that the information is always up to date.
3
+ class Docker::Container
4
+ include Docker::Base
5
+
6
+ # Update the @info hash, which is the only mutable state in this object.
7
+ # e.g. if you would like a live status from the #info hash, call #refresh! first.
8
+ def refresh!
9
+ other = Docker::Container.all({all: true}, connection).find { |c|
10
+ c.id.start_with?(self.id) || self.id.start_with?(c.id)
11
+ }
12
+
13
+ info.merge!(self.json)
14
+ other && info.merge!(other.info) { |key, info_value, other_value| info_value }
15
+ self
16
+ end
17
+
18
+ # Return a List of Hashes that represents the top running processes.
19
+ def top(opts = {})
20
+ format = opts.delete(:format) { :array }
21
+ resp = Docker::Util.parse_json(connection.get(path_for(:top), opts))
22
+ if resp['Processes'].nil?
23
+ format == :array ? [] : {}
24
+ else
25
+ format == :array ? resp['Processes'].map { |ary| Hash[resp['Titles'].zip(ary)] } : resp
26
+ end
27
+ end
28
+
29
+ # Wait for the current command to finish executing. Default wait time is
30
+ # `Excon.options[:read_timeout]`.
31
+ def wait(time = nil)
32
+ excon_params = { :read_timeout => time }
33
+ resp = connection.post(path_for(:wait), nil, excon_params)
34
+ Docker::Util.parse_json(resp)
35
+ end
36
+
37
+ # Given a command and an optional number of seconds to wait for the currently
38
+ # executing command, creates a new Container to run the specified command. If
39
+ # the command that is currently executing does not return a 0 status code, an
40
+ # UnexpectedResponseError is raised.
41
+ def run(cmd, time = 1000)
42
+ if (code = tap(&:start).wait(time)['StatusCode']).zero?
43
+ commit.run(cmd)
44
+ else
45
+ raise UnexpectedResponseError, "Command returned status code #{code}."
46
+ end
47
+ end
48
+
49
+ # Create an Exec instance inside the container
50
+ #
51
+ # @param command [String, Array] The command to run inside the Exec instance
52
+ # @param options [Hash] The options to pass to Docker::Exec
53
+ #
54
+ # @return [Docker::Exec] The Exec instance
55
+ def exec(command, options = {}, &block)
56
+ # Establish values
57
+ tty = options.delete(:tty) || false
58
+ detach = options.delete(:detach) || false
59
+ user = options.delete(:user)
60
+ stdin = options.delete(:stdin)
61
+ stdout = options.delete(:stdout) || !detach
62
+ stderr = options.delete(:stderr) || !detach
63
+ wait = options.delete(:wait)
64
+
65
+ opts = {
66
+ 'Container' => self.id,
67
+ 'User' => user,
68
+ 'AttachStdin' => !!stdin,
69
+ 'AttachStdout' => stdout,
70
+ 'AttachStderr' => stderr,
71
+ 'Tty' => tty,
72
+ 'Cmd' => command
73
+ }.merge(options)
74
+
75
+ # Create Exec Instance
76
+ instance = Docker::Exec.create(
77
+ opts,
78
+ self.connection
79
+ )
80
+
81
+ start_opts = {
82
+ :tty => tty,
83
+ :stdin => stdin,
84
+ :detach => detach,
85
+ :wait => wait
86
+ }
87
+
88
+ if detach
89
+ instance.start!(start_opts)
90
+ return instance
91
+ else
92
+ instance.start!(start_opts, &block)
93
+ end
94
+ end
95
+
96
+ # Export the Container as a tar.
97
+ def export(&block)
98
+ connection.get(path_for(:export), {}, :response_block => block)
99
+ self
100
+ end
101
+
102
+ # Attach to a container's standard streams / logs.
103
+ def attach(options = {}, excon_params = {}, &block)
104
+ stdin = options.delete(:stdin)
105
+ tty = options.delete(:tty)
106
+
107
+ opts = {
108
+ :stream => true, :stdout => true, :stderr => true
109
+ }.merge(options)
110
+ # Creates list to store stdout and stderr messages
111
+ msgs = Docker::Messages.new
112
+
113
+ if stdin
114
+ # If attaching to stdin, we must hijack the underlying TCP connection
115
+ # so we can stream stdin to the remote Docker process
116
+ opts[:stdin] = true
117
+ excon_params[:hijack_block] = Docker::Util.hijack_for(stdin, block,
118
+ msgs, tty)
119
+ else
120
+ excon_params[:response_block] = Docker::Util.attach_for(block, msgs, tty)
121
+ end
122
+
123
+ connection.post(
124
+ path_for(:attach),
125
+ opts,
126
+ excon_params
127
+ )
128
+ [msgs.stdout_messages, msgs.stderr_messages]
129
+ end
130
+
131
+ # Create an Image from a Container's change.s
132
+ def commit(options = {})
133
+ options.merge!('container' => self.id[0..7])
134
+ # [code](https://github.com/dotcloud/docker/blob/v0.6.3/commands.go#L1115)
135
+ # Based on the link, the config passed as run, needs to be passed as the
136
+ # body of the post so capture it, remove from the options, and pass it via
137
+ # the post body
138
+ config = MultiJson.dump(options.delete('run'))
139
+ hash = Docker::Util.parse_json(
140
+ connection.post('/commit', options, body: config)
141
+ )
142
+ Docker::Image.send(:new, self.connection, hash)
143
+ end
144
+
145
+ # Return a String representation of the Container.
146
+ def to_s
147
+ "Docker::Container { :id => #{self.id}, :connection => #{self.connection} }"
148
+ end
149
+
150
+ # #json returns information about the Container, #changes returns a list of
151
+ # the changes the Container has made to the filesystem.
152
+ [:json, :changes].each do |method|
153
+ define_method(method) do |opts = {}|
154
+ Docker::Util.parse_json(connection.get(path_for(method), opts))
155
+ end
156
+ end
157
+
158
+ def logs(opts = {})
159
+ connection.get(path_for(:logs), opts)
160
+ end
161
+
162
+ def stats(options = {})
163
+ if block_given?
164
+ options[:read_timeout] ||= 10
165
+ options[:idempotent] ||= false
166
+ parser = lambda do |chunk, remaining_bytes, total_bytes|
167
+ yield Docker::Util.parse_json(chunk)
168
+ end
169
+ begin
170
+ connection.get(path_for(:stats), nil, {response_block: parser}.merge(options))
171
+ rescue Docker::Error::TimeoutError
172
+ # If the container stops, the docker daemon will hold the connection
173
+ # open forever, but stop sending events.
174
+ # So this Timeout indicates the stream is over.
175
+ end
176
+ else
177
+ Docker::Util.parse_json(connection.get(path_for(:stats), {stream: 0}.merge(options)))
178
+ end
179
+ end
180
+
181
+ def rename(new_name)
182
+ query = {}
183
+ query['name'] = new_name
184
+ connection.post(path_for(:rename), query)
185
+ end
186
+
187
+ def update(opts)
188
+ connection.post(path_for(:update), {}, body: MultiJson.dump(opts))
189
+ end
190
+
191
+ def streaming_logs(opts = {}, &block)
192
+ stack_size = opts.delete('stack_size') || opts.delete(:stack_size) || -1
193
+ tty = opts.delete('tty') || opts.delete(:tty) || false
194
+ msgs = Docker::MessagesStack.new(stack_size)
195
+ excon_params = {response_block: Docker::Util.attach_for(block, msgs, tty), idempotent: false}
196
+
197
+ connection.get(path_for(:logs), opts, excon_params)
198
+ msgs.messages.join
199
+ end
200
+
201
+ def start!(opts = {})
202
+ connection.post(path_for(:start), {}, body: MultiJson.dump(opts))
203
+ self
204
+ end
205
+
206
+ def kill!(opts = {})
207
+ connection.post(path_for(:kill), opts)
208
+ self
209
+ end
210
+
211
+ # #start! and #kill! both perform the associated action and
212
+ # return the Container. #start and #kill do the same,
213
+ # but rescue from ServerErrors.
214
+ [:start, :kill].each do |method|
215
+ define_method(method) do |*args|
216
+ begin; public_send(:"#{method}!", *args); rescue ServerError; self end
217
+ end
218
+ end
219
+
220
+ # #stop! and #restart! both perform the associated action and
221
+ # return the Container. #stop and #restart do the same,
222
+ # but rescue from ServerErrors.
223
+ [:stop, :restart].each do |method|
224
+ define_method(:"#{method}!") do |opts = {}|
225
+ timeout = opts.delete('timeout')
226
+ query = {}
227
+ request_options = {
228
+ :body => MultiJson.dump(opts)
229
+ }
230
+ if timeout
231
+ query['t'] = timeout
232
+ # Ensure request does not timeout before Docker timeout
233
+ request_options.merge!(
234
+ read_timeout: timeout.to_i + 5,
235
+ write_timeout: timeout.to_i + 5
236
+ )
237
+ end
238
+ connection.post(path_for(method), query, request_options)
239
+ self
240
+ end
241
+
242
+ define_method(method) do |*args|
243
+ begin; public_send(:"#{method}!", *args); rescue ServerError; self end
244
+ end
245
+ end
246
+
247
+ # remove container
248
+ def remove(options = {})
249
+ connection.delete("/containers/#{self.id}", options)
250
+ nil
251
+ end
252
+ alias_method :delete, :remove
253
+
254
+ # pause and unpause containers
255
+ # #pause! and #unpause! both perform the associated action and
256
+ # return the Container. #pause and #unpause do the same,
257
+ # but rescue from ServerErrors.
258
+ [:pause, :unpause].each do |method|
259
+ define_method(:"#{method}!") do
260
+ connection.post path_for(method)
261
+ self
262
+ end
263
+
264
+ define_method(method) do
265
+ begin; public_send(:"#{method}!"); rescue ServerError; self; end
266
+ end
267
+ end
268
+
269
+ def archive_out(path, &block)
270
+ connection.get(
271
+ path_for(:archive),
272
+ { 'path' => path },
273
+ :response_block => block
274
+ )
275
+ self
276
+ end
277
+
278
+ def archive_in(inputs, output_path, opts = {})
279
+ file_hash = Docker::Util.file_hash_from_paths([*inputs])
280
+ tar = StringIO.new(Docker::Util.create_tar(file_hash))
281
+ archive_in_stream(output_path, opts) do
282
+ tar.read(Excon.defaults[:chunk_size]).to_s
283
+ end
284
+ end
285
+
286
+ def archive_in_stream(output_path, opts = {}, &block)
287
+ overwrite = opts[:overwrite] || opts['overwrite'] || false
288
+
289
+ connection.put(
290
+ path_for(:archive),
291
+ { 'path' => output_path, 'noOverwriteDirNonDir' => !overwrite },
292
+ :headers => {
293
+ 'Content-Type' => 'application/x-tar'
294
+ },
295
+ &block
296
+ )
297
+ self
298
+ end
299
+
300
+ def read_file(path)
301
+ content = StringIO.new
302
+ archive_out(path) do |chunk|
303
+ content.write chunk
304
+ end
305
+
306
+ content.rewind
307
+
308
+ Gem::Package::TarReader.new(content) do |tar|
309
+ tar.each do |tarfile|
310
+ return tarfile.read
311
+ end
312
+ end
313
+ end
314
+
315
+ def store_file(path, file_content)
316
+ output_io = StringIO.new(
317
+ Docker::Util.create_tar(
318
+ path => file_content
319
+ )
320
+ )
321
+
322
+ archive_in_stream("/", overwrite: true) { output_io.read }
323
+ end
324
+
325
+ # Create a new Container.
326
+ def self.create(opts = {}, conn = Docker.connection)
327
+ query = opts.select {|key| ['name', :name].include?(key) }
328
+ clean_opts = opts.reject {|key| ['name', :name].include?(key) }
329
+ resp = conn.post('/containers/create', query, :body => MultiJson.dump(clean_opts))
330
+ hash = Docker::Util.parse_json(resp) || {}
331
+ new(conn, hash)
332
+ end
333
+
334
+ # Return the container with specified ID
335
+ def self.get(id, opts = {}, conn = Docker.connection)
336
+ container_json = conn.get("/containers/#{id}/json", opts)
337
+ hash = Docker::Util.parse_json(container_json) || {}
338
+ new(conn, hash)
339
+ end
340
+
341
+ # Return all of the Containers.
342
+ def self.all(opts = {}, conn = Docker.connection)
343
+ hashes = Docker::Util.parse_json(conn.get('/containers/json', opts)) || []
344
+ hashes.map { |hash| new(conn, hash) }
345
+ end
346
+
347
+ # Prune images
348
+ def self.prune(conn = Docker.connection)
349
+ conn.post("/containers/prune", {})
350
+ nil
351
+ end
352
+
353
+ # Convenience method to return the path for a particular resource.
354
+ def path_for(resource)
355
+ "/containers/#{self.id}/#{resource}"
356
+ end
357
+
358
+ private :path_for
359
+ private_class_method :new
360
+ end