docker-api 1.27.0 → 2.0.0

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.
@@ -2,15 +2,26 @@
2
2
  class Docker::Event
3
3
  include Docker::Error
4
4
 
5
- attr_accessor :status, :id, :from, :time
5
+ # Represents the actor object nested within an event
6
+ class Actor
7
+ attr_accessor :ID, :Attributes
6
8
 
7
- def initialize(status, id, from, time)
8
- @status, @id, @from, @time = status, id, from, time
9
- end
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
10
17
 
11
- def to_s
12
- "Docker::Event { :status => #{self.status}, :id => #{self.id}, "\
13
- ":from => #{self.from}, :time => #{self.time} }"
18
+ if self.Attributes.nil?
19
+ self.Attributes = {}
20
+ end
21
+ end
22
+
23
+ alias_method :id, :ID
24
+ alias_method :attributes, :Attributes
14
25
  end
15
26
 
16
27
  class << self
@@ -18,7 +29,9 @@ class Docker::Event
18
29
 
19
30
  def stream(opts = {}, conn = Docker.connection, &block)
20
31
  conn.get('/events', opts, :response_block => lambda { |b, r, t|
21
- block.call(new_event(b, r, t))
32
+ b.each_line do |line|
33
+ block.call(new_event(line, r, t))
34
+ end
22
35
  })
23
36
  end
24
37
 
@@ -29,12 +42,85 @@ class Docker::Event
29
42
  def new_event(body, remaining, total)
30
43
  return if body.nil? || body.empty?
31
44
  json = Docker::Util.parse_json(body)
32
- Docker::Event.new(
33
- json['status'],
34
- json['id'],
35
- json['from'],
36
- json['time']
37
- )
45
+ Docker::Event.new(json)
38
46
  end
39
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
40
126
  end
@@ -20,7 +20,7 @@ class Docker::Exec
20
20
  def self.create(options = {}, conn = Docker.connection)
21
21
  container = options.delete('Container')
22
22
  resp = conn.post("/containers/#{container}/exec", {},
23
- :body => options.to_json)
23
+ body: MultiJson.dump(options))
24
24
  hash = Docker::Util.parse_json(resp) || {}
25
25
  new(conn, hash)
26
26
  end
@@ -51,11 +51,11 @@ class Docker::Exec
51
51
  read_timeout = options[:wait]
52
52
 
53
53
  # Create API Request Body
54
- body = {
55
- "Tty" => tty,
56
- "Detach" => detached
57
- }
58
- excon_params = { :body => body.to_json }
54
+ body = MultiJson.dump(
55
+ 'Tty' => tty,
56
+ 'Detach' => detached
57
+ )
58
+ excon_params = { body: body }
59
59
 
60
60
  msgs = Docker::Messages.new
61
61
  unless detached
@@ -6,17 +6,17 @@ class Docker::Image
6
6
  # an Image. This will not modify the Image, but rather create a new Container
7
7
  # to run the Image. If the image has an embedded config, no command is
8
8
  # necessary, but it will fail with 500 if no config is saved with the image
9
- def run(cmd=nil)
10
- opts = { 'Image' => self.id }
9
+ def run(cmd = nil, options = {})
10
+ opts = {'Image' => self.id}.merge(options)
11
11
  opts["Cmd"] = cmd.is_a?(String) ? cmd.split(/\s+/) : cmd
12
12
  begin
13
13
  Docker::Container.create(opts, connection)
14
14
  .tap(&:start!)
15
- rescue ServerError => ex
15
+ rescue ServerError, ClientError => ex
16
16
  if cmd
17
17
  raise ex
18
18
  else
19
- raise ServerError, "No command specified."
19
+ raise ex, "No command specified."
20
20
  end
21
21
  end
22
22
  end
@@ -108,7 +108,7 @@ class Docker::Image
108
108
 
109
109
  # Create a new Image.
110
110
  def create(opts = {}, creds = nil, conn = Docker.connection, &block)
111
- credentials = creds.nil? ? Docker.creds : creds.to_json
111
+ credentials = creds.nil? ? Docker.creds : MultiJson.dump(creds)
112
112
  headers = credentials && Docker::Util.build_auth_header(credentials) || {}
113
113
  body = ''
114
114
  conn.post(
@@ -117,17 +117,32 @@ class Docker::Image
117
117
  :headers => headers,
118
118
  :response_block => response_block(body, &block)
119
119
  )
120
+ # NOTE: see associated tests for why we're looking at image#end_with?
120
121
  image = opts['fromImage'] || opts[:fromImage]
122
+ tag = opts['tag'] || opts[:tag]
123
+ image = "#{image}:#{tag}" if tag && !image.end_with?(":#{tag}")
121
124
  get(image, {}, conn)
