本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

介绍 client-go 版本 6

Kubernetes API 服务器公开了一个可供任何客户端使用的 REST 接口client-go 是 Go 编程语言的官方客户端库。它既被 Kubernetes 内部使用(例如,在 kubectl 中),也被众多外部消费者使用:如 etcd-operatorprometheus-operator 等操作符;如 KubeLessOpenShift 等更高级的框架;以及更多。

client-go 的版本 6 更新增加了对 Kubernetes 1.9 的支持,允许访问最新的 Kubernetes 功能。虽然更新日志包含了所有详细信息,但这篇博文将重点介绍最突出的变化,并旨在指导如何从版本 5 升级。

这篇博文是使 client-go 更易于第三方消费者使用的一系列努力之一。更便捷的访问是许多来自不同公司的人员共同努力的成果,他们都在 Kubernetes Slack 的 #client-go-docs 频道中开会。我们很高兴听到改进的反馈和想法,当然也感谢所有愿意贡献的人。

API 组变更

以下 API 组的提升是 Kubernetes 1.9 的一部分

  • 工作负载对象(Deployment、DaemonSet、ReplicaSet 和 StatefulSet)已在 Kubernetes 1.9 中提升到 apps/v1 API 组。client-go 遵循此转换,允许开发者通过导入 k8s.io/api/apps/v1 包而不是 k8s.io/api/apps/v1beta1 并使用 Clientset.AppsV1() 来使用最新版本。
  • 准入 Webhook 注册已在 Kubernetes 1.9 中提升到 admissionregistration.k8s.io/v1beta1 API 组。旧的 ExternalAdmissionHookConfiguration 类型已被不兼容的 ValidatingWebhookConfiguration 和 MutatingWebhookConfiguration 类型取代。此外,admission.k8s.io 中的 webhook 准入负载类型 AdmissionReview 已提升到 v1beta1。请注意,版本化对象现在传递给 webhook。有关详细信息,请参阅准入 webhook 文档

自定义资源的验证

在 Kubernetes 1.8 中,我们引入了 CustomResourceDefinitions (CRD) 持久化前模式验证作为 Alpha 功能。在 1.9 中,该功能提升为 Beta 版,并将默认启用。作为 client-go 用户,您将在 k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1 中找到 API 类型。

OpenAPI v3 模式可以在 CRD 规范中定义,如下所示:


apiVersion: apiextensions.k8s.io/v1beta1  
kind: CustomResourceDefinition  
metadata: ...  
spec:  
  ...  
  validation:  
    openAPIV3Schema:  
      properties:  
        spec:  
          properties:  
            version:  
                type: string  
                enum:  
                - "v1.0.0"  
                - "v1.0.1"  
            replicas:  
                type: integer  
                minimum: 1  
                maximum: 10

上述 CRD 中的 schema 对实例应用以下验证:

  1. spec.version 必须是字符串,且必须是“v1.0.0”或“v1.0.1”。
  2. spec.replicas 必须是整数,且最小值必须为 1,最大值必须为 10。具有无效 spec.version (v1.0.2) 和 spec.replicas (15) 值的 CustomResource 将被拒绝。

apiVersion: mygroup.example.com/v1  
kind: App  
metadata:  
  name: example-app  
spec:  
  version: "v1.0.2"  
  replicas: 15
$ kubectl create -f app.yaml

The App "example-app" is invalid: []: Invalid value: map[string]interface {}{"apiVersion":"mygroup.example.com/v1", "kind":"App", "metadata":map[string]interface {}{"creationTimestamp":"2017-08-31T20:52:54Z", "uid":"5c674651-8e8e-11e7-86ad-f0761cb232d1", "clusterName":"", "name":"example-app", "namespace":"default", "deletionTimestamp":interface {}(nil), "deletionGracePeriodSeconds":(\*int64)(nil)}, "spec":map[string]interface {}{"replicas":15, "version":"v1.0.2"}}:
validation failure list:  
spec.replicas in body should be less than or equal to 10  
spec.version in body should be one of [v1.0.0 v1.0.1]

