Skip to content

Go 云原生工具选型 ​

Docker、Kubernetes、Helm、Argo CD、GitHub Actions、ko、client-go、controller-runtime 经常出现在同一张架构图上,但它们没有在争夺同一份工作。真正需要决定的是:谁构建程序,谁保存部署期望,谁持续修复偏差,以及有没有必要自己写一个控制器。本篇按这些责任选工具,而不是建议一个 Go 服务把八个都装上。

实际验证使用 Go 1.27.2、Linux/amd64,以及固定的 client-go v0.37.1、controller-runtime v0.25.2。文末给出真实客户端库 fake 实验、完整依赖锁定和 RBAC 配置。fake 在内存中运行,不读取 kubeconfig,也不连接集群;其通过不等于 Kubernetes 部署、权限或控制器运行已经验证。

先找自己缺的是哪一层 ​

工具输入 → 输出主要职责它不替你完成什么
Docker / BuildKitDockerfile 与构建上下文 → 镜像通用镜像构建、容器开发与运行工具多节点应用的持续协调
koGo main 包与配置 → OCI 镜像面向 Go 的简化镜像构建任意系统包安装脚本或完整运行平台
GitHub Actions仓库事件 → runner 中的一组 job检查、构建、发布流程编排集群长期漂移检测,除非另写这套机制
Helmchart 与 values → 清单及可管理 release参数化打包与发布自动让数据库形成高可用
Argo CDGit/声明来源 → 目标集群状态协调GitOps 持续比较、同步与健康展示编译 Go 或取代应用监控
KubernetesAPI 期望状态 → 工作负载运行与状态调度、控制循环、服务发现等平台业务幂等、数据备份与应用正确性
client-goGo API 调用 → Kubernetes API 交互typed/dynamic client、缓存与队列等基础能力自动决定该做什么业务协调
controller-runtime资源观察与 Reconcile → 控制器程序组织 manager、cache、client、controller 等自动生成正确的领域运维逻辑

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    S[Go 源码] --> CI[GitHub Actions 编排检查]
    CI --> B{构建选择}
    B --> D[Docker / BuildKit]
    B --> KO[ko]
    D --> I[同一套 Registry 中的 OCI 制品]
    KO --> I
    I --> P[部署声明固定 digest]
    H[Helm chart 与 values] --> P
    P --> G[Argo CD 协调]
    G --> K[Kubernetes API 与控制器]
    C[自写 Go 工具:client-go] --> K
    O[自写 Operator:controller-runtime] --> K
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    S[Go 源码] --> CI[GitHub Actions 编排检查]
    CI --> B{构建选择}
    B --> D[Docker / BuildKit]
    B --> KO[ko]
    D --> I[同一套 Registry 中的 OCI 制品]
    KO --> I
    I --> P[部署声明固定 digest]
    H[Helm chart 与 values] --> P
    P --> G[Argo CD 协调]
    G --> K[Kubernetes API 与控制器]
    C[自写 Go 工具:client-go] --> K
    O[自写 Operator:controller-runtime] --> K

两条构建分支通常选其一;Argo CD 使用 Helm 时,常由 Helm 渲染清单,再由 Argo CD 管理应用生命周期,不能把它想成在集群里不停执行 helm upgrade 的同义包装。Argo CD 与 Helm

Docker 和 ko:构建自由度与项目约定的取舍 ​

Dockerfile 适合包含多种语言、系统库、生成步骤和复杂文件布局的构建。多阶段构建可以把工具链留在 build stage,只把产物交给运行镜像。代价是团队要维护 Dockerfile、基础镜像、缓存顺序与构建安全;构建凭据需要 secret mount,不能进入镜像层。Docker 多阶段

ko 直接调用 Go 构建并组装镜像,不要求 Docker daemon。单个 Go 程序、少量 OS 依赖,是它更擅长的情况。常用入口是 ko build 构建,ko resolve 把清单中的 Go 引用解析为镜像引用;ko apply 还会进入部署动作,不能把它当成无副作用的渲染命令。ko 介绍、ko build

