1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
|
====================================
Tutorial: creating an API endpoint
====================================
This section of the document details how to create an endpoint for CloudKitty's
v2 API. The v1 API is frozen, no endpoint should be added.
Setting up the layout for a new resource
========================================
In this section, we will create an ``example`` endpoint. Create the following
files and subdirectories in ``cloudkitty/api/v2/``:
.. code-block:: console
cloudkitty/api/v2/
└── example
├── example.py
└── __init__.py
Creating a custom resource
==========================
Each v2 API endpoint is based on a Flask Blueprint and one Flask-RESTful
resource per sub-endpoint. This allows to have a logical grouping of the
resources. Let's take the ``/rating/hashmap`` route as an example. Each of
the hashmap module's resources should be a Flask-RESTful resource (eg.
``/rating/hashmap/service``, ``/rating/hashmap/field`` etc...).
.. note:: There should be a distinction between endpoints refering to a single
resource and to several ones. For example, if you want an endpoint
allowing to list resources of some kind, you should implement the
following:
* A ``MyResource`` resource with support for ``GET``, ``POST``
and ``PUT`` HTTP methods on the ``/myresource/<uuid:>`` route.
* A ``MyResourceList`` resource with support for the ``GET`` HTTP
method on the ``/myresource`` route.
* A blueprint containing these resources.
Basic resource
--------------
We'll create an ``/example/`` endpoint, used to manipulate fruits. We'll create
an ``Example`` resource, supporting ``GET`` and ``POST`` HTTP methods. First
of all, we'll create a class with ``get`` and ``post`` methods in
``cloudkitty/api/v2/example/example.py``:
.. code-block:: python
from cloudkitty.api.v2 import base
class Example(base.BaseResource):
def get(self):
pass
def post(self):
pass
Validating a method's parameters and output
-------------------------------------------
A ``GET`` request on our resource will simply return **{"message": "This is an
example endpoint"}**. The ``add_output_schema`` decorator adds voluptuous
validation to a method's output. This allows to set defaults.
.. autofunction:: cloudkitty.api.v2.utils.add_output_schema
:noindex:
Let's update our ``get`` method in order to use this decorator:
.. code-block:: python
import voluptuous
from cloudkitty.api.v2 import base
from cloudkitty import validation_utils
class Example(base.BaseResource):
@api_utils.add_output_schema({
voluptuous.Required(
'message',
default='This is an example endpoint',
): validation_utils.get_string_type(),
})
def get(self):
return {}
.. note:: In this snippet, ``get_string_type`` returns ``basestring`` in
python2 and ``str`` in python3.
.. code-block:: console
$ curl 'http://cloudkitty-api:8889/v2/example'
{"message": "This is an example endpoint"}
It is now time to implement the ``post`` method. This function will take a
parameter. In order to validate it, we'll use the ``add_input_schema``
decorator:
.. autofunction:: cloudkitty.api.v2.utils.add_input_schema
:noindex:
Arguments validated by the input schema are passed as named arguments to the
decorated function. Let's implement the post method. We'll use Werkzeug
exceptions for HTTP return codes.
.. code-block:: python
@api_utils.add_input_schema('body', {
voluptuous.Required('fruit'): validation_utils.get_string_type(),
})
def post(self, fruit=None):
policy.authorize(flask.request.context, 'example:submit_fruit', {})
if not fruit:
raise http_exceptions.BadRequest(
'You must submit a fruit',
)
if fruit not in ['banana', 'strawberry']:
raise http_exceptions.Forbidden(
'You submitted a forbidden fruit',
)
return {
'message': 'Your fruit is a ' + fruit,
}
Here, ``fruit`` is expected to be found in the request body:
.. code-block:: console
$ curl -X POST -H 'Content-Type: application/json' 'http://cloudkitty-api:8889/v2/example' -d '{"fruit": "banana"}'
{"message": "Your fruit is a banana"}
In order to retrieve ``fruit`` from the query, the function should have been
decorated like this:
.. code-block:: python
@api_utils.add_input_schema('query', {
voluptuous.Required('fruit'): api_utils.SingleQueryParam(str),
})
def post(self, fruit=None):
Note that a ``SingleQueryParam`` is used here: given that query parameters can
be specified several times (eg ``xxx?groupby=a&groupby=b``), Flask provides
query parameters as lists. The ``SingleQueryParam`` helper checks that a
parameter is provided only once, and returns it.
.. autoclass:: cloudkitty.api.v2.utils.SingleQueryParam
:noindex:
.. warning:: ``SingleQueryParam`` uses ``voluptuous.Coerce`` internally for
type checking. Thus, ``validation_utils.get_string_type`` cannot
be used as ``basestring`` can't be instantiated.
Authorising methods
-------------------
The ``Example`` resource is still missing some authorisations. We'll create a
policy per method, configurable via the ``policy.yaml`` file. Create a
``cloudkitty/common/policies/v2/example.py`` file with the following content:
.. code-block:: python
from oslo_policy import policy
from cloudkitty.common.policies import base
example_policies = [
policy.DocumentedRuleDefault(
name='example:get_example',
check_str=base.UNPROTECTED,
description='Get an example message',
operations=[{'path': '/v2/example',
'method': 'GET'}]),
policy.DocumentedRuleDefault(
name='example:submit_fruit',
check_str=base.UNPROTECTED,
description='Submit a fruit',
operations=[{'path': '/v2/example',
'method': 'POST'}]),
]
def list_rules():
return example_policies
Add the following lines to ``cloudkitty/common/policies/__init__.py``:
.. code-block:: python
# [...]
from cloudkitty.common.policies.v2 import example as v2_example
def list_rules():
return itertools.chain(
base.list_rules(),
# [...]
v2_example.list_rules(),
)
This registers two documented policies, ``get_example`` and ``submit_fruit``.
They are unprotected by default, which means that everybody can access them.
However, they can be overriden in ``policy.yaml``. Call them the following way:
.. code-block:: python
# [...]
import flask
from cloudkitty.common import policy
from cloudkitty.api.v2 import base
class Example(base.BaseResource):
# [...]
def get(self):
policy.authorize(flask.request.context, 'example:get_example', {})
return {}
# [...]
def post(self):
policy.authorize(flask.request.context, 'example:submit_fruit', {})
# [...]
Loading drivers
---------------
Most of the time, resources need to load some drivers (storage, SQL...).
As the instantiation of these drivers can take some time, this should be done
only once.
Some drivers (like the storage driver) are loaded in ``BaseResource`` and are
thus available to all resources.
Resources requiring some additional drivers should implement the ``reload``
function:
.. code-block:: python
class BaseResource(flask_restful.Resource):
@classmethod
def reload(cls):
"""Reloads all required drivers"""
Here's an example taken from ``cloudkitty.api.v2.scope.state.ScopeState``:
.. code-block:: python
@classmethod
def reload(cls):
super(ScopeState, cls).reload()
cls._client = messaging.get_client()
cls._storage_state = storage_state.StateManager()
Registering resources
=====================
Each endpoint should provide an ``init`` method taking a Flask app as only
parameter. This method should call ``do_init``:
.. autofunction:: cloudkitty.api.v2.utils.do_init
:noindex:
Add the following to ``cloudkitty/api/v2/example/__init__.py``:
.. code-block:: python
from cloudkitty.api.v2 import utils as api_utils
def init(app):
api_utils.do_init(app, 'example', [
{
'module': __name__ + '.' + 'example',
'resource_class': 'Example',
'url': '',
},
])
return app
Here, we call ``do_init`` with the flask app passed as parameter, a blueprint
name, and a list of resources. The blueprint name will prefix the URLs of all
resources. Each resource is represented by a dict with the following
attributes:
* ``module``: name of the python module containing the resource class
* ``resource_class``: class of the resource
* ``url``: url suffix
In our case, the ``Example`` resource will be served at ``/example`` (blueprint
name + URL suffix).
.. note:: In case you need to add a resource to an existing endpoint, just add
it to the list.
.. warning:: If you created a new module, you'll have to add it to
``API_MODULES`` in ``cloudkitty/api/v2/__init__.py``:
.. code-block:: python
API_MODULES = [
'cloudkitty.api.v2.example',
]
Documenting your endpoint
=========================
The v2 API is documented with `os_api_ref`_ . Each v2 API endpoint must be
documented in ``doc/source/api-reference/v2/<endpoint_name>/``.
.. _os_api_ref: https://docs.openstack.org/os-api-ref/latest/
|