深入kubectl create源码:从YAML到Pod的完整链路拆解

前言

有次帮同事排查问题,他的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使用了经典的三阶段模式

  1. Complete:完善配置(从命令行、kubeconfig获取信息)
  2. Validate:校验参数合法性
  3. 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模式的优势:

  1. 链式调用:配置清晰可读,像写DSL一样
  2. 延迟执行:Do()之前只是配置,Do()后才真正执行
  3. 灵活组合:每个方法都可以独立调用,组合出不同的构建逻辑

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个步骤:

  1. 添加注解:标记资源是由kubectl管理的(用于apply冲突检测)
  2. 录制历史:记录变更,用于审计
  3. DryRun检查:如果是server-side dry-run,先检查支持性
  4. 创建资源:调用RESTClient发送请求
  5. 打印输出 :根据-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,但显示的结果和实际创建的不一样。

根因

  • client dry-run只在本地校验,不会发给apiserver
  • server dry-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的完整执行链路:

  1. Cobra层:解析命令行,三阶段执行(Complete→Validate→Run)
  2. Builder层:使用Builder模式组装资源解析配置
  3. Visitor层:遍历资源,统一处理创建逻辑
  4. RESTClient层:发送HTTP请求,与apiserver通信

关键设计模式:

  • Builder模式:灵活配置资源构建
  • Visitor模式:统一处理多种来源的资源
  • 三阶段模式:清晰的执行流程

下次遇到kubectl create的问题时,你就知道该在哪个环节排查了。

相关推荐
深圳恒讯2 小时前
越南服务器BGP多线和单线有什么区别?
运维·服务器
志栋智能2 小时前
超自动化运维如何提升安全合规水平?
运维·安全·自动化
步步为营DotNet2 小时前
基于.NET Aspire 实现云原生应用的高效监控与可观测性
云原生·.net·wpf
A_humble_scholar3 小时前
Linux(九) 进程管理完全指南:从入门到实战
linux·运维·chrome
江华森3 小时前
Linux 操作命令完全指南
linux·运维
源图客3 小时前
【AI向量数据库】Weaviate介绍与部署
运维·docker·容器
用什么都重名4 小时前
Git分支合并与远程服务器同步实战:保留关键配置文件
运维·服务器·git
C++ 老炮儿的技术栈4 小时前
Ubuntu root账号自动登陆
linux·运维·服务器·c语言·c++·ubuntu·visual studio
2301_780789664 小时前
零信任架构中,身份感知防火墙(IAFW)的部署要点与最佳实践
linux·运维·服务器·人工智能·tcp/ip·架构