使用 clientcmd 实现统一的 API 服务器访问

如果你曾经想为 Kubernetes API 开发命令行客户端, 特别是如果你考虑过让你的客户端可用作 kubectl 插件, 你可能想知道如何让你的客户端让 kubectl 用户感到熟悉。 快速浏览一下 kubectl options 的输出可能会让你感到沮丧: "我真的需要实现所有这些选项吗?"

别担心,其他人已经为你做了很多相关工作。 实际上,Kubernetes 项目提供了两个库来帮助你在 Go 程序中处理 kubectl 风格的命令行参数: clientcmdcli-runtime(后者使用 clientcmd)。 本文将展示如何使用前者。

总体理念

作为 client-go 的一部分,clientcmd 的最终目的是提供 restclient.Config 的一个实例,该实例可以向 API 服务器发出请求。

它遵循 kubectl 语义:

  • 默认值取自 ~/.kube 或等效位置;
  • 可以使用 KUBECONFIG 环境变量指定文件;
  • 所有上述设置都可以通过命令行参数进一步覆盖。

它不会设置 --kubeconfig 命令行参数, 你可能希望这样做以与 kubectl 保持一致; 你将在"绑定标志"一节中看到如何做到这一点。

可用特性

clientcmd 允许程序处理。

  • kubeconfig 选择(使用 KUBECONFIG);
  • 上下文选择;
  • 命名空间选择;
  • 客户端证书和私钥;
  • 用户模拟;
  • HTTP 基本认证支持(用户名/密码)。

配置合并

在各种场景中,clientcmd 支持合并配置设置: KUBECONFIG 可以指定多个文件,其内容会被组合。 这可能令人困惑,因为设置根据实现方式的不同而以不同方向合并。 如果设置在 Map 中定义,第一个定义获胜,后续定义将被忽略。 如果设置不在 Map 中定义,最后一个定义获胜。

当使用 KUBECONFIG 检索设置时,缺失的文件只会导致警告。 如果用户显式指定路径(以 --kubeconfig 方式),则必须有相应的文件。

如果未定义 KUBECONFIG, 则使用默认配置文件 ~/.kube/config(如果存在)。

整体流程

一般使用模式在 clientcmd 包文档中有简洁的表达:

loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// if you want to change the loading rules (which files in which order), you can do so here

configOverrides := &clientcmd.ConfigOverrides{}
// if you want to change override values or bind them to flags, there are methods to help you

kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err := kubeConfig.ClientConfig()
if err != nil {
	// Do something
}
client, err := metav1.New(config)
// ...

在本文的上下文中,有六个步骤:

  1. 配置加载规则
  2. 配置覆盖
  3. 构建一组标志
  4. 绑定标志
  5. 构建合并配置
  6. 获取 API 客户端

配置加载规则

clientcmd.NewDefaultClientConfigLoadingRules() 构建加载规则, 该规则将使用 KUBECONFIG 环境变量的内容或默认配置文件名(~/.kube/config)。 此外,如果使用默认配置文件, 它能够从(非常)旧的默认配置文件(~/.kube/.kubeconfig)迁移设置。

你可以构建自己的 ClientConfigLoadingRules, 但在大多数情况下默认值就足够了。

配置覆盖

clientcmd.ConfigOverrides 是一个 struct,存储将应用于从加载规则派生的配置中加载的设置的覆盖。 在本文的上下文中,其主要目的是存储从命令行参数获取的值。 这些使用 pflag 库处理, 该库是 Go 的 flag 包的直接替代品, 添加了对带长名称的双连字符参数的支持。

在大多数情况下,覆盖中没有什么需要设置的;我只会将它们绑定到标志上。

构建一组标志

在此上下文中,标志是命令行参数的表示, 指定其长名称(如 --namespace)、短名称(如 -n)、默认值以及使用信息中显示的描述。 标志存储在 FlagInfo 结构体的实例中。

有三组标志可用,表示以下命令行参数:

  • 认证参数(证书、令牌、模拟、用户名/密码);
  • 集群参数(API 服务器、证书机构、TLS 配置、代理、压缩)
  • 上下文参数(集群名称、kubeconfig 用户名、命名空间)

推荐的选择包括所有三组,以及命名的上下文选择参数和超时参数。

这些都可以使用 Recommended…Flags 函数获得。 这些函数接受一个前缀,该前缀会添加到所有参数长名称之前。

