Skip to content

Commit 0e02519

Browse files
add image_prefix validation
test image_prefix trait validation add image name parser simplify get_image_manifest method extend get_image_manifest tests update zero-to-binderhub doc, private registry section
1 parent e3627e5 commit 0e02519

File tree

7 files changed

+71
-15
lines changed

7 files changed

+71
-15
lines changed

binderhub/app.py

+10
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,16 @@ def _valid_badge_base_url(self, proposal):
307307
config=True
308308
)
309309

310+
@validate('image_prefix')
311+
def _valid_image_prefix(self, proposal):
312+
image_regex = re.compile(r'^([^\/:]+(?::[0-9]+)?\/)?' +
313+
r'([_a-zA-Z0-9\-\.]+(?:\/[_a-zA-Z0-9\-\.]+)*)$')
314+
if image_regex.match(proposal.value) is None:
315+
raise AttributeError("'image_prefix' does not match the expected pattern "+
316+
"[<registry>[:<port>]/]<repo>")
317+
318+
return proposal.value
319+
310320
build_memory_request = ByteSpecification(
311321
0,
312322
help="""

binderhub/builder.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ async def get(self, provider_prefix, _unescaped_spec):
306306
if self.settings['use_registry']:
307307
for _ in range(3):
308308
try:
309-
image_manifest = await self.registry.get_image_manifest(*'/'.join(image_name.split('/')[-2:]).split(':', 1))
309+
image_manifest = await self.registry.get_image_manifest(image_name)
310310
image_found = bool(image_manifest)
311311
break
312312
except HTTPClientError:

binderhub/health.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,7 @@ async def check_docker_registry(self):
146146
# we are only interested in getting a response from the registry, we
147147
# don't care if the image actually exists or not
148148
image_name = self.settings["image_prefix"] + "some-image-name:12345"
149-
await registry.get_image_manifest(
150-
*'/'.join(image_name.split('/')[-2:]).split(':', 1)
151-
)
149+
await registry.get_image_manifest(image_name)
152150
return True
153151

154152
async def check_pod_quota(self):

binderhub/registry.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,28 @@ def _default_password(self):
185185
base64.b64decode(b64_auth.encode("utf-8")).decode("utf-8").split(":", 1)[1]
186186
)
187187

188-
async def get_image_manifest(self, image, tag):
188+
@staticmethod
189+
def parse_image_name(name):
190+
"""Parse a docker image string into (registry, image, tag)"""
191+
# name could be "localhost:5000/foo:tag"
192+
# or "gcr.io/project/subrepo/sub/sub/sub:tag"
193+
# or "org/repo:tag" for implicit docker.io
194+
registry, _, tagged_image = name.partition("/")
195+
if tagged_image and (registry == "localhost" or any(c in registry for c in (".", ":"))):
196+
# registry host is specified
197+
pass
198+
else:
199+
# registry not given, use implicit default docker.io registry
200+
registry = "docker.io"
201+
tagged_image = name
202+
image, _, tag = tagged_image.partition(":")
203+
204+
return registry, image, tag
205+
206+
async def get_image_manifest(self, name):
189207
client = httpclient.AsyncHTTPClient()
208+
_, image, tag = DockerRegistry.parse_image_name(name)
209+
190210
url = "{}/v2/{}/manifests/{}".format(self.url, image, tag)
191211
# first, get a token to perform the manifest request
192212
if self.token_url:

binderhub/tests/test_app.py

+19
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,29 @@
22

33
from subprocess import check_output
44
import sys
5+
import pytest
6+
7+
from binderhub.app import BinderHub
8+
59

610
def test_help():
711
check_output([sys.executable, '-m', 'binderhub', '-h'])
812

913
def test_help_all():
1014
check_output([sys.executable, '-m', 'binderhub', '--help-all'])
1115

16+
def test_image_prefix():
17+
b = BinderHub()
18+
19+
prefixes = ["foo/bar", "foo-bar/baz", "foo/bar-", "localhost/foo", "localhost/foo/bar/baz",
20+
"localhost:8080/foo/bar/baz", "127.0.0.1/foo", "127.0.0.1:5000/foo/b",
21+
"f/o/o/b/a/r/b/a/z", "gcr.io/foo", "my.gcr.io:5000/foo", "foo_bar/baz-",
22+
"foo_ba.r/baz-"]
23+
for name in prefixes:
24+
b.image_prefix = name
25+
26+
wrong_prefixes = ["foo/bar-baz:", "foo/bar-baz:", "foo/bar-baz:10", "foo/bar/", "/foo/bar",
27+
"http://localhost/foo/bar"]
28+
for name in wrong_prefixes:
29+
with pytest.raises(AttributeError):
30+
b.image_prefix = name

binderhub/tests/test_registry.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ async def test_get_image_manifest(tmpdir, request):
113113
[
114114
(r"/token", MockTokenHandler, {"test_handle": test_handle}),
115115
(
116-
r"/v2/([^/]+)/manifests/([^/]+)",
116+
r"/v2/([a-zA-Z0-9]+(?:/[a-zA-Z0-9]+)*)/manifests/([a-zA-Z0-9]+)",
117117
MockManifestHandler,
118118
{"test_handle": test_handle},
119119
),
@@ -145,5 +145,9 @@ async def test_get_image_manifest(tmpdir, request):
145145
assert registry.token_url == url + "/token"
146146
assert registry.username == username
147147
assert registry.password == password
148-
manifest = await registry.get_image_manifest("myimage", "abc123")
149-
assert manifest == {"image": "myimage", "tag": "abc123"}
148+
149+
names = ["myimage:abc123", "localhost/myimage:abc1234", "localhost:8080/myimage:abc3",
150+
"192.168.1.1:8080/myimage:abc321", "192.168.1.1:8080/some/repo/myimage:abc3210"]
151+
for name in names:
152+
manifest = await registry.get_image_manifest(name)
153+
assert manifest == {"image": name.split("/", 1)[-1].rsplit(":", 1)[0], "tag": name.rsplit(":", 1)[-1]}

doc/zero-to-binderhub/setup-binderhub.rst

+12-7
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,18 @@ If you setup your own local registry using
291291

292292
.. important::
293293

294-
BinderHub assumes that `image_prefix` contains an extra level for the project name such that: `gcr.io/<project-id>/<prefix>-name:tag`.
295-
Hence, your `image_prefix` field should be set to: `your-registry.io/<some-project-name>/<prefix>-`.
296-
See `this issue <https://github.com/jupyterhub/binderhub/issues/800>`_ for more details.
297-
298-
`<some-project-name>` can be completely arbitrary and/or made-up.
299-
For example, it could be the name you give your BinderHub.
300-
Without this extra level, you may find that your BinderHub always rebuilds images instead of pulling them from the registry.
294+
BinderHub validates that `image_prefix` matches the following pattern: `[<registry>/]<prefix>`.
295+
296+
<registry> is optional and defaults to `docker.io`. It may include the server port but does not
297+
include the protocol (http[s]://).
298+
299+
<prefix> supports any repository depth.
300+
301+
Examples:
302+
- `binder-`
303+
- `foo/binder-`
304+
- `docker.io/foo/bar/baz/binder-`
305+
- `127.0.0.1:5000/binder-`
301306

302307
Install BinderHub
303308
-----------------

0 commit comments

Comments
 (0)