项目条件倾向选择原因与边界
单个纯 Go 服务,文件依赖少ko少维护 Dockerfile;仍须固定基础镜像与工具链
需要 apt/apk、C 库、复杂系统准备Dockerfile构建步骤可控;运行镜像 ABI 仍要匹配
构建中有多个语言工具链Dockerfile 或既有构建系统不应强迫所有步骤塞进 Go 约定
希望 CI 不运行 Docker daemonko 或其他无 daemon 构建路线无 daemon 不代表无 Registry 凭据、无网络或无供应链风险

ko 的静态资产约定是 kodata,运行时位置通过 KO_DATA_PATH 提供。若使用 Go embed,文件随二进制进入镜像;使用 kodata 则保留独立资产目录,两种方式影响更新和路径处理。ko 对 cgo、OS 包等复杂依赖的限制,需要在真正替换 Dockerfile 前检查。ko 静态资产、限制

不要忽略发布默认值:构建工具可能把镜像推到 KO_DOCKER_REPO 指定仓库。ko resolve 本身也会构建并默认推送,并非纯文本替换。想只做文件输出,应明确检查所用版本的 --push=false 与 OCI layout/tarball 选项;--local 指向本地 Docker daemon,也不等于只写 YAML。ko resolve本文没有运行 ko,没有下载基础镜像或推送 Registry;“不需要 Docker”不是执行这些外部动作的授权。

GitHub Actions 负责一次流程,Kubernetes 负责持续运行 ​

Actions 把事件、job、依赖关系、runner 和权限组织起来。对 Go 服务,常用来跑 test/race/vet、构建一个已固定身份的制品,再记录发布证据。缓存用来加速,artifact 用来保存结果,Registry 用来分发镜像,这三者用途不同。完整流程见《Go CI-CD 与 GitOps》。

应把不可信 PR 验证与持有发布身份的 job 分开,限制 token 权限,固定 action commit,按组织规则约束 OIDC 身份。自托管 runner 的宿主访问权也属于安全边界,不要只检查 YAML 上有没有 secrets。GitHub Actions 安全使用

Kubernetes 则长期运行控制循环:Deployment 管理可替换 Pod,scheduler 放置实例,kubelet 与运行时启动容器。集群 API 与控制器提供的是平台能力,不保证应用能承受节点故障、读写兼容或数据库不可用。一个小服务若已有托管容器平台满足需求,不必为了“云原生”先维护自己的集群。Kubernetes 组件

二者可以合作而不互相替代。Actions 构建完就结束;Deployment controller 仍继续维持副本。Actions 也可以做部署,但若需要发现数小时后出现的手工漂移,必须安排持续协调者,而不是指望已经完成的 job 继续工作。

Helm 和 Argo CD:打包与长期所有权不要弄反 ​

Helm 把一组 Kubernetes 清单包装成 chart,通过 values 改变实例参数。helm template 渲染出文本,helm install/upgrade 才进入实际 release 操作。渲染成功不代表目标集群有对应 CRD、权限或资源容量;含 lookup 的模板还涉及渲染环境差异。helm template

Chart version、应用版本和镜像 digest 是三件事。一个 chart 可以不改应用,只修 probe 或 RBAC;同一应用版本也可以用不同 chart 配置。因此升级评审应看渲染后的差异、具体镜像和权限变化,而不只看 Chart.yaml 的版本号。

Argo CD 持续比较声明来源与集群状态。repo-server 负责取得并生成清单,application-controller 负责比较和协调,API/UI 提供访问和管理入口。实际部署身份、目标命名空间、项目约束和 RBAC 决定它能改什么。Argo CD 架构

团队需要合适组合不必马上加的东西
手动、低频部署一组配置Helm 加审查和明确操作流程为一次模板渲染装完整 GitOps 平台
多环境,要求持续检查漂移Helm/Kustomize 加 Argo CD再让 CI 同时写同一批对象
反复进行复杂领域运维上述平台加专用 Operator只靠越来越多 Helm hooks 模拟持久状态机

给一个字段安排两个互不协调的所有者,结果往往是反复覆盖。例如 CI 直接改镜像、Argo CD 又从 Git 修回;Helm 强制写 replicas、HPA 再修改。应先明确对象与字段所有权,变更从同一条审查路径进入。Argo CD 的 prune 会涉及删除,self-heal 会覆盖现场漂移,启用前需确认期望行为和事故流程。自动同步策略

client-go:想写集群工具,先从普通客户端开始 ​