因此调用 clientcmd.RecommendedConfigOverrideFlags("") 会产生诸如 --context--namespace 等命令行参数。 --timeout 参数的默认值为 0,--namespace 参数有一个对应的短变体 -n

添加前缀(如 "from-")会产生诸如 --from-context--from-namespace 等命令行参数。 这在涉及单个 API 服务器的命令上可能看起来不是特别有用, 但在涉及多个 API 服务器时(例如在多集群场景中)会派上用场。

这里有一个潜在的陷阱:前缀不会修改短名称,因此如果使用多个前缀,--namespace 需要小心: 只有一个前缀可以与 -n 短名称关联。 你必须清除与其他前缀的 --namespace 关联的短名称, 或者如果没有合理的 -n 关联,则清除所有前缀的短名称。 可以按以下方式清除短名称:

kflags := clientcmd.RecommendedConfigOverrideFlags(prefix)
kflags.ContextOverrideFlags.Namespace.ShortName = ""

类似地,可以通过清除长名称来完全禁用标志:

kflags.ContextOverrideFlags.Namespace.LongName = ""

绑定标志

一旦定义了一组标志, 就可以使用 clientcmd.BindOverrideFlags 将命令行参数绑定到覆盖标志。 这需要一个 pflag FlagSet, 而不是 Go 的 flag 包中的 FlagSet。

如果你还想绑定 --kubeconfig,现在就应该这样做, 通过绑定加载规则中的 ExplicitPath

flags.StringVarP(&loadingRules.ExplicitPath, "kubeconfig", "", "", "absolute path(s) to the kubeconfig file(s)")

构建合并配置

有两个函数可用于构建合并配置:

顾名思义,两者之间的区别在于第一个可以使用提供的阅读器交互式地请求认证信息, 而第二个仅根据调用者提供的信息进行操作。

这些函数名称中的"延迟(deferred)"指的是最终配置将尽可能晚地确定。 这意味着这些函数可以在解析命令行参数之前调用,生成的配置将使用实际构建时已解析的任何值。

获取 API 客户端

合并配置作为 ClientConfig 实例返回。 可以通过调用 ClientConfig() 方法从中获取 API 客户端。

如果没有提供配置 (KUBECONFIG 为空或指向不存在的文件、~/.kube/config 不存在、并且没有通过命令行参数提供配置), 默认设置将返回一个引用 KUBERNETES_MASTER 的模糊错误。 这是遗留行为;已经多次尝试摆脱它,但为了 --kubectl 中的 --local--dry-run 命令行参数而保留。 你应该通过调用 clientcmd.IsEmptyConfig()空配置错误,并提供更明确的错误消息。

Namespace() 方法也很有用: 它返回应该使用的名字空间。 它还指示名字空间是否被用户覆盖(使用 --namespace)。

完整示例

这是一个完整的示例。

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/spf13/pflag"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

func main() {
	// Loading rules, no configuration
	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()

	// Overrides and flag (command line argument) setup
	configOverrides := &clientcmd.ConfigOverrides{}
	flags := pflag.NewFlagSet("clientcmddemo", pflag.ExitOnError)
	clientcmd.BindOverrideFlags(configOverrides, flags,
		clientcmd.RecommendedConfigOverrideFlags(""))
	flags.StringVarP(&loadingRules.ExplicitPath, "kubeconfig", "", "", "absolute path(s) to the kubeconfig file(s)")
	flags.Parse(os.Args)

	// Client construction
	kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
	config, err := kubeConfig.ClientConfig()
	if err != nil {
		if clientcmd.IsEmptyConfig(err) {
			panic("Please provide a configuration pointing to the Kubernetes API server")
		}
		panic(err)
	}
	client, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err)
	}

	// How to find out what namespace to use
	namespace, overridden, err := kubeConfig.Namespace()
	if err != nil {
		panic(err)
	}
	fmt.Printf("Chosen namespace: %s; overridden: %t\n", namespace, overridden)

	// Let's use the client
	nodeList, err := client.CoreV1().Nodes().List(context.TODO(), v1.ListOptions{})
	if err != nil {
		panic(err)
	}
	for _, node := range nodeList.Items {
		fmt.Println(node.Name)
	}
}

祝你编码愉快,感谢你有兴趣实现具有熟悉使用模式的工具!