Phase-6: enable Gitea Actions, in-cluster act_runner, static CI gate
STEP 1 — enable Gitea Actions: pin GITEA__actions__ENABLED=true and DEFAULT_ACTIONS_URL=github in the gitea bootstrap patch (was implicit upstream default; make it a declared property). STEP 2 — deploy + register an act_runner in a dedicated non-app namespace (gitea-actions) so the privileged DinD executor sidecar is admitted without a Kyverno policy exception (restrict-host-namespaces scopes to app namespaces only). Docker executor + DinD sidecar over a shared TLS-cert emptyDir; runner state on a Longhorn PVC (registers once). Registration token delivered as a SOPS-encrypted Secret decrypted by Flux. Runner mounts no ServiceAccount token (R-SEC-02). STEP 3 — author .gitea/workflows/gate.yml: the live enforced merge gate running the STATIC layers (spec-immutability best-effort, lint, Kyverno L0, L1 render) via `make -C ci lint l0 l1` with the pinned toolchain installed at job start. L2/L3 conformance is an if:false placeholder (needs a toolchain image + scoped cluster creds — a design decision, not wired). merge-gate.yaml reduced to workflow_dispatch (dormant full-pipeline blueprint). install-tools.sh gains check-jsonschema so the schema stage validates in CI instead of skipping. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
81ef78295a
commit
bc4519bc4c
7 files changed
+327
-10
No files matched your search
@@ -0,0 +1,169 @@
|
||||
---
|
||||
# Gitea Actions runner (act_runner) — Phase 6 CI/CD (WS-GITOSS).
|
||||
#
|
||||
# WHY A DEDICATED, NON-APP NAMESPACE:
|
||||
# The runner's job executor is a Docker-in-Docker (DinD) sidecar that MUST run
|
||||
# `privileged: true` (dockerd needs it). The Kyverno `restrict-host-namespaces`
|
||||
# policy denies privileged containers, but it (and the egress-class / auth-mode /
|
||||
# default-deny-baseline policies) scope ONLY to namespaces labeled
|
||||
# homelab.keithjackson.dev/app-namespace: "true". This namespace deliberately does
|
||||
# NOT carry that label — it is CI plumbing, not a user-facing app — so the privileged
|
||||
# DinD sidecar is admitted WITHOUT a policy exception. (Verified: a privileged Pod in
|
||||
# a non-app namespace passes server-side admission; Kyverno test fixture
|
||||
# `platform-workload` is likewise Excluded from restrict-host-namespaces.)
|
||||
#
|
||||
# EXECUTOR MODEL — Docker executor + DinD sidecar (privileged):
|
||||
# act_runner runs each job step inside a container it launches via a Docker daemon.
|
||||
# That daemon is the co-located `dind` sidecar (shared pod netns → tcp://localhost:2376,
|
||||
# TLS via a shared emptyDir of certs). Tradeoff vs the host/self-hosted executor: DinD
|
||||
# needs `privileged`, but it gives clean per-job isolation and lets the workflow pick its
|
||||
# own job image (the pinned toolchain), which the host executor cannot. Privileged is
|
||||
# contained to this CI-only namespace and the runner mounts NO ServiceAccount token
|
||||
# (automountServiceAccountToken: false) — CI holds no cluster credentials (R-SEC-02).
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: gitea-actions
|
||||
labels:
|
||||
# NOTE: intentionally NO homelab.keithjackson.dev/app-namespace label (see header).
|
||||
homelab.keithjackson.dev/ci: act-runner
|
||||
---
|
||||
# Default-deny ingress; broad egress (a CI runner legitimately needs the internet:
|
||||
# ghcr.io / github.com / docker hub / dl.k8s.io / pypi for job images + pinned tools,
|
||||
# and the Gitea ingress VIP for registration + repo clone). Nothing connects INTO the
|
||||
# runner, so ingress is denied entirely.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: default-deny-ingress-allow-egress
|
||||
namespace: gitea-actions
|
||||
labels:
|
||||
homelab.keithjackson.dev/baseline: default-deny
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress: []
|
||||
egress:
|
||||
# DNS (cluster + split-horizon resolution of gitea.home.keithjackson.dev).
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
# Everything else the runner + DinD need: Gitea VIP + internet registries/tool hosts.
|
||||
- {}
|
||||
---
|
||||
# Runner state (config.yaml + the `.runner` registration file) persists here so the
|
||||
# runner registers exactly ONCE and reuses its identity across restarts (no duplicate
|
||||
# offline rows in Gitea). RWO → Deployment strategy must be Recreate (fix/recreate-strategy-rwo).
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: act-runner-data
|
||||
namespace: gitea-actions
|
||||
labels:
|
||||
app.kubernetes.io/name: act-runner
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: act-runner
|
||||
namespace: gitea-actions
|
||||
labels:
|
||||
app.kubernetes.io/name: act-runner
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: act-runner
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: act-runner
|
||||
spec:
|
||||
# CI holds no cluster credentials — do not mount a SA token into the runner or
|
||||
# (via the shared netns) the job containers (R-SEC-02, docs/14).
|
||||
automountServiceAccountToken: false
|
||||
containers:
|
||||
# ---- act_runner: registers with Gitea, dispatches jobs to the DinD daemon ----
|
||||
- name: runner
|
||||
image: gitea/act_runner:0.2.11
|
||||
env:
|
||||
- name: GITEA_INSTANCE_URL
|
||||
value: https://gitea.home.keithjackson.dev
|
||||
- name: GITEA_RUNNER_REGISTRATION_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: gitea-actions-runner
|
||||
key: token
|
||||
- name: GITEA_RUNNER_NAME
|
||||
value: incluster-dind
|
||||
# runs-on label -> job image. Both map to the catthehacker "act" image, which
|
||||
# carries bash/git/curl/tar/python3+pip/node so .gitea/workflows scripts can
|
||||
# install the pinned CI toolchain (ci/versions.env) and actions/checkout runs.
|
||||
- name: GITEA_RUNNER_LABELS
|
||||
value: "ubuntu-latest:docker://ghcr.io/catthehacker/ubuntu:act-22.04,homelab:docker://ghcr.io/catthehacker/ubuntu:act-22.04"
|
||||
# Reach the DinD daemon over the shared pod network namespace, TLS-verified
|
||||
# against the certs the sidecar drops into the shared /certs emptyDir.
|
||||
- name: DOCKER_HOST
|
||||
value: tcp://localhost:2376
|
||||
- name: DOCKER_CERT_PATH
|
||||
value: /certs/client
|
||||
- name: DOCKER_TLS_VERIFY
|
||||
value: "1"
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: certs
|
||||
mountPath: /certs
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
# ---- Docker-in-Docker: the job executor daemon (privileged) ----
|
||||
- name: dind
|
||||
image: docker:27-dind
|
||||
securityContext:
|
||||
privileged: true
|
||||
env:
|
||||
# Generate + publish TLS certs into the shared emptyDir; dockerd then listens
|
||||
# on tcp://0.0.0.0:2376 (TLS) which the runner reaches at localhost:2376.
|
||||
- name: DOCKER_TLS_CERTDIR
|
||||
value: /certs
|
||||
volumeMounts:
|
||||
- name: certs
|
||||
mountPath: /certs
|
||||
- name: dind-storage
|
||||
mountPath: /var/lib/docker
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
memory: 2Gi
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: act-runner-data
|
||||
- name: certs
|
||||
emptyDir: {}
|
||||
- name: dind-storage
|
||||
emptyDir: {}
|
||||
Reference in new issue
Block a user