122
125
  end
123
126
 
124
127
  # Return a specific image.
125
128
  def get(id, opts = {}, conn = Docker.connection)
126
- image_json = conn.get("/images/#{URI.encode(id)}/json", opts)
129
+ image_json = conn.get("/images/#{id}/json", opts)
127
130
  hash = Docker::Util.parse_json(image_json) || {}
128
131
  new(conn, hash)
129
132
  end
130
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
+
131
146
  # Save the raw binary representation or one or more Docker images
132
147
  #
133
148
  # @param names [String, Array#String] The image(s) you wish to save
@@ -159,7 +174,7 @@ class Docker::Image
159
174
  # By using compare_by_identity we can create a Hash that has
160
175
  # the same key multiple times.
161
176
  query = {}.tap(&:compare_by_identity)
162
- Array(names).each { |name| query['names'.dup] = URI.encode(name) }
177
+ Array(names).each { |name| query['names'.dup] = name }
163
178
  conn.get(
164
179
  '/images/get',
165
180
  query,
@@ -171,14 +186,14 @@ class Docker::Image
171
186
  # Load a tar Image
172
187
  def load(tar, opts = {}, conn = Docker.connection, creds = nil, &block)
173
188
  headers = build_headers(creds)
189
+ io = tar.is_a?(String) ? File.open(tar, 'rb') : tar
174
190
  body = ""
175
- f = File.open(tar,'rb')
176
191
  conn.post(
177
192
  '/images/load',
178
193
  opts,
179
194
  :headers => headers,
180
195
  :response_block => response_block(body, &block)
181
- ) { f.read(Excon.defaults[:chunk_size]).to_s }
196
+ ) { io.read(Excon.defaults[:chunk_size]).to_s }
182
197
  end
183
198
 
184
199
  # Check if an image exists.
@@ -197,8 +212,14 @@ class Docker::Image
197
212
 
198
213
  # Given a query like `{ :term => 'sshd' }`, queries the Docker Registry for
199
214
  # a corresponding Image.
200
- def search(query = {}, connection = Docker.connection)
201
- body = connection.get('/images/search', query)
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
+ )
202
223
  hashes = Docker::Util.parse_json(body) || []
203
224
  hashes.map { |hash| new(connection, 'id' => hash['name']) }
204
225
  end
@@ -2,18 +2,18 @@
2
2
  class Docker::Network
3
3
  include Docker::Base
4
4
 
5
- def connect(container, opts = {})
5
+ def connect(container, opts = {}, body_opts = {})
6
+ body = MultiJson.dump({ container: container }.merge(body_opts))
6
7
  Docker::Util.parse_json(
7
- connection.post(path_for('connect'), opts,
8
- body: { container: container }.to_json)
8
+ connection.post(path_for('connect'), opts, body: body)
9
9
  )
10
10
  reload
11
11
  end
12
12
 
13
13
  def disconnect(container, opts = {})
14
+ body = MultiJson.dump(container: container)
14
15
  Docker::Util.parse_json(
15
- connection.post(path_for('disconnect'), opts,
16
- body: { container: container }.to_json)
16
+ connection.post(path_for('disconnect'), opts, body: body)
17
17
  )
18
18
  reload
19
19
  end
@@ -34,25 +34,24 @@ class Docker::Network
34
34
  end
35
35
 
36
36
  def reload
37
- network_json = @connection.get("/networks/#{URI.encode(@id)}")
37
+ network_json = @connection.get("/networks/#{@id}")
38
38
  hash = Docker::Util.parse_json(network_json) || {}
39
39
  @info = hash
40
40
  end
41
41
 
42
42
  class << self
43
43
  def create(name, opts = {}, conn = Docker.connection)
44
- default_opts = {
44
+ default_opts = MultiJson.dump({
45
45
  'Name' => name,
46
46
  'CheckDuplicate' => true
47
- }
48
- resp = conn.post('/networks/create', {},
49
- body: default_opts.merge(opts).to_json)
47
+ }.merge(opts))
48
+ resp = conn.post('/networks/create', {}, body: default_opts)
50
49
  response_hash = Docker::Util.parse_json(resp) || {}
51
50
  get(response_hash['Id'], {}, conn) || {}
52
51
  end
53
52
 
54
53
  def get(id, opts = {}, conn = Docker.connection)
55
- network_json = conn.get("/networks/#{URI.encode(id)}", opts)
54
+ network_json = conn.get("/networks/#{id}", opts)
56
55
  hash = Docker::Util.parse_json(network_json) || {}
57
56
  new(conn, hash)
58
57
  end
@@ -63,10 +62,15 @@ class Docker::Network
63
62
  end
64
63
 
