chore(k8s): Address ArgoCD overlay review follow-ups and PR #4744 comments (#4982)

* Address ArgoCD overlay review follow-ups (#4977) and PR #4744 comments

Add CI validation of kustomize overlays, clarify the ignoreDifferences
/data vs /stringData ArgoCD normalization, separate sync-completes from
app-healthy in the Ingress wave comment, add TODO(v0.29) to the
bare-Pod fallback in terminate(), and update the sandbox-runners README
to reflect the bare-Pod → Job migration.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(ci): install kustomize via official script instead of third-party action

The pinned SHA for imranismail/setup-kustomize was unresolvable.

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>

* fix(docs): correct backoffLimit value in sandbox-runners README

The README stated `backoffLimit: 0` but the actual code uses
`_JOB_BACKOFF_LIMIT = 6` — fix the doc to match.

---------

Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
This commit is contained in:
Yuan Tang
2026-08-19 15:38:39 -04:00
committed by GitHub
parent 473941e322
commit ddc5f6ee60
5 changed files with 88 additions and 24 deletions
+55
View File
@@ -0,0 +1,55 @@
name: Kustomize validate
# Renders every deploy/kubernetes overlay with `kustomize build` so manifest
# drift (duplicate bases, missing patches, invalid YAML) is caught in CI
# rather than at deploy time. Only runs when overlay or base files change.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'deploy/kubernetes/**'
push:
branches:
- main
- 'release/v[0-9]*'
paths:
- 'deploy/kubernetes/**'
permissions:
contents: read
concurrency:
group: kustomize-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
validate:
name: Kustomize build (overlays)
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install kustomize
run: |
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/
- name: Render all overlays
run: |
failed=0
for overlay in deploy/kubernetes/overlays/*/; do
name="$(basename "$overlay")"
echo "::group::$name"
if kustomize build "$overlay"; then
echo "::endgroup::"
else
echo "::endgroup::"
echo "::error::kustomize build failed for overlay '$name'"
failed=1
fi
done
exit "$failed"
@@ -45,6 +45,9 @@ spec:
# them out of band (kubectl edit, sealed-secrets, external-secrets), and
# selfHeal must not revert those edits. Without this, selfHeal
# continuously overwrites live credentials with the placeholder.
#
# The pointer targets /data, not /stringData, because ArgoCD normalizes
# stringData into base64 /data on the live object before diffing.
- group: ""
kind: Secret
name: omnigent-secrets
@@ -32,10 +32,12 @@ patches:
# The Ingress depends on an ingress controller (nginx by default) and
# cert-manager. ArgoCD scores an Ingress without status.loadBalancer as
# Progressing, so on a cluster with no controller the sync stalls forever.
# Putting it in wave 1 (everything else is implicit wave 0) means nothing
# gates on its health. ArgoCD's kind ordering already applies it last within
# a wave, so this only affects the health gate.
# Progressing. Wave 1 (everything else is implicit wave 0) means no later
# sync wave gates on its health, so the *sync* completes. The Application
# itself may still report Progressing indefinitely on a cluster without a
# controller — with automated + selfHeal, that keeps the Application in a
# perpetual reconcile loop (harmless but noisy). Install the controller or
# add a custom health check that treats the Ingress as healthy.
- target:
kind: Ingress
patch: |
@@ -1,42 +1,43 @@
# Kubernetes sandbox runners (on-demand host Pods)
This Kustomize overlay turns on the **`kubernetes`** managed-sandbox provider: a
`host_type: managed` session spawns one **runner Pod** that runs `omnigent host`
as its container entrypoint and dials back to the server over the existing
launch-token tunnel. It layers the RBAC + config the provider needs onto the
base server deployment.
`host_type: managed` session spawns a **batch/v1 Job** whose child Pod runs
`omnigent host` as its container entrypoint and dials back to the server over the
existing launch-token tunnel. It layers the RBAC + config the provider needs onto
the base server deployment.
## Launch model: entrypoint-as-host
The runner Pod's container command **is** the host. An **init container**
prepares the workspace (`mkdir` + optional `git clone`); the **main container**
then runs `omnigent host` under a tiny PID-1 reaper. The host re-parents runner
processes to PID 1, which the reaper reaps; SIGTERM is forwarded for graceful
shutdown.
The runner is launched as a **batch/v1 Job** (one Pod, `backoffLimit: 6`). The
Job's child Pod runs `omnigent host` as its container command. An **init
container** prepares the workspace (`mkdir` + optional `git clone`); the **main
container** then runs `omnigent host` under a tiny PID-1 reaper. The host
re-parents runner processes to PID 1, which the reaper reaps; SIGTERM is
forwarded for graceful shutdown.
The launch token is delivered through a **per-Pod Kubernetes Secret** referenced
The launch token is delivered through a **per-Job Kubernetes Secret** referenced
by the Pod's `secretKeyRef` — it never enters the Pod spec, a command line, or
an audit log. The launcher creates that Secret at provision and deletes it
alongside the Pod at terminate.
alongside the Job at terminate.
Because the host is **never started by `exec`-ing into an already-running
container**, this provider needs **no `pods/exec` grant** — and avoids the
exec-into-running-container class of runtime issues entirely. The server SA's
rights are the minimum the launcher calls: create/get/delete Pods, get
`pods/log` (start-failure diagnostics only), create/delete Secrets (the per-Pod
token), and list events.
rights are the minimum the launcher calls: create/get/delete Jobs,
list/get Pods (to poll the Job's child), get `pods/log` (start-failure
diagnostics only), create/delete Secrets (the per-Job token), and list events.
## Two-namespace, least-blast-radius design
| Namespace | Holds |
|---|---|
| `omnigent` | the server, its DB/PVC, its Secrets, the `omnigent-server` SA |
| `omnigent-sandboxes` | runner Pods, the per-Pod token Secrets, the harness-creds Secret, the powerless `omnigent-runner` SA, the scoped Role + RoleBinding |
| `omnigent-sandboxes` | runner Jobs (and their child Pods), the per-Job token Secrets, the harness-creds Secret, the powerless `omnigent-runner` SA, the scoped Role + RoleBinding |
The server SA's Pod/Secret rights are a **namespaced Role** bound (cross-namespace)
to `omnigent-sandboxes` only — so a compromised server can manage runner Pods but
**cannot** delete the server/DB Pods, read the server's Secrets, or execute
commands inside any Pod. The runner namespace enforces Pod Security `restricted`;
The server SA's Job/Pod/Secret rights are a **namespaced Role** bound
(cross-namespace) to `omnigent-sandboxes` only — so a compromised server can
manage runner Jobs but **cannot** delete the server/DB Pods, read the server's
Secrets, or execute commands inside any Pod. The runner namespace enforces Pod Security `restricted`;
the generated runner Pod is already restricted-compliant (non-root uid 1000, drop
`ALL` caps, `seccompProfile: RuntimeDefault`, no privilege escalation).
@@ -83,7 +84,7 @@ runner Pod unexpectedly carries no credential:
(`omnigent/server/managed_hosts.py`), e.g. "agent … is not a genuine built-in;
omitting agent label".
- A name that is not a valid label value logs a `WARNING` from
`build_pod_manifest` (`omnigent/onboarding/sandboxes/kubernetes.py`), e.g.
`build_job_manifest` (`omnigent/onboarding/sandboxes/kubernetes.py`), e.g.
"agent … is not a valid omnigent.ai/agent value; runner Pod … stays
unclassified". Note the gate upstream will already have logged this agent as
classified, so this is the line that explains the missing label.
@@ -1704,6 +1704,9 @@ class KubernetesSandboxLauncher(SandboxHostLauncher):
_request_timeout=_POD_READY_REQUEST_TIMEOUT_S,
),
),
# TODO(v0.29): remove this entry once all runners have rolled
# past v0.28 — bare Pods are no longer created. Keep in sync
# with the pods:create/delete TODO in role.yaml.
# Fall back to deleting a bare Pod left by the pre-Job
# launcher. Child Pods are named <job>-<rand5> so this only
# targets pre-migration bare Pods whose name IS sandbox_id.