Skip to content

Latest commit

 

History

History
973 lines (707 loc) · 61 KB

File metadata and controls

973 lines (707 loc) · 61 KB

5.12 k8s api-changes(翻译)

本文档面向想要更改现有 API 的开发人员。可以在API 约定中找到一组适用于新 API 和更改的 API 约定。

目录

那么您想更改 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。

为了演示一般过程,这里有一个(假设的)示例:

  1. 用户将Pod对象发布到/api/v7beta1/...
  2. JSON 被解组为v7beta1.Pod结构
  3. 默认值应用于v7beta1.Pod
  4. v7beta1.Pod转换为api.Pod结构
  5. api.Pod已验证,任何错误都会返回给用户
  6. api.Pod转换为v6.Pod (因为 v6 是最新的稳定版本)
  7. v6.Pod被编组为 JSON 并写入 etcd

现在我们已经存储了Pod对象,用户可以在任何受支持的 api 版本中获取该对象。例如:

  1. 用户从/api/v5/...获取Pod
  2. JSON 从 etcd 读取并解组为v6.Pod结构
  3. 默认值应用于v6.Pod
  4. v6.Pod转换为api.Pod结构
  5. api.Pod转换为v5.Pod结构
  6. v5.Pod被编组为 JSON 并发送给用户

此过程的含义是 API 更改必须谨慎且向后兼容。

关于兼容性

在讨论如何更改 API 之前,有必要先澄清一下 API 兼容性的含义。 Kubernetes 将其 API 的向前和向后兼容性视为首要任务。兼容性很_困难_,尤其是处理回滚安全方面的问题。这是每次 API 更改都必须考虑的事情。

如果 API 更改满足以下条件,则视为兼容:

  • 添加了正确行为不需要的新功能(例如,不添加新的必填字段)
  • 不改变现有的语义,包括:
    • 默认值_和行为_的语义含义
    • 现有 API 类型、字段和值的解释
    • 哪些字段是必填字段,哪些字段不是
    • 可变字段不会变得不可变
    • 有效值不会变得无效
    • 明确无效的值不会变得有效

换句话说:

  1. 在更改之前成功的任何 API 调用(例如,发布到 REST 端点的结构)在更改之后也必须成功。
  2. 任何不使用您的更改的 API 调用的行为都必须与更改之前的行为相同。
  3. 当针对不包含您的更改的 API 服务器发出任何使用您的更改的 API 调用时,不得导致问题(例如崩溃或降级行为)。
  4. 必须能够往返更改(转换为不同的 API 版本并返回)而不会丢失信息。
  5. 现有客户无需知道您的更改,即可继续像以前一样运行,即使您的更改正在使用中也是如此。
  6. 必须能够回滚到不包含您的更改的 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 操作的上下文链接在一起来处理,以尽可能接近用户的意图。

任何读取操作时:

  • 如果未填充plural,API 逻辑必须将plural 填充为单元素列表,并将plural[0] 设置为单数值。