请注意,通过 Admission Webhooks,Kubernetes 1.9 提供了另一个 Beta 功能,用于在创建或更新对象之前对其进行验证。从 1.9 开始,这些 webhook 还允许修改对象(例如,设置默认值或注入值)。当然,webhook 也适用于 CRD。此外,webhook 可用于实现 CRD 验证不易表达的验证。请注意,webhook 比 CRD 验证更难实现,因此对于许多目的来说,CRD 验证是正确的工具。

创建命名空间内 Informer

通常,一个命名空间中的对象或者只具有某些标签的对象需要在控制器中进行处理。Informer 现在允许您调整用于查询 API 服务器以列出和观察对象的 ListOptions。未初始化的对象(供初始化器使用)可以通过将 IncludeUninitialized 设置为 true 来使其可见。所有这些都可以通过新的 NewFilteredSharedInformerFactory 构造函数实现共享 informer。


import “k8s.io/client-go/informers”
...  
sharedInformers := informers.NewFilteredSharedInformerFactory(  
 client,  
 30\*time.Minute,   
 “some-namespace”,  
 func(opt \*metav1.ListOptions) {  
  opt.LabelSelector = “foo=bar”  
 },  
)  

请注意,相应的 lister 将只知道与命名空间和给定 ListOptions 匹配的对象。请注意,相同的限制适用于客户端上的 List 或 Watch 调用。

这个来自 cert-manager 的生产代码示例展示了如何在实际代码中使用命名空间 informer。

多态伸缩客户端

历史上,只有 extensions API 组中的类型才能与自动生成的 Scale 客户端一起使用。此外,不同的 API 组为其 /scale 子资源使用不同的 Scale 类型。为了解决这些问题,k8s.io/client-go/scale 提供了一个多态伸缩客户端,以一致的方式伸缩不同 API 组中的不同资源。


import (


apimeta "k8s.io/apimachinery/pkg/api/meta"

 discocache "k8s.io/client-go/discovery/cached"  
 "k8s.io/client-go/discovery"

"k8s.io/client-go/dynamic"

“k8s.io/client-go/scale”  
)

...

cachedDiscovery := discocache.NewMemCacheClient(client.Discovery())  
restMapper := discovery.NewDeferredDiscoveryRESTMapper(

cachedDiscovery,

apimeta.InterfacesForUnstructured,

)  
scaleKindResolver := scale.NewDiscoveryScaleKindResolver(

client.Discovery(),

)  
scaleClient, err := scale.NewForConfig(

client, restMapper,

dynamic.LegacyAPIPathResolverFunc,

scaleKindResolver,

)
scale, err := scaleClient.Scales("default").Get(groupResource, "foo")

返回的伸缩对象是通用的,并作为 autoscaling/v1.Scale 对象公开。它由一个内部 Scale 类型支持,其中定义了所有支持伸缩的 API 组中特殊 Scale 类型之间的转换。我们计划在 1.10 中将其扩展到 CustomResources

如果您正在实现对 scale 子资源的支持,我们建议您公开 autoscaling/v1.Scale 对象。

类型安全的深拷贝

以前,深度复制对象需要调用 Scheme.Copy(Object),其显著缺点是会失去类型安全性。client-go 版本 5 中的典型代码片段需要类型转换:


newObj, err := runtime.NewScheme().Copy(node)


