From d5f08170a4d820f1fb252178013446ff123e42de Mon Sep 17 00:00:00 2001 From: Sam Betts Date: Tue, 15 Sep 2015 10:57:19 +0100 Subject: [PATCH] Add API Discovery to Ironic Inspector This patch allows for API discovery via the root API endpoint and version endpoints. These endpoints will return a json blob containing all the information required to talk to the different API versions and resources. Change-Id: Id427f1ca542523afb3513b047e32b5aad39bf743 Closes-Bug: #1477959 --- HTTP-API.rst | 37 +++++++++++++++++++++ ironic_inspector/main.py | 53 +++++++++++++++++++++++++++--- ironic_inspector/test/test_main.py | 52 ++++++++++++++++++++++++++--- 3 files changed, 133 insertions(+), 9 deletions(-) diff --git a/HTTP-API.rst b/HTTP-API.rst index 9d648c69a..bfc96a5ca 100644 --- a/HTTP-API.rst +++ b/HTTP-API.rst @@ -228,6 +228,43 @@ major version and is always ``1`` for now, ``Y`` is a minor version. ``X-OpenStack-Ironic-Inspector-API-Maximum-Version`` headers with minimum and maximum API versions supported by the server. +API Discovery +~~~~~~~~~~~~~ + +The API supports API discovery. You can query different parts of the API to +discover what other endpoints are avaliable. + +* ``GET /`` List API Versions + + Response: + + * 200 - OK + + Response body: JSON dictionary containing a list of ``versions``, each + version contains: + + * ``status`` Either CURRENT or SUPPORTED + * ``id`` The version identifier + * ``links`` A list of links to this version endpoint containing: + + * ``href`` The URL + * ``rel`` The relationship between the version and the href + +* ``GET /v1`` List API v1 resources + + Response: + + * 200 - OK + + Response body: JSON dictionary containing a list of ``resources``, each + resource contains: + + * ``name`` The name of this resources + * ``links`` A list of link to this resource containing: + + * ``href`` The URL + * ``rel`` The relationship between the resource and the href + Version History ^^^^^^^^^^^^^^^ diff --git a/ironic_inspector/main.py b/ironic_inspector/main.py index 437b940fc..d39a841ec 100644 --- a/ironic_inspector/main.py +++ b/ironic_inspector/main.py @@ -15,6 +15,8 @@ import eventlet eventlet.monkey_patch() import functools +import os +import re import ssl import sys @@ -108,13 +110,55 @@ def add_version_headers(res): return res +def create_link_object(urls): + links = [] + for url in urls: + links.append({"rel": "self", + "href": os.path.join(flask.request.url_root, url)}) + return links + + +def generate_resource_data(resources): + data = [] + for resource in resources: + item = {} + item['name'] = str(resource).split('/')[-1] + item['links'] = create_link_object([str(resource)[1:]]) + data.append(item) + return data + + @app.route('/', methods=['GET']) -@app.route('/v1', methods=['GET']) @convert_exceptions def api_root(): - # TODO(dtantsur): this endpoint only returns API version now, it's possible - # we'll return something meaningful in addition later - return flask.jsonify({}) + versions = [ + { + "status": "CURRENT", + "id": '%s.%s' % CURRENT_API_VERSION, + }, + ] + + for version in versions: + version['links'] = create_link_object( + ["v%s" % version['id'].split('.')[0]]) + + return flask.jsonify(versions=versions) + + +@app.route('/', methods=['GET']) +@convert_exceptions +def version_root(version): + pat = re.compile("^\/%s\/[^\/]*?$" % version) + + resources = [] + for url in app.url_map.iter_rules(): + if pat.match(str(url)): + resources.append(url) + + if not resources: + raise utils.Error(_('Version not found.'), code=404) + + return flask.jsonify(resources=generate_resource_data(resources)) @app.route('/v1/continue', methods=['POST']) @@ -126,6 +170,7 @@ def api_continue(): return flask.jsonify(process.process(data)) +# TODO(sambetts) Add API discovery for this endpoint @app.route('/v1/introspection/', methods=['GET', 'POST']) @convert_exceptions def api_introspection(uuid): diff --git a/ironic_inspector/test/test_main.py b/ironic_inspector/test/test_main.py index eaf3791e5..000f20bdc 100644 --- a/ironic_inspector/test/test_main.py +++ b/ironic_inspector/test/test_main.py @@ -305,11 +305,53 @@ class TestApiVersions(BaseAPITest): self.assertEqual('%d.%d' % main.CURRENT_API_VERSION, res.headers.get(main._MAX_VERSION_HEADER)) - def test_root_endpoints(self): - for endpoint in '/', '/v1': - res = self.app.get(endpoint) - self.assertEqual(200, res.status_code) - self._check_version_present(res) + def test_root_endpoint(self): + res = self.app.get("/") + self.assertEqual(200, res.status_code) + self._check_version_present(res) + data = res.data.decode('utf-8') + json_data = json.loads(data) + expected = {"versions": [{ + "status": "CURRENT", "id": '%s.%s' % main.CURRENT_API_VERSION, + "links": [{ + "rel": "self", + "href": ("http://localhost/v%s" % + main.CURRENT_API_VERSION[0]) + }] + }]} + self.assertEqual(expected, json_data) + + @mock.patch.object(main.app.url_map, "iter_rules", autospec=True) + def test_version_endpoint(self, mock_rules): + mock_rules.return_value = ["/v1/endpoint1", "/v1/endpoint2/", + "/v1/endpoint1/", + "/v2/endpoint1", "/v1/endpoint3", + "/v1/endpoint2//subpoint"] + endpoint = "/v1" + res = self.app.get(endpoint) + self.assertEqual(200, res.status_code) + self._check_version_present(res) + json_data = json.loads(res.data.decode('utf-8')) + expected = {u'resources': [ + { + u'name': u'endpoint1', + u'links': [{ + u'rel': u'self', + u'href': u'http://localhost/v1/endpoint1'}] + }, + { + u'name': u'endpoint3', + u'links': [{ + u'rel': u'self', + u'href': u'http://localhost/v1/endpoint3'}] + }, + ]} + self.assertDictEqual(expected, json_data) + + def test_version_endpoint_invalid(self): + endpoint = "/v-1" + res = self.app.get(endpoint) + self.assertEqual(404, res.status_code) def test_404_unexpected(self): # API version on unknown pages