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,40 @@
1
+ # This module holds the Errors for the gem.
2
+ module Docker::Error
3
+
4
+ # The default error. It's never actually raised, but can be used to catch all
5
+ # gem-specific errors that are thrown as they all subclass from this.
6
+ class DockerError < StandardError; end
7
+
8
+ # Raised when invalid arguments are passed to a method.
9
+ class ArgumentError < DockerError; end
10
+
11
+ # Raised when a request returns a 400.
12
+ class ClientError < DockerError; end
13
+
14
+ # Raised when a request returns a 401.
15
+ class UnauthorizedError < DockerError; end
16
+
17
+ # Raised when a request returns a 404.
18
+ class NotFoundError < DockerError; end
19
+
20
+ # Raised when a request returns a 409.
21
+ class ConflictError < DockerError; end
22
+
23
+ # Raised when a request returns a 500.
24
+ class ServerError < DockerError; end
25
+
26
+ # Raised when there is an unexpected response code / body.
27
+ class UnexpectedResponseError < DockerError; end
28
+
29
+ # Raised when there is an incompatible version of Docker.
30
+ class VersionError < DockerError; end
31
+
32
+ # Raised when a request times out.
33
+ class TimeoutError < DockerError; end
34
+
35
+ # Raised when login fails.
36
+ class AuthenticationError < DockerError; end
37
+
38
+ # Raised when an IO action fails.
39
+ class IOError < DockerError; end
40
+ end
@@ -0,0 +1,126 @@
1
+ # This class represents a Docker Event.
2
+ class Docker::Event
3
+ include Docker::Error
4
+
5
+ # Represents the actor object nested within an event
6
+ class Actor
7
+ attr_accessor :ID, :Attributes
8
+
9
+ def initialize(actor_attributes = {})
10
+ [:ID, :Attributes].each do |sym|
11
+ value = actor_attributes[sym]
12
+ if value.nil?
13
+ value = actor_attributes[sym.to_s]
14
+ end
15
+ send("#{sym}=", value)
16
+ end
17
+
18
+ if self.Attributes.nil?
19
+ self.Attributes = {}
20
+ end
21
+ end
22
+
23
+ alias_method :id, :ID
24
+ alias_method :attributes, :Attributes
25
+ end
26
+
27
+ class << self
28
+ include Docker::Error
29
+
30
+ def stream(opts = {}, conn = Docker.connection, &block)
31
+ conn.get('/events', opts, :response_block => lambda { |b, r, t|
32
+ b.each_line do |line|
33
+ block.call(new_event(line, r, t))
34
+ end
35
+ })
36
+ end
37
+
38
+ def since(since, opts = {}, conn = Docker.connection, &block)
39
+ stream(opts.merge(:since => since), conn, &block)
40
+ end
41
+
42
+ def new_event(body, remaining, total)
43
+ return if body.nil? || body.empty?
44
+ json = Docker::Util.parse_json(body)
45
+ Docker::Event.new(json)
46
+ end
47
+ end
48
+
49
+ attr_accessor :Type, :Action, :time, :timeNano
50
+ attr_reader :Actor
51
+ # Deprecated interface
52
+ attr_accessor :status, :from
53
+
54
+ def initialize(event_attributes = {})
55
+ [:Type, :Action, :Actor, :time, :timeNano, :status, :from].each do |sym|
56
+ value = event_attributes[sym]
57
+ if value.nil?
58
+ value = event_attributes[sym.to_s]
59
+ end
60
+ send("#{sym}=", value)
61
+ end
62
+
63
+ if @Actor.nil?
64
+ value = event_attributes[:id]
65
+ if value.nil?
66
+ value = event_attributes['id']
67
+ end
68
+ self.Actor = Actor.new(ID: value)
69
+ end
70
+ end
71
+
72
+ def ID
73
+ self.actor.ID
74
+ end
75
+
76
+ def Actor=(actor)
77
+ return if actor.nil?
78
+ if actor.is_a? Actor
79
+ @Actor = actor
80
+ else
81
+ @Actor = Actor.new(actor)
82
+ end
83
+ end
84
+
85
+ alias_method :type, :Type
86
+ alias_method :action, :Action
87
+ alias_method :actor, :Actor
88
+ alias_method :time_nano, :timeNano
89
+ alias_method :id, :ID
90
+
91
+ def to_s
92
+ if type.nil? && action.nil?
93
+ to_s_legacy
94
+ else
95
+ to_s_actor_style
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def to_s_legacy
102
+ attributes = []
103
+ attributes << "from=#{from}" unless from.nil?
104
+
105
+ unless attributes.empty?
106
+ attribute_string = "(#{attributes.join(', ')}) "
107
+ end
108
+
109
+ "Docker::Event { #{time} #{status} #{id} #{attribute_string}}"
110
+ end
111
+
112
+ def to_s_actor_style
113
+ most_accurate_time = time_nano || time
114
+
115
+ attributes = []
116
+ actor.attributes.each do |attribute, value|
117
+ attributes << "#{attribute}=#{value}"
118
+ end
119
+
120
+ unless attributes.empty?
121
+ attribute_string = "(#{attributes.join(', ')}) "
122
+ end
123
+
124
+ "Docker::Event { #{most_accurate_time} #{type} #{action} #{actor.id} #{attribute_string}}"
125
+ end
126
+ end
@@ -0,0 +1,107 @@
1
+ # This class represents a Docker Exec Instance.
2
+ class Docker::Exec
3
+ include Docker::Base
4
+
5
+ # Convert details about the object into a string
6
+ #
7
+ # @return [String] String representation of the Exec instance object
8
+ def to_s
9
+ "Docker::Exec { :id => #{self.id}, :connection => #{self.connection} }"
10
+ end
11
+
12
+ # Create a new Exec instance in a running container. Please note, this does
13
+ # NOT execute the instance - you must run #start. Also, each instance is
14
+ # one-time use only.
15
+ #
16
+ # @param options [Hash] Parameters to pass in to the API.
17
+ # @param conn [Docker::Connection] Connection to Docker Remote API
18
+ #
19
+ # @return [Docker::Exec] self
20
+ def self.create(options = {}, conn = Docker.connection)
21
+ container = options.delete('Container')
22
+ resp = conn.post("/containers/#{container}/exec", {},
23
+ body: MultiJson.dump(options))
24
+ hash = Docker::Util.parse_json(resp) || {}
25
+ new(conn, hash)
26
+ end
27
+
28
+ # Get info about the Exec instance
29
+ #
30
+ def json
31
+ Docker::Util.parse_json(connection.get(path_for(:json), {}))
32
+ end
33
+
34
+ # Start the Exec instance. The Exec instance is deleted after this so this
35
+ # command can only be run once.
36
+ #
37
+ # @param options [Hash] Options to dictate behavior of the instance
38
+ # @option options [Object] :stdin (nil) The object to pass to STDIN.
39
+ # @option options [TrueClass, FalseClass] :detach (false) Whether to attach
40
+ # to STDOUT/STDERR.
41
+ # @option options [TrueClass, FalseClass] :tty (false) Whether to attach using
42
+ # a pseudo-TTY.
43
+ #
44
+ # @return [Array, Array, Int] The STDOUT, STDERR and exit code
45
+ def start!(options = {}, &block)
46
+
47
+ # Parse the Options
48
+ tty = !!options.delete(:tty)
49
+ detached = !!options.delete(:detach)
50
+ stdin = options[:stdin]
51
+ read_timeout = options[:wait]
52
+
53
+ # Create API Request Body
54
+ body = MultiJson.dump(
55
+ 'Tty' => tty,
56
+ 'Detach' => detached
57
+ )
58
+ excon_params = { body: body }
59
+
60
+ msgs = Docker::Messages.new
61
+ unless detached
62
+ if stdin
63
+ excon_params[:hijack_block] = Docker::Util.hijack_for(stdin, block,
64
+ msgs, tty)
65
+ else
66
+ excon_params[:response_block] = Docker::Util.attach_for(block,
67
+ msgs, tty)
68
+ end
69
+ end
70
+
71
+ excon_params[:read_timeout] = read_timeout unless read_timeout.nil?
72
+
73
+ connection.post(path_for(:start), nil, excon_params)
74
+ [msgs.stdout_messages, msgs.stderr_messages, self.json['ExitCode']]
75
+ end
76
+
77
+ # #start! performs the associated action and returns the output.
78
+ # #start does the same, but rescues from ServerErrors.
79
+ [:start].each do |method|
80
+ define_method(method) do |*args|
81
+ begin; public_send(:"#{method}!", *args); rescue ServerError; self end
82
+ end
83
+ end
84
+
85
+ # Resize the TTY associated with the Exec instance
86
+ #
87
+ # @param query [Hash] API query parameters
88
+ # @option query [Fixnum] h Height of the TTY
89
+ # @option query [Fixnum] w Width of the TTY
90
+ #
91
+ # @return [Docker::Exec] self
92
+ def resize(query = {})
93
+ connection.post(path_for(:resize), query)
94
+ self
95
+ end
96
+
97
+ # Get the request URI for the given endpoint
98
+ #
99
+ # @param endpoint [Symbol] The endpoint to grab
100
+ # @return [String] The full Remote API endpoint with ID
101
+ def path_for(endpoint)
102
+ "/exec/#{self.id}/#{endpoint}"
103
+ end
104
+
105
+ private :path_for
106
+ private_class_method :new
107
+ end
@@ -0,0 +1,356 @@
1
+ # This class represents a Docker Image.
2
+ class Docker::Image
3
+ include Docker::Base
4
+
5
+ # Given a command and optional list of streams to attach to, run a command on
6
+ # an Image. This will not modify the Image, but rather create a new Container
7
+ # to run the Image. If the image has an embedded config, no command is
8
+ # necessary, but it will fail with 500 if no config is saved with the image
9
+ def run(cmd = nil, options = {})
10
+ opts = {'Image' => self.id}.merge(options)
11
+ opts["Cmd"] = cmd.is_a?(String) ? cmd.split(/\s+/) : cmd
12
+ begin
13
+ Docker::Container.create(opts, connection)
14
+ .tap(&:start!)
15
+ rescue ServerError, ClientError => ex
16
+ if cmd
17
+ raise ex
18
+ else
19
+ raise ex, "No command specified."
20
+ end
21
+ end
22
+ end
23
+
24
+ # Push the Image to the Docker registry.
25
+ def push(creds = nil, options = {}, &block)
26
+ repo_tag = options.delete(:repo_tag) || ensure_repo_tags.first
27
+ raise ArgumentError, "Image is untagged" if repo_tag.nil?
28
+ repo, tag = Docker::Util.parse_repo_tag(repo_tag)
29
+ raise ArgumentError, "Image does not have a name to push." if repo.nil?
30
+
31
+ body = ""
32
+ credentials = creds || Docker.creds || {}
33
+ headers = Docker::Util.build_auth_header(credentials)
34
+ opts = {:tag => tag}.merge(options)
35
+ connection.post("/images/#{repo}/push", opts, :headers => headers,
36
+ :response_block => self.class.response_block(body, &block))
37
+ self
38
+ end
39
+
40
+ # Tag the Image.
41
+ def tag(opts = {})
42
+ self.info['RepoTags'] ||= []
43
+ connection.post(path_for(:tag), opts)
44
+ repo = opts['repo'] || opts[:repo]
45
+ tag = opts['tag'] || opts[:tag] || 'latest'
46
+ self.info['RepoTags'] << "#{repo}:#{tag}"
47
+ end
48
+
49
+ # Given a path of a local file and the path it should be inserted, creates
50
+ # a new Image that has that file.
51
+ def insert_local(opts = {})
52
+ local_paths = opts.delete('localPath')
53
+ output_path = opts.delete('outputPath')
54
+
55
+ local_paths = [ local_paths ] unless local_paths.is_a?(Array)
56
+
57
+ file_hash = Docker::Util.file_hash_from_paths(local_paths)
58
+
59
+ file_hash['Dockerfile'] = dockerfile_for(file_hash, output_path)
60
+
61
+ tar = Docker::Util.create_tar(file_hash)
62
+ body = connection.post('/build', opts, :body => tar)
63
+ self.class.send(:new, connection, 'id' => Docker::Util.extract_id(body))
64
+ end
65
+
66
+ # Remove the Image from the server.
67
+ def remove(opts = {})
68
+ name = opts.delete(:name) || self.id
69
+ connection.delete("/images/#{name}", opts)
70
+ end
71
+ alias_method :delete, :remove
72
+
73
+ # Return a String representation of the Image.
74
+ def to_s
75
+ "Docker::Image { :id => #{self.id}, :info => #{self.info.inspect}, "\
76
+ ":connection => #{self.connection} }"
77
+ end
78
+
79
+ # #json returns extra information about an Image, #history returns its
80
+ # history.
81
+ [:json, :history].each do |method|
82
+ define_method(method) do |opts = {}|
83
+ Docker::Util.parse_json(connection.get(path_for(method), opts))
84
+ end
85
+ end
86
+
87
+ # Save the image as a tarball
88
+ def save(filename = nil)
89
+ self.class.save(self.id, filename, connection)
90
+ end
91
+
92
+ # Save the image as a tarball to an IO object.
93
+ def save_stream(opts = {}, &block)
94
+ self.class.save_stream(self.id, opts, connection, &block)
95
+ end
96
+
97
+ # Update the @info hash, which is the only mutable state in this object.
98
+ def refresh!
99
+ img = Docker::Image.all({:all => true}, connection).find { |image|
100
+ image.id.start_with?(self.id) || self.id.start_with?(image.id)
101
+ }
102
+ info.merge!(self.json)
103
+ img && info.merge!(img.info)
104
+ self
105
+ end
106
+
107
+ class << self
108
+
109
+ # Create a new Image.
110
+ def create(opts = {}, creds = nil, conn = Docker.connection, &block)
111
+ credentials = creds.nil? ? Docker.creds : MultiJson.dump(creds)
112
+ headers = credentials && Docker::Util.build_auth_header(credentials) || {}
113
+ body = ''
114
+ conn.post(
115
+ '/images/create',
116
+ opts,
117
+ :headers => headers,
118
+ :response_block => response_block(body, &block)
119
+ )
120
+ # NOTE: see associated tests for why we're looking at image#end_with?
121
+ image = opts['fromImage'] || opts[:fromImage]
122
+ tag = opts['tag'] || opts[:tag]
123
+ image = "#{image}:#{tag}" if tag && !image.end_with?(":#{tag}")
124
+ get(image, {}, conn)
125
+ end
126
+
127
+ # Return a specific image.
128
+ def get(id, opts = {}, conn = Docker.connection)
129
+ image_json = conn.get("/images/#{id}/json", opts)
130
+ hash = Docker::Util.parse_json(image_json) || {}
131
+ new(conn, hash)
132
+ end
133
+
134
+ # Delete a specific image
135
+ def remove(id, opts = {}, conn = Docker.connection)
136
+ conn.delete("/images/#{id}", opts)
137
+ end
138
+ alias_method :delete, :remove
139
+
140
+ # Prune images
141
+ def prune(conn = Docker.connection)
142
+ conn.post("/images/prune", {})
143
+ end
144
+
145
+
146
+ # Save the raw binary representation or one or more Docker images
147
+ #
148
+ # @param names [String, Array#String] The image(s) you wish to save
149
+ # @param filename [String] The file to export the data to.
150
+ # @param conn [Docker::Connection] The Docker connection to use
151
+ #
152
+ # @return [NilClass, String] If filename is nil, return the string
153
+ # representation of the binary data. If the filename is not nil, then
154
+ # return nil.
155
+ def save(names, filename = nil, conn = Docker.connection)
156
+ if filename
157
+ File.open(filename, 'wb') do |file|
158
+ save_stream(names, {}, conn, &response_block_for_save(file))
159
+ end
160
+ nil
161
+ else
162
+ string = ''
163
+ save_stream(names, {}, conn, &response_block_for_save(string))
164
+ string
165
+ end
166
+ end
167
+
168
+ # Stream the contents of Docker image(s) to a block.
169
+ #
170
+ # @param names [String, Array#String] The image(s) you wish to save
171
+ # @param conn [Docker::Connection] The Docker connection to use
172
+ # @yield chunk [String] a chunk of the Docker image(s).
173
+ def save_stream(names, opts = {}, conn = Docker.connection, &block)
174
+ # By using compare_by_identity we can create a Hash that has
175
+ # the same key multiple times.
176
+ query = {}.tap(&:compare_by_identity)
177
+ Array(names).each { |name| query['names'.dup] = name }
178
+ conn.get(
179
+ '/images/get',
180
+ query,
181
+ opts.merge(:response_block => block)
182
+ )
183
+ nil
184
+ end
185
+
186
+ # Load a tar Image
187
+ def load(tar, opts = {}, conn = Docker.connection, creds = nil, &block)
188
+ headers = build_headers(creds)
189
+ io = tar.is_a?(String) ? File.open(tar, 'rb') : tar
190
+ body = ""
191
+ conn.post(
192
+ '/images/load',
193
+ opts,
194
+ :headers => headers,
195
+ :response_block => response_block(body, &block)
196
+ ) { io.read(Excon.defaults[:chunk_size]).to_s }
197
+ end
198
+
199
+ # Check if an image exists.
200
+ def exist?(id, opts = {}, conn = Docker.connection)
201
+ get(id, opts, conn)
202
+ true
203
+ rescue Docker::Error::NotFoundError
204
+ false
205
+ end
206
+
207
+ # Return every Image.
208
+ def all(opts = {}, conn = Docker.connection)
209
+ hashes = Docker::Util.parse_json(conn.get('/images/json', opts)) || []
210
+ hashes.map { |hash| new(conn, hash) }
211
+ end
212
+
213
+ # Given a query like `{ :term => 'sshd' }`, queries the Docker Registry for
214
+ # a corresponding Image.
215
+ def search(query = {}, connection = Docker.connection, creds = nil)
216
+ credentials = creds.nil? ? Docker.creds : creds.to_json
217
+ headers = credentials && Docker::Util.build_auth_header(credentials) || {}
218
+ body = connection.get(
219
+ '/images/search',
220
+ query,
221
+ :headers => headers,
222
+ )
223
+ hashes = Docker::Util.parse_json(body) || []
224
+ hashes.map { |hash| new(connection, 'id' => hash['name']) }
225
+ end
226
+
227
+ # Import an Image from the output of Docker::Container#export. The first
228
+ # argument may either be a File or URI.
229
+ def import(imp, opts = {}, conn = Docker.connection)
230
+ open(imp) do |io|
231
+ import_stream(opts, conn) do
232
+ io.read(Excon.defaults[:chunk_size]).to_s
233
+ end
234
+ end
235
+ rescue StandardError
236
+ raise Docker::Error::IOError, "Could not import '#{imp}'"
237
+ end
238
+
239
+ def import_stream(options = {}, connection = Docker.connection, &block)
240
+ body = connection.post(
241
+ '/images/create',
242
+ options.merge('fromSrc' => '-'),
243
+ :headers => { 'Content-Type' => 'application/tar',
244
+ 'Transfer-Encoding' => 'chunked' },
245
+ &block
246
+ )
247
+ new(connection, 'id'=> Docker::Util.parse_json(body)['status'])
248
+ end
249
+
250
+ # Given a Dockerfile as a string, builds an Image.
251
+ def build(commands, opts = {}, connection = Docker.connection, &block)
252
+ body = ""
253
+ connection.post(
254
+ '/build', opts,
255
+ :body => Docker::Util.create_tar('Dockerfile' => commands),
256
+ :response_block => response_block(body, &block)
257
+ )
258
+ new(connection, 'id' => Docker::Util.extract_id(body))
259
+ rescue Docker::Error::ServerError
260
+ raise Docker::Error::UnexpectedResponseError
261
+ end
262
+
263
+ # Given File like object containing a tar file, builds an Image.
264
+ #
265
+ # If a block is passed, chunks of output produced by Docker will be passed
266
+ # to that block.
267
+ def build_from_tar(tar, opts = {}, connection = Docker.connection,
268
+ creds = nil, &block)
269
+
270
+ headers = build_headers(creds)
271
+
272
+ # The response_block passed to Excon will build up this body variable.
273
+ body = ""
274
+ connection.post(
275
+ '/build', opts,
276
+ :headers => headers,
277
+ :response_block => response_block(body, &block)
278
+ ) { tar.read(Excon.defaults[:chunk_size]).to_s }
279
+
280
+ new(connection,
281
+ 'id' => Docker::Util.extract_id(body),
282
+ :headers => headers)
283
+ end
284
+
285
+ # Given a directory that contains a Dockerfile, builds an Image.
286
+ #
287
+ # If a block is passed, chunks of output produced by Docker will be passed
288
+ # to that block.
289
+ def build_from_dir(dir, opts = {}, connection = Docker.connection,
290
+ creds = nil, &block)
291
+
292
+ tar = Docker::Util.create_dir_tar(dir)
293
+ build_from_tar tar, opts, connection, creds, &block
294
+ ensure
295
+ unless tar.nil?
296
+ tar.close
297
+ FileUtils.rm(tar.path, force: true)
298
+ end
299
+ end
300
+ end
301
+
302
+ private
303
+
304
+ # A method to build the config header and merge it into the
305
+ # headers sent by build_from_dir.
306
+ def self.build_headers(creds=nil)
307
+ credentials = creds || Docker.creds || {}
308
+ config_header = Docker::Util.build_config_header(credentials)
309
+
310
+ headers = { 'Content-Type' => 'application/tar',
311
+ 'Transfer-Encoding' => 'chunked' }
312
+ headers = headers.merge(config_header) if config_header
313
+ headers
314
+ end
315
+
316
+ # Convenience method to return the path for a particular resource.
317
+ def path_for(resource)
318
+ "/images/#{self.id}/#{resource}"
319
+ end
320
+
321
+
322
+ # Convience method to get the Dockerfile for a file hash and a path to
323
+ # output to.
324
+ def dockerfile_for(file_hash, output_path)
325
+ dockerfile = "from #{self.id}\n"
326
+
327
+ file_hash.keys.each do |basename|
328
+ dockerfile << "add #{basename} #{output_path}\n"
329
+ end
330
+
331
+ dockerfile
332
+ end
333
+
334
+ def ensure_repo_tags
335
+ refresh! unless info.has_key?('RepoTags')
336
+ info['RepoTags']
337
+ end
338
+
339
+ # Generates the block to be passed as a reponse block to Excon. The returned
340
+ # lambda will append Docker output to the first argument, and yield output to
341
+ # the passed block, if a block is given.
342
+ def self.response_block(body)
343
+ lambda do |chunk, remaining, total|
344
+ body << chunk
345
+ yield chunk if block_given?
346
+ end
347
+ end
348
+
349
+ # Generates the block to be passed in to the save request. This lambda will
350
+ # append the streaming data to the file provided.
351
+ def self.response_block_for_save(file)
352
+ lambda do |chunk, remianing, total|
353
+ file << chunk
354
+ end
355
+ end
356
+ end