REST API 微版本支持

https://blueprints.launchpad.net/nova/+spec/api-microversions

我们需要一种方法来引入对 REST API 的更改,以修复错误和添加新功能。其中一些更改与旧版本不兼容,而我们目前没有办法做到这一点。

问题描述

作为一个社区,我们非常擅长通过增量开发来不断发展接口和代码。我们不太擅长一次性的大规模代码更新。Nova API 已经变得足够大,并且通过新的扩展不断增长,因此它可能永远无法进行 API 的主要版本更新,因为这会对用户产生影响,并且开发者需要支持多种实现,这会增加开销。

与此同时,我们允许通过添加扩展来创新 API 的现状,已经发展到我们现在拥有扩展的扩展,假设扩展列表是版本控制的一种简陋机制。这导致了大量的技术债务。它阻止我们进行某些更改,例如弃用当前无意义或损坏的 API 部分。或者修复其他领域,增量开发导致 API 中不一致之处,这会让新用户感到困惑。

我们必须提出一种更好的方法,以满足以下需求

  • 能够以增量方式发展 API,这是我们作为社区的优势。

  • 为 REST API 的用户提供向后兼容性。

  • 使代码更简洁,从而降低我们在扩展或修改 API 时出错的可能性。

一个优秀的接口会竭尽所能地使其难以错误使用。一个好的接口会努力成为一个优秀的接口,但会屈服于现实情况。

用例

  • 允许开发者以向后兼容的方式修改 Nova API,并动态地向 API 用户发出更改可用信号,而无需创建新的 API 扩展。

  • 允许开发者以不向后兼容的方式修改 Nova API,同时仍然支持旧行为。REST API 的用户能够在每次请求的基础上决定是希望 Nova API 以新方式还是旧方式运行。部署者能够在支持开发者提供支持的前提下,提供新的不向后兼容的功能,而无需删除对先前行为的支持。

  • REST API 的用户能够在每次请求的基础上决定他们想要使用的 API 版本(假设部署者支持他们想要的版本)。

项目优先级

kilo 优先级列表当前未定义。但是,根据目前拟议的优先级列表,它将属于“用户体验”,因为它显著提高了我们改进 Nova API 的能力。

提议的变更

设计优先级

  • 最终用户将如何使用它,以及我们如何使其难以错误使用

  • 代码将如何内部构建。我们如何使其

    • 易于在代码中看到您即将破坏 API 兼容性。

    • 易于进行向后兼容的更改

    • 能够进行不向后兼容的更改

    • 最大限度地减少代码重复,以最大限度地减少维护开销

  • 我们将如何进行测试,包括单元测试和集成测试。这会施加什么限制。

版本控制

为了本次讨论的目的,“API”是 Nova 树中的所有核心和可选扩展。

API 的版本控制应为单个单调递增计数器。它将采用 X.Y 的形式,并遵循以下约定

  • 如果对 API 整体产生重大的不向后兼容的更改,则仅更改 X。也就是说,很少很少增加一次。

  • Y 当您对 API 进行任何更改时。请注意,这包括语义更改,这些更改可能不会影响输入或输出格式,甚至不会源自 API 代码层。我们在版本控制系统中不区分向后兼容和不向后兼容的更改。但是,将在文档中明确说明哪些是向后兼容的更改,哪些是不向后兼容的更改。

请注意,API 中类似的更改组不会在单个版本更新下进行。这将最大限度地减少对用户的影响,因为他们可以控制想要公开的更改。

向后兼容的更改定义为 OpenStack API 更改指南允许的更改

https://wiki.openstack.org/wiki/APIChangeGuidelines

版本响应如下所示

GET /
{
     "versions": [
        {
            "id": "v2.1",
            "links": [
                  {
                    "href": "https://:8774/v2/",
                    "rel": "self"
                }
            ],
            "status": "CURRENT",
            "version": "5.2"
            "min_version": "2.1"
        },
   ]
}

这指定了服务器可以理解的最小和最大版本。min_version 将从 2.1 开始,代表 v2.1 API(等效于 V2.0 API,但支持 XML)。如果存在我们认为不足以支持的支持负担,它最终可能会增加。

客户端交互

客户端通过以下方法指定他们想要使用的 API 版本,即一个新的标头

X-OpenStack-Nova-API-Version: 2.114

从概念上讲,这就像 accept 标头一样。这是一个全局 API 版本。

从语义上讲,这意味着

  • 如果未提供 X-OpenStack-Nova-API-Version,则表现得好像发送了 min_version。

  • 如果发送了 X-OpenStack-Nova-API-Version,则以该版本响应 API。如果超出支持的版本范围,则返回 406 Not Acceptable。

  • 如果 X-OpenStack-Nova-API-Version:latest(特殊关键字),则返回 API 的 max_version。

这意味着开箱即用,使用旧客户端,OpenStack 安装将返回 v2 的纯 OpenStack 响应。用户或 SDK 必须要求不同的内容才能获得新功能。

始终在响应中返回两个额外的标头

X-OpenStack-Nova-API-Version:version_number,experimental Vary:X-OpenStack-Nova-API-Version

第一个标头指定执行的 API 的版本号。如果操作员对 API 行为进行了非标准的修改,则仅返回 Experimental。这仅用作过渡机制,直到一些云操作员使用的功能被上游化,并且将在少数几个版本中删除。

第二个标头用作缓存代理的提示,表明响应也依赖于 X-Openstack-Compute-API-Version,而不仅仅是主体和查询参数。有关详细信息,请参阅 RFC 2616 第 14.44 节。

实现设计细节