client-go 提供与 Kubernetes API 交互的 Go 客户端。typed client 让 Deployment、ConfigMap 等内置资源有静态类型;dynamic client 适合按 GroupVersionResource 操作未编入程序的类型。它还提供 REST 配置、发现、informer、lister 和 workqueue 等控制器构件。client-go v0.37.1

任务常用入口需要记住的边界
一次查询或修改内置资源Clientset 的 typed APIcontext 超时、分页、错误分类、权限
操作不预先编译进程序的 CRDdynamic client字段缺少编译期类型检查,schema 仍由服务端决定
持续观察很多对象SharedInformer / Lister本地缓存有延迟,需等待初始同步
合并事件并重试处理rate-limiting workqueue事件不是完整事实,业务动作仍需幂等
对外部集群连接受控 kubeconfig 或集群内身份不要把凭据写死,也不要自动读取未授权账户

频繁全量 list 会增加 API server 和客户端开销。informer 通常通过 list/watch 维护本地缓存,让读取更便宜;但缓存不是每次都去 API server 取最新状态。拿到共享缓存中的对象后不要原地改写,应复制或交给符合语义的客户端操作。watch 中断后还要重新建立观察,不能把“一条连接没断”作为正确性前提。

客户端的 QPS/Burst 设置限制本客户端请求速率,不等于集群的全局配额。遇到 Conflict 应重新读取并重试适当更新;Forbidden 表示权限问题,盲目高频重试不会改善;NotFound 是否正常,则取决于正在协调创建还是删除。

本文真实调用了 client-go 的 typed API,但对象放在它自己的 fake tracker 中:

go
cs := clientgofake.NewClientset(cm.DeepCopy())
list, err := cs.CoreV1().ConfigMaps("demo").List(ctx, metav1.ListOptions{})
text
client-go fake: configmaps= 1

这证明 API 用法在固定版本上可编译执行,不证明网络、认证、RBAC 或 server-side defaulting 正确。client-go 的 NewClientset 源码明确说明它使用简化对象 tracker,不能替代真实 API server。

controller-runtime:当你确实要长期协调资源 ​

只做一次查询,无需引入 manager、cache 和 controller。需要根据自定义资源长期创建、更新、删除其他对象时,controller-runtime 能把这些组件组织起来:manager 管理共享组件和生命周期,controller 监听对象并把 key 放入队列,Reconciler 读取当前状态并执行协调。controller-runtime

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    A[API server list/watch] --> C[Cache / Informer]
    C --> E[事件映射为 namespace/name]
    E --> Q[去重与重试队列]
    Q --> R[Reconcile 重新读取当前状态]
    R --> W[按需要写 API 或外部系统]
    W --> A
    R -->|普通错误| Q
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    A[API server list/watch] --> C[Cache / Informer]
    C --> E[事件映射为 namespace/name]
    E --> Q[去重与重试队列]
    Q --> R[Reconcile 重新读取当前状态]
    R --> W[按需要写 API 或外部系统]
    W --> A
    R -->|普通错误| Q

Reconcile 收到的是资源标识,不是某次事件的全部内容。事件可能合并,也可能在处理前又发生变化,因此应问“现在还差什么”,而不是“刚才那一次事件让我加几份”。v0.25.2 Reconcile 源码

本文的最小协调逻辑只给指定 ConfigMap 的 Data 增加 managed=true,保留其他键。关键条件是:已经满足期望时不写回。

go
if cm.Data["managed"] == "true" {
    return reconcile.Result{}, nil
}
if cm.Data == nil { cm.Data = map[string]string{} }
cm.Data["managed"] = "true"
return reconcile.Result{}, r.Client.Update(ctx, &cm)

连续调用两次的实际输出:

text
two reconciles: updates=1 managed=true owner=team

第一次补字段,第二次观察到期望已满足,不再 Update;原有 owner 没丢。若每次都写“当前时间”或无条件修改对象,可能使自己的 watch 不断触发,形成无意义循环。

测试另外注入一次 Conflict:第一次返回错误,第二次重新 Get 后成功。这里第二次是测试明确调用,不是框架队列自动重试的实测。按固定版本 Reconcile 契约,普通非 nil error 会使 controller 通过退避重排;TerminalError 是不再重排的例外,等待已知外部事件可使用 RequeueAfter。业务需要的重试策略不能只靠“所有错误全部吞掉”。

