版本发现¶
关于 API 可发现性 的主题文档描述了 REST 服务如何暴露版本发现信息。然而,由于存在七年的历史,早于该文档的出现,因此在实际环境中存在一些非最优的设置。本文档描述了正确使用 OpenStack 版本发现的完整算法。该算法的目的是,对于完全实现 API 可发现性 指南的所有云,通过系统的路径应该是最高效的,但对于尚未…的系统,该过程会优雅地降级,最终降级到与“仅使用目录中的内容”方法行为相同。
注意
本文档包含引用处理在实际环境中遇到的一切形式的内容。在不分散其余描述的情况下,会注意指出哪种形式是首选形式,哪些形式是出于遗留原因支持的。本文档中对某种形式的提及不应被视为认可。数据的首选形式的定义可以在其他文档中找到。
版本发现算法¶
版本发现算法是 使用服务目录 的一部分。它的输入参数和返回值是 用户请求 中描述的输入参数和返回值的子集。目前预计 {catalog-endpoint} 已经从服务目录或直接从 {endpoint-override} 知道。
算法如下
如果用户省略了
{endpoint-version},请遵循 用户省略的 API 版本。使用 推断版本 过程从
{catalog-endpoint}推断{found-endpoint-version}。如果
{found-endpoint-version}存在且{fetch-version-information}为 false,则停止。将{catalog-endpoint}作为{service-endpoint}返回。如果 推断版本 过程返回错误,则
{catalog-endpoint}不匹配{endpoint-version}。尝试 查找文档。注意
如果实现了 API 可发现性 指南,则始终存在
{discovery-document}。如果无法找到
{discovery-document}且{be-strict}为 true,则停止。返回一个错误,表明版本发现失败。确定
{single-or-multiple}的{discovery-document}(参见 单个或多个版本文档)。注意
如果实现了 API 可发现性 指南,
{single-or-multiple}将始终是multiple。
此时,存在四种可能性
如果
{endpoint-version}是latest且{single-or-multiple}是single,请遵循 最新单个版本。如果
{endpoint-version}是latest且{single-or-multiple}是multiple,请遵循 最新多个版本。如果
{endpoint-version}是一个版本且{single-or-multiple}是single,请遵循 请求的单个版本。如果
{endpoint-version}是一个版本且{single-or-multiple}是multiple,请遵循 请求的多个版本。
用户省略的 API 版本¶
如果用户省略了 API 版本,则用户表示他们希望使用 {catalog-endpoint} 作为他们的 {service-endpoint}。仅运行发现以查找该端点的版本信息。
{service-endpoint}是{catalog-endpoint}。如果
{fetch-version-information}为 false,则停止。从{service-endpoint}推断{found-endpoint-version}。(参见 推断版本)在
{service-endpoint}处检索{discovery-document}。如果找到
{discovery-document},则停止。返回其中的{endpoint-information}(参见 返回信息)。如果没有
{discovery-document},则尝试 查找文档。如果没有
{discovery-document},则停止。从{service-endpoint}推断{found-endpoint-version}。(参见 推断版本)确定
{discovery-document}的{single-or-multiple}是single还是multiple(参见 单个或多个版本文档)。如果
{single-or-multiple}是single,则停止。返回其中的{endpoint-information}(参见 返回信息)。如果
{single-or-multiple}是multiple,则在{discovery-document}中找到与{service-endpoint}匹配的{endpoint-information}(参见 匹配端点)。如果没有
{endpoint-information},则停止。从{catalog-endpoint}推断{found-endpoint-version}。(参见 推断版本)停止。返回
{endpoint-information}中的信息(参见 返回信息)。
查找文档¶
在某些情况下,{discovery-endpoint} 要么不会返回文档,要么不会返回我们想要的文档,因此我们需要寻找一个新的文档。
无版本文档始终优于有版本文档,因为无版本文档提供了可能的版本列表,允许发现过程一次性处理该列表并做出决策。有版本文档仅包含一个版本,因此如果其中的版本与用户的请求不匹配,则必须进行额外的调用。
查找新文档的算法如下
如果存在现有的
{discovery-document}且{single-or-multiple}是multiple,则停止。没有更好的文档了。如果
存在现有的
{discovery-document}{single-or-multiple}是single链接部分中的
collection链接与当前的{discovery-endpoint}不同
将
collection链接处的端点设为新的{discovery-endpoint}并获取新的{discovery-document}。停止。返回新的{discovery-document}。从令牌中获取当前范围的
project_id(如果存在)。如果
{discovery-endpoint}以一个以project_id结尾的路径元素结尾,则删除该路径元素并将结果 URL 设为新的{discovery-endpoint}。如果当前的
{discovery-endpoint}以一个形式为“v[0-9]+(.[0-9]+)?$”的路径元素结尾,则删除该路径元素,但将其保存为{removed-version-path-element}。将结果 URL 设为新的{discovery-endpoint}。如果
{discovery-endpoint}匹配{catalog-endpoint},则停止。返回一个错误报告,没有可用的{discovery-document}。尝试从
{discovery-endpoint}获取{discovery-document}。如果存在,则停止。对其进行规范化(参见 规范化文档)并将其作为{dicovery-document}返回。如果在新的端点找不到新的
{discovery-document},但{removed-version-path-element}中保存了值,则将{removed-version-path-element}附加到{discovery-endpoint}并将结果 URL 设为新的{discovery-endpoint}。尝试从
{discovery-endpoint}获取{discovery-document}。如果存在,则停止。对其进行规范化(参见 规范化文档)并将其作为{dicovery-document}返回。如果找不到文档,则返回一个错误报告,没有可用的
{discovery-document}。
例如
# Given a discovery document from the cloud
original_document = {
"version": {
"status": "SUPPORTED",
"id": "v2.0",
"links": [
{
"href": "http://compute.example.com/v2/",
"rel": "self"
},
{
"href": "http://compute.example.com/",
"rel": "collection"
}
]
}
}
# It is a single version document
single_or_multiple = 'single'
# We apply the normalization process
normalized_document = {
"versions": [
{
"status": "SUPPORTED",
"id": "v2.0",
"min_version": "",
"max_version": "",
"links": [
{
"href": "http://compute.example.com/v2/",
"rel": "self"
},
{
"href": "http://compute.example.com/",
"rel": "collection"
}
]
}
]
}
# We see that a collection link exists, so we'll use it as the new discovery
# endpoint.
discovery_endpoint = "http://compute.example.com/"
# We fetch the document from that endpoint and normalize it.
normalized_better_discovery_document = {
"versions": [
{
"status": "SUPPORTED",
"links": [
{
"href": "http://compute.example.com/v2/",
"rel": "self"
}
],
"min_version": "",
"max_version": "",
"id": "v2.0"
}, {
"status": "CURRENT",
"links": [
{
"href": "http://compute.example.com/v2.1/",
"rel": "self"
}
],
"min_version": "2.1",
"max_version": "2.38",
"id": "v2.1"
}
]
}
# single-or-multiple is multiple, so it's better
return normalized_better_discovery_document
带有 project_id 的示例
# The user has requested service-type=file-storage
# The user's token reports the project_id
project_id = '45f0034e8c5a4ef4895b5a87b6b57def'
# The service-catalog contains an entry for filestorage
catalog_endpoint = 'https://file-storage.example.com/v2/45f0034e8c5a4ef4895b5a87b6b57def'
# The catalog_endpoint ends with the user's project_id, so we pop it.
new_endpoint = 'https://file-storage.example.com/v2'
# Fetch the document, normalize it and return it
return {
"versions": [
{
"status": "CURRENT",
"id": "v2.0",
"links": [
{
"href": "http://file-storage.example.com/v2/",
"rel": "self"
},
{
"href": "http://file-storage.example.com/",
"rel": "collection"
}
]
}
]
}
更病态的示例
# The user has requested service-type=file-storage
# The user's token reports the project_id
project_id = '45f0034e8c5a4ef4895b5a87b6b57def'
catalog_endpoint = 'https://file-storage.example.com/v2/45f0034e8c5a4ef4895b5a87b6b57def'
# The catalog_endpoint ends with the user's project_id, so we pop it.
discovery_endpoint = 'https://file-storage.example.com/v2'
# We try to fetch https://file-storage.example.com/v2 but it returns an error
# Pop version string from the endpoint
new_discovery_endpoint = 'https://file-storage.example.com/'
# Fetch the document, normalize and return it
return {
"versions": [
{
"status": "SUPPORTED",
"links": [
{
"href": "http://file-storage.example.com/v1/",
"rel": "self"
}
],
"min_version": "",
"max_version": "",
"id": "v1.0"
},
{
"status": "CURRENT",
"links": [
{
"href": "http://file-storage.example.com/v2/",
"rel": "self"
}
],
"min_version": "2.0",
"max_version": "2.22",
"id": "v2.0"
}
]
}
推断版本¶
在大多数情况下,{service-endpoint} 的版本应该可以从 {discovery-document} 中检索到,并且在这些情况下,它应该被认为是 {service-endpoint} 处的服务的版本。在某些情况下,找不到与问题中的 {service-endpoint} 对应的发现文档。或者,在某些情况下,{catalog-endpoint} 包含版本信息,并且用户没有寻找微版本信息。
当使用此过程时,微版本信息将始终为空。
推断版本的算法如下
如果存在,则从令牌中获取当前范围的
project_id。如果端点以一个以
project_id结尾的路径元素结尾,则删除它。如果端点以形式为
^v[0-9]+(\.[0-9]+)?$的路径元素结尾,则去除v并将剩余部分用作{found-endpoint-version}。如果端点不包含版本元素,则无法推断版本。返回
{found-endpoint-version}的空值。如果提供了
{endpoint-version}且与{found-endpoint-version}不匹配,则停止。返回一个错误,说明用户请求了一个版本,并且从 URL 推断的版本不匹配。返回
{found-endpoint-version}。
例如
catalog_endpoint = 'https://file-storage.example.com/v2/45f0034e8c5a4ef4895b5a87b6b57def'
# Match path elements - /v2/ matches ...
found_api_version = '2'
catalog_endpoint = 'https://identity-storage.example.com/'
# Match path elements - no matches
found_api_version = None
catalog_endpoint = 'https://object-store.example.com/v1/AUTH_622b11a1-5dfa-43b4-9f58-4ad3c6dbc4a0'
# Match path elements - /v1/ matches ...
found_api_version = '1'
catalog_endpoint = 'https://compute.example.com/v2.1'
# Match path elements - /v2.1/ matches ...
found_api_version = '2.1'
匹配端点¶
如果 {single-or-multiple} 是 multiple 并且发现算法选择回退到目录提供的端点,则应找到一个与目录 URL 匹配的 URL,以便可以提取版本。
使用版本比较按降序对
{discovery-document}中的端点按id排序。对于列表中的每个端点,对其进行扩展(参见 扩展端点)并将其与目录端点进行比较。第一个匹配的端点是赢家。
例如
catalog_endpoint = 'https://file-storage.example.com/v2/45f0034e8c5a4ef4895b5a87b6b57def'
discovery_document = {
"versions": [
{
"status": "CURRENT",
"id": "v2.0",
"links": [
{
"href": "http://file-storage.example.com/v2/",
"rel": "self"
}
],
}
]
}
# Expand endpoint http://file-storage.example.com/v2/
expanded_endpoint = "https://file-storage.example.com/v2/45f0034e8c5a4ef4895b5a87b6b57def"
# expanded_endpoint matches catalog_endpoint - id v2.0 is the match
扩展端点¶
发现文档中的端点可以是相对路径,也可能以已知方式存在错误。在使用发现文档中的端点之前,必须对其进行扩展。算法如下
将发现文档中的端点与获取发现文档的端点连接起来。如果文档中的端点是绝对 URL,则结果应为文档中的端点保持不变。如果文档中的端点是相对路径,则应根据常规相对 URL 规则将其附加到获取文档的端点。Python 模块
six.moves.urllib.parse.urljoin是一个行为符合预期的 URL 连接实现的示例。替换发现文档中端点的
scheme和host,使用其获取的端点的scheme和host。这是为了解决在实际环境中看到的旧版存在错误的发现文档的问题。例如
def replace_scheme(endpoint, discovery_url):
parsed_endpoint = urllib.parse.urlparse(endpoint)
parsed_discovery_url = urllib.parse.urlparse(discovery_url)
return urllib.parse.ParseResult(
parsed_discovery_url.scheme,
parsed_discovery_url.netloc,
parsed_endpoint.path,
parsed_endpoint.params,
parsed_endpoint.query,
parsed_endpoint.fragment).geturl()
如果存在,则从令牌中获取当前范围的
project_id。如果
{catalog-endpoint}结尾的路径元素以project_id结尾,但端点没有,则将{catalog-endpoint}路径的最后一个元素附加到端点的末尾。
注意
有些服务会在其端点中的 project_id 前面加上一个字符串,因此仅仅将 project_id 附加到 catalog-endpoint 并不足以解决问题。
例如
project_id = '45f0034e8c5a4ef4895b5a87b6b57def'
catalog_endpoint = 'https://file-storage.example.com/v2/45f0034e8c5a4ef4895b5a87b6b57def'
discovery_document = {
"versions": [
{
"status": "CURRENT",
"id": "v2.0",
"links": [
{
"href": "/v2.0",
"rel": "self"
}
]
}
]
}
# Pop project_id from catalog_endpoint
shortened_catalog_endpoint = 'https://file-storage.example.com/v2'
# Apply URL join to https://file-storage.example.com/v2 and /v2.0
joined_endpoint = 'https://file-storage.example.com/v2.0'
# catalog_endpoint ends with project_id, append project_id
service_endpoint = 'http://file-storage.example.com/v2.0/45f0034e8c5a4ef4895b5a87b6b57def'
使用发现中损坏的服务端点
project_id = '45f0034e8c5a4ef4895b5a87b6b57def'
catalog_endpoint = 'https://file-storage.example.com/v2/45f0034e8c5a4ef4895b5a87b6b57def'
# This discovery_document is the result of a service with a broken
# configuration. Obviously the service is not on "localhost". Similarly,
# since the discovery endpoint is an https endpoint, it can be assumed
# that the actual service endpoint is https.
discovery_document = {
"versions": [
{
"status": "CURRENT",
"id": "v2.0",
"links": [
{
"href": "https:///v2.0",
"rel": "self"
}
],
}
]
}
# Pop project_id from catalog_endpoint
shortened_catalog_endpoint = 'https://file-storage.example.com/v2'
# Apply URL join to https://file-storage.example.com/v2 and
# https:///v2.0 - endpoint from discovery is absolute
joined_endpoint = 'https:///v2.0'
# Replace scheme and host from https://file-storage.example.com/v2
joined_endpoint = 'https://file-storage.example.com/v2.0'
# catalog_endpoint ends with project_id, append project_id
service_endpoint = 'http://file-storage.example.com/v2.0/45f0034e8c5a4ef4895b5a87b6b57def'
单个或多个版本文档¶
即使将版本文档按照 标准化文档 的方式标准化为 API 可发现性 中描述的形式,仍然重要的是要知道文档是否列出了所有可用版本,或者只是更大集合中的单个版本。由于也可能只有一个版本,仅仅查看列表的长度是不够的。
注意
一旦所有服务都实现了 API 可发现性 中的所有建议,将永远不会存在包含更大集合中单个版本的文档,因此不需要此逻辑。但是,该逻辑与期望的未来状态是向上兼容的。
为了应用发现算法,必须检测文档的类型。
如果文档在
links列表中包含一个rel为collection的链接描述,并且该链接的href与rel为self的链接的href不同,那么它就是单个版本文档。否则,它就是多个版本文档,可以依赖它包含完整的可用版本集合。
注意
TODO(mordred) 添加示例
标准化文档¶
注意
如果 API-SIG 在 API 可发现性 中的建议得以实施,则可以跳过本节中的所有逻辑。
除了 API 可发现性 中描述的首选形式之外,还存在三种现有的版本发现文档形式。为了合理地应用算法,应将获取的文档标准化,使其与 API 可发现性 对齐。
注意
实际上,标准化不需要在客户端或库中进行。此处描述是为了简化本文档的其他部分,并能够以正确的文档格式描述该过程。
如果文档具有一个名为
versions的键,该键包含一个名为values的键,则将values中包含的列表移动到versions下方。然后,该列表就是版本对象列表。
例如
{
"versions": {
"values": [
{
"status": "stable",
"updated": "2016-10-06T00:00:00Z",
"id": "v3.7",
"links": [
{
"href": "https://auth.example.com/v3/",
"rel": "self"
}
]
},
{
"status": "deprecated",
"updated": "2016-08-04T00:00:00Z",
"id": "v2.0",
"links": [
{
"href": "https://auth.example.com/v2.0/",
"rel": "self"
}
]
}
]
}
}
变为
{
"versions": [
{
"status": "stable",
"updated": "2016-10-06T00:00:00Z",
"id": "v3.7",
"links": [
{
"href": "https://auth.example.com/v3/",
"rel": "self"
}
]
},
{
"status": "deprecated",
"updated": "2016-08-04T00:00:00Z",
"id": "v2.0",
"links": [
{
"href": "https://auth.example.com/v2.0/",
"rel": "self"
}
]
}
]
}
如果文档具有一个名为
id的键,则创建一个名为version的键,并将所有值放在其中。
例如
{
"status": "CURRENT",
"id": "v2.0",
"links": [
{
"href": "http://network.example.com/v2.0",
"rel": "self"
}
]
}
变为
{
"version": {
"status": "CURRENT",
"id": "v2.0",
"links": [
{
"href": "http://network.example.com/v2.0",
"rel": "self"
}
]
}
}
如果文档具有一个名为
version的键(即使您刚刚创建了它),请在链接列表中查找一个collection链接。如果不存在,则从self链接中获取href。如果self链接以“v[0-9]+(.[0-9]+)?$”形式的版本字符串结尾,则从端点的末尾删除该版本字符串,并将一个collection条目添加到links列表中,其中包含生成的端点。
例如
{
"version": {
"status": "CURRENT",
"id": "v2.0",
"links": [
{
"href": "http://network.example.com/v2.0",
"rel": "self"
}
]
}
}
变为
{
"version": {
"status": "CURRENT",
"id": "v2.0",
"links": [
{
"href": "http://network.example.com/v2.0",
"rel": "self"
},
{
"href": "http://network.example.com/",
"rel": "collection"
}
]
}
}
如果文档具有一个名为
version的键,则创建一个名为versions的顶级键,其中包含一个列表。将version的内容移动到versions列表中的一个字典中,并删除顶级键version。
例如
{
"version": {
"status": "CURRENT",
"id": "v2.0",
"links": [
{
"href": "http://network.example.com/v2.0",
"rel": "self"
},
{
"href": "http://network.example.com/",
"rel": "collection"
}
]
}
}
变为
{
"versions": [
{
"status": "CURRENT",
"id": "v2.0",
"links": [
{
"href": "http://network.example.com/v2.0",
"rel": "self"
},
{
"href": "http://network.example.com/",
"rel": "collection"
}
]
}
]
}
对于 versions 列表中的每个版本对象
可以忽略或删除除
id、version、min_version、max_version、status和links之外的键。将
status字段中的值转换为大写。如果
status是STABLE,则将其更改为CURRENT。(处理 keystone)如果存在
version字段但没有max_version字段,则创建一个max_version字段,其值为version字段中的值。(处理 nova、cinder、manila 和 ironic 微版本)links键应包含一个列表,并且该列表应包含一个rel等于self的字典,并且可能包含另一个rel等于collection的字典。可以丢弃任何其他条目。
以下是一些完整的标准化示例。
原始文档
{
"versions": [
{
"status": "stable",
"updated": "2016-10-06T00:00:00Z",
"id": "v3.7",
"links": [
{
"href": "https://auth.example.com/v3/",
"rel": "self"
}
]
},
{
"status": "deprecated",
"updated": "2016-08-04T00:00:00Z",
"id": "v2.0",
"links": [
{
"href": "https://auth.example.com/v2.0/",
"rel": "self"
}
]
}
]
}
变为
{
"versions": [
{
"status": "CURRENT",
"id": "v3.7",
"links": [
{
"href": "https://auth.example.com/v3/",
"rel": "self"
}
]
},
{
"status": "DEPRECATED",
"id": "v2.0",
"links": [
{
"href": "https://auth.example.com/v2.0/",
"rel": "self"
}
]
}
]
}
原始文档
{
"versions": [
{
"status": "SUPPORTED",
"updated": "2011-01-21T11:33:21Z",
"links": [
{
"href": "http://compute.example.com/v2/",
"rel": "self"
}
],
"min_version": "",
"version": "",
"id": "v2.0"
},
{
"status": "CURRENT",
"updated": "2013-07-23T11:33:21Z",
"links": [
{
"href": "http://compute.example.com/v2.1/",
"rel": "self"
}
],
"min_version": "2.1",
"version": "2.38",
"id": "v2.1"
}
]
}
变为
{
"versions": [
{
"status": "SUPPORTED",
"links": [
{
"href": "http://compute.example.com/v2/",
"rel": "self"
}
],
"min_version": "",
"max_version": "",
"id": "v2.0"
},
{
"status": "CURRENT",
"links": [
{
"href": "http://compute.example.com/v2.1/",
"rel": "self"
}
],
"min_version": "2.1",
"max_version": "2.38",
"id": "v2.1"
}
]
}
查找匹配版本¶
通过将 {endpoint-version} 与描述的 id 字段进行比较,来查找一个版本列表,以找到 {candidate-endpoints} 列表(参见 比较主要版本)。
如果匹配请求的 {endpoint-version} 的 {id} 超过一个,并且其中一个的 status 为 CURRENT,则应返回它。
如果匹配请求的 {endpoint-version} 的 {id} 超过一个,并且没有一个的 status 为 CURRENT,则应返回最高的版本。
如果匹配请求的 {endpoint-version} 的 {id} 超过一个,并且多个版本的 status 为 CURRENT,则应返回最高的版本。
最新的单个版本¶
{endpoint-version} 是 latest,并且 {single-or-multiple} 是 single。
如果
{discovery-document}中的status是CURRENT,则停止。返回{discovery-document}中的{endpoint-information}(参见 返回信息)。尝试 查找文档
如果有一个新的
{discovery-document},则确定{single-or-multiple}是single还是multiple(参见 单个或多个版本文档)。如果新的
{single-or-multiple}是multiple,则遵循 最新的多个版本。如果新的
{single-or-multiple}是single,或者没有新的{discovery-document},则停止。返回{discovery-document}中的{endpoint-information}(参见 返回信息)。
最新的多个版本¶
{endpoint-version} 是 latest,并且 {single-or-multiple} 是 multiple。
请求的单个版本¶
{endpoint-version} 是一个版本或范围,并且 {single-or-multiple} 是 single。
通过遵循 查找匹配版本,检查
{discovery-document}中的版本是否与{endpoint-version}匹配。在
{discovery-document}中找到一个与{endpoint-version}匹配的{endpoint-information}。(参见 查找匹配版本)如果找到
{endpoint-information},则停止。返回{endpoint-information}中的信息(参见 返回信息)。如果版本不匹配,则尝试 查找文档。
如果有一个新的
{discovery-document},则确定{single-or-multiple}是single还是multiple(参见 单个或多个版本文档)。如果
{single-or-multiple}是multiple,则遵循 请求的多个版本。如果没有新的
{discovery-document},则停止。返回一个错误,告诉用户他们请求的版本未找到。在错误中包含找到的版本。
请求的多个版本¶
{endpoint-version} 是一个版本或范围,并且 {single-or-multiple} 是 multiple。
在
{discovery-document}中找到一个匹配的{endpoint-information}(参见 查找匹配版本)如果找到
{endpoint-information},则停止。返回{endpoint-information}中的信息(参见 返回信息)。如果未找到匹配的
{endpoint-information}并且{be-strict}是True,则停止。返回一个错误,告诉用户他们请求的版本未找到。在错误中包含找到的版本列表。如果未找到匹配的
{endpoint-information}并且{be-strict}是 False,则使用{catalog-endpoint}作为{service-endpoint}。找到文档中与{catalog-endpoint}匹配的{endpoint-information}并使用它。(参见 匹配端点)。如果没有
{endpoint-information},则停止。从{service-endpoint}推断{found-endpoint-version}(参见 推断版本)。停止。返回
{endpoint-information}中的信息(参见 返回信息)。
查找最新版本¶
如果列表中某个版本的 status 为 CURRENT,则使用它。
否则,选择 id 最高的版本,排除任何 status 为 EXPERIMENTAL 或 DEPRECATED 的版本,并使用版本比较(而非词法排序)进行排序。
返回信息¶
当端点信息被选中后,以以下方式返回信息
去除
{id}开头的 “v”,并将其作为{found-endpoint-version}返回。扩展
links中rel为self的条目的href,并将其作为{service-endpoint}返回(参见 扩展端点)。如果存在,则返回
{min-version}和{max-version}。