From 06e271540a44c7987261c7556573df3d136fdf76 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 11 Sep 2017 00:17:29 -0700 Subject: [PATCH] oci_discovery/uri_template: Stub for URI Template expansion Python's .format can handle things like {host}, but there's a lot more than that in RFC 6570. Instead of rolling our own implementation, punt to the uritemplate package [1]. The license is [1,2]: BSD-3-Clause OR Apache-2.0 which should be compatible with the OCI's Apache-2.0 requirement [3], even if that requirement applies to third-party dependencies, which is not clear to me. [1]: https://pypi.python.org/pypi/uritemplate [2]: https://github.com/python-hyper/uritemplate/blob/3.0.0/LICENSE [3]: https://www.opencontainers.org/about/governance Section 8.a --- README.md | 15 + .../ref_engine/oci_index_template.py | 14 +- oci_discovery/uri_template/__init__.py | 34 ++ oci_discovery/uri_template/test.py | 294 ++++++++++++++++++ requirements.txt | 1 + 5 files changed, 351 insertions(+), 7 deletions(-) create mode 100644 oci_discovery/uri_template/__init__.py create mode 100644 oci_discovery/uri_template/test.py create mode 100644 requirements.txt diff --git a/README.md b/README.md index 14acfaf..dfc4103 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,18 @@ The strategies in these specifications are inspired by some previous implementat * [App Container Image Discovery](https://github.com/appc/spec/blob/v0.8.10/spec/discovery.md) * [parcel](https://github.com/cyphar/parcel) +## Python dependencies + +The [OCI Index Template Protocol](index-template.md) [implementation](oci_discovery/ref_engine/oci_index_template) depends on the [uritemplate][] package. +You can install the dependencies with [pip][]: + +``` +$ pip install -r requirements.txt +``` + +When uritemplate is not installed, a local implementation is used instead. +But the local stub supports only the most basic [URI Templates][rfc6570]. + ## Using the Python 3 ref-engine discovery tool The individual components are usable as libraries, but the ref-engine discovery implementation can also be used from the command line: @@ -258,5 +270,8 @@ location ~ ^/oci-image/.*/index.json$ { [layout]: https://github.com/opencontainers/image-spec/blob/v1.0.0/image-layout.md [location]: http://nginx.org/en/docs/http/ngx_http_core_module.html#location [Nginx]: https://nginx.org/ +[pip]: https://pip.pypa.io/en/stable/ [python3]: https://docs.python.org/3/ +[rfc6570]: https://tools.ietf.org/html/rfc6570 [signed-name-assertions]: https://github.com/opencontainers/image-spec/issues/176 +[uritemplate]: https://pypi.python.org/pypi/uritemplate diff --git a/oci_discovery/ref_engine/oci_index_template.py b/oci_discovery/ref_engine/oci_index_template.py index 37f0dc9..c582f61 100644 --- a/oci_discovery/ref_engine/oci_index_template.py +++ b/oci_discovery/ref_engine/oci_index_template.py @@ -15,6 +15,11 @@ import logging as _logging import pprint as _pprint +try: + import uritemplate as _uritemplate +except ImportError as error: + from .. import uri_template as _uritemplate + from .. import fetch_json as _fetch_json from .. import host_based_image_names as _host_based_image_names @@ -30,16 +35,11 @@ def __str__(self): self.uri_template) def __init__(self, uri): - self.uri_template = uri + self.uri_template = _uritemplate.URITemplate(uri=uri) def resolve(self, name): name_parts = _host_based_image_names.parse(name=name) - try: - uri = self.uri_template.format(**name_parts) - except KeyError as error: - raise ValueError( - 'failed to format {}'.format(self.uri_template) - ) from error + uri = self.uri_template.expand(**name_parts) _LOGGER.debug('fetching an OCI index for {} from {}'.format(name, uri)) index = _fetch_json.fetch( uri=uri, diff --git a/oci_discovery/uri_template/__init__.py b/oci_discovery/uri_template/__init__.py new file mode 100644 index 0000000..10e5c3a --- /dev/null +++ b/oci_discovery/uri_template/__init__.py @@ -0,0 +1,34 @@ +# Copyright 2017 oci-discovery contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class URITemplate(object): + """Stub implementation of uritemplate's URITemplate. + + https://pypi.python.org/pypi/uritemplate + """ + def __init__(self, uri): + self.uri = uri + + def __str__(self): + return self.uri + + def expand(self, **kwargs): + # Basic URI Templates match Python's str.format() syntax, just + # try that. + try: + return self.uri.format(**kwargs) + except KeyError as error: + raise ValueError( + 'failed to format {}'.format(self.uri) + ) from error diff --git a/oci_discovery/uri_template/test.py b/oci_discovery/uri_template/test.py new file mode 100644 index 0000000..fc87982 --- /dev/null +++ b/oci_discovery/uri_template/test.py @@ -0,0 +1,294 @@ +# Copyright 2017 oci-discovery contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import unittest + +try: + import uritemplate +except ImportError: + uritemplate = None + + +from . import URITemplate + + +class TestURITemplate(unittest.TestCase): + def _run(self, cls, exceptions=(), wrong_values=()): + """Test against examples from RFC 6570. + + https://tools.ietf.org/html/rfc6570 + """ + # Defined in https://tools.ietf.org/html/rfc6570#section-3.2 + # Other sections have their own definitions, but they're + # subsets of this set. + variables = { + 'count': ('one', 'two', 'three'), + 'dom': ('example', 'com'), + 'dub': 'me/too', + 'hello': 'Hello World!', + 'half': '50%', + 'var': 'value', + 'who': 'fred', + 'base': 'http://example.com/home/', + 'path': '/foo/bar', + 'list': ('red', 'green', 'blue'), + 'keys': collections.OrderedDict(( + ('semi', ';'), + ('dot', '.'), + ('comma', ','), + )), + 'v': '6', + 'x': '1024', + 'y': '768', + 'empty': '', + 'empty_keys': {}, + #'undef': None. + } + for name, checks in ( + ( + 'section 1.2, level 1', + ( + ('{var}', 'value'), + ('{hello}', 'Hello%20World%21'), + ), + ), + ( + 'section 1.2, level 2', + ( + ('{+var}', 'value'), + ('{+hello}', 'Hello%20World!'), + ('{+path}/here', '/foo/bar/here'), + ('here?ref={+path}', 'here?ref=/foo/bar'), + ('X#{var}', 'X#value'), + ('X#{hello}', 'X#Hello%20World!'), + ), + ), + ( + 'section 1.2, level 3', + ( + ('map?{x,y}', 'map?1024,768'), + ('{x,hello,y}', '1024,Hello%20World%21,768'), + ('{+x,hello,y}', '1024,Hello%20World!,768'), + ('{+path,x}/here', '/foo/bar,1024/here'), + ('{#x,hello,y}', '#1024,Hello%20World!,768'), + ('{#path,x}/here', '#/foo/bar,1024/here'), + ('X{.var}', 'X.value'), + ('X{.x,y}', 'X.1024.768'), + ('{/var}', '/value'), + ('{/var,x}/here', '/value/1024/here'), + ('{;x,y}', ';x=1024;y=768'), + ('{;x,y,empty}', ';x=1024;y=768;empty'), + ('{?x,y}', '?x=1024&y=768'), + ('{?x,y,empty}', '?x=1024&y=768&empty='), + ('?fixed=yes{&x}', '?fixed=yes&x=1024'), + ('{&x,y,empty}', '&x=1024&y=768&empty='), + ), + ), + ( + 'section 1.2, level 4', + ( + ('{var:3}', 'val'), + ('{var:30}', 'value'), + ('{list}', 'red,green,blue'), + ('{list*}', 'red,green,blue'), + ('{keys}', 'semi,%3B,dot,.,comma,%2C'), + ('{keys*}', 'semi=%3B,dot=.,comma=%2C'), + ('{+path:6}/here', '/foo/b/here'), + ('{+list}', 'red,green,blue'), + ('{+list*}', 'red,green,blue'), + ('{+keys}', 'semi,;,dot,.,comma,,'), + ('{+keys*}', 'semi=;,dot=.,comma=,'), + ('{#path:6}/here', '#/foo/b/here'), + ('{#list}', '#red,green,blue'), + ('{#list*}', '#red,green,blue'), + ('{#keys}', '#semi,;,dot,.,comma,,'), + ('{#keys*}', '#semi=;,dot=.,comma=,'), + ('X{.var:3}', 'X.val'), + ('X{.list}', 'X.red,green,blue'), + ('X{.list*}', 'X.red.green.blue'), + ('X{.keys}', 'X.semi,%3B,dot,.,comma,%2C'), + ('X{.keys*}', 'X.semi=%3B.dot=..comma=%2C'), + ('{/var:1,var}', '/v/value'), + ('{/list}', '/red,green,blue'), + ('{/list*}', '/red/green/blue'), + ('{/list*,path:4}', '/red/green/blue/%2Ffoo'), + ('{/keys}', '/semi,%3B,dot,.,comma,%2C'), + ('{/keys*}', '/semi=%3B/dot=./comma=%2C'), + ('{;hello:5}', ';hello=Hello'), + ('{;list}', ';list=red,green,blue'), + ('{;list*}', ';list=red;list=green;list=blue'), + ('{;keys}', ';keys=semi,%3B,dot,.,comma,%2C'), + ('{;keys*}', ';semi=%3B;dot=.;comma=%2C'), + ('{?var:3}', '?var=val'), + ('{?list}', '?list=red,green,blue'), + ('{?list*}', '?list=red&list=green&list=blue'), + ('{?keys}', '?keys=semi,%3B,dot,.,comma,%2C'), + ('{?keys*}', '?semi=%3B&dot=.&comma=%2C'), + ('{&var:3}', '&var=val'), + ('{&list}', '&list=red,green,blue'), + ('{&list*}', '&list=red&list=green&list=blue'), + ('{&keys}', '&keys=semi,%3B,dot,.,comma,%2C'), + ('{&keys*}', '&semi=%3B&dot=.&comma=%2C'), + ), + ), + ( + 'section 3.2.2', + ( + ('{var}', 'value'), + ('{hello}', 'Hello%20World%21'), + ('{half}', '50%25'), + ('O{empty}X', 'OX'), + ('O{undef}X', 'OX'), + ('{x,y}', '1024,768'), + ('{x,hello,y}', '1024,Hello%20World%21,768'), + ('?{x,empty}', '?1024,'), + ('?{x,undef}', '?1024'), + ('?{undef,y}', '?768'), + ('{var:3}', 'val'), + ('{var:30}', 'value'), + ('{list}', 'red,green,blue'), + ('{list*}', 'red,green,blue'), + ('{keys}', 'semi,%3B,dot,.,comma,%2C'), + ('{keys*}', 'semi=%3B,dot=.,comma=%2C'), + ), + ), + ): + with self.subTest(name=name): + for template, expected in checks: + with self.subTest(template=template): + if template in exceptions and template in wrong_values: + self.fail( + msg="entries in both 'exceptions' and 'wrong_values'. Pick one.") + obj = cls(uri=template) + try: + expanded = obj.expand(**variables) + except Exception as error: + if template in exceptions: + self.skipTest( + reason='expected failure: raised {}' + .format(error)) + raise + if template in exceptions: + self.fail( + msg='expected a failure, but this no longer raises an exception') + if template in wrong_values: + self.assertNotEqual(expanded, expected) + else: + self.assertEqual(expanded, expected) + + def test_stub(self): + self._run( + cls=URITemplate, + exceptions={ + '?fixed=yes{&x}', + '?{undef,y}', + '?{x,empty}', + '?{x,undef}', + 'O{undef}X', + 'X{.keys*}', + 'X{.keys}', + 'X{.list*}', + 'X{.list}', + 'X{.var:3}', + 'X{.var}', + 'X{.x,y}', + 'here?ref={+path}', + 'map?{x,y}', + '{#keys*}', + '{#keys}', + '{#list*}', + '{#list}', + '{#path,x}/here', + '{#path:6}/here', + '{#x,hello,y}', + '{&keys*}', + '{&keys}', + '{&list*}', + '{&list}', + '{&var:3}', + '{&x,y,empty}', + '{+hello}', + '{+keys*}', + '{+keys}', + '{+list*}', + '{+list}', + '{+path,x}/here', + '{+path:6}/here', + '{+path}/here', + '{+var}', + '{+x,hello,y}', + '{/keys*}', + '{/keys}', + '{/list*,path:4}', + '{/list*}', + '{/list}', + '{/var,x}/here', + '{/var:1,var}', + '{/var}', + '{;hello:5}', + '{;keys*}', + '{;keys}', + '{;list*}', + '{;list}', + '{;x,y,empty}', + '{;x,y}', + '{?keys*}', + '{?keys}', + '{?list*}', + '{?list}', + '{?var:3}', + '{?x,y,empty}', + '{?x,y}', + '{keys*}', + '{list*}', + '{undef,y}', + '{x,hello,y}', + '{x,y}', + }, + wrong_values={ + 'X#{hello}', + '{half}', + '{hello}', + '{keys}', + '{list}', + '{var:30}', + '{var:3}', + }, + ) + + @unittest.skipIf(uritemplate is None, 'failed to import uritemplate') + def test_external(self): + self._run( + cls=uritemplate.URITemplate, + wrong_values={ + 'X#{hello}', + 'X{.keys*}', + 'X{.keys}', + '{#keys*}', + '{#keys}', + '{&keys*}', + '{&keys}', + '{+keys*}', + '{+keys}', + '{/keys*}', + '{/keys}', + '{;keys*}', + '{;keys}', + '{?keys*}', + '{?keys}', + '{keys*}', + '{keys}', + }, + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9b4e1d4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +uritemplate>=3.0