前言
有次帮同事排查问题,他的YAML文件里有3个资源(Deployment、Service、ConfigMap),执行kubectl create -f app.yaml后只成功创建了2个,Service死活创建不了,报错说已存在。但kubectl get svc一看,根本没有同名的Service!
折腾了半小时,最后通过加-v=6看调试日志,才发现是Builder的Flatten()逻辑把资源列表拍平了,加上ContinueOnError()后错误被吞掉了。这个经历让我意识到:如果不理解kubectl内部的执行流程,排查问题就像在黑箱里摸象。
今天我就从源码角度,完整拆解kubectl create的执行链路,带你看看这条从YAML文件到K8s资源的路是怎么走的。
kubectl create整体流程
先来个全景图:
用户执行:kubectl create -f deployment.yaml
↓
【Cobra层】
1. 解析命令行参数 (-f, --dry-run等)
2. 调用Complete()填充选项
3. 调用ValidateArgs()校验参数
4. 调用RunCreate()执行业务逻辑
↓
【Builder层】
5. 创建ResourceBuilder
6. FilenameParam()解析YAML/JSON文件
7. 生成Visitor列表(文件/URL/Stdin)
8. Flatten()拍平嵌套资源
↓
【Visitor层】
9. Visit()遍历每个资源
10. 解码YAML为runtime.Object
11. 填充Namespace等元数据
↓
【RESTClient层】
12. 创建RESTHelper
13. 调用Create()发送HTTP POST
14. apiserver处理请求
15. 返回创建结果
整个流程涉及4个核心组件:
| 组件 | 职责 | 设计模式 |
|---|---|---|
| Cobra | 命令解析、参数绑定 | 命令模式 |
| ResourceBuilder | 资源组装、配置解析 | Builder模式 |
| Visitor | 资源遍历、统一处理 | Visitor模式 |
| RESTClient | HTTP通信、序列化 | 客户端模式 |
第一阶段:Cobra命令解析
1. 命令初始化
kubectl create的入口在pkg/cmd/create/create.go:
go
func NewCmdCreate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
// 创建选项对象(存储所有配置)
o := NewCreateOptions(ioStreams)
// 初始化cobra命令
cmd := &cobra.Command{
Use: "create -f FILENAME",
Short: "Create a resource from a file or from stdin",
Long: createLong,
Example: createExample,
// 核心执行函数
Run: func(cmd *cobra.Command, args []string) {
// 1. 校验必须指定-f或-k
if cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) {
ioStreams.ErrOut.Write([]byte("Error: must specify one of -f and -k\n\n"))
return
}
// 2. 三阶段执行流程
cmdutil.CheckErr(o.Complete(f, cmd)) // 填充选项
cmdutil.CheckErr(o.ValidateArgs(cmd, args)) // 校验参数
cmdutil.CheckErr(o.RunCreate(f, cmd)) // 执行业务逻辑
},
}
// 绑定各种选项到命令行参数
o.RecordFlags.AddFlags(cmd)
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
cmdutil.AddValidateFlags(cmd)
cmd.Flags().BoolVar(&o.EditBeforeCreate, "edit", o.EditBeforeCreate, "Edit before create")
cmdutil.AddDryRunFlag(cmd)
return cmd
}
关键点 :kubectl使用了经典的三阶段模式:
- Complete:完善配置(从命令行、kubeconfig获取信息)
- Validate:校验参数合法性
- Run:执行实际业务逻辑
2. 子命令注册
create命令还注册了多个子命令,用于快速创建特定资源:
go
// 注册create的子命令
cmd.AddCommand(NewCmdCreateNamespace(f, ioStreams)) // create namespace
cmd.AddCommand(NewCmdCreateDeployment(f, ioStreams)) // create deployment
cmd.AddCommand(NewCmdCreateService(f, ioStreams)) // create service
cmd.AddCommand(NewCmdCreateConfigMap(f, ioStreams)) // create configmap
cmd.AddCommand(NewCmdCreateSecret(f, ioStreams)) // create secret
cmd.AddCommand(NewCmdCreateJob(f, ioStreams)) // create job
cmd.AddCommand(NewCmdCreateIngress(f, ioStreams)) // create ingress
// ... 还有role、serviceaccount等十几个子命令
设计优势:每个子命令都是独立的cobra.Command,可以有自己的参数和逻辑,但共享create的基础能力。
第二阶段:Complete - 完善配置
Complete阶段负责从各种来源收集配置信息:
go
func (o *CreateOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
// 1. 解析DryRun策略(dry-run=client/server/none)
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
// 2. 获取打印选项(-o json/yaml/name等)
o.ToPrinter, err = o.PrintFlags.ToPrinter()
// 3. 获取字段管理器(用于Server-Side Apply)
o.fieldManager = cmdutil.GetFlagString(cmd, "field-manager")
if len(o.fieldManager) == 0 {
o.fieldManager = "kubectl-create"
}
// 4. 获取录制器(用于记录变更历史)
o.Recorder, err = o.RecordFlags.ToRecorder()
return nil
}
为什么需要Complete阶段?
有些配置不能直接在Flag绑定时解析,比如:
- 需要从kubeconfig解析namespace
- 需要根据其他选项的值动态决定
- 需要初始化客户端对象
第三阶段:RunCreate - 核心执行逻辑
RunCreate是整个create命令的核心,它协调Builder和Visitor完成资源创建。
1. 特殊分支处理
go
func (o *CreateOptions) RunCreate(f cmdutil.Factory, cmd *cobra.Command) error {
// 分支1:如果指定了--raw,直接发送原始HTTP请求
if len(o.Raw) > 0 {
restClient, err := f.RESTClient()
return rawhttp.RawPost(restClient, o.IOStreams, o.Raw, o.FilenameOptions.Filenames[0])
}
// 分支2:如果指定了--edit,先打开编辑器让用户修改
if o.EditBeforeCreate {
return RunEditOnCreate(f, o.PrintFlags, o.RecordFlags, o.IOStreams, cmd, &o.FilenameOptions, o.fieldManager)
}
// 主逻辑:走Builder模式创建资源
// ...
}
2. 构建ResourceBuilder
这是整个create流程的核心设计------Builder模式:
go
// 获取验证器(用于校验YAML schema)
schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"))
// 从kubeconfig获取namespace
cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
// ========== Builder模式核心 ==========
r := f.NewBuilder().
Unstructured(). // 使用unstructured类型(延迟反序列化)
Schema(schema). // 设置校验器
ContinueOnError(). // 遇到错误继续处理其他资源
NamespaceParam(cmdNamespace).DefaultNamespace(). // 设置namespace
FilenameParam(enforceNamespace, &o.FilenameOptions). // 解析文件
LabelSelectorParam(o.Selector). // 标签选择器过滤
Flatten(). // 拍平嵌套资源
Do() // 构建完成,生成Result对象
Builder模式的优势:
- 链式调用:配置清晰可读,像写DSL一样
- 延迟执行:Do()之前只是配置,Do()后才真正执行
- 灵活组合:每个方法都可以独立调用,组合出不同的构建逻辑
3. FilenameParam的奥秘
FilenameParam负责解析-f参数指定的文件,它支持多种形式:
go
func (b *Builder) FilenameParam(enforceNamespace bool, filenameOptions *FilenameOptions) *Builder {
for _, s := range filenameOptions.Filenames {
switch {
case s == "-":
// 从标准输入读取
b.Stdin()
case strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://"):
// 从URL下载
url, err := url.Parse(s)
b.URL(defaultHttpGetAttempts, url)
default:
// 本地文件或目录
if filenameOptions.Recursive {
// 递归处理目录
b.Path(true, s)
} else {
b.Path(false, s)
}
}
}
// 支持Kustomize
if filenameOptions.Kustomize != "" {
b.paths = append(b.paths, &KustomizeVisitor{
mapper: b.mapper,
dirPath: filenameOptions.Kustomize,
})
}
return b
}
支持的输入源:
| 输入形式 | 示例 | 用途 |
|---|---|---|
| 本地文件 | -f deployment.yaml |
最常用的方式 |
| 目录 | -f ./manifests/ |
批量创建目录下所有YAML |
| 递归目录 | -f ./manifests/ -R |
递归子目录 |
| 标准输入 | -f - |
管道输入:`cat app.yaml |
| HTTP URL | -f https://example.com/app.yaml |
从网络加载配置 |
| Kustomize | -k ./overlays/prod |
使用Kustomize构建 |
实用技巧 :
kubectl create -f -特别适合管道操作,比如配合helm template使用:helm template mychart | kubectl create -f -
第四阶段:Visitor模式处理资源
Builder构建完成后,会生成一个Result对象。调用Visit()方法开始遍历处理资源,这就是Visitor模式的应用。
Visitor模式简介
Visitor模式的核心思想是:将数据结构的遍历与对元素的操作分离。
在kubectl中:
- 数据结构:各种来源的资源(文件、URL、stdin)
- 遍历逻辑:Visitor统一遍历
- 操作逻辑:在Visit回调函数中定义(创建、更新、删除等)
create的Visit实现
go
err = r.Visit(func(info *resource.Info, err error) error {
if err != nil {
return err
}
// 1. 添加kubectl管理的注解(用于apply识别)
if err := util.CreateOrUpdateAnnotation(info.Object, scheme.DefaultJSONEncoder()); err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
// 2. 录制变更历史(用于审计)
if err := o.Recorder.Record(info.Object); err != nil {
klog.V(4).Infof("error recording: %v", err)
}
// 3. 处理DryRun逻辑
if o.DryRunStrategy != cmdutil.DryRunClient {
if o.DryRunStrategy == cmdutil.DryRunServer {
// 检查apiserver是否支持dry-run
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
}
// 4. 真正的创建操作!
obj, err := resource.
NewHelper(info.Client, info.Mapping).
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
WithFieldManager(o.fieldManager).
Create(info.Namespace, true, info.Object)
if err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
info.Refresh(obj, true)
}
// 5. 打印结果
return o.PrintObj(info.Object, o.IOStreams.Out)
})
Visit回调函数的5个步骤:
- 添加注解:标记资源是由kubectl管理的(用于apply冲突检测)
- 录制历史:记录变更,用于审计
- DryRun检查:如果是server-side dry-run,先检查支持性
- 创建资源:调用RESTClient发送请求
- 打印输出 :根据
-o参数格式化输出
resource.Info里有什么?
Visit回调接收的info *resource.Info包含了资源的完整信息:
go
type Info struct {
Client RESTClient // REST客户端
Mapping *meta.RESTMapping // 资源类型映射(GVR ↔ GVK)
Namespace string // 所属namespace
Name string // 资源名称
Source string // 来源(文件名/URL)
Object runtime.Object // 解码后的资源对象
ResourceVersion string // 资源版本(用于乐观锁)
}
第五阶段:RESTClient发送请求
Visitor的Create最终会调用Helper.Create,这里才是真正的HTTP请求:
Helper.Create方法
go
func (m *Helper) Create(namespace string, modify bool, obj runtime.Object) (runtime.Object, error) {
// 1. 设置创建选项
createOptions := &metav1.CreateOptions{
FieldManager: m.FieldManager,
}
if m.DryRun {
createOptions.DryRun = []string{metav1.DryRunAll}
}
// 2. 调用底层createResource
return m.createResource(
m.RESTClient, // REST客户端
m.Resource, // 资源名称(如"pods")
namespace, // namespace
obj, // 资源对象
createOptions, // 创建选项
)
}
底层HTTP请求
go
func (m *Helper) createResource(c RESTClient, resource, namespace string,
obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
return c.Post(). // HTTP POST
NamespaceIfScoped(namespace, m.NamespaceScoped). // 添加namespace(如果是namespaced资源)
Resource(resource). // 资源路径
VersionedParams(options, metav1.ParameterCodec). // 查询参数
Body(obj). // 请求体(序列化后的JSON/YAML)
Do(context.TODO()). // 执行请求
Get() // 获取结果
}
请求构建过程:
POST /api/v1/namespaces/default/pods
Content-Type: application/json
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": { "name": "nginx" },
"spec": { ... }
}
完整的调用链路图
NewCmdCreate()
↓
Run()
↓
Complete() → ValidateArgs() → RunCreate()
↓
f.NewBuilder()
.Unstructured()
.Schema()
.ContinueOnError()
.NamespaceParam()
.FilenameParam() ← 解析-f参数
.Flatten()
.Do()
↓
r.Visit(func(info *resource.Info) {
↓
NewHelper(info.Client, info.Mapping)
.Create(namespace, true, info.Object)
↓
c.Post().Resource().Body().Do().Get()
↓
HTTP POST → apiserver
})
踩坑实录
坑1:ContinueOnError导致错误被吞
现象:YAML里有3个资源,第2个创建失败,但命令只显示了部分成功,没看到错误详情。
根因 :Builder配置了ContinueOnError(),遇到错误会继续处理下一个资源,但默认输出只显示最后一个错误。
解决方案:
bash
# 使用--validate=true先校验
kubectl create -f app.yaml --validate=true
# 或者分开创建,逐个排查
kubectl create -f app-deployment.yaml
kubectl create -f app-service.yaml
# 加-v=6看详细日志
kubectl create -f app.yaml -v=6
坑2:Flatten合并了同名资源
现象:YAML里定义了2个不同namespace的同名Service,只创建了一个。
根因 :Flatten()方法会把资源列表"拍平",如果资源名称相同(即使namespace不同),可能会被覆盖。
解决方案:避免在同个YAML文件中定义同名资源,即使namespace不同。
坑3:Namespace没正确设置
现象:YAML文件里写了namespace,但创建到了default namespace。
根因 :命令行--namespace参数优先级高于YAML中的namespace定义。
解决方案:
bash
# 方案1:不指定--namespace,使用YAML中的
kubectl create -f app.yaml
# 方案2:明确指定namespace
kubectl create -f app.yaml --namespace=production
# 方案3:使用--enforce-namespace强制使用命令行的namespace
坑4:DryRun不理解差异
现象 :用了--dry-run=client,但显示的结果和实际创建的不一样。
根因:
clientdry-run只在本地校验,不会发给apiserverserverdry-run会发给apiserver,但真的不会创建
解决方案:
bash
# 本地校验(快,但可能和server行为不一致)
kubectl create -f app.yaml --dry-run=client
# Server端校验(慢,但结果准确)
kubectl create -f app.yaml --dry-run=server
坑5:编辑模式(--edit)的坑
现象 :用了--edit想修改后再创建,但修改后保存退出,资源没创建。
根因 :编辑器退出码不为0(比如:q!强制退出),kubectl会放弃创建。
解决方案 :确保正常保存退出(:wq),并检查编辑器配置。
调试技巧:如何跟踪create的执行流程
1. 使用-v参数查看详细日志
bash
# -v=0: 默认,几乎无日志
# -v=2: 显示重要操作
# -v=4: 显示请求详情
# -v=6: 显示HTTP请求/响应
# -v=8: 显示完整HTTP内容(包括body)
kubectl create -f app.yaml -v=6
2. 查看执行的API路径
bash
kubectl create -f app.yaml -v=6 2>&1 | grep "POST"
# 输出示例:
# POST https://192.168.1.100:6443/api/v1/namespaces/default/pods 201 Created
3. 使用--dry-run预览
bash
# 预览JSON输出
kubectl create -f app.yaml --dry-run=client -o json
# 对比差异
diff <(cat app.yaml) <(kubectl create -f app.yaml --dry-run=client -o yaml)
给自己的CLI工具设计的启示
从kubectl create的实现,我们可以学到以下设计思想:
1. 三阶段模式
go
Complete() → Validate() → Run()
- Complete:收集配置,处理默认值
- Validate:校验业务规则
- Run:执行业务逻辑
好处:逻辑清晰,便于测试(每个阶段可独立测试)。
2. Builder模式构建复杂对象
go
builder := NewBuilder().
Option1().
Option2().
Option3()
好处:配置灵活,可读性强,延迟执行。
3. Visitor模式统一处理异构数据
好处:新增数据源无需修改处理逻辑,符合开闭原则。
4. 分层架构
CLI层 → Builder层 → Visitor层 → Client层 → Server层
好处:每层职责单一,便于替换和扩展。
总结
通过源码分析,我们理解了kubectl create的完整执行链路:
- Cobra层:解析命令行,三阶段执行(Complete→Validate→Run)
- Builder层:使用Builder模式组装资源解析配置
- Visitor层:遍历资源,统一处理创建逻辑
- RESTClient层:发送HTTP请求,与apiserver通信
关键设计模式:
- Builder模式:灵活配置资源构建
- Visitor模式:统一处理多种来源的资源
- 三阶段模式:清晰的执行流程
下次遇到kubectl create的问题时,你就知道该在哪个环节排查了。