Appearance
从空目录开发一个带状态和删除逻辑的 Operator
我们开发一个独立的 Runner API。提交 image 和 replicas 后应出现一个 Deployment 和一个 Service;删除 Runner 后它们必须消失。这个 typed 工程位于 labs/runner,使用 Kubebuilder v4.16.0、controller-runtime v0.25.0。业务项目用另一个 API group,因此两者可在同一个 Kind 中对照。以下命令分两条路径:从零练习的人生成骨架;只想运行成品的人直接从第 3 步开始。
1 · 生成骨架,确定 API 合约
Mac 上安装 Go 1.26+、Docker Desktop、kubectl 和 kind;从实验包根目录运行。锁定脚手架版本避免生成布局变化。Kubebuilder 官方 quick start。
sh
ROOT="$PWD"
mkdir -p .bin tmp/runner-practice
curl -fL "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v4.16.0/kubebuilder_$(go env GOOS)_$(go env GOARCH)" -o .bin/kubebuilder
chmod +x .bin/kubebuilder
cd tmp/runner-practice
"$ROOT/.bin/kubebuilder" init --domain cloudlab.training --repo cloudlab.training/runner
"$ROOT/.bin/kubebuilder" create api --group lab --version v1alpha1 --kind Runner --resource --controller --make=false在 api/v1alpha1/runner_types.go 定义 Image 和 Replicas,数量区间 1–3 在 API 准入时校验;conditions 是 list-map,每个 type 唯一。CRD 的 status 子资源将“用户声明”和“控制器观察”分开授权。生成的 DeepCopy 供缓存/客户端隔离对象副本,不是保存版本历史。
完整源码:labs/runner/api/v1alpha1/runner_types.go · 第 26–39 行
go
// RunnerSpec defines the desired state of Runner
type RunnerSpec struct {
// +kubebuilder:validation:MinLength=1
Image string `json:"image"`
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=3
Replicas int32 `json:"replicas"`
}
// RunnerStatus defines the observed state of Runner.
type RunnerStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file2 · 实现 Reconcile,而不是在 create 事件里启动容器
先 Get 当前 Runner;NotFound 表示已经删除。正常路径登记 finalizer 并返回,等更新事件再处理。修改子资源前验证 owner UID,拒绝收养同名对象。Service 的 ClusterIP 由服务端分配;只改自己拥有的字段。Deployment 与 Service 是两次独立写:若第二次失败,下次从已存在的 Deployment 继续,所以第一步必须可以重做。
完整源码:labs/runner/internal/controller/runner_controller.go · 第 84–104 行
go
if waiting {
return ctrl.Result{RequeueAfter: time.Second}, nil
}
cu.RemoveFinalizer(&u, finalizer)
return done, r.Update(ctx, &u)
}
if cu.AddFinalizer(&u, finalizer) {
return done, r.Update(ctx, &u)
}
labels := map[string]string{"cloudlab.training/runner": string(u.UID)}
own := func(obj client.Object) error {
if obj.GetUID() != "" && !meta.IsControlledBy(obj, &u) {
return fmt.Errorf("refusing to adopt %s", obj.GetName())
}
return cu.SetControllerReference(&u, obj, r.Scheme)
}
_, err := cu.CreateOrUpdate(ctx, r.Client, d, func() error {
if err := own(d); err != nil {
return err
}
d.Spec.Replicas = &u.Spec.Replicas删除路径有相反的次序:只删除属于该 UID 的对象,带 UID 前置条件,轮询确认两者均消失后才移除 finalizer。若不需要等待或回收外部资源,只用 ownerReference 会更简单。状态读 Deployment 当前 revision 的 updated 和 available 副本;没有变化就不更新 condition。否则一次没有实际变化的通知也会触发自己的下一轮写。完整的 SetupWithManager、删除、状态和 RBAC 标记都在同一源码链接中。
3 · 生成、测试、在真实节点运行
从零练习者可将成品中以下三个文件复制回刚生成的同名位置,对照实现并重新生成;不要手改生成的 CRD。
sh
# 在 runner-practice 中,ROOT 仍为实验包根目录
for p in api/v1alpha1/runner_types.go internal/controller/runner_controller.go internal/controller/runner_controller_test.go; do cp "$ROOT/labs/runner/$p" "$p"; done
make generate manifests
# 运行随包的完整实现
# 如果直接使用成品,先在实验包根目录执行 ROOT="$PWD"
cd "$ROOT/labs/runner"
make generate manifests
make test
export KUBECONFIG="$ROOT/tmp/handbook.kubeconfig"
./kind-test.shmake generate 调 controller-gen 产生 DeepCopy;make manifests 同时生成 CRD 和 RBAC。make test 首次下载 envtest 资产;Kind 脚本要求 CPU 镜像已载入,会创建临时 namespace,运行本机 manager,等待 Ready,删 Runner 并验证两个子对象均消失,只清理它创建的 CRD。观察时另开终端 kubectl get runner,deploy,svc,pods -A -w。envtest 实测覆盖重复 reconcile、owner 所管理的服务创建与未就绪 condition;Kind 实测覆盖 Pod 运行和删除。
4 · 改为集群内进程,诊断启动失败
在专用集群、无其他 Runner manager 工作时,可用下面的打包路径。首次拉构建镜像需联网;这与本机 manager 的集成测试是两种部署方式。
sh
make docker-build IMG=runner-controller:lab
kind load docker-image runner-controller:lab --name handbook-lab
make deploy IMG=runner-controller:lab
kubectl -n runner-system rollout status deployment/runner-controller-manager
kubectl -n runner-system logs deployment/runner-controller-manager -c manager
# 专用实验环境,用完后先删 Runner,再卸载 manager 与 CRD
make undeploy禁止把生产同名 CRD 拿来卸载。若 controller 无日志先看镜像与 Pod Events;若运行却无子对象,检查 cache 所监听 namespace、ServiceAccount 是否能 create Deployment;若卡 Terminating,检查子对象、finalizer 与删除权限,直接清掉 finalizer 会绕过回收承诺。**追问:**如果更新 status 时有人刚改了 spec,怎么办?保留冲突,交回工作队列重新读取新版本;不把旧 Ready 盖到新声明上。
真实排错:manager 已 Ready,为什么一个子对象也没有?
本轮首次把 manager 放入集群时复现了这个问题。本机运行使用开发者 kubeconfig,envtest 使用测试客户端,两者都不等于 controller Pod 的 ServiceAccount。容器日志显示 reflector 无法 list deployments 和 services;检查生成的 role.yaml,发现只生成了 Runner 自身的规则。两个新加的 RBAC 标记紧挨着函数声明,没有作为包级 RBAC 标记被收集。
修复方法:把 RBAC 标记放在同一独立注释组,标记和函数之间留一行,再 make manifests。检查 role.yaml 的 resources 包含 deployments、services 及需要的动词。之后重新应用 RBAC,并用真实 ServiceAccount 验证:
sh
kubectl auth can-i list deployments.apps --all-namespaces --as=system:serviceaccount:runner-system:runner-controller-manager
kubectl auth can-i watch services --all-namespaces --as=system:serviceaccount:runner-system:runner-controller-manager从根目录执行 ./labs/runner/kind-image-test.sh 可复现本轮的替代构建方法。它只在不存在 runner-system 和专属 Runner CRD 时运行,结束会清理本次资源。本轮用 Linux 二进制镜像和该工程生成的 Kustomize 配置重新部署,manager 和 runner-sample 均为 1/1 Ready。集群实际输出。这套 typed 教学 manager 默认跨 namespace 观察;业务版 ModelDock 限定 modeldock-lab。生产中应按 watch 范围缩小授权,而不是给 cluster-admin 消除错误。