进行任何创建操作时:

  • 如果仅指定单数字段(例如较旧的客户端),API 逻辑必须将复数填充为单元素列表,并将plural[0] 设置为单数值。理由:这是一个老客户,他们的行为兼容。
  • 如果同时指定了单数和复数字段,API 逻辑必须验证plural[0] 是否与单数值匹配。
  • 任何其他情况都是错误,必须被拒绝。这包括指定复数字段和不指定单数字段的情况。理由:在更新中,不可能区分旧客户端通过补丁清除单数字段和新客户端设置复数字段之间的区别。为了兼容性,我们必须假设前者,并且我们不希望更新语义与创建不同(请参见下面的单双歧义

对于上述内容:“已指定”表示该字段存在于用户提供的输入中(包括默认字段)。

进行任何更新操作(包括补丁)时:

  • 如果单数被清除并且复数没有改变,API逻辑必须清除复数。理由:这是一个老客户清理它所知道的领域。
  • 如果复数被清除且单数未更改,API 逻辑必须使用与旧复数相同的值填充新复数。理由:这是一个旧客户端,无法发送它不知道的字段。
  • 如果单数字段已更改(但未清除)并且复数字段未更改,API 逻辑必须将plural 填充为单元素列表,并将plural[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 调用可能会以 API v7beta1 格式发布一个对象,该格式使用新的Params字段,但 API 服务器可能会以可靠的旧 v6 形式存储该对象(因为 v7beta1 是“beta”)。当用户在 v7beta1 API 中读回对象时,丢失除Params[0]之外的所有对象是不可接受的。这意味着,即使它很丑陋,也必须对 v6 API 进行兼容的更改,如上所述。

对于某些更改,正确执行此操作可能具有挑战性。它可能需要同一 API 资源中相同信息的多种表示形式,这些表示形式需要保持同步才能更改。

例如,假设您决定重命名同一 API 版本中的字段。在这种情况下,您可以为heightwidth添加单位。您可以通过添加新字段来实现这一点:

type Frobber struct {
  Height         *int          `json:"height"`
  Width          *int          `json:"width"`
  HeightInInches *int          `json:"heightInInches"`
  WidthInInches  *int          `json:"widthInInches"`
}

您将所有字段转换为指针,以便区分未设置和设置为 0,然后在默认逻辑中设置每个相应的字段(例如heightInInchesheight ,反之亦然)。当用户创建发送手写配置时,效果很好——客户端可以写入任一字段并读取任一字段。

但是,如何从 GET 的输出创建或更新,或者通过 PATCH 更新(请参阅就地更新)?在这些情况下,这两个字段将发生冲突,因为如果旧客户端只知道旧字段(例如height ),则只会更新一个字段。

假设客户端创建:

{
  "height": 10,
  "width": 5
}

和获取:

{
  "height": 10,
  "heightInInches": 10,
  "width": 5,
  "widthInInches": 5
}

然后放回去:

{
  "height": 13,
  "heightInInches": 10,
  "width": 5,
  "widthInInches": 5
}

根据兼容性规则,更新不得失败,因为它在更改之前就可以工作。

向后兼容性陷阱

  • 单个功能/属性无法在 API 版本中同时使用多个规范字段来表示。一次只能填充一种表示,并且客户端需要能够指定他们希望在突变和读取时使用哪个字段(通常通过 API 版本)。如上所述,老客户端必须继续正常运行。
  • 即使在新的 API 版本中,新的表示形式也比旧的表示形式更具表现力,这会破坏向后兼容性,因为仅理解旧表示形式的客户端不会意识到新的表示形式及其语义。遇到这一挑战的提案示例包括通用标签选择器Pod 级安全上下文
  • 枚举值会带来类似的挑战。将新值添加到枚举集_不是_兼容的更改。假设他们知道如何处理给定字段的所有可能值的客户端将无法处理新值。但是,如果处理得当,从枚举集中删除值_可能_是兼容的更改(将删除的值视为已弃用但允许)。对于期望在未来添加新值的类似枚举的字段(例如reason字段),请在该字段可用的第一个版本的 API 字段描述中清楚地记录该期望,并描述客户端应如何处理未知值。客户应将此类价值观视为潜在的开放式价值观。
  • 对于Unions来说,最多应设置一个字段的集合,如果在原始对象中遵循适当的约定,则可以向联合添加新选项。删除选项需要遵循弃用流程
  • 更改任何验证规则总是有可能破坏某些客户端,因为它改变了有关部分 API 的假设,类似于添加新的枚举值。规范字段的验证规则既不能放松也不能加强。不允许强化,因为任何以前有效的请求都必须继续有效。弱化验证有可能破坏 API 资源的其他使用者和生成者。状态字段的写入者在我们的控制之下(例如,由不可插入的控制器写入),可能会加强验证,因为这将导致客户端可以观察到先前有效值的子集。
  • 不要添加现有资源的新 API 版本并将其设为同一版本中的首选版本,也不要将其设为存储版本。后者是必要的,这样 apiserver 的回滚不会使 etcd 中的资源在回滚后变得不可解码。
  • 在一个 API 版本中具有默认值的任何字段在所有 API 版本中都必须具有_非零_默认值。这可以分为2种情况:
    • 为现有的非默认字段添加具有默认值的新 API 版本:需要添加语义上等同于在所有以前的 API 版本中未设置的默认值,以保留未设置值的语义含义。
    • 添加具有默认值的新字段:默认值在所有当前支持的 API 版本中必须在语义上等效。

不兼容的 API 更改

有时,不兼容的更改可能没问题,但大多数情况下我们希望更改满足上述定义。如果您认为需要破坏兼容性,您应该首先与 Kubernetes API 审阅者交谈。

破坏测试版或稳定 API 版本(例如 v1)的兼容性是不可接受的。实验性或 alpha API 的兼容性并不是严格要求的,但不应该轻易破坏兼容性,因为它会破坏该功能的所有用户。 Alpha 和 Beta API 版本可能会被弃用并最终批量删除,如弃用政策中所述。

如果您的更改将向后不兼容或者可能对 API 使用者来说是重大更改,请在更改生效之前向[email protected]发送公告。如果您不确定,请询问。还要确保通过使用“release-note-action-required”github 标签标记 PR,将更改记录在下一个版本的发行说明中。

如果您发现您的更改意外破坏了客户端,则应将其恢复。

简而言之,预期的 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 版本中添加默认值,而不是在旧 API 版本中保留新字段未设置(例如nil )。这是必需的,因为每当读取序列化版本时就会发生默认设置(请参阅#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 秒,零值要求系统选择默认值。

不要忘记运行测试!

编辑 conversion.go

鉴于您尚未更改内部结构,这可能感觉为时过早,但事实确实如此。您还没有任何可转换的内容。我们将在“内部”部分重新讨论这一点。如果您以不同的顺序执行这一切(即您从内部结构开始),那么您应该跳到下面的该主题。在极少数情况下,您正在进行不兼容的更改,您现在可能想也可能不想这样做,但稍后您必须做更多的事情。你想要的文件是 pkg/apis/<group>/<version>/conversion.gopkg/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 包含自动生成的转换函数

由于自动生成的转换函数使用手动编写的函数,因此手动编写的转换函数应按照定义的约定命名,即将 pkg a中的类型X转换为 pkg b中的类型Y函数应命名为: 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.gozz_generated.defaults.goapi/openapi-spec/swagger.json

如果由于编译错误而无法重新生成,最简单的解决方法是删除导致错误的文件并重新运行命令。

生成代码

除了defaulter-gendeepcopy-genconversion-genopenapi-gen之外,还有一些其他生成器:

  • go-to-protobuf
  • client-gen
  • lister-gen
  • informer-gen
  • codecgen (用于使用ugorji编解码器进行快速json序列化)

许多生成器都基于gengo并共享公共标志。 --verify-only标志将检查磁盘上的现有文件,如果它们不是本来会生成的文件,则会失败。

创建 go 代码的生成器有一个--go-header-file标志,该标志应该是包含应包含的标头的文件。此标头是应出现在生成文件顶部的版权,并应与构建后期阶段的脚本。

要调用这些生成器,您可以运行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需要在内部pkg/apis/<group>/types.go以及每个特定版本中的每个导出类型上// +genclient注释 staging/src/k8s.io/api/<group>/<version>/types.go

如果 apiserver 将您的 API 托管在与文件系统中的<group>不同的组名下(通常这是因为文件系统中的<group>省略了“k8s.io”后缀,例如,admission 与 grant)。 k8s.io),您可以通过在内部pkg/apis/<group>/doc.go doc.go中添加// +groupName=注释来指示client-gen使用正确的组名称就像每个特定版本一样 staging/src/k8s.io/api/<group>/<version>/types.go

添加注释后,使用以下命令生成客户端

请注意,您可以使用可选的// +groupGoName=指定 CamelCase 自定义 Golang 标识符来消除冲突,例如policy.authorization.k8s.iopolicy.k8s.io 。这两个都将映射到客户端集中的Policy()

client-gen 很灵活。如果您需要非 kubernetes API 的 client-gen,请参阅此文档

生成列表

lister-gen是一个为客户端生成列表的工具。它重用了//+genclient// +groupName=注释,因此您不需要指定额外的注释。

您之前运行的hack/update-codegen.sh已调用lister-gen

生成告密者

informer-gen生成非常有用的 Informer,用于监视 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 很有帮助,以便审阅者更容易看到两个版本之间发生了什么变化:

  1. 只是复制的提交 pkg/apis/<group>/<existing-version>staging/src/k8s.io/api/<group>/<existing-version> 包到<new-version>
  2. 将新文件中的<existing-version>重命名为<new-version>的提交。
  3. <new-version>进行任何新更改的提交。
  4. 包含运行make generated_filesmake update等生成的文件的提交。

由于项目的快速变化性质,以下内容可能已过时:

您需要按照上面部分中的说明重新生成生成的代码。

测试

需要对测试进行一些更新。

创建一个新的 API 组

您必须在pkg/apis/staging/src/k8s.io/api下创建一个新目录;复制现有 API 组的目录结构,例如pkg/apis/authenticationstaging/src/k8s.io/api/authentication ;将“authentication”替换为您的组名,并将 versions 替换为您的版本;将版本化内部的register.go 和install.go中的 API 类型替换为您的类型。

您必须将 API 组/版本添加到代码库中的几个位置,如创建新的 API 版本部分所述。

您需要按照上面部分中的说明重新生成生成的代码。

更新模糊器

我们的 API 测试方案的一部分是“模糊”(用随机值填充)API 对象,然后将它们与不同的 API 版本相互转换。这是揭露您丢失信息或做出错误假设的地方的好方法。

模糊器的工作原理是创建一个随机 API 对象并调用自定义模糊器函数 pkg/apis/$GROUP/fuzzer/fuzzer.go 。然后,生成的对象从一个 api 版本往返到另一个版本,并验证是否与开始时的版本相同。在此过程中不会运行验证,但会运行默认值。

如果您添加了任何需要非常仔细格式化的字段(测试不运行验证),或者如果您在默认设置期间做出了假设,例如“此切片将始终具有至少 1 个元素”,您可能会收到错误甚至恐慌从 k8s.io/kubernetes/pkg/api/testing.TestRoundTripTypes./pkg/api/testing/serialization_test.go

如果您默认任何字段,则必须在自定义模糊器功能中检查该字段,因为模糊器可能会将某些字段留空。如果您的对象具有结构引用,模糊器可能会将其保留为零,或者可能会创建一个随机对象。您的自定义模糊器函数必须确保默认设置不会进一步更改对象,因为这将在往返测试中显示为差异。

最后,模糊测试在没有任何功能门配置的情况下运行。如果默认或其他行为位于功能门之后,请注意当功能门默认打开时,模糊行为将会改变。

更新语义比较

很少需要这样做,但是当它发生时,就会很痛苦。在某些罕见的情况下,我们最终得到的对象(例如资源数量)具有道德上等效的值,但具有不同的按位表示形式(例如,使用基数 2 格式化程序的值 10 与使用基数 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 默默地删除了未知字段。你很聪明——你会明白的。充分利用grepack

如果您添加了功能,您应该考虑记录它和/或编写一个示例来说明您的更改。

确保通过运行以下命令更新 swagger 和 OpenAPI 规范:

API 规范更改应与其他更改分开提交。

Alpha、Beta 和稳定版本

新功能的开发经历了一系列日益成熟的阶段:

  • 发展水平
    • 对象版本控制:无约定
    • 可用性:未提交到主 kubernetes 存储库,因此在官方版本中不可用
    • 受众:在功能或概念验证方面密切合作的其他开发人员
    • 可升级性、可靠性、完整性和支持:无要求或保证
  • 阿尔法级
    • 对象版本控制:API 版本名称包含alpha (例如v1alpha1
    • 可用性:致力于主要的 kubernetes 存储库;出现在正式版本中;该功能默认禁用,但可以通过标志启用
    • 受众:有兴趣提供功能早期反馈的开发人员和专家用户
    • 完整性:某些API操作、CLI命令或UI支持可能未实现; API 不需要进行_API 审查_(在正常的代码审查之上对 API 进行深入且有针对性的审查)
    • 可升级性:对象模式和语义可能会在以后的软件版本中发生变化,而无需在现有集群中保留对象;消除可升级性问题使开发人员能够取得快速进展;特别是,API 版本可以比次要发布节奏更快地增加,并且开发人员无需维护多个版本;当对象架构或语义以不兼容的方式更改时,开发人员仍应增加 API 版本
    • 集群可靠性:由于该功能相对较新,并且可能缺乏完整的端到端测试,因此通过标志启用该功能可能会暴露导致集群不稳定的错误(例如,控制循环中的错误可能会快速创建过多的对象,耗尽 API 存储)。
    • 支持:项目_没有承诺_完成该功能;该功能可能会在以后的软件版本中完全删除
    • 推荐用例:由于可升级性的复杂性以及缺乏长期支持和可升级性,仅在短期测试集群中使用。
  • 测试版级别:
    • 对象版本控制:API 版本名称包含beta (例如v2beta3
    • 可用性:在官方 Kubernetes 版本中; API 默认情况下处于禁用状态,但可以通过标志启用。 (注意:默认情况下启用 v1.24 之前引入的 beta API,但对于新的 beta API,情况发生了变化
    • 受众:有兴趣提供功能反馈的用户
    • 完整性:所有API操作、CLI命令和UI支持都应该实现;端到端测试完成;该 API 已经过彻底的 API 审查,并且被认为是完整的,尽管在测试期间使用可能会经常出现审查期间未考虑到的 API 问题
    • 可升级性:对象模式和语义可能会在以后的软件版本中发生变化;当发生这种情况时,将记录升级路径;在某些情况下,对象会自动转换为新版本;在其他情况下,可能需要手动升级;手动升级可能需要对依赖新功能的任何内容进行停机,并且可能需要将对象手动转换为新版本;当需要手动转换时,项目将提供该过程的文档
    • 集群可靠性:由于该功能具有端到端测试,因此通过标志启用该功能不应在不相关的功能中创建新的错误;由于该功能是新功能,因此可能存在小错误
    • 支持:项目承诺在后续稳定版本中以某种形式完成该功能;通常这会在 3 个月内发生,但有时会更长;版本应同时支持两个连续版本(例如v1beta1v1beta2 ;或v1beta2v1 )至少一个次要发布周期(通常为 3 个月),以便用户有足够的时间升级和迁移对象
    • 推荐用例:在短期测试集群中;在生产集群中,作为功能短期评估的一部分,以便提供反馈
  • 稳定水平:
    • 对象版本控制:API 版本vX ,其中X是整数(例如v1
    • 可用性:在官方 Kubernetes 版本中,默认启用
    • 受众:所有用户
    • 完整性:必须在适当的一致性配置文件中进行经 SIG Architecture 批准的一致性测试(例如,不可移植和/或可选功能可能不在默认配置文件中)
    • 可升级性:后续软件版本中仅允许严格兼容的更改
    • 集群可靠性:高
    • 支持: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 字段,并确保默认情况下禁用它:

  1. 向 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},
    }
  2. 将字段添加到 API 类型:

    • 确保该字段是可选的
      • 添加omitempty结构体标签
      • 添加// +optional注释标签
      • 添加// +featureGate=<gate-name>注释标签
      • 确保该字段在空时完全不存在于 API 响应中(可选字段必须是指针)
    • 在字段描述中包含有关 alpha 级别的详细信息
    // API v6.
    type Frobber struct {
      // height ...
      Height int32  `json:"height"`
      // param ...
      Param  string `json:"param"`
      // width indicates how wide the object is.
      // This field is alpha-level and is only honored by servers that enable the Frobber2D feature.
      // +optional
      // +featureGate=Frobber2D
      Width  *int32 `json:"width,omitempty"`
    }
  3. 在将对象持久存储到存储之前,请在创建和更新时清除禁用的 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
      }
    }
  4. 为了让您的 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 ...
      })
    }
  5. 在验证中,验证该字段(如果存在):

    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 对象,正确处理它或故障安全。例如,kubelet 使用的Pod枚举字段中的新允许值必须由比允许新值的第一个 API 服务器版本早三个版本的 kubelet 安全处理。
  • 如果 API 驱动外部客户端(例如IngressNetworkPolicy )实现的行为,则枚举字段必须明确指示将来可能允许使用其他值,并定义客户端必须如何处理无法识别的值。如果在包含枚举字段的第一个版本中没有这样做,则添加可能破坏现有客户端的新值是不安全的。

