中文内容
使用 clientcmd 统一访问 API 服务器
如果你曾想为 Kubernetes API 开发一个命令行客户端,尤其是考虑让你的客户端可作为 kubectl 插件使用,你可能会想:如何让 kubectl 用户觉得你的客户端用起来很熟悉。快速看一眼 kubectl options 的输出可能会让人有些泄气:“我真的要实现所有这些选项吗?”
别担心,其他人已经为你完成了大量相关工作。事实上,Kubernetes 项目提供了两个库,帮助你在 Go 程序中处理 kubectl 风格的命令行参数:clientcmd 和 cli-runtime(后者使用 clientcmd)。本文将展示如何使用前者。
总体理念
正如可以预期的那样,由于它是 client-go 的一部分,clientcmd 的最终目的是提供一个 restclient.Config 实例,用于向 API 服务器发出请求。
它遵循 kubectl 语义:
- 默认值取自 ~/.kube 或等效位置;
- 可以使用 KUBECONFIG 环境变量指定文件;
- 以上所有设置还可以通过命令行参数进一步覆盖。
它不会设置 --kubeconfig 命令行参数,而你可能希望这样做以与 kubectl 保持一致;你将在“绑定标志”一节中看到如何实现。
可用功能
clientcmd 允许程序处理
- kubeconfig 选择(使用 KUBECONFIG);
- 上下文选择;
- 命名空间选择;
- 客户端证书和私钥;
- 用户模拟;
- HTTP Basic 认证支持(用户名/密码)。
配置合并
在各种场景下,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)
// ...
在本文语境中,共有六个步骤:
- 配置加载规则。
- 配置覆盖项。
- 构建一组标志。
- 绑定标志。
- 构建合并后的配置。
- 获取 API 客户端。
配置加载规则
clientcmd.NewDefaultClientConfigLoadingRules() 会构建加载规则,这些规则将使用 KUBECONFIG 环境变量的内容,或默认配置文件名(~/.kube/config)。此外,如果使用默认配置文件,它还能够从(非常)旧的默认配置文件(~/.kube/.kubeconfig)迁移设置。
你可以构建自己的 ClientConfigLoadingRules,但在大多数情况下默认设置就足够了。
配置覆盖项
clientcmd.ConfigOverrides 是一个结构体,用于存储会覆盖通过加载规则得出的配置中已加载设置的覆盖项。在本文语境中,它的主要用途是存储从命令行参数获取的值。这些参数使用 pflag 库处理;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)")
构建合并后的配置
有两个函数可用于构建合并后的配置:
- 正文:clientcmd.NewInteractiveDeferredLoadingClientConfig
- 正文:clientcmd.NewNonInteractiveDeferredLoadingClientConfig
顾名思义,两者的区别在于:第一个可以使用提供的 reader 以交互方式请求认证信息,而第二个只处理调用方提供给它的信息。
这些函数名中的“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)
}
}
祝编码愉快,感谢你有兴趣实现具有熟悉使用模式的工具!
- ← 上一篇
- 下一篇 →