65
64
  def remove(id, opts = {}, conn = Docker.connection)
66
- conn.delete("/networks/#{URI.encode(id)}", opts)
65
+ conn.delete("/networks/#{id}", opts)
67
66
  nil
68
67
  end
69
68
  alias_method :delete, :remove
69
+
70
+ def prune(conn = Docker.connection)
71
+ conn.post("/networks/prune", {})
72
+ nil
73
+ end
70
74
  end
71
75
 
72
76
  # Convenience method to return the path for a particular resource.
@@ -1,6 +1,9 @@
1
1
  # This module holds shared logic that doesn't really belong anywhere else in the
2
2
  # gem.
3
3
  module Docker::Util
4
+ # http://www.tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm#STANDARD-WILDCARDS
5
+ GLOB_WILDCARDS = /[\?\*\[\{]/
6
+
4
7
  include Docker::Error
5
8
 
6
9
  module_function
@@ -99,8 +102,8 @@ module Docker::Util
99
102
  end
100
103
 
101
104
  def parse_json(body)
102
- JSON.parse(body) unless body.nil? || body.empty? || (body == 'null')
103
- rescue JSON::ParserError => ex
105
+ MultiJson.load(body) unless body.nil? || body.empty? || (body == 'null')
106
+ rescue MultiJson::ParseError => ex
104
107
  raise UnexpectedResponseError, ex.message
105
108
  end
106
109
 
@@ -119,8 +122,12 @@ module Docker::Util
119
122
  def create_tar(hash = {})
120
123
  output = StringIO.new
121
124
  Gem::Package::TarWriter.new(output) do |tar|
122
- hash.each do |file_name, input|
123
- tar.add_file(file_name, 0640) { |tar_file| tar_file.write(input) }
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
124
131
  end
125
132
  end
126
133
  output.tap(&:rewind).string
@@ -137,7 +144,10 @@ module Docker::Util
137
144
 
138
145
  def create_relative_dir_tar(directory, output)
139
146
  Gem::Package::TarWriter.new(output) do |tar|
140
- Find.find(directory) do |prefixed_file_name|
147
+ files = glob_all_files(File.join(directory, "**/*"))
148
+ remove_ignored_files!(directory, files)
149
+
150
+ files.each do |prefixed_file_name|
141
151
  stat = File.stat(prefixed_file_name)
142
152
  next unless stat.file?
143
153
 
@@ -201,18 +211,29 @@ module Docker::Util
201
211
  basename = File.basename(local_path)
202
212
  if File.directory?(local_path)
203
213
  tar = create_dir_tar(local_path)
204
- file_hash[basename] = tar.read
214
+ file_hash[basename] = {
215
+ content: tar.read,
216
+ permissions: filesystem_permissions(local_path)
217
+ }
205
218
  tar.close
206
219
  FileUtils.rm(tar.path)
207
220
  else
208
- file_hash[basename] = File.read(local_path)
221
+ file_hash[basename] = {
222
+ content: File.read(local_path, mode: 'rb'),
223
+ permissions: filesystem_permissions(local_path)
224
+ }
209
225
  end
210
226
  end
211
227
  end
212
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
+
213
234
  def build_auth_header(credentials)
214
- credentials = credentials.to_json if credentials.is_a?(Hash)
215
- encoded_creds = Base64.encode64(credentials).gsub(/\n/, '')
235
+ credentials = MultiJson.dump(credentials) if credentials.is_a?(Hash)
236
+ encoded_creds = Base64.urlsafe_encode64(credentials)
216
237
  {
217
238
  'X-Registry-Auth' => encoded_creds
218
239
  }
@@ -220,20 +241,39 @@ module Docker::Util
220
241
 
221
242
  def build_config_header(credentials)
222
243
  if credentials.is_a?(String)
223
- credentials = JSON.parse(credentials, symbolize_names: true)
244
+ credentials = MultiJson.load(credentials, symbolize_keys: true)
224
245
  end
225
- header = {
246
+
247
+ header = MultiJson.dump(
226
248
  credentials[:serveraddress].to_s => {
227
- "username" => credentials[:username].to_s,
228
- "password" => credentials[:password].to_s,
229
- "email" => credentials[:email].to_s
249
+ 'username' => credentials[:username].to_s,
250
+ 'password' => credentials[:password].to_s,
251
+ 'email' => credentials[:email].to_s
230
252
  }
231
- }.to_json
253
+ )
232
254
 
233
- encoded_header = Base64.encode64(header).gsub(/\n/, '')
255
+ encoded_header = Base64.urlsafe_encode64(header)
234
256
 
235
257
  {
236
258
  'X-Registry-Config' => encoded_header
237
259
  }
238
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
239
279
  end