接入 OpenSandbox 动态 Runner
ansible / collection-test (pull_request) Successful in 2m58s
ansible / lint (pull_request) Successful in 4m33s
yaml / yaml (pull_request) Successful in 29s

This commit is contained in:
2026-09-18 18:33:00 +00:00
parent 518dba6036
commit 6eb4875db6
18 changed files with 626 additions and 84 deletions
+60
View File
@@ -0,0 +1,60 @@
# OpenSandbox Gitea runner Pools
本目录部署 `ci-vm` 和 `ci-pod` 两个零预热 Pool、sandbox 内的 SPIFFE identity
controller,以及仅供内网 VyOS 转发的 OpenSandbox NodePort。`ci-vm` 使用
`kata-clh-runtime-rs`,`ci-pod` 使用默认 runtime;两者均为单任务、用后删除。
Pool 中 task-executor 接收 Lifecycle API 下发的进程环境。guest-local SPIRE Agent 用
Pod-bound PSAT 向中央 SPIRE 注册;identity controller 从 BatchSandbox allocation
取得真实 Pod UID,再创建精确的 `ClusterStaticEntry`。runner 只有拿到请求中的完整
repository/task SVID 后才领取一次性 Gitea registration token。
## 部署依赖
- OpenSandbox chart 和 CRD 已 Ready;
- RuntimeClass `kata-clh-runtime-rs` 已存在;
- 中央 SPIRE 已发布 `ConfigMap/opensandbox/spire-bundle-pem`;
- VyOS `10.60.0.13:8080` 转发 sandbox1/2 的 NodePort `30080`;
- runner/controller 镜像均使用 Zot digest,而不是可变 tag。
本目录不读取 OpenBao,也不修改 OpenSandbox 平台侧 ExternalSecret、ClusterSecretStore
或 Bao policy。OpenSandbox API key 只存在于平台 server Secret 和 homelab controller
Secret,两边由各自身份读取同一 Bao 资源。
## 上线验收
先确认 Secret 和 API 认证,命令不得输出 key:
```bash
kubectl -n dynamic-runner wait externalsecret/dynamic-runner --for=condition=Ready --timeout=2m
kubectl -n dynamic-runner exec deploy/dynamic-runner-controller -- \
wget -qO- http://10.60.0.13:8080/health
```
然后触发一个 `runs-on: [self-hosted, vm]` 的最小 workflow,并同时观察:
```bash
kubectl -n dynamic-runner logs deploy/dynamic-runner-controller -f
kubectl -n opensandbox get pool,batchsandbox,pod -w
kubectl get clusterstaticentry -l app.kubernetes.io/component=opensandbox-identity -w
kubectl -n opensandbox logs deploy/opensandbox-identity -f
```
合格证据必须同时包含:
1. Lifecycle create 成功并选择 `ci-vm`;
2. 分配 Pod 的 `runtimeClassName` 为 `kata-clh-runtime-rs`;
3. entry 的 parent ID 包含该 Pod 的 UID,SPIFFE ID 使用 repository/task;
4. Gitea 显示临时 runner 带 `self-hosted,vm` labels 并完成真实任务;
5. 任务后 BatchSandbox、Pod、ClusterStaticEntry 和临时 runner 均消失。
清理超时对象时只调用 Lifecycle DELETE,不直接删除 Pool Pod。若 controller 已不可用,
可从 OpenSandbox API 按 metadata 定位 sandbox 后执行 DELETE;不要绕过 API 伪造状态。
## 故障定位
- `401`:检查 homelab ExternalSecret Ready 和文件挂载,不打印 Secret;
- `PoolCapacityExhausted`:检查 `ci-vm` 的 `poolMax` 及残留 BatchSandbox;
- runner 等待 SVID:核对 allocation Pod UID、ClusterStaticEntry parentID、guest Agent 日志;
- runner 等待 token:核对 `192.168.10.127:8787` 的 sandbox 到 homelab 路由;
- Docker 任务失败:检查 `docker` sidecar 和 `/run/docker/docker.sock` 的 group 2000。
@@ -0,0 +1,31 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: gitea-ci-spire-agent
namespace: opensandbox
data:
agent.conf: |
agent {
data_dir = "/run/spire/data"
log_level = "INFO"
server_address = "spire-server.ad.ddupan.top"
server_port = "8081"
socket_path = "/run/spire/agent-sockets/spire-agent.sock"
trust_bundle_path = "/run/spire/bundle/bundle.pem"
trust_domain = "ddupan.top"
}
plugins {
NodeAttestor "k8s_psat" {
plugin_data {
cluster = "sandbox-kata"
token_path = "/run/spire/tokens/token"
}
}
KeyManager "memory" {
plugin_data {}
}
WorkloadAttestor "unix" {
plugin_data {}
}
}
@@ -0,0 +1,41 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: opensandbox-identity
namespace: opensandbox
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: opensandbox-identity
template:
metadata:
labels:
app.kubernetes.io/name: opensandbox-identity
spec:
serviceAccountName: opensandbox-identity
containers:
- name: controller
image: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-controller@sha256:1e481726cbcf6da50f1ef61aa5a1b070d2859209329842e954463293c27c0e66
imagePullPolicy: IfNotPresent
command: [/venv/bin/gitea-dynamic-runner-opensandbox-identity]
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 96Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
securityContext:
seccompProfile:
type: RuntimeDefault
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- service.yaml
- agent-config.yaml
- rbac.yaml
- identity-controller.yaml
- pools.yaml
+295
View File
@@ -0,0 +1,295 @@
---
apiVersion: sandbox.opensandbox.io/v1alpha1
kind: Pool
metadata:
name: ci-vm
namespace: opensandbox
labels:
app.kubernetes.io/name: gitea-ci-vm
app.kubernetes.io/component: runner-pool
spec:
capacitySpec:
bufferMax: 0
bufferMin: 0
poolMax: 2
poolMin: 0
recycleStrategy:
type: Delete
template:
metadata:
labels:
app.kubernetes.io/name: gitea-ci-vm
ci.ddupan.top/backend: vm
spec:
runtimeClassName: kata-clh-runtime-rs
serviceAccountName: gitea-ci
restartPolicy: Never
terminationGracePeriodSeconds: 30
shareProcessNamespace: true
securityContext:
fsGroup: 2000
fsGroupChangePolicy: OnRootMismatch
initContainers:
- name: task-executor-installer
image: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/task-executor:v0.1.0
command: [/bin/sh, -c]
args:
- cp /workspace/server /opt/opensandbox/task-executor && chmod 0755 /opt/opensandbox/task-executor
volumeMounts:
- name: opensandbox-bin
mountPath: /opt/opensandbox
- name: execd-installer
image: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:v1.0.22
command: [/bin/sh, -c]
args:
- cp ./execd /opt/opensandbox/execd && cp ./bootstrap.sh /opt/opensandbox/bootstrap.sh && chmod 0755 /opt/opensandbox/execd /opt/opensandbox/bootstrap.sh
volumeMounts:
- name: opensandbox-bin
mountPath: /opt/opensandbox
containers:
- name: sandbox
image: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-runner@sha256:a45875fd2d0e67429b0b7bc3914735581669c705bb4e1a05d712134d2bceb86f
imagePullPolicy: IfNotPresent
command: [/opt/opensandbox/task-executor]
args: [-listen-addr=0.0.0.0:5758, -log-dir=/tmp]
env:
- name: SANDBOX_MAIN_CONTAINER
value: sandbox
- name: EXECD_ENVS
value: /opt/opensandbox/.env
- name: EXECD
value: /opt/opensandbox/execd
- name: GITEA_INSTANCE_URL
value: https://git.ddupan.top
- name: HOME
value: /data
- name: DOCKER_HOST
value: unix:///run/docker/docker.sock
ports:
- name: task-executor
containerPort: 5758
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
runAsNonRoot: true
runAsUser: 2000
runAsGroup: 2000
volumeMounts:
- name: opensandbox-bin
mountPath: /opt/opensandbox
- name: runner-data
mountPath: /data
- name: docker-socket
mountPath: /run/docker
- name: spire-socket
mountPath: /run/spire/agent-sockets
- name: spire-agent
image: ghcr.io/spiffe/spire-agent:1.15.3@sha256:41b0dcd8b258a69db9e2768292a060766fb76fd866e4bc925849981ea1b825ff
args: [-config, /run/spire/config/agent.conf]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
volumeMounts:
- name: spire-config
mountPath: /run/spire/config
readOnly: true
- name: spire-bundle
mountPath: /run/spire/bundle
readOnly: true
- name: spire-token
mountPath: /run/spire/tokens
readOnly: true
- name: spire-data
mountPath: /run/spire/data
- name: spire-socket
mountPath: /run/spire/agent-sockets
- name: docker
image: docker.io/library/docker:29.1.5-dind
command: [/bin/sh, -c]
args:
- test -e /dev/kmsg || mknod /dev/kmsg c 1 11; exec dockerd --host=unix:///run/docker/docker.sock --group=2000 --storage-driver=overlay2
securityContext:
privileged: true
volumeMounts:
- name: docker-socket
mountPath: /run/docker
- name: docker-data
mountPath: /var/lib/docker
volumes:
- name: opensandbox-bin
emptyDir: {}
- name: runner-data
emptyDir: {}
- name: docker-data
emptyDir: {}
- name: docker-socket
emptyDir:
medium: Memory
- name: spire-data
emptyDir:
medium: Memory
- name: spire-socket
emptyDir:
medium: Memory
- name: spire-config
configMap:
name: gitea-ci-spire-agent
- name: spire-bundle
configMap:
name: spire-bundle-pem
- name: spire-token
projected:
sources:
- serviceAccountToken:
audience: spire-server
expirationSeconds: 3600
path: token
---
apiVersion: sandbox.opensandbox.io/v1alpha1
kind: Pool
metadata:
name: ci-pod
namespace: opensandbox
labels:
app.kubernetes.io/name: gitea-ci-pod
app.kubernetes.io/component: runner-pool
spec:
capacitySpec:
bufferMax: 0
bufferMin: 0
poolMax: 4
poolMin: 0
recycleStrategy:
type: Delete
template:
metadata:
labels:
app.kubernetes.io/name: gitea-ci-pod
ci.ddupan.top/backend: pod
spec:
serviceAccountName: gitea-ci
restartPolicy: Never
terminationGracePeriodSeconds: 30
shareProcessNamespace: true
securityContext:
fsGroup: 2000
fsGroupChangePolicy: OnRootMismatch
initContainers:
- name: task-executor-installer
image: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/task-executor:v0.1.0
command: [/bin/sh, -c]
args:
- cp /workspace/server /opt/opensandbox/task-executor && chmod 0755 /opt/opensandbox/task-executor
volumeMounts:
- name: opensandbox-bin
mountPath: /opt/opensandbox
- name: execd-installer
image: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:v1.0.22
command: [/bin/sh, -c]
args:
- cp ./execd /opt/opensandbox/execd && cp ./bootstrap.sh /opt/opensandbox/bootstrap.sh && chmod 0755 /opt/opensandbox/execd /opt/opensandbox/bootstrap.sh
volumeMounts:
- name: opensandbox-bin
mountPath: /opt/opensandbox
containers:
- name: sandbox
image: zot.ad.ddupan.top/panxiao81/gitea-dynamic-runner-runner@sha256:a45875fd2d0e67429b0b7bc3914735581669c705bb4e1a05d712134d2bceb86f
imagePullPolicy: IfNotPresent
command: [/opt/opensandbox/task-executor]
args: [-listen-addr=0.0.0.0:5758, -log-dir=/tmp]
env:
- name: SANDBOX_MAIN_CONTAINER
value: sandbox
- name: EXECD_ENVS
value: /opt/opensandbox/.env
- name: EXECD
value: /opt/opensandbox/execd
- name: GITEA_INSTANCE_URL
value: https://git.ddupan.top
- name: HOME
value: /data
- name: DOCKER_HOST
value: unix:///run/docker/docker.sock
ports:
- name: task-executor
containerPort: 5758
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
runAsNonRoot: true
runAsUser: 2000
runAsGroup: 2000
volumeMounts:
- name: opensandbox-bin
mountPath: /opt/opensandbox
- name: runner-data
mountPath: /data
- name: docker-socket
mountPath: /run/docker
- name: spire-socket
mountPath: /run/spire/agent-sockets
- name: spire-agent
image: ghcr.io/spiffe/spire-agent:1.15.3@sha256:41b0dcd8b258a69db9e2768292a060766fb76fd866e4bc925849981ea1b825ff
args: [-config, /run/spire/config/agent.conf]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
volumeMounts:
- name: spire-config
mountPath: /run/spire/config
readOnly: true
- name: spire-bundle
mountPath: /run/spire/bundle
readOnly: true
- name: spire-token
mountPath: /run/spire/tokens
readOnly: true
- name: spire-data
mountPath: /run/spire/data
- name: spire-socket
mountPath: /run/spire/agent-sockets
- name: docker
image: docker.io/library/docker:29.1.5-dind
command: [/bin/sh, -c]
args:
- test -e /dev/kmsg || mknod /dev/kmsg c 1 11; exec dockerd --host=unix:///run/docker/docker.sock --group=2000 --storage-driver=overlay2
securityContext:
privileged: true
volumeMounts:
- name: docker-socket
mountPath: /run/docker
- name: docker-data
mountPath: /var/lib/docker
volumes:
- name: opensandbox-bin
emptyDir: {}
- name: runner-data
emptyDir: {}
- name: docker-data
emptyDir: {}
- name: docker-socket
emptyDir:
medium: Memory
- name: spire-data
emptyDir:
medium: Memory
- name: spire-socket
emptyDir:
medium: Memory
- name: spire-config
configMap:
name: gitea-ci-spire-agent
- name: spire-bundle
configMap:
name: spire-bundle-pem
- name: spire-token
projected:
sources:
- serviceAccountToken:
audience: spire-server
expirationSeconds: 3600
path: token
+87
View File
@@ -0,0 +1,87 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitea-ci
namespace: opensandbox
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: opensandbox-identity
namespace: opensandbox
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: opensandbox-identity
namespace: opensandbox
rules:
- apiGroups: [""]
resources: [pods]
verbs: [get, list, watch]
- apiGroups: [sandbox.opensandbox.io]
resources: [batchsandboxes]
verbs: [get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: opensandbox-identity
namespace: opensandbox
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: opensandbox-identity
subjects:
- kind: ServiceAccount
name: opensandbox-identity
namespace: opensandbox
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: opensandbox-identity
rules:
- apiGroups: [spire.spiffe.io]
resources: [clusterstaticentries]
verbs: [create, delete, get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: opensandbox-identity
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: opensandbox-identity
subjects:
- kind: ServiceAccount
name: opensandbox-identity
namespace: opensandbox
---
# The central SPIRE external controller uses this existing sandbox credential
# to publish a PEM bundle for guest-local Agents. It cannot read Secrets here.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: spire-runner-bundle-publisher
namespace: opensandbox
rules:
- apiGroups: [""]
resources: [configmaps]
verbs: [create, delete, get, list, patch, update, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: spire-runner-bundle-publisher
namespace: opensandbox
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: spire-runner-bundle-publisher
subjects:
- kind: ServiceAccount
name: spire-controller-manager
namespace: spire-system
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: opensandbox-server-internal
namespace: opensandbox-system
spec:
type: NodePort
selector:
app.kubernetes.io/instance: opensandbox
app.kubernetes.io/name: opensandbox-server
ports:
- name: http
port: 80
targetPort: http
nodePort: 30080