缓存和并发是控制器容易遗漏的成本 ​

manager 的常用 client 读缓存、写 API,因此写成功后的立刻 Get 未必立即看到新状态。需要强一致读取的特定步骤可以选择直接 APIReader,但大量绕过缓存又会提高 API 压力。不要把 fake client 里立刻可见的结果当成真实 cache 的保证。controller-runtime FAQ

控制器通常按不同资源 key 并发协调。同一逻辑可能在重启、leader 切换或失败后重复运行。对云资源创建、发送通知、支付等外部副作用,应另设幂等键和状态恢复机制;leader election 不等于 exactly-once。finalizer 可以延迟删除以清理外部资源,但也会因控制器失效阻塞删除,必须有可诊断状态与人工恢复路径。

fake client 的限制非常关键:它不做完整 OpenAPI validation,resourceVersion、generation、子资源等行为与真实 API 有差异。更接近 API 语义的下一层是 envtest,它运行测试 API server/etcd;但 envtest 本身也没有完整 kubelet、scheduler 等运行集群。本文没有启用 envtest,因此只声称验证了分支与调用方式。v0.25.2 fake 包

RBAC 要和缓存范围、动作范围一起收窄 ​

文末 Role 只允许在 demo 命名空间 get/list/watch/update ConfigMap,不包含 Secret、Pod、删除、集群管理员权限。RoleBinding 把它授给特定 ServiceAccount。若真正把 controller 部署进去,manager 的缓存观察范围也要限制在 demo,否则默认跨命名空间 list/watch 会被拒绝。Kubernetes RBAC

这个 Role 允许修改 demo 中的全部 ConfigMap,仍比“只改一个指定对象”更宽。实际系统可按功能拆命名空间或进一步限定读取条件与授权,但需考虑 list/watch 的约束;不能给所有 verbs 和 resources 写星号来消除报错。示例只演示 ConfigMap 业务权限,若启用 leader election,还需针对所选 namespace 和 Lease 配置单独评估权限,不能声称此 Role 已覆盖完整生产 manager。

本地程序没有加载 ServiceAccount 或 kubeconfig,RBAC YAML 也没有 apply。它之所以附在这里,是为了让“这个程序需要哪些外部权限”成为可审查的具体文件,而不是让测试通过替代权限设计。

版本选择要看组合,不是把每个包都升到 latest ​

2026-10-13 查阅的版本与本例选择如下。这个表是可核对的快照,阅读时不应当成永远最新的安装命令。

工具官方资料快照或本例固定值选择时还要核对
Docker Engine29.9.0 发布说明CLI、daemon、BuildKit、存储后端与宿主条件
Kubernetes发布页为 1.37.1发行版支持、版本偏差、feature gates、CNI/CSI
Helmv4.3.0 releasechart、插件和部署平台对主版本的兼容
Argo CDv3.5.5 release目标 Kubernetes、CRD、权限与升级说明
GitHub Actions服务没有一个统一客户端版本;示例 action 需固定 SHArunner 版本、action runtime 与仓库策略
kov0.19.1 release本地 Go 工具链、基础镜像、平台与推送策略
client-go本例 v0.37.1与 k8s.io/api、apimachinery 同系列
controller-runtime本例 v0.25.2官方兼容表对应 client-go v0.37;最低 Go 1.26

controller-runtime 每个 minor 对应特定 client-go minor,跨 minor 兼容不能想当然。其 v0.25.2 go.mod 使用 k8s.io v0.37 系列;本例三个直接依赖统一为 v0.37.1,并以 Go 1.27.2 实际编译验证。库兼容和 server API 兼容又是两层:库能编译,不保证旧集群支持新字段或新资源。兼容表与 go.mod

最后可以用三个问题收敛选择:只差一个镜像,就先在 Dockerfile 和 ko 中选;只差可追踪发布,就建立 CI、不可变制品与部署声明;已经出现重复、复杂且可定义的运维状态转换,再考虑 controller-runtime。增加一个长期运行的控制器,意味着增加一份需要升级、告警和故障恢复的服务。

验证记录与限制 ​

