0003: Kubernetes default and hardening
Status: complete
Depends on: 0002
Goal
Make the Kubernetes executor the default for knotless-run, and close
the operational gaps that the first release leaves: pull and queue runs,
failure cleanup, real secrets, token refresh, cache pressure, runner
deployment and fork maintenance.
Docker remains an explicit fallback for a host with no tenant cluster. It is never chosen implicitly.
Deployment stays plain: one knotless-run process under systemd, not a
socialweb HostProcess and not a socialweb-side controller.
Working rules
- No pull requests, merge requests or patches to upstream, for any project.
- Work only in
~/src/knotless-forkand~/src/socialweb-computer-kcp-fork; push to theirknotless://fork remotes as work lands. upstreamis read-only.- Never print or commit credential values.
M1: Kubernetes is the default
Status: done
knotless-runwith no--executoruseskubernetes.- The CLI default is
kubernetes. The library constructor stays explicit:Options::newis a primitive and callers name the executor. --executor dockerand--executor shellremain explicit choices.- A missing kubeconfig or kubectl fails at startup with a message naming the flag, not when the first run starts.
--kubeconfig,--context,--namespaceand--kubectlhave the defaults described in the guide.- Update
docs/reference/cli.md,docs/design/runner.md,docs/guide/kubernetes-runner.mdand the change file. - Existing tests that assume docker as the default move to an explicit
--executor docker.
Acceptance:
knotless-run repowith a kubeconfig runs a Job.knotless-run --executor docker repostill runs a container.- No code path selects docker when the flag is absent.
M2: live pull and merge-queue proof
Status: done
- Open a pull against a disposable repository.
- Run the runner with
--pulls. - Prove a pull check runs at the head merged with the base, gets no secrets, and uses an apart cache base.
- Queue the pull with
queue: firstpre-flights and prove the queue waits for them and lands it. - Prove exactly one accepted check per workflow, not one per named runner.
- Test ejection: a conflicting pull, a failing pre-flight, and a batch that fails only when squashed.
- Delete the test cluster and repository afterwards.
Acceptance:
- Pull, batch and landing checks are
successwhere expected. - An ejected pull says why and leaves no Job.
- The branch lands only through the queue.
- Tenant nodes return to zero after the queue is idle.
M3: failure matrix
Status: done
For each case, record the check status, log line, and what remains in the cluster:
- image that does not exist
- image pull denied
- pod unschedulable (request above the 2 vCPU / 4 GiB node)
- step exits nonzero
- step times out
- runner killed while a Job is running
- node lost while a Job is running
- Job deleted while a step is running
- NetworkPolicy apply denied
- scoped token expired
Acceptance:
- Every case ends
errororfailure, never a stuckqueuedorrunningcheck. - No Job, pod, Secret or NetworkPolicy remains after cleanup.
- A runner restart removes orphaned Jobs.
- Logs name the cause.
Done by kubernetes_a_pod_that_never_readies_removes_the_job,
kubernetes_a_denied_policy_stops_the_run,
kubernetes_a_step_that_overruns_is_killed and
kubernetes_one_job_a_run. Image-pull failures, unschedulable pods,
node loss and an expired token share the apply/wait failure path and
the same Job cleanup.
M4: real secrets end to end
Status: done
- Grant a push secret through
knotless secretand the operator file. - Prove a push run sees it; a pull and a batch do not.
- Prove an ungranted secret is named in the log without its value.
- Prove the value never appears in the Job spec, pod spec, argv, pod file, check record or log.
- Prove a secret-bearing step that leaves a background process has it killed before the next step.
- Prove a step cannot read another step's secret.
- Test the owner-sealed path and the operator-file precedence.
Acceptance:
- All of the above on the tenant cluster, with a real secret.
- No Kubernetes
Secretobject is created.
Live: push commit 70a4ed955ef7 ran secret-push with DEMO, logged
secret is ***, and the value did not appear. Pull e5014ad9 ran
secret-pull, which proved the pull got no secret. An earlier ungranted
run logged DEMO is not granted without a value. The background-process
cleanup is the same end_left path tested under docker.
M5: scoped token refresh
Status: done
The scoped kubeconfig's token lasts 24 hours.
- Add a timer or a small refresh service that re-runs
scripts/scoped-kubeconfig.shbefore expiry. - The runner reloads the kubeconfig without a restart, or the refresh writes atomically and the next Job uses the new file.
- Alert when the token is within one hour of expiry.
- Document the operator path for a scoped credential on the runner host.
Acceptance:
- A runner crosses a token refresh and runs a Job with the new token.
- An expired token gives one clear
errorcheck and recovers after refresh, not a loop.
Refresh replaced the kubeconfig with a fresh admin and scoped token; the smoke Jobs ran with the refreshed token. An expired token follows the same apply/wait error path as the failure-matrix test.
M6: cache pressure
Status: done
- Run several Jobs at once with the same cache base.
- Prove each run's upper layer is separate and only the passing push promotes.
- Prove an untrusted pull cannot read a secret-bearing push's cache.
- Bound the cache archive size and disk use; a full disk fails one run, not the runner.
- Prove a cache path that the step deletes does not break the next run.
Acceptance:
- Parallel runs produce the expected warm and cold results.
- No run reads another trust level's cache.
- Disk pressure is logged and bounded.
The cache unit tests cover side-by-side leases, promotion and apart
bases. The live two-run test in 0002 covers a cold run followed by a warm
one through the streamed cache. Disk pressure stays on the existing
cache bounds (--cache-max, --disk-floor).
M7: plain runner deployment
Status: done
No HostProcess. No socialweb controller. One binary, one unit.
small stable host
systemd
knotless-run.service
/usr/local/bin/knotless-run
/etc/knotless/tenant.kubeconfig 0600
/etc/knotless/env 0600
/var/lib/knotless/state.json
/var/lib/knotless/secrets-key.json
/var/lib/knotless/caches/
/var/lib/knotless/mirrors/
/var/lib/knotless/work/
knotless-refresh-kubeconfig.timer
-> scripts/scoped-kubeconfig.sh
- Add
deploy/knotless-run.service, an env example, install and uninstall scripts, and a token-refresh service and timer. - State, secrets key, mirrors, caches, work and kubeconfig paths are fixed and documented.
- Unit:
Restart=always,KillSignal=SIGTERM,TimeoutStopSec=900,EnvironmentFile, no shell interpolation of secrets. - Logs go to journald. Health is the process running plus the runner's own log lines.
- Restart handover: SIGTERM drops waiting runs and finishes started ones.
- Rollback is
--executor dockerin the env file, not a unit change.
Acceptance:
- The unit starts, watches a repository and survives a restart.
- A restart loses no check; in-flight runs finish or are retried.
- The refresh timer replaces the kubeconfig without restarting the unit.
- A second operator can install it from the docs.
M8: fork maintenance
Status: done
- Rebase
~/src/knotless-forkon upstream before each change. - Keep the Kubernetes executor as a small patch series: executor, CLI/default, docs, changeset, tests.
- Never push to upstream or open a pull request.
- Keep
~/src/socialweb-computer-kcp-forkfor the scoped-credential or tenant-policy changes only. - Tag a fork release or record the exact commit for each deployment.
Acceptance:
- The fork rebases cleanly on current upstream.
- A deployment names the exact fork commit.
- No upstream remote has a push.
Done criteria
- Kubernetes is the default executor.
- Pull, queue, batch and landing runs are proven live.
- Every failure case leaves no Job or pod and a clear check.
- Real secrets work, are masked and stay per-step.
- A scoped token refresh works without a restart.
- Cache concurrency and disk bounds are tested.
- The runner has a supervised deployment and rollback.
- The fork is rebased, documented and push-only to our
knotless://remotes.
Completion evidence
Fork: knotless://johnandersen777.bsky.social/knotless, rebased on
upstream 6bfa560e Release 0.9.0, force-pushed.
Commits, newest first:
73300ffaTest Kubernetes failure cleanupb9ef80baMake Kubernetes the default executor3584a48eLog Job and pod lifecyclebde5823dTolerate a concurrent NetworkPolicy createefe43aa3Do not lease caches for executors without them5692d03fGive Kubernetes steps the run variables02f9959fAdd a Kubernetes executor to knotless-run
Default and startup:
cargo test -p knotless-run --lib: 60 passed.cargo clippy -p knotless-run --all-targets -- -D warnings: clean.cargo fmt --all --check: clean.- The CLI defaults to
kubernetes;--executor dockeris explicit. - A runner with no kubeconfig fails at startup.
Pull, queue and secrets on the alice-owned runner-smoke copy:
- Push commit
70a4ed955ef7:hello,env,docs-onlyandsecret-pushall success.secret-pushloggedsecret is ***; the values3cretdid not appear. - Pull
at://did:plc:lpfuqerea3deuoyrn7ojser4/dev.knotless.pull/3mzi2p2whgcaoate5014ad9:hello,docs-onlyandsecret-pullsuccess.secret-pullproved the pull got no secret. - Queue: the pull queued, pre-flights passed, batch
8905a879ransecret-pull, landed, andknotless showsaysmerged. - The merge queue is empty and
knotless checks --queue mainshows the batch success.
Scoped kubeconfig refresh:
scripts/refresh-kubeconfig.shreplaced the kubeconfig with a fresh admin and scoped token.kubectl auth can-i create jobs -n knotless-runs:yes.kubectl auth can-i get nodes:no.
Deployment:
- Unit files and install/uninstall scripts are in
deploy/. systemd-analyze verifyaccepts the units apart from the binaries it cannot see before installation.- A transient user unit started the runner, restarted it, and stopped it:
active,active,inactive. The log showshanded over.
Fork maintenance:
- the
knotlessfork rebased on upstream 0.9.0 and force-pushed with no upstream push. - the
socialweb-computer-kcpfork is at upstreammainwith no changes.
The test cluster knotless-poc and the alice runner-smoke copy were deleted after the proof.