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,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