本例通过官方 Go 模块代理与 checksum 服务获取固定依赖,go.sum 随文保留。go mod verify 返回 all modules verified;go test -p 2 ./...、go test -p 2 -race ./...、go vet -p 2 ./... 均通过,go run -p 2 . 输出上文的两行结果。测试覆盖重复协调不重复写、保留已有键、对象缺失与注入冲突后的重新读取。程序没有注册 manager、watch 或队列,是直接调用 Reconcile 的单元级实验,不是一份已经运行的完整控制器。没有执行 Docker、ko、Helm、Argo CD 或 GitHub 工作流,没有调用任何真实 Kubernetes API,也没有创建资源或身份。

依据 ​

完整复现文件 ​

保存以下文件。首次需从官方模块代理下载 go.mod/go.sum 锁定的依赖,可在该目录执行 GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download;示例不读集群凭据。已有缓存后,可离线运行 sh verify.sh。脚本将编译并行度设为 2,避免首次构建庞大客户端依赖时造成过高瞬时压力。RBAC 清单仅供审查,未部署;demo 命名空间与真实 controller 生命周期不在本实验中创建。

go.mod ​

代码块 · 64 行 · 点击收起 / 展开
go
module example.com/toolchoice

go 1.27.0

require (
	k8s.io/api v0.37.1
	k8s.io/apimachinery v0.37.1
	k8s.io/client-go v0.37.1
	sigs.k8s.io/controller-runtime v0.25.2
)

require (
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
	github.com/emicklei/go-restful/v3 v3.13.0 // indirect
	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
	github.com/fxamacker/cbor/v2 v2.9.1 // indirect
	github.com/go-logr/logr v1.4.3 // indirect
	github.com/go-openapi/jsonpointer v1.0.0 // indirect
	github.com/go-openapi/jsonreference v1.0.0 // indirect
	github.com/go-openapi/swag v0.27.1 // indirect
	github.com/go-openapi/swag/cmdutils v0.27.1 // indirect
	github.com/go-openapi/swag/conv v0.27.1 // indirect
	github.com/go-openapi/swag/fileutils v0.27.1 // indirect
	github.com/go-openapi/swag/jsonutils v0.27.1 // indirect
	github.com/go-openapi/swag/loading v0.27.1 // indirect
	github.com/go-openapi/swag/mangling v0.27.1 // indirect
	github.com/go-openapi/swag/netutils v0.27.1 // indirect
	github.com/go-openapi/swag/pools v0.27.1 // indirect
	github.com/go-openapi/swag/stringutils v0.27.1 // indirect
	github.com/go-openapi/swag/typeutils v0.27.1 // indirect
	github.com/go-openapi/swag/yamlutils v0.27.1 // indirect
	github.com/google/gnostic-models v0.7.0 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
	github.com/prometheus/client_golang v1.24.0 // indirect
	github.com/prometheus/client_model v0.6.2 // indirect
	github.com/prometheus/common v0.70.0 // indirect
	github.com/prometheus/procfs v0.21.1 // indirect
	github.com/x448/float16 v0.8.4 // indirect
	go.yaml.in/yaml/v2 v2.4.4 // indirect
	go.yaml.in/yaml/v3 v3.0.4 // indirect
	golang.org/x/net v0.57.0 // indirect
	golang.org/x/oauth2 v0.36.0 // indirect
	golang.org/x/sys v0.47.0 // indirect
	golang.org/x/term v0.45.0 // indirect
	golang.org/x/text v0.40.0 // indirect
	golang.org/x/time v0.15.0 // indirect
	google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
	gopkg.in/inf.v0 v0.9.1 // indirect
	k8s.io/klog/v2 v2.140.0 // indirect
	k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
	k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect
	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
	sigs.k8s.io/randfill v1.0.0 // indirect
	sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
	sigs.k8s.io/yaml v1.6.0 // indirect
)

go.sum ​