如果预期的 API 客户端安全地处理新的枚举值,下一个要求是开始以不破坏先前 API 服务器对该对象的验证的方式允许它。这需要至少两个版本才能安全完成:

版本 1:

  • 仅在更新已包含新枚举值的现有对象时才允许使用新枚举值
  • 在其他情况下禁止它(创建和更新尚未包含新枚举值的对象)
  • 验证已知客户端是否按预期处理新值、遵守新值或使用先前定义的“未知值”行为(取决于是否启用关联的功能门)

版本 2:

  • 允许在创建和更新场景中使用新的枚举值

这确保了具有倾斜版本的多个服务器的集群(在滚动升级期间发生)不会允许保留先前版本的 API 服务器会阻塞的数据。

通常,功能门用于执行此部署,从 alpha 版本开始,在版本 1 中默认禁用,然后升级到 beta 版本,在版本 2 中默认启用。

  1. 向 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},
    }
  2. 更新有关 API 类型的文档:

    • 在字段描述中包含有关 alpha 级别的详细信息
    type Frobber struct {
      // restartPolicy may be set to "Always" or "Never" (or "OnTuesday" if the alpha "FrobberRestartPolicyOnTuesday" feature is enabled).
      // Additional policies may be defined in the future.
      // Unrecognized policies should be treated as "Never".
      RestartPolicy string `json:"policy"
    }
  3. 验证对象时,确定是否应允许新的枚举值。这可以防止在禁用该功能时重新使用新值,同时确保保留现有数据。需要确保保留现有数据,以便在未来版本_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
    }
  4. 在验证中,根据传入的选项验证枚举值:

    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()))
      }
      ...
    }
  5. 至少发布一个版本后,该功能可以升级为 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 版本

另一种选择是引入带有新的alphabeta版本指示符的新类型,如下所示:

// API v7alpha1
type Frobber struct {
  // height ...
  Height *int32 `json:"height"`
  // param ...
  Param  string `json:"param"`
  // width ...
  Width  *int32 `json:"width,omitempty"`
}

后者要求在新版本v7alpha1中复制与Frobber相同 API 组中的所有对象。这还要求用户使用使用其他版本的新客户端。因此,这不是优选的选择。

一个相关的问题是集群管理器如何从用户已经使用的具有新功能的新版本进行回滚。看 kubernetes/kubernetes#4855