Add guideline on exposing microversions in SDKs
Change-Id: I69651d9df572bb33ea4fb413c57a4c3d15c98d7e
This commit is contained in:
parent
9fd03c71c7
commit
b0903d3b0e
291
guidelines/sdk-exposing-microversions.rst
Normal file
291
guidelines/sdk-exposing-microversions.rst
Normal file
@ -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
|
||||||
|
<microversion_specification>` 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-<service-name>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<NUMBER>`` 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<ActualDataType>`` 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
|
Loading…
x
Reference in New Issue
Block a user