From b0903d3b0e02431a258983461859bd91fc94ad6e Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Thu, 11 Jan 2018 14:29:48 +0100 Subject: [PATCH] Add guideline on exposing microversions in SDKs Change-Id: I69651d9df572bb33ea4fb413c57a4c3d15c98d7e --- guidelines/sdk-exposing-microversions.rst | 291 ++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 guidelines/sdk-exposing-microversions.rst diff --git a/guidelines/sdk-exposing-microversions.rst b/guidelines/sdk-exposing-microversions.rst new file mode 100644 index 0000000..1dfa34a --- /dev/null +++ b/guidelines/sdk-exposing-microversions.rst @@ -0,0 +1,291 @@ +Exposing microversions in SDKs +============================== + +While we are striving to design OpenStack API as easy to use as possible, SDKs +for various programming languages will always be an important part of +experience for developers, consuming it. This documentation contains +recommendations on how to deal with :doc:`microversions +` in SDKs (software development kits) +targeting OpenStack. + +This document recognizes two types of deliverables that we usually call SDKs. +They will differ in the recommended approaches to exposing microversions +to their consumers. + +* `High-level SDK`_ or just `SDK` is one that hides details of the underlying + API from consumers, building its own abstraction layers. Its approach + to backward and forward compatibility, as well as feature discovery, is + independent of the one used by the underlying API. Shade_ is an example of + such SDK for OpenStack. + +* `Language binding`_ closely follows the structure and design of the + underlying API. It usually tries to build as little additional + abstraction layers on top of the underlying API as possible. Examples + include all OpenStack ``python-client`` libraries. + +.. note:: + If in doubt, you should write a high-level SDK. The benefit of using an + SDK is in consuming API in a way, natural to the programming language and + any used frameworks. Things like microversions are likely to look foreign + and confusing for developers who do not specialize on API design. + +Concepts used in this document: + +consumer + programming code that interfaces with an SDK, as well as its author. +microversion + API version as defined in :doc:microversion_specification. For simplicity, + this guideline uses `version` as a synonym of `microversion`. + + .. note:: + When using the word ``microversion`` in your SDK, be careful to avoid + associations with semantic versioning. A microversion is not the same + as a patch version, and can be even major in a sense of semantic + versioning. +major version + is not really an API version in a sense of :doc:microversion_specification, + but rather a separate generation of the API, co-existing with other + generations in the same HTTP endpoints tree. + + Major versions are distinguished in the URLs by ``/v`` parts and + are the first components of a microversion. For example, in microversion + ``1.42``, ``1`` is a major version. + + .. note:: + We don't seem to have an established name for the second component. + + As major versions may change the structure of API substantially, including + changing the very mechanism of the microversioning, an SDK should generally + try to stay within the requested major version, if any. +negotiation + process of agreeing on the most suitable common version between the client + and the server. Negotiation should happen once, and its results should be + cached for the whole session. + +.. note:: + We will use the Python programming language in all examples, but + the recommendations will apply to any programming languages, including + statically compiled ones. For examples here we will use + a fictional Cats-as-a-Service API and its ``python-catsclient`` SDK. + +.. _Shade: https://docs.openstack.org/shade/latest/ + +High-level SDK +-------------- + +Generally, SDKs should not expose underlying API microversions to users. +The structure of input and output data should not depend on the microversion +used. Means, specific to the programming language and/or data formats in use, +should be employed to indicate absence or presence of certain features +and behaviors. + + For example, a field, missing in the current microversion, can be + expressed by ``None`` value in Python, ``null`` value in Java or its type + can be ``Option`` in Rust: + + .. code-block:: python + + import catsclient + + sdk = catsclient.SDK() + + cat = sdk.get_cat('fluffy') + if cat.color is None: + print("Cat colors are not supported by this cat server") + else: + print("The cat is", cat.color) + + In this example, the SDK negotiates the API microversion that can return + as much information as possible during the ``get_cat`` call. If the + resulting version does not contain the ``color`` field, it is set to + ``None``. + +An SDK should negotiate the highest microversion that will allow it to serve +consumer's needs better. However, it should never negotiate a microversion +outside of the range it was written and tested with to avoid confusing +breakages on future changes to the API. It goes without saying that an SDK +should not crush or exhibit undefined behavior on any microversion returned +by a server. Any incompatibilities should be expressed as soon as possible +in a form that is natural for the given programming language. + + For example, a Python SDK should raise an exception when a method is + called that is not possible to express in any microversion supported by + both the SDK and the server: + + .. code-block:: python + + import catsclient + + sdk = catsclient.SDK() + + cat = sdk.get_cat('fluffy') + try: + cat.bark() + except catsclient.UnsupportedFeature: + cat.meow() + + It is also useful to allow detecting supported features before + using them: + + .. code-block:: python + + import catsclient + + sdk = catsclient.SDK() + + cat = sdk.get_cat('fluffy') + if cat.can_bark(): + cat.bark() + else: + cat.meow() + + In this example, ``can_bark`` uses the negotiated microversion to check if + it is possible for the ``bark`` call to work. + +.. note:: + If possible, an SDK should inform the consumer of the required API + microversion and why it is not possible to use it. This is probably the + only place where microversions can and should leak to a consumer. + +If possible, major versions should be treated the same way, and should not be +exposed to users. If not possible, an SDK should pick the most recent +major version from the available. + +Language binding +---------------- + +A low-level SDKs, which is essentially just a language binding for the API, +stays close to the underlying API. Thus, it must expose microversions +to consumers, and must do it in a way, closest to how API does it. We +recommend that all calls accept an explicit API microversion that is sent +directly to the underlying API. If none is provided, no version should be sent: + +.. code-block:: python + + import catsclient + + client = catsclient.v1.get_client() + + cat = client.get_cat('fluffy') # executed with no explicit version + try: + cat.bark(api_version='1.42') # executed with 1.42 + except catsclient.IncompatibleApiVersion: + # no support for 1.42, falling back to older behavior + cat.meow() # executed with no explicit version + +.. note:: + In some programming languages, particularly those without default arguments + for functions, it may be inconvenient to add a version argument to all + calls. Other means may be used to achieve the same result, for example, + temporary context objects: + + .. code-block:: python + + import catsclient + + client = catsclient.v1.get_client() + + cat = client.get_cat('fluffy') # executed with no explicit version + with cat.use_api_version('1.42') as new_cat: + new_cat.bark() # executed with 1.42 + +Major versions +~~~~~~~~~~~~~~ + +A low-level SDK should make it explicit which major version it is working +with. It can be done by namespacing the API or by accepting an explicit +major version as an argument. The preferred approach depends on how +different the major versions of an API are. + +Using Python as an example, either + +.. code-block:: python + + import catsclient + client = catsclient.v1.get_client() + +or + +.. code-block:: python + + import catsclient + client = catsclient.get_client(1) + +Supported versions +~~~~~~~~~~~~~~~~~~ + +It's highly recommended to provide a way to query the server for the +supported version range: + +.. code-block:: python + + import catsclient + + client = catsclient.v1.get_client() + min_version, max_version = client.supported_api_versions() + + cat = client.get_cat('fluffy') # executed with no explicit version + if max_version >= (1, 42): + cat.bark(api_version='1.42') # executed with 1.42 + else: + # no support for 1.42, falling back to older behavior + cat.meow() # executed with no explicit version + +Minimum version +~~~~~~~~~~~~~~~ + +Applications often have a base minimum API version they are capable of working +with. It is recommended to provide a way to accept such version and use it +as a default when no explicit version is provided: + +.. code-block:: python + + import catsclient + + try: + client = catsclient.v1.get_client(api_version='1.2') + except catsclient.IncompatibleApiVersion: + sys.exit("Cat API version 1.2 is not supported") + + cat = client.get_cat('fluffy') # executed with version 1.2 + try: + cat.bark(api_version='1.42') # executed with 1.42 + except catsclient.IncompatibleApiVersion: + # no support for 1.42, falling back to older behavior + cat.meow() # executed with version 1.2 + +As in this example, an SDK using this approach must provide a clear way to +indicate that the requested version is not supported and do it as early as +possible. + +List of versions +~~~~~~~~~~~~~~~~ + +As a simplification extension, a language binding may accept a list of versions +as a base version. The highest version supported by the server must be picked +and used as a default. + +.. code-block:: python + + import catsclient + + try: + client = catsclient.v1.get_client(api_version=['1.0', '1.42']) + except catsclient.IncompatibleApiVersion: + sys.exit("Neither Cat API 1.0 nor 1.42 is supported") + + cat = client.get_cat('fluffy') # executed with either 1.0 or 1.42 + # whichever is available + if client.current_api_version == (1, 42): + # Here we know that the negotiated version is 1.42 + cat.bark() # executes with 1.42 + else: + # Here we know that the negotiated version is 1.0 + cat.meow() # executes with 1.0 + + # The default version can still be overwritten + try: + cat.drink(catsclient.MILK, api_version='1.66') # executed with 1.66 + except catsclient.IncompatibleApiVersion: + # no support for 1.66, falling back to older behavior + cat.drink() # executed with either 1.0 or 1.42 whichever is available