cloudkitty/doc/source/developer/api/tutorial.rst
Luka Peschke 3426a6648a Build docs with -W
This fixes all build warnings in the docs, and makes them build
with -W (consider warnings as errors).

Work items:

* Remove the useless "METRICS_CONF" global variable in the monasca collector.
  It was causing the "load_conf()" function to fail, as the
  /etc/cloudkitty/metrics.yml file couldn't be found.

* Change the docstring indentation in
  cloudkitty/rating/hash/controllers/mapping.py. This was the cause for the
  "doc/source/api-reference/v1/rating/hashmap.rst:12: WARNING: Field list ends
  without a blank line; unexpected unindent." message.

* Added a cloudkitty.conf sample to the admin documentation.

* Deleted unused files.

* Fixed the policy.yaml sample include path.

* Sorted the v2 API parameters alphabetically.

* Ignored the "wsmeext.sphinxext: directive 'autoattribute' is
  already registered" warning

* Added :noindex: directives to functions documented twice.

* Made sphinx always build with -W.

Change-Id: I5f1ab04e18920f3352c2807878cad327692a225f
2019-11-25 11:27:13 +01:00

9.6 KiB

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/:

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:

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.

cloudkitty.api.v2.utils.add_output_schema

Let's update our get method in order to use this decorator:

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.

$ 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:

cloudkitty.api.v2.utils.add_input_schema

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.

@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:

$ 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:

@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.

cloudkitty.api.v2.utils.SingleQueryParam

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:

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:

# [...]
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:

# [...]
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:

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:

@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:

cloudkitty.api.v2.utils.do_init

Add the following to cloudkitty/api/v2/example/__init__.py:

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:

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>/.