代码块 · 169 行 · 点击收起 / 展开
text
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
github.com/go-openapi/swag v0.27.1 h1:VotvOLWW8q/EAxB0YdsBBGC8XYyeL1YwBj2ungAGPNg=
github.com/go-openapi/swag v0.27.1/go.mod h1:GTkJPwHfhJp6MWr4/rCh64HVI3Ofu+tcsbfjfHmTxpE=
github.com/go-openapi/swag/cmdutils v0.27.1 h1:I7sYqaWVl5mq0NEmNQkAmFDyNin9ufvMX/p2zwtQaOE=
github.com/go-openapi/swag/cmdutils v0.27.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
github.com/go-openapi/swag/conv v0.27.1 h1:8wi9ZG+olmY1wXphl93EWniPtbSPkXM/feH7FgjsvrU=
github.com/go-openapi/swag/conv v0.27.1/go.mod h1:QbqMivkpKhC3g1B1GGGOJ6ANewI3S62dbzYu3Duowqs=
github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzrb0QkmLKf9oM=
github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU=
github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1 h1:mJu3COL9WEaZVp/Kf2PRMi7tPszPEJfSr/OO75ynCs8=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY=
github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE=
github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA=
github.com/go-openapi/swag/mangling v0.27.1/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
github.com/go-openapi/swag/netutils v0.27.1 h1:mICMFoS82F5TZ4Zy3cqmcQk+BFeCp3Uyq3Np7GI0/qU=
github.com/go-openapi/swag/netutils v0.27.1/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
github.com/go-openapi/swag/pools v0.27.1 h1:9LeadcMyb2GJCbXX5hVQDbZ2Lq9TL4dCs/nx1j5DO0E=
github.com/go-openapi/swag/pools v0.27.1/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
github.com/go-openapi/swag/stringutils v0.27.1 h1:ZXePZ0r2p1qSjo8tD3Un4vFj8+FqlCkczxDrJIhYUp8=
github.com/go-openapi/swag/stringutils v0.27.1/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71UeEB3//PtVXc=
github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA=
github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y=
github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.37.1 h1:l6N77U7tjwB5L056bgrBTJIEdevac/naBZ3iSvDNfpM=
k8s.io/api v0.37.1/go.mod h1:zSlbB1YpJ1YQlFVQy20UYll81UJSJJUMLhkhvg6Z78M=
k8s.io/apiextensions-apiserver v0.37.0 h1:zRMQ3+/LIE5oZ0tVvXwYHC+dIkSP5cjNWju7AZU1LOI=
k8s.io/apiextensions-apiserver v0.37.0/go.mod h1:HU0PfSBwchHL5iDau6jjt9zU6ryWkDDlaVUiq91NK80=
k8s.io/apimachinery v0.37.1 h1:hGCYyvKHCwtwMitj2vU4vYx0Z16N9GyZk9BBnz0wDAE=
k8s.io/apimachinery v0.37.1/go.mod h1:jF84AyUi/IRIXRot5f+lm6MpxoWI+F1XgjaMmwCdTFw=
k8s.io/client-go v0.37.1 h1:QTv/5ha4jAHtW9qxxVBkQVFBRDb4jHfFopQqqMdc+wM=
k8s.io/client-go v0.37.1/go.mod h1:dnAPtTnCNY38Ho04D2KdY1F4IKausa9UbqaAZKl60SY=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/controller-runtime v0.25.2 h1:bEkK3PVOIVK9X8QWLGVhJgmFc++47vfT6wakSzAOLsQ=
sigs.k8s.io/controller-runtime v0.25.2/go.mod h1:4QqLdT6z/L6Olj8JJCtvztid4/fnIiYsfaTFScegctc=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

main.go ​

代码块 · 72 行 · 点击收起 / 展开
go
package main

