docker_registry2 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +138 -5
- data/lib/docker_registry2.rb +4 -0
- data/lib/registry/exceptions.rb +3 -0
- data/lib/registry/registry.rb +141 -79
- data/lib/registry/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6ea58879e9bc75f39640a3db5ace45d46291437
|
4
|
+
data.tar.gz: 92cc2a0bce7d5fa73a4a6c21da456ac23d25968e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be00444d06f0e8e742a28d0f40e4b97e1f62bbbf4803d16d09439215024720f4b145d5fdaf4394956e2490d3215bcf68c21e161e67360b44ebe112d647b26534
|
7
|
+
data.tar.gz: 5f3ba88b12ad4378c5b20fd554f87f81903b86ac8be4c5fb177e1d69ee09019acef37ea03089e9596c3cd27fb3c2403b24db615592297f2423bd809fe3a9748a
|
data/README.md
CHANGED
@@ -35,7 +35,7 @@ Once it is installed, you first *open* a connection to a registry, and then *req
|
|
35
35
|
To connect to a registry:
|
36
36
|
|
37
37
|
````ruby
|
38
|
-
reg = DockerRegistry.
|
38
|
+
reg = DockerRegistry.connect("https://my.registy.corp.com")
|
39
39
|
````
|
40
40
|
|
41
41
|
The above will connect anonymously to the registry via the endpoint `https://my.registry.corp.com/v2/`.
|
@@ -47,7 +47,7 @@ The following exceptions are thrown:
|
|
47
47
|
* `RegistrySSLException`: registry SSL certificate cannot be validated
|
48
48
|
|
49
49
|
#### Authenticated
|
50
|
-
If you wish to authenticate, pass a username and password as
|
50
|
+
If you wish to authenticate, pass a username and password as part of the URL.
|
51
51
|
|
52
52
|
````ruby
|
53
53
|
reg = DockerRegistry.connect("https://myuser:mypass@my.registy.corp.com")
|
@@ -62,7 +62,7 @@ The following exceptions are thrown:
|
|
62
62
|
|
63
63
|
|
64
64
|
### Requests
|
65
|
-
Once you have a valid `reg` object return by `DockerRegistry.
|
65
|
+
Once you have a valid `reg` object return by `DockerRegistry.connect()`, you can make requests. As of this version, only search and tags are supported. Others will be added over time.
|
66
66
|
|
67
67
|
|
68
68
|
#### search
|
@@ -87,30 +87,163 @@ The following exceptions are thrown:
|
|
87
87
|
|
88
88
|
#### tags
|
89
89
|
````ruby
|
90
|
-
results = reg.tags("mylibs")
|
90
|
+
results = reg.tags("mylibs",withHashes)
|
91
91
|
````
|
92
92
|
|
93
|
-
Returns all known tags for the repository precisely named `"mylibs"`.
|
93
|
+
Returns all known tags for the repository precisely named `"mylibs"`. If `withHashes` is present and set to `true`, also will return all of the hashes for each tag. See below. Note that retrieving the hashes is an expensive operations, as it requires a separate `HEAD` for each tag. This is why the default is `false`.
|
94
94
|
|
95
95
|
Returns an object with the following key value pairs:
|
96
96
|
array of objects, each of which has the following key/value pairs:
|
97
97
|
|
98
98
|
* `name`: full name of repository, e.g. `redis` or `user/redis`
|
99
99
|
* `tags`: array of strings, each of which is a tag for ths given repository
|
100
|
+
* `hashes`: object, keys of which are the tag name, and values of which are the hash. Only provided if `withHashes` is true.
|
100
101
|
|
101
102
|
Other fields may be added later. Do *not* assume those are the only fields.
|
102
103
|
|
103
104
|
If no tags are found, or the named repository does not exist, return an empty object `{}`. An unknown repository will not throw an exception.
|
104
105
|
|
106
|
+
The response structure looks something like this:
|
107
|
+
|
108
|
+
````ruby
|
109
|
+
{
|
110
|
+
"name" => "special/repo",
|
111
|
+
"tags" => ["1.0","1.1","1.3","latest"],
|
112
|
+
"hashes" => {
|
113
|
+
"1.0" => "abc4567",
|
114
|
+
"1.1" => "87def23",
|
115
|
+
"1.3" => "998adf2",
|
116
|
+
"latest" => "998adf2"
|
117
|
+
}
|
118
|
+
}
|
119
|
+
````
|
120
|
+
|
121
|
+
It is important to note that the hashes **may** or **may not** match the hashes that you receive when running `docker images` on your machine. These are the hashes returned by the `Docker-Content-Digest` for the manifest. See [v2 API Spec](https://docs.docker.com/registry/spec/api/#get-manifest).
|
122
|
+
|
123
|
+
These **may** or **may not** be useful for comparing to the local image on disk when running `docker images`. These **are** useful for comparing 2 different tags or images in one or more registries.
|
124
|
+
|
125
|
+
The following exceptions are thrown:
|
126
|
+
|
127
|
+
* `RegistryAuthenticationException`: username and password are invalid
|
128
|
+
* `RegistryAuthorizationException`: registry does not support tags using the given credentials, probably because the repository is private and the credentials provided do not have access
|
129
|
+
|
130
|
+
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
#### manifest
|
135
|
+
````ruby
|
136
|
+
manifest = reg.manifest("namespace/repo","2.5.6")
|
137
|
+
````
|
138
|
+
|
139
|
+
Returns the manifest for the given tag of the given repository. For the format and syntax of the manifest, see the [registry API](https://github.com/docker/distribution/blob/master/docs/spec/api.md) and the [manifest issue](https://github.com/docker/docker/issues/8093).
|
140
|
+
|
141
|
+
|
142
|
+
If the given repository and/or tag is not found, return an empty object `{}`.
|
143
|
+
|
105
144
|
The following exceptions are thrown:
|
106
145
|
|
107
146
|
* `RegistryAuthenticationException`: username and password are invalid
|
108
147
|
* `RegistryAuthorizationException`: registry does not support tags using the given credentials, probably because the repository is private and the credentials provided do not have access
|
109
148
|
|
149
|
+
#### pull
|
150
|
+
````ruby
|
151
|
+
reg.pull("namespace/repo","2.5.6",dir)
|
152
|
+
````
|
153
|
+
|
154
|
+
Pulls the given tag of the given repository to the given `dir`. If given `dir`does not exist, will create it.
|
155
|
+
|
156
|
+
It is important to note that the image for a tag is not likely to be a single file, but rather multiple layers, each of which is an individual file. Thus, it is necessary to have a directory where the downloaded layers are to be kept. The actual docker engine stores these under `/var/lib/docker/`.
|
157
|
+
|
158
|
+
If the given repository and/or tag is not found, return an empty object `{}`.
|
159
|
+
|
160
|
+
The following exceptions are thrown:
|
161
|
+
|
162
|
+
* `RegistryAuthenticationException`: username and password are invalid
|
163
|
+
* `RegistryAuthorizationException`: registry does not support tags or pull using the given credentials, probably because the repository is private and the credentials provided do not have access.
|
164
|
+
|
165
|
+
#### push
|
166
|
+
````ruby
|
167
|
+
reg.push(manifest,dir)
|
168
|
+
````
|
169
|
+
|
170
|
+
Pushes the given manifest to the registry, using images in the given `dir`. You are assumed to have created the manifest or gotten it from somewhere else. This is especially useful for moving an image from one registry to another. You can get the manifest, pull the layers, and push the manifest to a different registry.
|
171
|
+
|
172
|
+
The following exceptions are thrown:
|
173
|
+
|
174
|
+
* `RegistryAuthenticationException`: username and password are invalid
|
175
|
+
* `RegistryAuthorizationException`: registry does not support pushing the layers or uploading the manifest using the given credentials, probably because the repository is private and the credentials provided do not have access
|
176
|
+
* `MissingLayerException`: A layer from the manifest is missing from the given `dir` and thus cannot be pushed.
|
177
|
+
|
178
|
+
#### tag
|
179
|
+
````ruby
|
180
|
+
reg.tag("namespace/repo","tag",newrepo,newtag)
|
181
|
+
````
|
182
|
+
|
183
|
+
`tag` is a convenience method to create a new repository for a given repository in the same registry. For example, you have a registry at `https://my.registry.local`. In the registry is a repository named "myspace/repo", with a tag "1.2". You wish to duplicate "myspace/repo:1.2" to "myspace/repo:latest". Or, perhaps you wish to duplicate "myspace/repo:1.2" to "others/image:10.5". You can do it using `tag`:
|
184
|
+
|
185
|
+
````ruby
|
186
|
+
reg.tag("myspace/repo","1.2","myspace/repo","latest")
|
187
|
+
reg.tag("myspace/repo","1.2","other/image","10.5")
|
188
|
+
````
|
189
|
+
|
190
|
+
This is a convenience and efficiency method. Because it only manipulates the manifest, and not the layers (which already are present in the registry), it can do so quickly.
|
191
|
+
|
192
|
+
The following exceptions are thrown:
|
193
|
+
|
194
|
+
* `RegistryAuthenticationException`: username and password are invalid
|
195
|
+
* `RegistryAuthorizationException`: registry does not support pushing the layers or uploading the manifest using the given credentials, probably because the repository is private and the credentials provided do not have access
|
196
|
+
|
197
|
+
#### copy
|
198
|
+
````ruby
|
199
|
+
reg.copy("namespace/repo","tag",newregistry,newrepo,newtag)
|
200
|
+
````
|
201
|
+
|
202
|
+
`copy` copies an image from one registry to another. It does so in the following manner:
|
203
|
+
|
204
|
+
1. Download the manifest
|
205
|
+
2. Download all relevant layers
|
206
|
+
3. Modify the manifest as needed to reflect the `newrepo` and `newtag`
|
207
|
+
4. Upload the layers to `newregistry`
|
208
|
+
5. Upload the manifest to `newregistry`
|
209
|
+
|
210
|
+
The following exceptions are thrown:
|
211
|
+
|
212
|
+
* `RegistryAuthenticationException`: username and password are invalid
|
213
|
+
* `RegistryAuthorizationException`: registry does not support pushing the layers or uploading the manifest using the given credentials, probably because the repository is private and the credentials provided do not have access
|
214
|
+
|
215
|
+
#### rmtag
|
216
|
+
````ruby
|
217
|
+
reg.rmtag("namespace/repo","tag")
|
218
|
+
````
|
219
|
+
|
220
|
+
`rmtag` removes a given tag from a repository.
|
221
|
+
|
222
|
+
The following exceptions are thrown:
|
223
|
+
|
224
|
+
* `RegistryAuthenticationException`: username and password are invalid
|
225
|
+
* `RegistryAuthorizationException`: registry does not support your deleting the given tag, probably because you do not have sufficient access rights.
|
226
|
+
|
227
|
+
#### rmrepo
|
228
|
+
````ruby
|
229
|
+
reg.rmrepo("namespace/repo")
|
230
|
+
````
|
231
|
+
|
232
|
+
`rmrepo` removes the named repository entirely.
|
233
|
+
|
234
|
+
The following exceptions are thrown:
|
235
|
+
|
236
|
+
* `RegistryAuthenticationException`: username and password are invalid
|
237
|
+
* `RegistryAuthorizationException`: registry does not support your deleting the given repository, probably because you do not have sufficient access rights.
|
238
|
+
|
239
|
+
|
110
240
|
### Exceptions
|
111
241
|
|
112
242
|
All exceptions thrown inherit from `DockerRegistry::Exception`.
|
113
243
|
|
244
|
+
## Tests
|
245
|
+
The simplest way to test is against a true v2 registry. Thus, the test setup and teardown work against a docker registry. That means that to test, you need a docker engine running. The tests will start up a registry (actually, two registries, to be able to test `copy()`), initialize the data and test against them.
|
246
|
+
|
114
247
|
## License
|
115
248
|
|
116
249
|
MIT License.
|
data/lib/docker_registry2.rb
CHANGED
data/lib/registry/exceptions.rb
CHANGED
data/lib/registry/registry.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'fileutils'
|
1
2
|
require 'rest-client'
|
2
3
|
require 'json'
|
3
4
|
|
@@ -16,85 +17,11 @@ class DockerRegistry::Registry
|
|
16
17
|
end
|
17
18
|
|
18
19
|
def doget(url)
|
19
|
-
|
20
|
-
response = RestClient.get @base_uri+url
|
21
|
-
rescue SocketError
|
22
|
-
raise DockerRegistry::RegistryUnknownException
|
23
|
-
rescue RestClient::Unauthorized => e
|
24
|
-
header = e.response.headers[:www_authenticate]
|
25
|
-
method = header.downcase.split(' ')[0]
|
26
|
-
case method
|
27
|
-
when 'basic'
|
28
|
-
response = do_basic_get(url)
|
29
|
-
when 'bearer'
|
30
|
-
response = do_bearer_get(url, header)
|
31
|
-
else
|
32
|
-
raise DockerRegistry::RegistryUnknownException
|
33
|
-
end
|
34
|
-
end
|
35
|
-
return response
|
20
|
+
return doreq "get", url
|
36
21
|
end
|
37
22
|
|
38
|
-
def
|
39
|
-
|
40
|
-
res = RestClient::Resource.new( @base_uri+url, @user, @password)
|
41
|
-
response = res.get
|
42
|
-
rescue SocketError
|
43
|
-
raise DockerRegistry::RegistryUnknownException
|
44
|
-
rescue RestClient::Unauthorized
|
45
|
-
raise DockerRegistry::RegistryAuthenticationException
|
46
|
-
end
|
47
|
-
return response
|
48
|
-
end
|
49
|
-
|
50
|
-
def do_bearer_get(url, header)
|
51
|
-
token = authenticate_bearer(header)
|
52
|
-
begin
|
53
|
-
response = RestClient.get @base_uri+url, Authorization: 'Bearer '+token
|
54
|
-
rescue SocketError
|
55
|
-
raise DockerRegistry::RegistryUnknownException
|
56
|
-
rescue RestClient::Unauthorized
|
57
|
-
raise DockerRegistry::RegistryAuthenticationException
|
58
|
-
end
|
59
|
-
|
60
|
-
return response
|
61
|
-
end
|
62
|
-
|
63
|
-
def authenticate_bearer(header)
|
64
|
-
# get the parts we need
|
65
|
-
target = split_auth_header(header)
|
66
|
-
# did we have a username and password?
|
67
|
-
if defined? @user and @user.to_s.strip.length != 0
|
68
|
-
target[:params][:account] = @user
|
69
|
-
end
|
70
|
-
# authenticate against the realm
|
71
|
-
uri = URI.parse(target[:realm])
|
72
|
-
uri.user = @user if defined? @user
|
73
|
-
uri.password = @password if defined? @password
|
74
|
-
begin
|
75
|
-
response = RestClient.get uri.to_s, {params: target[:params]}
|
76
|
-
rescue RestClient::Unauthorized
|
77
|
-
# bad authentication
|
78
|
-
raise DockerRegistry::RegistryAuthenticationException
|
79
|
-
end
|
80
|
-
# now save the web token
|
81
|
-
return JSON.parse(response)["token"]
|
82
|
-
end
|
83
|
-
|
84
|
-
def split_auth_header(header = '')
|
85
|
-
h = Hash.new
|
86
|
-
h = {params: {}}
|
87
|
-
header.split(/[\s,]+/).each {|entry|
|
88
|
-
p = entry.split('=')
|
89
|
-
case p[0]
|
90
|
-
when 'Bearer'
|
91
|
-
when 'realm'
|
92
|
-
h[:realm] = p[1].gsub(/(^\"|\"$)/,'')
|
93
|
-
else
|
94
|
-
h[:params][p[0]] = p[1].gsub(/(^\"|\"$)/,'')
|
95
|
-
end
|
96
|
-
}
|
97
|
-
h
|
23
|
+
def dohead(url)
|
24
|
+
return doreq "head", url
|
98
25
|
end
|
99
26
|
|
100
27
|
def ping
|
@@ -112,9 +39,144 @@ class DockerRegistry::Registry
|
|
112
39
|
return repos
|
113
40
|
end
|
114
41
|
|
115
|
-
def tags(repo)
|
42
|
+
def tags(repo,withHashes = false)
|
116
43
|
response = doget "/v2/#{repo}/tags/list"
|
117
44
|
# parse the response
|
118
|
-
JSON.parse response
|
45
|
+
resp = JSON.parse response
|
46
|
+
# do we include the hashes?
|
47
|
+
if withHashes then
|
48
|
+
useGet = false
|
49
|
+
resp["hashes"] = {}
|
50
|
+
resp["tags"].each {|tag|
|
51
|
+
if useGet then
|
52
|
+
head = doget "/v2/#{repo}/manifests/#{tag}"
|
53
|
+
else
|
54
|
+
begin
|
55
|
+
head = dohead "/v2/#{repo}/manifests/#{tag}"
|
56
|
+
rescue DockerRegistry::InvalidMethod
|
57
|
+
# in case we are in a registry pre-2.3.0, which did not support manifest HEAD
|
58
|
+
useGet = true
|
59
|
+
head = doget "/v2/#{repo}/manifests/#{tag}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
resp["hashes"][tag] = head.headers[:docker_content_digest]
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
return resp
|
67
|
+
end
|
68
|
+
|
69
|
+
def manifest(repo,tag)
|
70
|
+
# first get the manifest
|
71
|
+
JSON.parse doget "/v2/#{repo}/manifests/#{tag}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def pull(repo,tag,dir)
|
75
|
+
# make sure the directory exists
|
76
|
+
FileUtils::mkdir_p dir
|
77
|
+
# get the manifest
|
78
|
+
m = manifest repo,tag
|
79
|
+
# pull each of the layers
|
80
|
+
layers = m["fsLayers"].each { |layer|
|
81
|
+
# make sure the layer does not exist first
|
82
|
+
if ! File.file? "#{dir}/#{layer.blobSum}" then
|
83
|
+
doget "/v2/#{repo}/blobs/#{layer.blobSum}" "#{dir}/#{layer.blobSum}"
|
84
|
+
end
|
85
|
+
}
|
119
86
|
end
|
87
|
+
|
88
|
+
def push(manifest,dir)
|
89
|
+
end
|
90
|
+
|
91
|
+
def tag(repo,tag,newrepo,newtag)
|
92
|
+
end
|
93
|
+
|
94
|
+
def copy(repo,tag,newregistry,newrepo,newtag)
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
def doreq(type,url)
|
99
|
+
begin
|
100
|
+
response = RestClient::Request.execute method: type, url: @base_uri+url
|
101
|
+
rescue SocketError
|
102
|
+
raise DockerRegistry::RegistryUnknownException
|
103
|
+
rescue RestClient::Unauthorized => e
|
104
|
+
header = e.response.headers[:www_authenticate]
|
105
|
+
method = header.downcase.split(' ')[0]
|
106
|
+
case method
|
107
|
+
when 'basic'
|
108
|
+
response = do_basic_req(type, url)
|
109
|
+
when 'bearer'
|
110
|
+
response = do_bearer_req(type, url, header)
|
111
|
+
else
|
112
|
+
raise DockerRegistry::RegistryUnknownException
|
113
|
+
end
|
114
|
+
end
|
115
|
+
return response
|
116
|
+
end
|
117
|
+
|
118
|
+
def do_basic_req(type, url)
|
119
|
+
begin
|
120
|
+
response = RestClient::Request.execute method: type, url: @base_uri+url, user: @user, password: @password
|
121
|
+
rescue SocketError
|
122
|
+
raise DockerRegistry::RegistryUnknownException
|
123
|
+
rescue RestClient::Unauthorized
|
124
|
+
raise DockerRegistry::RegistryAuthenticationException
|
125
|
+
rescue RestClient::MethodNotAllowed
|
126
|
+
raise DockerRegistry::InvalidMethod
|
127
|
+
end
|
128
|
+
return response
|
129
|
+
end
|
130
|
+
|
131
|
+
def do_bearer_req(type, url, header)
|
132
|
+
token = authenticate_bearer(header)
|
133
|
+
begin
|
134
|
+
response = RestClient::Request.execute method: type, url: @base_uri+url, headers: {Authorization: 'Bearer '+token}
|
135
|
+
rescue SocketError
|
136
|
+
raise DockerRegistry::RegistryUnknownException
|
137
|
+
rescue RestClient::Unauthorized
|
138
|
+
raise DockerRegistry::RegistryAuthenticationException
|
139
|
+
rescue RestClient::MethodNotAllowed
|
140
|
+
raise DockerRegistry::InvalidMethod
|
141
|
+
end
|
142
|
+
|
143
|
+
return response
|
144
|
+
end
|
145
|
+
|
146
|
+
def authenticate_bearer(header)
|
147
|
+
# get the parts we need
|
148
|
+
target = split_auth_header(header)
|
149
|
+
# did we have a username and password?
|
150
|
+
if defined? @user and @user.to_s.strip.length != 0
|
151
|
+
target[:params][:account] = @user
|
152
|
+
end
|
153
|
+
# authenticate against the realm
|
154
|
+
uri = URI.parse(target[:realm])
|
155
|
+
uri.user = @user if defined? @user
|
156
|
+
uri.password = @password if defined? @password
|
157
|
+
begin
|
158
|
+
response = RestClient.get uri.to_s, {params: target[:params]}
|
159
|
+
rescue RestClient::Unauthorized
|
160
|
+
# bad authentication
|
161
|
+
raise DockerRegistry::RegistryAuthenticationException
|
162
|
+
end
|
163
|
+
# now save the web token
|
164
|
+
return JSON.parse(response)["token"]
|
165
|
+
end
|
166
|
+
|
167
|
+
def split_auth_header(header = '')
|
168
|
+
h = Hash.new
|
169
|
+
h = {params: {}}
|
170
|
+
header.split(/[\s,]+/).each {|entry|
|
171
|
+
p = entry.split('=')
|
172
|
+
case p[0]
|
173
|
+
when 'Bearer'
|
174
|
+
when 'realm'
|
175
|
+
h[:realm] = p[1].gsub(/(^\"|\"$)/,'')
|
176
|
+
else
|
177
|
+
h[:params][p[0]] = p[1].gsub(/(^\"|\"$)/,'')
|
178
|
+
end
|
179
|
+
}
|
180
|
+
h
|
181
|
+
end
|
120
182
|
end
|
data/lib/registry/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: docker_registry2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Avi Deitcher https://github.com/deitch
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2016-02-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|