本文档面向想要更改现有 API 的开发人员。可以在 API约定中找到一组适用于新 API 和更改的 API 约定。
目录
- 所以你想改变 API?
- 运营概况
- 关于兼容性
- 向后兼容性问题
- 不兼容的 API 更改
- 更改版本化 API
- 改变内部结构
- 编辑validation.go
- 编辑版本转换
- 生成代码
- 创建新的 API 版本
- 创建新的 API 组
- 更新模糊测试器
- 更新语义比较
- 实施你的改变
- 编写端到端测试
- 示例和文档
- Alpha、Beta 和稳定版本
所以你想改变 API?
在尝试更改 API 之前,您应该熟悉一些现有的 API 类型和API 约定。如果创建新的 API 类型/资源,我们还建议您首先发送仅包含新 API 类型提案的 PR。
Kubernetes API 有两个主要组成部分:内部结构和版本化 API。版本化 API 旨在保持稳定,而内部结构的实现则最好地反映 Kubernetes 代码本身的需求。
对于 API 更改而言,这意味着您必须在处理更改时深思熟虑,并且必须触及多个部分才能完成更改。本文档旨在指导您完成整个过程,但并非所有 API 更改都需要所有这些步骤。
运营概况
为了理解本文档的其余部分,对 Kubernetes 中使用的 API 系统有较高的理解非常重要。
如上所述,API 对象的内部表示与任何一个 API 版本都是分离的。这为代码的演进提供了很大的自由度,但它需要强大的基础架构来在表示之间进行转换。处理 API 操作有多个步骤 – 即使是像 GET 这样简单的操作也涉及大量机制。
转换过程在逻辑上是一个“星型”,内部形式位于中心。每个版本化 API 都可以转换为内部形式(反之亦然),但版本化 API 不会直接转换为其他版本化 API。这听起来像是一个繁重的过程,但实际上我们并不打算同时保留超过少数几个版本。虽然所有 Kubernetes 代码都在内部结构上运行,但它们在写入存储(磁盘或 etcd)或通过线路发送之前始终会转换为版本化形式。客户端应该只使用和操作版本化 API。
为了演示一般过程,这里有一个(假设的)例子:
- 用户将
Pod
对象发布到/api/v7beta1/...
- JSON 被解组为一个
v7beta1.Pod
结构 - 默认值应用于
v7beta1.Pod
- 转换
v7beta1.Pod
为api.Pod
结构 - 已
api.Pod
验证,并将任何错误返回给用户 - 转换
api.Pod
为v6.Pod
(因为 v6 是最新稳定版本) - 被
v6.Pod
编组为 JSON 并写入 etcd
现在我们已经Pod
存储了对象,用户可以在任何受支持的 API 版本中获取该对象。例如:
- 用户
Pod
从/api/v5/...
- JSON 从 etcd 读取并解组为一个
v6.Pod
结构 - 默认值应用于
v6.Pod
- 转换
v6.Pod
为api.Pod
结构 - 转换
api.Pod
为v5.Pod
结构 - 被
v5.Pod
编组为 JSON 并发送给用户
这个过程的含义是 API 的更改必须小心进行并且向后兼容。
关于兼容性
在讨论如何进行 API 更改之前,有必要澄清一下我们所说的 API 兼容性。Kubernetes 将其 API 的前向和后向兼容性视为首要任务。兼容性很难,尤其是处理回滚安全问题。这是每个 API 更改都必须考虑的事情。
如果 API 更改符合以下条件,则视为兼容:
- 添加不需要正确行为的新功能(例如,不添加新的必填字段)
- 不会改变现有的语义,包括:
- 默认值和行为的语义含义
- 现有 API 类型、字段和值的解释
- 哪些字段是必填的,哪些不是
- 可变字段不会变得不可变
- 有效值不会变为无效
- 明确无效的值不会变为有效
换一种方式:
- 任何在您更改之前成功的 API 调用(例如,发布到 REST 端点的结构)在更改之后也必须成功。
- 任何未使用您的更改的 API 调用的行为都必须与更改之前相同。
- 任何使用您的更改的 API 调用在针对不包含您的更改的 API 服务器发出时都不得导致问题(例如崩溃或降低行为)。
- 必须能够往返执行您的更改(转换为不同的 API 版本并转回),并且不会丢失信息。
- 现有客户端不需要知道您的更改,即使您的更改正在使用中,他们仍可以继续像以前一样运行。
- 必须能够回滚到不包含您的更改的 API 服务器的先前版本,并且不会对未使用您的更改的 API 对象产生影响。如果回滚,使用您的更改的 API 对象将受到影响。
如果您的更改不符合这些标准,则将被视为不兼容,并且可能会破坏旧客户端,或导致新客户端导致未定义的行为。此类更改通常是不允许的,但在极端情况下会例外(例如安全性或明显的错误)。
让我们考虑一些例子。
添加字段
在假设的 API 中(假设我们处于版本 v6),该Frobber
结构看起来像这样:
// API v6. type Frobber struct { Height int `json:"height"` Param string `json:"param"` }
你想添加一个新Width
字段。通常允许添加新字段而不更改 API 版本,因此你可以简单地将其更改为:
// Still API v6. type Frobber struct { Height int `json:"height"` Width int `json:"width"` Param string `json:"param"` }
您有责任定义一个合理的默认值,以Width
使上述规则 1 和规则 2 成立 – 以前工作的 API 调用和存储对象必须继续工作。
将单数字段变为复数
对于下一个更改,您希望允许多个Param
值。您不能简单地删除Param string
和添加Params []string
(而不创建全新的 API 版本)——这会导致规则 #1、#2、#3 和 #6 失败。您也不能简单地添加Params []string
并使用它——这会导致规则 #2 和 #6 失败。
您必须定义一个新字段以及该字段与现有字段之间的关系。首先添加新的复数字段:
// Still API v6. type Frobber struct { Height int `json:"height"` Width int `json:"width"` Param string `json:"param"` // the first param Params []string `json:"params"` // all of the params }
这个新字段必须包含单数字段。为了满足兼容性规则,您必须处理版本偏差、多个客户端和回滚的所有情况。这可以通过准入控制或 API 注册表逻辑(例如策略)来处理,这些逻辑将字段与 API 操作中的上下文链接在一起,以尽可能接近用户的意图。
执行任何读取操作时:
- 如果未填充复数,则 API 逻辑必须将复数填充为单元素列表,并将复数[0] 设置为单数值。
执行任何创建操作时:
- 如果仅指定单数字段(例如较旧的客户端),API 逻辑必须将复数填充为单元素列表,并将复数[0]设置为单数值。理由:这是一个旧客户端,它们具有兼容的行为。
- 如果同时指定了单数和复数字段,则 API 逻辑必须验证复数[0] 是否与单数值匹配。
- 任何其他情况都是错误的,必须拒绝。这包括指定复数字段而未指定单数的情况。理由:在更新中,无法区分旧客户端通过补丁清除单数字段和新客户端设置复数字段。为了兼容性,我们必须假设前者,并且我们不希望更新语义与创建语义不同(请参阅下面的单对偶歧义)。
对于上述内容:“已指定”表示该字段存在于用户提供的输入中(包括默认字段)。
执行任何更新操作(包括补丁)时:
- 如果单数被清除而复数没有改变,API 逻辑必须清除复数。理由:这是旧客户端清除它所知道的字段。
- 如果清除了复数而单数没有改变,API 逻辑必须用与旧复数相同的值填充新复数。理由:这是一个旧客户端,无法发送它不知道的字段。
- 如果单数字段已更改(但未清除)且复数字段未更改,则 API 逻辑必须将复数填充为单元素列表,并将复数[0]设置为单数值。理由:这是老客户更改他们知道的字段。
用代码来表达的话,如下所示:
// normalizeParams adjusts Params based on Param. This must not consider
// any other fields.
func normalizeParams(after, before *api.Frobber) {
// Validation will be called on the new object soon enough. All this
// needs to do is try to divine what user meant with these linked fields.
// The below is verbosely written for clarity.
// **** IMPORTANT *****
// As a governing rule. User must either:
// a) Use singular field only (old client)
// b) Use singular *and* plural fields (new client)
if before == nil {
// This was a create operation.
// User specified singular and not plural (an old client), so we can
// init plural for them.
if len(after.Param) > 0 && len(after.Params) == 0 {
after.Params = []string{after.Param}
return
}
// Either both were specified or both were not. Catch this in
// validation.
return
}
// This was an update operation.
// Plural was cleared by an old client which was trying to patch
// some field and didn't provide it.
if len(before.Params) > 0 && len(after.Params) == 0 {
// If singular is unchanged, then it is an old client trying to
// patch, and didn't provide plural. Bring the old value forward.
if before.Param == after.Param {
after.Params = before.Params
}
}
if before.Param != after.Param {
// Singular is changed.
if len(before.Param) > 0 && len(after.Param) == 0 {
// If singular was cleared and plural is unchanged, then we can
// clear plural to match.
if sameStringSlice(before.Params, after.Params) {
after.Params = nil
}
// Else they also changed plural - check it in validation.
} else {
// If singular was changed (but not cleared) and plural was not,
// then we can set plural based on singular (same as create).
if sameStringSlice(before.Params, after.Params) {
after.Params = []string{after.Param}
}
}
}
}
仅知道单数字段的旧客户端将继续成功并产生与更改前相同的结果。较新的客户端可以使用您的更改而不会影响旧客户端。API 服务器可以回滚,并且只有使用您的更改的对象会受到影响。
对 API 进行版本控制并使用与任何一个版本不同的内部类型的部分原因是为了处理此类增长。内部表示可以实现为:
// Internal, soon to be v7beta1. type Frobber struct { Height int Width int Params []string }
转换为/从版本化 API 转换的代码可以将其解码为兼容结构。最终,将分叉出新的 API 版本(例如 v7beta1),并且可以完全删除单数字段。
单对双歧义
假设用户从以下内容开始:
kind: Frobber
height: 42
width: 3
param: "super"
在创建时我们可以设置params: ["super"]
。
在不相关的 POST(又称替换)中,旧客户端将发送:
kind: Frobber
height: 3
width: 42
param: "super"
如果我们不要求新客户端同时使用单数和复数字段,则新客户端将发送:
kind: Frobber
height: 3
width: 42
params: ["super"]
这似乎足够清楚——我们可以假设param: "super"
。
但是旧客户端可以通过补丁发送此信息:
PATCH /frobbers/1
{ param: "" }
在注册表代码看到它之前,它会被应用于旧对象,最终我们得到:
kind: Frobber
height: 42
width: 3
params: ["super"]
根据之前的逻辑,我们会复制params[0]
到param
并最终得到 param: "super"
。但这不是用户想要的,更重要的是,这与我们在复数化之前发生的情况不同。
为了消除歧义,我们要求复数用户也始终指定单数。
多个 API 版本
我们已经了解了如何满足规则 1、规则 2 和规则 3。规则 4 意味着您不能扩展一个版本的 API 而不扩展其他版本。例如,API 调用可能会以 API v7beta1 格式发布一个对象,该格式使用新字段 Params
,但 API 服务器可能会以可靠的旧 v6 格式存储该对象(因为 v7beta1 是“beta”)。当用户在 v7beta1 API 中读回该对象时,除了 之外的所有内容都是不可接受的Params[0]
。这意味着,即使它很丑陋,也必须对 v6 API 进行兼容的更改,如上所述。
对于某些更改,这可能很难正确执行。它可能需要在同一 API 资源中对同一信息进行多种表示,并且如果发生更改,则需要保持同步。
例如,假设您决定重命名同一 API 版本中的某个字段。在这种情况下,您将单位添加到height
和width
。您可以通过添加新字段来实现此目的:
type Frobber struct { Height *int `json:"height"` Width *int `json:"width"` HeightInInches *int `json:"heightInInches"` WidthInInches *int `json:"widthInInches"` }
您将所有字段转换为指针,以区分未设置和设置为 0,然后在默认逻辑中将每个相应字段设置为另一个(例如,heightInInches
从height
,反之亦然)。当用户创建发送手写配置时,这种方法很有效 – 客户端可以写入任一字段并读取任一字段。
但是,如果从 GET 的输出创建或更新,或者通过 PATCH 进行更新(请参阅就地更新),情况又会怎样呢?在这些情况下,这两个字段将发生冲突,因为在旧客户端仅知道旧字段的情况下,只有一个字段会被更新(例如height
)。
假设客户端创建:
{ "height": 10, "width": 5 }
并获取:
{ "height": 10, "heightInInches": 10, "width": 5, "widthInInches": 5 }
然后 PUT 回来:
{ "height": 13, "heightInInches": 10, "width": 5, "widthInInches": 5 }
根据兼容性规则,更新不能失败,因为它在更改之前就可以正常工作。
向后兼容性问题
- 单个功能/属性不能同时使用 API 版本中的多个规范字段来表示。一次只能填充一个表示,并且客户端需要能够指定他们希望在更改和读取时使用哪个字段(通常通过 API 版本)。如上所述,旧客户端必须继续正常运行。
- 即使是在新 API 版本中,如果新表示比旧表示更具表现力,也会破坏向后兼容性,因为只理解旧表示的客户端不会知道新表示及其语义。遇到此挑战的提案示例包括 通用标签选择器和pod 级安全上下文。
- 枚举值也会带来类似的挑战。向枚举集添加新值不是兼容更改。假设自己知道如何处理给定字段的所有可能值的客户端将无法处理新值。但是,如果处理得当(将删除的值视为已弃用但允许),从枚举集中删除值可以算是兼容更改。对于期望将来添加新值的枚举类字段(例如
reason
字段),请在字段可用的第一个版本中的 API 字段描述中清楚地记录该期望,并描述客户端应如何处理未知值。客户端应将此类值集视为潜在的开放式值。 - 对于联合体(最多应设置一个字段的字段集),如果原始对象遵循了适当的约定,则可以向联合体添加新选项。删除选项需要遵循弃用流程。
- 更改任何验证规则都有可能破坏某些客户端,因为它会改变对部分 API 的假设,类似于添加新的枚举值。规范字段的验证规则既不能放宽也不能加强。加强是不允许的,因为任何以前有效的请求都必须继续有效。削弱验证可能会破坏 API 资源的其他消费者和生成器。状态字段的编写者在我们的控制之下(例如,由非可插拔控制器编写),可能会收紧验证,因为这会导致客户端可以观察到以前有效值的子集。
- 不要添加现有资源的新 API 版本并将其设为同一版本中的首选版本,也不要将其设为存储版本。后者是必要的,这样 apiserver 的回滚才不会导致 etcd 中的资源在回滚后无法解码。
- 在一个 API 版本中具有默认值的任何字段在所有 API 版本中都必须具有非零默认值。这可以分为两种情况:
- 为现有的非默认字段添加具有默认值的新 API 版本:需要在所有以前的 API 版本中添加一个语义上等同于未设置的默认值,以保留未设置的值的语义含义。
- 添加具有默认值的新字段:在所有当前支持的 API 版本中,默认值必须在语义上等效。
不兼容的 API 更改
有时不兼容的更改可能没问题,但大多数情况下我们希望更改符合上述定义。如果您认为需要破坏兼容性,则应首先与 Kubernetes API 审阅者沟通。
破坏测试版或稳定版 API (例如 v1)的兼容性是不可接受的。实验性或 alpha 版 API 的兼容性并非严格要求,但破坏兼容性不应轻易进行,因为这会扰乱该功能的所有用户。Alpha 和测试版 API 可能会被弃用并最终被全部移除,如弃用政策中所述。
如果您的更改将向后不兼容或可能对 API 使用者造成重大更改,请 dev@kubernetes.io
在更改生效之前发送公告。如果您不确定,请询问。此外,请确保更改已记录在下一个版本的发行说明中,方法是将 PR 标记为“release-note-action-required”github 标签。
如果您发现您的更改意外破坏了客户端,则应将其恢复。
简而言之,预期的 API 演变如下:
newapigroup/v1alpha1
->…->newapigroup/v1alphaN
->newapigroup/v1beta1
->…->newapigroup/v1betaN
->newapigroup/v1
->newapigroup/v2alpha1
-> …
虽然仍处于 alpha 阶段,但我们期望能够继续前进,但可能会破坏它。
一旦进入测试阶段,我们将保留向前兼容性,但可能会引入新版本并删除旧版本。
v1 必须长期向后兼容。
更改版本化 API
对于大多数更改,您可能会发现最容易的方法是先更改版本化的 API。这迫使您思考如何以兼容的方式进行更改。通常,一次执行一个版本化的 API 或先完成一个版本的所有更改,然后再开始“所有其余更改”,而不是在每个版本中执行每个步骤,这样通常更容易。
编辑 types.go
每个 API 的结构定义都在 中 staging/src/k8s.io/api/<group>/<version>/types.go
。编辑这些文件以反映您想要进行的更改。请注意,版本化 API 中的所有类型和非内联字段都必须以描述性注释开头 – 这些注释用于生成文档。类型的注释不应包含类型名称;API 文档是根据这些注释生成的,最终用户不应接触 golang 类型名称。
对于需要生成的DeepCopyObject方法的类型 ,通常只有顶级类型才需要Pod
,请在注释中添加此行(示例):
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
可选字段应该具有,omitempty
json 标签;否则,字段将被解释为必需的。
编辑 defaults.go
如果您的更改包括需要默认值的新字段,则需要添加案例pkg/apis/<group>/<version>/defaults.go
。
注意:在为新字段添加默认值时,您还必须在所有 API 版本中添加默认值,而不是nil
在旧 API 版本中保留未设置的新字段(例如)。这是必需的,因为只要读取序列化版本就会发生默认值(请参阅#66135)。如果可能,请选择有意义的值作为未设置值的标记。
过去,核心 v1 API 比较特殊。它defaults.go
过去位于pkg/api/v1/defaults.go
。如果您看到引用该路径的代码,则可以肯定它已经过时了。现在,核心 v1 API 位于 , pkg/apis/core/v1/defaults.go
它遵循上述约定。
当然,既然你添加了代码,你就必须添加测试: pkg/apis/<group>/<version>/defaults_test.go
。
当您需要区分未设置值和自动零值时,请使用指向标量的指针。例如, PodSpec.TerminationGracePeriodSeconds
定义为*int64
go 类型定义。零值表示 0 秒,而 nil 值要求系统选择默认值。
不要忘记运行测试!
编辑 conversion.go
鉴于您尚未更改内部结构,这可能感觉为时过早,事实也确实如此。您还没有任何要转换的内容。我们将在“内部”部分重新讨论这一点。如果您以不同的顺序执行所有操作(即从内部结构开始),则应跳至下面的主题。在极少数情况下,如果您要进行不兼容的更改,您可能现在想要或不想这样做,但您以后必须做更多。您想要的文件是 pkg/apis/<group>/<version>/conversion.go
和 pkg/apis/<group>/<version>/conversion_test.go
。
请注意,转换机制通常不会处理值的转换,例如各种字段引用和 API 常量。客户端库 具有用于字段引用的自定义转换代码。您还需要添加对 AddFieldLabelConversionFunc
方案的调用,其中包含一个可以理解所支持转换的映射函数,例如这一 行。
改变内部结构
现在是时候改变内部结构,以便可以使用版本化的更改。
编辑 types.go
与版本化 API 类似,内部结构的定义位于 中 pkg/apis/<group>/types.go
。编辑这些文件以反映您想要进行的更改。请记住,内部结构必须能够表达所有版本化 API。
与版本化 API 类似,您需要+k8s:deepcopy-gen
向需要生成 DeepCopyObject 方法的类型添加标签。
编辑validation.go
对内部结构所做的大多数更改都需要某种形式的输入验证。目前对 中的内部对象进行验证 pkg/apis/<group>/validation/validation.go
。此验证是我们创造出色用户体验的首要机会之一 – 良好的错误消息和彻底的验证有助于确保用户提供您期望的内容,如果没有,他们知道原因以及如何修复它。认真考虑string
字段的内容、字段的边界int
和字段的可选性。
当然,代码需要测试- pkg/apis/<group>/validation/validation_test.go
。
编辑版本转换
此时,您已完成版本化 API 更改和内部结构更改。如果有任何显著差异(特别是字段名称、类型、结构更改),则必须添加一些逻辑来将版本化 API 转换为内部表示形式。如果您看到错误serialization_test
,则可能表示需要进行显式转换。
转换的性能对 apiserver 的性能影响很大。因此,我们自动生成比通用函数(基于反射,因此效率极低)更高效的转换函数。
转换代码驻留在每个版本的 API 中。有两个文件:
pkg/apis/<group>/<version>/conversion.go
包含手写转换函数pkg/apis/<group>/<version>/zz_generated.conversion.go
包含自动生成的转换函数
由于自动生成的转换函数使用的是手写函数,因此手写函数的命名应当遵循定义的约定,即把X
pkg 中的类型转换为pkg 中的a
类型的函数应当命名为: 。Y
b
convert_a_X_To_b_Y
还要注意,您在编写转换函数时可以(并且出于效率原因应该)使用自动生成的转换函数。
添加手动编写的转换还需要您添加测试 pkg/apis/<group>/<version>/conversion_test.go
。
添加所有必要的手动编写的转换后,您需要重新生成自动生成的转换。要重新生成它们,请运行:
make clean && make generated_files
make clean
很重要,否则生成的文件可能会过时,因为构建系统使用自定义缓存。
make all
也会调用make generated_files
。
还将make generated_files
再生zz_generated.deepcopy.go
、、 zz_generated.defaults.go
和api/openapi-spec/swagger.json
。
如果由于编译错误而无法重新生成,最简单的解决方法是删除导致错误的文件并重新运行命令。
生成代码
除了defaulter-gen
、deepcopy-gen
和conversion-gen
之外 openapi-gen
,还有一些其他生成器:
go-to-protobuf
client-gen
lister-gen
informer-gen
codecgen
(使用 ugorji 编解码器进行快速 json 序列化)
许多生成器都基于 gengo
并共享通用标志。该--verify-only
标志将检查磁盘上的现有文件,如果它们不是要生成的文件,则失败。
创建 go 代码的生成器有一个--go-header-file
标志,该标志应为包含应包含的标头的文件。此标头是应出现在生成文件顶部的版权信息,应 repo-infra/verify/verify-boilerplane.sh
在构建的后期阶段使用脚本进行检查。
要调用这些生成器,您可以运行make update
,它将运行一堆 脚本。请继续阅读接下来的几节,因为有些生成器有先决条件,还因为它们介绍了如果您发现make update
运行时间过长时如何单独调用生成器。
生成 protobuf 对象
对于任何核心 API 对象,我们还需要生成 Protobuf IDL 和编组器。该生成过程通过以下方式调用:
hack/update-generated-protobuf.sh
绝大多数对象在转换为 protobuf 时不需要任何考虑,但请注意,如果您依赖标准库中的 Golang 类型,则可能需要额外的工作,尽管在实践中我们通常使用自己的等效项进行 JSON 序列化。将pkg/api/serialization_test.go
验证您的 protobuf 序列化是否保留了所有字段 – 请务必运行几次以确保没有未完全计算的字段。
生成客户集
client-gen
是一个为顶级 API 对象生成客户端集的工具。
client-gen
需要// +genclient
在内部pkg/apis/<group>/types.go
以及每个特定版本中对每个导出类型进行注释staging/src/k8s.io/api/<group>/<version>/types.go
。
如果 apiserver 将您的 API 托管在与文件系统中的不同的组名下<group>
(通常是因为<group>
文件系统中的省略了“k8s.io”后缀,例如 admission 与 admission.k8s.io),您可以通过在内部 以及每个特定版本的中 添加注释来指示client-gen
使用正确的组名。// +groupName=
doc.go
pkg/apis/<group>/doc.go
staging/src/k8s.io/api/<group>/<version>/types.go
添加注释后,使用以下命令生成客户端
hack/update-codegen.sh
请注意,您可以使用可选项// +groupGoName=
指定 CamelCase 自定义 Golang 标识符来消除冲突,例如policy.authorization.k8s.io
和 policy.k8s.io
。这两个都将映射到Policy()
客户端集中。
client-gen 非常灵活。如果您需要非 kubernetes API 的 client-gen,请参阅此文档。
生成列表者
lister-gen
是一个为客户端生成列表器的工具。它重用 //+genclient
和// +groupName=
注释,因此您无需指定额外的注释。
您之前运行的hack/update-codegen.sh
已调用lister-gen
。
生成告密者
informer-gen
生成非常有用的 Informers,用于监视 API 资源的变化。它重用//+genclient
和 //+groupName=
注释,因此您无需指定额外的注释。
您之前运行的hack/update-codegen.sh
已调用informer-gen
。
编辑 json 解编代码
我们正在自动生成用于编组和解组 api 对象的 json 表示的代码 – 这是为了提高整体系统性能。
自动生成的代码位于每个版本的 API 中:
staging/src/k8s.io/api/<group>/<version>/generated.proto
staging/src/k8s.io/api/<group>/<version>/generated.pb.go
要重新生成它们,请运行:
hack/update-generated-protobuf.sh
创建新的 API 版本
由于我们要使工具完全通用,因此本节正在建设中。
如果要向现有组添加新 API 版本,则可以复制现有的结构pkg/apis/<group>/<existing-version>
和 staging/src/k8s.io/api/<group>/<existing-version>
目录。
将 PR 构建为分层提交会很有帮助,这样审阅者可以更轻松地查看两个版本之间发生了什么变化:
pkg/apis/<group>/<existing-version>
仅将和staging/src/k8s.io/api/<group>/<existing-version>
包复制到的 提交<new-version>
。- 在新文件中重
<existing-version>
命名的提交。<new-version>
- 对 做出任何新更改的提交
<new-version>
。 make generated_files
包含运行、make update
等生成的文件的提交。
由于项目快速变化的性质,以下内容可能已经过时:
- 您必须将版本添加到 pkg/controlplane/instance.go, 对于稳定版本默认启用,或者对于 alpha 和 beta 版本默认禁用。
- 您必须将新版本添加到
pkg/apis/group_name/install/install.go
(例如,pkg/apis/apps/install/install.go)。 - 您必须将新版本添加到 hack/lib/init.sh#KUBE_AVAILABLE_GROUP_VERSIONS。
- 您必须将新版本添加到 cmd/kube-apiserver/app#apiVersionPriorities。
pkg/registry/group_name/rest
您必须在(例如,pkg/registry/authentication/rest )中为新版本设置存储 。- 因为
kubectl get
您必须将表定义添加到pkg/printers/internalversion/printers.go。此集成测试位于test/integration/apiserver/print_test.go中。
您需要按照上面部分的说明重新生成已生成的代码。
测试
需要对一些测试进行更新。
- 您必须将 API 发现数据中发布的新存储版本哈希添加到 pkg/controlplane/storageversionhashdata/datago#GVRToStorageVersionHash。
- 运行
go test ./pkg/controlplane -run StorageVersion
验证。
- 运行
- 您必须将新版本存根添加到test/integration/etcd/data.go中存储在 etcd 中的持久版本中。
- 运行
go test ./test/integration/etcd
验证
- 运行
- 通过启动集群(即 local-up-cluster.sh、kind 等)并运行来对更改进行健全性测试
kubectl get <resource>.<version>.<group>
。 - 集成测试 也适合与控制器一起测试完整的 CRUD 生命周期。
- 要为 beta API 编写集成测试,您需要有选择地启用所需的资源。您可以使用cmd/kube-apiserver/app/testing/testserver.go#StartTestServerOrDie执行此操作。然后,您将传递
--runtime-config=groupname/v1beta1/resourcename
作为标志以启用 beta API。
- 要为 beta API 编写集成测试,您需要有选择地启用所需的资源。您可以使用cmd/kube-apiserver/app/testing/testserver.go#StartTestServerOrDie执行此操作。然后,您将传递
- 对于 beta API,e2e 测试需要针对 kube-apiserver 执行发现检查,以确定 beta API 是否已启用。请参阅test/e2e/apimachinery/discovery.go 以获取示例。有一个用于 beta API 作业的 prow 仪表板,可以查看您的结果。
创建新的 API 组
您必须在pkg/apis/
和 下创建一个新目录staging/src/k8s.io/api
;复制现有 API 组的目录结构,例如pkg/apis/authentication
和;将“authentication”替换为您的组名,并将版本替换为您的版本;将版本化 和 内部 register.go 和 install.gostaging/src/k8s.io/api/authentication
中的 API 种类替换 为您的种类。
您必须将您的 API 组/版本添加到代码库中的几个位置,如制作新 API 版本部分所述。
您需要按照上面部分的说明重新生成已生成的代码。
更新模糊测试器
我们对 API 的测试方案的一部分是“模糊测试”(用随机值填充)API 对象,然后将它们转换为不同的 API 版本。这是一种很好的方法,可以揭露您丢失信息或做出错误假设的地方。
模糊测试器的工作原理是创建一个随机 API 对象并调用 中的自定义模糊测试器函数pkg/apis/$GROUP/fuzzer/fuzzer.go
。然后将生成的对象从一个 API 版本往返到另一个 API 版本,并验证其是否与开始时的版本相同。此过程中不运行验证,但默认运行。
如果您添加了任何需要非常仔细格式化的字段(测试不运行验证)或者如果您在默认期间做出了假设(例如“此切片将始终至少有 1 个元素”),则可能会收到错误甚至从中产生k8s.io/kubernetes/pkg/api/testing.TestRoundTripTypes
恐慌 ./pkg/api/testing/serialization_test.go
。
如果您默认任何字段,则必须在自定义模糊测试器函数中检查该字段,因为模糊测试器可能会将某些字段留空。如果您的对象具有结构引用,模糊测试器可能会将其保留为零,或者可能会创建一个随机对象。您的自定义模糊测试器函数必须确保默认设置不会进一步更改对象,因为这将在往返测试中显示为差异。
最后,模糊测试无需任何功能门控配置即可运行。如果默认或其他行为位于功能门控后面,请注意当功能门控默认开启时,模糊行为将会改变。
更新语义比较
这种事情很少需要发生,但一旦发生,就会很麻烦。在极少数情况下,我们最终会得到具有不同位表示的道德等价值的对象(例如资源数量)(例如,使用二进制格式化程序的值 10 与使用十进制格式化程序的值 0 相同)。Go 知道如何进行深度相等的唯一方法是通过逐个字段的位比较。这对我们来说是个问题。
你首先应该做的就是尽量不要这样做。如果你真的无法避免,我想向你介绍我们的apiequality.Semantic.DeepEqual
例程。它支持特定类型的自定义覆盖 – 你可以在 中找到它 pkg/api/helper/helpers.go
。
还有一次你可能不得不接触这个:unexported fields
。你看,虽然 Go 的reflect
包被允许接触unexported fields
,但我们这些凡人却不能——这包括apiequality.Semantic.DeepEqual
。幸运的是,我们的大多数 API 对象都是“哑结构”——所有字段都是导出的(以大写字母开头),没有未导出的字段。但有时你想在我们的 API 中包含一个确实在某处有未导出字段的对象(例如,time.Time
有未导出的字段)。如果这击中了你,你可能不得不接触apiequality.Semantic.DeepEqual
自定义功能。
实施你的改变
现在您已经完全改变了 API – 去实现您正在做的事情吧!
编写端到端测试
请参阅E2E 文档,了解有关如何为您的功能编写端到端测试的详细信息。确保 E2E 测试在默认启用的功能/API 的默认预提交中运行。
示例和文档
最后,您的更改已完成,所有单元测试都通过了,e2e 也通过了,您就大功告成了,对吗?其实不然。您只是更改了 API。如果您要触及 API 的现有方面,则必须非常努力地确保更新所有示例和文档。没有简单的方法可以做到这一点,部分原因是 JSON 和 YAML 会默默删除未知字段。您很聪明 – 您会弄清楚的。好好利用它grep
。ack
如果您添加了功能,您应该考虑记录它和/或编写示例来说明您的更改。
确保通过运行以下命令更新 swagger 和 OpenAPI 规范:
make update
API 规范的更改应该与其他更改分开提交。
Alpha、Beta 和稳定版本
新功能开发要经过一系列逐渐成熟阶段:
- 发展水平
- 对象版本控制:无约定
- 可用性:未提交到主 kubernetes 仓库,因此在官方版本中不可用
- 受众:正在密切合作开发某个功能或概念验证的其他开发人员
- 可升级性、可靠性、完整性和支持:无要求或保证
- 阿尔法水平
- 对象版本控制:API 版本名称包含
alpha
(例如v1alpha1
) - 可用性:已提交至主 kubernetes repo;出现在官方版本中;该功能默认禁用,但可以通过标志启用
- 受众:有兴趣对功能提供早期反馈的开发人员和专家用户
- 完整性:某些 API 操作、CLI 命令或 UI 支持可能未实现;API 不需要进行API 审查(在正常代码审查的基础上,对 API 进行深入而有针对性的审查)
- 可升级性:对象模式和语义可能会在以后的软件版本中发生变化,而没有任何规定保留现有集群中的对象;消除可升级性问题可使开发人员快速取得进展;特别是,API 版本的增量速度可以比次要版本发布节奏更快,开发人员无需维护多个版本;当对象模式或语义以不兼容的方式发生变化时,开发人员仍应增加 API版本
- 集群可靠性:由于该功能相对较新,并且可能缺乏完整的端到端测试,通过标志启用该功能可能会暴露错误并破坏集群的稳定性(例如,控制循环中的错误可能会迅速创建过多的对象,从而耗尽 API 存储)。
- 支持:项目没有承诺完成该功能;该功能可能会在以后的软件版本中完全删除
- 推荐使用案例:仅在短期测试集群中,因为可升级性的复杂性以及缺乏长期支持和缺乏可升级性。
- 对象版本控制:API 版本名称包含
- Beta 级别:
- 对象版本控制:API 版本名称包含
beta
(例如v2beta3
) - 可用性:在官方 Kubernetes 版本中;默认情况下,API 是禁用的,但可以通过标志启用。(注意:在 v1.24 之前引入的 beta API 默认启用,但 对于新的 beta API 而言,这种情况有所改变)
- 受众:有兴趣提供功能反馈的用户
- 完整性:应实现所有 API 操作、CLI 命令和 UI 支持;端到端测试已完成;API 已经过彻底的 API 审查并被认为是完整的,尽管在测试期间使用可能会经常出现审查期间未想到的 API 问题
- 可升级性:对象模式和语义可能会在以后的软件版本中发生变化;发生这种情况时,将记录升级路径;在某些情况下,对象将自动转换为新版本;在其他情况下,可能需要手动升级;手动升级可能需要停机来依赖新功能,并且可能需要手动将对象转换为新版本;当需要手动转换时,项目将提供有关该过程的文档
- 集群可靠性:由于该功能具有端到端测试,因此通过标志启用该功能不会在无关功能中创建新的错误;由于该功能是新功能,因此可能存在小错误
- 支持:项目承诺以某种形式在后续的稳定版本中完成该功能;通常这将在 3 个月内发生,但有时会更长;发布应同时支持两个连续版本(例如
v1beta1
和v1beta2
;或v1beta2
和v1
)至少一个次要发布周期(通常为 3 个月),以便用户有足够的时间升级和迁移对象 - 推荐使用案例:在短期测试集群中;在生产集群中作为功能短期评估的一部分,以提供反馈
- 对象版本控制:API 版本名称包含
- 稳定水平:
- 对象版本控制:API 版本,
vX
其中X
是一个整数(例如v1
) - 可用性:在 Kubernetes 官方版本中,默认启用
- 受众:所有用户
- 完整性:必须在适当的一致性配置文件中进行一致性测试,并经 SIG Architecture 批准(例如,不可移植和/或可选功能可能不在默认配置文件中)
- 可升级性:后续软件版本仅允许严格兼容的更改
- 集群可靠性:高
- 支持:API 版本将继续存在于许多后续软件版本中;
- 推荐使用案例:任何
- 对象版本控制:API 版本,
将不稳定的功能添加到稳定版本
在为已经稳定的对象添加功能时,新增字段和新行为需要满足稳定级别的要求,如果不能满足,则无法将新字段添加到对象中。
例如,考虑以下对象:
// API v6. type Frobber struct { // height ... Height *int32 `json:"height" // param ... Param string `json:"param" }
开发人员正在考虑添加一个新Width
参数,如下所示:
// API v6. type Frobber struct { // height ... Height *int32 `json:"height" // param ... Param string `json:"param" // width ... Width *int32 `json:"width,omitempty" }
但是,新功能还不够稳定,无法在稳定版本中使用(v6
)。原因可能包括:
- 最终的表现形式尚未确定(例如,应该称之为
Width
还是Breadth
?) - 对于一般用途来说,实现不够稳定(例如,
Area()
例程有时会溢出)。
在满足稳定性之前,开发人员不能无条件地添加新字段。但是,有时,只有当一些用户尝试新功能时,才能满足稳定性要求,而有些用户只能或愿意接受已发布的 Kubernetes 版本。在这种情况下,开发人员有几个选择,这两种选择都需要在多个版本中进行准备工作。
所使用的机制取决于是否添加新字段或者在现有字段中允许新值。
现有 API 版本中的新字段
以前,注释用于实验性的 alpha 功能,但由于以下几个原因,不再推荐使用:
- 它们将集群暴露给针对早期 API 服务器添加的非结构化注释的“定时炸弹”数据(https://issue.k8s.io/30819)
- 它们无法迁移到同一 API 版本中的一流字段(请参阅向后兼容性陷阱中在多个位置表示单个值的问题)
首选方法是向现有对象添加一个 alpha 字段,并确保默认情况下禁用它:
- 向 API 服务器添加功能门控来控制新字段的启用:在staging/src/k8s.io/apiserver/pkg/features/kube_features.go 中:// owner: @you // alpha: v1.11 // // Add multiple dimensions to frobbers. Frobber2D utilfeature.Feature = “Frobber2D” var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{ … Frobber2D: {Default: false, PreRelease: utilfeature.Alpha}, }
- 将字段添加到 API 类型:
- 确保字段是可选的
- 添加
omitempty
结构标签 - 添加
// +optional
评论标签 - 添加
// +featureGate=<gate-name>
评论标签 - 确保字段为空时在 API 响应中完全不存在(可选字段必须是指针)
- 添加
- 在字段描述中包含有关 alpha 级别的详细信息
- 确保字段是可选的
- 在将对象持久化到存储之前,在创建时清除已禁用的 alpha 字段,如果现有对象在字段中尚无值,则在更新时清除。这可防止在禁用该功能时再次使用该功能,同时确保保留现有数据。确保保留现有数据是必要的,以便当该功能在未来版本n中默认启用 并且无条件允许数据持久化在字段中时,n-1 API 服务器(该功能仍默认禁用)不会在更新时删除数据。建议在 REST 存储策略的 PrepareForCreate/PrepareForUpdate 方法中执行此操作:func (frobberStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { frobber := obj.(*api.Frobber) if !utilfeature.DefaultFeatureGate.Enabled(features.Frobber2D) { frobber.Width = nil } } func (frobberStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) { newFrobber := obj.(*api.Frobber) oldFrobber := old.(*api.Frobber) if !utilfeature.DefaultFeatureGate.Enabled(features.Frobber2D) && oldFrobber.Width == nil { newFrobber.Width = nil } }
- 为了让您的 API 测试适应未来,在开启和关闭功能门控的情况下进行测试时,请确保根据需要特意设置门控。不要假设门控处于关闭或打开状态。随着您的功能从 到
alpha
的进展beta
,stable
该功能可能会在整个代码库中默认打开或关闭。以下示例提供了一些详细信息func TestAPI(t *testing.T){ testCases:= []struct{ // … test definition … }{ { // .. test case .. }, { // … test case .. }, } for _, testCase := range testCases{ t.Run(“..name…”, func(t *testing.T){ // run with gate on defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features. Frobber2D, true)() // … test logic … }) t.Run(“..name…”, func(t *testing.T){ // run with gate off, *do not assume it is off by default* defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features. Frobber2D, false)() // … test gate-off testing logic logic … }) } - 在验证中,验证字段是否存在:func ValidateFrobber(f *api.Frobber, fldPath *field.Path) field.ErrorList { … if f.Width != nil { … validation of width field … } … }
在未来的 Kubernetes 版本中:
- 如果该功能进展到测试版或稳定状态,则可以删除或默认启用该功能门控。
- 如果 alpha 字段的模式必须以不兼容的方式改变,则必须使用新的字段名称。
- 如果该功能被放弃,或者字段名称被更改,则应该从 go 结构体中删除该字段,并使用墓碑注释确保字段名称和 protobuf 标签不会被重复使用:// API v6. type Frobber struct { // height … Height int32 `json:”height” protobuf:”varint,1,opt,name=height”` // param … Param string `json:”param” protobuf:”bytes,2,opt,name=param”` // +k8s:deprecated=width,protobuf=3 }
现有字段中的新枚举值
"OnlyOnTuesday"
开发人员正在考虑向以下现有枚举字段添加新的允许枚举值:
type Frobber struct { // restartPolicy may be set to "Always" or "Never". // Additional policies may be defined in the future. // Clients should expect to handle additional values, // and treat unrecognized values in this field as "Never". RestartPolicy string `json:"policy" }
预期 API 客户端的旧版本必须能够以安全的方式处理新值:
- 如果枚举字段驱动单个组件的行为,请确保该组件的所有版本在遇到包含新值的 API 对象时都能正确处理该值或确保万无一失。例如,
Pod
kubelet 使用的枚举字段中允许的新值必须由比允许新值的第一个 API 服务器版本早三个版本的 kubelet 安全处理。 - 如果 API 驱动由外部客户端(如
Ingress
或NetworkPolicy
)实现的行为,则枚举字段必须明确指示将来可能允许使用其他值,并定义客户端必须如何处理无法识别的值。如果在包含枚举字段的第一个版本中没有做到这一点,则添加可能破坏现有客户端的新值是不安全的。
如果预期的 API 客户端可以安全地处理新的枚举值,则下一个要求是开始以不破坏先前 API 服务器对该对象的验证的方式允许它。这需要至少两个版本才能安全完成:
版本 1:
- 仅在更新已包含新枚举值的现有对象时才允许使用新枚举值
- 在其他情况下不允许它(创建和更新尚不包含新枚举值的对象)
- 验证已知客户端是否按预期处理新值,遵守新值或使用先前定义的“未知值”行为(取决于相关功能门控是否启用)
第 2 版:
- 在创建和更新场景中允许新的枚举值
这确保了在倾斜版本中(在滚动升级期间发生)具有多个服务器的集群将不允许保留数据,因为 API 服务器的先前版本会阻塞数据。
通常,使用功能门控来执行此推出,从 alpha 版本开始并在版本 1 中默认禁用,然后逐渐升级到 beta 版本并在版本 2 中默认启用。
- 向 API 服务器添加功能门控来控制新枚举值(及相关函数)的启用:在staging/src/k8s.io/apiserver/pkg/features/kube_features.go 中:// owner: @you // alpha: v1.11 // // Allow OnTuesday restart policy in frobbers. FrobberRestartPolicyOnTuesday utilfeature.Feature = “FrobberRestartPolicyOnTuesday” var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{ … FrobberRestartPolicyOnTuesday: {Default: false, PreRelease: utilfeature.Alpha}, }
- 更新 API 类型的文档:
- 在字段描述中包含有关 alpha 级别的详细信息
- 验证对象时,确定是否应允许新的枚举值。这可防止在禁用该功能时再次使用新值,同时确保保留现有数据。需要确保保留现有数据,以便当该功能在未来版本n中默认启用 并且无条件允许数据保留在字段中时,n-1 API 服务器(该功能仍默认禁用)不会因验证而受阻。建议在 REST 存储策略的 Validate/ValidateUpdate 方法中执行此操作:func (frobberStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { frobber := obj.(*api.Frobber) return validation.ValidateFrobber(frobber, validationOptionsForFrobber(frobber, nil)) } func (frobberStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { newFrobber := obj.(*api.Frobber) oldFrobber := old.(*api.Frobber) return validation.ValidateFrobberUpdate(newFrobber, oldFrobber, validationOptionsForFrobber(newFrobber, oldFrobber)) } func validationOptionsForFrobber(newFrobber, oldFrobber *api.Frobber) validation.FrobberValidationOptions { opts := validation.FrobberValidationOptions{ // allow if the feature is enabled AllowRestartPolicyOnTuesday: utilfeature.DefaultFeatureGate.Enabled(features.FrobberRestartPolicyOnTuesday) } if oldFrobber == nil { // if there’s no old object, use the options based solely on feature enablement return opts } if oldFrobber.RestartPolicy == api.RestartPolicyOnTuesday { // if the old object already used the enum value, continue to allow it in the new object opts.AllowRestartPolicyOnTuesday = true } return opts }
- 在验证中,根据传入的选项验证枚举值:func ValidateFrobber(f *api.Frobber, opts FrobberValidationOptions) field.ErrorList { … validRestartPolicies := sets.NewString(RestartPolicyAlways, RestartPolicyNever) if opts.AllowRestartPolicyOnTuesday { validRestartPolicies.Insert(RestartPolicyOnTuesday) } if f.RestartPolicy == RestartPolicyOnTuesday && !opts.AllowRestartPolicyOnTuesday { allErrs = append(allErrs, field.Invalid(field.NewPath(“restartPolicy”), f.RestartPolicy, “only allowed if the FrobberRestartPolicyOnTuesday feature is enabled”)) } else if !validRestartPolicies.Has(f.RestartPolicy) { allErrs = append(allErrs, field.NotSupported(field.NewPath(“restartPolicy”), f.RestartPolicy, validRestartPolicies.List())) } … }
- 至少发布一次后,该功能可以升级为 Beta 版或 GA 并默认启用。在staging/src/k8s.io/apiserver/pkg/features/kube_features.go 中:// owner: @you // alpha: v1.11 // beta: v1.12 // // Allow OnTuesday restart policy in frobbers. FrobberRestartPolicyOnTuesday utilfeature.Feature = “FrobberRestartPolicyOnTuesday” var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{ … FrobberRestartPolicyOnTuesday: {Default: true, PreRelease: utilfeature.Beta}, }
新的 alpha API 版本
另一种选择是引入具有新类型alpha
或beta
版本指示符的新类型,如下所示:
// API v7alpha1 type Frobber struct { // height ... Height *int32 `json:"height"` // param ... Param string `json:"param"` // width ... Width *int32 `json:"width,omitempty"` }
后者要求同一 API 组中的所有对象都必须Frobber
在新版本中进行复制,v7alpha1
这也要求用户使用使用其他版本的新客户端。因此,这不是一个首选方案。
一个相关的问题是集群管理器如何从具有新功能的新版本回滚,而该功能已被用户使用。请参阅 kubernetes/kubernetes#4855。