# knotless-runner-k8s Proof of concept for a Kubernetes Job executor for `knotless-run`. `knotless-run` normally runs one docker container per workflow run. This crate proves the same shape on a socialweb-computer tenant k3s cluster: one Job per run, one pod that stays alive, one `kubectl exec` per step. The runner daemon stays outside the cluster; the Job pod is what makes the tenant scale a node from zero. Status: proof of concept. It does not patch `knotless-run` yet. `PLAN.md` maps this code to `crates/knotless-run/src/executor.rs`. ## Working rules - Do not open pull requests, merge requests, or send patches to any upstream repository. All work stays here and in our own forks. - Target repos are local forks, not the original checkouts: `~/src/knotless-fork` and `~/src/socialweb-computer-kcp-fork`. Their `upstream` remotes are read-only; push to `fork` as work lands, over `knotless://`. - Never print or commit credential values. `scripts/oauth-signin.ts` reads the credential file itself. See `AGENTS.md` for the full rules. ## Build and test ```bash cargo test cargo run -- --help ``` Tests use a fake `kubectl`, so no cluster is needed. ## Dry run ```bash cargo run --quiet -- run \ --name demo \ --image busybox:1.36 \ --workdir examples \ --steps-file examples/steps.tsv \ --outputs "$HOME/.tmp/knotless-runner-k8s/outputs" \ --dry-run ``` This prints the Job manifest and every `kubectl` call without touching a cluster. ## Live run on a hosted tenant cluster The gateway is API-only, so sign-in is driven by `socialweb-computer-kcp/scripts/oauth-signin.ts`. The script below reads `~/.aliceoa_bsky_social_source` through that script, never itself, and writes the session cookie and kubeconfig 0600 under `~/.tmp/knotless-runner-k8s/tenant`. ```bash scripts/tenant-kubeconfig.sh export KUBECONFIG=$HOME/.tmp/knotless-runner-k8s/tenant/knotless-poc.kubeconfig.yaml kubectl get nodes ``` Then run a workflow: ```bash cargo run --quiet -- run \ --name knotless-poc \ --runner did:plc:runner \ --image busybox:1.36 \ --workdir examples \ --steps-file examples/steps.tsv \ --outputs "$HOME/.tmp/knotless-runner-k8s/outputs" \ --result "$HOME/.tmp/knotless-runner-k8s/result.json" \ --log "$HOME/.tmp/knotless-runner-k8s/run.log" ``` The Job is deleted unless `--keep` is given. A deleted Job lets the tenant return to zero nodes after its scale hold. ### Scoped kubeconfig `scripts/tenant-kubeconfig.sh` returns the gateway's admin kubeconfig. Make a runner-scoped one from it: ```bash ADMIN_KUBECONFIG=$HOME/.tmp/knotless-runner-k8s/tenant/knotless-poc.kubeconfig.yaml \ scripts/scoped-kubeconfig.sh ``` That creates a ServiceAccount, a namespaced Role for Jobs, pods, exec, logs, events and NetworkPolicies, and a 24-hour token kubeconfig at `~/.tmp/knotless-runner-k8s/tenant/knotless-runner.kubeconfig.yaml`, mode 0600. The script checks that the token can create Jobs but cannot read nodes. To delete the tenant cluster: ```bash DELETE=1 scripts/tenant-kubeconfig.sh ``` ## How a run works 1. `kubectl apply` a `batch/v1 Job` whose pod runs `sleep infinity`. 2. Wait for the pod to be Ready. 3. Stream the checkout to `/work` with tar over `kubectl exec -i`. 4. Stream shared steps to `/steps`. 5. Run each step with `kubectl exec -- sh -ec