作者 | Jason Snouffer
译者 | Luga Lee
策划 | Luga Lee
通常,在某些情况下,我们需要通用的方法去使用 Kubernetes 资源对象,而不是编写代码来处理特定类型。 比如,如下一些简单的用例参考场景:
1、使用来自没有关联 Golang 结构的插件的 K8s API 对象。
2、使用 JsonPath、JMESPath、jq 等对 K8s 对象执行通用 CRUD(创建/读取/更新/删除)操作。需要一种通用方法以避免必须编写显式代码来处理每种可能的资源类型。
API Machinery SIG
Kubernetes 社区中的许多项目都由特别兴趣小组 (SIG) 管理。API Machinery SIG 用于维护与 K8s API 服务器接口的客户端库,包括用于通用 API CRUD 语义的包。API Machinery 的一个著名子项目 client-go,其是一个用于与 K8s API 服务器交互的官方 Go API。
client-go 最常见的入口点是 kubernetes.Clientset,一组类型化的客户端,为每个核心资源类型(Pod、部署、服务等)提供预先生成的本地 API 对象。基于其易用性,建议大家尽可能使用此入口点。但是,使用类型化客户端可能会受到很大限制,因为代码往往与所使用的特定类型及版本紧密耦合。
client-go/dynamic 和非结构化对象
API Machinery 的 universal-machinery 子项目维护了一个共享依赖库,供服务器和客户端使用 Kubernetes API 基础设施,没有直接的类型依赖。非结构化包是这个共享依赖库的一部分,允许对 K8s 资源对象进行通用操作。
struct unstructured.Unstructured 是一种简单类型,它使用一组嵌套的 map[string]interface{} 值来创建一个内部结构,该结构与来自 K8s API 服务器的 REST 负载非常相似。
client-go/dynamic 包提供了一个动态客户端,可以对任意 API 资源执行 RESTful 操作。struct dynamic.Interface 使用 unstructured.Unstructured 来表示来自 API 服务器的所有对象值。动态包将所有数据绑定推迟到运行时。
基本示例
以下代码示例需要依赖项 k8s.io/client-go/kubernetes 和 sigs.k8s.io/controller-runtime。controller-runtime 项目是一组用于构建 Kubernetes 操作符的库。可以在没有控制器运行时的情况下使用 client-go,但简化了为 K8s API 服务器访问配置 client-go 客户端。
在为 API 访问配置 client-go 时,有两种常见的配置方法。在 Pod 内运行时使用集群内配置,并使用挂载到 Pod 的服务帐户令牌。在集群外运行时使用集群外配置,并使用提供的 kubeconfig 文件或当前用户的默认 kubeconfig 文件。控制器运行时库提供了一个方便的多合一函数 GetConfig(),它首先尝试集群外配置,如果失败,则尝试集群内配置。
要将所需的依赖项添加到 Go 项目,请执行以下命令:
go get k8s.io/client-go/kubernetes go get sigs.k8s.io/controller-runtime
以下示例在功能上等效,但演示了使用类型化客户端与动态客户端时的语义差异。
使用 kubernetes.Clientset 获取 K8s 对象
以下代码片段定义了一个函数,用于使用来自 kubernetes.Clientset 的类型化部署客户端检索 K8s 部署对象。
package main import ( "context" "fmt" v1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ctrl "sigs.k8s.io/controller-runtime" ) func main() { ctx := context.Background() config := ctrl.GetConfigOrDie() clientset := kubernetes.NewForConfigOrDie(config) namespace := "default" items, err := GetDeployments(clientset, ctx, namespace) if err != nil { fmt.Println(err) } else { for _, item := range items { fmt.Printf("%+v\n", item) } } } func GetDeployments(clientset *kubernetes.Clientset, ctx context.Context, namespace string) ([]v1.Deployment, error) { list, err := clientset.AppsV1().Deployments(namespace). List(ctx, metav1.ListOptions{}) if err != nil { return nil, err } return list.Items, nil }
使用 dynamic.Interface 获取 K8s 对象
以下代码片段定义了一个使用动态客户端检索 K8s 对象的函数。 调用该函数以检索默认命名空间中的部署列表。
import ( "context" "fmt" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" ctrl "sigs.k8s.io/controller-runtime" ) func main() { ctx := context.Background() config := ctrl.GetConfigOrDie() dynamic := dynamic.NewForConfigOrDie(config) namespace := "default" items, err := GetResourcesDynamically(dynamic, ctx, "apps", "v1", "deployments", namespace) if err != nil { fmt.Println(err) } else { for _, item := range items { fmt.Printf("%+v\n", item) } } } func GetResourcesDynamically(dynamic dynamic.Interface, ctx context.Context, group string, version string, resource string, namespace string) ( []unstructured.Unstructured, error) { resourceId := schema.GroupVersionResource{ Group: group, Version: version, Resource: resource, } list, err := dynamic.Resource(resourceId).Namespace(namespace). List(ctx, metav1.ListOptions{}) if err != nil { return nil, err } return list.Items, nil }
在这两个示例中,我们很明显看到,使用类型化客户端来处理 K8s 对象更简单,代码更少。不过,动态方法更加强大和灵活,尤其是当资源类型事先未知或需要使用缺少关联 Golang 结构的自定义资源定义时。
高级示例
真正受益于动态客户端提供的灵活性的用例是使用 jq 评估或改变 K8s 对象。 对于 JSON 数据,Jq 就像 sed、awk 和 grep。 它是 Kubectl 的有用伴侣,简化了 K8s 对象的读取、解析和变异。
在这种情况下,为遇到的每个资源类型编写显式类型处理可能会很乏味。此外,可能无法提前知道所有可能遇到的资源类型。
此代码示例使用 github.com/itchyny/gojq,这是 jq 的纯 Go 实现。要将所需的依赖项添加到 Go 项目,请执行以下命令:
go get github.com/itchyny/gojq
检查特定标签的 Kubernetes 对象
以下代码片段重用了上一个示例中的 GetResourcesDynamically 函数来获取默认命名空间中的部署列表。然后检查每个部署是否使用 jq 将标签 app.kubernetes.io/managed-by 设置为 Helm 值。
为了能够进行 jq 评估,必须将从 API 服务器返回的对象转换为 JSON。k8s.io/apimachinery/pkg/runtime 包通过在 runtime.DefaultUnstructuredConverter 提供一个非结构化到 JSON 的转换器来简化这个过程。一旦转换为 JSON,就会执行 jq 评估,如果它返回一个布尔结果并且结果为“true”,则将 K8s 对象添加到函数返回的切片中。
package main import ( "context" "fmt" "github.com/itchyny/gojq" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/dynamic" ctrl "sigs.k8s.io/controller-runtime" ) func main() { ctx := context.Background() config := ctrl.GetConfigOrDie() dynamic := dynamic.NewForConfigOrDie(config) namespace := "default" query := ".metadata.labels[\"app.kubernetes.io/managed-by\"] == \"Helm\"" items, err := GetResourcesByJq(dynamic, ctx, "apps", "v1", "deployments", namespace, query) if err != nil { fmt.Println(err) } else { for _, item := range items { fmt.Printf("%+v\n", item) } } } func GetResourcesByJq(dynamic dynamic.Interface, ctx context.Context, group string, version string, resource string, namespace string, jq string) ( []unstructured.Unstructured, error) { resources := make([]unstructured.Unstructured, 0) query, err := gojq.Parse(jq) if err != nil { return nil, err } items, err := GetResourcesDynamically(dynamic, ctx, group, version, resource, namespace) if err != nil { return nil, err } for _, item := range items { // Convert object to raw JSON var rawJson interface{} err = runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, &rawJson) if err != nil { return nil, err } // Evaluate jq against JSON iter := query.Run(rawJson) for { result, ok := iter.Next() if !ok { break } if err, ok := result.(error); ok { if err != nil { return nil, err } } else { boolResult, ok := result.(bool) if !ok { fmt.Println("Query returned non-boolean value") } else if boolResult { resources = append(resources, item) } } } } return resources, nil }
再一次,使用类型化客户端可以更简单地完成上面的示例,并使用更少的代码。不过,这是因为我们知道我们正在处理部署并查看 Kubernetes 元数据,这在所有对象类型中都很常见。然而,设想一下,如果我们正在编写一个可以评估任何对象类型中的任何字段的函数,我们将需要多少代码。如果没有动态客户端的能力、对底层 JSON 内容的访问以及 jq,那将是一项无法完成的任务。
概括
在这篇文章中,我们使用 API machinery 子项目 client-go 提供的类型化和动态客户端评估了在 Go 中使用实时 Kubernetes 对象的情况。对于基本用例,类型化客户端提供对 K8s 对象的简单、优雅的访问。但是,如果对象类型很多,或者在类型之前不知道特定的对象类型,或者对象类型来自缺乏关联 Golang 结构体的第三方资源,那么动态客户端则提供了所需的灵活性。
# 参考