dockerapi 0.2.0 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12366435331bd8fc215857a65d28b337f9ccb22556d8451e6f213d4fda8a5113
4
- data.tar.gz: 724f52952a3f24dc664dc1b1b5cc66a75ae920b281de61b36ae74ed5431d7159
3
+ metadata.gz: 756ada080034328ec7c5e7950d5565e6e7d3612bc92af58d9c8f89558a75f5c5
4
+ data.tar.gz: cba4ac000ef8e9d4374bc36be4e87dd3c2227b2fa59cc062f24ad7a06de11c52
5
5
  SHA512:
6
- metadata.gz: 61ac762e369e72c7b715b2ae3c19cc9046fd25425ef865bbb723a86f79df57716dfa1c1d42720830b10f441150d96def147649921e764180c194e778589071bd
7
- data.tar.gz: 9d2327454dc893fe4cf9ab5adf06619a69142dea22bad32536886552711f3f6119060f859092af4ab17285d2a740e2aa56b563d32fb367c5a55cc6183c2967dd
6
+ metadata.gz: 0c6c059fa47afc4749dac18c62d61a984c2874b41046c2926d7eb884a7761ea088fef4209deaf71fe5864e5351e8e96a975f29520760ae6559209d323dd4f4b6
7
+ data.tar.gz: 25bad2ed4c92a2b5551d78e3a12833037623e7f26045a829b587d4136c383d7431ae7a3d17bf6b9ca139c4fcc060cbf07f15a61854a4a43f601455613174f943
@@ -1,3 +1,57 @@
1
+ # 0.7.0
2
+
3
+ Major changes: Docker::API::Connection is now a regular class intead of a Singleton, allowing multiple connections to be stablished within the same program (replacing the connect_to implementation). To leverage this feature, API-related classes must be initialized and may or may not receive a Docker::API::Connection as parameter, or it'll connect to /var/run/docker.sock by default. For this reason, class methods were replaced with instance methods. Documentation will reflect this changes of implementation.
4
+
5
+ Bug fix: Image push returns a 20X status even when the push is unsucessful. To prevent false positives, it now requires the authentication parameters to be provided, generating a 403 status for invalid credentials or an error if they are absent.
6
+
7
+ # 0.6.0
8
+
9
+ Add connection parameters specifications with connect_to in Docker::API::Connection.
10
+
11
+ Add Docker::API::Exec methods:
12
+ * create
13
+ * start
14
+ * resize
15
+ * inspect
16
+
17
+ # 0.5.0
18
+
19
+ Add Docker::API::System methods:
20
+ * auth
21
+ * ping
22
+ * info
23
+ * version
24
+ * events
25
+ * df
26
+
27
+ Add new response class Docker::API::Response with the following methods:
28
+ * json
29
+ * path
30
+ * success?
31
+
32
+ Error classes output better error messages.
33
+
34
+ # 0.4.0
35
+
36
+ Add Docker::API::Network methods:
37
+ * list
38
+ * inspect
39
+ * create
40
+ * remove
41
+ * prune
42
+ * connect
43
+ * disconnect
44
+
45
+ # 0.3.0
46
+
47
+ Add Docker::API::Volume methods:
48
+ * list
49
+ * inspect
50
+ * create
51
+ * remove
52
+ * prune
53
+
54
+
1
55
  # 0.2.0
2
56
 