import (
	"context"
	"fmt"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	clientgofake "k8s.io/client-go/kubernetes/fake"
	"sigs.k8s.io/controller-runtime/pkg/client"
	runtimefake "sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

type Reconciler struct{ Client client.Client }

func (r Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
	var cm corev1.ConfigMap
	if err := r.Client.Get(ctx, req.NamespacedName, &cm); err != nil {
		if apierrors.IsNotFound(err) {
			return reconcile.Result{}, nil
		}
		return reconcile.Result{}, err
	}
	if cm.Data["managed"] == "true" {
		return reconcile.Result{}, nil
	}
	if cm.Data == nil {
		cm.Data = map[string]string{}
	}
	cm.Data["managed"] = "true"
	return reconcile.Result{}, r.Client.Update(ctx, &cm)
}

type countingClient struct {
	client.Client
	updates int
}

func (c *countingClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
	c.updates++
	return c.Client.Update(ctx, obj, opts...)
}
func main() {
	ctx := context.Background()
	cm := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "app", Namespace: "demo"}, Data: map[string]string{"owner": "team"}}
	cs := clientgofake.NewClientset(cm.DeepCopy())
	list, err := cs.CoreV1().ConfigMaps("demo").List(ctx, metav1.ListOptions{})
	if err != nil {
		panic(err)
	}
	fmt.Println("client-go fake: configmaps=", len(list.Items))
	scheme := runtime.NewScheme()
	if err := corev1.AddToScheme(scheme); err != nil {
		panic(err)
	}
	c := &countingClient{Client: runtimefake.NewClientBuilder().WithScheme(scheme).WithObjects(cm.DeepCopy()).Build()}
	r := Reconciler{Client: c}
	req := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "demo", Name: "app"}}
	for range 2 {
		if _, err := r.Reconcile(ctx, req); err != nil {
			panic(err)
		}
	}
	var got corev1.ConfigMap
	if err := c.Get(ctx, req.NamespacedName, &got); err != nil {
		panic(err)
	}
	fmt.Printf("two reconciles: updates=%d managed=%s owner=%s\n", c.updates, got.Data["managed"], got.Data["owner"])
}

main_test.go ​

代码块 · 75 行 · 点击收起 / 展开
go
package main

import (
	"context"
	"errors"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	runtimefake "sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	"testing"
)

func newClient(t *testing.T) client.Client {
	t.Helper()
	s := runtime.NewScheme()
	if err := corev1.AddToScheme(s); err != nil {
		t.Fatal(err)
	}
	return runtimefake.NewClientBuilder().WithScheme(s).WithObjects(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "app", Namespace: "demo"}, Data: map[string]string{"owner": "team"}}).Build()
}
func request(name string) reconcile.Request {
	return reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "demo", Name: name}}
}
func TestIdempotent(t *testing.T) {
	ctx := context.Background()
	c := &countingClient{Client: newClient(t)}
	r := Reconciler{Client: c}
	for range 2 {
		if _, err := r.Reconcile(ctx, request("app")); err != nil {
			t.Fatal(err)
		}
	}
	if c.updates != 1 {
		t.Fatal(c.updates)
	}
	var cm corev1.ConfigMap
	if err := c.Get(ctx, request("app").NamespacedName, &cm); err != nil {
		t.Fatal(err)
	}
	if cm.Data["managed"] != "true" || cm.Data["owner"] != "team" {
		t.Fatal(cm.Data)
	}
	if _, err := r.Reconcile(ctx, request("missing")); err != nil {
		t.Fatal(err)
	}
}

type conflictOnce struct {
	client.Client
	failed bool
}

func (c *conflictOnce) Update(ctx context.Context, o client.Object, opts ...client.UpdateOption) error {
	if !c.failed {
		c.failed = true
		return apierrors.NewConflict(schema.GroupResource{Resource: "configmaps"}, o.GetName(), errors.New("injected conflict"))
	}
	return c.Client.Update(ctx, o, opts...)
}
func TestConflictIsReturnedThenReread(t *testing.T) {
	ctx := context.Background()
	c := &conflictOnce{Client: newClient(t)}
	r := Reconciler{Client: c}
	if _, err := r.Reconcile(ctx, request("app")); !apierrors.IsConflict(err) {
		t.Fatalf("expected conflict: %v", err)
	}
	if _, err := r.Reconcile(ctx, request("app")); err != nil {
		t.Fatal(err)
	}
}

rbac.yaml ​

yaml
# Unapplied teaching configuration. Namespace demo must already exist.
apiVersion: v1
kind: ServiceAccount
metadata:
  name: config-reconciler
  namespace: demo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: config-reconciler
  namespace: demo
rules:
  - apiGroups: ['']
    resources: [configmaps]
    verbs: [get, list, watch, update]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: config-reconciler
  namespace: demo
subjects:
  - kind: ServiceAccount
    name: config-reconciler
    namespace: demo
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: config-reconciler

verify.sh ​

sh
#!/bin/sh
set -eu
cd "$(dirname "$0")"
go version
go mod verify
go test -p 2 ./...
go test -p 2 -race ./...
go vet -p 2 ./...
go run -p 2 .

保留各书来源与许可 · 示例运行条件以正文为准