实现 client-go Pod backend
This commit is contained in:
+21
-12
@@ -21,18 +21,20 @@ runs-on: [self-hosted, vm]
|
||||
|
||||
## 一个 job,一个环境
|
||||
|
||||
Controller 根据 Gitea `workflow_job` webhook 创建执行环境。每个 Pod 或 VM 注册一个
|
||||
ephemeral runner,只执行一个 job;任务结束后注销 runner,并删除计算环境及其全部
|
||||
本地状态。
|
||||
Controller 通过 Gitea RunnerService 原子领取具体 task,再把版本化 assignment 交给
|
||||
一个一次性 Pod 或 VM。executor 直接执行已经领取的 task,不再注册临时 runner 去
|
||||
二次竞争任务;任务结束并回报 Gitea 后删除计算环境及其全部本地状态。
|
||||
|
||||
`job_id` 仅用于消息去重、状态追踪、实例关联和失败清理,不进入 workload 身份,也
|
||||
不参与资源授权。
|
||||
|
||||
Gitea 不保证由某次 `queued` webhook 创建的 runner 一定领取该 webhook 对应的 job。
|
||||
因此创建环境时只赋予无业务权限的启动身份。runner 实际领取任务后,controller 根据
|
||||
`in_progress` webhook 返回的 `runner_name` 和真实 job 名称绑定业务身份;环境中的
|
||||
job-start hook 必须等目标 SVID 可用后才放行 workflow 的第一步。不能依据 queued
|
||||
事件提前赋予任务权限。
|
||||
assignment ID 只用于消息去重、状态追踪、实例关联和失败清理,不进入 workload 身份,
|
||||
也不参与资源授权。worker 通过 assignment ID 从 Kubernetes labels 或 OpenSandbox
|
||||
metadata 恢复 executor;JetStream 不保存 executor 生命周期状态。
|
||||
|
||||
Pod executor 创建后,controller 使用实际 Pod UID 创建幂等 `ClusterStaticEntry`,将
|
||||
SPIFFE ID绑定到该 Pod 的 workload selector。executor 必须等目标 SVID 可用后才执行
|
||||
workflow 的第一步。
|
||||
|
||||
## 环境只提供运行边界
|
||||
|
||||
@@ -54,15 +56,15 @@ kind 等工具由 pipeline 按需安装和启动,而不是由 controller 预
|
||||
SPIFFE ID 由具有业务意义且稳定的 workflow 上下文派生:
|
||||
|
||||
```text
|
||||
spiffe://ddupan.top/ci/<owner>/<repository>/<job-name>
|
||||
spiffe://ddupan.top/ci/<owner>/<repository>/<job-key>
|
||||
```
|
||||
|
||||
同一种任务在不同运行中使用相同的逻辑 SPIFFE ID;每次运行取得独立、短期的 SVID。
|
||||
Pod 与 VM 是可替换的执行实现,因此默认不写入 SPIFFE ID。
|
||||
|
||||
job 名称必须经过确定性的路径规范化。规范化结果必须保留仓库边界,并在发生冲突时
|
||||
拒绝创建环境,不能静默地让两个任务共享身份。同一仓库内需要不同权限的任务应使用
|
||||
不同的 job 名称;workflow 文件只是编排载体,不进入权限身份。
|
||||
job key 必须满足 `[A-Za-z_][A-Za-z0-9_-]*`,展示名称 `name` 不参与身份计算。同一
|
||||
仓库内需要不同权限的任务应使用不同的 job key;workflow 文件只是编排载体,不进入
|
||||
权限身份。
|
||||
|
||||
## Self-service 与授权边界
|
||||
|
||||
@@ -86,3 +88,10 @@ OpenBao 或其他资源的特殊权限。资源所有者在资源端按照有意
|
||||
|
||||
仓库中的 `jwt-broker` 是早期方案的实验实现,在 Pod/VM 动态执行环境完成迁移后不应
|
||||
部署。
|
||||
|
||||
## 实现依赖原则
|
||||
|
||||
基础设施协议优先使用上游维护的成熟客户端,不在 controller 内重复实现认证、连接、
|
||||
资源编码或错误语义。Kubernetes 使用 `client-go`,NATS JetStream 使用 `nats.go`,
|
||||
Gitea RunnerService 使用 `actionslib`;自定义代码只保留领域模型、reconcile 规则及
|
||||
上游客户端未覆盖的最小适配层。
|
||||
|
||||
@@ -7,14 +7,58 @@ require (
|
||||
gitea.dev/actionslib v1.0.0
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
k8s.io/api v0.37.0
|
||||
k8s.io/apimachinery v0.37.0
|
||||
k8s.io/client-go v0.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // 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/klauspost/compress v1.20.0 // 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/nats-io/nkeys v0.4.16 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // 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.5 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/term v0.46.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/time v0.15.0 // 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
|
||||
)
|
||||
|
||||
@@ -2,27 +2,133 @@ connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||
gitea.dev/actionslib v1.0.0 h1:l0oFJP+P4Ds1rlCI5zk618dYkuBc2mU7Gz5wPeG0lZY=
|
||||
gitea.dev/actionslib v1.0.0/go.mod h1:6O8YHkqVTKSR0LL2e5VhIDePYzGTZCbfmSVqJWEhk9g=
|
||||
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/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-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/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/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/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
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/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
|
||||
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
|
||||
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
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/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
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.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
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.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
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/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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=
|
||||
k8s.io/api v0.37.0 h1:Z//Vj9N7RA/yS2sDmxyeo7h+RR4zbUrd2vrd3Z0TbB4=
|
||||
k8s.io/api v0.37.0/go.mod h1:LKXgcJWMc+f4OLbP5SFR8rulEg07zZhpi/zMULiBImk=
|
||||
k8s.io/apimachinery v0.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0=
|
||||
k8s.io/apimachinery v0.37.0/go.mod h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE=
|
||||
k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ=
|
||||
k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0=
|
||||
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/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=
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
// Package podbackend implements the native homelab Kubernetes executor backend.
|
||||
package podbackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskassignment"
|
||||
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskidentity"
|
||||
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskworker"
|
||||
)
|
||||
|
||||
const assignmentLabel = "ci.ddupan.top/assignment-id"
|
||||
|
||||
// Pod is the backend state required by the reconciler, not an in-memory lifecycle record.
|
||||
type Pod struct {
|
||||
Name string
|
||||
Namespace string
|
||||
UID string
|
||||
Phase string
|
||||
Labels map[string]string
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
// PodManifest leaves task execution wiring to the executor image while fixing
|
||||
// the metadata contract needed for recovery.
|
||||
type PodManifest struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Labels map[string]string
|
||||
Annotations map[string]string
|
||||
Image string
|
||||
ServiceAccount string
|
||||
Args []string
|
||||
}
|
||||
|
||||
// IdentityEntry is a ClusterStaticEntry pinned to one concrete Pod UID.
|
||||
type IdentityEntry struct {
|
||||
Name string
|
||||
Labels map[string]string
|
||||
ClassName string
|
||||
ParentID string
|
||||
SPIFFEID string
|
||||
Selectors []string
|
||||
}
|
||||
|
||||
// API is the narrow Kubernetes boundary used by the backend adapter.
|
||||
type API interface {
|
||||
ListPods(context.Context, string, string) ([]Pod, error)
|
||||
CreatePod(context.Context, PodManifest) (Pod, error)
|
||||
DeletePod(context.Context, string, string) error
|
||||
EnsureIdentityEntry(context.Context, IdentityEntry) error
|
||||
DeleteIdentityEntry(context.Context, string) error
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Namespace string
|
||||
Image string
|
||||
ServiceAccount string
|
||||
ExecutorArgs []string
|
||||
TrustDomain string
|
||||
SPIRECluster string
|
||||
SPIREClass string
|
||||
ExecutorUID int
|
||||
}
|
||||
|
||||
type Backend struct {
|
||||
API API
|
||||
Config Config
|
||||
}
|
||||
|
||||
func (b Backend) Find(ctx context.Context, assignmentID string) (*taskworker.Executor, error) {
|
||||
if err := b.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pods, err := b.API.ListPods(ctx, b.Config.Namespace, assignmentLabel+"="+assignmentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list assignment Pods: %w", err)
|
||||
}
|
||||
if len(pods) > 1 {
|
||||
return nil, fmt.Errorf("assignment %s owns %d Pods", assignmentID, len(pods))
|
||||
}
|
||||
if len(pods) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return executor(pods[0]), nil
|
||||
}
|
||||
|
||||
func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignment, metadata taskworker.Metadata) (*taskworker.Executor, error) {
|
||||
if err := b.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if assignment.Backend != taskassignment.BackendPod {
|
||||
return nil, fmt.Errorf("Pod backend cannot create %q assignment", assignment.Backend)
|
||||
}
|
||||
labels := clone(metadata.Labels)
|
||||
labels["app.kubernetes.io/name"] = "gitea-dynamic-runner"
|
||||
labels["app.kubernetes.io/component"] = "executor"
|
||||
pod, err := b.API.CreatePod(ctx, PodManifest{
|
||||
Name: assignment.ID,
|
||||
Namespace: b.Config.Namespace,
|
||||
Labels: labels,
|
||||
Annotations: clone(metadata.Annotations),
|
||||
Image: b.Config.Image,
|
||||
ServiceAccount: b.Config.ServiceAccount,
|
||||
Args: append(append([]string{}, b.Config.ExecutorArgs...), assignment.ID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create assignment Pod: %w", err)
|
||||
}
|
||||
return executor(pod), nil
|
||||
}
|
||||
|
||||
func (b Backend) BindIdentity(ctx context.Context, executor *taskworker.Executor, identity taskidentity.Identity) error {
|
||||
if err := b.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if executor == nil || executor.Name == "" || executor.IdentityTarget == "" {
|
||||
return errors.New("Pod name and UID are required for identity binding")
|
||||
}
|
||||
if _, err := identityPath(identity.SPIFFEID, b.Config.TrustDomain); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.API.EnsureIdentityEntry(ctx, IdentityEntry{
|
||||
Name: executor.Name,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/name": "gitea-dynamic-runner",
|
||||
"app.kubernetes.io/component": "pod-identity",
|
||||
assignmentLabel: executor.Name,
|
||||
},
|
||||
ClassName: b.Config.SPIREClass,
|
||||
ParentID: fmt.Sprintf(
|
||||
"spiffe://%s/spire/agent/k8s_psat/%s/pod/%s",
|
||||
b.Config.TrustDomain, b.Config.SPIRECluster, executor.IdentityTarget,
|
||||
),
|
||||
SPIFFEID: identity.SPIFFEID,
|
||||
Selectors: []string{fmt.Sprintf("unix:uid:%d", b.Config.ExecutorUID)},
|
||||
})
|
||||
}
|
||||
|
||||
func identityPath(spiffeID, trustDomain string) (string, error) {
|
||||
parsed, err := url.Parse(spiffeID)
|
||||
if err != nil || parsed.Scheme != "spiffe" || parsed.Host != trustDomain || !strings.HasPrefix(parsed.Path, "/ci/") {
|
||||
return "", fmt.Errorf("invalid CI SPIFFE ID %q", spiffeID)
|
||||
}
|
||||
return strings.TrimPrefix(parsed.Path, "/ci/"), nil
|
||||
}
|
||||
|
||||
func (b Backend) Delete(ctx context.Context, executor *taskworker.Executor) error {
|
||||
if err := b.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if executor == nil || executor.Name == "" {
|
||||
return nil
|
||||
}
|
||||
if err := b.API.DeleteIdentityEntry(ctx, executor.Name); err != nil {
|
||||
return fmt.Errorf("delete Pod identity entry: %w", err)
|
||||
}
|
||||
if err := b.API.DeletePod(ctx, b.Config.Namespace, executor.Name); err != nil {
|
||||
return fmt.Errorf("delete assignment Pod: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Backend) validate() error {
|
||||
if b.API == nil || b.Config.Namespace == "" || b.Config.Image == "" || b.Config.ServiceAccount == "" || b.Config.TrustDomain == "" || b.Config.SPIRECluster == "" || b.Config.SPIREClass == "" || b.Config.ExecutorUID < 1 {
|
||||
return errors.New("Pod API and complete executor/SPIRE configuration are required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func executor(pod Pod) *taskworker.Executor {
|
||||
return &taskworker.Executor{
|
||||
Name: pod.Name,
|
||||
IdentityTarget: pod.UID,
|
||||
Phase: phase(pod.Phase),
|
||||
}
|
||||
}
|
||||
|
||||
func phase(value string) taskworker.Phase {
|
||||
switch value {
|
||||
case "Succeeded":
|
||||
return taskworker.PhaseSucceeded
|
||||
case "Failed":
|
||||
return taskworker.PhaseFailed
|
||||
case "Running":
|
||||
return taskworker.PhaseRunning
|
||||
default:
|
||||
return taskworker.PhasePending
|
||||
}
|
||||
}
|
||||
|
||||
func clone(source map[string]string) map[string]string {
|
||||
target := make(map[string]string, len(source))
|
||||
for key, value := range source {
|
||||
target[key] = value
|
||||
}
|
||||
return target
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package podbackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
|
||||
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskassignment"
|
||||
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskidentity"
|
||||
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskworker"
|
||||
)
|
||||
|
||||
type fakeAPI struct {
|
||||
pods []Pod
|
||||
selector string
|
||||
created PodManifest
|
||||
entry IdentityEntry
|
||||
entryGone string
|
||||
deleted string
|
||||
}
|
||||
|
||||
func (a *fakeAPI) ListPods(_ context.Context, _ string, selector string) ([]Pod, error) {
|
||||
a.selector = selector
|
||||
return a.pods, nil
|
||||
}
|
||||
func (a *fakeAPI) CreatePod(_ context.Context, manifest PodManifest) (Pod, error) {
|
||||
a.created = manifest
|
||||
return Pod{Name: manifest.Name, Namespace: manifest.Namespace, UID: "pod-uid", Phase: "Pending"}, nil
|
||||
}
|
||||
func (a *fakeAPI) DeletePod(_ context.Context, _, name string) error {
|
||||
a.deleted = name
|
||||
return nil
|
||||
}
|
||||
func (a *fakeAPI) EnsureIdentityEntry(_ context.Context, entry IdentityEntry) error {
|
||||
a.entry = entry
|
||||
return nil
|
||||
}
|
||||
func (a *fakeAPI) DeleteIdentityEntry(_ context.Context, name string) error {
|
||||
a.entryGone = name
|
||||
return nil
|
||||
}
|
||||
|
||||
func backend(api API) Backend {
|
||||
return Backend{API: api, Config: Config{
|
||||
Namespace: "gitea-actions", Image: "zot/ci-executor:main",
|
||||
ServiceAccount: "gitea-task-executor", ExecutorArgs: []string{"execute"},
|
||||
TrustDomain: "ddupan.top", SPIRECluster: "homelab",
|
||||
SPIREClass: "spire-mgmt-spire", ExecutorUID: 2000,
|
||||
}}
|
||||
}
|
||||
|
||||
func assignment() taskassignment.Assignment {
|
||||
return taskassignment.Assignment{
|
||||
ID: "gitea-task-42", Backend: taskassignment.BackendPod,
|
||||
Task: &runnerv1.Task{Id: 42},
|
||||
Identity: taskidentity.Identity{
|
||||
Repository: "owner/repo", Task: "publish",
|
||||
SPIFFEID: "spiffe://ddupan.top/ci/owner/repo/publish",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRecoversPodByAssignmentLabel(t *testing.T) {
|
||||
api := &fakeAPI{pods: []Pod{{Name: "gitea-task-42", UID: "uid", Phase: "Running"}}}
|
||||
executor, err := backend(api).Find(context.Background(), "gitea-task-42")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if api.selector != "ci.ddupan.top/assignment-id=gitea-task-42" || executor.Name != "gitea-task-42" || executor.IdentityTarget != "uid" || executor.Phase != taskworker.PhaseRunning {
|
||||
t.Fatalf("selector=%q executor=%#v", api.selector, executor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateUsesDeterministicNameAndRecoveryMetadata(t *testing.T) {
|
||||
api := &fakeAPI{}
|
||||
metadata := taskworker.BackendMetadata(assignment())
|
||||
executor, err := backend(api).Create(context.Background(), assignment(), metadata)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if api.created.Name != "gitea-task-42" || api.created.Labels[assignmentLabel] != "gitea-task-42" {
|
||||
t.Fatalf("manifest = %#v", api.created)
|
||||
}
|
||||
if api.created.Annotations["ci.ddupan.top/spiffe-id"] != assignment().Identity.SPIFFEID {
|
||||
t.Fatalf("annotations = %#v", api.created.Annotations)
|
||||
}
|
||||
if len(api.created.Args) != 2 || api.created.Args[1] != "gitea-task-42" || executor.IdentityTarget != "pod-uid" {
|
||||
t.Fatalf("args=%v executor=%#v", api.created.Args, executor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindIdentityCreatesEntryPinnedToPodUID(t *testing.T) {
|
||||
api := &fakeAPI{}
|
||||
executor := &taskworker.Executor{Name: "gitea-task-42", IdentityTarget: "pod-uid"}
|
||||
if err := backend(api).BindIdentity(context.Background(), executor, assignment().Identity); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if api.entry.Name != executor.Name || api.entry.SPIFFEID != assignment().Identity.SPIFFEID || api.entry.ParentID != "spiffe://ddupan.top/spire/agent/k8s_psat/homelab/pod/pod-uid" || len(api.entry.Selectors) != 1 || api.entry.Selectors[0] != "unix:uid:2000" {
|
||||
t.Fatalf("entry=%#v", api.entry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRemovesIdentityBeforePod(t *testing.T) {
|
||||
api := &fakeAPI{}
|
||||
executor := &taskworker.Executor{Name: "gitea-task-42", IdentityTarget: "pod-uid"}
|
||||
if err := backend(api).Delete(context.Background(), executor); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if api.entryGone != executor.Name || api.deleted != executor.Name {
|
||||
t.Fatalf("entry=%q pod=%q", api.entryGone, api.deleted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRejectsDuplicatePods(t *testing.T) {
|
||||
api := &fakeAPI{pods: []Pod{{Name: "one"}, {Name: "two"}}}
|
||||
if _, err := backend(api).Find(context.Background(), "gitea-task-42"); err == nil {
|
||||
t.Fatal("expected duplicate executor error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package podbackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
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/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
var identityEntryResource = schema.GroupVersionResource{
|
||||
Group: "spire.spiffe.io", Version: "v1alpha1", Resource: "clusterstaticentries",
|
||||
}
|
||||
|
||||
// Client uses client-go's typed client for Pods and its dynamic client for the
|
||||
// SPIRE Operator CRD.
|
||||
type Client struct {
|
||||
Kubernetes kubernetes.Interface
|
||||
Dynamic dynamic.Interface
|
||||
}
|
||||
|
||||
func NewClient(config *rest.Config) (*Client, error) {
|
||||
kubernetesClient, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create Kubernetes client: %w", err)
|
||||
}
|
||||
dynamicClient, err := dynamic.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create Kubernetes dynamic client: %w", err)
|
||||
}
|
||||
return &Client{Kubernetes: kubernetesClient, Dynamic: dynamicClient}, nil
|
||||
}
|
||||
|
||||
func NewInClusterClient() (*Client, error) {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load in-cluster Kubernetes config: %w", err)
|
||||
}
|
||||
return NewClient(config)
|
||||
}
|
||||
|
||||
func (c *Client) ListPods(ctx context.Context, namespace, selector string) ([]Pod, error) {
|
||||
list, err := c.Kubernetes.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{LabelSelector: selector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pods := make([]Pod, 0, len(list.Items))
|
||||
for _, item := range list.Items {
|
||||
pods = append(pods, podFromKubernetes(item))
|
||||
}
|
||||
return pods, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreatePod(ctx context.Context, manifest PodManifest) (Pod, error) {
|
||||
document := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: manifest.Name, Namespace: manifest.Namespace,
|
||||
Labels: manifest.Labels, Annotations: manifest.Annotations,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
ServiceAccountName: manifest.ServiceAccount,
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
Containers: []corev1.Container{{
|
||||
Name: "executor", Image: manifest.Image, Args: manifest.Args,
|
||||
SecurityContext: &corev1.SecurityContext{Privileged: boolPointer(true)},
|
||||
VolumeMounts: []corev1.VolumeMount{{
|
||||
Name: "spire-agent-socket", MountPath: "/run/spire/agent-sockets", ReadOnly: true,
|
||||
}},
|
||||
}},
|
||||
Volumes: []corev1.Volume{{
|
||||
Name: "spire-agent-socket",
|
||||
VolumeSource: corev1.VolumeSource{CSI: &corev1.CSIVolumeSource{
|
||||
Driver: "csi.spiffe.io", ReadOnly: boolPointer(true),
|
||||
}},
|
||||
}},
|
||||
},
|
||||
}
|
||||
created, err := c.Kubernetes.CoreV1().Pods(manifest.Namespace).Create(ctx, document, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return Pod{}, err
|
||||
}
|
||||
return podFromKubernetes(*created), nil
|
||||
}
|
||||
|
||||
func (c *Client) DeletePod(ctx context.Context, namespace, name string) error {
|
||||
policy := metav1.DeletePropagationBackground
|
||||
err := c.Kubernetes.CoreV1().Pods(namespace).Delete(ctx, name, metav1.DeleteOptions{PropagationPolicy: &policy})
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) EnsureIdentityEntry(ctx context.Context, entry IdentityEntry) error {
|
||||
resource := c.Dynamic.Resource(identityEntryResource)
|
||||
existing, err := resource.Get(ctx, entry.Name, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
existingSpec, _, nestedErr := unstructured.NestedMap(existing.Object, "spec")
|
||||
if nestedErr != nil {
|
||||
return nestedErr
|
||||
}
|
||||
if !reflect.DeepEqual(existingSpec, identityEntryObject(entry).Object["spec"]) {
|
||||
return fmt.Errorf("identity entry %s exists with different selectors or SPIFFE ID", entry.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
_, err = resource.Create(ctx, identityEntryObject(entry), metav1.CreateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) DeleteIdentityEntry(ctx context.Context, name string) error {
|
||||
policy := metav1.DeletePropagationBackground
|
||||
err := c.Dynamic.Resource(identityEntryResource).Delete(ctx, name, metav1.DeleteOptions{PropagationPolicy: &policy})
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func identityEntryObject(entry IdentityEntry) *unstructured.Unstructured {
|
||||
selectors := make([]any, len(entry.Selectors))
|
||||
for index, selector := range entry.Selectors {
|
||||
selectors[index] = selector
|
||||
}
|
||||
return &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "spire.spiffe.io/v1alpha1",
|
||||
"kind": "ClusterStaticEntry",
|
||||
"metadata": map[string]any{
|
||||
"name": entry.Name, "labels": stringMap(entry.Labels),
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"className": entry.ClassName, "parentID": entry.ParentID,
|
||||
"spiffeID": entry.SPIFFEID, "selectors": selectors,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func podFromKubernetes(pod corev1.Pod) Pod {
|
||||
return Pod{
|
||||
Name: pod.Name, Namespace: pod.Namespace, UID: string(pod.UID),
|
||||
Phase: string(pod.Status.Phase), Labels: pod.Labels, Annotations: pod.Annotations,
|
||||
}
|
||||
}
|
||||
|
||||
func boolPointer(value bool) *bool { return &value }
|
||||
|
||||
func stringMap(values map[string]string) map[string]any {
|
||||
result := make(map[string]any, len(values))
|
||||
for key, value := range values {
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package podbackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func testClient(objects ...runtime.Object) *Client {
|
||||
return &Client{
|
||||
Kubernetes: fake.NewSimpleClientset(objects...),
|
||||
Dynamic: dynamicfake.NewSimpleDynamicClient(runtime.NewScheme()),
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientPodLifecycleUsesTypedClient(t *testing.T) {
|
||||
client := testClient()
|
||||
created, err := client.CreatePod(context.Background(), PodManifest{
|
||||
Name: "gitea-task-42", Namespace: "gitea-actions",
|
||||
Labels: map[string]string{assignmentLabel: "gitea-task-42"},
|
||||
Image: "zot/ci-executor:main", ServiceAccount: "gitea-task-executor",
|
||||
Args: []string{"execute", "gitea-task-42"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pod, err := client.Kubernetes.CoreV1().Pods("gitea-actions").Get(context.Background(), created.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pod.UID = types.UID("pod-uid")
|
||||
pod.Status.Phase = corev1.PodRunning
|
||||
if _, err := client.Kubernetes.CoreV1().Pods("gitea-actions").Update(context.Background(), pod, metav1.UpdateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pods, err := client.ListPods(context.Background(), "gitea-actions", assignmentLabel+"=gitea-task-42")
|
||||
if err != nil || len(pods) != 1 || pods[0].UID != "pod-uid" || pods[0].Phase != "Running" {
|
||||
t.Fatalf("pods=%#v err=%v", pods, err)
|
||||
}
|
||||
if err := client.DeletePod(context.Background(), "gitea-actions", created.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientEnsuresIdempotentClusterStaticEntry(t *testing.T) {
|
||||
client := testClient()
|
||||
entry := IdentityEntry{
|
||||
Name: "gitea-task-42", Labels: map[string]string{assignmentLabel: "gitea-task-42"},
|
||||
ClassName: "spire-mgmt-spire",
|
||||
ParentID: "spiffe://ddupan.top/spire/agent/k8s_psat/homelab/pod/pod-uid",
|
||||
SPIFFEID: "spiffe://ddupan.top/ci/owner/repo/publish",
|
||||
Selectors: []string{"unix:uid:2000"},
|
||||
}
|
||||
if err := client.EnsureIdentityEntry(context.Background(), entry); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.EnsureIdentityEntry(context.Background(), entry); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.DeleteIdentityEntry(context.Background(), entry.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.DeleteIdentityEntry(context.Background(), entry.Name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user