feat: bootstrap official runner through SPIFFE facade
This commit is contained in:
@@ -94,4 +94,5 @@ OpenBao 或其他资源的特殊权限。资源所有者在资源端按照有意
|
|||||||
基础设施协议优先使用上游维护的成熟客户端,不在 controller 内重复实现认证、连接、
|
基础设施协议优先使用上游维护的成熟客户端,不在 controller 内重复实现认证、连接、
|
||||||
资源编码或错误语义。Kubernetes 使用 `client-go`,NATS JetStream 使用 `nats.go`,
|
资源编码或错误语义。Kubernetes 使用 `client-go`,NATS JetStream 使用 `nats.go`,
|
||||||
Gitea RunnerService 使用 `actionslib`,OpenSandbox Lifecycle API 使用官方 Go SDK;
|
Gitea RunnerService 使用 `actionslib`,OpenSandbox Lifecycle API 使用官方 Go SDK;
|
||||||
自定义代码只保留领域模型、reconcile 规则及上游客户端未覆盖的最小适配层。
|
SPIFFE Workload API 与 mTLS 使用 `go-spiffe`。自定义代码只保留领域模型、reconcile
|
||||||
|
规则及上游客户端未覆盖的最小适配层。
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ controller 暴露兼容 RunnerService 的 facade:`FetchTask` 只返回已分
|
|||||||
ID,以及由 controller 密钥确定性生成的 assignment HMAC capability;该 capability
|
ID,以及由 controller 密钥确定性生成的 assignment HMAC capability;该 capability
|
||||||
只绑定执行实例,不参与 Zot/OpenBao 等业务授权。
|
只绑定执行实例,不参与 Zot/OpenBao 等业务授权。
|
||||||
|
|
||||||
|
executor 为官方 runner 生成与 v3.5.0 schema 一致的一次性 `.runner` 文件,并以
|
||||||
|
`daemon --once` 启动。runner 只访问 executor 内的 loopback HTTP proxy;proxy 使用
|
||||||
|
`go-spiffe` 从 Workload API 持续取得和轮换 X509-SVID,再以 mTLS 连接 controller
|
||||||
|
facade,并严格校验 facade 的 SPIFFE ID。这样无需修改 runner 或把静态客户端证书写入
|
||||||
|
镜像。assignment capability 会进入一次性 executor 环境,但不会进入 label、annotation
|
||||||
|
或 OpenSandbox metadata;它只对该 assignment 有效,并且不能绕过 SPIFFE 身份校验。
|
||||||
|
|
||||||
这与“收到 webhook 后临时注册另一个 act_runner”不同。`FetchTask` 已经完成任务分配,
|
这与“收到 webhook 后临时注册另一个 act_runner”不同。`FetchTask` 已经完成任务分配,
|
||||||
不能再期待 Gitea 把同一个 task 分配给随后启动的 runner。协议调度器必须让 executor
|
不能再期待 Gitea 把同一个 task 分配给随后启动的 runner。协议调度器必须让 executor
|
||||||
执行已经领取的 task,并继续完成日志、状态、心跳、取消和最终结果上报。
|
执行已经领取的 task,并继续完成日志、状态、心跳、取消和最终结果上报。
|
||||||
@@ -67,6 +74,8 @@ ID,以及由 controller 密钥确定性生成的 assignment HMAC capability;
|
|||||||
- consumer 在 executor 使用上述 facade 成功 claim task 后确认 assignment;无需把完整
|
- consumer 在 executor 使用上述 facade 成功 claim task 后确认 assignment;无需把完整
|
||||||
task 写入 Pod annotation、OpenSandbox metadata 或环境变量。
|
task 写入 Pod annotation、OpenSandbox metadata 或环境变量。
|
||||||
- Pod 与 VM 共享 task/executor 协议,只有环境创建和销毁实现不同。
|
- Pod 与 VM 共享 task/executor 协议,只有环境创建和销毁实现不同。
|
||||||
|
- 两种 backend 都注入同一份 runner bootstrap 环境;Pod 仍由 homelab Kubernetes 原生
|
||||||
|
创建,只有 VM 经 OpenSandbox 创建,bootstrap 机制不改变 backend 边界。
|
||||||
|
|
||||||
## 实现顺序
|
## 实现顺序
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ require (
|
|||||||
gitea.dev/actionslib v1.0.0
|
gitea.dev/actionslib v1.0.0
|
||||||
github.com/alibaba/OpenSandbox/sdks/sandbox/go v1.0.5
|
github.com/alibaba/OpenSandbox/sdks/sandbox/go v1.0.5
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
|
github.com/spiffe/go-spiffe/v2 v2.8.2
|
||||||
|
golang.org/x/sync v0.23.0
|
||||||
google.golang.org/protobuf v1.36.12
|
google.golang.org/protobuf v1.36.12
|
||||||
k8s.io/api v0.37.0
|
k8s.io/api v0.37.0
|
||||||
k8s.io/apimachinery v0.37.0
|
k8s.io/apimachinery v0.37.0
|
||||||
@@ -14,9 +16,11 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
||||||
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
||||||
@@ -49,11 +53,12 @@ require (
|
|||||||
golang.org/x/crypto v0.57.0 // indirect
|
golang.org/x/crypto v0.57.0 // indirect
|
||||||
golang.org/x/net v0.58.0 // indirect
|
golang.org/x/net v0.58.0 // indirect
|
||||||
golang.org/x/oauth2 v0.36.0 // indirect
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
golang.org/x/sync v0.23.0 // indirect
|
|
||||||
golang.org/x/sys v0.48.0 // indirect
|
golang.org/x/sys v0.48.0 // indirect
|
||||||
golang.org/x/term v0.46.0 // indirect
|
golang.org/x/term v0.46.0 // indirect
|
||||||
golang.org/x/text v0.42.0 // indirect
|
golang.org/x/text v0.42.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
|
google.golang.org/grpc v1.79.3 // indirect
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
k8s.io/klog/v2 v2.140.0 // indirect
|
k8s.io/klog/v2 v2.140.0 // indirect
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
|||||||
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
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 h1:l0oFJP+P4Ds1rlCI5zk618dYkuBc2mU7Gz5wPeG0lZY=
|
||||||
gitea.dev/actionslib v1.0.0/go.mod h1:6O8YHkqVTKSR0LL2e5VhIDePYzGTZCbfmSVqJWEhk9g=
|
gitea.dev/actionslib v1.0.0/go.mod h1:6O8YHkqVTKSR0LL2e5VhIDePYzGTZCbfmSVqJWEhk9g=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/alibaba/OpenSandbox/sdks/sandbox/go v1.0.5 h1:7mZNkBh4VaI+i4VOTJIknLCBkbGizTkdHYqgcMqeDBM=
|
github.com/alibaba/OpenSandbox/sdks/sandbox/go v1.0.5 h1:7mZNkBh4VaI+i4VOTJIknLCBkbGizTkdHYqgcMqeDBM=
|
||||||
github.com/alibaba/OpenSandbox/sdks/sandbox/go v1.0.5/go.mod h1:w0nIMCTL1L3oSS67ABFOdYxrezYQZhk01UtmKSi2UhA=
|
github.com/alibaba/OpenSandbox/sdks/sandbox/go v1.0.5/go.mod h1:w0nIMCTL1L3oSS67ABFOdYxrezYQZhk01UtmKSi2UhA=
|
||||||
|
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.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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
@@ -12,8 +16,12 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF
|
|||||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
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 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
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/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
|
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/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 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||||
@@ -48,6 +56,8 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL
|
|||||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
|
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 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
|
||||||
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
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/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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
@@ -80,6 +90,8 @@ github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBB
|
|||||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
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 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spiffe/go-spiffe/v2 v2.8.2 h1:jUEsvCMD6fH25J8K/w3q/XnIx8W1lb8+YLaEEHIjHmc=
|
||||||
|
github.com/spiffe/go-spiffe/v2 v2.8.2/go.mod h1:w2CLWKLMTX/PPYUEUPv3ltH0RXsw5S8suwNF46w9/Aw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
@@ -88,6 +100,18 @@ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWD
|
|||||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
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/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 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
@@ -110,6 +134,12 @@ 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/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 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
|
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||||
|
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func (b Backend) Find(ctx context.Context, assignmentID string) (*taskworker.Exe
|
|||||||
return executor(result.Items[0]), nil
|
return executor(result.Items[0]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignment, metadata taskworker.Metadata) (*taskworker.Executor, error) {
|
func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignment, launch taskworker.LaunchSpec) (*taskworker.Executor, error) {
|
||||||
if err := b.validate(); err != nil {
|
if err := b.validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -69,10 +69,11 @@ func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignmen
|
|||||||
return nil, fmt.Errorf("OpenSandbox backend cannot create %q assignment", assignment.Backend)
|
return nil, fmt.Errorf("OpenSandbox backend cannot create %q assignment", assignment.Backend)
|
||||||
}
|
}
|
||||||
environment := clone(b.Config.Env)
|
environment := clone(b.Config.Env)
|
||||||
environment["CI_ASSIGNMENT_ID"] = assignment.ID
|
for key, value := range launch.Environment {
|
||||||
environment["CI_SPIFFE_ID"] = assignment.Identity.SPIFFEID
|
environment[key] = value
|
||||||
sandboxMetadata := clone(metadata.Annotations)
|
}
|
||||||
for key, value := range metadata.Labels {
|
sandboxMetadata := clone(launch.Metadata.Annotations)
|
||||||
|
for key, value := range launch.Metadata.Labels {
|
||||||
sandboxMetadata[key] = value
|
sandboxMetadata[key] = value
|
||||||
}
|
}
|
||||||
request := opensandbox.CreateSandboxRequest{
|
request := opensandbox.CreateSandboxRequest{
|
||||||
|
|||||||
@@ -62,13 +62,22 @@ func TestFindRecoversSandboxByMetadata(t *testing.T) {
|
|||||||
func TestCreateUsesPoolAndPersistsRecoveryMetadata(t *testing.T) {
|
func TestCreateUsesPoolAndPersistsRecoveryMetadata(t *testing.T) {
|
||||||
lifecycle := &fakeLifecycle{}
|
lifecycle := &fakeLifecycle{}
|
||||||
metadata := taskworker.BackendMetadata(assignment())
|
metadata := taskworker.BackendMetadata(assignment())
|
||||||
executor, err := backend(lifecycle).Create(context.Background(), assignment(), metadata)
|
executor, err := backend(lifecycle).Create(context.Background(), assignment(), taskworker.LaunchSpec{
|
||||||
|
Metadata: metadata,
|
||||||
|
Environment: map[string]string{
|
||||||
|
"CI_SPIFFE_ID": assignment().Identity.SPIFFEID,
|
||||||
|
"CI_RUNNER_CAPABILITY": "capability",
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if lifecycle.created.Extensions["poolRef"] != "ci-vm" || lifecycle.created.Metadata[assignmentMetadata] != assignment().ID || lifecycle.created.Env["CI_SPIFFE_ID"] != assignment().Identity.SPIFFEID || executor.Name != "sandbox-42" {
|
if lifecycle.created.Extensions["poolRef"] != "ci-vm" || lifecycle.created.Metadata[assignmentMetadata] != assignment().ID || lifecycle.created.Env["CI_SPIFFE_ID"] != assignment().Identity.SPIFFEID || executor.Name != "sandbox-42" {
|
||||||
t.Fatalf("request=%#v executor=%#v", lifecycle.created, executor)
|
t.Fatalf("request=%#v executor=%#v", lifecycle.created, executor)
|
||||||
}
|
}
|
||||||
|
if lifecycle.created.Env["CI_RUNNER_CAPABILITY"] != "capability" {
|
||||||
|
t.Fatalf("environment = %#v", lifecycle.created.Env)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindIdentityVerifiesPersistedMetadata(t *testing.T) {
|
func TestBindIdentityVerifiesPersistedMetadata(t *testing.T) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type PodManifest struct {
|
|||||||
Image string
|
Image string
|
||||||
ServiceAccount string
|
ServiceAccount string
|
||||||
Args []string
|
Args []string
|
||||||
|
Environment map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// IdentityEntry is a ClusterStaticEntry pinned to one concrete Pod UID.
|
// IdentityEntry is a ClusterStaticEntry pinned to one concrete Pod UID.
|
||||||
@@ -89,24 +90,25 @@ func (b Backend) Find(ctx context.Context, assignmentID string) (*taskworker.Exe
|
|||||||
return executor(pods[0]), nil
|
return executor(pods[0]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignment, metadata taskworker.Metadata) (*taskworker.Executor, error) {
|
func (b Backend) Create(ctx context.Context, assignment taskassignment.Assignment, launch taskworker.LaunchSpec) (*taskworker.Executor, error) {
|
||||||
if err := b.validate(); err != nil {
|
if err := b.validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if assignment.Backend != taskassignment.BackendPod {
|
if assignment.Backend != taskassignment.BackendPod {
|
||||||
return nil, fmt.Errorf("Pod backend cannot create %q assignment", assignment.Backend)
|
return nil, fmt.Errorf("Pod backend cannot create %q assignment", assignment.Backend)
|
||||||
}
|
}
|
||||||
labels := clone(metadata.Labels)
|
labels := clone(launch.Metadata.Labels)
|
||||||
labels["app.kubernetes.io/name"] = "gitea-dynamic-runner"
|
labels["app.kubernetes.io/name"] = "gitea-dynamic-runner"
|
||||||
labels["app.kubernetes.io/component"] = "executor"
|
labels["app.kubernetes.io/component"] = "executor"
|
||||||
pod, err := b.API.CreatePod(ctx, PodManifest{
|
pod, err := b.API.CreatePod(ctx, PodManifest{
|
||||||
Name: assignment.ID,
|
Name: assignment.ID,
|
||||||
Namespace: b.Config.Namespace,
|
Namespace: b.Config.Namespace,
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
Annotations: clone(metadata.Annotations),
|
Annotations: clone(launch.Metadata.Annotations),
|
||||||
Image: b.Config.Image,
|
Image: b.Config.Image,
|
||||||
ServiceAccount: b.Config.ServiceAccount,
|
ServiceAccount: b.Config.ServiceAccount,
|
||||||
Args: append(append([]string{}, b.Config.ExecutorArgs...), assignment.ID),
|
Args: append(append([]string{}, b.Config.ExecutorArgs...), assignment.ID),
|
||||||
|
Environment: clone(launch.Environment),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create assignment Pod: %w", err)
|
return nil, fmt.Errorf("create assignment Pod: %w", err)
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ func TestFindRecoversPodByAssignmentLabel(t *testing.T) {
|
|||||||
func TestCreateUsesDeterministicNameAndRecoveryMetadata(t *testing.T) {
|
func TestCreateUsesDeterministicNameAndRecoveryMetadata(t *testing.T) {
|
||||||
api := &fakeAPI{}
|
api := &fakeAPI{}
|
||||||
metadata := taskworker.BackendMetadata(assignment())
|
metadata := taskworker.BackendMetadata(assignment())
|
||||||
executor, err := backend(api).Create(context.Background(), assignment(), metadata)
|
executor, err := backend(api).Create(context.Background(), assignment(), taskworker.LaunchSpec{
|
||||||
|
Metadata: metadata, Environment: map[string]string{"CI_RUNNER_CAPABILITY": "capability"},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -85,6 +87,9 @@ func TestCreateUsesDeterministicNameAndRecoveryMetadata(t *testing.T) {
|
|||||||
if api.created.Annotations["ci.ddupan.top/spiffe-id"] != assignment().Identity.SPIFFEID {
|
if api.created.Annotations["ci.ddupan.top/spiffe-id"] != assignment().Identity.SPIFFEID {
|
||||||
t.Fatalf("annotations = %#v", api.created.Annotations)
|
t.Fatalf("annotations = %#v", api.created.Annotations)
|
||||||
}
|
}
|
||||||
|
if api.created.Environment["CI_RUNNER_CAPABILITY"] != "capability" {
|
||||||
|
t.Fatalf("environment = %#v", api.created.Environment)
|
||||||
|
}
|
||||||
if len(api.created.Args) != 2 || api.created.Args[1] != "gitea-task-42" || executor.IdentityTarget != "pod-uid" {
|
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)
|
t.Fatalf("args=%v executor=%#v", api.created.Args, executor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ func (c *Client) ListPods(ctx context.Context, namespace, selector string) ([]Po
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) CreatePod(ctx context.Context, manifest PodManifest) (Pod, error) {
|
func (c *Client) CreatePod(ctx context.Context, manifest PodManifest) (Pod, error) {
|
||||||
|
environment := make([]corev1.EnvVar, 0, len(manifest.Environment))
|
||||||
|
for name, value := range manifest.Environment {
|
||||||
|
environment = append(environment, corev1.EnvVar{Name: name, Value: value})
|
||||||
|
}
|
||||||
document := &corev1.Pod{
|
document := &corev1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: manifest.Name, Namespace: manifest.Namespace,
|
Name: manifest.Name, Namespace: manifest.Namespace,
|
||||||
@@ -68,7 +72,7 @@ func (c *Client) CreatePod(ctx context.Context, manifest PodManifest) (Pod, erro
|
|||||||
ServiceAccountName: manifest.ServiceAccount,
|
ServiceAccountName: manifest.ServiceAccount,
|
||||||
RestartPolicy: corev1.RestartPolicyNever,
|
RestartPolicy: corev1.RestartPolicyNever,
|
||||||
Containers: []corev1.Container{{
|
Containers: []corev1.Container{{
|
||||||
Name: "executor", Image: manifest.Image, Args: manifest.Args,
|
Name: "executor", Image: manifest.Image, Args: manifest.Args, Env: environment,
|
||||||
SecurityContext: &corev1.SecurityContext{Privileged: boolPointer(true)},
|
SecurityContext: &corev1.SecurityContext{Privileged: boolPointer(true)},
|
||||||
VolumeMounts: []corev1.VolumeMount{{
|
VolumeMounts: []corev1.VolumeMount{{
|
||||||
Name: "spire-agent-socket", MountPath: "/run/spire/agent-sockets", ReadOnly: true,
|
Name: "spire-agent-socket", MountPath: "/run/spire/agent-sockets", ReadOnly: true,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func TestClientPodLifecycleUsesTypedClient(t *testing.T) {
|
|||||||
Name: "gitea-task-42", Namespace: "gitea-actions",
|
Name: "gitea-task-42", Namespace: "gitea-actions",
|
||||||
Labels: map[string]string{assignmentLabel: "gitea-task-42"},
|
Labels: map[string]string{assignmentLabel: "gitea-task-42"},
|
||||||
Image: "zot/ci-executor:main", ServiceAccount: "gitea-task-executor",
|
Image: "zot/ci-executor:main", ServiceAccount: "gitea-task-executor",
|
||||||
Args: []string{"execute", "gitea-task-42"},
|
Args: []string{"execute", "gitea-task-42"}, Environment: map[string]string{"CI_RUNNER_CAPABILITY": "capability"},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -34,6 +34,9 @@ func TestClientPodLifecycleUsesTypedClient(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if got := pod.Spec.Containers[0].Env; len(got) != 1 || got[0].Name != "CI_RUNNER_CAPABILITY" || got[0].Value != "capability" {
|
||||||
|
t.Fatalf("environment = %#v", got)
|
||||||
|
}
|
||||||
pod.UID = types.UID("pod-uid")
|
pod.UID = types.UID("pod-uid")
|
||||||
pod.Status.Phase = corev1.PodRunning
|
pod.Status.Phase = corev1.PodRunning
|
||||||
if _, err := client.Kubernetes.CoreV1().Pods("gitea-actions").Update(context.Background(), pod, metav1.UpdateOptions{}); err != nil {
|
if _, err := client.Kubernetes.CoreV1().Pods("gitea-actions").Update(context.Background(), pod, metav1.UpdateOptions{}); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// Package runnerbootstrap configures an unmodified, one-shot Gitea Runner to
|
||||||
|
// consume exactly the task assigned by the controller facade.
|
||||||
|
package runnerbootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/runnerfacade"
|
||||||
|
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskassignment"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnvAssignmentID = "CI_ASSIGNMENT_ID"
|
||||||
|
EnvCapability = "CI_RUNNER_CAPABILITY"
|
||||||
|
EnvFacadeURL = "CI_RUNNER_FACADE_URL"
|
||||||
|
EnvFacadeID = "CI_RUNNER_FACADE_SPIFFE_ID"
|
||||||
|
EnvSPIFFEID = "CI_SPIFFE_ID"
|
||||||
|
EnvBackend = "CI_RUNNER_BACKEND"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bootstrap emits assignment-scoped launch configuration. FacadeURL is the
|
||||||
|
// controller endpoint reached by the local SPIFFE proxy, not by Runner itself.
|
||||||
|
type Bootstrap struct {
|
||||||
|
Capabilities runnerfacade.Capabilities
|
||||||
|
FacadeURL string
|
||||||
|
FacadeSPIFFEID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Bootstrap) Environment(assignment taskassignment.Assignment) (map[string]string, error) {
|
||||||
|
if assignment.ID == "" || assignment.Identity.SPIFFEID == "" || b.FacadeSPIFFEID == "" {
|
||||||
|
return nil, errors.New("assignment ID and SPIFFE ID are required")
|
||||||
|
}
|
||||||
|
parsed, err := url.Parse(b.FacadeURL)
|
||||||
|
if err != nil || parsed.Scheme != "https" || parsed.Host == "" {
|
||||||
|
return nil, fmt.Errorf("runner facade URL must be an absolute https URL")
|
||||||
|
}
|
||||||
|
capability := b.Capabilities.Issue(assignment.ID)
|
||||||
|
if capability == "" {
|
||||||
|
return nil, errors.New("runner capability issuer is not configured")
|
||||||
|
}
|
||||||
|
return map[string]string{
|
||||||
|
EnvAssignmentID: assignment.ID,
|
||||||
|
EnvCapability: capability,
|
||||||
|
EnvFacadeURL: b.FacadeURL,
|
||||||
|
EnvFacadeID: b.FacadeSPIFFEID,
|
||||||
|
EnvSPIFFEID: assignment.Identity.SPIFFEID,
|
||||||
|
EnvBackend: string(assignment.Backend),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration mirrors Gitea Runner v3.5.0's registration file schema. ID is
|
||||||
|
// intentionally zero: the facade authenticates UUID and token and never uses
|
||||||
|
// the server-issued runner database ID.
|
||||||
|
type Registration struct {
|
||||||
|
Warning string `json:"WARNING"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Labels []string `json:"labels"`
|
||||||
|
Ephemeral bool `json:"ephemeral"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegistrationJSON(assignmentID, capability, localProxyURL string, backend taskassignment.Backend) ([]byte, error) {
|
||||||
|
if assignmentID == "" || capability == "" {
|
||||||
|
return nil, errors.New("assignment ID and runner capability are required")
|
||||||
|
}
|
||||||
|
parsed, err := url.Parse(localProxyURL)
|
||||||
|
if err != nil || parsed.Scheme != "http" || parsed.Host == "" {
|
||||||
|
return nil, errors.New("local runner proxy URL must be an absolute http URL")
|
||||||
|
}
|
||||||
|
if backend != taskassignment.BackendPod && backend != taskassignment.BackendVM {
|
||||||
|
return nil, fmt.Errorf("unsupported runner backend %q", backend)
|
||||||
|
}
|
||||||
|
registration := Registration{
|
||||||
|
Warning: "Generated for one preassigned task by gitea-dynamic-runner.",
|
||||||
|
UUID: assignmentID, Name: assignmentID, Token: capability,
|
||||||
|
Address: localProxyURL, Labels: []string{"self-hosted", string(backend)}, Ephemeral: true,
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(registration, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return append(data, '\n'), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package runnerbootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/runnerfacade"
|
||||||
|
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskassignment"
|
||||||
|
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskidentity"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testBootstrap(t *testing.T) Bootstrap {
|
||||||
|
t.Helper()
|
||||||
|
capabilities, err := runnerfacade.NewCapabilities([]byte("0123456789abcdef0123456789abcdef"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return Bootstrap{
|
||||||
|
Capabilities: capabilities,
|
||||||
|
FacadeURL: "https://runner-facade.gitea-actions.svc:8443",
|
||||||
|
FacadeSPIFFEID: "spiffe://ddupan.top/ns/gitea-actions/sa/gitea-dynamic-runner",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAssignment() taskassignment.Assignment {
|
||||||
|
return taskassignment.Assignment{
|
||||||
|
ID: "gitea-task-42", Backend: taskassignment.BackendPod,
|
||||||
|
Identity: taskidentity.Identity{SPIFFEID: "spiffe://ddupan.top/ci/owner/repo/publish"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvironmentIsDeterministicAndAssignmentScoped(t *testing.T) {
|
||||||
|
bootstrap := testBootstrap(t)
|
||||||
|
first, err := bootstrap.Environment(testAssignment())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
second, err := bootstrap.Environment(testAssignment())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if first[EnvCapability] == "" || first[EnvCapability] != second[EnvCapability] {
|
||||||
|
t.Fatalf("capabilities = %q, %q", first[EnvCapability], second[EnvCapability])
|
||||||
|
}
|
||||||
|
if first[EnvAssignmentID] != "gitea-task-42" || first[EnvSPIFFEID] != testAssignment().Identity.SPIFFEID {
|
||||||
|
t.Fatalf("environment = %#v", first)
|
||||||
|
}
|
||||||
|
if first[EnvBackend] != "pod" || first[EnvFacadeID] == "" {
|
||||||
|
t.Fatalf("environment = %#v", first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistrationMatchesOfficialRunnerSchema(t *testing.T) {
|
||||||
|
data, err := RegistrationJSON("gitea-task-42", "capability", "http://127.0.0.1:8080", taskassignment.BackendVM)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var registration Registration
|
||||||
|
if err := json.Unmarshal(data, ®istration); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if registration.UUID != "gitea-task-42" || registration.Token != "capability" || registration.Address != "http://127.0.0.1:8080" || !registration.Ephemeral {
|
||||||
|
t.Fatalf("registration = %#v", registration)
|
||||||
|
}
|
||||||
|
if len(registration.Labels) != 2 || registration.Labels[0] != "self-hosted" || registration.Labels[1] != "vm" {
|
||||||
|
t.Fatalf("labels = %#v", registration.Labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistrationRejectsNonLocalTLSAddress(t *testing.T) {
|
||||||
|
if _, err := RegistrationJSON("id", "capability", "https://facade.example", taskassignment.BackendPod); err == nil {
|
||||||
|
t.Fatal("expected local proxy URL validation error")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package runnerbootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/gitea-dynamic-runner/internal/taskassignment"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExecutorConfig struct {
|
||||||
|
AssignmentID string
|
||||||
|
Capability string
|
||||||
|
Backend taskassignment.Backend
|
||||||
|
FacadeURL string
|
||||||
|
FacadeSPIFFEID string
|
||||||
|
WorkloadAPIAddr string
|
||||||
|
RunnerBinary string
|
||||||
|
ListenAddress string
|
||||||
|
WorkDir string
|
||||||
|
Stdout *os.File
|
||||||
|
Stderr *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunExecutor runs the SPIFFE proxy and one unmodified official Runner process.
|
||||||
|
// The generated registration file exists only in the executor's temporary
|
||||||
|
// work directory and the runner exits after its preassigned task.
|
||||||
|
func RunExecutor(ctx context.Context, config ExecutorConfig) error {
|
||||||
|
if config.RunnerBinary == "" {
|
||||||
|
config.RunnerBinary = "gitea-runner"
|
||||||
|
}
|
||||||
|
if config.ListenAddress == "" {
|
||||||
|
config.ListenAddress = "127.0.0.1:0"
|
||||||
|
}
|
||||||
|
listener, err := net.Listen("tcp", config.ListenAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listen for local runner proxy: %w", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
address, ok := listener.Addr().(*net.TCPAddr)
|
||||||
|
if !ok || !address.IP.IsLoopback() {
|
||||||
|
return errors.New("runner proxy must listen on a loopback address")
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy, err := NewProxy(ctx, config.FacadeURL, config.FacadeSPIFFEID, config.WorkloadAPIAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer proxy.Close()
|
||||||
|
|
||||||
|
workDir := config.WorkDir
|
||||||
|
removeWorkDir := false
|
||||||
|
if workDir == "" {
|
||||||
|
workDir, err = os.MkdirTemp("", "gitea-task-runner-")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create runner work directory: %w", err)
|
||||||
|
}
|
||||||
|
removeWorkDir = true
|
||||||
|
}
|
||||||
|
if removeWorkDir {
|
||||||
|
defer os.RemoveAll(workDir)
|
||||||
|
}
|
||||||
|
registration, err := RegistrationJSON(
|
||||||
|
config.AssignmentID, config.Capability, "http://"+listener.Addr().String(), config.Backend,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(workDir, ".runner"), registration, 0o600); err != nil {
|
||||||
|
return fmt.Errorf("write one-shot runner registration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &http.Server{Handler: proxy.Handler, ReadHeaderTimeout: 10 * time.Second}
|
||||||
|
serverErrors := make(chan error, 1)
|
||||||
|
go func() { serverErrors <- server.Serve(listener) }()
|
||||||
|
|
||||||
|
command := exec.CommandContext(ctx, config.RunnerBinary, "daemon", "--once")
|
||||||
|
command.Dir = workDir
|
||||||
|
command.Stdout = config.Stdout
|
||||||
|
command.Stderr = config.Stderr
|
||||||
|
runnerErr := command.Run()
|
||||||
|
shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
shutdownErr := server.Shutdown(shutdownContext)
|
||||||
|
cancel()
|
||||||
|
serverErr := <-serverErrors
|
||||||
|
if errors.Is(serverErr, http.ErrServerClosed) {
|
||||||
|
serverErr = nil
|
||||||
|
}
|
||||||
|
return errors.Join(runnerErr, shutdownErr, serverErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutorConfigFromEnvironment reads the non-secret image configuration and
|
||||||
|
// the assignment-scoped values injected by the backend. The Workload API
|
||||||
|
// address follows SPIFFE_ENDPOINT_SOCKET through go-spiffe when not set here.
|
||||||
|
func ExecutorConfigFromEnvironment() (ExecutorConfig, error) {
|
||||||
|
backend := taskassignment.Backend(os.Getenv(EnvBackend))
|
||||||
|
if backend != taskassignment.BackendPod && backend != taskassignment.BackendVM {
|
||||||
|
return ExecutorConfig{}, fmt.Errorf("invalid %s %q", EnvBackend, backend)
|
||||||
|
}
|
||||||
|
config := ExecutorConfig{
|
||||||
|
AssignmentID: os.Getenv(EnvAssignmentID), Capability: os.Getenv(EnvCapability),
|
||||||
|
Backend: backend, FacadeURL: os.Getenv(EnvFacadeURL), FacadeSPIFFEID: os.Getenv(EnvFacadeID),
|
||||||
|
RunnerBinary: os.Getenv("GITEA_RUNNER_BINARY"), ListenAddress: "127.0.0.1:0",
|
||||||
|
Stdout: os.Stdout, Stderr: os.Stderr,
|
||||||
|
}
|
||||||
|
if config.AssignmentID == "" || config.Capability == "" || config.FacadeURL == "" || config.FacadeSPIFFEID == "" {
|
||||||
|
return ExecutorConfig{}, errors.New("complete runner assignment and facade environment is required")
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package runnerbootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/spiffe/go-spiffe/v2/spiffeid"
|
||||||
|
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
|
||||||
|
"github.com/spiffe/go-spiffe/v2/workloadapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Proxy struct {
|
||||||
|
Handler http.Handler
|
||||||
|
source *workloadapi.X509Source
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProxy obtains rotating X509-SVIDs from the Workload API and authorizes
|
||||||
|
// one exact controller identity. The official Runner talks plain HTTP only to
|
||||||
|
// this executor-local handler.
|
||||||
|
func NewProxy(ctx context.Context, facadeURL, facadeSPIFFEID, workloadAPIAddr string) (*Proxy, error) {
|
||||||
|
target, err := url.Parse(facadeURL)
|
||||||
|
if err != nil || target.Scheme != "https" || target.Host == "" {
|
||||||
|
return nil, fmt.Errorf("runner facade URL must be an absolute https URL")
|
||||||
|
}
|
||||||
|
serverID, err := spiffeid.FromString(facadeSPIFFEID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse runner facade SPIFFE ID: %w", err)
|
||||||
|
}
|
||||||
|
options := []workloadapi.X509SourceOption{}
|
||||||
|
if workloadAPIAddr != "" {
|
||||||
|
options = append(options, workloadapi.WithClientOptions(workloadapi.WithAddr(workloadAPIAddr)))
|
||||||
|
}
|
||||||
|
source, err := workloadapi.NewX509Source(ctx, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open SPIFFE Workload API X509 source: %w", err)
|
||||||
|
}
|
||||||
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
transport.TLSClientConfig = tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeID(serverID))
|
||||||
|
return &Proxy{Handler: NewProxyHandler(target, transport), source: source}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyHandler(target *url.URL, transport http.RoundTripper) http.Handler {
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||||
|
proxy.Transport = transport
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) Close() error {
|
||||||
|
if p == nil || p.source == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.source.Close()
|
||||||
|
}
|
||||||
@@ -34,11 +34,26 @@ type Metadata struct {
|
|||||||
Annotations map[string]string
|
Annotations map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LaunchSpec contains the durable resource metadata and the short-lived
|
||||||
|
// executor environment. Environment values configure the one-shot runner but
|
||||||
|
// are deliberately excluded from labels and annotations.
|
||||||
|
type LaunchSpec struct {
|
||||||
|
Metadata Metadata
|
||||||
|
Environment map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap produces assignment-scoped executor configuration. Implementations
|
||||||
|
// must be deterministic so a redelivery after controller restart creates the
|
||||||
|
// same credentials without storing another lifecycle record.
|
||||||
|
type Bootstrap interface {
|
||||||
|
Environment(taskassignment.Assignment) (map[string]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Backend is implemented by the native Pod and OpenSandbox adapters.
|
// Backend is implemented by the native Pod and OpenSandbox adapters.
|
||||||
// Every method must be idempotent.
|
// Every method must be idempotent.
|
||||||
type Backend interface {
|
type Backend interface {
|
||||||
Find(context.Context, string) (*Executor, error)
|
Find(context.Context, string) (*Executor, error)
|
||||||
Create(context.Context, taskassignment.Assignment, Metadata) (*Executor, error)
|
Create(context.Context, taskassignment.Assignment, LaunchSpec) (*Executor, error)
|
||||||
BindIdentity(context.Context, *Executor, taskidentity.Identity) error
|
BindIdentity(context.Context, *Executor, taskidentity.Identity) error
|
||||||
Delete(context.Context, *Executor) error
|
Delete(context.Context, *Executor) error
|
||||||
}
|
}
|
||||||
@@ -53,16 +68,17 @@ type TaskState interface {
|
|||||||
// Worker has no correctness-critical in-memory state. Handle may be called
|
// Worker has no correctness-critical in-memory state. Handle may be called
|
||||||
// again for the same assignment after any operation.
|
// again for the same assignment after any operation.
|
||||||
type Worker struct {
|
type Worker struct {
|
||||||
Backend Backend
|
Backend Backend
|
||||||
Tasks TaskState
|
Tasks TaskState
|
||||||
|
Bootstrap Bootstrap
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept completes the durable handoff from JetStream to the backend. Once it
|
// Accept completes the durable handoff from JetStream to the backend. Once it
|
||||||
// returns true, all recovery information exists in Kubernetes/OpenSandbox and
|
// returns true, all recovery information exists in Kubernetes/OpenSandbox and
|
||||||
// the assignment message can be acknowledged immediately.
|
// the assignment message can be acknowledged immediately.
|
||||||
func (w Worker) Accept(ctx context.Context, assignment taskassignment.Assignment) (bool, error) {
|
func (w Worker) Accept(ctx context.Context, assignment taskassignment.Assignment) (bool, error) {
|
||||||
if w.Backend == nil || w.Tasks == nil {
|
if w.Backend == nil || w.Tasks == nil || w.Bootstrap == nil {
|
||||||
return false, errors.New("backend and Gitea task state are required")
|
return false, errors.New("backend, Gitea task state, and runner bootstrap are required")
|
||||||
}
|
}
|
||||||
if assignment.ID == "" || assignment.Task == nil {
|
if assignment.ID == "" || assignment.Task == nil {
|
||||||
return false, errors.New("valid assignment is required")
|
return false, errors.New("valid assignment is required")
|
||||||
@@ -84,7 +100,11 @@ func (w Worker) Accept(ctx context.Context, assignment taskassignment.Assignment
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
if executor == nil {
|
if executor == nil {
|
||||||
executor, err = w.Backend.Create(ctx, assignment, BackendMetadata(assignment))
|
launch, launchErr := w.launchSpec(assignment)
|
||||||
|
if launchErr != nil {
|
||||||
|
return false, launchErr
|
||||||
|
}
|
||||||
|
executor, err = w.Backend.Create(ctx, assignment, launch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -101,8 +121,8 @@ func (w Worker) Accept(ctx context.Context, assignment taskassignment.Assignment
|
|||||||
// Handle performs one reconciliation. Done means the queue message may be
|
// Handle performs one reconciliation. Done means the queue message may be
|
||||||
// acknowledged. A false result should remain pending and be reconciled again.
|
// acknowledged. A false result should remain pending and be reconciled again.
|
||||||
func (w Worker) Handle(ctx context.Context, assignment taskassignment.Assignment) (done bool, err error) {
|
func (w Worker) Handle(ctx context.Context, assignment taskassignment.Assignment) (done bool, err error) {
|
||||||
if w.Backend == nil || w.Tasks == nil {
|
if w.Backend == nil || w.Tasks == nil || w.Bootstrap == nil {
|
||||||
return false, errors.New("backend and Gitea task state are required")
|
return false, errors.New("backend, Gitea task state, and runner bootstrap are required")
|
||||||
}
|
}
|
||||||
if assignment.ID == "" || assignment.Task == nil {
|
if assignment.ID == "" || assignment.Task == nil {
|
||||||
return false, errors.New("valid assignment is required")
|
return false, errors.New("valid assignment is required")
|
||||||
@@ -126,7 +146,11 @@ func (w Worker) Handle(ctx context.Context, assignment taskassignment.Assignment
|
|||||||
}
|
}
|
||||||
|
|
||||||
if executor == nil {
|
if executor == nil {
|
||||||
executor, err = w.Backend.Create(ctx, assignment, BackendMetadata(assignment))
|
launch, launchErr := w.launchSpec(assignment)
|
||||||
|
if launchErr != nil {
|
||||||
|
return false, launchErr
|
||||||
|
}
|
||||||
|
executor, err = w.Backend.Create(ctx, assignment, launch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -154,6 +178,14 @@ func (w Worker) Handle(ctx context.Context, assignment taskassignment.Assignment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w Worker) launchSpec(assignment taskassignment.Assignment) (LaunchSpec, error) {
|
||||||
|
environment, err := w.Bootstrap.Environment(assignment)
|
||||||
|
if err != nil {
|
||||||
|
return LaunchSpec{}, err
|
||||||
|
}
|
||||||
|
return LaunchSpec{Metadata: BackendMetadata(assignment), Environment: environment}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// BackendMetadata is the shared metadata contract for Pods and OpenSandbox.
|
// BackendMetadata is the shared metadata contract for Pods and OpenSandbox.
|
||||||
func BackendMetadata(assignment taskassignment.Assignment) Metadata {
|
func BackendMetadata(assignment taskassignment.Assignment) Metadata {
|
||||||
return Metadata{
|
return Metadata{
|
||||||
|
|||||||
@@ -17,8 +17,14 @@ type fakeBackend struct {
|
|||||||
deleted int
|
deleted int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeBootstrap struct{}
|
||||||
|
|
||||||
|
func (fakeBootstrap) Environment(taskassignment.Assignment) (map[string]string, error) {
|
||||||
|
return map[string]string{"CI_RUNNER_CAPABILITY": "capability"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *fakeBackend) Find(context.Context, string) (*Executor, error) { return b.executor, nil }
|
func (b *fakeBackend) Find(context.Context, string) (*Executor, error) { return b.executor, nil }
|
||||||
func (b *fakeBackend) Create(_ context.Context, _ taskassignment.Assignment, _ Metadata) (*Executor, error) {
|
func (b *fakeBackend) Create(_ context.Context, _ taskassignment.Assignment, _ LaunchSpec) (*Executor, error) {
|
||||||
b.created++
|
b.created++
|
||||||
b.executor = &Executor{Name: "executor", IdentityTarget: "pod-uid", Phase: PhaseRunning}
|
b.executor = &Executor{Name: "executor", IdentityTarget: "pod-uid", Phase: PhaseRunning}
|
||||||
return b.executor, nil
|
return b.executor, nil
|
||||||
@@ -60,7 +66,7 @@ func assignment() taskassignment.Assignment {
|
|||||||
|
|
||||||
func TestHandleRecoversExistingExecutorWithoutCreatingAnother(t *testing.T) {
|
func TestHandleRecoversExistingExecutorWithoutCreatingAnother(t *testing.T) {
|
||||||
backend := &fakeBackend{executor: &Executor{Name: "existing", IdentityTarget: "uid", Phase: PhaseRunning}}
|
backend := &fakeBackend{executor: &Executor{Name: "existing", IdentityTarget: "uid", Phase: PhaseRunning}}
|
||||||
worker := Worker{Backend: backend, Tasks: &fakeTasks{}}
|
worker := Worker{Backend: backend, Tasks: &fakeTasks{}, Bootstrap: fakeBootstrap{}}
|
||||||
|
|
||||||
done, err := worker.Handle(context.Background(), assignment())
|
done, err := worker.Handle(context.Background(), assignment())
|
||||||
if err != nil || done {
|
if err != nil || done {
|
||||||
@@ -73,7 +79,7 @@ func TestHandleRecoversExistingExecutorWithoutCreatingAnother(t *testing.T) {
|
|||||||
|
|
||||||
func TestAcceptAcknowledgesAfterBackendAndIdentityAreDurable(t *testing.T) {
|
func TestAcceptAcknowledgesAfterBackendAndIdentityAreDurable(t *testing.T) {
|
||||||
backend := &fakeBackend{}
|
backend := &fakeBackend{}
|
||||||
worker := Worker{Backend: backend, Tasks: &fakeTasks{}}
|
worker := Worker{Backend: backend, Tasks: &fakeTasks{}, Bootstrap: fakeBootstrap{}}
|
||||||
|
|
||||||
accepted, err := worker.Accept(context.Background(), assignment())
|
accepted, err := worker.Accept(context.Background(), assignment())
|
||||||
if err != nil || !accepted {
|
if err != nil || !accepted {
|
||||||
@@ -86,7 +92,7 @@ func TestAcceptAcknowledgesAfterBackendAndIdentityAreDurable(t *testing.T) {
|
|||||||
|
|
||||||
func TestAcceptRetriesWhileBackendIdentityTargetIsUnavailable(t *testing.T) {
|
func TestAcceptRetriesWhileBackendIdentityTargetIsUnavailable(t *testing.T) {
|
||||||
backend := &fakeBackend{executor: &Executor{Name: "pending", Phase: PhasePending}}
|
backend := &fakeBackend{executor: &Executor{Name: "pending", Phase: PhasePending}}
|
||||||
worker := Worker{Backend: backend, Tasks: &fakeTasks{}}
|
worker := Worker{Backend: backend, Tasks: &fakeTasks{}, Bootstrap: fakeBootstrap{}}
|
||||||
|
|
||||||
accepted, err := worker.Accept(context.Background(), assignment())
|
accepted, err := worker.Accept(context.Background(), assignment())
|
||||||
if err != nil || accepted {
|
if err != nil || accepted {
|
||||||
@@ -100,7 +106,7 @@ func TestAcceptRetriesWhileBackendIdentityTargetIsUnavailable(t *testing.T) {
|
|||||||
func TestHandleReportsBeforeCleanupAndBecomesRecoverable(t *testing.T) {
|
func TestHandleReportsBeforeCleanupAndBecomesRecoverable(t *testing.T) {
|
||||||
backend := &fakeBackend{executor: &Executor{Name: "finished", IdentityTarget: "uid", Phase: PhaseSucceeded}}
|
backend := &fakeBackend{executor: &Executor{Name: "finished", IdentityTarget: "uid", Phase: PhaseSucceeded}}
|
||||||
tasks := &fakeTasks{}
|
tasks := &fakeTasks{}
|
||||||
worker := Worker{Backend: backend, Tasks: tasks}
|
worker := Worker{Backend: backend, Tasks: tasks, Bootstrap: fakeBootstrap{}}
|
||||||
|
|
||||||
done, err := worker.Handle(context.Background(), assignment())
|
done, err := worker.Handle(context.Background(), assignment())
|
||||||
if err != nil || !done {
|
if err != nil || !done {
|
||||||
|
|||||||
Reference in New Issue
Block a user