diff --git a/cas-template.md b/cas-template.md index ca15775..4d81ca0 100644 --- a/cas-template.md +++ b/cas-template.md @@ -12,6 +12,7 @@ For a given blob digest, consumers MUST provide at least the following variables * `encoded`, matching `encoded` in the `digest` rule. and expand the URI Template as defined in [RFC 6570 section 3][rfc6570-s3]. +If the expanded URI reference is a relative reference, it MUST be resolved following [RFC 3986 section 5][rfc3986-s5]. ## Example @@ -35,5 +36,6 @@ so the expanded URI is: https://a.example.com/cas/sha256/e3/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 [digest]: https://github.com/opencontainers/image-spec/blob/v1.0.0/descriptor.md#digests +[rfc3986-s5]: https://tools.ietf.org/html/rfc3986#section-5 [rfc6570]: https://tools.ietf.org/html/rfc6570 [rfc6570-s3]: https://tools.ietf.org/html/rfc6570#section-3 diff --git a/index-template.md b/index-template.md index c91cf0b..02b29ec 100644 --- a/index-template.md +++ b/index-template.md @@ -14,6 +14,7 @@ Consumers MUST provide at least the following variables: If `fragment` was not provided in the image name, it defaults to an empty string. and expand the URI Template as defined in [RFC 6570 section 3][rfc6570-s3]. +If the expanded URI reference is a relative reference, it MUST be resolved following [RFC 3986 section 5][rfc3986-s5]. The server providing the expanded URI MUST support requests for media type [`application/vnd.oci.image.index.v1+json`][index]. Servers MAY support other media types using HTTP content negotiation, as described in [RFC 7231 section 3.4][rfc7231-s3.4] (which is [also supported over HTTP/2][rfc7540-s8]). @@ -88,6 +89,7 @@ Deciding whether to look for `1.0` (the `fragment`) or the full `a.b.example.com [index]: https://github.com/opencontainers/image-spec/blob/v1.0.0/image-index.md [index.json]: https://github.com/opencontainers/image-spec/blob/v1.0.0/image-layout.md#indexjson-file [rfc2606-s3]: https://tools.ietf.org/html/rfc2606#section-3 +[rfc3986-s5]: https://tools.ietf.org/html/rfc3986#section-5 [rfc6570]: https://tools.ietf.org/html/rfc6570 [rfc6570-s3]: https://tools.ietf.org/html/rfc6570#section-3 [rfc7231-s3.4]: https://tools.ietf.org/html/rfc7231#section-3.4 diff --git a/oci_discovery/ref_engine/oci_index_template.py b/oci_discovery/ref_engine/oci_index_template.py index c582f61..bb0699b 100644 --- a/oci_discovery/ref_engine/oci_index_template.py +++ b/oci_discovery/ref_engine/oci_index_template.py @@ -14,6 +14,7 @@ import logging as _logging import pprint as _pprint +import urllib.parse as _urllib_parse try: import uritemplate as _uritemplate @@ -34,12 +35,15 @@ def __str__(self): self.__class__.__name__, self.uri_template) - def __init__(self, uri): + def __init__(self, uri, base=None): self.uri_template = _uritemplate.URITemplate(uri=uri) + self.base = base def resolve(self, name): name_parts = _host_based_image_names.parse(name=name) uri = self.uri_template.expand(**name_parts) + if self.base: + uri = _urllib_parse.urljoin(base=self.base, url=uri) _LOGGER.debug('fetching an OCI index for {} from {}'.format(name, uri)) index = _fetch_json.fetch( uri=uri, diff --git a/oci_discovery/ref_engine/test_oci_index_template.py b/oci_discovery/ref_engine/test_oci_index_template.py index f893269..c5f6e66 100644 --- a/oci_discovery/ref_engine/test_oci_index_template.py +++ b/oci_discovery/ref_engine/test_oci_index_template.py @@ -135,3 +135,67 @@ def test_bad(self): return_value=response): generator = engine.resolve(name='example.com/a') self.assertRaisesRegex(error, regex, list, generator) + + def test_reference_expansion(self): + response = { + 'manifests': [ + { + 'entry': 'a', + 'annotations': { + 'org.opencontainers.image.ref.name': '1.0', + }, + }, + ], + } + for uri, base, expected in [ + ( + 'index.json', + 'https://example.com/a', + 'https://example.com/index.json', + ), + ( + 'index.json', + 'https://example.com/a/', + 'https://example.com/a/index.json', + ), + ( + 'https://{host}/{path}#{fragment}', + 'https://a.example.com/b/', + 'https://example.com/a#1.0' + ), + ( + '//{host}/{path}#{fragment}', + 'https://a.example.com/b/', + 'https://example.com/a#1.0' + ), + ( + '/{path}#{fragment}', + 'https://b.example.com/c/', + 'https://b.example.com/a#1.0', + ), + ( + '{path}#{fragment}', + 'https://b.example.com/c/', + 'https://b.example.com/c/a#1.0', + ), + ( + '#{fragment}', + 'https://example.com/a', + 'https://example.com/a#1.0', + ), + ( + '#{fragment}', + 'https://example.com/a/', + 'https://example.com/a/#1.0', + ), + ]: + with self.subTest(label='{} from {}'.format(uri, base)): + engine = oci_index_template.Engine(uri=uri, base=base) + with unittest.mock.patch( + target='oci_discovery.ref_engine.oci_index_template._fetch_json.fetch', + return_value=response) as mock: + resolved = list(engine.resolve(name='example.com/a#1.0')) + mock.assert_called_with( + uri=expected, + media_type='application/vnd.oci.image.index.v1+json') + self.assertEqual(resolved, response['manifests'])