3
57
  Add Docker::API::Image methods:
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dockerapi (0.2.0)
4
+ dockerapi (0.7.0)
5
5
  excon (~> 0.74.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -20,72 +20,76 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
+ New implementation details as of v0.7.0.
24
+
23
25
  ### Images
24
26
 
25
27
  ```ruby
28
+ # Connect to local image endpoints
29
+ image = Docker::API::Image.new
30
+
26
31
  # Pull from a public repository
27
- Docker::API::Image.create( fromImage: "nginx:latest" )
32
+ image.create( fromImage: "nginx:latest" )
28
33
 
29
34
  # Pull from a private repository
30
- Docker::API::Image.create( {fromImage: "private/repo:tag"}, {username: "janedoe", password: "password"} )
35
+ image.create( {fromImage: "private/repo:tag"}, {username: "janedoe", password: "password"} )
31
36
 
32
37
  # Create image from local tar file
33
- Docker::API::Image.create( fromSrc: "/path/to/file.tar", repo: "repo:tag" )
38
+ image.create( fromSrc: "/path/to/file.tar", repo: "repo:tag" )
34
39
 
35
40
  # Create image from remote tar file
36
- Docker::API::Image.create( fromSrc: "https://url.to/file.tar", repo: "repo:tag" )
41
+ image.create( fromSrc: "https://url.to/file.tar", repo: "repo:tag" )
37
42
 
38
43
  # List images
39
- Docker::API::Image.list
40
- Docker::API::Image.list( all:true )
44
+ image.list
41
45
 
42
46
  # Inspect image
43
- Docker::API::Image.inspect("image")
47
+ image.inspect("image")
44
48
 
45
49
  # History
46
- Docker::API::Image.history("image")
50
+ image.history("image")
47
51
 
48
52
  # Search image
49
- Docker::API::Image.search(term: "busybox", limit: 2)
50
- Docker::API::Image.search(term: "busybox", filters: {"is-automated": {"true": true}})
51
- Docker::API::Image.search(term: "busybox", filters: {"is-official": {"true": true}})
53
+ image.search(term: "busybox", limit: 2)
54
+ image.search(term: "busybox", filters: {"is-automated": {"true": true}})
55
+ image.search(term: "busybox", filters: {"is-official": {"true": true}})
52
56
 
53
57
  # Tag image
54
- Docker::API::Image.tag("current:tag", repo: "new:tag") # or
55
- Docker::API::Image.tag("current:tag", repo: "new", tag: "tag")
58
+ image.tag("current:tag", repo: "new:tag") # or
59
+ image.tag("current:tag", repo: "new", tag: "tag")
56
60
 
57
61
  # Push image
58
- Docker::API::Image.push("repo:tag") # to dockerhub
59
- Docker::API::Image.push("localhost:5000/repo:tag") # to local registry
60
- Docker::API::Image.push("private/repo", {tag: "tag"}, {username: "janedoe", password: "password"} # to private repository
62
+ image.push("repo:tag") # to dockerhub
63
+ image.push("localhost:5000/repo:tag") # to local registry
64
+ image.push("private/repo", {tag: "tag"}, {username: "janedoe", password: "password"} # to private repository
61
65
 
62
- # Remove container
63
- Docker::API::Image.remove("image")
64
- Docker::API::Image.remove("image", force: true)
66
+ # Remove image
67
+ image.remove("image")
68
+ image.remove("image", force: true)
65
69
 
66
70
  # Remove unsued images (prune)
67
- Docker::API::Image.prune(filters: {dangling: {"false": true}})
71
+ image.prune(filters: {dangling: {"false": true}})
68
72
 
69
73
  # Create image from a container (commit)
70
- Docker::API::Image.commit(container: container, repo: "my/image", tag: "latest", comment: "Comment from commit", author: "dockerapi", pause: false )
74
+ image.commit(container: container, repo: "my/image", tag: "latest", comment: "Comment from commit", author: "dockerapi", pause: false )
71
75
 
72
76
  # Build image from a local tar file
73
- Docker::API::Image.build("/path/to/file.tar")
77
+ image.build("/path/to/file.tar")
74
78
 
75
79
  # Build image from a remote tar file
76
- Docker::API::Image.build(nil, remote: "https://url.to/file.tar")
80
+ image.build(nil, remote: "https://url.to/file.tar")
77
81
 
78
82
  # Build image from a remote Dockerfile
79
- Docker::API::Image.build(nil, remote: "https://url.to/Dockerfile")
83
+ image.build(nil, remote: "https://url.to/Dockerfile")
80
84
 
81
85
  # Delete builder cache
82
- Docker::API::Image.delete_cache
86
+ image.delete_cache
83
87
 
84
88
  # Export repo
85
- Docker::API::Image.export("repo:tag", "~/exported_image.tar")
89
+ image.export("repo:tag", "~/exported_image.tar")
86
90
 
87
91
  # Import repo
88
- Docker::API::Image.import("/path/to/file.tar")
92
+ image.import("/path/to/file.tar")
89
93
  ```
90
94
 
91
95
  ### Containers
@@ -94,62 +98,177 @@ Let's test a Nginx container
94
98
 
95
99
  ```ruby
96
100
  # Pull nginx image
97
- Docker::API::Image.create( fromImage: "nginx:latest" )
101
+ Docker::API::Image.new.create( fromImage: "nginx:latest" )
102
+
103
+ # Connect to local container endpoints
104
+ container = Docker::API::Container.new
98
105
 
99
106
  # Create container
100
- Docker::API::Container.create( {name: "nginx"}, {Image: "nginx:latest", HostConfig: {PortBindings: {"80/tcp": [ {HostIp: "0.0.0.0", HostPort: "80"} ]}}})
107
+ container.create( {name: "nginx"}, {Image: "nginx:latest", HostConfig: {PortBindings: {"80/tcp": [ {HostIp: "0.0.0.0", HostPort: "80"} ]}}})
101
108
 
102
109
  # Start container
103
- Docker::API::Container.start("nginx")
110
+ container.start("nginx")
104
111
 
105
112
  # Open localhost or machine IP to check the container running
106
113
 
107
114
  # Restart container
108
- Docker::API::Container.restart("nginx")
115
+ container.restart("nginx")
109
116
 
110
117
  # Pause/unpause container
111
- Docker::API::Container.pause("nginx")
112
- Docker::API::Container.unpause("nginx")
118
+ container.pause("nginx")
119
+ container.unpause("nginx")
113
120
 
114
121
  # List containers
115
- Docker::API::Container::list
122
+ container.list
116
123
 
117
124
  # List containers (including stopped ones)
118
- Docker::API::Container::list(all: true)
125
+ container.list(all: true)
119
126
 
120
127
  # Inspect container
121
- Docker::API::Container.inspect("nginx")
128
+ container.inspect("nginx")
122
129
 
123
130
  # View container's processes
124
- Docker::API::Container.top("nginx")
131
+ container.top("nginx")
125
132
 
126
- # Let's enhance the output
127
- JSON.parse(Docker::API::Container.top("nginx").body)
133
+ # Using json output
134
+ container.top("nginx").json
128
135
 
129
136
  # View filesystem changes
130
- Docker::API::Container.changes("nginx")
137
+ container.changes("nginx")
131
138
 
132
139
  # View filesystem logs
133
- Docker::API::Container.logs("nginx", stdout: true)
134
- Docker::API::Container.logs("nginx", stdout: true, follow: true)
140
+ container.logs("nginx", stdout: true)
141
+ container.logs("nginx", stdout: true, follow: true)
135
142
 
136
143
  # View filesystem stats
137
- Docker::API::Container.stats("nginx", stream: true)
144
+ container.stats("nginx", stream: true)
138
145
 
139
146
  # Export container
140
- Docker::API::Container.export("nginx", "~/exported_container")
147
+ container.export("nginx", "~/exported_container")
141
148
 
142
149
  # Get files from container
143
- Docker::API::Container.archive("nginx", "~/html.tar", path: "/usr/share/nginx/html/")
150
+ container.archive("nginx", "~/html.tar", path: "/usr/share/nginx/html/")
144
151
 
145
152
  # Stop container
146
- Docker::API::Container.stop("nginx")
153
+ container.stop("nginx")
147
154
 
148
155
  # Remove container
149
- Docker::API::Container.remove("nginx")
156
+ container.remove("nginx")
150
157
 
151
158
  # Remove stopped containers (prune)
152
- Docker::API::Container.prune
159
+ container.prune
160
+ ```
161
+
162
+ ### Volumes
163
+
164
+ ```ruby
165
+ # Connect to local volume endpoints
166
+ volume = Docker::API::Volume.new
167
+
168
+ # Create volume
169
+ volume.create( Name:"my-volume" )
170
+
171
+ # List volumes
172
+ volume.list
173
+
174
+ # Inspect volume
175
+ volume.inspect("my-volume")
176
+
177
+ # Remove volume
178
+ volume.remove("my-volume")
179
+
180
+ # Remove unused volumes (prune)
181
+ volume.prune
182
+ ```
183
+
184
+ ### Network
185
+
186
+ ```ruby
187
+ # Connect to local network endpoints
188
+ network = Docker::API::Network.new
189
+
190
+ # List networks
191
+ network.list
192
+
193
+ # Inspect network
194
+ network.inspect("bridge")
195
+
196
+ # Create network
197
+ network.create( Name:"my-network" )
198
+
199
+ # Remove network
200
+ network.remove("my-network")
201
+
202
+ # Remove unused network (prune)
203
+ network.prune
204
+
205
+ # Connect container to a network
206
+ network.connect( "my-network", Container: "my-container" )
207
+
208
+ # Disconnect container to a network
209
+ network.disconnect( "my-network", Container: "my-container" )
210
+ ```
211
+
212
+ ### System
213
+
214
+ ```ruby
215
+ # Connect to local system endpoints
216
+ sys = Docker::API::System.new
217
+
218
+ # Ping docker api
219
+ sys.ping
220
+
221
+ # Docker components versions
222
+ sys.version
223
+
224
+ # System info
225
+ sys.info
226
+
227
+ # System events (stream)
228
+ sys.events(until: Time.now.to_i)
229
+
230
+ # Data usage information
231
+ sys.df
232
+ ```
233
+
234
+ ### Exec
235
+
236
+ ```ruby
237
+ # Connect to local exec endpoints
238
+ exe = Docker::API::Exec.new
239
+
240
+ # Create exec instance, get generated exec ID
241
+ response = exe.create(container, AttachStdout:true, Cmd: ["ls", "-l"])
242
+ id = response.json["Id"]
243
+
244
+ # Execute the command, stream from Stdout is stored in response data
245
+ response = exe.start(id)
246
+ print response.data[:stream]
247
+
248
+ # Inspect exec instance
249
+ exe.inspect(id)
250
+ ```
251
+
252
+ ### Connection
253
+
254
+ By default Docker::API::Connection will connect to local Docker socket at `/var/run/docker.sock`. See examples below to use a different path or connect to a remote address.
255
+
256
+ ```ruby
257
+ # Setting different connections
258
+ local = Docker::API::Connection.new('unix:///', socket: "/path/to/docker.sock")
259
+ remote = Docker::API::Connection.new("http://127.0.0.1:2375") # change the IP address accordingly
260
+
261
+ # Using default /var/run/docker.sock
262
+ image_default = Docker::API::Image.new
263
+ image_default.list
264
+
265
+ # Using custom socket path
266
+ image_custom = Docker::API::Image.new(local)
267
+ image_custom.list
268
+
269
+ # Using remote address
270
+ image_remote = Docker::API::Image.new(remote)
271
+ image_remote.list
153
272
  ```
154
273
 
155
274
  ### Requests
@@ -158,7 +277,39 @@ Requests should work as described in [Docker API documentation](https://docs.doc
158
277
 
159
278
  ### Response
160
279
 
161
- All requests return a Excon::Response object.
280
+ All requests return a response class that inherits from Excon::Response. Available attribute readers and methods include: `status`, `data`, `body`, `headers`, `json`, `path`, `success?`.
281
+
282
+ ```ruby
283
+ response = Docker::API::Image.new.create(fromImage: "busybox:latest")
284
+
285
+ response
286
+ => #<Docker::API::Response:0x000055bb390b35c0 ... >
287
+
288
+ response.status
289
+ => 200
290
+
291
+ response.data
292
+ => {:body=>"...", :cookies=>[], :host=>nil, :headers=>{ ... }, :path=>"/images/create?fromImage=busybox:latest", :port=>nil, :status=>200, :status_line=>"HTTP/1.1 200 OK\r\n", :reason_phrase=>"OK"}
293
+
294
+ response.headers
295
+ => {"Api-Version"=>"1.40", "Content-Type"=>"application/json", "Docker-Experimental"=>"false", "Ostype"=>"linux", "Server"=>"Docker/19.03.11 (linux)", "Date"=>"Mon, 29 Jun 2020 16:10:06 GMT"}
296
+
297
+ response.body
298
+ => "{\"status\":\"Pulling from library/busybox\" ... "
299
+
300
+ response.json
301
+ => [{:status=>"Pulling from library/busybox", :id=>"latest"}, {:status=>"Pulling fs layer", :progressDetail=>{}, :id=>"76df9210b28c"}, ... , {:status=>"Status: Downloaded newer image for busybox:latest"}]
302
+
303
+ response.path
304
+ => "/images/create?fromImage=busybox:latest"
305
+
306
+ response.success?
307
+ => true
308
+ ```
309
+
310
+ ### Error handling
311
+
312
+ `Docker::API::InvalidParameter` and `Docker::API::InvalidRequestBody` may be raised when an invalid option is passed as argument (ie: an option not described in Docker API documentation for request query parameters nor request body (json) parameters). Even if no errors were raised, consider validating the status code and/or message of the response to check if the Docker daemon has fulfilled the operation properly.
162
313
 
163
314
  ## Development
164
315
 
@@ -177,10 +328,10 @@ WIP: Work In Progress
177
328
  |---|---|---|---|
178
329
  | Container | Ok | Ok | NS |
179
330
  | Image | Ok | Ok | NS |
180
- | Volume | NS | NS | NS |
181
- | Network | NS | NS | NS |
182
- | System | NS | NS | NS |
183
- | Exec | NS | NS | NS |
331
+ | Volume | Ok | Ok | NS |
332
+ | Network | Ok | Ok | NS |
333
+ | System | Ok | Ok | NS |
334
+ | Exec | Ok | Ok | NS |
184
335
  | Swarm | NS | NS | NS |
185
336
  | Node | NS | NS | NS |
186
337
  | Service | NS | NS | NS |
@@ -188,8 +339,8 @@ WIP: Work In Progress
188
339
  | Secret | NS | NS | NS |
189
340
 
190
341
  Misc:
191
- * Improve response object
192
- * Improve error objects
342
+ * ~~Improve response object~~
343
+ * ~~Improve error objects~~
193
344
 
194
345
  ## Contributing
195
346
 
data/bin/setup CHANGED
@@ -1,8 +1,22 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
  IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
4
+ #set -vx
7
5
 
8
6
  # Do any other automated setup that you need to do here
7
+ if [ $USER == 'root' ]
8
+ then
9
+ echo "Enabling TCP port 2375 for external connection to Docker"
10
+ echo '{"hosts": ["tcp://0.0.0.0:2375", "unix:///var/run/docker.sock"]}' > /etc/docker/daemon.json
11
+ mkdir -p /etc/systemd/system/docker.service.d
12
+ echo '[Service]' > /etc/systemd/system/docker.service.d/override.conf
13
+ echo 'ExecStart=' >> /etc/systemd/system/docker.service.d/override.conf
14
+ echo 'ExecStart=/usr/bin/dockerd' >> /etc/systemd/system/docker.service.d/override.conf
15
+ systemctl daemon-reload
16
+ systemctl restart docker.service
17
+ echo "Done!"
18
+ else
19
+ echo "Running bundle install"
20
+ bundle install
21
+ echo "Run this script as root for further configurations"
22
+ fi
@@ -1,32 +1,32 @@
1
1
  module Docker
2
2
  module API
3
3
  class Base
4
+
5
+ def initialize connection = nil
6
+ @connection = connection || Docker::API::Connection.new
7
+ end
4
8
 
5
9
  private
6
10
 
7
- def self.base_path
11
+ def base_path
8
12
  "/"
9
13
  end
10
14
 
11
- def self.connection
12
- Docker::API::Connection.instance
13
- end
14
-
15
- def self.validate error, permitted_keys, params
16
- not_permitted = params.keys.map(&:to_s) - permitted_keys.map(&:to_s)
17
- raise error if not_permitted.size > 0
15
+ def validate error, permitted, params
16
+ unpermitted = params.keys.map(&:to_s) - permitted.map(&:to_s)
17
+ raise error.new(permitted, unpermitted) if unpermitted.size > 0
18
18
  end
19
19
 
20
20
  ## Converts Ruby Hash into query parameters
21
21
  ## In general, the format is key=value
22
22
  ## If value is another Hash, it should keep a json syntax {key:value}
23
- def self.hash_to_params h
23
+ def hash_to_params h
24
24
  p = []
25
25
  h.each { |k,v| p.push( v.is_a?(Hash) ? "#{k}=#{v.to_json}" : "#{k}=#{v}") }
26
26
  p.join("&").gsub(" ","")
27
27
  end
28
28
 
29
- def self.build_path path, params = {}
29
+ def build_path path, params = {}
30
30
  p = path.is_a?(Array) ? ([base_path] << path).join("/") : path
31
31
  params.size > 0 ? [p, hash_to_params(params)].join("?") : p
32
32
  end
@@ -1,26 +1,18 @@
1
- require "excon"
2
- require "singleton"
3
- require "json"
4
1
  module Docker
5
2
  module API
6
3
  class Connection
7
- include Singleton
8
-
9
4
  [:get, :post, :head, :delete, :put].each do | method |
10
5
  define_method(method) { | path | self.request(method: method, path: path) }
11
6
  end
12
7
 
13
- def post(path, body = nil)
14
- self.request(method: :post, path: path, headers: { "Content-Type" => "application/json" }, body: body.to_json)
15
- end
16
-
17
8
  def request params
18
- @connection.request(params)
9
+ Docker::API::Response.new(@connection.request(params).data)
19
10
  end
20
11
 
21
- private
22
- def initialize
23
- @connection = Excon.new('unix:///', :socket => '/var/run/docker.sock')
12
+ def initialize url = nil, params = nil
13
+ url ||= 'unix:///'
14
+ params ||= url == 'unix:///' ? {socket: '/var/run/docker.sock'} : {}
15
+ @connection = Excon.new(url, params)
24
16
  end
25
17
 
26
18
  end
@@ -5,111 +5,111 @@ module Docker
5
5
 
6
6
  class Container < Docker::API::Base
7
7
 
8
- def self.base_path
8
+ def base_path
9
9
  "/containers"
10
10
  end
11
11
 
12
- def self.list params = {}
12
+ def list params = {}
13
13
  validate Docker::API::InvalidParameter, [:all, :limit, :size, :filters], params
14
- connection.get(build_path(["json"], params))
14
+ @connection.get(build_path(["json"], params))
15
15
  end
16
16
 
17
- def self.inspect name, params = {}
17
+ def inspect name, params = {}
18
18
  validate Docker::API::InvalidParameter, [:size], params
19
- connection.get(build_path([name, "json"], params))
19
+ @connection.get(build_path([name, "json"], params))
20
20
  end
21
21
 
22
- def self.top name, params = {}
22
+ def top name, params = {}
23
23
  validate Docker::API::InvalidParameter, [:ps_args], params
24
- connection.get(build_path([name, "top"], params))
24
+ @connection.get(build_path([name, "top"], params))
25
25
  end
26
26
 
27
- def self.changes name
28
- connection.get(build_path([name, "changes"]))
27
+ def changes name
28
+ @connection.get(build_path([name, "changes"]))
29
29
  end
30
30
 
31
- def self.start name, params = {}
31
+ def start name, params = {}
32
32
  validate Docker::API::InvalidParameter, [:detachKeys], params
33
- connection.post(build_path([name, "start"], params))
33
+ @connection.post(build_path([name, "start"], params))
34
34
  end
35
35
 
36
- def self.stop name, params = {}
36
+ def stop name, params = {}
37
37
  validate Docker::API::InvalidParameter, [:t], params
38
- connection.post(build_path([name, "stop"], params))
38
+ @connection.post(build_path([name, "stop"], params))
39
39
  end
40
40
 
41
- def self.restart name, params = {}
41
+ def restart name, params = {}
42
42
  validate Docker::API::InvalidParameter, [:t], params
43
- connection.post(build_path([name, "restart"], params))
43
+ @connection.post(build_path([name, "restart"], params))
44
44
  end
45
45
 
46
- def self.kill name, params = {}
46
+ def kill name, params = {}
47
47
  validate Docker::API::InvalidParameter, [:signal], params
48
- connection.post(build_path([name, "kill"], params))
48
+ @connection.post(build_path([name, "kill"], params))
49
49
  end
50
50
 
51
- def self.wait name, params = {}
51
+ def wait name, params = {}
52
52
  validate Docker::API::InvalidParameter, [:condition], params
53
- connection.post(build_path([name, "wait"], params))
53
+ @connection.post(build_path([name, "wait"], params))
54
54
  end
55
55
 
56
- def self.update name, body = {}
56
+ def update name, body = {}
57
57
  validate Docker::API::InvalidRequestBody, Docker::API::UpdateBody, body
58
- connection.post(build_path([name, "update"]), body)
58
+ @connection.request(method: :post, path: build_path([name, "update"]), headers: {"Content-Type": "application/json"}, body: body.to_json)
59
59
  end
60
60
 
61
- def self.rename name, params = {}
61
+ def rename name, params = {}
62
62
  validate Docker::API::InvalidParameter, [:name], params
63
- connection.post(build_path([name, "rename"], params))
63
+ @connection.post(build_path([name, "rename"], params))
64
64
  end
65
65
 
66
- def self.resize name, params = {}
66
+ def resize name, params = {}
67
67
  validate Docker::API::InvalidParameter, [:w, :h], params
68
- connection.post(build_path([name, "resize"], params))
68
+ @connection.post(build_path([name, "resize"], params))
69
69
  end
70
70
 
71
- def self.prune params = {}
71
+ def prune params = {}
72
72
  validate Docker::API::InvalidParameter, [:filters], params
73
- connection.post(build_path(["prune"], params))
73
+ @connection.post(build_path(["prune"], params))
74
74
  end
75
75
 
76
- def self.pause name
77
- connection.post(build_path([name, "pause"]))
76
+ def pause name
77
+ @connection.post(build_path([name, "pause"]))
78
78
  end
79
79
 
80
- def self.unpause name
81
- connection.post(build_path([name, "unpause"]))
80
+ def unpause name
81
+ @connection.post(build_path([name, "unpause"]))
82
82
  end
83
83
 
84
- def self.remove name, params = {}
84
+ def remove name, params = {}
85
85
  validate Docker::API::InvalidParameter, [:v, :force, :link], params
86
- connection.delete(build_path([name]))
86
+ @connection.delete(build_path([name]))
87
87
  end
88
88
 
89
- def self.logs name, params = {}
89
+ def logs name, params = {}
90
90
  validate Docker::API::InvalidParameter, [:follow, :stdout, :stderr, :since, :until, :timestamps, :tail], params
91
91
 
92
92
  path = build_path([name, "logs"], params)
93
93
 
94
94
  if params[:follow] == true || params[:follow] == 1
95
- connection.request(method: :get, path: path , response_block: lambda { |chunk, remaining_bytes, total_bytes| puts chunk.inspect })
95
+ @connection.request(method: :get, path: path , response_block: lambda { |chunk, remaining_bytes, total_bytes| puts chunk.inspect })
96
96
  else
97
- connection.get(path)
97
+ @connection.get(path)
98
98
  end
99
99
  end
100
100
 
101
- def self.attach name, params = {}
101
+ def attach name, params = {}
102
102
  validate Docker::API::InvalidParameter, [:detachKeys, :logs, :stream, :stdin, :stdout, :stderr], params
103
- connection.request(method: :post, path: build_path([name, "attach"], params) , response_block: lambda { |chunk, remaining_bytes, total_bytes| puts chunk.inspect })
103
+ @connection.request(method: :post, path: build_path([name, "attach"], params) , response_block: lambda { |chunk, remaining_bytes, total_bytes| puts chunk.inspect })
104
104
  end
105
105
 
106
- def self.create params = {}, body = {}
106
+ def create params = {}, body = {}
107
107
  validate Docker::API::InvalidParameter, [:name], params
108
108
  validate Docker::API::InvalidRequestBody, Docker::API::CreateBody, body
109
- connection.post(build_path(["create"], params), body)
109
+ @connection.request(method: :post, path: build_path(["create"], params), headers: {"Content-Type": "application/json"}, body: body.to_json)
110
110
  end
111
111
 
112
- def self.stats name, params = {}
112
+ def stats name, params = {}
113
113
  validate Docker::API::InvalidParameter, [:stream], params
114
114
  path = build_path([name, "stats"], params)
115
115
 
@@ -117,37 +117,37 @@ module Docker
117
117
  streamer = lambda do |chunk, remaining_bytes, total_bytes|
118
118
  puts chunk
119
119
  end
120
- connection.request(method: :get, path: path , response_block: streamer)
120
+ @connection.request(method: :get, path: path , response_block: streamer)
121
121
  else
122
- connection.get(path)
122
+ @connection.get(path)
123
123
  end
124
124
  end
125
125
 
126
- def self.export name, path = "exported_container"
127
- response = Docker::API::Container.inspect(name)
126
+ def export name, path = "exported_container"
127
+ response = self.inspect(name)
128
128
  if response.status == 200
129
129
  file = File.open(File.expand_path(path), "wb")
130
130
  streamer = lambda do |chunk, remaining_bytes, total_bytes|
131
131
  file.write(chunk)
132
132
  end
133
- response = connection.request(method: :get, path: build_path([name, "export"]) , response_block: streamer)
133
+ response = @connection.request(method: :get, path: build_path([name, "export"]) , response_block: streamer)
134
134
  file.close
135
135
  end
136
136
  response
137
137
  end
138
138
 
139
- def self.archive name, file, params = {}
139
+ def archive name, file, params = {}
140
140
  validate Docker::API::InvalidParameter, [:path, :noOverwriteDirNonDir, :copyUIDGID], params
141
141
 
142
142
  begin # File exists on disk, send it to container
143
143
  file = File.open( File.expand_path( file ) , "r")
144
- response = connection.request(method: :put, path: build_path([name, "archive"], params) , request_block: lambda { file.read(Excon.defaults[:chunk_size]).to_s} )
144
+ response = @connection.request(method: :put, path: build_path([name, "archive"], params) , request_block: lambda { file.read(Excon.defaults[:chunk_size]).to_s} )
145
145
  file.close
146
146
  rescue Errno::ENOENT # File doesnt exist, get it from container
147
- response = connection.head(build_path([name, "archive"], params))
147
+ response = @connection.head(build_path([name, "archive"], params))
148
148
  if response.status == 200 # file exists in container
149
149
  file = File.open( File.expand_path( file ) , "wb")
150
- response = connection.request(method: :get, path: build_path([name, "archive"], params) , response_block: lambda { |chunk, remaining_bytes, total_bytes| file.write(chunk) })
150
+ response = @connection.request(method: :get, path: build_path([name, "archive"], params) , response_block: lambda { |chunk, remaining_bytes, total_bytes| file.write(chunk) })
151
151
  file.close
152
152
  end
153
153
  end
@@ -1,6 +1,17 @@
1
1
  module Docker
2
2
  module API
3
- class InvalidParameter < StandardError; end
4
- class InvalidRequestBody < StandardError; end
3
+ class ValidationError < StandardError
4
+ def initialize permitted, unpermitted
5
+ super("Unpermitted options found: #{unpermitted.to_s}. Permitted are #{permitted.to_s}")
6
+ end
7
+ end
8
+ class Error < StandardError
9
+ def initialize msg = "Error without specific message"
10
+ super(msg)
11
+ end
12
+ end
13
+
14
+ class InvalidParameter < Docker::API::ValidationError; end
15
+ class InvalidRequestBody < Docker::API::ValidationError; end
5
16
  end
6
17
  end
@@ -0,0 +1,39 @@
1
+ module Docker
2
+ module API
3
+ class Exec < Docker::API::Base
4
+
5
+ def base_path
6
+ "/exec"
7
+ end
8
+
9
+ def create name, body = {}
10
+ validate Docker::API::InvalidRequestBody, [:AttachStdin, :AttachStdout, :AttachStderr, :DetachKeys, :Tty, :Env, :Cmd, :Privileged, :User, :WorkingDir], body
11
+ @connection.request(method: :post, path: "/containers/#{name}/exec", headers: {"Content-Type": "application/json"}, body: body.to_json )
12
+ end
13
+
14
+ def start name, body = {}
15
+ validate Docker::API::InvalidRequestBody, [:Detach, :Tty], body
16
+
17
+ stream = ""
18
+ response = @connection.request(method: :post,
19
+ path: "/exec/#{name}/start",
20
+ headers: {"Content-Type": "application/json"},
21
+ body: body.to_json,
22
+ response_block: lambda { |chunk, remaining_bytes, total_bytes| stream += chunk.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') }
23
+ )
24
+ response.data.merge!({stream: stream})
25
+ response
26
+ end
27
+
28
+ def resize name, params = {}
29
+ validate Docker::API::InvalidParameter, [:w, :h], params
30
+ @connection.post(build_path([name, "resize"], params))
31
+ end
32
+
33
+ def inspect name
34
+ @connection.get(build_path([name, "json"]))
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -7,113 +7,111 @@ module Docker
7
7
  BuildParams = [:dockerfile, :t, :extrahosts, :remote, :q, :nocache, :cachefrom, :pull, :rm, :forcerm, :memory, :memswap, :cpushares, :cpusetcpus, :cpuperiod, :cpuquota, :buildargs, :shmsize, :squash, :labels, :networkmode, :platform, :target, :outputs]
8
8
  class Image < Docker::API::Base
9
9
 
10
- def self.base_path
10
+ def base_path
11
11
  "/images"
12
12
  end
13
13
 
14
- def self.inspect name
15
- connection.get(build_path([name, "json"]))
14
+ def inspect name
15
+ @connection.get(build_path([name, "json"]))
16
16
  end
17
17
 
18
- def self.history name
19
- connection.get(build_path([name, "history"]))
18
+ def history name
19
+ @connection.get(build_path([name, "history"]))
20
20
  end
21
21
 
22
- def self.list params = {}
22
+ def list params = {}
23
23
  validate Docker::API::InvalidParameter, [:all, :filters, :digests], params
24
- connection.get(build_path(["json"], params))
24
+ @connection.get(build_path(["json"], params))
25
25
  end
26
26
 
27
- def self.search params = {}
27
+ def search params = {}
28
28
  validate Docker::API::InvalidParameter, [:term, :limit, :filters], params
29
- connection.get(build_path(["search"], params))
29
+ @connection.get(build_path(["search"], params))
30
30
  end
31
31
 
32
- def self.tag name, params = {}
32
+ def tag name, params = {}
33
33
  validate Docker::API::InvalidParameter, [:repo, :tag], params
34
- connection.post(build_path([name, "tag"], params))
34
+ @connection.post(build_path([name, "tag"], params))
35
35
  end
36
36
 
37
- def self.prune params = {}
37
+ def prune params = {}
38
38
  validate Docker::API::InvalidParameter, [:filters], params
39
- connection.post(build_path(["prune"], params))
39
+ @connection.post(build_path(["prune"], params))
40
40
  end
41
41
 
42
- def self.remove name, params = {}
42
+ def remove name, params = {}
43
43
  validate Docker::API::InvalidParameter, [:force, :noprune], params
44
- connection.delete(build_path([name], params))
44
+ @connection.delete(build_path([name], params))
45
45
  end
46
46
 
47
- def self.export name, path = "exported_image"
47
+ def export name, path = "exported_image"
48
48
  file = File.open("/tmp/exported-image", "wb")
49
49
  streamer = lambda do |chunk, remaining_bytes, total_bytes|
50
50
  file.write(chunk)
51
51
  end
52
- response = connection.request(method: :get, path: build_path([name, "get"]) , response_block: streamer)
52
+ response = @connection.request(method: :get, path: build_path([name, "get"]) , response_block: streamer)
53
53
  file.close
54
54
  response.status == 200 ? FileUtils.mv("/tmp/exported-image", File.expand_path(path)) : FileUtils.rm("/tmp/exported-image")
55
55
  response
56
56
  end
57
57
 
58
- def self.import path, params = {}
58
+ def import path, params = {}
59
59
  validate Docker::API::InvalidParameter, [:quiet], params
60
60
  file = File.open(File.expand_path(path), "r")
61
- response = connection.request(method: :post, path: build_path(["load"], params) , headers: {"Content-Type" => "application/x-tar"}, request_block: lambda { file.read(Excon.defaults[:chunk_size]).to_s} )
61
+ response = @connection.request(method: :post, path: build_path(["load"], params) , headers: {"Content-Type" => "application/x-tar"}, request_block: lambda { file.read(Excon.defaults[:chunk_size]).to_s} )
62
62
  file.close
63
63
  response
64
64
  end
65
65
 
66
- def self.push name, params = {}, authentication = {}
66
+ def push name, params = {}, authentication = {}
67
67
  validate Docker::API::InvalidParameter, [:tag], params
68
68
 
69
69
  if authentication.keys.size > 0
70
- auth = Docker::API::System.auth(authentication)
71
- return auth unless [200, 204].include? auth.status
72
- connection.request(method: :post, path: build_path([name, "push"], params), headers: { "X-Registry-Auth" => Base64.encode64(authentication.to_json.to_s).chomp } )
70
+ @connection.request(method: :post, path: build_path([name, "push"], params), headers: { "X-Registry-Auth" => Base64.urlsafe_encode64(authentication.to_json.to_s).chomp } )
73
71
  else
74
- connection.post(build_path([name, "push"], params))
72
+ raise Docker::API::Error.new("Provide authentication parameters to push an image")
75
73
  end
76
74
  end
77
75
 
78
- def self.commit params = {}, body = {}
76
+ def commit params = {}, body = {}
79
77
  validate Docker::API::InvalidParameter, [:container, :repo, :tag, :comment, :author, :pause, :changes], params
80
78
  validate Docker::API::InvalidRequestBody, Docker::API::CommitBody, body
81
- container = Docker::API::Container.inspect(params[:container])
79
+ container = Docker::API::Container.new.inspect(params[:container])
82
80
  return container if [404, 301].include? container.status
83
81
  body = JSON.parse(container.body)["Config"].merge(body)
84
- connection.request(method: :post, path: build_path("/commit", params), headers: {"Content-Type": "application/json"}, body: body.to_json)
82
+ @connection.request(method: :post, path: build_path("/commit", params), headers: {"Content-Type": "application/json"}, body: body.to_json)
85
83
  end
86
84
 
87
- def self.create params = {}, authentication = {}
85
+ def create params = {}, authentication = {}
88
86
  validate Docker::API::InvalidParameter, [:fromImage, :fromSrc, :repo, :tag, :message, :platform], params
89
87
 
90
88
  if authentication.keys.size > 0
91
- auth = Docker::API::System.auth(authentication)
89
+ auth = Docker::API::System.new.auth(authentication)
92
90
  return auth unless [200, 204].include? auth.status
93
- connection.request(method: :post, path: build_path(["create"], params), headers: { "X-Registry-Auth" => Base64.encode64(authentication.to_json.to_s).chomp } )
91
+ @connection.request(method: :post, path: build_path(["create"], params), headers: { "X-Registry-Auth" => Base64.encode64(authentication.to_json.to_s).chomp } )
94
92
  elsif params.has_key? :fromSrc
95
93
  if params[:fromSrc].match(/^(http|https)/)
96
- connection.request(method: :post, path: build_path(["create"], params))
94
+ @connection.request(method: :post, path: build_path(["create"], params))
97
95
  else
98
96
  file = File.open(File.expand_path(params[:fromSrc]), "r")
99
97
  params[:fromSrc] = "-"
100
- response = connection.request(method: :post, path: build_path(["create"], params) , headers: {"Content-Type" => "application/x-tar"}, request_block: lambda { file.read(Excon.defaults[:chunk_size]).to_s} )
98
+ response = @connection.request(method: :post, path: build_path(["create"], params) , headers: {"Content-Type" => "application/x-tar"}, request_block: lambda { file.read(Excon.defaults[:chunk_size]).to_s} )
101
99
  file.close
102
100
  response
103
101
  end
104
102
  else
105
- connection.post(build_path(["create"], params))
103
+ @connection.post(build_path(["create"], params))
106
104
  end
107
105
  end
108
106
 
109
- def self.build path, params = {}, authentication = {}
110
- raise Docker::API::InvalidRequestBody unless path || params[:remote]
107
+ def build path, params = {}, authentication = {}
108
+ raise Docker::API::Error.new("Expected path or params[:remote]") unless path || params[:remote]
111
109
  validate Docker::API::InvalidParameter, Docker::API::BuildParams, params
112
110
 
113
111
  header = {"Content-type": "application/x-tar"}
114
112
  if authentication.keys.size > 0
115
113
  authentication.each_key do |server|
116
- auth = Docker::API::System.auth({username: authentication[server][:username] ,password:authentication[server][:password], serveraddress: server})
114
+ auth = Docker::API::System.new.auth({username: authentication[server][:username] ,password:authentication[server][:password], serveraddress: server})
117
115
  return auth unless [200, 204].include? auth.status
118
116
  end
119
117
  header.merge!({"X-Registry-Config": Base64.urlsafe_encode64(authentication.to_json.to_s).chomp})
@@ -121,17 +119,17 @@ module Docker
121
119
 
122
120
  begin
123
121
  file = File.open( File.expand_path( path ) , "r")
124
- response = connection.request(method: :post, path: build_path("/build", params), headers: header, request_block: lambda { file.read(Excon.defaults[:chunk_size]).to_s})
122
+ response = @connection.request(method: :post, path: build_path("/build", params), headers: header, request_block: lambda { file.read(Excon.defaults[:chunk_size]).to_s})
125
123
  file.close
126
124
  rescue
127
- response = connection.request(method: :post, path: build_path("/build", params), headers: header)
125
+ response = @connection.request(method: :post, path: build_path("/build", params), headers: header)
128
126
  end
129
127
  response
130
128
  end
131
129
 
132
- def self.delete_cache params = {}
130
+ def delete_cache params = {}
133
131
  validate Docker::API::InvalidParameter, [:all, "keep-storage", :filters], params
134
- connection.post(build_path("/build/prune", params))
132
+ @connection.post(build_path("/build/prune", params))
135
133
  end
136
134
 
137
135
  end
@@ -0,0 +1,45 @@
1
+ module Docker
2
+ module API
3
+ class Network < Docker::API::Base
4
+
5
+ def base_path
6
+ "/networks"
7
+ end
8
+
9
+ def list params = {}
10
+ validate Docker::API::InvalidParameter, [:filters], params
11
+ @connection.get(build_path("/networks", params))
12
+ end
13
+
14
+ def inspect name, params = {}
15
+ validate Docker::API::InvalidParameter, [:verbose, :scope], params
16
+ @connection.get(build_path([name], params))
17
+ end
18
+
19
+ def create body = {}
20
+ validate Docker::API::InvalidRequestBody, [:Name, :CheckDuplicate, :Driver, :Internal, :Attachable, :Ingress, :IPAM, :EnableIPv6, :Options, :Labels], body
21
+ @connection.request(method: :post, path: build_path(["create"]), headers: {"Content-Type": "application/json"}, body: body.to_json)
22
+ end
23
+
24
+ def remove name
25
+ @connection.delete(build_path([name]))
26
+ end
27
+
28
+ def prune params = {}
29
+ validate Docker::API::InvalidParameter, [:filters], params
30
+ @connection.post(build_path(["prune"], params))
31
+ end
32
+
33
+ def connect name, body = {}
34
+ validate Docker::API::InvalidRequestBody, [:Container, :EndpointConfig], body
35
+ @connection.request(method: :post, path: build_path([name, "connect"]), headers: {"Content-Type": "application/json"}, body: body.to_json)
36
+ end
37
+
38
+ def disconnect name, body = {}
39
+ validate Docker::API::InvalidRequestBody, [:Container, :Force], body
40
+ @connection.request(method: :post, path: build_path([name, "disconnect"]), headers: {"Content-Type": "application/json"}, body: body.to_json)
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ module Docker
2
+ module API
3
+ class Response < Excon::Response
4
+ attr_reader(:json, :path)
5
+
6
+ def initialize data
7
+ super data
8
+ @json = parse_json @body
9
+ @path = @data[:path]
10
+ end
11
+
12
+ def success?
13
+ (200..204).include? @status
14
+ end
15
+
16
+ private
17
+
18
+ def parse_json data
19
+ return nil unless headers["Content-Type"] == "application/json"
20
+ return nil if data == ""
21
+ data.split("\r\n").size > 1 ? data.split("\r\n").map{ |e| eval(e) } : JSON.parse(data)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -2,11 +2,33 @@ require "json"
2
2
  module Docker
3
3
  module API
4
4
  class System < Docker::API::Base
5
-
6
- def self.auth body = {}
5
+
6
+ def auth body = {}
7
7
  validate Docker::API::InvalidRequestBody, [:username, :password, :email, :serveraddress, :identitytoken], body
8
- connection.request(method: :post, path: "/auth", headers: { "Content-Type" => "application/json" }, body: body.to_json)
8
+ @connection.request(method: :post, path: "/auth", headers: { "Content-Type" => "application/json" }, body: body.to_json)
9
9
  end
10
+
11
+ def events params = {}
12
+ validate Docker::API::InvalidParameter, [:since, :until, :filters], params
13
+ @connection.request(method: :get, path: build_path("/events", params), response_block: lambda { |chunk, remaining_bytes, total_bytes| puts chunk.inspect } )
14
+ end
15
+
16
+ def ping
17
+ @connection.get("/_ping")
18
+ end
19
+
20
+ def info
21
+ @connection.get("/info")
22
+ end
23
+
24
+ def version
25
+ @connection.get("/version")
26
+ end
27
+
28
+ def df
29
+ @connection.get("/system/df")
30
+ end
31
+
10
32
  end
11
33
  end
12
34
  end
@@ -1,6 +1,6 @@
1
1
  module Docker
2
2
  module API
3
- GEM_VERSION = "0.2.0"
3
+ GEM_VERSION = "0.7.0"
4
4
 
5
5
  API_VERSION = "1.40"
6
6
 
@@ -0,0 +1,34 @@
1
+ module Docker
2
+ module API
3
+ class Volume < Docker::API::Base
4
+ def base_path
5
+ "/volumes"
6
+ end
7
+
8
+ def list params = {}
9
+ validate Docker::API::InvalidParameter, [:filters], params
10
+ @connection.get(build_path("/volumes", params))
11
+ end
12
+
13
+ def create body = {}
14
+ validate Docker::API::InvalidRequestBody, [:Name, :Driver, :DriverOpts, :Labels], body
15
+ @connection.request(method: :post, path: build_path(["create"]), headers: {"Content-Type": "application/json"}, body: body.to_json)
16
+ end
17
+
18
+ def inspect name
19
+ @connection.get(build_path([name]))
20
+ end
21
+
22
+ def remove name, params = {}
23
+ validate Docker::API::InvalidParameter, [:force], params
24
+ @connection.delete(build_path([name]))
25
+ end
26
+
27
+ def prune params = {}
28
+ validate Docker::API::InvalidParameter, [:filters], params
29
+ @connection.post(build_path(["prune"], params))
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -1,10 +1,16 @@
1
- require "docker/api/version"
1
+ require "excon"
2
+ require "json"
2
3
 
4
+ require "docker/api/version"
3
5
  require "docker/api/error"
4
6
  require "docker/api/connection"
7
+ require "docker/api/response"
5
8
  require "docker/api/base"
6
9
  require "docker/api/container"
7
10
  require "docker/api/image"
11
+ require "docker/api/volume"
12
+ require "docker/api/network"
13
+ require "docker/api/exec"
8
14
  require "docker/api/system"
9
15
 
10
16
  module Docker
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dockerapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alysson A. Costa
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-25 00:00:00.000000000 Z
11
+ date: 2020-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: excon
@@ -47,9 +47,13 @@ files:
47
47
  - lib/docker/api/connection.rb
48
48
  - lib/docker/api/container.rb
49
49
  - lib/docker/api/error.rb
50
+ - lib/docker/api/exec.rb
50
51
  - lib/docker/api/image.rb
52
+ - lib/docker/api/network.rb
53
+ - lib/docker/api/response.rb
51
54
  - lib/docker/api/system.rb
52
55
  - lib/docker/api/version.rb
56
+ - lib/docker/api/volume.rb
53
57
  - lib/dockerapi.rb
54
58
  homepage: https://github.com/nu12/dockerapi
55
59
  licenses: