CRD多版本中version列表顺序决定存储版本,仅首个storage:true版本生效;新增版本须先追加末尾、设served:true,待conversionWebhook就绪并验证双向转换后,再移至首位并启用storage:true。

CRD 多版本定义里 version 字段不是随便改的
CRD 的 spec.versions 列表顺序决定哪个是存储版本(storage: true 的那个),Kubernetes 只允许一个版本设为 storage: true,且该版本必须在列表首位。如果你把旧版(比如 v1alpha1)放在前面又标了 storage: true,即使你新加了 v1 版本,数据依然以旧格式存入 etcd —— 后续 Operator 升级后读不到新字段,转换逻辑也压根不会触发。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 新增版本时,先在
spec.versions末尾追加新版本(如v1),不设storage: true,只设served: true - 确保 webhook(
conversionWebhook)已部署并就绪,且conversionReviewVersions包含所有待转换的版本(如["v1alpha1", "v1"]) - 等集群中所有存量对象都能通过 webhook 正常双向转换后,再把新版本移到列表首位,并将
storage: true切过去 - 切 storage 版本前,务必确认所有 Operator 实例(包括旧副本)都支持新版本结构,否则可能 panic 或丢字段
ConversionWebhook 必须实现 ConvertTo / ConvertFrom 两个方向
Operator 用 client-go 的 scheme 注册类型时,如果只实现了 ConvertTo(比如从 v1alpha1 → v1),而没实现 ConvertFrom(v1 → v1alpha1),Kubernetes 在处理旧客户端请求(如 kubectl get mycrd.v1alpha1.example.com)时会直接返回 InternalError,错误信息类似 no conversion function found。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
controller-gen生成基础 conversion 函数骨架:controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." output:format=go,paths=-all,它会自动添加Convertible接口和空实现 - 手动补全两个方向:每个字段映射都要显式赋值,不能依赖零值覆盖;特别注意 slice、map、嵌套 struct 的 deep copy 和 nil 判断
- 在 webhook handler 中,对
ConvertRequest的desiredAPIVersion做 switch,调用对应方向的转换函数,别漏掉v1alpha1 ↔ v1全组合 - 测试时用
kubectl convert -f old.yaml --output-version=mygroup/v1验证单向转换,再用--output-version=mygroup/v1alpha1验证回转是否保值
Operator 的 Reconcile 函数要兼容多版本对象实例
Reconciler 收到的 req.Object 类型取决于客户端请求的 API 版本,不是固定某一个。如果你的 Reconcile 方法只接收 *v1.MyResource,那当用户用 v1alpha1 创建资源时,client-go 会静默失败或 panic —— 因为 scheme 里没注册该类型,或者解码失败后传进来的对象是 nil。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 在 Operator 初始化时,用
mgr.GetScheme()显式注册所有版本类型:scheme.AddKnownTypes(v1alpha1.SchemeGroupVersion, &v1alpha1.MyResource{}, &v1alpha1.MyResourceList{}),同样注册v1版本 - Reconciler 不应硬编码类型断言,而是用
runtime.DefaultUnstructuredConverter.FromUnstructured()先转成unstructured.Unstructured,再根据obj.GroupVersionKind().Version分支处理 - 或者更稳妥:在 SetupWithManager 里用
Owns(&v1.MyResource{})和Owns(&v1alpha1.MyResource{})双注册,让 controller-runtime 自动分发到同一 Reconciler,此时入参req.Object就是具体版本的 Go struct - 字段逻辑差异大的场景(如
v1alpha1.Spec.Replicas是 int,v1.Spec.Replicas是 pointer),必须在 Reconcile 开头做归一化,统一转成内部表示再处理
etcd 数据真正迁移靠的是“读时转换 + 写时落盘”
Kubernetes 不提供批量迁移工具。所谓“平滑迁移”,本质是:当 storage 版本切换后,首次读取旧版本对象时,webhook 把 etcd 里的旧数据转成新结构返回给 client;后续任何 update(哪怕只是加个 annotation)都会以新版本格式写回 etcd。所以没有“一键升级所有存量对象”的操作,迁移是懒加载式的。
这意味着:
- 如果某个旧版本对象长期没人 touch,它在 etcd 里就一直保持旧格式,直到被 read-modify-write 流程覆盖
- Operator 自己发起的 update(如 status 更新)也会触发转换,但前提是它用的是新版本 client(比如
client.Update(ctx, &v1.MyResource{...})),否则可能写回旧格式 - 不要依赖
kubectl replace --force强制重写,它会绕过 conversion webhook,直接报错或破坏数据 - 线上有大量旧对象时,可写一个临时 job:用 dynamic client list 所有对象,逐个 patch(带
resourceVersion),强制触发一次读+写,完成格式落地
最易被忽略的一点:conversion webhook 的 TLS 证书必须由集群 CA 签发,且 service.name 和 service.namespace 必须与 CRD 中 conversion.webhook.clientConfig.service 完全一致,差一个字母都会导致转换静默失败,日志里只显示 generic timeout。

