在每次请求中,X-OpenStack-Nova-API-Version 标头字符串将在 wsgi 代码中转换为 APIVersionRequest 对象。路由将以通常的方式进行,并将版本对象附加到请求对象(所有 API 方法都期望该对象)。API 方法然后可以使用它来确定其对传入请求的行为。

我们需要支持的更改类型

* Status code changes (success and error codes)
* Allowable body parameters (affects input validation schemas too)
* Allowable url parameters
* General semantic changes
* Data returned in response
* Removal of resources in the API
* Removal of fields in a response object or changing the layout of the response

注意:此列表并非详尽无遗

在控制器案例中,可以使用装饰器标记方法,以指示它们实现哪些 API 版本。例如

@api_version(min_version='2.1', max_version='2.9')
def show(self, req, id):
   pass

@api_version(min_version='3.0')
def show(self, req, id):
   pass

对 API 版本 2.2 的传入请求将执行第一种方法,而对 API 版本 3.1 的传入请求将执行第二种方法。

版本对象会传递到与请求对象关联的方法,因此也可以在方法中进行非常具体的检查。例如

def show(self, req, id):
  .... stuff ....

  if req.ver_obj.matches(start_version, end_version):
    .... Do version specific stuff ....

  ....  stuff ....

请注意,end_version 是可选的,在这种情况下,它将匹配大于或等于 start_version 的任何版本。

这里有一些原型代码,解释了它是如何工作的

https://github.com/cyeoh/microversions_poc

验证模式装饰器还需要扩展以支持版本控制

@validation.schema(schema_definition, min_version, max_version)

请注意,min_version 和 max_version 都是可选参数。

可以通过指定 max_version 从 API 中删除方法、扩展或请求或响应中的字段

@api_version(min_version='2.1', max_version='2.9')
def show(self, req, id):
  ....  stuff ....

如果客户端发出对版本 2.11 的请求,则客户端将收到 404,就好像该方法根本不存在一样。如果 API 的最小版本提高到 2.10,则可以删除扩展本身。

API 的最小版本将仅由 Nova 开发者达成共识来提高,他们有维护向后兼容性的开销,以及希望永远保持向后兼容性的部署者和用户。

由于我们有一个在整个 API 中单调递增的版本号,而不是对单个插件进行版本控制,因此我们将面临与当前 DB 迁移更改集相同的潜在合并冲突。抱歉,我不相信有任何方法可以避免这种情况,但欢迎提出任何建议!

客户端期望

与支持版本协商的系统一样,使用此 API 的健壮客户端还需要支持一定范围的版本,否则该客户端将无法用于与多个云通信的软件中。

具体示例是 OpenStack Infra 中的 nodepool。假设有一个世界,它定期连接到 4 个公共云。它们的状态如下

- Cloud A:
  - min_ver: 2.100
  - max_ver: 2.300
- Cloud B:
  - min_ver: 2.200
  - max_ver: 2.450
- Cloud C:
  - min_ver: 2.300
  - max_ver: 2.600
- Cloud D:
  - min_ver: 2.400
  - max_ver: 2.800

没有一个版本的 API 可用在所有这些云中,这基于其中一些云的古老程度。但是,在客户端 SDK 中,某些基本功能(如启动)将存在,尽管根据 API 的版本可能会获得不同的附加数据。客户端应尽可能平滑这些差异。

现实地说,这今天就存在一个问题,只是没有基础设施来支持创建解决方案来解决它。

备选方案

另一种选择是立即进行所有不向后兼容的更改并进行主要的 API 发布。例如,将 url 前缀更改为 /v3 而不是 /v2。然后支持这两种实现很长时间。过去已经拒绝这种方法,因为担心维护开销。

数据模型影响

REST API 影响

如上所述,将附加版本信息添加到 GET /。这些应该是向后兼容的更改,而且我怀疑实际上没有人真正使用这些信息。

否则,除非提供请求中描述的客户端标头,否则不会进行任何更改。

安全影响

通知影响

其他最终用户影响

SDK 作者需要开始使用 X-OpenStack-Nova-API-Version 标头来访问新功能。只有在新版本中添加新功能这一事实才会鼓励他们这样做。

python-novaclient 处于相同的情况,需要更新以支持新的标头才能支持新的 API 功能。

性能影响

其他部署者影响

开发人员影响

这显然会影响 Nova 开发者修改 REST API 代码和添加新扩展的方式。

常见问题解答

  • 添加新插件会更改版本号吗? 是的。

  • 错误状态码更改时,我们是否会增加版本号? 是的,这是一个 API 更改。

实现

负责人

主要负责人

cyeoh-0

其他贡献者

<launchpad-id 或 None>

工作项

  • 实现 APIVersions 类

  • 实现处理 X-OpenStack-Nova-API-Version 标头

  • 实现基于版本标头调用的方法路由。

  • 找到并实现需要微版本更新的第一个 API 更改。

依赖项

  • 这取决于 v2.1 v2-on-v3-api 规范的完成。

  • 任何希望对 API 进行不向后兼容的更改的 nova 规范(例如任务 api 规范)都依赖于此更改。同样,任何希望对 v2.1 API 进行任何 API 更改而无需添加虚拟扩展的规范也依赖于此更改。

  • JSON-Home 与此相关,但它们提供不同的服务。微版本允许客户端控制他们暴露于哪个版本的 API,而 JSON-Home 描述了该 API,从而实现资源发现。

测试

Tempest 无法测试微版本支持的所有可能的 API 组合。我们将不得不选择代表已实现内容的特定版本。现有的 Nova tempest 测试将作为未来 API 版本测试的基线。

文档影响

长期目标是至少部分自动化地使用当前的 json schema 支持和未来的 JSON-Home 支持来生成 API 文档。这个问题与此规范相当正交。

参考资料