if err != nil {

    return fmt.Errorf("failed to copy node %v: %s”, node, err)

}


newNode, ok := newObj.(\*v1.Node)

if !ok {

    return fmt.Errorf("failed to type-assert node %v", newObj)


}

感谢 k8s.io/code-generator,Copy 现在已被每个对象上的类型安全 DeepCopy 方法取代,允许您在代码量和 API 错误表面方面显着简化代码。

newNode := node.DeepCopy()

无需错误处理:此调用绝不会失败。当且仅当节点为 nil 时,DeepCopy() 返回 nil。

为了复制 runtime.Objects,runtime.Object 接口中还有一个额外的 DeepCopyObject() 方法。

由于旧方法已被删除,客户端需要相应地更新其复制调用。

代码生成和自定义资源

不鼓励使用 client-go 的动态客户端访问自定义资源,而是由使用 k8s.io/code-generator 中生成器的类型安全代码取代。请查看 Open Shift 博客上的深度解析,了解如何将代码生成与 client-go 结合使用。

注释块

您现在可以将标签放置在类型或函数正上方的注释块中,或在其上方的第二个块中。这两个注释块之间不再有任何区别。这曾是使用生成器时出现细微错误的原因。

// second block above  
// +k8s:some-tag  

// first block above  
// +k8s:another-tag  
type Foo struct {}

自定义客户端方法

您现在可以使用扩展标签定义来创建自定义动词。这使您能够超越 HTTP 定义的动词。这为更高级别的自定义打开了大门。

例如,此块导致生成方法 UpdateScale(s *autoscaling.Scale) (*autoscaling.Scale, error)。

// genclient:method=UpdateScale,verb=update,subresource=scale,input=k8s.io/kubernetes/pkg/apis/autoscaling.Scale,result=k8s.io/kubernetes/pkg/apis/autoscaling.Scale

解决 Golang 命名冲突

在更复杂的 API 组中,Kind、组名、Go 包名和 Go 组别名可能存在冲突。在 1.9 之前,这并未正确处理。以下标签解决了命名冲突并使生成的代码更美观:

// +groupName=example2.example.com  
// +groupGoName=SecondExample

这些通常位于 API 包的 doc.go 文件中。第一个用作 CustomResource 组名,当使用 HTTP 以 RESTful 方式与 API 服务器通信时使用。第二个用于生成的 Golang 代码(例如,在 clientset 中)以访问组版本。

clientset.SecondExampleV1()

最后,Go 包名中可以包含点。在本节的示例中,您需要将 groupName 片段放入项目的 pkg/apis/example2.example.com 目录中。

示例项目

Kubernetes 1.9 包含了一些示例项目,可以作为您自己项目的蓝图。

依赖管理

为了从 client-go 的上一个版本 5 更新到版本 6,库本身以及某些第三方依赖项都必须更新。以前,这个过程很繁琐,因为很多代码在现有包布局中跨版本进行了重构或重新定位。幸运的是,最新版本中需要移动的代码少得多,这应该会简化大多数用户的升级过程。

已发布仓库的状态

过去,k8s.io/client-gok8s.io/apik8s.io/apimachinery 更新不频繁。标签(例如 v4.0.0)在 Kubernetes 发布后很久才创建。随着 1.9 版本的发布,我们恢复了运行夜间机器人,该机器人会自动更新所有仓库供公众使用,甚至在手动标记之前。这包括以下分支:

  • master
  • release-1.8 / release-5.0
  • release-1.9 / release-6.0 Kubernetes 标签(例如 v1.9.1-beta1)也会自动应用到已发布的仓库,前缀为 kubernetes-(例如 kubernetes-1.9.1-beta1)。

这些标签的测试覆盖率有限,但可以供 client-go 和其他库的早期采用者使用。此外,它们有助于管理 k8s.io/apik8s.io/apimachinery 的正确版本。请注意,我们只在 k8s.io/client-go 上创建了 v6.0.3 类似的语义版本标签。k8s.io/api 和 k8s.io/apimachinery 的相应标签是 kubernetes-1.9.3。

另请注意,只有这些标签才对应经过测试的 Kubernetes 版本。如果您依赖于发布分支,例如 release-1.9,您的客户端正在运行未发布的 Kubernetes 代码。

client-go 的依赖管理现状

一般来说,要管理的依赖项列表是自动生成的,并写入文件 Godeps/Godeps.json。只有其中列出的修订版经过测试。这意味着我们不且无法针对依赖项的主分支测试代码库。根据使用的依赖管理工具,我们处于以下情况:

  • godep 通过在您的 GOPATH 中从 k8s.io/client-go 运行 godep restore 来读取 Godeps/Godeps.json。然后使用 godep save 在您的项目中进行依赖管理。godep 将从您的 GOPATH 中选择正确的版本。
  • glide 会在初始化和更新时自动从其依赖项(包括 k8s.io/client-go)中读取 Godeps/Godeps.json。因此,只要没有冲突,glide 大部分应该是自动的。
  • dep 目前无法以一致的方式遵守 Godeps/Godeps.json,尤其是在更新时。至关重要的是手动将 client-go 依赖项指定为约束或覆盖,也适用于非 k8s.io/* 依赖项。如果没有这些,dep 只会选择依赖项的主分支,这可能会导致问题,因为它们会频繁更新。
  • Kubernetes 和 golang/dep 社区已经意识到这些问题 [问题 #1124, 问题 #1236],并且正在共同努力寻找解决方案。在此之前,必须特别小心。有关详细信息,请参阅 client-go 的 INSTALL.md

更新依赖项 – golang/dep

即使 golang/dep 目前存在不足,dep 仍在逐渐成为 Go 生态系统中事实上的标准。只要小心谨慎并意识到其缺失的功能,dep 就可以(而且已经!)成功使用。以下是演示如何使用 dep 将 client-go 5 的项目更新到最新版本 6 的方法:

(如果您仍在运行 client-go 版本 4,并且希望通过不跳过发布来确保安全,那么现在是查看这篇由 Heptio 的朋友们整理的优秀博文的好时机,该博文介绍了如何升级到版本 5。)

在开始之前,重要的是要了解 client-go 依赖于另外两个 Kubernetes 项目:k8s.io/apimachineryk8s.io/api。此外,如果您正在使用 CRD,您可能还依赖 k8s.io/apiextensions-apiserver 来获取 CRD 客户端。第一个公开了较低级别的 API 机制(例如方案、序列化和类型转换),第二个持有 API 定义,第三个提供与 CustomResourceDefinitions 相关的 API。为了使 client-go 正确运行,它需要将其配套库以相应匹配的版本进行 vendoring。每个库仓库都提供一个名为 release-<version> 的分支,其中 <version> 指的是特定的 Kubernetes 版本;对于 client-go 版本 6,必须引用每个仓库上的 release-1.9 分支。

假设 client-go 的最新版本 5 补丁版本是通过 dep 托管的,Gopkg.toml 清单文件应该如下所示(可能使用分支而不是版本):






[[constraint]]


  name = "k8s.io/api"

  version = "kubernetes-1.8.1"


[[constraint]]

  name = "k8s.io/apimachinery"

  version = "kubernetes-1.8.1"


[[constraint]]

  name = "k8s.io/apiextensions-apiserver"

  version = "kubernetes-1.8.1"


[[constraint]]

  name = "k8s.io/client-go"




  version = "5.0.1"

请注意,如果客户端实际不需要某些库,则这些库可能会缺失。

升级到 client-go 版本 6 意味着按照以下方式增加版本和标签标识符(已**加粗**显示):






[constraint]]


  name = "k8s.io/api"

  version = "kubernetes-1.9.0"


[[constraint]]

  name = "k8s.io/apimachinery"

  version = "kubernetes-1.9.0"


[[constraint]]

  name = "k8s.io/apiextensions-apiserver"

  version = "kubernetes-1.9.0"


[[constraint]]

  name = "k8s.io/client-go"




  version = "6.0.0"

升级结果可在此处找到。

需要注意:如上所述,dep 无法以可靠和可重现的方式捕获完整的依赖项集。这意味着对于一个 100% 具备未来适应性的项目,您必须为 client-go 的 Godeps/Godeps.json 中列出的许多其他包添加约束(甚至覆盖)。如果出现问题,请准备好添加它们。我们正在与 golang/dep 社区合作,以使这一体验更轻松、更顺畅。

最后,我们需要通过执行 dep ensure 命令来告诉 dep 升级到指定的版本。如果一切顺利,命令调用的输出应该为空,唯一表明成功的是 vendor 文件夹中更新了一些文件。

如果您正在使用 CRD,您可能也会使用代码生成。以下 Gopkg.toml 代码块将向您的项目添加所需的代码生成包:


required = [  
  "k8s.io/code-generator/cmd/client-gen",  
  "k8s.io/code-generator/cmd/conversion-gen",  
  "k8s.io/code-generator/cmd/deepcopy-gen",  
  "k8s.io/code-generator/cmd/defaulter-gen",  
  "k8s.io/code-generator/cmd/informer-gen",  
  "k8s.io/code-generator/cmd/lister-gen",  
]


[[constraint]]

  branch = "kubernetes-1.9.0"


  name = "k8s.io/code-generator"

此时,您是否希望通过 dep 修剪不需要的包(例如测试文件)或将更改提交到版本控制系统,这取决于您——但从升级的角度来看,您现在应该已经准备好通过 client-go 利用 Kubernetes 1.9 带来的所有酷炫新功能。