docker-api 2.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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