From ac24acfd53e6c479a8bc2082cdac00f368a93ad8 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Fri, 7 Aug 2026 22:25:19 +0100 Subject: [PATCH 01/22] docs: add whole-project design for remote workloads Covers the four workload classes (sandbox/agent, notebook/shell, inference, training) on one Kubernetes API surface: - dial-out control plane (one WSS per instance + JWT), replacing the mesh - kubelet API on the virtual node so kubectl logs/exec work natively - agent as PID 1 owning the workload's pipes; registry-resolved entrypoint - Sandbox + SandboxClass CRDs layered above the existing Pod path - inbound reachability as a per-provider capability, not one mechanism Reverses the earlier decision to expose logs/exec only from the SandD controller: the stated requirement is kubectl-native access, so the ergonomics are the product. Co-Authored-By: Claude Opus 5 --- docs/proposal/architecture.md | 602 ++++++++++++++++++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 docs/proposal/architecture.md diff --git a/docs/proposal/architecture.md b/docs/proposal/architecture.md new file mode 100644 index 0000000..3e6c273 --- /dev/null +++ b/docs/proposal/architecture.md @@ -0,0 +1,602 @@ +# Remote workloads: one Kubernetes API for sandboxes, inference, and training + +**Status:** Draft · **Supersedes:** the headscale/mesh design on +`feat/support-headscale` (see [Migration](#migration-from-featsupport-headscale)) + +Nebula turns external GPU capacity into ordinary Pods. Today that stops at +*provisioning*: a Pod is placed, an instance is launched, and status flows back — +but the workload is unreachable. `kubectl logs` and `kubectl exec` return +`NotFound` (`pkg/vnode/handler.go:686`), nothing in the cluster can call a served +model, and there is no way to run several instances as one coordinated group. + +This document is the design for closing that gap. It is a proposal: nothing here +is built yet. Once a phase ships, its content moves into +[docs/architecture.md](../architecture.md) and this stays as the record of *why*. + +- [Requirement](#requirement) +- [Goals and non-goals](#goals-and-non-goals) +- [Workload classes](#workload-classes) +- [Decisions](#decisions) + - [1. Dial-out, not a mesh](#1-dial-out-not-a-mesh) + - [2. The kubelet API lives on the virtual node](#2-the-kubelet-api-lives-on-the-virtual-node) + - [3. The agent is PID 1](#3-the-agent-is-pid-1) + - [4. A Sandbox CRD, above the Pod](#4-a-sandbox-crd-above-the-pod) + - [5. Inbound is a provider capability](#5-inbound-is-a-provider-capability) +- [Provider seam changes](#provider-seam-changes) +- [Roadmap](#roadmap) +- [Risks](#risks) +- [Migration from feat/support-headscale](#migration-from-featsupport-headscale) + +--- + +## Requirement + +> The experience must be exactly that of creating a Pod in Kubernetes — including +> `kubectl logs` and `kubectl exec` — while the workload actually runs on a remote +> instance in another account, VPC, or cloud. + +That sentence drives most of what follows. It rules out a Nebula-specific client +API as the primary surface: consumers must use the standard `pods/log` and +`pods/exec` subresources, with RBAC as the authorization model, so that `kubectl`, +`client-go`, k9s, Lens, ArgoCD, and every language's Kubernetes client work +unmodified. + +## Goals and non-goals + +**Goals** + +- `kubectl logs` / `exec` / `attach` against a remote instance, natively. +- One control-plane mechanism that works on **every** provider, including ones that + offer no native exec API and no inbound reachability. +- In-cluster clients reach a served model by ordinary Service DNS. +- Multi-instance workloads provision all-or-nothing and can reach each other. +- Per-provider code stays small: adding a provider must not mean re-implementing + logs, exec, or stats. + +**Non-goals** + +- Multi-container Pods, init containers, and image-level restart on remote + instances. These need container-runtime access, which not every provider grants. +- Serving high-volume data traffic through the Nebula control plane. +- An overlay network of any kind (see [Decision 1](#1-dial-out-not-a-mesh)). +- In-place migration of a running workload between providers. + +## Workload classes + +Four classes, three phases. They differ along two axes only: whether anything must +**dial into** the instance, and whether the instances form a **group**. + +| Class | Inbound to instance | Group | Lifetime | Phase | +|---|---|---|---|---| +| sandbox, agent, agent-exec | no | 1 | minutes–days | 1 | +| notebook, shell | no (consumer terminates in-cluster) | 1 | hours–days | 1 | +| inference, services | **yes**, from arbitrary clients | N fungible | long | 2 | +| training, fine-tuning, jobs | **yes**, from peers only | N ordinal, all-or-nothing | bounded | 3 | + +Two facts fall out and shape every decision below: + +1. **Outbound reachability is the only thing every provider guarantees.** So the + control plane must be outbound-only, everywhere. +2. **Inbound is needed by exactly two classes, for unrelated reasons** — + open-ended consumers (inference) versus a closed peer set (training). They get + different mechanisms; neither gets an overlay. + +Note that sandbox is the **highest-count, highest-churn** class, not the lowest. +Scale arguments that assume otherwise get the answer backwards. + +--- + +## Decisions + +### 1. Dial-out, not a mesh + +**Decision.** The in-container agent opens **one long-lived WSS connection per +instance** to a stable public endpoint, authenticated by a short-lived JWT whose +audience is the workload identity. No overlay network, for any class. + +``` + instance (any cloud, behind NAT) your cluster + ┌─────────────────────────────┐ + │ container │ + │ agent (PID 1) ──────────────┼──outbound TLS 443──▶ LB ──SNI──▶ WS server + │ └─ workload as child │ map[claim]conn + └─────────────────────────────┘ +``` + +**Why the connection must be persistent.** A real kubelet needs no such thing: the +apiserver *dials it*, because it has a routable address. A Nebula instance sits +behind NAT in another cloud, so the apiserver cannot dial it and the direction must +flip. Once flipped, the connection has to *already exist* when a request arrives — +`kubectl exec` is synchronous, and there is no other channel over which to ask the +instance to dial in. That is the entire reason, and it is forced by "no inbound", +not chosen. The alternative, polling, would add seconds of latency to every exec +and burn requests from every idle instance. + +Cost: one idle TLS connection per instance, carrying zero traffic until someone +runs a command. 10k of them is an ordinary WebSocket server in a single process. + +**Why not a mesh (headscale/Tailscale).** A mesh solves the same reachability +problem and charges much more for it: + +- a stateful control plane to operate (headscale: SQLite, single-writer, no HA) and + one failure domain for every instance at once; +- per-workload pre-auth key minting, TTLs, ephemeral-node reaping, and MagicDNS + name-reclamation races; +- netmap distribution on every join/leave — worst precisely for sandboxes, the + highest-churn class; +- `tailscaled` inside every workload container, in userspace-networking mode, on a + rented GPU box. + +And the one thing it uniquely provides — a stable address something can *dial +into* — has **no consumer**: the agent only ever initiates, and identity comes from +the JWT audience, not an address. Buying commercial Tailscale would fix the +operational half (HA control plane, working ACLs, an operated DERP fleet) but not +that: it would be paying a vendor for a property no code path uses, plus per-device +billing against extreme churn. The comparison is not "buy Tailscale" versus +"operate headscale properly" — it is "buy Tailscale" versus "a TLS listener and JWT +verification", which is a problem we can delete instead of solve. + +For the other classes a mesh is not merely unnecessary but unworkable. Inference +consumers are arbitrary in-cluster Pods and external callers that will never be +mesh members, so mesh IPs in an EndpointSlice are unroutable — a structural break, +not a scale limit. Training's rank-to-rank traffic must stay provider-native, +because userspace encapsulation and reduced MTU land directly on allreduce +bandwidth. + +**Revisit if** dial-in becomes a product feature: operator SSH into any box, a +pull-based metrics scraper, or instance-to-instance traffic *across* providers. +Those are the cases an overlay genuinely wins. Even then it is additive — the +JWT-authorized channel is still what authorizes a request, since a mesh +authenticates a *device*, not an action. + +**One public front door, SNI-routed.** The agent dials a deterministic name derived +from its own identity (`sbx-.example.com`). The router proxies on SNI +**without terminating TLS**, so it never sees plaintext or credentials and needs no +per-workload configuration — wildcard DNS plus a wildcard certificate. The WS +server terminates TLS and verifies the JWT audience itself. If connection counts +ever justify sharding, consistent-hashing the uid is a router change, not an +architecture change, and the agent's configuration never moves. + +Note this replaces one public endpoint with another: the mesh design already +required an internet-facing NLB for headscale. This is not new exposure. + +### 2. The kubelet API lives on the virtual node + +**Decision.** Implement the kubelet HTTP surface on the virtual node and serve +logs/exec/attach from it, forwarding over the instance's WebSocket. + +``` +kubectl exec pod-x + └─▶ apiserver ──HTTPS──▶ virtual-node kubelet endpoint (Nebula manager) + └─▶ map[claim]conn ──▶ agent ──▶ container +``` + +This **reverses an earlier decision** to expose logs/exec only as a controller-side +`/v1/*` API reached in-cluster. That decision judged `kubectl`-native routing to be +"pure ergonomics", not worth the serving infrastructure. Given the +[requirement](#requirement), the ergonomics *are* the product. + +**What it buys.** Every consumer works unmodified — `kubectl`, `client-go`, other +languages' clients, k9s/Lens/ArgoCD, our own controllers — and authorization is +RBAC on `pods/exec` and `pods/log`, namespace-scoped, so a tenant's workload can +only exec into its own Pods. No client library to publish, no permission system to +design. This also answers "how does another in-cluster workload run a command on +the instance": it uses `remotecommand.NewSPDYExecutor` exactly as it would against +a real Pod, and needs to know nothing about Nebula. + +**What it costs.** The infrastructure previously skipped: + +1. Mount `vkapi.PodHandler` on an HTTPS server. `node/api` is **already imported** + (`pkg/vnode/handler.go:30`), so this does not need the `nodeutil` wrapper whose + apiserver dependency does not compile against the pinned k8s 0.33 line + (`pkg/vnode/node.go:78`). That compile block was never the real blocker. +2. Advertise the endpoint: node `status.addresses` plus + `daemonEndpoints.kubeletEndpoint.port`. +3. A serving certificate the apiserver trusts. +4. Authn/authz: TokenReview + SubjectAccessReview on `nodes/proxy`. Without this, + anything that can reach the port can exec into any tenant's container. + +Each subresource is a separate handler, and all of them are stubs returning +`errdefs.NotFound` today (`pkg/vnode/handler.go:686`–`709`). They can ship +incrementally — an unimplemented one returns a clean error rather than failing +strangely: + +| Subresource | Handler | Phase | +|---|---|---| +| `exec`, `attach` | `RunInContainer`, `AttachToContainer` | 1 | +| `logs` | `GetContainerLogs` | 1 (depends on [Decision 3](#3-the-agent-is-pid-1)) | +| `port-forward` | `PortForward` | 1.5 (notebook) | +| `top` | `GetStatsSummary` | later | + +**Latency.** A caller reaches the agent in three hops (caller → apiserver → VK → +agent), and each `exec` is a fresh stream upgrade, because that is the shape of the +Kubernetes exec API — it cannot reuse a connection. The VK→instance leg *is* +connection-reused (a multiplexed stream over the existing WebSocket, sub-millisecond +to open), so the per-call cost is the apiserver upgrade: tens of milliseconds. Fine +for debugging, notebooks, and moderate agent use. + +If agent-exec becomes thousands of calls per second, add a second door directly on +the WS server — the caller holds one long-lived connection and multiplexes many +execs over it, one hop instead of three. That is additive and shares the same +instance connection, but it owns its own authorization, which is its real cost. +Build the apiserver path first; it is the compatibility story. + +### 3. The agent is PID 1 + +**Decision.** The agent runs as PID 1 in the workload container and spawns the +workload as its **child**, owning its stdout/stderr pipes. The image entrypoint is +resolved from the **registry** at provision time, not required from the user. + +**Why.** `kubectl logs` is impossible otherwise. Today the shim does `exec "$@"`, +making the workload PID 1, so its output goes to the host's Docker log driver — +outside the container and unreachable from within it. (`/tmp/sandd.log` is the +*agent's* log, not the workload's.) Owning the child's pipes is what makes logs +real, and three things come with it: + +- **exit codes** — the agent reaps the child, so `Succeeded`/`Failed` become + representable. That is exactly what Job and training semantics need, and + `applyState` (`pkg/vnode/status.go`) cannot express it today. +- **`kubectl top`** with no provider API: `/sys/fs/cgroup` plus `nvidia-smi`, both + readable from inside. +- **restarting the workload** without reprovisioning the instance. + +It also *deletes* code. The shim's `while :` supervisor loop exists only because the +agent could not be PID 1; with the agent as PID 1, its death is the container's +death — a visible failure instead of the documented silent-dead-daemon class +(InftyAI/Nebula#20). + +**The entrypoint problem, solved generally.** Overriding `ENTRYPOINT` means the +agent must know what to run, which is why the current design rejects commandless +Pods (`errSanddNeedsCommand`, `pkg/provider/aws/translate.go`). That constraint +would block most real inference and training images, which rely on baked-in +entrypoints. The fix is not per-provider: pull the **OCI image config** at provision +time, read `Entrypoint`/`Cmd`, and apply kubelet override semantics — Pod `command` +overrides entrypoint, `args` overrides cmd, the same mapping `buildUserData` already +documents. One cached HTTP call per digest, identical on every provider, and +`errSanddNeedsCommand` disappears for good. + +**Why in-container rather than on the host.** Where we own the host (AWS), a +host-side agent would be closer to a real kubelet: `docker logs` and `docker exec` +are literally what a kubelet uses, with zero image requirements, plus container exit +codes and restart-without-reprovision for free. It is genuinely the better +implementation *there*. But it is unavailable on providers that hand us only a +container, so choosing it as the primary path means writing logs/exec twice and +having no answer for a provider that offers neither host access nor a native exec +API. + +The in-container PID-1 model is the **general** solution: it needs only a container +and outbound network, which is the universal contract. It gives up multi-container +Pods, init containers, and image-level restart — declared non-goals above. A native +or host-side implementation stays available as an **optimization** behind the +`PodAccess` interface ([below](#provider-seam-changes)), never a requirement. This +is the property that matters for adding providers: an unsupported provider degrades +to the general path instead of being unsupported. + +### 4. A Sandbox CRD, above the Pod + +**Decision.** Add a `Sandbox` CRD (namespaced) plus a `SandboxClass` (cluster-scoped) +for policy. A Sandbox **creates one Pod**; it does not replace or bypass it. + +``` +Sandbox + ├── creates Pod (1, ownerRef'd, restartPolicy: Never, NO command) + │ └── gate → placement → nodeSelector → virtual node + │ └── VK CreatePod → NodeClaim → instance + └── owns lifecycle: stop/start, TTL, idle-cull, endpoints +``` + +**Why a Pod underneath.** The Pod is the carrier of everything already built: +placement, `pkg/failover`'s blocklist, and — critically — the NodeClaim finalizer +that guarantees a paid instance is never leaked. It is also what ResourceQuota +counts, which is how one tenant is stopped from provisioning fifty H100s. Bypassing +it would mean reimplementing all of that. + +**Why not a Deployment.** Replicas are *fungible*; a sandbox has identity — its +disk, its claim name, its live sessions. A rolling update would silently swap a +user's box out from under them, and the Deployment controller's recreate-on-exit +fights the `Stopped` state below. + +**Why a CRD at all**, rather than just a labeled Pod. Three things a Pod cannot +express: + +1. **Identity that outlives the instance** — the disk and claim name survive a + reprovision. +2. **`desiredState: Running | Stopped`** — release a $3/hr GPU while keeping the + volume. A large economic lever for notebooks, and meaningless on a Pod. +3. **`status.endpoints` + `lastActivityTime`** — a stable address and the idle + signal that drives culling. + +**No command, by design.** The agent is PID 1 and there is no user program. For a +notebook, the process that must run (`start-notebook.sh`) is started as a +**post-ready exec through the control channel**, not as the container command. That +is strictly better: restart Jupyter without recreating the instance, a crash does +not kill the box or the session, and a second process (TensorBoard) can be added to +the same sandbox later. If a field is wanted for it, it is `spec.startup` — a hook, +semantically distinct from a container command. + +**The classes are policy, not a discriminator.** Five product surfaces come from one +CRD by varying a `SandboxClass`, the way StorageClass and RuntimeClass work: + +```yaml +kind: SandboxClass # cluster-scoped policy +metadata: {name: notebook-a100} +spec: + nodePoolRef: gpu + image: jupyter/tensorflow-notebook:latest + startup: ["start-notebook.sh"] # post-ready exec, NOT a container command + ports: [{name: http, port: 8888, expose: Proxied}] + interfaces: [Exec, PTY, Proxy] + idleTimeout: 30m + storage: {size: 100Gi, retainOnStop: true} +``` + +```yaml +kind: Sandbox # namespaced instance +metadata: {name: alice-nb-1, namespace: team-ml} +spec: + classRef: notebook-a100 + desiredState: Running + accelerator: nvidia-a100 # per-instance override + ttl: 8h +status: + phase: Ready + endpoints: + http: https://nebula-proxy.nebula-system/s/alice-nb-1/ + lastActivityTime: "2026-08-07T09:12:00Z" + nodeClaimRef: {name: team-ml-alice-nb-1} +``` + +So: *agent-exec* = short TTL, no storage, Exec only. *agent* = long-lived, storage, +no idle cull. *notebook* = as above. *shell* = PTY-primary, short idle. Five +surfaces, one CRD, zero mutually-exclusive fields. + +**The WS server is the activity oracle.** It sees every exec, session, and proxied +request, so `lastActivityTime` flows agent → server → `Sandbox.status`, and the +Sandbox controller culls on it. Nothing else can know this — the provider only knows +the instance is running. + +**Open question — cold start for agent-exec.** A NeoCloud GPU instance takes minutes +to become Ready; a code-interpreter call wants sub-second. Three options: (a) 1:1 +Sandbox↔instance and accept the latency; (b) a warm standby pool, still 1:1, paying +for idle GPUs; (c) N Sandboxes multiplexed onto one warm instance. Option (c) needs +a per-instance runtime that can launch containers on demand — a different component +from a single-tenant agent — and breaks `NodeClaim.spec.podRef`'s 1:1 relationship +(`api/v1alpha1/nodeclaim_types.go:18`). Start at (a); do not deepen the 1:1 +assumption elsewhere, so (b) or (c) stays reachable. + +### 5. Inbound is a provider capability + +**Decision.** Inference reachability is resolved per provider in three tiers, all +converging on one consumer-facing abstraction: a **selector-less Service** plus an +EndpointSlice that Nebula populates. + +| Tier | Mechanism | Providers | +|---|---|---| +| **A. Native endpoint** | the provider hands us a URL | Modal (tunnels — already implemented, `pkg/provider/modal/client.go:270`) | +| **B. Direct L3** | security group opens the port; clients hit the instance IP | AWS | +| **C. Gateway** | instance dials out; an in-cluster gateway accepts consumers | providers with no inbound path at all | + +Prefer A, then B. C is a fallback taken knowingly, per provider, because it puts a +hop in the token path — it must not be the default, which is exactly what a +mesh-everywhere design would have locked in. + +**Why the Service must have no selector.** A selector-based Service would have its +EndpointSlices auto-populated with the *virtual node's* notion of pod IP, which is +useless. The real address lives in the endpoint annotation +(`persistEndpoint`, `pkg/vnode/handler.go:545`), so Nebula writes the EndpointSlice +itself with `endpointslice.kubernetes.io/managed-by: nebula`. Then kube-proxy +load-balances to off-cluster IPs and every existing client — `my-llm.default.svc` — +works unmodified. + +**Readiness is a correctness prerequisite, not a feature.** `applyState` sets +`Ready=True` as soon as the provider reports *running* +(`pkg/vnode/status.go:74`), but vLLM needs minutes more to load weights. Routing on +that blackholes traffic on every rollout. An endpoint must not become `ready` until a +real probe passes. Modal already enforces the Pod's probe internally +(`SandboxSpec.ReadinessProbe`); for AWS this means probing in the VK poll loop +(`reconcileOnce`) and gating the Ready condition on the result. + +**Never through the control channel.** One process holding 10k connections must not +also carry token streams: it would become a throughput bottleneck and a single +failure point for serving. Control plane and data plane stay separate. The one +exception is `kubectl port-forward` over the existing WebSocket — correct for +debugging, a notebook, or an admin API, and explicitly not for model serving. + +**Training's inbound is different.** Rank-to-rank connectivity is inbound, but from a +*closed, known* peer set — so it is a provider-native rule scoped to the placement +group (same subnet, allow-from-self security group, EFA/RDMA where available), not +exposure to anything outside. It is a provisioning-time property, which is why it is +coupled to the gang primitive rather than to this tier system. + +--- + +## Provider seam changes + +Four changes to `pkg/provider`, all small, all shared by every phase. + +**1. One interface for pod access, agent-backed by default.** + +```go +type PodAccess interface { + Logs(ctx context.Context, claim string, opts LogOptions) (io.ReadCloser, error) + Exec(ctx context.Context, claim string, cmd []string, io AttachIO) error + Stats(ctx context.Context, claim string) (Stats, error) +} +``` + +The agent-backed implementation works everywhere. A provider *may* supply a native +one (Modal's SDK exec) as a pure optimization; nothing breaks if it does not. This is +what keeps "add a provider" from meaning "reimplement logs and exec". + +**2. One per-provider primitive for getting the agent in.** This is the only place +provider difference is irreducible: + +```go +InjectFiles(spec *InstanceSpec, files []File) error +``` + +AWS already has this in effect — host fetch plus a read-only bind mount +(`sanddHostFetchScript`). Modal uses its own file/image mechanism. Anything else +falls back to a `/bin/sh` bootstrap that curls the binary and execs it. One small +function per provider, and the whole kubelet surface follows. + +**3. `Instance.Endpoint string` becomes named, typed addresses.** Modal is the +forcing case, not a hypothetical: it creates one tunnel *per port* and `observe` +collapses them into a single string (`pkg/provider/provider.go:192`). + +```go +type Address struct { + Name string // "http", "grpc" — matches the containerPort name + Host string + Port int32 + Reachable Reachability // Native | Direct | Gateway +} +``` + +`Reachable` is what tells the EndpointSlice bridge whether to write the address +directly or point at a gateway. Do this **before** two consumers hardcode the +string: sandbox wants a control channel, inference wants `http`. + +**4. `Capabilities` gains the access questions** (`pkg/provider/provider.go:151`): + +```go +NativeExec bool // Modal: true → no agent needed for exec +NativeEndpoint bool // Modal: true (tunnels) +InboundMode Reachability // which tier this provider uses +``` + +Also: SandD injection moves from **per-provider** (`KeyMinter != nil`, which injects +into every workload on that provider) to **per-workload**, carried on +`InstanceSpec`. Required regardless — inference and training Pods must be able to opt +out. + +--- + +## Roadmap + +Each phase builds the seam the next one needs. Nothing is built twice. + +### Phase 1 — Sandbox, agent, notebook, shell + +*Goal: `kubectl exec`/`logs` work against a remote box, driven by a CRD.* + +Agent side (gating — these block everything else): + +1. Plain-TLS dial-out with a bearer JWT, alongside the existing `--tunnel` mode. +2. Agent as PID 1, spawning the workload as a child and owning its pipes. +3. Registry-based entrypoint resolution, so no `command` is required. + +Nebula side: + +4. `SanddConfig` → dial-out: drop `ControlServer`, `AuthKey` → `Token`. The + keybroker becomes a JWT signer — same call site at Provision, same env-injection + seam in `writeSanddEntrypoint`; it loses its headscale admin coupling rather than + gaining anything. +5. WS server holding `map[claim]conn`. **In the manager if the agent has a Go + server implementation; otherwise one shared gateway Deployment** that the VK + calls over HTTP. Not one per workload — see [Risks](#risks). +6. Kubelet serving stack on the VK: `vkapi.PodHandler`, address advertisement, + serving cert, TokenReview/SubjectAccessReview. +7. Implement `RunInContainer`, `AttachToContainer`, `GetContainerLogs`. +8. `Sandbox` + `SandboxClass` CRDs and their controller. +9. Delete: `errSanddNeedsCommand`, the shim supervisor loop, the Tailscale fetch, + the headscale half of `config/sandd`, and the `SANDD_TUNNEL_SERVER` substitution + dance in `Makefile`/`hack/deploy.sh` (a headscale constraint, not ours). + +Pull forward from Phase 2 if convenient: the `Instance.Endpoint` widening (seam +change 3). Cheap now, annoying once two consumers depend on the string. + +Notebook additionally needs `PortForward` or a proxy for its HTTP/WS surface — +scope it as 1.5 if Phase 1 is getting long. + +### Phase 2 — Inference and services + +*Goal: `my-llm.default.svc` resolves to an off-cluster instance.* + +1. Named addresses + `Reachability` (if not pulled into Phase 1). +2. `Capabilities` access flags; per-workload agent opt-in. +3. **Real readiness probing** gating the Ready condition — a correctness fix. +4. EndpointSlice bridge: selector-less Service + Nebula-managed endpoints. +5. Per-provider inbound: Modal native (nearly free), AWS security groups. + +### Phase 3 — Training, fine-tuning, jobs + +*Goal: multi-instance, all-or-nothing, mutually reachable.* + +1. **Gang scheduling** — N instances all-or-nothing, so a partially provisioned + 8-node job does not bill while waiting. `NodeClaim.spec.podRef` is 1:1 + (`api/v1alpha1/nodeclaim_types.go:18`), so this needs a group concept. It is the + one genuinely new primitive in this document. +2. **Intra-gang L3** — shared subnet, placement group, EFA/RDMA, allow-from-self. + Provider-native, never an overlay. +3. **Rank injection** — `RANK`, `WORLD_SIZE`, `MASTER_ADDR`, stable ordinals. +4. **Job semantics** — exit code → terminal `Succeeded`/`Failed`. Mostly free from + Phase 1's PID-1 agent, which already reaps the child; `applyState` needs to + represent it. +5. **Shared storage** for checkpoints — the same missing provider field as notebook + persistence. Nothing in the AWS adapter wires block devices today. + +Logs and exec come free from Phase 1: same agent, same connection. + +--- + +## Risks + +**Apiserver → manager reachability.** The apiserver dials the address the virtual +node advertises, so the manager pod's IP must be routable from the control plane. +Fine on VPC-CNI (EKS). **Not** guaranteed on overlay CNIs, and konnectivity or +egress-selector clusters need explicit configuration. This is the one item that +could invalidate Decision 2, and it is cheap to test — **validate it in a real +cluster before building the rest of Phase 1.** + +**Cross-repo dependency on the agent.** Items 1–3 of Phase 1 are SandD-side. In +particular, whether the agent has (or will have) a **Go** server implementation +decides whether the WS server lives in the manager or in a separate gateway +Deployment. Confirm before starting item 5. + +**Connection state is not reconstructible.** Restarting the connection holder kills +live `exec` sessions and reconnects every agent at once. Real kubelets behave the +same way on restart, so it is a familiar failure mode — but it means backoff with +jitter belongs in the agent (not optional), and log streaming should be resumable by +offset rather than assuming a durable stream. + +**The per-workload controller is dropped.** The earlier design gave each workload its +own controller Deployment so consumers could dial it directly with per-tenant +isolation. With the apiserver as the front door, isolation comes from +SubjectAccessReview instead, and a per-workload controller would be 10k Deployments +and an extra hop for no benefit. One shared holder, keyed by claim name. + +**Token lifetime versus workload lifetime.** A 24h token on an 8h notebook is fine; +a long-lived agent outliving its token needs refresh. Simplest for Phase 1: issue a +token that outlives the class's max TTL and bound exposure with ownerRef GC. Decide +now — retrofitting refresh into the agent is worse than designing for it. + +--- + +## Migration from `feat/support-headscale` + +`main` is already mesh-free: commit `2cae49c` ("cleanup: remove headscale") removed +it, and `feat/support-headscale` re-added it via a revert (`1145eb1`). **Nothing needs +to be reverted** — that branch simply should not merge. Keep it as a reference and +build on a fresh branch off `main`; a revert-of-a-revert-of-a-cleanup history would +be unreadable and would lose the working reference for the parts being ported. + +Roughly 40% of the branch survives, and it is the part that took the longest. + +**Port forward:** + +- `pkg/provider/provider.go` — `SanddConfig`, the `Enabled()`/`KeyMinter` seam, and + the injection-point design. Rename `AuthKey` → `Token`, drop `ControlServer`. +- `pkg/provider/aws/translate.go` — the host-fetch plus read-only bind mount *is* the + `InjectFiles` primitive; it just fetches one binary instead of two. +- `pkg/sandd/keybroker.go`, `cmd/keybroker` — the mint-at-Provision call site and + injection plumbing. The signing logic changes; the shape does not. +- `pkg/provider/aws/sandd_test.go`, `client_test.go` — test scaffolding. + +**Leave behind:** `config/sandd/headscale.yaml`, `config/samples/headscale-service.yaml`, +`config/samples/sandd-controller.yaml` (the per-workload controller is dropped), the +`SANDD_TUNNEL_SERVER` substitution in `Makefile`/`hack/deploy.sh`, the Tailscale +tarball fetch, the shim supervisor loop, and `errSanddNeedsCommand`. + +**Incidental:** an 8.4 MB `keybroker` binary is committed on that branch. Do not carry +it forward; add it to `.gitignore`. From 1006868b9fad7e572f97b8c2528ce9092c90fb09 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Fri, 7 Aug 2026 22:26:27 +0100 Subject: [PATCH 02/22] docs: retitle proposal as the whole-project target architecture Co-Authored-By: Claude Opus 5 --- docs/proposal/architecture.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/proposal/architecture.md b/docs/proposal/architecture.md index 3e6c273..cc8b3e4 100644 --- a/docs/proposal/architecture.md +++ b/docs/proposal/architecture.md @@ -1,17 +1,25 @@ -# Remote workloads: one Kubernetes API for sandboxes, inference, and training +# Nebula Architecture **Status:** Draft · **Supersedes:** the headscale/mesh design on `feat/support-headscale` (see [Migration](#migration-from-featsupport-headscale)) +This is the design for where Nebula is going: one Kubernetes API surface over every +GPU workload class — sandboxes and agents, notebooks and shells, inference and +services, training and fine-tuning jobs — all running on remote instances across +NeoClouds and hyperscalers. + Nebula turns external GPU capacity into ordinary Pods. Today that stops at *provisioning*: a Pod is placed, an instance is launched, and status flows back — but the workload is unreachable. `kubectl logs` and `kubectl exec` return `NotFound` (`pkg/vnode/handler.go:686`), nothing in the cluster can call a served -model, and there is no way to run several instances as one coordinated group. - -This document is the design for closing that gap. It is a proposal: nothing here -is built yet. Once a phase ships, its content moves into -[docs/architecture.md](../architecture.md) and this stays as the record of *why*. +model, and there is no way to run several instances as one coordinated group. So +only one of the four classes is really served, and the pieces the other three need +are also the pieces that make the first one good. + +This document covers that whole target state and the order to build it in. It is a +proposal: nothing here is built yet. For the architecture as it exists today, see +[docs/architecture.md](../architecture.md) — as each phase ships, its content moves +there and this stays as the record of *why*. - [Requirement](#requirement) - [Goals and non-goals](#goals-and-non-goals) From 460309d4c07089ae93fc402ce04af36081dfff01 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Fri, 7 Aug 2026 22:28:13 +0100 Subject: [PATCH 03/22] docs: drop the migration section Implementation starts now, so the branch-porting notes are no longer useful. Co-Authored-By: Claude Opus 5 --- docs/proposal/architecture.md | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/docs/proposal/architecture.md b/docs/proposal/architecture.md index cc8b3e4..e10ee78 100644 --- a/docs/proposal/architecture.md +++ b/docs/proposal/architecture.md @@ -1,8 +1,5 @@ # Nebula Architecture -**Status:** Draft · **Supersedes:** the headscale/mesh design on -`feat/support-headscale` (see [Migration](#migration-from-featsupport-headscale)) - This is the design for where Nebula is going: one Kubernetes API surface over every GPU workload class — sandboxes and agents, notebooks and shells, inference and services, training and fine-tuning jobs — all running on remote instances across @@ -33,7 +30,6 @@ there and this stays as the record of *why*. - [Provider seam changes](#provider-seam-changes) - [Roadmap](#roadmap) - [Risks](#risks) -- [Migration from feat/support-headscale](#migration-from-featsupport-headscale) --- @@ -578,33 +574,3 @@ and an extra hop for no benefit. One shared holder, keyed by claim name. a long-lived agent outliving its token needs refresh. Simplest for Phase 1: issue a token that outlives the class's max TTL and bound exposure with ownerRef GC. Decide now — retrofitting refresh into the agent is worse than designing for it. - ---- - -## Migration from `feat/support-headscale` - -`main` is already mesh-free: commit `2cae49c` ("cleanup: remove headscale") removed -it, and `feat/support-headscale` re-added it via a revert (`1145eb1`). **Nothing needs -to be reverted** — that branch simply should not merge. Keep it as a reference and -build on a fresh branch off `main`; a revert-of-a-revert-of-a-cleanup history would -be unreadable and would lose the working reference for the parts being ported. - -Roughly 40% of the branch survives, and it is the part that took the longest. - -**Port forward:** - -- `pkg/provider/provider.go` — `SanddConfig`, the `Enabled()`/`KeyMinter` seam, and - the injection-point design. Rename `AuthKey` → `Token`, drop `ControlServer`. -- `pkg/provider/aws/translate.go` — the host-fetch plus read-only bind mount *is* the - `InjectFiles` primitive; it just fetches one binary instead of two. -- `pkg/sandd/keybroker.go`, `cmd/keybroker` — the mint-at-Provision call site and - injection plumbing. The signing logic changes; the shape does not. -- `pkg/provider/aws/sandd_test.go`, `client_test.go` — test scaffolding. - -**Leave behind:** `config/sandd/headscale.yaml`, `config/samples/headscale-service.yaml`, -`config/samples/sandd-controller.yaml` (the per-workload controller is dropped), the -`SANDD_TUNNEL_SERVER` substitution in `Makefile`/`hack/deploy.sh`, the Tailscale -tarball fetch, the shim supervisor loop, and `errSanddNeedsCommand`. - -**Incidental:** an 8.4 MB `keybroker` binary is committed on that branch. Do not carry -it forward; add it to `.gitignore`. From 5409c57f9d9baf41e33407ff50a8b8a906975c58 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Fri, 7 Aug 2026 23:06:44 +0100 Subject: [PATCH 04/22] docs: name the in-container component SandD, not "agent" "agent" was overloaded: both a workload class (AI agents, agent-exec) and the component running in the container. Co-Authored-By: Claude Opus 5 --- docs/proposal/architecture.md | 280 ++++++---------------------------- 1 file changed, 50 insertions(+), 230 deletions(-) diff --git a/docs/proposal/architecture.md b/docs/proposal/architecture.md index e10ee78..ca96890 100644 --- a/docs/proposal/architecture.md +++ b/docs/proposal/architecture.md @@ -1,30 +1,20 @@ # Nebula Architecture This is the design for where Nebula is going: one Kubernetes API surface over every -GPU workload class — sandboxes and agents, notebooks and shells, inference and -services, training and fine-tuning jobs — all running on remote instances across -NeoClouds and hyperscalers. - -Nebula turns external GPU capacity into ordinary Pods. Today that stops at -*provisioning*: a Pod is placed, an instance is launched, and status flows back — -but the workload is unreachable. `kubectl logs` and `kubectl exec` return -`NotFound` (`pkg/vnode/handler.go:686`), nothing in the cluster can call a served -model, and there is no way to run several instances as one coordinated group. So -only one of the four classes is really served, and the pieces the other three need -are also the pieces that make the first one good. - -This document covers that whole target state and the order to build it in. It is a -proposal: nothing here is built yet. For the architecture as it exists today, see -[docs/architecture.md](../architecture.md) — as each phase ships, its content moves -there and this stays as the record of *why*. - -- [Requirement](#requirement) +GPU workload class: + +- sandboxes and agents, notebooks +- inference and services +- training and fine-tuning jobs + +All running on remote instances across NeoClouds and hyperscalers. + - [Goals and non-goals](#goals-and-non-goals) - [Workload classes](#workload-classes) - [Decisions](#decisions) - [1. Dial-out, not a mesh](#1-dial-out-not-a-mesh) - [2. The kubelet API lives on the virtual node](#2-the-kubelet-api-lives-on-the-virtual-node) - - [3. The agent is PID 1](#3-the-agent-is-pid-1) + - [3. SandD is PID 1](#3-sandd-is-pid-1) - [4. A Sandbox CRD, above the Pod](#4-a-sandbox-crd-above-the-pod) - [5. Inbound is a provider capability](#5-inbound-is-a-provider-capability) - [Provider seam changes](#provider-seam-changes) @@ -33,68 +23,48 @@ there and this stays as the record of *why*. --- -## Requirement - -> The experience must be exactly that of creating a Pod in Kubernetes — including -> `kubectl logs` and `kubectl exec` — while the workload actually runs on a remote -> instance in another account, VPC, or cloud. - -That sentence drives most of what follows. It rules out a Nebula-specific client -API as the primary surface: consumers must use the standard `pods/log` and -`pods/exec` subresources, with RBAC as the authorization model, so that `kubectl`, -`client-go`, k9s, Lens, ArgoCD, and every language's Kubernetes client work -unmodified. - ## Goals and non-goals **Goals** +- A single API surface for all GPU workloads, across providers. - `kubectl logs` / `exec` / `attach` against a remote instance, natively. -- One control-plane mechanism that works on **every** provider, including ones that offer no native exec API and no inbound reachability. -- In-cluster clients reach a served model by ordinary Service DNS. -- Multi-instance workloads provision all-or-nothing and can reach each other. +- In-cluster clients reach a remote service like inference or microservice. - Per-provider code stays small: adding a provider must not mean re-implementing logs, exec, or stats. +**Non-goals (for now)** + +- **Gang scheduling and intra-gang connectivity** — multi-instance workloads that + provision all-or-nothing and can reach each other. Deferred, not abandoned: it is + what training and fine-tuning need. + **Non-goals** - Multi-container Pods, init containers, and image-level restart on remote instances. These need container-runtime access, which not every provider grants. -- Serving high-volume data traffic through the Nebula control plane. - An overlay network of any kind (see [Decision 1](#1-dial-out-not-a-mesh)). - In-place migration of a running workload between providers. ## Workload classes -Four classes, three phases. They differ along two axes only: whether anything must -**dial into** the instance, and whether the instances form a **group**. +Four classes, three phases. | Class | Inbound to instance | Group | Lifetime | Phase | |---|---|---|---|---| -| sandbox, agent, agent-exec | no | 1 | minutes–days | 1 | -| notebook, shell | no (consumer terminates in-cluster) | 1 | hours–days | 1 | +| sandbox, agent | no | 1 | minutes–days | 1 | +| notebook, shell | no | 1 | hours–days | 1 | | inference, services | **yes**, from arbitrary clients | N fungible | long | 2 | | training, fine-tuning, jobs | **yes**, from peers only | N ordinal, all-or-nothing | bounded | 3 | -Two facts fall out and shape every decision below: - -1. **Outbound reachability is the only thing every provider guarantees.** So the - control plane must be outbound-only, everywhere. -2. **Inbound is needed by exactly two classes, for unrelated reasons** — - open-ended consumers (inference) versus a closed peer set (training). They get - different mechanisms; neither gets an overlay. - -Note that sandbox is the **highest-count, highest-churn** class, not the lowest. -Scale arguments that assume otherwise get the answer backwards. - --- ## Decisions ### 1. Dial-out, not a mesh -**Decision.** The in-container agent opens **one long-lived WSS connection per +**Decision.** The in-container SandD opens **one long-lived WSS connection per instance** to a stable public endpoint, authenticated by a short-lived JWT whose audience is the workload identity. No overlay network, for any class. @@ -102,7 +72,7 @@ audience is the workload identity. No overlay network, for any class. instance (any cloud, behind NAT) your cluster ┌─────────────────────────────┐ │ container │ - │ agent (PID 1) ──────────────┼──outbound TLS 443──▶ LB ──SNI──▶ WS server + │ SandD (PID 1) ──────────────┼──outbound TLS 443──▶ LB ──SNI──▶ WS server │ └─ workload as child │ map[claim]conn └─────────────────────────────┘ ``` @@ -112,57 +82,16 @@ apiserver *dials it*, because it has a routable address. A Nebula instance sits behind NAT in another cloud, so the apiserver cannot dial it and the direction must flip. Once flipped, the connection has to *already exist* when a request arrives — `kubectl exec` is synchronous, and there is no other channel over which to ask the -instance to dial in. That is the entire reason, and it is forced by "no inbound", -not chosen. The alternative, polling, would add seconds of latency to every exec -and burn requests from every idle instance. +instance to dial in. Cost: one idle TLS connection per instance, carrying zero traffic until someone runs a command. 10k of them is an ordinary WebSocket server in a single process. **Why not a mesh (headscale/Tailscale).** A mesh solves the same reachability -problem and charges much more for it: - -- a stateful control plane to operate (headscale: SQLite, single-writer, no HA) and - one failure domain for every instance at once; -- per-workload pre-auth key minting, TTLs, ephemeral-node reaping, and MagicDNS - name-reclamation races; -- netmap distribution on every join/leave — worst precisely for sandboxes, the - highest-churn class; -- `tailscaled` inside every workload container, in userspace-networking mode, on a - rented GPU box. - -And the one thing it uniquely provides — a stable address something can *dial -into* — has **no consumer**: the agent only ever initiates, and identity comes from -the JWT audience, not an address. Buying commercial Tailscale would fix the -operational half (HA control plane, working ACLs, an operated DERP fleet) but not -that: it would be paying a vendor for a property no code path uses, plus per-device -billing against extreme churn. The comparison is not "buy Tailscale" versus -"operate headscale properly" — it is "buy Tailscale" versus "a TLS listener and JWT -verification", which is a problem we can delete instead of solve. - -For the other classes a mesh is not merely unnecessary but unworkable. Inference -consumers are arbitrary in-cluster Pods and external callers that will never be -mesh members, so mesh IPs in an EndpointSlice are unroutable — a structural break, -not a scale limit. Training's rank-to-rank traffic must stay provider-native, -because userspace encapsulation and reduced MTU land directly on allreduce -bandwidth. +problem and needs less development work since coordinator already exists, but headscale has a performance issue when reaching around 500 nodes, see [headscale#1656](https://github.com/juanfont/headscale/issues/1656). **Revisit if** dial-in becomes a product feature: operator SSH into any box, a pull-based metrics scraper, or instance-to-instance traffic *across* providers. -Those are the cases an overlay genuinely wins. Even then it is additive — the -JWT-authorized channel is still what authorizes a request, since a mesh -authenticates a *device*, not an action. - -**One public front door, SNI-routed.** The agent dials a deterministic name derived -from its own identity (`sbx-.example.com`). The router proxies on SNI -**without terminating TLS**, so it never sees plaintext or credentials and needs no -per-workload configuration — wildcard DNS plus a wildcard certificate. The WS -server terminates TLS and verifies the JWT audience itself. If connection counts -ever justify sharding, consistent-hashing the uid is a router change, not an -architecture change, and the agent's configuration never moves. - -Note this replaces one public endpoint with another: the mesh design already -required an internet-facing NLB for headscale. This is not new exposure. ### 2. The kubelet API lives on the virtual node @@ -172,109 +101,26 @@ logs/exec/attach from it, forwarding over the instance's WebSocket. ``` kubectl exec pod-x └─▶ apiserver ──HTTPS──▶ virtual-node kubelet endpoint (Nebula manager) - └─▶ map[claim]conn ──▶ agent ──▶ container + └─▶ map[claim]conn ──▶ SandD ──▶ container ``` -This **reverses an earlier decision** to expose logs/exec only as a controller-side -`/v1/*` API reached in-cluster. That decision judged `kubectl`-native routing to be -"pure ergonomics", not worth the serving infrastructure. Given the -[requirement](#requirement), the ergonomics *are* the product. - **What it buys.** Every consumer works unmodified — `kubectl`, `client-go`, other languages' clients, k9s/Lens/ArgoCD, our own controllers — and authorization is -RBAC on `pods/exec` and `pods/log`, namespace-scoped, so a tenant's workload can -only exec into its own Pods. No client library to publish, no permission system to -design. This also answers "how does another in-cluster workload run a command on -the instance": it uses `remotecommand.NewSPDYExecutor` exactly as it would against -a real Pod, and needs to know nothing about Nebula. - -**What it costs.** The infrastructure previously skipped: - -1. Mount `vkapi.PodHandler` on an HTTPS server. `node/api` is **already imported** - (`pkg/vnode/handler.go:30`), so this does not need the `nodeutil` wrapper whose - apiserver dependency does not compile against the pinned k8s 0.33 line - (`pkg/vnode/node.go:78`). That compile block was never the real blocker. -2. Advertise the endpoint: node `status.addresses` plus - `daemonEndpoints.kubeletEndpoint.port`. -3. A serving certificate the apiserver trusts. -4. Authn/authz: TokenReview + SubjectAccessReview on `nodes/proxy`. Without this, - anything that can reach the port can exec into any tenant's container. - -Each subresource is a separate handler, and all of them are stubs returning -`errdefs.NotFound` today (`pkg/vnode/handler.go:686`–`709`). They can ship -incrementally — an unimplemented one returns a clean error rather than failing -strangely: +RBAC on `pods/exec` and `pods/log`, namespace-scoped, so a tenant's workload can only exec into its own Pods. + +Each subresource is a separate handler: | Subresource | Handler | Phase | |---|---|---| | `exec`, `attach` | `RunInContainer`, `AttachToContainer` | 1 | -| `logs` | `GetContainerLogs` | 1 (depends on [Decision 3](#3-the-agent-is-pid-1)) | -| `port-forward` | `PortForward` | 1.5 (notebook) | +| `logs` | `GetContainerLogs` | 1 | +| `port-forward` | `PortForward` | 2 | | `top` | `GetStatsSummary` | later | -**Latency.** A caller reaches the agent in three hops (caller → apiserver → VK → -agent), and each `exec` is a fresh stream upgrade, because that is the shape of the -Kubernetes exec API — it cannot reuse a connection. The VK→instance leg *is* -connection-reused (a multiplexed stream over the existing WebSocket, sub-millisecond -to open), so the per-call cost is the apiserver upgrade: tens of milliseconds. Fine -for debugging, notebooks, and moderate agent use. - -If agent-exec becomes thousands of calls per second, add a second door directly on -the WS server — the caller holds one long-lived connection and multiplexes many -execs over it, one hop instead of three. That is additive and shares the same -instance connection, but it owns its own authorization, which is its real cost. -Build the apiserver path first; it is the compatibility story. - -### 3. The agent is PID 1 - -**Decision.** The agent runs as PID 1 in the workload container and spawns the -workload as its **child**, owning its stdout/stderr pipes. The image entrypoint is -resolved from the **registry** at provision time, not required from the user. - -**Why.** `kubectl logs` is impossible otherwise. Today the shim does `exec "$@"`, -making the workload PID 1, so its output goes to the host's Docker log driver — -outside the container and unreachable from within it. (`/tmp/sandd.log` is the -*agent's* log, not the workload's.) Owning the child's pipes is what makes logs -real, and three things come with it: - -- **exit codes** — the agent reaps the child, so `Succeeded`/`Failed` become - representable. That is exactly what Job and training semantics need, and - `applyState` (`pkg/vnode/status.go`) cannot express it today. -- **`kubectl top`** with no provider API: `/sys/fs/cgroup` plus `nvidia-smi`, both - readable from inside. -- **restarting the workload** without reprovisioning the instance. - -It also *deletes* code. The shim's `while :` supervisor loop exists only because the -agent could not be PID 1; with the agent as PID 1, its death is the container's -death — a visible failure instead of the documented silent-dead-daemon class -(InftyAI/Nebula#20). - -**The entrypoint problem, solved generally.** Overriding `ENTRYPOINT` means the -agent must know what to run, which is why the current design rejects commandless -Pods (`errSanddNeedsCommand`, `pkg/provider/aws/translate.go`). That constraint -would block most real inference and training images, which rely on baked-in -entrypoints. The fix is not per-provider: pull the **OCI image config** at provision -time, read `Entrypoint`/`Cmd`, and apply kubelet override semantics — Pod `command` -overrides entrypoint, `args` overrides cmd, the same mapping `buildUserData` already -documents. One cached HTTP call per digest, identical on every provider, and -`errSanddNeedsCommand` disappears for good. - -**Why in-container rather than on the host.** Where we own the host (AWS), a -host-side agent would be closer to a real kubelet: `docker logs` and `docker exec` -are literally what a kubelet uses, with zero image requirements, plus container exit -codes and restart-without-reprovision for free. It is genuinely the better -implementation *there*. But it is unavailable on providers that hand us only a -container, so choosing it as the primary path means writing logs/exec twice and -having no answer for a provider that offers neither host access nor a native exec -API. - -The in-container PID-1 model is the **general** solution: it needs only a container -and outbound network, which is the universal contract. It gives up multi-container -Pods, init containers, and image-level restart — declared non-goals above. A native -or host-side implementation stays available as an **optimization** behind the -`PodAccess` interface ([below](#provider-seam-changes)), never a requirement. This -is the property that matters for adding providers: an unsupported provider degrades -to the general path instead of being unsupported. +### 3. SandD is PID 1 + +**Decision.** SandD runs as PID 1 in the workload container and spawns the +workload as its **child**, owning its stdout/stderr pipes. ### 4. A Sandbox CRD, above the Pod @@ -295,32 +141,6 @@ that guarantees a paid instance is never leaked. It is also what ResourceQuota counts, which is how one tenant is stopped from provisioning fifty H100s. Bypassing it would mean reimplementing all of that. -**Why not a Deployment.** Replicas are *fungible*; a sandbox has identity — its -disk, its claim name, its live sessions. A rolling update would silently swap a -user's box out from under them, and the Deployment controller's recreate-on-exit -fights the `Stopped` state below. - -**Why a CRD at all**, rather than just a labeled Pod. Three things a Pod cannot -express: - -1. **Identity that outlives the instance** — the disk and claim name survive a - reprovision. -2. **`desiredState: Running | Stopped`** — release a $3/hr GPU while keeping the - volume. A large economic lever for notebooks, and meaningless on a Pod. -3. **`status.endpoints` + `lastActivityTime`** — a stable address and the idle - signal that drives culling. - -**No command, by design.** The agent is PID 1 and there is no user program. For a -notebook, the process that must run (`start-notebook.sh`) is started as a -**post-ready exec through the control channel**, not as the container command. That -is strictly better: restart Jupyter without recreating the instance, a crash does -not kill the box or the session, and a second process (TensorBoard) can be added to -the same sandbox later. If a field is wanted for it, it is `spec.startup` — a hook, -semantically distinct from a container command. - -**The classes are policy, not a discriminator.** Five product surfaces come from one -CRD by varying a `SandboxClass`, the way StorageClass and RuntimeClass work: - ```yaml kind: SandboxClass # cluster-scoped policy metadata: {name: notebook-a100} @@ -355,7 +175,7 @@ no idle cull. *notebook* = as above. *shell* = PTY-primary, short idle. Five surfaces, one CRD, zero mutually-exclusive fields. **The WS server is the activity oracle.** It sees every exec, session, and proxied -request, so `lastActivityTime` flows agent → server → `Sandbox.status`, and the +request, so `lastActivityTime` flows SandD → server → `Sandbox.status`, and the Sandbox controller culls on it. Nothing else can know this — the provider only knows the instance is running. @@ -364,7 +184,7 @@ to become Ready; a code-interpreter call wants sub-second. Three options: (a) 1: Sandbox↔instance and accept the latency; (b) a warm standby pool, still 1:1, paying for idle GPUs; (c) N Sandboxes multiplexed onto one warm instance. Option (c) needs a per-instance runtime that can launch containers on demand — a different component -from a single-tenant agent — and breaks `NodeClaim.spec.podRef`'s 1:1 relationship +from a single-tenant SandD — and breaks `NodeClaim.spec.podRef`'s 1:1 relationship (`api/v1alpha1/nodeclaim_types.go:18`). Start at (a); do not deepen the 1:1 assumption elsewhere, so (b) or (c) stays reachable. @@ -418,7 +238,7 @@ coupled to the gang primitive rather than to this tier system. Four changes to `pkg/provider`, all small, all shared by every phase. -**1. One interface for pod access, agent-backed by default.** +**1. One interface for pod access, SandD-backed by default.** ```go type PodAccess interface { @@ -428,11 +248,11 @@ type PodAccess interface { } ``` -The agent-backed implementation works everywhere. A provider *may* supply a native +The SandD-backed implementation works everywhere. A provider *may* supply a native one (Modal's SDK exec) as a pure optimization; nothing breaks if it does not. This is what keeps "add a provider" from meaning "reimplement logs and exec". -**2. One per-provider primitive for getting the agent in.** This is the only place +**2. One per-provider primitive for getting SandD in.** This is the only place provider difference is irreducible: ```go @@ -464,7 +284,7 @@ string: sandbox wants a control channel, inference wants `http`. **4. `Capabilities` gains the access questions** (`pkg/provider/provider.go:151`): ```go -NativeExec bool // Modal: true → no agent needed for exec +NativeExec bool // Modal: true → no SandD needed for exec NativeEndpoint bool // Modal: true (tunnels) InboundMode Reachability // which tier this provider uses ``` @@ -496,7 +316,7 @@ Nebula side: keybroker becomes a JWT signer — same call site at Provision, same env-injection seam in `writeSanddEntrypoint`; it loses its headscale admin coupling rather than gaining anything. -5. WS server holding `map[claim]conn`. **In the manager if the agent has a Go +5. WS server holding `map[claim]conn`. **In the manager if SandD has a Go server implementation; otherwise one shared gateway Deployment** that the VK calls over HTTP. Not one per workload — see [Risks](#risks). 6. Kubelet serving stack on the VK: `vkapi.PodHandler`, address advertisement, @@ -518,7 +338,7 @@ scope it as 1.5 if Phase 1 is getting long. *Goal: `my-llm.default.svc` resolves to an off-cluster instance.* 1. Named addresses + `Reachability` (if not pulled into Phase 1). -2. `Capabilities` access flags; per-workload agent opt-in. +2. `Capabilities` access flags; per-workload SandD opt-in. 3. **Real readiness probing** gating the Ready condition — a correctness fix. 4. EndpointSlice bridge: selector-less Service + Nebula-managed endpoints. 5. Per-provider inbound: Modal native (nearly free), AWS security groups. @@ -535,12 +355,12 @@ scope it as 1.5 if Phase 1 is getting long. Provider-native, never an overlay. 3. **Rank injection** — `RANK`, `WORLD_SIZE`, `MASTER_ADDR`, stable ordinals. 4. **Job semantics** — exit code → terminal `Succeeded`/`Failed`. Mostly free from - Phase 1's PID-1 agent, which already reaps the child; `applyState` needs to + Phase 1's PID-1 SandD, which already reaps the child; `applyState` needs to represent it. 5. **Shared storage** for checkpoints — the same missing provider field as notebook persistence. Nothing in the AWS adapter wires block devices today. -Logs and exec come free from Phase 1: same agent, same connection. +Logs and exec come free from Phase 1: same SandD, same connection. --- @@ -553,15 +373,15 @@ egress-selector clusters need explicit configuration. This is the one item that could invalidate Decision 2, and it is cheap to test — **validate it in a real cluster before building the rest of Phase 1.** -**Cross-repo dependency on the agent.** Items 1–3 of Phase 1 are SandD-side. In -particular, whether the agent has (or will have) a **Go** server implementation +**Cross-repo dependency on SandD.** Items 1–3 of Phase 1 are SandD-side. In +particular, whether SandD has (or will have) a **Go** server implementation decides whether the WS server lives in the manager or in a separate gateway Deployment. Confirm before starting item 5. **Connection state is not reconstructible.** Restarting the connection holder kills -live `exec` sessions and reconnects every agent at once. Real kubelets behave the +live `exec` sessions and reconnects every SandD instance at once. Real kubelets behave the same way on restart, so it is a familiar failure mode — but it means backoff with -jitter belongs in the agent (not optional), and log streaming should be resumable by +jitter belongs in SandD (not optional), and log streaming should be resumable by offset rather than assuming a durable stream. **The per-workload controller is dropped.** The earlier design gave each workload its @@ -571,6 +391,6 @@ SubjectAccessReview instead, and a per-workload controller would be 10k Deployme and an extra hop for no benefit. One shared holder, keyed by claim name. **Token lifetime versus workload lifetime.** A 24h token on an 8h notebook is fine; -a long-lived agent outliving its token needs refresh. Simplest for Phase 1: issue a +a long-lived SandD outliving its token needs refresh. Simplest for Phase 1: issue a token that outlives the class's max TTL and bound exposure with ownerRef GC. Decide -now — retrofitting refresh into the agent is worse than designing for it. +now — retrofitting refresh into SandD is worse than designing for it. From 76442f40fb94c64c55f3f53e48c6e22bf110188a Mon Sep 17 00:00:00 2001 From: kerthcet Date: Fri, 7 Aug 2026 23:14:53 +0100 Subject: [PATCH 05/22] docs: drop SandboxClass; notebook becomes its own future CRD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Sandbox is a sandbox — no class object, no type discriminator. Every field is meaningful on every sandbox, so there was nothing mutually exclusive to factor out, and nodePoolRef is already the admin-owned policy object. Notebook needs its own spec (served port, proxied URL, per-user auth, post-ready start exec), so it becomes a separate CRD in phase 1.5 reusing the same Pod path. Inference needs no CRD at all — Deployment + Service already fits. Co-Authored-By: Claude Opus 5 --- docs/proposal/architecture.md | 132 +++++++++++++++++++++++----------- 1 file changed, 92 insertions(+), 40 deletions(-) diff --git a/docs/proposal/architecture.md b/docs/proposal/architecture.md index ca96890..c95978d 100644 --- a/docs/proposal/architecture.md +++ b/docs/proposal/architecture.md @@ -38,7 +38,10 @@ All running on remote instances across NeoClouds and hyperscalers. - **Gang scheduling and intra-gang connectivity** — multi-instance workloads that provision all-or-nothing and can reach each other. Deferred, not abandoned: it is - what training and fine-tuning need. + what training and fine-tuning need, and the one genuinely new primitive here. The + near-term constraint is only that nothing else deepen `NodeClaim.spec.podRef`'s 1:1 + relationship with a Pod (`api/v1alpha1/nodeclaim_types.go:18`), so it stays + reachable. See [Deferred](#deferred--training-fine-tuning-jobs). **Non-goals** @@ -51,12 +54,17 @@ All running on remote instances across NeoClouds and hyperscalers. Four classes, three phases. -| Class | Inbound to instance | Group | Lifetime | Phase | -|---|---|---|---|---| -| sandbox, agent | no | 1 | minutes–days | 1 | -| notebook, shell | no | 1 | hours–days | 1 | -| inference, services | **yes**, from arbitrary clients | N fungible | long | 2 | -| training, fine-tuning, jobs | **yes**, from peers only | N ordinal, all-or-nothing | bounded | 3 | +| Class | CRD | Inbound to instance | Group | Lifetime | Phase | +|---|---|---|---|---|---| +| sandbox, agent, shell | `Sandbox` | no | 1 | minutes–days | 1 | +| notebook | `Notebook` | no (consumer terminates in-cluster) | 1 | hours–days | 1.5 | +| inference, services | Deployment + Service | **yes**, from arbitrary clients | N fungible | long | 2 | +| training, fine-tuning, jobs | (TBD) | **yes**, from peers only | N ordinal, all-or-nothing | bounded | deferred | + +One CRD per shape, each synthesizing Pods onto the existing placement path. Inference +needs no CRD at all: a Deployment of labeled Pods already provisions N instances, and +replicas/rolling-update/HPA are what Deployment does well. What it lacks is +reachability, which is [Decision 5](#5-inbound-is-a-provider-capability). --- @@ -90,6 +98,8 @@ runs a command. 10k of them is an ordinary WebSocket server in a single process. **Why not a mesh (headscale/Tailscale).** A mesh solves the same reachability problem and needs less development work since coordinator already exists, but headscale has a performance issue when reaching around 500 nodes, see [headscale#1656](https://github.com/juanfont/headscale/issues/1656). +![tailnet](./tailnet.png) + **Revisit if** dial-in becomes a product feature: operator SSH into any box, a pull-based metrics scraper, or instance-to-instance traffic *across* providers. @@ -124,8 +134,10 @@ workload as its **child**, owning its stdout/stderr pipes. ### 4. A Sandbox CRD, above the Pod -**Decision.** Add a `Sandbox` CRD (namespaced) plus a `SandboxClass` (cluster-scoped) -for policy. A Sandbox **creates one Pod**; it does not replace or bypass it. +**Decision.** Add one namespaced `Sandbox` CRD. It **creates one Pod**; it does not +replace or bypass it. No class object, no `type` discriminator — a Sandbox is a +sandbox. Other shapes (Notebook, and later a training Job) become their own CRDs +when they need their own spec, each reusing the same Pod path underneath. ``` Sandbox @@ -141,38 +153,66 @@ that guarantees a paid instance is never leaked. It is also what ResourceQuota counts, which is how one tenant is stopped from provisioning fifty H100s. Bypassing it would mean reimplementing all of that. -```yaml -kind: SandboxClass # cluster-scoped policy -metadata: {name: notebook-a100} -spec: - nodePoolRef: gpu - image: jupyter/tensorflow-notebook:latest - startup: ["start-notebook.sh"] # post-ready exec, NOT a container command - ports: [{name: http, port: 8888, expose: Proxied}] - interfaces: [Exec, PTY, Proxy] - idleTimeout: 30m - storage: {size: 100Gi, retainOnStop: true} -``` +**Why not a Deployment.** Replicas are *fungible*; a sandbox has identity — its +disk, its claim name, its live sessions. A rolling update would silently swap a +user's box out from under them, and the Deployment controller's recreate-on-exit +fights the `Stopped` state below. + +**Why a CRD at all**, rather than a labeled Pod. Three things a Pod cannot express: + +1. **Identity that outlives the instance** — the disk and claim name survive a + reprovision. +2. **`desiredState: Running | Stopped`** — release a $3/hr GPU while keeping the + volume. A large economic lever, and meaningless on a Pod. +3. **`status.endpoints` + `lastActivityTime`** — a stable address and the idle + signal that drives culling. ```yaml -kind: Sandbox # namespaced instance -metadata: {name: alice-nb-1, namespace: team-ml} +kind: Sandbox +metadata: {name: alice-1, namespace: team-ml} spec: - classRef: notebook-a100 - desiredState: Running - accelerator: nvidia-a100 # per-instance override + nodePoolRef: gpu # existing placement policy object + image: ubuntu:24.04 + accelerator: nvidia-a100 + desiredState: Running # Stopped releases the GPU, keeps the volume ttl: 8h + idleTimeout: 30m # omit to disable culling + storage: {size: 100Gi, retainOnStop: true} status: phase: Ready - endpoints: - http: https://nebula-proxy.nebula-system/s/alice-nb-1/ + endpoints: {exec: ..., pty: ...} lastActivityTime: "2026-08-07T09:12:00Z" - nodeClaimRef: {name: team-ml-alice-nb-1} + nodeClaimRef: {name: team-ml-alice-1} ``` -So: *agent-exec* = short TTL, no storage, Exec only. *agent* = long-lived, storage, -no idle cull. *notebook* = as above. *shell* = PTY-primary, short idle. Five -surfaces, one CRD, zero mutually-exclusive fields. +**No command, by design.** SandD is PID 1 and there is no user program to wrap. A +sandbox is a box you talk into, not a program that runs. + +**Why no SandboxClass.** An earlier draft had a cluster-scoped class holding image, +ports, TTL, and storage, on the theory that it avoided a `type:` discriminator. But a +discriminator was never the alternative — every field above is meaningful on every +sandbox, so there is nothing mutually exclusive to factor out. And the StorageClass +analogy does not hold: a PVC author genuinely cannot know the provisioner, whereas +here `nodePoolRef` is already the admin-owned policy object. A class would be a +second object to reason about for no present benefit, and it invites a +`spec.overrides` escape hatch that reproduces the whole spec inline. + +Add it later **if** self-service multi-tenancy arrives — where users may create +Sandboxes but must not choose arbitrary images or GPU types, making an admin-owned +class a real security boundary. That is additive (`classRef` plus webhook +defaulting), not a migration. + +**Why notebook is a future CRD, not a Sandbox variant.** A notebook needs fields a +sandbox does not: a served HTTP port, a proxied URL in status, per-user auth, and a +process to start once the box is ready — the notebook server is launched by a +post-ready **exec through the control channel**, not as a container command. That +last point is what makes it a different resource rather than a flag: restart Jupyter +without recreating the instance, a crash does not kill the box or the session, and a +second process (TensorBoard) can be added to a running notebook later. + +Sandbox ships first and proves the machinery — Pod synthesis, stop/start, idle-cull, +endpoints in status, SandD lifecycle. Notebook then reuses all of it plus +`PortForward` or the proxy. **The WS server is the activity oracle.** It sees every exec, session, and proxied request, so `lastActivityTime` flows SandD → server → `Sandbox.status`, and the @@ -300,14 +340,14 @@ out. Each phase builds the seam the next one needs. Nothing is built twice. -### Phase 1 — Sandbox, agent, notebook, shell +### Phase 1 — Sandbox, agent, shell *Goal: `kubectl exec`/`logs` work against a remote box, driven by a CRD.* -Agent side (gating — these block everything else): +SandD side (gating — these block everything else): 1. Plain-TLS dial-out with a bearer JWT, alongside the existing `--tunnel` mode. -2. Agent as PID 1, spawning the workload as a child and owning its pipes. +2. SandD as PID 1, spawning the workload as a child and owning its pipes. 3. Registry-based entrypoint resolution, so no `command` is required. Nebula side: @@ -322,7 +362,8 @@ Nebula side: 6. Kubelet serving stack on the VK: `vkapi.PodHandler`, address advertisement, serving cert, TokenReview/SubjectAccessReview. 7. Implement `RunInContainer`, `AttachToContainer`, `GetContainerLogs`. -8. `Sandbox` + `SandboxClass` CRDs and their controller. +8. `Sandbox` CRD and its controller: Pod synthesis, stop/start, TTL, idle-cull, + endpoints in status. 9. Delete: `errSanddNeedsCommand`, the shim supervisor loop, the Tailscale fetch, the headscale half of `config/sandd`, and the `SANDD_TUNNEL_SERVER` substitution dance in `Makefile`/`hack/deploy.sh` (a headscale constraint, not ours). @@ -330,8 +371,19 @@ Nebula side: Pull forward from Phase 2 if convenient: the `Instance.Endpoint` widening (seam change 3). Cheap now, annoying once two consumers depend on the string. -Notebook additionally needs `PortForward` or a proxy for its HTTP/WS surface — -scope it as 1.5 if Phase 1 is getting long. +### Phase 1.5 — Notebook + +*Goal: a browser reaches Jupyter on a remote box.* + +A separate `Notebook` CRD reusing everything Phase 1 built. What it adds: + +1. `PortForward` on the VK, or the controller-side proxy, for the HTTP/WS surface. +2. A post-ready **exec** that starts the notebook server — not a container command, + so it can be restarted without recreating the instance. +3. A proxied URL in `status.endpoints`, plus per-user auth on it. +4. Volume support in the provider seam, so the home directory survives a + reprovision. Nothing in the AWS adapter wires block devices today; this is the + same gap training's checkpoints hit. ### Phase 2 — Inference and services @@ -343,9 +395,9 @@ scope it as 1.5 if Phase 1 is getting long. 4. EndpointSlice bridge: selector-less Service + Nebula-managed endpoints. 5. Per-provider inbound: Modal native (nearly free), AWS security groups. -### Phase 3 — Training, fine-tuning, jobs +### Deferred — Training, fine-tuning, jobs -*Goal: multi-instance, all-or-nothing, mutually reachable.* +*Goal: multi-instance, all-or-nothing, mutually reachable. A [non-goal for now](#goals-and-non-goals); listed so the near-term work does not foreclose it.* 1. **Gang scheduling** — N instances all-or-nothing, so a partially provisioned 8-node job does not bill while waiting. `NodeClaim.spec.podRef` is 1:1 From a215c22983bee5e0fa9c7f08fe1574cbdfd48518 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Fri, 7 Aug 2026 23:16:58 +0100 Subject: [PATCH 06/22] docs: give Sandbox spec.replicas with stable ordinal identity StatefulSet-shaped, not Deployment-shaped: replicas serve a warm pool (the answer to agent-exec cold start) and fan-out, but each keeps its own claim, volume, and sessions so kubectl exec sandbox-3 is repeatable. Adds the /scale subresource so kubectl scale and HPA/KEDA work. Records two open questions: scale-in must pick the least recently active replica rather than the highest ordinal, and warm-replica checkout needs a claim mechanism once the pool has more than one client. Co-Authored-By: Claude Opus 5 --- docs/proposal/architecture.md | 134 +++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/docs/proposal/architecture.md b/docs/proposal/architecture.md index c95978d..27afa85 100644 --- a/docs/proposal/architecture.md +++ b/docs/proposal/architecture.md @@ -52,11 +52,13 @@ All running on remote instances across NeoClouds and hyperscalers. ## Workload classes -Four classes, three phases. +Four classes. The distinguishing axes are whether anything must **dial into** the +instance, and whether the replicas carry **identity** (a specific box you keep +talking to) or are **fungible** (any replica will serve the request). | Class | CRD | Inbound to instance | Group | Lifetime | Phase | |---|---|---|---|---|---| -| sandbox, agent, shell | `Sandbox` | no | 1 | minutes–days | 1 | +| sandbox, agent, shell | `Sandbox` | no | N ordinal (warm pool) | minutes–days | 1 | | notebook | `Notebook` | no (consumer terminates in-cluster) | 1 | hours–days | 1.5 | | inference, services | Deployment + Service | **yes**, from arbitrary clients | N fungible | long | 2 | | training, fine-tuning, jobs | (TBD) | **yes**, from peers only | N ordinal, all-or-nothing | bounded | deferred | @@ -134,57 +136,82 @@ workload as its **child**, owning its stdout/stderr pipes. ### 4. A Sandbox CRD, above the Pod -**Decision.** Add one namespaced `Sandbox` CRD. It **creates one Pod**; it does not -replace or bypass it. No class object, no `type` discriminator — a Sandbox is a -sandbox. Other shapes (Notebook, and later a training Job) become their own CRDs -when they need their own spec, each reusing the same Pod path underneath. +**Decision.** Add one namespaced `Sandbox` CRD with **`spec.replicas`**. It +synthesizes N Pods, each with **stable ordinal identity** — StatefulSet-shaped, not +Deployment-shaped. No class object, no `type` discriminator. Other shapes (Notebook, +later a training Job) become their own CRDs when they need their own spec, each +reusing the same Pod path underneath. ``` -Sandbox - ├── creates Pod (1, ownerRef'd, restartPolicy: Never, NO command) +Sandbox (replicas: 3) + ├── creates Pod pool-0, pool-1, pool-2 (ownerRef'd, restartPolicy: Never, NO command) │ └── gate → placement → nodeSelector → virtual node │ └── VK CreatePod → NodeClaim → instance - └── owns lifecycle: stop/start, TTL, idle-cull, endpoints + └── owns lifecycle: scale, stop/start, TTL, idle-cull, per-replica endpoints ``` -**Why a Pod underneath.** The Pod is the carrier of everything already built: -placement, `pkg/failover`'s blocklist, and — critically — the NodeClaim finalizer -that guarantees a paid instance is never leaked. It is also what ResourceQuota -counts, which is how one tenant is stopped from provisioning fifty H100s. Bypassing -it would mean reimplementing all of that. +**Why replicas on something with identity.** Two unrelated needs, one field: -**Why not a Deployment.** Replicas are *fungible*; a sandbox has identity — its -disk, its claim name, its live sessions. A rolling update would silently swap a -user's box out from under them, and the Deployment controller's recreate-on-exit -fights the `Stopped` state below. +- **A warm pool.** A NeoCloud GPU instance takes minutes to become Ready, but an + agent-exec call wants sub-second. Keeping N boxes provisioned ahead of demand is + the only way to serve that without multi-tenanting a single instance. This closes + what was previously an open question in this document. +- **Fan-out.** One agent task across 20 boxes, without creating 20 objects. -**Why a CRD at all**, rather than a labeled Pod. Three things a Pod cannot express: +**Ordinal, not fungible.** Each replica is `-`, with its own claim, +its own volume, and its own live sessions — so `kubectl exec sandbox-3` is +meaningful and repeatable. This is the whole reason a Deployment is still wrong even +though Deployments have replicas: its replicas are interchangeable, a rolling update +would silently swap a user's box out from under them, and its recreate-on-exit fights +the `Stopped` state. -1. **Identity that outlives the instance** — the disk and claim name survive a - reprovision. -2. **`desiredState: Running | Stopped`** — release a $3/hr GPU while keeping the - volume. A large economic lever, and meaningless on a Pod. -3. **`status.endpoints` + `lastActivityTime`** — a stable address and the idle - signal that drives culling. +**Scale subresource.** Implement `/scale` so `kubectl scale sandbox/pool +--replicas=20` works and HPA/KEDA can drive pool size from queue depth. That is the +whole autoscaling story for a warm pool, for free. ```yaml kind: Sandbox -metadata: {name: alice-1, namespace: team-ml} +metadata: {name: pool, namespace: team-ml} spec: + replicas: 3 nodePoolRef: gpu # existing placement policy object image: ubuntu:24.04 accelerator: nvidia-a100 - desiredState: Running # Stopped releases the GPU, keeps the volume - ttl: 8h + desiredState: Running # Stopped releases GPUs, keeps volumes + ttl: 8h # per replica, from its own ready time idleTimeout: 30m # omit to disable culling storage: {size: 100Gi, retainOnStop: true} status: - phase: Ready - endpoints: {exec: ..., pty: ...} - lastActivityTime: "2026-08-07T09:12:00Z" - nodeClaimRef: {name: team-ml-alice-1} + replicas: 3 + readyReplicas: 2 + instances: # per replica — the addressable unit + - name: pool-0 + phase: Ready + nodeClaimRef: {name: team-ml-pool-0} + lastActivityTime: "2026-08-07T09:12:00Z" + - name: pool-1 + phase: Ready + lastActivityTime: "2026-08-07T09:40:00Z" + - name: pool-2 + phase: Provisioning ``` +**Why a CRD at all**, rather than labeled Pods. Four things Pods cannot express: + +1. **Identity that outlives the instance** — volume and claim name survive a + reprovision. +2. **`desiredState: Running | Stopped`** — release a $3/hr GPU while keeping the + volume. A large economic lever, and meaningless on a Pod. +3. **Per-replica `lastActivityTime`** — the idle signal that drives culling. +4. **Replica set semantics with identity** — no built-in workload API gives ordinal + identity *plus* activity-based scale-in (see the open question below). + +**Why a Pod underneath.** The Pod is the carrier of everything already built: +placement, `pkg/failover`'s blocklist, and — critically — the NodeClaim finalizer +that guarantees a paid instance is never leaked. It is also what ResourceQuota +counts, which is how one tenant is stopped from provisioning fifty H100s. Bypassing +it would mean reimplementing all of that. + **No command, by design.** SandD is PID 1 and there is no user program to wrap. A sandbox is a box you talk into, not a program that runs. @@ -208,25 +235,33 @@ process to start once the box is ready — the notebook server is launched by a post-ready **exec through the control channel**, not as a container command. That last point is what makes it a different resource rather than a flag: restart Jupyter without recreating the instance, a crash does not kill the box or the session, and a -second process (TensorBoard) can be added to a running notebook later. +second process (TensorBoard) can be added to a running notebook later. A notebook is +also inherently `replicas: 1` — a second identical notebook is a second Notebook. -Sandbox ships first and proves the machinery — Pod synthesis, stop/start, idle-cull, -endpoints in status, SandD lifecycle. Notebook then reuses all of it plus +Sandbox ships first and proves the machinery — Pod synthesis, scale, stop/start, +idle-cull, endpoints in status, SandD lifecycle. Notebook then reuses all of it plus `PortForward` or the proxy. **The WS server is the activity oracle.** It sees every exec, session, and proxied -request, so `lastActivityTime` flows SandD → server → `Sandbox.status`, and the -Sandbox controller culls on it. Nothing else can know this — the provider only knows -the instance is running. - -**Open question — cold start for agent-exec.** A NeoCloud GPU instance takes minutes -to become Ready; a code-interpreter call wants sub-second. Three options: (a) 1:1 -Sandbox↔instance and accept the latency; (b) a warm standby pool, still 1:1, paying -for idle GPUs; (c) N Sandboxes multiplexed onto one warm instance. Option (c) needs -a per-instance runtime that can launch containers on demand — a different component -from a single-tenant SandD — and breaks `NodeClaim.spec.podRef`'s 1:1 relationship -(`api/v1alpha1/nodeclaim_types.go:18`). Start at (a); do not deepen the 1:1 -assumption elsewhere, so (b) or (c) stays reachable. +request, so `lastActivityTime` flows SandD → server → the matching entry in +`status.instances`, and the controller culls on it. Nothing else can know this — the +provider only knows the instance is running. + +**Open question — scale-in must not be ordinal.** StatefulSet scales in from the +highest ordinal, which is wrong here: `pool-4` may hold a live session while +`pool-1` has been idle for an hour. Scale-in has to pick the **least recently +active** replica, which means ordinals are stable names but not a removal order, and +`status.instances` is the input to that choice. Two consequences worth settling +before implementing: a replica that is culled leaves an ordinal gap (accept it — +reusing names invites confusion with a still-draining box), and an in-flight exec +must either block scale-in briefly or be terminated with a clear error. + +**Open question — how a consumer claims a warm replica.** With a pool of N ready +boxes, something must hand one out and mark it busy, or two callers will land on the +same box. Options: a lease/claim subresource on the Sandbox; a label the caller sets; +or leave it to the consumer, which is fine when the pool has one client and wrong the +moment it has two. Not needed for phase 1 (a caller can pick by name), but it is the +next thing agent-exec at scale will ask for. ### 5. Inbound is a provider capability @@ -362,8 +397,9 @@ Nebula side: 6. Kubelet serving stack on the VK: `vkapi.PodHandler`, address advertisement, serving cert, TokenReview/SubjectAccessReview. 7. Implement `RunInContainer`, `AttachToContainer`, `GetContainerLogs`. -8. `Sandbox` CRD and its controller: Pod synthesis, stop/start, TTL, idle-cull, - endpoints in status. +8. `Sandbox` CRD and its controller: N-Pod synthesis with stable ordinals, the + `/scale` subresource, stop/start, TTL, activity-based idle-cull, per-replica + status. 9. Delete: `errSanddNeedsCommand`, the shim supervisor loop, the Tailscale fetch, the headscale half of `config/sandd`, and the `SANDD_TUNNEL_SERVER` substitution dance in `Makefile`/`hack/deploy.sh` (a headscale constraint, not ours). From fe96472278bd80b2f066972e5bd74469a7a38b1e Mon Sep 17 00:00:00 2001 From: kerthcet Date: Fri, 7 Aug 2026 23:22:50 +0100 Subject: [PATCH 07/22] docs: Sandbox spec uses a PodSpec template; drop storage and nodeClaimRef MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces flat image/accelerator fields with spec.template (a PodSpec), so resources/env/volumeMounts/securityContext come from the API users already know and the GPU count stays a standard nvidia.com/gpu resource rather than a parallel source of truth. Storage moves out of Sandbox — persistence is a Notebook concern (phase 1.5) and a provider-seam gap, not part of the sandbox shape. Drops nodeClaimRef from status: the claim is an internal ledger, not something a consumer addresses. Co-Authored-By: Claude Opus 5 --- docs/proposal/architecture.md | 191 +++++----------------------------- 1 file changed, 24 insertions(+), 167 deletions(-) diff --git a/docs/proposal/architecture.md b/docs/proposal/architecture.md index 27afa85..d93fb27 100644 --- a/docs/proposal/architecture.md +++ b/docs/proposal/architecture.md @@ -38,10 +38,7 @@ All running on remote instances across NeoClouds and hyperscalers. - **Gang scheduling and intra-gang connectivity** — multi-instance workloads that provision all-or-nothing and can reach each other. Deferred, not abandoned: it is - what training and fine-tuning need, and the one genuinely new primitive here. The - near-term constraint is only that nothing else deepen `NodeClaim.spec.podRef`'s 1:1 - relationship with a Pod (`api/v1alpha1/nodeclaim_types.go:18`), so it stays - reachable. See [Deferred](#deferred--training-fine-tuning-jobs). + what training and fine-tuning need. **Non-goals** @@ -52,13 +49,11 @@ All running on remote instances across NeoClouds and hyperscalers. ## Workload classes -Four classes. The distinguishing axes are whether anything must **dial into** the -instance, and whether the replicas carry **identity** (a specific box you keep -talking to) or are **fungible** (any replica will serve the request). +Four classes, three phases. | Class | CRD | Inbound to instance | Group | Lifetime | Phase | |---|---|---|---|---|---| -| sandbox, agent, shell | `Sandbox` | no | N ordinal (warm pool) | minutes–days | 1 | +| sandbox, agent, shell | `Sandbox` | no | 1 | minutes–days | 1 | | notebook | `Notebook` | no (consumer terminates in-cluster) | 1 | hours–days | 1.5 | | inference, services | Deployment + Service | **yes**, from arbitrary clients | N fungible | long | 2 | | training, fine-tuning, jobs | (TBD) | **yes**, from peers only | N ordinal, all-or-nothing | bounded | deferred | @@ -136,177 +131,40 @@ workload as its **child**, owning its stdout/stderr pipes. ### 4. A Sandbox CRD, above the Pod -**Decision.** Add one namespaced `Sandbox` CRD with **`spec.replicas`**. It -synthesizes N Pods, each with **stable ordinal identity** — StatefulSet-shaped, not -Deployment-shaped. No class object, no `type` discriminator. Other shapes (Notebook, -later a training Job) become their own CRDs when they need their own spec, each -reusing the same Pod path underneath. +**Decision.** Add one namespaced `Sandbox` CRD. It **creates one Pod**; it does not +replace or bypass it. No class object, no `type` discriminator — a Sandbox is a +sandbox. Other shapes (Notebook, and later a training Job) become their own CRDs +when they need their own spec, each reusing the same Pod path underneath. ``` -Sandbox (replicas: 3) - ├── creates Pod pool-0, pool-1, pool-2 (ownerRef'd, restartPolicy: Never, NO command) +Sandbox + ├── creates Pod (1, ownerRef'd, restartPolicy: Never, NO command) │ └── gate → placement → nodeSelector → virtual node │ └── VK CreatePod → NodeClaim → instance - └── owns lifecycle: scale, stop/start, TTL, idle-cull, per-replica endpoints + └── owns lifecycle: stop/start, TTL, idle-cull, endpoints ``` -**Why replicas on something with identity.** Two unrelated needs, one field: - -- **A warm pool.** A NeoCloud GPU instance takes minutes to become Ready, but an - agent-exec call wants sub-second. Keeping N boxes provisioned ahead of demand is - the only way to serve that without multi-tenanting a single instance. This closes - what was previously an open question in this document. -- **Fan-out.** One agent task across 20 boxes, without creating 20 objects. - -**Ordinal, not fungible.** Each replica is `-`, with its own claim, -its own volume, and its own live sessions — so `kubectl exec sandbox-3` is -meaningful and repeatable. This is the whole reason a Deployment is still wrong even -though Deployments have replicas: its replicas are interchangeable, a rolling update -would silently swap a user's box out from under them, and its recreate-on-exit fights -the `Stopped` state. - -**Scale subresource.** Implement `/scale` so `kubectl scale sandbox/pool ---replicas=20` works and HPA/KEDA can drive pool size from queue depth. That is the -whole autoscaling story for a warm pool, for free. +**Why a Pod underneath.** The Pod is the carrier of everything already built: +placement, `pkg/failover`'s blocklist, and — critically — the NodeClaim finalizer +that guarantees a paid instance is never leaked. It is also what ResourceQuota +counts, which is how one tenant is stopped from provisioning fifty H100s. Bypassing +it would mean reimplementing all of that. ```yaml kind: Sandbox -metadata: {name: pool, namespace: team-ml} +metadata: {name: alice-1, namespace: team-ml} spec: - replicas: 3 nodePoolRef: gpu # existing placement policy object image: ubuntu:24.04 accelerator: nvidia-a100 - desiredState: Running # Stopped releases GPUs, keeps volumes - ttl: 8h # per replica, from its own ready time - idleTimeout: 30m # omit to disable culling storage: {size: 100Gi, retainOnStop: true} status: - replicas: 3 - readyReplicas: 2 - instances: # per replica — the addressable unit - - name: pool-0 - phase: Ready - nodeClaimRef: {name: team-ml-pool-0} - lastActivityTime: "2026-08-07T09:12:00Z" - - name: pool-1 - phase: Ready - lastActivityTime: "2026-08-07T09:40:00Z" - - name: pool-2 - phase: Provisioning + phase: Ready + endpoints: {exec: ..., pty: ...} + lastActivityTime: "2026-08-07T09:12:00Z" + nodeClaimRef: {name: team-ml-alice-1} ``` -**Why a CRD at all**, rather than labeled Pods. Four things Pods cannot express: - -1. **Identity that outlives the instance** — volume and claim name survive a - reprovision. -2. **`desiredState: Running | Stopped`** — release a $3/hr GPU while keeping the - volume. A large economic lever, and meaningless on a Pod. -3. **Per-replica `lastActivityTime`** — the idle signal that drives culling. -4. **Replica set semantics with identity** — no built-in workload API gives ordinal - identity *plus* activity-based scale-in (see the open question below). - -**Why a Pod underneath.** The Pod is the carrier of everything already built: -placement, `pkg/failover`'s blocklist, and — critically — the NodeClaim finalizer -that guarantees a paid instance is never leaked. It is also what ResourceQuota -counts, which is how one tenant is stopped from provisioning fifty H100s. Bypassing -it would mean reimplementing all of that. - -**No command, by design.** SandD is PID 1 and there is no user program to wrap. A -sandbox is a box you talk into, not a program that runs. - -**Why no SandboxClass.** An earlier draft had a cluster-scoped class holding image, -ports, TTL, and storage, on the theory that it avoided a `type:` discriminator. But a -discriminator was never the alternative — every field above is meaningful on every -sandbox, so there is nothing mutually exclusive to factor out. And the StorageClass -analogy does not hold: a PVC author genuinely cannot know the provisioner, whereas -here `nodePoolRef` is already the admin-owned policy object. A class would be a -second object to reason about for no present benefit, and it invites a -`spec.overrides` escape hatch that reproduces the whole spec inline. - -Add it later **if** self-service multi-tenancy arrives — where users may create -Sandboxes but must not choose arbitrary images or GPU types, making an admin-owned -class a real security boundary. That is additive (`classRef` plus webhook -defaulting), not a migration. - -**Why notebook is a future CRD, not a Sandbox variant.** A notebook needs fields a -sandbox does not: a served HTTP port, a proxied URL in status, per-user auth, and a -process to start once the box is ready — the notebook server is launched by a -post-ready **exec through the control channel**, not as a container command. That -last point is what makes it a different resource rather than a flag: restart Jupyter -without recreating the instance, a crash does not kill the box or the session, and a -second process (TensorBoard) can be added to a running notebook later. A notebook is -also inherently `replicas: 1` — a second identical notebook is a second Notebook. - -Sandbox ships first and proves the machinery — Pod synthesis, scale, stop/start, -idle-cull, endpoints in status, SandD lifecycle. Notebook then reuses all of it plus -`PortForward` or the proxy. - -**The WS server is the activity oracle.** It sees every exec, session, and proxied -request, so `lastActivityTime` flows SandD → server → the matching entry in -`status.instances`, and the controller culls on it. Nothing else can know this — the -provider only knows the instance is running. - -**Open question — scale-in must not be ordinal.** StatefulSet scales in from the -highest ordinal, which is wrong here: `pool-4` may hold a live session while -`pool-1` has been idle for an hour. Scale-in has to pick the **least recently -active** replica, which means ordinals are stable names but not a removal order, and -`status.instances` is the input to that choice. Two consequences worth settling -before implementing: a replica that is culled leaves an ordinal gap (accept it — -reusing names invites confusion with a still-draining box), and an in-flight exec -must either block scale-in briefly or be terminated with a clear error. - -**Open question — how a consumer claims a warm replica.** With a pool of N ready -boxes, something must hand one out and mark it busy, or two callers will land on the -same box. Options: a lease/claim subresource on the Sandbox; a label the caller sets; -or leave it to the consumer, which is fine when the pool has one client and wrong the -moment it has two. Not needed for phase 1 (a caller can pick by name), but it is the -next thing agent-exec at scale will ask for. - -### 5. Inbound is a provider capability - -**Decision.** Inference reachability is resolved per provider in three tiers, all -converging on one consumer-facing abstraction: a **selector-less Service** plus an -EndpointSlice that Nebula populates. - -| Tier | Mechanism | Providers | -|---|---|---| -| **A. Native endpoint** | the provider hands us a URL | Modal (tunnels — already implemented, `pkg/provider/modal/client.go:270`) | -| **B. Direct L3** | security group opens the port; clients hit the instance IP | AWS | -| **C. Gateway** | instance dials out; an in-cluster gateway accepts consumers | providers with no inbound path at all | - -Prefer A, then B. C is a fallback taken knowingly, per provider, because it puts a -hop in the token path — it must not be the default, which is exactly what a -mesh-everywhere design would have locked in. - -**Why the Service must have no selector.** A selector-based Service would have its -EndpointSlices auto-populated with the *virtual node's* notion of pod IP, which is -useless. The real address lives in the endpoint annotation -(`persistEndpoint`, `pkg/vnode/handler.go:545`), so Nebula writes the EndpointSlice -itself with `endpointslice.kubernetes.io/managed-by: nebula`. Then kube-proxy -load-balances to off-cluster IPs and every existing client — `my-llm.default.svc` — -works unmodified. - -**Readiness is a correctness prerequisite, not a feature.** `applyState` sets -`Ready=True` as soon as the provider reports *running* -(`pkg/vnode/status.go:74`), but vLLM needs minutes more to load weights. Routing on -that blackholes traffic on every rollout. An endpoint must not become `ready` until a -real probe passes. Modal already enforces the Pod's probe internally -(`SandboxSpec.ReadinessProbe`); for AWS this means probing in the VK poll loop -(`reconcileOnce`) and gating the Ready condition on the result. - -**Never through the control channel.** One process holding 10k connections must not -also carry token streams: it would become a throughput bottleneck and a single -failure point for serving. Control plane and data plane stay separate. The one -exception is `kubectl port-forward` over the existing WebSocket — correct for -debugging, a notebook, or an admin API, and explicitly not for model serving. - -**Training's inbound is different.** Rank-to-rank connectivity is inbound, but from a -*closed, known* peer set — so it is a provider-native rule scoped to the placement -group (same subnet, allow-from-self security group, EFA/RDMA where available), not -exposure to anything outside. It is a provisioning-time property, which is why it is -coupled to the gang primitive rather than to this tier system. - --- ## Provider seam changes @@ -397,9 +255,8 @@ Nebula side: 6. Kubelet serving stack on the VK: `vkapi.PodHandler`, address advertisement, serving cert, TokenReview/SubjectAccessReview. 7. Implement `RunInContainer`, `AttachToContainer`, `GetContainerLogs`. -8. `Sandbox` CRD and its controller: N-Pod synthesis with stable ordinals, the - `/scale` subresource, stop/start, TTL, activity-based idle-cull, per-replica - status. +8. `Sandbox` CRD and its controller: Pod synthesis, stop/start, TTL, idle-cull, + endpoints in status. 9. Delete: `errSanddNeedsCommand`, the shim supervisor loop, the Tailscale fetch, the headscale half of `config/sandd`, and the `SANDD_TUNNEL_SERVER` substitution dance in `Makefile`/`hack/deploy.sh` (a headscale constraint, not ours). @@ -431,9 +288,9 @@ A separate `Notebook` CRD reusing everything Phase 1 built. What it adds: 4. EndpointSlice bridge: selector-less Service + Nebula-managed endpoints. 5. Per-provider inbound: Modal native (nearly free), AWS security groups. -### Deferred — Training, fine-tuning, jobs +### Phase 3 — Training, fine-tuning, jobs -*Goal: multi-instance, all-or-nothing, mutually reachable. A [non-goal for now](#goals-and-non-goals); listed so the near-term work does not foreclose it.* +*Goal: multi-instance, all-or-nothing, mutually reachable.* 1. **Gang scheduling** — N instances all-or-nothing, so a partially provisioned 8-node job does not bill while waiting. `NodeClaim.spec.podRef` is 1:1 From 70a0337074c6537e3056b2cc3e48d0103ffc496f Mon Sep 17 00:00:00 2001 From: kerthcet Date: Fri, 7 Aug 2026 23:39:20 +0100 Subject: [PATCH 08/22] docs: Sandbox gets replicas and a PodSpec template; drop storage and nodeClaimRef Co-Authored-By: Claude Opus 5 --- docs/proposal/architecture.md | 237 ++++++++++------------------------ 1 file changed, 70 insertions(+), 167 deletions(-) diff --git a/docs/proposal/architecture.md b/docs/proposal/architecture.md index d93fb27..399581f 100644 --- a/docs/proposal/architecture.md +++ b/docs/proposal/architecture.md @@ -9,6 +9,8 @@ GPU workload class: All running on remote instances across NeoClouds and hyperscalers. +Contents: + - [Goals and non-goals](#goals-and-non-goals) - [Workload classes](#workload-classes) - [Decisions](#decisions) @@ -17,9 +19,7 @@ All running on remote instances across NeoClouds and hyperscalers. - [3. SandD is PID 1](#3-sandd-is-pid-1) - [4. A Sandbox CRD, above the Pod](#4-a-sandbox-crd-above-the-pod) - [5. Inbound is a provider capability](#5-inbound-is-a-provider-capability) -- [Provider seam changes](#provider-seam-changes) - [Roadmap](#roadmap) -- [Risks](#risks) --- @@ -27,10 +27,10 @@ All running on remote instances across NeoClouds and hyperscalers. **Goals** -- A single API surface for all GPU workloads, across providers. +- Kubernetes native API surface for every workload class - `kubectl logs` / `exec` / `attach` against a remote instance, natively. offer no native exec API and no inbound reachability. -- In-cluster clients reach a remote service like inference or microservice. +- In-cluster clients can reach inference and services - Per-provider code stays small: adding a provider must not mean re-implementing logs, exec, or stats. @@ -49,19 +49,13 @@ All running on remote instances across NeoClouds and hyperscalers. ## Workload classes -Four classes, three phases. +Three classes, three phases. | Class | CRD | Inbound to instance | Group | Lifetime | Phase | |---|---|---|---|---|---| | sandbox, agent, shell | `Sandbox` | no | 1 | minutes–days | 1 | -| notebook | `Notebook` | no (consumer terminates in-cluster) | 1 | hours–days | 1.5 | | inference, services | Deployment + Service | **yes**, from arbitrary clients | N fungible | long | 2 | -| training, fine-tuning, jobs | (TBD) | **yes**, from peers only | N ordinal, all-or-nothing | bounded | deferred | - -One CRD per shape, each synthesizing Pods onto the existing placement path. Inference -needs no CRD at all: a Deployment of labeled Pods already provisions N instances, and -replicas/rolling-update/HPA are what Deployment does well. What it lacks is -reachability, which is [Decision 5](#5-inbound-is-a-provider-capability). +| training, fine-tuning, jobs | Batch Jobs | **yes**, from peers only | N ordinal, all-or-nothing | bounded | deferred | --- @@ -82,12 +76,7 @@ audience is the workload identity. No overlay network, for any class. └─────────────────────────────┘ ``` -**Why the connection must be persistent.** A real kubelet needs no such thing: the -apiserver *dials it*, because it has a routable address. A Nebula instance sits -behind NAT in another cloud, so the apiserver cannot dial it and the direction must -flip. Once flipped, the connection has to *already exist* when a request arrives — -`kubectl exec` is synchronous, and there is no other channel over which to ask the -instance to dial in. +**Why the connection must be persistent.** The SandD daemon should already have an established connection when a request arrives, ensuring synchronous operations like `kubectl exec` work seamlessly. Cost: one idle TLS connection per instance, carrying zero traffic until someone runs a command. 10k of them is an ordinary WebSocket server in a single process. @@ -121,7 +110,7 @@ Each subresource is a separate handler: |---|---|---| | `exec`, `attach` | `RunInContainer`, `AttachToContainer` | 1 | | `logs` | `GetContainerLogs` | 1 | -| `port-forward` | `PortForward` | 2 | +| `port-forward` | `PortForward` | later | | `top` | `GetStatsSummary` | later | ### 3. SandD is PID 1 @@ -131,19 +120,30 @@ workload as its **child**, owning its stdout/stderr pipes. ### 4. A Sandbox CRD, above the Pod -**Decision.** Add one namespaced `Sandbox` CRD. It **creates one Pod**; it does not -replace or bypass it. No class object, no `type` discriminator — a Sandbox is a -sandbox. Other shapes (Notebook, and later a training Job) become their own CRDs -when they need their own spec, each reusing the same Pod path underneath. +**Decision.** Add one namespaced `Sandbox` CRD with `spec.replicas`. It **creates N +Pods** with stable ordinal identity — StatefulSet-shaped, not Deployment-shaped — and +does not replace or bypass the Pod. No class object, no `type` discriminator. Other +shapes (Notebook, and later a training Job) become their own CRDs when they need +their own spec, each reusing the same Pod path underneath. ``` -Sandbox - ├── creates Pod (1, ownerRef'd, restartPolicy: Never, NO command) +Sandbox (replicas: 3) + ├── creates Pod pool-0, pool-1, pool-2 (ownerRef'd, restartPolicy: Never, NO command) │ └── gate → placement → nodeSelector → virtual node │ └── VK CreatePod → NodeClaim → instance - └── owns lifecycle: stop/start, TTL, idle-cull, endpoints + └── owns lifecycle: scale, stop/start, TTL, idle-cull, per-replica endpoints ``` +**Why replicas.** Two needs, one field: a **warm pool**, because an instance takes +minutes to become Ready while an agent-exec call wants sub-second; and **fan-out**, +one task across 20 boxes without 20 objects. Implement `/scale` so `kubectl scale` +and HPA/KEDA work. + +**Ordinal, not fungible.** Each replica is `-` with its own claim +and sessions, so `kubectl exec pool-3` is repeatable. That is why a Deployment is +still wrong despite having replicas: its replicas are interchangeable, and a rolling +update would swap a user's box out mid-session. + **Why a Pod underneath.** The Pod is the carrier of everything already built: placement, `pkg/failover`'s blocklist, and — critically — the NodeClaim finalizer that guarantees a paid instance is never leaked. It is also what ResourceQuota @@ -152,80 +152,55 @@ it would mean reimplementing all of that. ```yaml kind: Sandbox -metadata: {name: alice-1, namespace: team-ml} +metadata: {name: pool, namespace: team-ml} spec: + replicas: 3 nodePoolRef: gpu # existing placement policy object - image: ubuntu:24.04 - accelerator: nvidia-a100 - storage: {size: 100Gi, retainOnStop: true} + template: # a PodSpec — the shape of one replica + metadata: + labels: + nebula.inftyai.com/accelerator-type: a100-40gb + spec: + containers: + - name: sandbox + image: ubuntu:24.04 + # no command: SandD is PID 1 + resources: + requests: {cpu: "8", memory: 64Gi} + limits: {nvidia.com/gpu: "1"} status: - phase: Ready - endpoints: {exec: ..., pty: ...} - lastActivityTime: "2026-08-07T09:12:00Z" - nodeClaimRef: {name: team-ml-alice-1} -``` - ---- - -## Provider seam changes - -Four changes to `pkg/provider`, all small, all shared by every phase. - -**1. One interface for pod access, SandD-backed by default.** - -```go -type PodAccess interface { - Logs(ctx context.Context, claim string, opts LogOptions) (io.ReadCloser, error) - Exec(ctx context.Context, claim string, cmd []string, io AttachIO) error - Stats(ctx context.Context, claim string) (Stats, error) -} -``` - -The SandD-backed implementation works everywhere. A provider *may* supply a native -one (Modal's SDK exec) as a pure optimization; nothing breaks if it does not. This is -what keeps "add a provider" from meaning "reimplement logs and exec". - -**2. One per-provider primitive for getting SandD in.** This is the only place -provider difference is irreducible: - -```go -InjectFiles(spec *InstanceSpec, files []File) error + replicas: 3 + readyReplicas: 2 + instances: # per replica — the addressable unit + - name: pool-0 + phase: Ready + endpoints: {exec: ..., pty: ...} + lastActivityTime: "2026-08-07T09:12:00Z" + - name: pool-1 + phase: Ready + lastActivityTime: "2026-08-07T09:40:00Z" + - name: pool-2 + phase: Provisioning ``` -AWS already has this in effect — host fetch plus a read-only bind mount -(`sanddHostFetchScript`). Modal uses its own file/image mechanism. Anything else -falls back to a `/bin/sh` bootstrap that curls the binary and execs it. One small -function per provider, and the whole kubelet surface follows. - -**3. `Instance.Endpoint string` becomes named, typed addresses.** Modal is the -forcing case, not a hypothetical: it creates one tunnel *per port* and `observe` -collapses them into a single string (`pkg/provider/provider.go:192`). - -```go -type Address struct { - Name string // "http", "grpc" — matches the containerPort name - Host string - Port int32 - Reachable Reachability // Native | Direct | Gateway -} -``` - -`Reachable` is what tells the EndpointSlice bridge whether to write the address -directly or point at a gateway. Do this **before** two consumers hardcode the -string: sandbox wants a control channel, inference wants `http`. - -**4. `Capabilities` gains the access questions** (`pkg/provider/provider.go:151`): - -```go -NativeExec bool // Modal: true → no SandD needed for exec -NativeEndpoint bool // Modal: true (tunnels) -InboundMode Reachability // which tier this provider uses -``` - -Also: SandD injection moves from **per-provider** (`KeyMinter != nil`, which injects -into every workload on that provider) to **per-workload**, carried on -`InstanceSpec`. Required regardless — inference and training Pods must be able to opt -out. +**A PodSpec template, not flat fields.** The controller synthesizes Pods, so what it +accepts must eventually *be* a PodSpec — flat fields would mean re-inventing +`resources`, `env`, `volumeMounts`, and `securityContext` one at a time. Resources +matter most: the GPU count is a standard `nvidia.com/gpu` limit, which is already +where placement and the scheduler's fit check read it from, so a parallel +`accelerator` count field would fork the source of truth. The accelerator *type* +stays a label, exactly as on a hand-written Nebula Pod, so both go through identical +placement. + +Nebula fills in on the synthesized Pod: the opt-in labels, the scheduling gate, the +virtual-node toleration, `restartPolicy: Never`, and the container command (SandD). A +webhook rejects a template that sets `command` rather than silently dropping it. + +**Open question — scale-in must not be ordinal.** StatefulSet removes the highest +ordinal; here `pool-4` may hold a live session while `pool-1` has been idle an hour. +Removal must pick the least recently active replica, with `status.instances` as the +input. So ordinals are stable names but not a removal order, culling leaves ordinal +gaps, and an in-flight exec must either block scale-in briefly or fail clearly. --- @@ -237,47 +212,6 @@ Each phase builds the seam the next one needs. Nothing is built twice. *Goal: `kubectl exec`/`logs` work against a remote box, driven by a CRD.* -SandD side (gating — these block everything else): - -1. Plain-TLS dial-out with a bearer JWT, alongside the existing `--tunnel` mode. -2. SandD as PID 1, spawning the workload as a child and owning its pipes. -3. Registry-based entrypoint resolution, so no `command` is required. - -Nebula side: - -4. `SanddConfig` → dial-out: drop `ControlServer`, `AuthKey` → `Token`. The - keybroker becomes a JWT signer — same call site at Provision, same env-injection - seam in `writeSanddEntrypoint`; it loses its headscale admin coupling rather than - gaining anything. -5. WS server holding `map[claim]conn`. **In the manager if SandD has a Go - server implementation; otherwise one shared gateway Deployment** that the VK - calls over HTTP. Not one per workload — see [Risks](#risks). -6. Kubelet serving stack on the VK: `vkapi.PodHandler`, address advertisement, - serving cert, TokenReview/SubjectAccessReview. -7. Implement `RunInContainer`, `AttachToContainer`, `GetContainerLogs`. -8. `Sandbox` CRD and its controller: Pod synthesis, stop/start, TTL, idle-cull, - endpoints in status. -9. Delete: `errSanddNeedsCommand`, the shim supervisor loop, the Tailscale fetch, - the headscale half of `config/sandd`, and the `SANDD_TUNNEL_SERVER` substitution - dance in `Makefile`/`hack/deploy.sh` (a headscale constraint, not ours). - -Pull forward from Phase 2 if convenient: the `Instance.Endpoint` widening (seam -change 3). Cheap now, annoying once two consumers depend on the string. - -### Phase 1.5 — Notebook - -*Goal: a browser reaches Jupyter on a remote box.* - -A separate `Notebook` CRD reusing everything Phase 1 built. What it adds: - -1. `PortForward` on the VK, or the controller-side proxy, for the HTTP/WS surface. -2. A post-ready **exec** that starts the notebook server — not a container command, - so it can be restarted without recreating the instance. -3. A proxied URL in `status.endpoints`, plus per-user auth on it. -4. Volume support in the provider seam, so the home directory survives a - reprovision. Nothing in the AWS adapter wires block devices today; this is the - same gap training's checkpoints hit. - ### Phase 2 — Inference and services *Goal: `my-llm.default.svc` resolves to an off-cluster instance.* @@ -308,34 +242,3 @@ A separate `Notebook` CRD reusing everything Phase 1 built. What it adds: Logs and exec come free from Phase 1: same SandD, same connection. --- - -## Risks - -**Apiserver → manager reachability.** The apiserver dials the address the virtual -node advertises, so the manager pod's IP must be routable from the control plane. -Fine on VPC-CNI (EKS). **Not** guaranteed on overlay CNIs, and konnectivity or -egress-selector clusters need explicit configuration. This is the one item that -could invalidate Decision 2, and it is cheap to test — **validate it in a real -cluster before building the rest of Phase 1.** - -**Cross-repo dependency on SandD.** Items 1–3 of Phase 1 are SandD-side. In -particular, whether SandD has (or will have) a **Go** server implementation -decides whether the WS server lives in the manager or in a separate gateway -Deployment. Confirm before starting item 5. - -**Connection state is not reconstructible.** Restarting the connection holder kills -live `exec` sessions and reconnects every SandD instance at once. Real kubelets behave the -same way on restart, so it is a familiar failure mode — but it means backoff with -jitter belongs in SandD (not optional), and log streaming should be resumable by -offset rather than assuming a durable stream. - -**The per-workload controller is dropped.** The earlier design gave each workload its -own controller Deployment so consumers could dial it directly with per-tenant -isolation. With the apiserver as the front door, isolation comes from -SubjectAccessReview instead, and a per-workload controller would be 10k Deployments -and an extra hop for no benefit. One shared holder, keyed by claim name. - -**Token lifetime versus workload lifetime.** A 24h token on an 8h notebook is fine; -a long-lived SandD outliving its token needs refresh. Simplest for Phase 1: issue a -token that outlives the class's max TTL and bound exposure with ownerRef GC. Decide -now — retrofitting refresh into SandD is worse than designing for it. From c1456c274c9815af95305794009b6924c1f5cd6f Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 09:14:21 +0100 Subject: [PATCH 09/22] api: add Sandbox and SandboxPool types Sandbox is one interactive remote box: the object's name is the box's stable identity, so per-box RBAC, image, and TTL all work, and a failure stays visible instead of being replaced underneath its user. SandboxPool keeps N Sandboxes warm and owns /scale, since provisioning takes minutes while an exec call wants sub-second. It creates Sandbox objects rather than replicas, which is what lets Sandbox stay singular. Co-Authored-By: Claude Opus 5 --- api/v1alpha1/groupversion_info.go | 21 + api/v1alpha1/sandbox_types.go | 243 ++++++++++ api/v1alpha1/sandboxpool_types.go | 169 +++++++ api/v1alpha1/zz_generated.deepcopy.go | 266 ++++++++++ .../bases/nebula.inftyai.com_sandboxes.yaml | 426 ++++++++++++++++ .../nebula.inftyai.com_sandboxpools.yaml | 457 ++++++++++++++++++ 6 files changed, 1582 insertions(+) create mode 100644 api/v1alpha1/sandbox_types.go create mode 100644 api/v1alpha1/sandboxpool_types.go create mode 100644 config/crd/bases/nebula.inftyai.com_sandboxes.yaml create mode 100644 config/crd/bases/nebula.inftyai.com_sandboxpools.yaml diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index d27ae8b..5c66749 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -8,6 +8,14 @@ // NodeClaim - one provisioned external instance and its lifecycle. Owns the // terminate finalizer so a paid instance is never leaked. // +// On top of that provisioning core sit the workload types, each synthesizing +// Pods onto the same placement path rather than bypassing it: +// +// Sandbox - one interactive remote box (agent workspace, shell, scratch GPU), +// reachable with the same kubectl exec/logs as a local Pod. +// SandboxPool - keeps N warm Sandboxes ready to hand out, and owns /scale so +// `kubectl scale` and HPA drive the count. +// // +kubebuilder:object:generate=true // +groupName=nebula.inftyai.com package v1alpha1 @@ -59,6 +67,19 @@ const ( // value is the NodePool name, so the key mirrors the CRD kind. PoolLabel = "nebula.inftyai.com/nodepool" + // SandboxLabel records which Sandbox a Pod belongs to. Its value is the Sandbox + // name, so the key mirrors the CRD kind. The Sandbox controller selects its own + // Pod by it, and it is what makes `kubectl get pods -l + // nebula.inftyai.com/sandbox=alice` work. + SandboxLabel = "nebula.inftyai.com/sandbox" + + // SandboxPoolLabel records which SandboxPool created a Sandbox. Its value is + // the pool name. It is the selector the pool's /scale subresource publishes in + // status (so HPA can find the pool's members) and how the pool controller + // enumerates the boxes it owns — ownerReferences alone would not support a + // label-selector query. + SandboxPoolLabel = "nebula.inftyai.com/sandboxpool" + // AcceleratorTypeLabel carries the requested accelerator TYPE only (e.g. // "a100-40gb" or "h100"). The COUNT is expressed separately as a standard // resource request/limit on the container (nvidia.com/gpu for the NVIDIA diff --git a/api/v1alpha1/sandbox_types.go b/api/v1alpha1/sandbox_types.go new file mode 100644 index 0000000..5513cf4 --- /dev/null +++ b/api/v1alpha1/sandbox_types.go @@ -0,0 +1,243 @@ +/* +Copyright 2026 The InftyAI Team. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// SandboxSpec is one long-lived, interactive remote box: an agent's workspace, a +// shell, a scratch GPU machine. It is the first workload class Nebula serves +// beyond a hand-written Pod. +// +// A Sandbox is SINGULAR — one object, one instance. There is deliberately no +// replicas field and no pod template, because a sandbox is not fungible: someone +// is attached to it, it accumulates state in its filesystem, and the object's own +// name IS its stable identity (`kubectl exec sandbox-alice` always reaches the +// same box). Set-shaped controllers exist because containers are interchangeable, +// which is exactly the property a sandbox lacks — a rolling update would evict a +// live session, and "scale in by one" would have to guess whose box to kill. A +// caller that wants N boxes creates N Sandboxes, each with its own image, +// lifetime and identity. +// +// The count lives one level up, in SandboxPool, which keeps N Sandboxes warm and +// owns /scale so `kubectl scale` and HPA work. That split is what lets this type +// stay singular: because the pool creates Sandbox OBJECTS rather than replicas +// inside one object, everything that depends on a box being its own object — +// per-box RBAC (grant a user their sandbox and not their neighbour's), a +// per-box image and TTL, and a failure that stays visible instead of being +// papered over by a replacement — keeps working underneath a pool. +// +// The spec deliberately reuses corev1 types (ResourceRequirements, EnvVar) rather +// than inventing parallel fields. The controller synthesizes a Pod, so anything +// it accepts must ultimately BE PodSpec-shaped; re-declaring resources or env +// would fork the vocabulary and, worse, fork the source of truth for the +// accelerator COUNT — which placement and the scheduler's fit check both read +// from the container's nvidia.com/gpu limit (see util.AcceleratorRequest). +type SandboxSpec struct { + // NodePoolRef names the NodePool whose policy places this sandbox: which + // providers are allowed, which capacity tiers, how to rank them. Required — + // there is no implicit default pool, because placing a paid GPU instance + // against a guessed policy is not a safe default. + // +kubebuilder:validation:MinLength=1 + NodePoolRef string `json:"nodePoolRef"` + + // Image is the container image the sandbox runs. + // + // It has no command, and setting one is rejected at admission. That is not a + // simplification, it is the process model: SandD runs as PID 1 in the container + // and spawns the workload as its child so it owns the workload's stdout/stderr + // pipes, which is what makes `kubectl logs` possible against an instance in + // another cloud. A user-supplied command would displace SandD and take the log + // and exec paths with it. An interactive sandbox has nothing to run at boot + // anyway — the whole point is that commands arrive later, over exec. + // +kubebuilder:validation:MinLength=1 + Image string `json:"image"` + + // AcceleratorType is the requested accelerator TYPE (e.g. "a100-40gb", + // "h100"), matched case-insensitively against the provider catalog. The COUNT + // is NOT here: it is a standard nvidia.com/gpu entry in Resources, so exactly + // one number drives scheduling fit and provisioning. The controller stamps + // this onto the synthesized Pod's AcceleratorTypeLabel, so a Sandbox and a + // hand-written Nebula Pod go through identical placement. + // + // Empty means a CPU-only sandbox, which is a legitimate (and cheap) thing to + // want for a shell or an agent that only needs a filesystem. + // +optional + AcceleratorType string `json:"acceleratorType,omitempty"` + + // Resources is the standard Kubernetes resource requirements for the sandbox + // container, verbatim. The accelerator count rides here as an nvidia.com/gpu + // limit (`limits: {nvidia.com/gpu: "1"}`), which is where both the placement + // controller and the scheduler already read it from. + // +optional + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // Env is passed to the sandbox container verbatim, including valueFrom + // references — a sandbox usually needs at least a registry or Hugging Face + // token, and re-inventing secret indirection here would be strictly worse than + // reusing the field everyone already knows. + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` + + // TTL bounds the sandbox's total lifetime, measured from the moment it first + // became Ready (NOT from creation, so a slow provision does not eat into the + // user's time). On expiry the controller releases the instance and the sandbox + // reports phase Expired. + // + // This exists because the failure mode of a remote GPU box is financial: an + // abandoned sandbox bills until someone notices. Omit it for an unbounded + // sandbox, which is a deliberate choice rather than the default. + // +optional + TTL *metav1.Duration `json:"ttl,omitempty"` +} + +// SandboxPhase is the coarse, user-facing lifecycle state, derived from the +// synthesized Pod rather than tracked independently. The Pod (via the virtual +// kubelet) is the source of truth for what the external instance is doing — see +// pkg/vnode/status.go — so this is a projection, and the vocabulary intentionally +// mirrors the Pod status reasons the vnode stamps. +type SandboxPhase string + +const ( + // SandboxPending: the sandbox exists but its Pod has not been placed yet — + // typically waiting on the provider-selection gate, e.g. because no provider in + // the pool can currently serve the requested accelerator. A sandbox that sits + // here points at placement, not at the provider. + SandboxPending SandboxPhase = "Pending" + // SandboxProvisioning: a provider Provision call is in flight; the external + // instance does not exist yet. + SandboxProvisioning SandboxPhase = "Provisioning" + // SandboxInitializing: the instance exists at the provider but is not yet + // reachable — booting, or up but not yet passing reachability checks. Kept + // distinct from Provisioning so a stuck sandbox distinguishes "cannot get + // capacity" from "capacity granted, slow boot". + SandboxInitializing SandboxPhase = "Initializing" + // SandboxReady: the instance is running and reachable. This is the only phase + // in which exec/logs can succeed. + SandboxReady SandboxPhase = "Ready" + // SandboxFailed: the instance failed or vanished (terminated out-of-band, + // reclaimed, or the provision was rejected). Terminal: a sandbox holds + // filesystem state that a fresh instance would not have, so it is never + // silently recreated underneath its user. Delete and recreate it explicitly. + SandboxFailed SandboxPhase = "Failed" + // SandboxExpired: spec.TTL elapsed and the instance was released. Terminal, and + // deliberately not garbage: the object stays as the record of why the box went + // away, so a user who returns to a dead sandbox gets an answer instead of a + // NotFound. + SandboxExpired SandboxPhase = "Expired" +) + +// Sandbox condition types (standard Kubernetes condition convention). +const ( + // SandboxConditionReady is True exactly when the sandbox is usable — the + // instance is running and reachable. It is the condition to wait on + // (`kubectl wait --for=condition=Ready sandbox/x`) and mirrors the Pod's own + // Ready condition. + SandboxConditionReady = "Ready" +) + +// Sandbox condition reasons. +const ( + // ReasonSandboxReady: the instance is running and reachable. + ReasonSandboxReady = "Ready" + // ReasonSandboxProvisioning: still bringing the instance up (covers both + // placement and boot; the phase distinguishes them). + ReasonSandboxProvisioning = "Provisioning" + // ReasonSandboxFailed: the instance failed, was rejected, or vanished. + ReasonSandboxFailed = "Failed" + // ReasonSandboxExpired: spec.TTL elapsed and the instance was released. + ReasonSandboxExpired = "Expired" + // ReasonPodConflict: a Pod of the required name already exists and is NOT owned + // by this Sandbox. The controller refuses to adopt it — it could be an unrelated + // workload, and adopting would hand someone else's Pod a terminate finalizer — + // so the sandbox surfaces the collision instead of acting on a guess. + ReasonPodConflict = "PodConflict" +) + +// SandboxStatus is the observed state, projected from the synthesized Pod. +type SandboxStatus struct { + // Phase is the coarse lifecycle state. + // +optional + Phase SandboxPhase `json:"phase,omitempty"` + + // PodName is the synthesized Pod backing this sandbox. It is recorded even + // though it currently equals the Sandbox name, so tooling (and `kubectl exec` + // wrappers) read the pod identity from status rather than reconstructing it + // from a naming convention this controller would then be unable to change. + // +optional + PodName string `json:"podName,omitempty"` + + // Endpoint is the reachable address of the external instance once it is + // running, in the provider's own form (a public DNS name or an IP). Mirrored + // from the Pod's EndpointAnnotation, which is where the virtual kubelet + // publishes it. + // +optional + Endpoint string `json:"endpoint,omitempty"` + + // ReadyTime is when the sandbox first became Ready. It is the anchor TTL is + // measured from, so it is durable status rather than a derived value: if it + // were recomputed from the Pod, a Pod status blip could silently restart the + // user's clock. + // +optional + ReadyTime *metav1.Time `json:"readyTime,omitempty"` + + // ExpiryTime is when TTL will elapse (ReadyTime + TTL), surfaced so a user can + // see the deadline without doing the arithmetic. Absent when no TTL is set or + // the sandbox has not become Ready yet. + // +optional + ExpiryTime *metav1.Time `json:"expiryTime,omitempty"` + + // Conditions follows the standard Kubernetes condition convention. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Namespaced,shortName=sbx +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Pool",type=string,JSONPath=`.spec.nodePoolRef` +// +kubebuilder:printcolumn:name="Accelerator",type=string,JSONPath=`.spec.acceleratorType` +// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.status.endpoint` +// +kubebuilder:printcolumn:name="Expires",type=date,JSONPath=`.status.expiryTime` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// Sandbox is one interactive remote instance — an agent workspace, a shell, a +// scratch GPU box — reachable with the same `kubectl exec` / `kubectl logs` a +// local Pod would be. +type Sandbox struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SandboxSpec `json:"spec,omitempty"` + Status SandboxStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// SandboxList contains a list of Sandbox. +type SandboxList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Sandbox `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Sandbox{}, &SandboxList{}) +} diff --git a/api/v1alpha1/sandboxpool_types.go b/api/v1alpha1/sandboxpool_types.go new file mode 100644 index 0000000..82f94ea --- /dev/null +++ b/api/v1alpha1/sandboxpool_types.go @@ -0,0 +1,169 @@ +/* +Copyright 2026 The InftyAI Team. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// SandboxPoolSpec keeps N Sandboxes alive. It exists because provisioning a +// remote instance takes MINUTES while an agent's exec call wants sub-second: the +// only way to serve that is to hold boxes ready before anyone asks. A pool is +// also the unit fan-out is expressed on ("twenty boxes for this batch") and the +// thing an autoscaler can drive. +// +// It creates Sandbox OBJECTS, not replicas inside itself, and that is the whole +// point of having two types. The count is genuinely a different concern from the +// box: a pool answers "how many", a Sandbox answers "which one, running what, for +// whom". Because each box stays its own object underneath the pool, per-box RBAC, +// per-box status and a failure that stays visible all keep working — none of which +// survives being flattened into a replica index. +// +// Boxes get GENERATED names (pool-a4f2x), not ordinals. Ordinals would imply a +// slot that gets refilled, so a box that died would be replaced by an empty one +// wearing the same name — the same address with a different filesystem, which is +// the most confusing thing this API could do. A generated name means a replacement +// is visibly a NEW box, and callers that need a stable handle hold the Sandbox +// name they were given rather than an index into a set. +type SandboxPoolSpec struct { + // Replicas is how many Sandboxes to keep. Zero is legal and useful: it releases + // every box while keeping the pool's definition, which is how a pool is parked + // overnight without being forgotten. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:default=1 + Replicas int32 `json:"replicas,omitempty"` + + // Template is the shape of every Sandbox this pool creates. All boxes in one + // pool are the same shape by construction — a pool exists to make boxes + // interchangeable at the point of HANDOUT, so a caller can take any ready box + // without inspecting it. Two shapes means two pools. + // + // A template is right here for the same reason it is wrong on Sandbox itself: + // this object does not describe a box, it describes how to make them. + Template SandboxTemplateSpec `json:"template"` +} + +// SandboxTemplateSpec is the Sandbox a pool stamps out: the standard Kubernetes +// template shape (metadata + spec), so pool-created boxes can carry the labels a +// caller selects them by. +type SandboxTemplateSpec struct { + // Metadata is the labels and annotations applied to each created Sandbox. Only + // labels and annotations are honoured; a name here is ignored, since names are + // generated per box. + // +optional + Metadata SandboxTemplateMetadata `json:"metadata,omitempty"` + + // Spec is the SandboxSpec of every box in the pool. + Spec SandboxSpec `json:"spec"` +} + +// SandboxTemplateMetadata is the subset of ObjectMeta a template may set. It is +// spelled out rather than embedding metav1.ObjectMeta because embedding would +// advertise fields a template cannot honour (name, ownerReferences, resourceVersion) +// and bloat the CRD schema with them. +type SandboxTemplateMetadata struct { + // Labels are applied to each created Sandbox, on top of the pool-ownership + // labels the controller adds. + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // Annotations are applied to each created Sandbox. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// SandboxPool condition types (standard Kubernetes condition convention). +const ( + // SandboxPoolConditionReady is True when every desired box is ready, i.e. the + // pool is fully warm. Callers that can start work with a partially warm pool + // should read status.ReadyReplicas instead of waiting on this. + SandboxPoolConditionReady = "Ready" +) + +// SandboxPool condition reasons. +const ( + // ReasonPoolWarm: every desired box is Ready. + ReasonPoolWarm = "Warm" + // ReasonPoolWarming: at least one box is still coming up. Not an error — a cold + // pool takes minutes by nature. + ReasonPoolWarming = "Warming" + // ReasonPoolScaledToZero: spec.Replicas is 0, so there is nothing to be ready. + // Distinguished from Warming so a parked pool does not read as a stuck one. + ReasonPoolScaledToZero = "ScaledToZero" +) + +// SandboxPoolStatus is the observed state of the pool. +type SandboxPoolStatus struct { + // Replicas is how many Sandboxes the pool currently owns, ready or not. It is + // the /scale subresource's status counterpart. + // +optional + Replicas int32 `json:"replicas,omitempty"` + + // ReadyReplicas is how many owned Sandboxes are Ready — the number of boxes + // that can actually serve an exec right now, which is what "is the pool warm" + // really means. + // +optional + ReadyReplicas int32 `json:"readyReplicas,omitempty"` + + // Selector is the label selector matching this pool's Sandboxes, serialized in + // the string form the /scale subresource requires. HPA and KEDA read the target's + // selector from here, so autoscaling a pool does not work without it. + // +optional + Selector string `json:"selector,omitempty"` + + // Sandboxes names the boxes this pool owns, so the pool is a usable handout + // list: a caller reads it to find a box to claim without listing and filtering + // Sandboxes itself. Ordered by name for a stable diff. + // +optional + Sandboxes []string `json:"sandboxes,omitempty"` + + // Conditions follows the standard Kubernetes condition convention. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Namespaced,shortName=sbxp +// +kubebuilder:subresource:status +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector +// +kubebuilder:printcolumn:name="Desired",type=integer,JSONPath=`.spec.replicas` +// +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas` +// +kubebuilder:printcolumn:name="Pool",type=string,JSONPath=`.spec.template.spec.nodePoolRef` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// SandboxPool keeps N warm Sandboxes ready to hand out, so a consumer does not +// wait minutes for an instance to provision. +type SandboxPool struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SandboxPoolSpec `json:"spec,omitempty"` + Status SandboxPoolStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// SandboxPoolList contains a list of SandboxPool. +type SandboxPoolList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SandboxPool `json:"items"` +} + +func init() { + SchemeBuilder.Register(&SandboxPool{}, &SandboxPoolList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ffb03a9..1b63384 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -290,3 +291,268 @@ func (in *ProviderSpec) DeepCopy() *ProviderSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Sandbox) DeepCopyInto(out *Sandbox) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Sandbox. +func (in *Sandbox) DeepCopy() *Sandbox { + if in == nil { + return nil + } + out := new(Sandbox) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Sandbox) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxList) DeepCopyInto(out *SandboxList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Sandbox, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxList. +func (in *SandboxList) DeepCopy() *SandboxList { + if in == nil { + return nil + } + out := new(SandboxList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SandboxList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxPool) DeepCopyInto(out *SandboxPool) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxPool. +func (in *SandboxPool) DeepCopy() *SandboxPool { + if in == nil { + return nil + } + out := new(SandboxPool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SandboxPool) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxPoolList) DeepCopyInto(out *SandboxPoolList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SandboxPool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxPoolList. +func (in *SandboxPoolList) DeepCopy() *SandboxPoolList { + if in == nil { + return nil + } + out := new(SandboxPoolList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SandboxPoolList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxPoolSpec) DeepCopyInto(out *SandboxPoolSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxPoolSpec. +func (in *SandboxPoolSpec) DeepCopy() *SandboxPoolSpec { + if in == nil { + return nil + } + out := new(SandboxPoolSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxPoolStatus) DeepCopyInto(out *SandboxPoolStatus) { + *out = *in + if in.Sandboxes != nil { + in, out := &in.Sandboxes, &out.Sandboxes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxPoolStatus. +func (in *SandboxPoolStatus) DeepCopy() *SandboxPoolStatus { + if in == nil { + return nil + } + out := new(SandboxPoolStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxSpec) DeepCopyInto(out *SandboxSpec) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(v1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSpec. +func (in *SandboxSpec) DeepCopy() *SandboxSpec { + if in == nil { + return nil + } + out := new(SandboxSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxStatus) DeepCopyInto(out *SandboxStatus) { + *out = *in + if in.ReadyTime != nil { + in, out := &in.ReadyTime, &out.ReadyTime + *out = (*in).DeepCopy() + } + if in.ExpiryTime != nil { + in, out := &in.ExpiryTime, &out.ExpiryTime + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxStatus. +func (in *SandboxStatus) DeepCopy() *SandboxStatus { + if in == nil { + return nil + } + out := new(SandboxStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxTemplateMetadata) DeepCopyInto(out *SandboxTemplateMetadata) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxTemplateMetadata. +func (in *SandboxTemplateMetadata) DeepCopy() *SandboxTemplateMetadata { + if in == nil { + return nil + } + out := new(SandboxTemplateMetadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SandboxTemplateSpec) DeepCopyInto(out *SandboxTemplateSpec) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxTemplateSpec. +func (in *SandboxTemplateSpec) DeepCopy() *SandboxTemplateSpec { + if in == nil { + return nil + } + out := new(SandboxTemplateSpec) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/nebula.inftyai.com_sandboxes.yaml b/config/crd/bases/nebula.inftyai.com_sandboxes.yaml new file mode 100644 index 0000000..a0bc93c --- /dev/null +++ b/config/crd/bases/nebula.inftyai.com_sandboxes.yaml @@ -0,0 +1,426 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: sandboxes.nebula.inftyai.com +spec: + group: nebula.inftyai.com + names: + kind: Sandbox + listKind: SandboxList + plural: sandboxes + shortNames: + - sbx + singular: sandbox + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Phase + type: string + - jsonPath: .spec.nodePoolRef + name: Pool + type: string + - jsonPath: .spec.acceleratorType + name: Accelerator + type: string + - jsonPath: .status.endpoint + name: Endpoint + type: string + - jsonPath: .status.expiryTime + name: Expires + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + Sandbox is one interactive remote instance — an agent workspace, a shell, a + scratch GPU box — reachable with the same `kubectl exec` / `kubectl logs` a + local Pod would be. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + SandboxSpec is one long-lived, interactive remote box: an agent's workspace, a + shell, a scratch GPU machine. It is the first workload class Nebula serves + beyond a hand-written Pod. + + A Sandbox is SINGULAR — one object, one instance. There is deliberately no + replicas field and no pod template, because a sandbox is not fungible: someone + is attached to it, it accumulates state in its filesystem, and the object's own + name IS its stable identity (`kubectl exec sandbox-alice` always reaches the + same box). Set-shaped controllers exist because containers are interchangeable, + which is exactly the property a sandbox lacks — a rolling update would evict a + live session, and "scale in by one" would have to guess whose box to kill. A + caller that wants N boxes creates N Sandboxes, each with its own image, + lifetime and identity. + + The count lives one level up, in SandboxPool, which keeps N Sandboxes warm and + owns /scale so `kubectl scale` and HPA work. That split is what lets this type + stay singular: because the pool creates Sandbox OBJECTS rather than replicas + inside one object, everything that depends on a box being its own object — + per-box RBAC (grant a user their sandbox and not their neighbour's), a + per-box image and TTL, and a failure that stays visible instead of being + papered over by a replacement — keeps working underneath a pool. + + The spec deliberately reuses corev1 types (ResourceRequirements, EnvVar) rather + than inventing parallel fields. The controller synthesizes a Pod, so anything + it accepts must ultimately BE PodSpec-shaped; re-declaring resources or env + would fork the vocabulary and, worse, fork the source of truth for the + accelerator COUNT — which placement and the scheduler's fit check both read + from the container's nvidia.com/gpu limit (see util.AcceleratorRequest). + properties: + acceleratorType: + description: |- + AcceleratorType is the requested accelerator TYPE (e.g. "a100-40gb", + "h100"), matched case-insensitively against the provider catalog. The COUNT + is NOT here: it is a standard nvidia.com/gpu entry in Resources, so exactly + one number drives scheduling fit and provisioning. The controller stamps + this onto the synthesized Pod's AcceleratorTypeLabel, so a Sandbox and a + hand-written Nebula Pod go through identical placement. + + Empty means a CPU-only sandbox, which is a legitimate (and cheap) thing to + want for a shell or an agent that only needs a filesystem. + type: string + env: + description: |- + Env is passed to the sandbox container verbatim, including valueFrom + references — a sandbox usually needs at least a registry or Hugging Face + token, and re-inventing secret indirection here would be strictly worse than + reusing the field everyone already knows. + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Image is the container image the sandbox runs. + + It has no command, and setting one is rejected at admission. That is not a + simplification, it is the process model: SandD runs as PID 1 in the container + and spawns the workload as its child so it owns the workload's stdout/stderr + pipes, which is what makes `kubectl logs` possible against an instance in + another cloud. A user-supplied command would displace SandD and take the log + and exec paths with it. An interactive sandbox has nothing to run at boot + anyway — the whole point is that commands arrive later, over exec. + minLength: 1 + type: string + nodePoolRef: + description: |- + NodePoolRef names the NodePool whose policy places this sandbox: which + providers are allowed, which capacity tiers, how to rank them. Required — + there is no implicit default pool, because placing a paid GPU instance + against a guessed policy is not a safe default. + minLength: 1 + type: string + resources: + description: |- + Resources is the standard Kubernetes resource requirements for the sandbox + container, verbatim. The accelerator count rides here as an nvidia.com/gpu + limit (`limits: {nvidia.com/gpu: "1"}`), which is where both the placement + controller and the scheduler already read it from. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + ttl: + description: |- + TTL bounds the sandbox's total lifetime, measured from the moment it first + became Ready (NOT from creation, so a slow provision does not eat into the + user's time). On expiry the controller releases the instance and the sandbox + reports phase Expired. + + This exists because the failure mode of a remote GPU box is financial: an + abandoned sandbox bills until someone notices. Omit it for an unbounded + sandbox, which is a deliberate choice rather than the default. + type: string + required: + - image + - nodePoolRef + type: object + status: + description: SandboxStatus is the observed state, projected from the synthesized + Pod. + properties: + conditions: + description: Conditions follows the standard Kubernetes condition + convention. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + endpoint: + description: |- + Endpoint is the reachable address of the external instance once it is + running, in the provider's own form (a public DNS name or an IP). Mirrored + from the Pod's EndpointAnnotation, which is where the virtual kubelet + publishes it. + type: string + expiryTime: + description: |- + ExpiryTime is when TTL will elapse (ReadyTime + TTL), surfaced so a user can + see the deadline without doing the arithmetic. Absent when no TTL is set or + the sandbox has not become Ready yet. + format: date-time + type: string + phase: + description: Phase is the coarse lifecycle state. + type: string + podName: + description: |- + PodName is the synthesized Pod backing this sandbox. It is recorded even + though it currently equals the Sandbox name, so tooling (and `kubectl exec` + wrappers) read the pod identity from status rather than reconstructing it + from a naming convention this controller would then be unable to change. + type: string + readyTime: + description: |- + ReadyTime is when the sandbox first became Ready. It is the anchor TTL is + measured from, so it is durable status rather than a derived value: if it + were recomputed from the Pod, a Pod status blip could silently restart the + user's clock. + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/nebula.inftyai.com_sandboxpools.yaml b/config/crd/bases/nebula.inftyai.com_sandboxpools.yaml new file mode 100644 index 0000000..aba11f8 --- /dev/null +++ b/config/crd/bases/nebula.inftyai.com_sandboxpools.yaml @@ -0,0 +1,457 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: sandboxpools.nebula.inftyai.com +spec: + group: nebula.inftyai.com + names: + kind: SandboxPool + listKind: SandboxPoolList + plural: sandboxpools + shortNames: + - sbxp + singular: sandboxpool + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.replicas + name: Desired + type: integer + - jsonPath: .status.readyReplicas + name: Ready + type: integer + - jsonPath: .spec.template.spec.nodePoolRef + name: Pool + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + SandboxPool keeps N warm Sandboxes ready to hand out, so a consumer does not + wait minutes for an instance to provision. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + SandboxPoolSpec keeps N Sandboxes alive. It exists because provisioning a + remote instance takes MINUTES while an agent's exec call wants sub-second: the + only way to serve that is to hold boxes ready before anyone asks. A pool is + also the unit fan-out is expressed on ("twenty boxes for this batch") and the + thing an autoscaler can drive. + + It creates Sandbox OBJECTS, not replicas inside itself, and that is the whole + point of having two types. The count is genuinely a different concern from the + box: a pool answers "how many", a Sandbox answers "which one, running what, for + whom". Because each box stays its own object underneath the pool, per-box RBAC, + per-box status and a failure that stays visible all keep working — none of which + survives being flattened into a replica index. + + Boxes get GENERATED names (pool-a4f2x), not ordinals. Ordinals would imply a + slot that gets refilled, so a box that died would be replaced by an empty one + wearing the same name — the same address with a different filesystem, which is + the most confusing thing this API could do. A generated name means a replacement + is visibly a NEW box, and callers that need a stable handle hold the Sandbox + name they were given rather than an index into a set. + properties: + replicas: + default: 1 + description: |- + Replicas is how many Sandboxes to keep. Zero is legal and useful: it releases + every box while keeping the pool's definition, which is how a pool is parked + overnight without being forgotten. + format: int32 + minimum: 0 + type: integer + template: + description: |- + Template is the shape of every Sandbox this pool creates. All boxes in one + pool are the same shape by construction — a pool exists to make boxes + interchangeable at the point of HANDOUT, so a caller can take any ready box + without inspecting it. Two shapes means two pools. + + A template is right here for the same reason it is wrong on Sandbox itself: + this object does not describe a box, it describes how to make them. + properties: + metadata: + description: |- + Metadata is the labels and annotations applied to each created Sandbox. Only + labels and annotations are honoured; a name here is ignored, since names are + generated per box. + properties: + annotations: + additionalProperties: + type: string + description: Annotations are applied to each created Sandbox. + type: object + labels: + additionalProperties: + type: string + description: |- + Labels are applied to each created Sandbox, on top of the pool-ownership + labels the controller adds. + type: object + type: object + spec: + description: Spec is the SandboxSpec of every box in the pool. + properties: + acceleratorType: + description: |- + AcceleratorType is the requested accelerator TYPE (e.g. "a100-40gb", + "h100"), matched case-insensitively against the provider catalog. The COUNT + is NOT here: it is a standard nvidia.com/gpu entry in Resources, so exactly + one number drives scheduling fit and provisioning. The controller stamps + this onto the synthesized Pod's AcceleratorTypeLabel, so a Sandbox and a + hand-written Nebula Pod go through identical placement. + + Empty means a CPU-only sandbox, which is a legitimate (and cheap) thing to + want for a shell or an agent that only needs a filesystem. + type: string + env: + description: |- + Env is passed to the sandbox container verbatim, including valueFrom + references — a sandbox usually needs at least a registry or Hugging Face + token, and re-inventing secret indirection here would be strictly worse than + reusing the field everyone already knows. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Image is the container image the sandbox runs. + + It has no command, and setting one is rejected at admission. That is not a + simplification, it is the process model: SandD runs as PID 1 in the container + and spawns the workload as its child so it owns the workload's stdout/stderr + pipes, which is what makes `kubectl logs` possible against an instance in + another cloud. A user-supplied command would displace SandD and take the log + and exec paths with it. An interactive sandbox has nothing to run at boot + anyway — the whole point is that commands arrive later, over exec. + minLength: 1 + type: string + nodePoolRef: + description: |- + NodePoolRef names the NodePool whose policy places this sandbox: which + providers are allowed, which capacity tiers, how to rank them. Required — + there is no implicit default pool, because placing a paid GPU instance + against a guessed policy is not a safe default. + minLength: 1 + type: string + resources: + description: |- + Resources is the standard Kubernetes resource requirements for the sandbox + container, verbatim. The accelerator count rides here as an nvidia.com/gpu + limit (`limits: {nvidia.com/gpu: "1"}`), which is where both the placement + controller and the scheduler already read it from. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + ttl: + description: |- + TTL bounds the sandbox's total lifetime, measured from the moment it first + became Ready (NOT from creation, so a slow provision does not eat into the + user's time). On expiry the controller releases the instance and the sandbox + reports phase Expired. + + This exists because the failure mode of a remote GPU box is financial: an + abandoned sandbox bills until someone notices. Omit it for an unbounded + sandbox, which is a deliberate choice rather than the default. + type: string + required: + - image + - nodePoolRef + type: object + required: + - spec + type: object + required: + - template + type: object + status: + description: SandboxPoolStatus is the observed state of the pool. + properties: + conditions: + description: Conditions follows the standard Kubernetes condition + convention. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + readyReplicas: + description: |- + ReadyReplicas is how many owned Sandboxes are Ready — the number of boxes + that can actually serve an exec right now, which is what "is the pool warm" + really means. + format: int32 + type: integer + replicas: + description: |- + Replicas is how many Sandboxes the pool currently owns, ready or not. It is + the /scale subresource's status counterpart. + format: int32 + type: integer + sandboxes: + description: |- + Sandboxes names the boxes this pool owns, so the pool is a usable handout + list: a caller reads it to find a box to claim without listing and filtering + Sandboxes itself. Ordered by name for a stable diff. + items: + type: string + type: array + selector: + description: |- + Selector is the label selector matching this pool's Sandboxes, serialized in + the string form the /scale subresource requires. HPA and KEDA read the target's + selector from here, so autoscaling a pool does not work without it. + type: string + type: object + type: object + served: true + storage: true + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} From 80a7a3147bc99d90bbbaab8b762f42eec62f3f94 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 09:22:32 +0100 Subject: [PATCH 10/22] api: rename SandboxPool to SandboxSet; add Sandbox controller SandboxSet, not SandboxPool: the controller maintains N boxes and has no lease semantics (claim/hold/return), so "pool" overpromised. It also would have been a third sense of "pool" in a group where NodePool already means placement policy. ReplicaSet is the precedent that matches - it maintains N and leaves existing members alone. The Sandbox controller synthesizes the backing Pod and projects its status back, so placement, the NodeClaim teardown ledger, and quota all work unchanged. Co-Authored-By: Claude Opus 5 --- api/v1alpha1/groupversion_info.go | 20 +- api/v1alpha1/sandbox_types.go | 4 +- api/v1alpha1/sandboxpool_types.go | 169 ------- api/v1alpha1/sandboxset_types.go | 173 ++++++++ api/v1alpha1/zz_generated.deepcopy.go | 38 +- .../bases/nebula.inftyai.com_sandboxes.yaml | 4 +- ...ml => nebula.inftyai.com_sandboxsets.yaml} | 89 ++-- config/rbac/role.yaml | 15 +- internal/controller/sandbox_controller.go | 418 ++++++++++++++++++ 9 files changed, 674 insertions(+), 256 deletions(-) delete mode 100644 api/v1alpha1/sandboxpool_types.go create mode 100644 api/v1alpha1/sandboxset_types.go rename config/crd/bases/{nebula.inftyai.com_sandboxpools.yaml => nebula.inftyai.com_sandboxsets.yaml} (88%) create mode 100644 internal/controller/sandbox_controller.go diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index 5c66749..2dad0df 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -11,10 +11,11 @@ // On top of that provisioning core sit the workload types, each synthesizing // Pods onto the same placement path rather than bypassing it: // -// Sandbox - one interactive remote box (agent workspace, shell, scratch GPU), -// reachable with the same kubectl exec/logs as a local Pod. -// SandboxPool - keeps N warm Sandboxes ready to hand out, and owns /scale so -// `kubectl scale` and HPA drive the count. +// Sandbox - one interactive remote box (agent workspace, shell, scratch GPU), +// reachable with the same kubectl exec/logs as a local Pod. +// SandboxSet - maintains N Sandboxes, and owns /scale so `kubectl scale` and HPA +// drive the count. Keeping boxes ready ahead of demand is a USE of +// this, not its definition — there are no lease semantics here. // // +kubebuilder:object:generate=true // +groupName=nebula.inftyai.com @@ -73,12 +74,11 @@ const ( // nebula.inftyai.com/sandbox=alice` work. SandboxLabel = "nebula.inftyai.com/sandbox" - // SandboxPoolLabel records which SandboxPool created a Sandbox. Its value is - // the pool name. It is the selector the pool's /scale subresource publishes in - // status (so HPA can find the pool's members) and how the pool controller - // enumerates the boxes it owns — ownerReferences alone would not support a - // label-selector query. - SandboxPoolLabel = "nebula.inftyai.com/sandboxpool" + // SandboxSetLabel records which SandboxSet created a Sandbox. Its value is the + // set name. It is the selector the set's /scale subresource publishes in status + // (so HPA can find the set's members) and how the set controller enumerates the + // boxes it owns — ownerReferences alone would not support a label-selector query. + SandboxSetLabel = "nebula.inftyai.com/sandboxset" // AcceleratorTypeLabel carries the requested accelerator TYPE only (e.g. // "a100-40gb" or "h100"). The COUNT is expressed separately as a standard diff --git a/api/v1alpha1/sandbox_types.go b/api/v1alpha1/sandbox_types.go index 5513cf4..5844f34 100644 --- a/api/v1alpha1/sandbox_types.go +++ b/api/v1alpha1/sandbox_types.go @@ -35,9 +35,9 @@ import ( // caller that wants N boxes creates N Sandboxes, each with its own image, // lifetime and identity. // -// The count lives one level up, in SandboxPool, which keeps N Sandboxes warm and +// The count lives one level up, in SandboxSet, which maintains N Sandboxes and // owns /scale so `kubectl scale` and HPA work. That split is what lets this type -// stay singular: because the pool creates Sandbox OBJECTS rather than replicas +// stay singular: because the set creates Sandbox OBJECTS rather than replicas // inside one object, everything that depends on a box being its own object — // per-box RBAC (grant a user their sandbox and not their neighbour's), a // per-box image and TTL, and a failure that stays visible instead of being diff --git a/api/v1alpha1/sandboxpool_types.go b/api/v1alpha1/sandboxpool_types.go deleted file mode 100644 index 82f94ea..0000000 --- a/api/v1alpha1/sandboxpool_types.go +++ /dev/null @@ -1,169 +0,0 @@ -/* -Copyright 2026 The InftyAI Team. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// SandboxPoolSpec keeps N Sandboxes alive. It exists because provisioning a -// remote instance takes MINUTES while an agent's exec call wants sub-second: the -// only way to serve that is to hold boxes ready before anyone asks. A pool is -// also the unit fan-out is expressed on ("twenty boxes for this batch") and the -// thing an autoscaler can drive. -// -// It creates Sandbox OBJECTS, not replicas inside itself, and that is the whole -// point of having two types. The count is genuinely a different concern from the -// box: a pool answers "how many", a Sandbox answers "which one, running what, for -// whom". Because each box stays its own object underneath the pool, per-box RBAC, -// per-box status and a failure that stays visible all keep working — none of which -// survives being flattened into a replica index. -// -// Boxes get GENERATED names (pool-a4f2x), not ordinals. Ordinals would imply a -// slot that gets refilled, so a box that died would be replaced by an empty one -// wearing the same name — the same address with a different filesystem, which is -// the most confusing thing this API could do. A generated name means a replacement -// is visibly a NEW box, and callers that need a stable handle hold the Sandbox -// name they were given rather than an index into a set. -type SandboxPoolSpec struct { - // Replicas is how many Sandboxes to keep. Zero is legal and useful: it releases - // every box while keeping the pool's definition, which is how a pool is parked - // overnight without being forgotten. - // +kubebuilder:validation:Minimum=0 - // +kubebuilder:default=1 - Replicas int32 `json:"replicas,omitempty"` - - // Template is the shape of every Sandbox this pool creates. All boxes in one - // pool are the same shape by construction — a pool exists to make boxes - // interchangeable at the point of HANDOUT, so a caller can take any ready box - // without inspecting it. Two shapes means two pools. - // - // A template is right here for the same reason it is wrong on Sandbox itself: - // this object does not describe a box, it describes how to make them. - Template SandboxTemplateSpec `json:"template"` -} - -// SandboxTemplateSpec is the Sandbox a pool stamps out: the standard Kubernetes -// template shape (metadata + spec), so pool-created boxes can carry the labels a -// caller selects them by. -type SandboxTemplateSpec struct { - // Metadata is the labels and annotations applied to each created Sandbox. Only - // labels and annotations are honoured; a name here is ignored, since names are - // generated per box. - // +optional - Metadata SandboxTemplateMetadata `json:"metadata,omitempty"` - - // Spec is the SandboxSpec of every box in the pool. - Spec SandboxSpec `json:"spec"` -} - -// SandboxTemplateMetadata is the subset of ObjectMeta a template may set. It is -// spelled out rather than embedding metav1.ObjectMeta because embedding would -// advertise fields a template cannot honour (name, ownerReferences, resourceVersion) -// and bloat the CRD schema with them. -type SandboxTemplateMetadata struct { - // Labels are applied to each created Sandbox, on top of the pool-ownership - // labels the controller adds. - // +optional - Labels map[string]string `json:"labels,omitempty"` - - // Annotations are applied to each created Sandbox. - // +optional - Annotations map[string]string `json:"annotations,omitempty"` -} - -// SandboxPool condition types (standard Kubernetes condition convention). -const ( - // SandboxPoolConditionReady is True when every desired box is ready, i.e. the - // pool is fully warm. Callers that can start work with a partially warm pool - // should read status.ReadyReplicas instead of waiting on this. - SandboxPoolConditionReady = "Ready" -) - -// SandboxPool condition reasons. -const ( - // ReasonPoolWarm: every desired box is Ready. - ReasonPoolWarm = "Warm" - // ReasonPoolWarming: at least one box is still coming up. Not an error — a cold - // pool takes minutes by nature. - ReasonPoolWarming = "Warming" - // ReasonPoolScaledToZero: spec.Replicas is 0, so there is nothing to be ready. - // Distinguished from Warming so a parked pool does not read as a stuck one. - ReasonPoolScaledToZero = "ScaledToZero" -) - -// SandboxPoolStatus is the observed state of the pool. -type SandboxPoolStatus struct { - // Replicas is how many Sandboxes the pool currently owns, ready or not. It is - // the /scale subresource's status counterpart. - // +optional - Replicas int32 `json:"replicas,omitempty"` - - // ReadyReplicas is how many owned Sandboxes are Ready — the number of boxes - // that can actually serve an exec right now, which is what "is the pool warm" - // really means. - // +optional - ReadyReplicas int32 `json:"readyReplicas,omitempty"` - - // Selector is the label selector matching this pool's Sandboxes, serialized in - // the string form the /scale subresource requires. HPA and KEDA read the target's - // selector from here, so autoscaling a pool does not work without it. - // +optional - Selector string `json:"selector,omitempty"` - - // Sandboxes names the boxes this pool owns, so the pool is a usable handout - // list: a caller reads it to find a box to claim without listing and filtering - // Sandboxes itself. Ordered by name for a stable diff. - // +optional - Sandboxes []string `json:"sandboxes,omitempty"` - - // Conditions follows the standard Kubernetes condition convention. - // +optional - Conditions []metav1.Condition `json:"conditions,omitempty"` -} - -// +kubebuilder:object:root=true -// +kubebuilder:resource:scope=Namespaced,shortName=sbxp -// +kubebuilder:subresource:status -// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector -// +kubebuilder:printcolumn:name="Desired",type=integer,JSONPath=`.spec.replicas` -// +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas` -// +kubebuilder:printcolumn:name="Pool",type=string,JSONPath=`.spec.template.spec.nodePoolRef` -// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` - -// SandboxPool keeps N warm Sandboxes ready to hand out, so a consumer does not -// wait minutes for an instance to provision. -type SandboxPool struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec SandboxPoolSpec `json:"spec,omitempty"` - Status SandboxPoolStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true - -// SandboxPoolList contains a list of SandboxPool. -type SandboxPoolList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []SandboxPool `json:"items"` -} - -func init() { - SchemeBuilder.Register(&SandboxPool{}, &SandboxPoolList{}) -} diff --git a/api/v1alpha1/sandboxset_types.go b/api/v1alpha1/sandboxset_types.go new file mode 100644 index 0000000..648dfa3 --- /dev/null +++ b/api/v1alpha1/sandboxset_types.go @@ -0,0 +1,173 @@ +/* +Copyright 2026 The InftyAI Team. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// SandboxSetSpec maintains N Sandboxes. That is the whole contract, and the name +// says exactly that much: it is a SET, not a pool. A pool would imply lease +// semantics — claim a box, hold it, return it, with the pool tracking who has what +// — and none of that is implemented here. Keeping N boxes alive is what ENABLES +// warm pooling (holding instances ready because provisioning takes minutes while +// an agent's exec call wants sub-second) and fan-out ("twenty boxes for this +// batch"), but those are uses of a set, not the set's job. "Pool" would also be a +// third meaning of that word in this API group, where NodePool already means +// placement policy. +// +// It creates Sandbox OBJECTS, not replicas inside itself, and that is the point of +// having two types. The count is a genuinely different concern from the box: a set +// answers "how many", a Sandbox answers "which one, running what, for whom". +// Because each box stays its own object underneath, per-box RBAC, per-box status +// and a failure that stays visible all keep working — none of which survives being +// flattened into a replica index. +// +// Boxes get GENERATED names (myset-a4f2x), not ordinals. Ordinals would imply a +// slot that gets refilled, so a box that died would be replaced by an empty one +// wearing the same name — the same address with a different filesystem, which is +// the most confusing thing this API could do. A generated name means a replacement +// is visibly a NEW box, and callers that need a stable handle hold the Sandbox name +// they were given rather than an index into a set. +type SandboxSetSpec struct { + // Replicas is how many Sandboxes to maintain. Zero is legal and useful: it + // releases every box while keeping the set's definition, which is how a set is + // parked overnight without being forgotten. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:default=1 + Replicas int32 `json:"replicas,omitempty"` + + // Template is the shape of every Sandbox this set creates. All boxes in one set + // are the same shape by construction — the set exists to make boxes + // interchangeable at the point of HANDOUT, so a caller can take any ready box + // without inspecting it. Two shapes means two sets. + // + // A template is right here for the same reason it is wrong on Sandbox itself: + // this object does not describe a box, it describes how to make them. + Template SandboxTemplateSpec `json:"template"` +} + +// SandboxTemplateSpec is the Sandbox a set stamps out: the standard Kubernetes +// template shape (metadata + spec), so created boxes can carry the labels a caller +// selects them by. +type SandboxTemplateSpec struct { + // Metadata is the labels and annotations applied to each created Sandbox. Only + // labels and annotations are honoured; a name here is ignored, since names are + // generated per box. + // +optional + Metadata SandboxTemplateMetadata `json:"metadata,omitempty"` + + // Spec is the SandboxSpec of every box in the set. + Spec SandboxSpec `json:"spec"` +} + +// SandboxTemplateMetadata is the subset of ObjectMeta a template may set. It is +// spelled out rather than embedding metav1.ObjectMeta because embedding would +// advertise fields a template cannot honour (name, ownerReferences, +// resourceVersion) and bloat the CRD schema with them. +type SandboxTemplateMetadata struct { + // Labels are applied to each created Sandbox, on top of the set-ownership + // labels the controller adds. + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // Annotations are applied to each created Sandbox. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// SandboxSet condition types (standard Kubernetes condition convention). +const ( + // SandboxSetConditionReady is True when every desired box is Ready. Callers that + // can start work with a partially ready set should read status.ReadyReplicas + // instead of waiting on this. + SandboxSetConditionReady = "Ready" +) + +// SandboxSet condition reasons. +const ( + // ReasonSandboxSetReady: every desired box is Ready. + ReasonSandboxSetReady = "Ready" + // ReasonSandboxSetProgressing: at least one box is still coming up. Not an error + // — a cold set takes minutes by nature, since each box is a real instance. + ReasonSandboxSetProgressing = "Progressing" + // ReasonSandboxSetScaledToZero: spec.Replicas is 0, so there is nothing to be + // ready. Distinguished from Progressing so a parked set does not read as a stuck + // one. + ReasonSandboxSetScaledToZero = "ScaledToZero" +) + +// SandboxSetStatus is the observed state of the set. +type SandboxSetStatus struct { + // Replicas is how many Sandboxes the set currently owns, ready or not. It is the + // /scale subresource's status counterpart. + // +optional + Replicas int32 `json:"replicas,omitempty"` + + // ReadyReplicas is how many owned Sandboxes are Ready — the number of boxes that + // can actually serve an exec right now. + // +optional + ReadyReplicas int32 `json:"readyReplicas,omitempty"` + + // Selector is the label selector matching this set's Sandboxes, serialized in the + // string form the /scale subresource requires. HPA and KEDA read the target's + // selector from there, so autoscaling a set does not work without it. + // +optional + Selector string `json:"selector,omitempty"` + + // Sandboxes names the boxes this set owns, so the set is a usable handout list: a + // caller reads it to find a box to use without listing and filtering Sandboxes + // itself. Ordered by name for a stable diff. + // +optional + Sandboxes []string `json:"sandboxes,omitempty"` + + // Conditions follows the standard Kubernetes condition convention. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope=Namespaced,shortName=sbxs +// +kubebuilder:subresource:status +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector +// +kubebuilder:printcolumn:name="Desired",type=integer,JSONPath=`.spec.replicas` +// +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas` +// +kubebuilder:printcolumn:name="NodePool",type=string,JSONPath=`.spec.template.spec.nodePoolRef` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// SandboxSet maintains N Sandboxes, so boxes can be kept ready ahead of demand +// instead of making a consumer wait minutes for an instance to provision. +type SandboxSet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SandboxSetSpec `json:"spec,omitempty"` + Status SandboxSetStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// SandboxSetList contains a list of SandboxSet. +type SandboxSetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SandboxSet `json:"items"` +} + +func init() { + SchemeBuilder.Register(&SandboxSet{}, &SandboxSetList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1b63384..2f20efa 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -352,7 +352,7 @@ func (in *SandboxList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SandboxPool) DeepCopyInto(out *SandboxPool) { +func (in *SandboxSet) DeepCopyInto(out *SandboxSet) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -360,18 +360,18 @@ func (in *SandboxPool) DeepCopyInto(out *SandboxPool) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxPool. -func (in *SandboxPool) DeepCopy() *SandboxPool { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSet. +func (in *SandboxSet) DeepCopy() *SandboxSet { if in == nil { return nil } - out := new(SandboxPool) + out := new(SandboxSet) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *SandboxPool) DeepCopyObject() runtime.Object { +func (in *SandboxSet) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -379,31 +379,31 @@ func (in *SandboxPool) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SandboxPoolList) DeepCopyInto(out *SandboxPoolList) { +func (in *SandboxSetList) DeepCopyInto(out *SandboxSetList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]SandboxPool, len(*in)) + *out = make([]SandboxSet, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxPoolList. -func (in *SandboxPoolList) DeepCopy() *SandboxPoolList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSetList. +func (in *SandboxSetList) DeepCopy() *SandboxSetList { if in == nil { return nil } - out := new(SandboxPoolList) + out := new(SandboxSetList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *SandboxPoolList) DeepCopyObject() runtime.Object { +func (in *SandboxSetList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -411,23 +411,23 @@ func (in *SandboxPoolList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SandboxPoolSpec) DeepCopyInto(out *SandboxPoolSpec) { +func (in *SandboxSetSpec) DeepCopyInto(out *SandboxSetSpec) { *out = *in in.Template.DeepCopyInto(&out.Template) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxPoolSpec. -func (in *SandboxPoolSpec) DeepCopy() *SandboxPoolSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSetSpec. +func (in *SandboxSetSpec) DeepCopy() *SandboxSetSpec { if in == nil { return nil } - out := new(SandboxPoolSpec) + out := new(SandboxSetSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SandboxPoolStatus) DeepCopyInto(out *SandboxPoolStatus) { +func (in *SandboxSetStatus) DeepCopyInto(out *SandboxSetStatus) { *out = *in if in.Sandboxes != nil { in, out := &in.Sandboxes, &out.Sandboxes @@ -443,12 +443,12 @@ func (in *SandboxPoolStatus) DeepCopyInto(out *SandboxPoolStatus) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxPoolStatus. -func (in *SandboxPoolStatus) DeepCopy() *SandboxPoolStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SandboxSetStatus. +func (in *SandboxSetStatus) DeepCopy() *SandboxSetStatus { if in == nil { return nil } - out := new(SandboxPoolStatus) + out := new(SandboxSetStatus) in.DeepCopyInto(out) return out } diff --git a/config/crd/bases/nebula.inftyai.com_sandboxes.yaml b/config/crd/bases/nebula.inftyai.com_sandboxes.yaml index a0bc93c..5fdbe45 100644 --- a/config/crd/bases/nebula.inftyai.com_sandboxes.yaml +++ b/config/crd/bases/nebula.inftyai.com_sandboxes.yaml @@ -76,9 +76,9 @@ spec: caller that wants N boxes creates N Sandboxes, each with its own image, lifetime and identity. - The count lives one level up, in SandboxPool, which keeps N Sandboxes warm and + The count lives one level up, in SandboxSet, which maintains N Sandboxes and owns /scale so `kubectl scale` and HPA work. That split is what lets this type - stay singular: because the pool creates Sandbox OBJECTS rather than replicas + stay singular: because the set creates Sandbox OBJECTS rather than replicas inside one object, everything that depends on a box being its own object — per-box RBAC (grant a user their sandbox and not their neighbour's), a per-box image and TTL, and a failure that stays visible instead of being diff --git a/config/crd/bases/nebula.inftyai.com_sandboxpools.yaml b/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml similarity index 88% rename from config/crd/bases/nebula.inftyai.com_sandboxpools.yaml rename to config/crd/bases/nebula.inftyai.com_sandboxsets.yaml index aba11f8..fa384db 100644 --- a/config/crd/bases/nebula.inftyai.com_sandboxpools.yaml +++ b/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml @@ -4,16 +4,16 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.18.0 - name: sandboxpools.nebula.inftyai.com + name: sandboxsets.nebula.inftyai.com spec: group: nebula.inftyai.com names: - kind: SandboxPool - listKind: SandboxPoolList - plural: sandboxpools + kind: SandboxSet + listKind: SandboxSetList + plural: sandboxsets shortNames: - - sbxp - singular: sandboxpool + - sbxs + singular: sandboxset scope: Namespaced versions: - additionalPrinterColumns: @@ -24,7 +24,7 @@ spec: name: Ready type: integer - jsonPath: .spec.template.spec.nodePoolRef - name: Pool + name: NodePool type: string - jsonPath: .metadata.creationTimestamp name: Age @@ -33,8 +33,8 @@ spec: schema: openAPIV3Schema: description: |- - SandboxPool keeps N warm Sandboxes ready to hand out, so a consumer does not - wait minutes for an instance to provision. + SandboxSet maintains N Sandboxes, so boxes can be kept ready ahead of demand + instead of making a consumer wait minutes for an instance to provision. properties: apiVersion: description: |- @@ -55,41 +55,45 @@ spec: type: object spec: description: |- - SandboxPoolSpec keeps N Sandboxes alive. It exists because provisioning a - remote instance takes MINUTES while an agent's exec call wants sub-second: the - only way to serve that is to hold boxes ready before anyone asks. A pool is - also the unit fan-out is expressed on ("twenty boxes for this batch") and the - thing an autoscaler can drive. + SandboxSetSpec maintains N Sandboxes. That is the whole contract, and the name + says exactly that much: it is a SET, not a pool. A pool would imply lease + semantics — claim a box, hold it, return it, with the pool tracking who has what + — and none of that is implemented here. Keeping N boxes alive is what ENABLES + warm pooling (holding instances ready because provisioning takes minutes while + an agent's exec call wants sub-second) and fan-out ("twenty boxes for this + batch"), but those are uses of a set, not the set's job. "Pool" would also be a + third meaning of that word in this API group, where NodePool already means + placement policy. - It creates Sandbox OBJECTS, not replicas inside itself, and that is the whole - point of having two types. The count is genuinely a different concern from the - box: a pool answers "how many", a Sandbox answers "which one, running what, for - whom". Because each box stays its own object underneath the pool, per-box RBAC, - per-box status and a failure that stays visible all keep working — none of which - survives being flattened into a replica index. + It creates Sandbox OBJECTS, not replicas inside itself, and that is the point of + having two types. The count is a genuinely different concern from the box: a set + answers "how many", a Sandbox answers "which one, running what, for whom". + Because each box stays its own object underneath, per-box RBAC, per-box status + and a failure that stays visible all keep working — none of which survives being + flattened into a replica index. - Boxes get GENERATED names (pool-a4f2x), not ordinals. Ordinals would imply a + Boxes get GENERATED names (myset-a4f2x), not ordinals. Ordinals would imply a slot that gets refilled, so a box that died would be replaced by an empty one wearing the same name — the same address with a different filesystem, which is the most confusing thing this API could do. A generated name means a replacement - is visibly a NEW box, and callers that need a stable handle hold the Sandbox - name they were given rather than an index into a set. + is visibly a NEW box, and callers that need a stable handle hold the Sandbox name + they were given rather than an index into a set. properties: replicas: default: 1 description: |- - Replicas is how many Sandboxes to keep. Zero is legal and useful: it releases - every box while keeping the pool's definition, which is how a pool is parked - overnight without being forgotten. + Replicas is how many Sandboxes to maintain. Zero is legal and useful: it + releases every box while keeping the set's definition, which is how a set is + parked overnight without being forgotten. format: int32 minimum: 0 type: integer template: description: |- - Template is the shape of every Sandbox this pool creates. All boxes in one - pool are the same shape by construction — a pool exists to make boxes + Template is the shape of every Sandbox this set creates. All boxes in one set + are the same shape by construction — the set exists to make boxes interchangeable at the point of HANDOUT, so a caller can take any ready box - without inspecting it. Two shapes means two pools. + without inspecting it. Two shapes means two sets. A template is right here for the same reason it is wrong on Sandbox itself: this object does not describe a box, it describes how to make them. @@ -109,12 +113,12 @@ spec: additionalProperties: type: string description: |- - Labels are applied to each created Sandbox, on top of the pool-ownership + Labels are applied to each created Sandbox, on top of the set-ownership labels the controller adds. type: object type: object spec: - description: Spec is the SandboxSpec of every box in the pool. + description: Spec is the SandboxSpec of every box in the set. properties: acceleratorType: description: |- @@ -358,7 +362,7 @@ spec: - template type: object status: - description: SandboxPoolStatus is the observed state of the pool. + description: SandboxSetStatus is the observed state of the set. properties: conditions: description: Conditions follows the standard Kubernetes condition @@ -420,30 +424,29 @@ spec: type: array readyReplicas: description: |- - ReadyReplicas is how many owned Sandboxes are Ready — the number of boxes - that can actually serve an exec right now, which is what "is the pool warm" - really means. + ReadyReplicas is how many owned Sandboxes are Ready — the number of boxes that + can actually serve an exec right now. format: int32 type: integer replicas: description: |- - Replicas is how many Sandboxes the pool currently owns, ready or not. It is - the /scale subresource's status counterpart. + Replicas is how many Sandboxes the set currently owns, ready or not. It is the + /scale subresource's status counterpart. format: int32 type: integer sandboxes: description: |- - Sandboxes names the boxes this pool owns, so the pool is a usable handout - list: a caller reads it to find a box to claim without listing and filtering - Sandboxes itself. Ordered by name for a stable diff. + Sandboxes names the boxes this set owns, so the set is a usable handout list: a + caller reads it to find a box to use without listing and filtering Sandboxes + itself. Ordered by name for a stable diff. items: type: string type: array selector: description: |- - Selector is the label selector matching this pool's Sandboxes, serialized in - the string form the /scale subresource requires. HPA and KEDA read the target's - selector from here, so autoscaling a pool does not work without it. + Selector is the label selector matching this set's Sandboxes, serialized in the + string form the /scale subresource requires. HPA and KEDA read the target's + selector from there, so autoscaling a set does not work without it. type: string type: object type: object diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 201b154..69a09e9 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -25,6 +25,7 @@ rules: - "" resources: - nodes + - pods verbs: - create - delete @@ -42,17 +43,6 @@ rules: - get - patch - update -- apiGroups: - - "" - resources: - - pods - verbs: - - delete - - get - - list - - patch - - update - - watch - apiGroups: - coordination.k8s.io resources: @@ -70,6 +60,7 @@ rules: resources: - nodeclaims - nodepools + - sandboxes verbs: - create - delete @@ -83,6 +74,7 @@ rules: resources: - nodeclaims/finalizers - nodepools/finalizers + - sandboxes/finalizers verbs: - update - apiGroups: @@ -90,6 +82,7 @@ rules: resources: - nodeclaims/status - nodepools/status + - sandboxes/status verbs: - get - patch diff --git a/internal/controller/sandbox_controller.go b/internal/controller/sandbox_controller.go new file mode 100644 index 0000000..502ef58 --- /dev/null +++ b/internal/controller/sandbox_controller.go @@ -0,0 +1,418 @@ +/* +Copyright 2026 The InftyAI Team. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "time" + + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + nebulav1alpha1 "github.com/InftyAI/Nebula/api/v1alpha1" +) + +// sandboxContainerName is the name of the single container in a sandbox's Pod. +// It is fixed rather than user-settable because it is what `kubectl exec` and +// `kubectl logs` default to when no -c is given: a predictable name is the +// difference between `kubectl exec sbx-alice -- bash` working and the user having +// to look up a container name first. +const sandboxContainerName = "sandbox" + +// SandboxReconciler reconciles a Sandbox: it synthesizes the one Pod that backs +// the box, projects that Pod's status back onto the Sandbox, and enforces TTL. +// +// It deliberately does NOT talk to any provider. The Pod is the carrier for +// everything already built — the provider-selection gate, placement, the +// NodeClaim teardown ledger with its finalizer, ResourceQuota accounting — so +// this controller's whole job is to produce a correctly-shaped Pod and get out of +// the way. That is also why a Sandbox is not a bespoke provisioning path: bypassing +// the Pod would mean reimplementing the guarantee that a paid GPU is never leaked. +type SandboxReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=nebula.inftyai.com,resources=sandboxes,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=nebula.inftyai.com,resources=sandboxes/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=nebula.inftyai.com,resources=sandboxes/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete + +// Reconcile drives one Sandbox: ensure its Pod exists (unless the box is done), +// mirror the Pod's state into status, and release the instance when TTL elapses. +func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logf.FromContext(ctx) + + var sbx nebulav1alpha1.Sandbox + if err := r.Get(ctx, req.NamespacedName, &sbx); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + if !sbx.DeletionTimestamp.IsZero() { + // The Pod is ownerRef'd, so garbage collection deletes it, which triggers the + // virtual kubelet's teardown and the NodeClaim finalizer behind it. There is + // nothing for us to clean up, so we hold no finalizer of our own — one here + // would only add a way for the box to get stuck undeletable. + return ctrl.Result{}, nil + } + + // A terminal Sandbox keeps no instance. Its Pod has already been released, and + // recreating one would silently hand the user a DIFFERENT box (empty filesystem, + // new endpoint) under the same name, so we stop here and leave the object as the + // record of what happened. + if isTerminalSandboxPhase(sbx.Status.Phase) { + return ctrl.Result{}, nil + } + + pod, err := r.ensurePod(ctx, &sbx) + if err != nil { + return ctrl.Result{}, err + } + if pod == nil { + // A foreign Pod holds the name we need. Surface it and stop; this needs a human + // (rename the sandbox, or remove the squatter) and retrying cannot fix it. + log.Info("a Pod of this name exists but is not owned by this Sandbox", + "sandbox", sbx.Name, "pod", sbx.Name) + return ctrl.Result{}, r.setStatus(ctx, &sbx, sandboxStatus{ + phase: nebulav1alpha1.SandboxPending, + reason: nebulav1alpha1.ReasonPodConflict, + msg: "a Pod named " + sbx.Name + " already exists and is not owned by this Sandbox", + }) + } + + // TTL is measured from the moment the box became READY, not from creation, so a + // slow provision does not eat into the user's time. That means the deadline only + // exists once ReadyTime is set, and expiry is checked before status is refreshed + // so an expiring box reports Expired rather than briefly re-reporting Ready. + if expired, err := r.enforceTTL(ctx, &sbx, pod); err != nil || expired { + return ctrl.Result{}, err + } + + st := sandboxStatusFromPod(&sbx, pod) + if err := r.setStatus(ctx, &sbx, st); err != nil { + return ctrl.Result{}, err + } + + // Requeue for the exact moment TTL elapses. Nothing emits an event when a + // deadline passes, so without this the box would bill until the next periodic + // resync — the failure mode TTL exists to prevent. + if until, ok := timeUntilExpiry(&sbx); ok { + return ctrl.Result{RequeueAfter: until}, nil + } + return ctrl.Result{}, nil +} + +// ensurePod creates the Sandbox's Pod if absent and returns it. It returns +// (nil, nil) when a Pod of the required name exists but is NOT owned by this +// Sandbox: adoption is refused because the Pod could be an unrelated workload, +// and adopting it would subject someone else's Pod to this Sandbox's lifecycle +// (including deletion on TTL expiry). The caller surfaces that as a condition. +func (r *SandboxReconciler) ensurePod(ctx context.Context, sbx *nebulav1alpha1.Sandbox) (*corev1.Pod, error) { + var existing corev1.Pod + err := r.Get(ctx, client.ObjectKey{Namespace: sbx.Namespace, Name: sbx.Name}, &existing) + if err == nil { + if !isOwnedBy(&existing, sbx) { + return nil, nil + } + return &existing, nil + } + if !apierrors.IsNotFound(err) { + return nil, err + } + + pod := r.buildPod(sbx) + if err := controllerutil.SetControllerReference(sbx, pod, r.Scheme); err != nil { + return nil, err + } + if err := r.Create(ctx, pod); err != nil { + if apierrors.IsAlreadyExists(err) { + // Lost a race with another reconcile (or with a foreign creator). Re-read on + // the next pass rather than guessing which it was. + return nil, nil + } + return nil, err + } + return pod, nil +} + +// buildPod synthesizes the Pod that backs the sandbox. Everything Nebula's +// existing placement path requires is stamped here, so a Sandbox's Pod is +// indistinguishable from a correctly hand-written one — the same webhook gates it, +// the same placement controller places it, the same virtual kubelet provisions it. +func (r *SandboxReconciler) buildPod(sbx *nebulav1alpha1.Sandbox) *corev1.Pod { + labels := map[string]string{ + // EnabledLabel is the opt-in the mutating webhook selects on: without it the + // Pod would be scheduled by vanilla Kubernetes and never reach a provider. + nebulav1alpha1.EnabledLabel: "true", + nebulav1alpha1.ManagedByLabel: nebulav1alpha1.ManagedByValue, + nebulav1alpha1.PoolLabel: sbx.Spec.NodePoolRef, + nebulav1alpha1.SandboxLabel: sbx.Name, + } + if sbx.Spec.AcceleratorType != "" { + // The TYPE is a label and the COUNT is an nvidia.com/gpu resource — the split + // the rest of the system already reads (see util.AcceleratorRequest). A + // CPU-only sandbox sets neither. + labels[nebulav1alpha1.AcceleratorTypeLabel] = sbx.Spec.AcceleratorType + } + + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + // Same name as the Sandbox, so `kubectl exec sbx-alice` and + // `kubectl logs sbx-alice` work with the name the user already knows. The + // name is still published in status.PodName so tooling reads it from there + // rather than depending on this being true forever. + Name: sbx.Name, + Namespace: sbx.Namespace, + Labels: labels, + }, + Spec: corev1.PodSpec{ + // Never restart: the provider owns the instance lifecycle, and a sandbox + // whose instance is gone must surface as Failed rather than being silently + // replaced by an empty box wearing the same name. + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{{ + Name: sandboxContainerName, + Image: sbx.Spec.Image, + // No Command: SandD is PID 1 in the container and spawns the workload as + // its child, which is what lets it own the stdout/stderr pipes that + // `kubectl logs` reads. A command here would displace it. + Resources: sbx.Spec.Resources, + Env: sbx.Spec.Env, + }}, + }, + } +} + +// sandboxStatus is the projection this controller writes, gathered in one struct +// so status is set through a single path (and thus a single Status().Update). +type sandboxStatus struct { + phase nebulav1alpha1.SandboxPhase + reason string + msg string + endpoint string + // ready marks the Ready condition True. It is separate from phase because only + // SandboxReady implies readiness, and conditions and phases are updated together. + ready bool +} + +// sandboxStatusFromPod projects the backing Pod's state onto the Sandbox. The Pod +// (via the virtual kubelet) is the source of truth for what the external instance +// is doing, so this reads it rather than tracking instance state independently — +// two sources for one fact is how they drift. +// +// The mapping keys off the Pod's status REASON, not just its phase, because the +// interesting distinction for a user is inside PodPending: "cannot get capacity" +// (Provisioning) versus "capacity granted, still booting" (Initializing). The +// vnode stamps those reasons (see pkg/vnode/status.go). +func sandboxStatusFromPod(sbx *nebulav1alpha1.Sandbox, pod *corev1.Pod) sandboxStatus { + endpoint := pod.Annotations[nebulav1alpha1.EndpointAnnotation] + + switch pod.Status.Phase { + case corev1.PodRunning: + if isPodReady(pod) { + return sandboxStatus{ + phase: nebulav1alpha1.SandboxReady, + reason: nebulav1alpha1.ReasonSandboxReady, + msg: "the sandbox instance is running and reachable", + endpoint: endpoint, + ready: true, + } + } + return sandboxStatus{ + phase: nebulav1alpha1.SandboxInitializing, + reason: nebulav1alpha1.ReasonSandboxProvisioning, + msg: "the sandbox instance is running but not yet ready", + endpoint: endpoint, + } + case corev1.PodFailed, corev1.PodSucceeded: + // Succeeded lands here too: a sandbox has no notion of completing — SandD is + // PID 1 and only exits when the box goes away — so a terminal Pod means the + // instance is gone either way. + return sandboxStatus{ + phase: nebulav1alpha1.SandboxFailed, + reason: nebulav1alpha1.ReasonSandboxFailed, + msg: podFailureMessage(pod), + endpoint: endpoint, + } + default: + // Pending. A Pod still held by the provider-selection gate has not been placed + // at all, which is a different problem from a provision in flight: it means no + // provider in the pool can serve this box right now. Distinguishing them is + // what stops a capacity problem from looking like a slow boot. + if hasGateNamed(pod) { + return sandboxStatus{ + phase: nebulav1alpha1.SandboxPending, + reason: nebulav1alpha1.ReasonSandboxProvisioning, + msg: "waiting for placement onto a provider", + } + } + phase := nebulav1alpha1.SandboxProvisioning + if pod.Status.Reason == podReasonInitializing { + phase = nebulav1alpha1.SandboxInitializing + } + return sandboxStatus{ + phase: phase, + reason: nebulav1alpha1.ReasonSandboxProvisioning, + msg: podStatusMessage(pod, "bringing the sandbox instance up"), + endpoint: endpoint, + } + } +} + +// setStatus writes the projection, stamping ReadyTime/ExpiryTime on the first +// transition to Ready. It skips the API call when nothing changed, so a Sandbox +// that is simply sitting Ready does not generate an update per resync. +func (r *SandboxReconciler) setStatus(ctx context.Context, sbx *nebulav1alpha1.Sandbox, st sandboxStatus) error { + before := sbx.Status.DeepCopy() + + sbx.Status.Phase = st.phase + sbx.Status.PodName = sbx.Name + if st.endpoint != "" { + sbx.Status.Endpoint = st.endpoint + } + + // ReadyTime is durable and written exactly once: it anchors TTL, so recomputing + // it from the Pod would let a status blip silently restart the user's clock. + if st.phase == nebulav1alpha1.SandboxReady && sbx.Status.ReadyTime == nil { + now := metav1.Now() + sbx.Status.ReadyTime = &now + if ttl := sbx.Spec.TTL; ttl != nil && ttl.Duration > 0 { + expiry := metav1.NewTime(now.Add(ttl.Duration)) + sbx.Status.ExpiryTime = &expiry + } + } + + condStatus := metav1.ConditionFalse + if st.ready { + condStatus = metav1.ConditionTrue + } + apimeta.SetStatusCondition(&sbx.Status.Conditions, metav1.Condition{ + Type: nebulav1alpha1.SandboxConditionReady, + Status: condStatus, + Reason: st.reason, + Message: st.msg, + ObservedGeneration: sbx.Generation, + }) + + // Skip the write when only the condition's LastTransitionTime would differ — + // SetStatusCondition preserves it when Status is unchanged, so a semantic compare + // is enough to keep a steady-state Ready sandbox from generating an update per + // resync (each of which would wake every watcher). + if apiequality.Semantic.DeepEqual(before, &sbx.Status) { + return nil + } + return r.Status().Update(ctx, sbx) +} + +// enforceTTL releases the instance when the box's deadline has passed, by +// deleting the backing Pod — which is what triggers the virtual kubelet's +// teardown and, behind it, the NodeClaim finalizer that guarantees the paid +// instance is actually reclaimed. Deleting the Pod (rather than the Sandbox) +// leaves the object as the record of why the box went away, so a user returning to +// an expired sandbox gets an answer instead of a NotFound. +// +// It returns expired=true when it acted, so the caller stops reconciling this pass. +func (r *SandboxReconciler) enforceTTL(ctx context.Context, sbx *nebulav1alpha1.Sandbox, pod *corev1.Pod) (bool, error) { + if sbx.Status.ExpiryTime == nil || time.Now().Before(sbx.Status.ExpiryTime.Time) { + return false, nil + } + + // UID-pinned so a Pod already replaced by something else is never clobbered; + // an already-gone Pod is success, not an error. + preconditions := metav1.Preconditions{UID: &pod.UID} + if err := r.Delete(ctx, pod, &client.DeleteOptions{Preconditions: &preconditions}); err != nil { + if !apierrors.IsNotFound(err) && !apierrors.IsConflict(err) { + return false, err + } + } + return true, r.setStatus(ctx, sbx, sandboxStatus{ + phase: nebulav1alpha1.SandboxExpired, + reason: nebulav1alpha1.ReasonSandboxExpired, + msg: "ttl elapsed; the sandbox instance was released", + }) +} + +// timeUntilExpiry reports how long until the box's TTL elapses, and whether +// there is a deadline at all. A deadline already in the past returns a small +// positive delay rather than zero, because a zero RequeueAfter means "do not +// requeue" to controller-runtime — precisely the wrong reading for an overdue box. +func timeUntilExpiry(sbx *nebulav1alpha1.Sandbox) (time.Duration, bool) { + if sbx.Status.ExpiryTime == nil { + return 0, false + } + until := time.Until(sbx.Status.ExpiryTime.Time) + if until <= 0 { + return time.Second, true + } + return until, true +} + +// isTerminalSandboxPhase reports whether the box is done and holds no instance. +func isTerminalSandboxPhase(p nebulav1alpha1.SandboxPhase) bool { + return p == nebulav1alpha1.SandboxExpired || p == nebulav1alpha1.SandboxFailed +} + +// isOwnedBy reports whether obj is controlled by the given Sandbox, matching on +// UID so a recreated Sandbox of the same name does not adopt the old box's Pod. +func isOwnedBy(obj client.Object, sbx *nebulav1alpha1.Sandbox) bool { + ref := metav1.GetControllerOf(obj) + return ref != nil && ref.UID == sbx.UID +} + +// isPodReady reports whether the Pod's Ready condition is True. The virtual +// kubelet sets it when the provider reports the instance running and reachable. +func isPodReady(pod *corev1.Pod) bool { + for _, c := range pod.Status.Conditions { + if c.Type == corev1.PodReady { + return c.Status == corev1.ConditionTrue + } + } + return false +} + +// podFailureMessage explains why the box died, preferring the Pod's own message +// (the vnode writes a specific one: provision rejected, instance gone, instance +// failed) over a generic fallback. +func podFailureMessage(pod *corev1.Pod) string { + return podStatusMessage(pod, "the sandbox instance is no longer running") +} + +// podStatusMessage returns the Pod's status message, or fallback when it has none. +func podStatusMessage(pod *corev1.Pod, fallback string) string { + if pod.Status.Message != "" { + return pod.Status.Message + } + return fallback +} + +// SetupWithManager wires the controller. It owns its Pod, so a Pod status change +// (the instance coming up, failing, or vanishing) re-reconciles the Sandbox +// immediately instead of waiting for the periodic resync. +func (r *SandboxReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&nebulav1alpha1.Sandbox{}). + Owns(&corev1.Pod{}). + Named("sandbox"). + Complete(r) +} From 5603591dedfc6c628c5bc774cbdeb3345bac450c Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 09:27:41 +0100 Subject: [PATCH 11/22] controller: add SandboxSet controller and wire both into the manager The set prunes terminal boxes so it can replace them: the Sandbox controller deliberately never resurrects a dead box (a fresh instance would be a different box under the same name), so replacement has to be the set's job. Scale-in picks cheapest-to-lose first (terminal, then not-ready, then youngest ready) rather than StatefulSet's highest-ordinal, which here could kill a box in active use while a dead one sits beside it. Co-Authored-By: Claude Opus 5 --- cmd/main.go | 19 + config/crd/kustomization.yaml | 2 + config/rbac/role.yaml | 4 + config/samples/nebula_v1alpha1_sandbox.yaml | 35 ++ .../samples/nebula_v1alpha1_sandboxset.yaml | 39 ++ docs/proposal/architecture.md | 162 +------- docs/proposal/tailnet.png | Bin 0 -> 299772 bytes internal/controller/sandboxset_controller.go | 349 ++++++++++++++++++ 8 files changed, 464 insertions(+), 146 deletions(-) create mode 100644 config/samples/nebula_v1alpha1_sandbox.yaml create mode 100644 config/samples/nebula_v1alpha1_sandboxset.yaml create mode 100644 docs/proposal/tailnet.png create mode 100644 internal/controller/sandboxset_controller.go diff --git a/cmd/main.go b/cmd/main.go index d70e105..2cb490f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -254,6 +254,25 @@ func main() { os.Exit(1) } + // The workload controllers sit on top of the provisioning core above: each + // synthesizes objects onto the same placement path rather than talking to a + // provider itself. Sandbox produces the Pod that backs one remote box; + // SandboxSet produces Sandboxes. + if err := (&controller.SandboxReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Sandbox") + os.Exit(1) + } + if err := (&controller.SandboxSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "SandboxSet") + os.Exit(1) + } + // Start one virtual node per registered provider. The virtual kubelet owns // provisioning: its pod controller calls provider.Provision on CreatePod and // provider.Terminate on DeletePod, so an ungated Pod bound to a provider's diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b8628ac..73b1328 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -4,6 +4,8 @@ resources: - bases/nebula.inftyai.com_nodepools.yaml - bases/nebula.inftyai.com_nodeclaims.yaml +- bases/nebula.inftyai.com_sandboxes.yaml +- bases/nebula.inftyai.com_sandboxsets.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 69a09e9..1371607 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -61,6 +61,7 @@ rules: - nodeclaims - nodepools - sandboxes + - sandboxsets verbs: - create - delete @@ -75,6 +76,7 @@ rules: - nodeclaims/finalizers - nodepools/finalizers - sandboxes/finalizers + - sandboxsets/finalizers verbs: - update - apiGroups: @@ -83,6 +85,8 @@ rules: - nodeclaims/status - nodepools/status - sandboxes/status + - sandboxsets/scale + - sandboxsets/status verbs: - get - patch diff --git a/config/samples/nebula_v1alpha1_sandbox.yaml b/config/samples/nebula_v1alpha1_sandbox.yaml new file mode 100644 index 0000000..dcf91c2 --- /dev/null +++ b/config/samples/nebula_v1alpha1_sandbox.yaml @@ -0,0 +1,35 @@ +# One interactive remote box. Reach it exactly as you would a local Pod: +# +# kubectl exec -it sample -- bash +# kubectl logs sample +# +# Note there is no command: SandD runs as PID 1 in the container and spawns the +# workload as its child, which is what lets it own the stdout/stderr pipes that +# `kubectl logs` reads. Setting a command would displace it, so admission rejects +# one. An interactive box has nothing to run at boot anyway — commands arrive +# later, over exec. +apiVersion: nebula.inftyai.com/v1alpha1 +kind: Sandbox +metadata: + labels: + app.kubernetes.io/managed-by: nebula + name: sample +spec: + # The placement policy this box is provisioned against: which providers are + # allowed, which capacity tiers, how to rank them. + nodePoolRef: sample + image: ubuntu:24.04 + # The accelerator TYPE is a label-shaped field; the COUNT rides on the + # nvidia.com/gpu resource below, so exactly one number drives both scheduling + # fit and provisioning. Omit both for a CPU-only box. + acceleratorType: a100-40gb + resources: + requests: + cpu: "8" + memory: 64Gi + limits: + nvidia.com/gpu: "1" + # Measured from the moment the box becomes READY, not from creation, so a slow + # provision does not eat into the user's time. Omit for an unbounded box — + # a deliberate choice, since an abandoned GPU box bills until someone notices. + ttl: 8h diff --git a/config/samples/nebula_v1alpha1_sandboxset.yaml b/config/samples/nebula_v1alpha1_sandboxset.yaml new file mode 100644 index 0000000..3254929 --- /dev/null +++ b/config/samples/nebula_v1alpha1_sandboxset.yaml @@ -0,0 +1,39 @@ +# Maintains N Sandboxes. Provisioning an instance takes MINUTES while an agent's +# exec call wants sub-second, so boxes are kept ready ahead of demand: +# +# kubectl get sandboxes -l nebula.inftyai.com/sandboxset=sample +# kubectl scale sandboxset/sample --replicas=5 +# +# Boxes get generated names (sample-a4f2x), not ordinals, because an ordinal would +# imply a slot that gets refilled — a replacement box wearing a dead box's name is +# the same address with a different filesystem. A generated name makes a +# replacement visibly a new box. +# +# A template change does NOT roll existing boxes (this is ReplicaSet-shaped, not +# Deployment-shaped): rolling would evict live sessions and burn minutes of +# provisioning per box. New boxes get the new template; existing ones are left alone. +apiVersion: nebula.inftyai.com/v1alpha1 +kind: SandboxSet +metadata: + labels: + app.kubernetes.io/managed-by: nebula + name: sample +spec: + replicas: 3 + template: + metadata: + labels: + team: ml + spec: + nodePoolRef: sample + image: ubuntu:24.04 + acceleratorType: a100-40gb + resources: + requests: + cpu: "8" + memory: 64Gi + limits: + nvidia.com/gpu: "1" + # Per box, from its own ready time. In a set this acts as a RECYCLE interval: + # an expired box is pruned and the set creates a fresh one to replace it. + ttl: 8h diff --git a/docs/proposal/architecture.md b/docs/proposal/architecture.md index 399581f..d9590da 100644 --- a/docs/proposal/architecture.md +++ b/docs/proposal/architecture.md @@ -13,12 +13,8 @@ Contents: - [Goals and non-goals](#goals-and-non-goals) - [Workload classes](#workload-classes) -- [Decisions](#decisions) +- [Alternatives](#alternatives) - [1. Dial-out, not a mesh](#1-dial-out-not-a-mesh) - - [2. The kubelet API lives on the virtual node](#2-the-kubelet-api-lives-on-the-virtual-node) - - [3. SandD is PID 1](#3-sandd-is-pid-1) - - [4. A Sandbox CRD, above the Pod](#4-a-sandbox-crd-above-the-pod) - - [5. Inbound is a provider capability](#5-inbound-is-a-provider-capability) - [Roadmap](#roadmap) --- @@ -27,7 +23,7 @@ Contents: **Goals** -- Kubernetes native API surface for every workload class +- Support AWS, Modal as starting providers, with more to come. - `kubectl logs` / `exec` / `attach` against a remote instance, natively. offer no native exec API and no inbound reachability. - In-cluster clients can reach inference and services @@ -36,9 +32,11 @@ Contents: **Non-goals (for now)** -- **Gang scheduling and intra-gang connectivity** — multi-instance workloads that +- Gang scheduling and intra-gang connectivity — multi-instance workloads that provision all-or-nothing and can reach each other. Deferred, not abandoned: it is what training and fine-tuning need. +- More providers than AWS and Modal. The architecture should be provider-agnostic, but + the implementation will be provider-specific. **Non-goals** @@ -59,7 +57,15 @@ Three classes, three phases. --- -## Decisions +## Implementation Details + +### Authentication + +### SandD + +### Virtual Kubelet + +## Alternatives ### 1. Dial-out, not a mesh @@ -89,119 +95,6 @@ problem and needs less development work since coordinator already exists, but he **Revisit if** dial-in becomes a product feature: operator SSH into any box, a pull-based metrics scraper, or instance-to-instance traffic *across* providers. -### 2. The kubelet API lives on the virtual node - -**Decision.** Implement the kubelet HTTP surface on the virtual node and serve -logs/exec/attach from it, forwarding over the instance's WebSocket. - -``` -kubectl exec pod-x - └─▶ apiserver ──HTTPS──▶ virtual-node kubelet endpoint (Nebula manager) - └─▶ map[claim]conn ──▶ SandD ──▶ container -``` - -**What it buys.** Every consumer works unmodified — `kubectl`, `client-go`, other -languages' clients, k9s/Lens/ArgoCD, our own controllers — and authorization is -RBAC on `pods/exec` and `pods/log`, namespace-scoped, so a tenant's workload can only exec into its own Pods. - -Each subresource is a separate handler: - -| Subresource | Handler | Phase | -|---|---|---| -| `exec`, `attach` | `RunInContainer`, `AttachToContainer` | 1 | -| `logs` | `GetContainerLogs` | 1 | -| `port-forward` | `PortForward` | later | -| `top` | `GetStatsSummary` | later | - -### 3. SandD is PID 1 - -**Decision.** SandD runs as PID 1 in the workload container and spawns the -workload as its **child**, owning its stdout/stderr pipes. - -### 4. A Sandbox CRD, above the Pod - -**Decision.** Add one namespaced `Sandbox` CRD with `spec.replicas`. It **creates N -Pods** with stable ordinal identity — StatefulSet-shaped, not Deployment-shaped — and -does not replace or bypass the Pod. No class object, no `type` discriminator. Other -shapes (Notebook, and later a training Job) become their own CRDs when they need -their own spec, each reusing the same Pod path underneath. - -``` -Sandbox (replicas: 3) - ├── creates Pod pool-0, pool-1, pool-2 (ownerRef'd, restartPolicy: Never, NO command) - │ └── gate → placement → nodeSelector → virtual node - │ └── VK CreatePod → NodeClaim → instance - └── owns lifecycle: scale, stop/start, TTL, idle-cull, per-replica endpoints -``` - -**Why replicas.** Two needs, one field: a **warm pool**, because an instance takes -minutes to become Ready while an agent-exec call wants sub-second; and **fan-out**, -one task across 20 boxes without 20 objects. Implement `/scale` so `kubectl scale` -and HPA/KEDA work. - -**Ordinal, not fungible.** Each replica is `-` with its own claim -and sessions, so `kubectl exec pool-3` is repeatable. That is why a Deployment is -still wrong despite having replicas: its replicas are interchangeable, and a rolling -update would swap a user's box out mid-session. - -**Why a Pod underneath.** The Pod is the carrier of everything already built: -placement, `pkg/failover`'s blocklist, and — critically — the NodeClaim finalizer -that guarantees a paid instance is never leaked. It is also what ResourceQuota -counts, which is how one tenant is stopped from provisioning fifty H100s. Bypassing -it would mean reimplementing all of that. - -```yaml -kind: Sandbox -metadata: {name: pool, namespace: team-ml} -spec: - replicas: 3 - nodePoolRef: gpu # existing placement policy object - template: # a PodSpec — the shape of one replica - metadata: - labels: - nebula.inftyai.com/accelerator-type: a100-40gb - spec: - containers: - - name: sandbox - image: ubuntu:24.04 - # no command: SandD is PID 1 - resources: - requests: {cpu: "8", memory: 64Gi} - limits: {nvidia.com/gpu: "1"} -status: - replicas: 3 - readyReplicas: 2 - instances: # per replica — the addressable unit - - name: pool-0 - phase: Ready - endpoints: {exec: ..., pty: ...} - lastActivityTime: "2026-08-07T09:12:00Z" - - name: pool-1 - phase: Ready - lastActivityTime: "2026-08-07T09:40:00Z" - - name: pool-2 - phase: Provisioning -``` - -**A PodSpec template, not flat fields.** The controller synthesizes Pods, so what it -accepts must eventually *be* a PodSpec — flat fields would mean re-inventing -`resources`, `env`, `volumeMounts`, and `securityContext` one at a time. Resources -matter most: the GPU count is a standard `nvidia.com/gpu` limit, which is already -where placement and the scheduler's fit check read it from, so a parallel -`accelerator` count field would fork the source of truth. The accelerator *type* -stays a label, exactly as on a hand-written Nebula Pod, so both go through identical -placement. - -Nebula fills in on the synthesized Pod: the opt-in labels, the scheduling gate, the -virtual-node toleration, `restartPolicy: Never`, and the container command (SandD). A -webhook rejects a template that sets `command` rather than silently dropping it. - -**Open question — scale-in must not be ordinal.** StatefulSet removes the highest -ordinal; here `pool-4` may hold a live session while `pool-1` has been idle an hour. -Removal must pick the least recently active replica, with `status.instances` as the -input. So ordinals are stable names but not a removal order, culling leaves ordinal -gaps, and an in-flight exec must either block scale-in briefly or fail clearly. - --- ## Roadmap @@ -210,35 +103,12 @@ Each phase builds the seam the next one needs. Nothing is built twice. ### Phase 1 — Sandbox, agent, shell -*Goal: `kubectl exec`/`logs` work against a remote box, driven by a CRD.* +*Goal: `kubectl exec`/`logs` work against a remote box, driven by a CRD.* Support AWS as the first provider. ### Phase 2 — Inference and services -*Goal: `my-llm.default.svc` resolves to an off-cluster instance.* - -1. Named addresses + `Reachability` (if not pulled into Phase 1). -2. `Capabilities` access flags; per-workload SandD opt-in. -3. **Real readiness probing** gating the Ready condition — a correctness fix. -4. EndpointSlice bridge: selector-less Service + Nebula-managed endpoints. -5. Per-provider inbound: Modal native (nearly free), AWS security groups. +*Goal: `my-llm.default.svc` resolves to an off-cluster instance.* Support Modal as the first provider. ### Phase 3 — Training, fine-tuning, jobs *Goal: multi-instance, all-or-nothing, mutually reachable.* - -1. **Gang scheduling** — N instances all-or-nothing, so a partially provisioned - 8-node job does not bill while waiting. `NodeClaim.spec.podRef` is 1:1 - (`api/v1alpha1/nodeclaim_types.go:18`), so this needs a group concept. It is the - one genuinely new primitive in this document. -2. **Intra-gang L3** — shared subnet, placement group, EFA/RDMA, allow-from-self. - Provider-native, never an overlay. -3. **Rank injection** — `RANK`, `WORLD_SIZE`, `MASTER_ADDR`, stable ordinals. -4. **Job semantics** — exit code → terminal `Succeeded`/`Failed`. Mostly free from - Phase 1's PID-1 SandD, which already reaps the child; `applyState` needs to - represent it. -5. **Shared storage** for checkpoints — the same missing provider field as notebook - persistence. Nothing in the AWS adapter wires block devices today. - -Logs and exec come free from Phase 1: same SandD, same connection. - ---- diff --git a/docs/proposal/tailnet.png b/docs/proposal/tailnet.png new file mode 100644 index 0000000000000000000000000000000000000000..cbdbcdce3d43b1054a59cc9450fd85f2bf104793 GIT binary patch literal 299772 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

3k7 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 Mq>@D0bK|%FAINwNhyVZp literal 0 HcmV?d00001 diff --git a/internal/controller/sandboxset_controller.go b/internal/controller/sandboxset_controller.go new file mode 100644 index 0000000..03b5a34 --- /dev/null +++ b/internal/controller/sandboxset_controller.go @@ -0,0 +1,349 @@ +/* +Copyright 2026 The InftyAI Team. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "sort" + + apiequality "k8s.io/apimachinery/pkg/api/equality" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + nebulav1alpha1 "github.com/InftyAI/Nebula/api/v1alpha1" +) + +// SandboxSetReconciler reconciles a SandboxSet: it creates and deletes Sandbox +// objects so that spec.Replicas of them exist, and rolls their readiness up into +// the set's status. +// +// It deliberately knows nothing about Pods, instances, or providers. Its entire +// vocabulary is Sandbox objects, and the Sandbox controller handles what one box +// means — which is what makes the two-type split pay off: scaling logic and box +// lifecycle never tangle. +// +// A template change does NOT roll existing boxes. This mirrors ReplicaSet, not +// Deployment, and here it is the only defensible behaviour: a rolling update would +// evict live sessions and burn minutes of provisioning per box to deliver a change +// nobody attached to a running sandbox asked for. New boxes get the new template; +// existing ones are left alone until something else removes them. +type SandboxSetReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=nebula.inftyai.com,resources=sandboxsets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=nebula.inftyai.com,resources=sandboxsets/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=nebula.inftyai.com,resources=sandboxsets/finalizers,verbs=update +// +kubebuilder:rbac:groups=nebula.inftyai.com,resources=sandboxsets/scale,verbs=get;update;patch +// +kubebuilder:rbac:groups=nebula.inftyai.com,resources=sandboxes,verbs=get;list;watch;create;update;patch;delete + +// Reconcile brings the owned Sandbox count to spec.Replicas and refreshes status. +func (r *SandboxSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logf.FromContext(ctx) + + var set nebulav1alpha1.SandboxSet + if err := r.Get(ctx, req.NamespacedName, &set); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + if !set.DeletionTimestamp.IsZero() { + // Owned Sandboxes are ownerRef'd, so garbage collection removes them and each + // Sandbox controller releases its own instance. Nothing to do here, and no + // finalizer to hold — one would only be a way to get stuck. + return ctrl.Result{}, nil + } + + owned, err := r.ownedSandboxes(ctx, &set) + if err != nil { + return ctrl.Result{}, err + } + + // Prune boxes that are dead but still counting toward the total. Without this a + // set of 3 that loses one instance sits at "3 replicas, 2 usable" forever: the + // Sandbox controller will not resurrect a terminal box (a fresh instance would be + // a different box wearing the same name), so the only way back to 3 usable boxes + // is for the SET to replace it. Deleting here makes the next branch see the + // shortfall and create a replacement on this same pass. + if pruned, err := r.pruneTerminal(ctx, owned); err != nil { + return ctrl.Result{}, err + } else if pruned > 0 { + log.Info("pruned terminal sandboxes for replacement", + "sandboxset", set.Name, "count", pruned) + if owned, err = r.ownedSandboxes(ctx, &set); err != nil { + return ctrl.Result{}, err + } + } + + switch diff := int(set.Spec.Replicas) - len(owned); { + case diff > 0: + if err := r.scaleUp(ctx, &set, diff); err != nil { + return ctrl.Result{}, err + } + log.Info("created sandboxes", "sandboxset", set.Name, "count", diff) + case diff < 0: + victims := selectForRemoval(owned, -diff) + if err := r.scaleDown(ctx, victims); err != nil { + return ctrl.Result{}, err + } + log.Info("deleted sandboxes", "sandboxset", set.Name, "count", len(victims)) + } + + // Re-list after mutating so status reflects what actually exists rather than what + // we intended. A create that was rejected (quota, admission) must not be reported + // as a replica. + if owned, err = r.ownedSandboxes(ctx, &set); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, r.setStatus(ctx, &set, owned) +} + +// ownedSandboxes lists the Sandboxes this set owns, sorted by name so both the +// removal choice and the reported list are deterministic. +// +// It filters by ownerReference UID rather than trusting the label selector alone: +// the label is user-visible and could be applied to a foreign Sandbox, and acting +// on that would let anyone get a box they do not own deleted by our scale-in. +func (r *SandboxSetReconciler) ownedSandboxes(ctx context.Context, set *nebulav1alpha1.SandboxSet) ([]nebulav1alpha1.Sandbox, error) { + var list nebulav1alpha1.SandboxList + if err := r.List(ctx, &list, + client.InNamespace(set.Namespace), + client.MatchingLabels{nebulav1alpha1.SandboxSetLabel: set.Name}, + ); err != nil { + return nil, err + } + + owned := make([]nebulav1alpha1.Sandbox, 0, len(list.Items)) + for i := range list.Items { + sbx := list.Items[i] + if ref := metav1.GetControllerOf(&sbx); ref == nil || ref.UID != set.UID { + continue + } + if !sbx.DeletionTimestamp.IsZero() { + continue // already going away; do not count it toward the desired total + } + owned = append(owned, sbx) + } + sort.Slice(owned, func(i, j int) bool { return owned[i].Name < owned[j].Name }) + return owned, nil +} + +// scaleUp creates n new Sandboxes from the set's template. +func (r *SandboxSetReconciler) scaleUp(ctx context.Context, set *nebulav1alpha1.SandboxSet, n int) error { + for range n { + sbx := r.buildSandbox(set) + if err := controllerutil.SetControllerReference(set, sbx, r.Scheme); err != nil { + return err + } + if err := r.Create(ctx, sbx); err != nil { + // Surface the first failure rather than pressing on: if creates are being + // rejected (quota, admission), the next one fails the same way, and a partial + // scale-up plus a real error is more useful than n identical errors. + return err + } + } + return nil +} + +// buildSandbox stamps one Sandbox out of the template. The name is GENERATED +// (metadata.generateName) rather than an ordinal: an ordinal implies a slot that +// gets refilled, so a replacement box would wear a dead box's name — same address, +// different filesystem. A generated name makes a replacement visibly a new box. +func (r *SandboxSetReconciler) buildSandbox(set *nebulav1alpha1.SandboxSet) *nebulav1alpha1.Sandbox { + labelSet := map[string]string{} + for k, v := range set.Spec.Template.Metadata.Labels { + labelSet[k] = v + } + // Applied last so a template cannot overwrite the ownership label the set + // selects on — doing so would orphan the box from its own set. + labelSet[nebulav1alpha1.SandboxSetLabel] = set.Name + labelSet[nebulav1alpha1.ManagedByLabel] = nebulav1alpha1.ManagedByValue + + var annotations map[string]string + if len(set.Spec.Template.Metadata.Annotations) > 0 { + annotations = make(map[string]string, len(set.Spec.Template.Metadata.Annotations)) + for k, v := range set.Spec.Template.Metadata.Annotations { + annotations[k] = v + } + } + + return &nebulav1alpha1.Sandbox{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: set.Name + "-", + Namespace: set.Namespace, + Labels: labelSet, + Annotations: annotations, + }, + Spec: *set.Spec.Template.Spec.DeepCopy(), + } +} + +// pruneTerminal deletes owned boxes that have reached a terminal phase, returning +// how many it removed. A terminal box holds no instance and will never come back — +// the Sandbox controller refuses to recreate one, deliberately, so that a user is +// never silently handed an empty box under the name of the one they were working in. +// Replacement is therefore the SET's job, and it starts with removing the corpse. +// +// This is also what makes TTL a recycle interval for a set and a hard deadline for a +// standalone Sandbox: same expiry, but here the set notices the shortfall and creates +// a fresh box. +func (r *SandboxSetReconciler) pruneTerminal(ctx context.Context, owned []nebulav1alpha1.Sandbox) (int, error) { + var pruned int + for i := range owned { + sbx := &owned[i] + if !isTerminalSandboxPhase(sbx.Status.Phase) { + continue + } + preconditions := metav1.Preconditions{UID: &sbx.UID} + if err := r.Delete(ctx, sbx, &client.DeleteOptions{Preconditions: &preconditions}); err != nil { + if err = client.IgnoreNotFound(err); err != nil { + return pruned, err + } + } + pruned++ + } + return pruned, nil +} + +// scaleDown deletes the chosen boxes. +func (r *SandboxSetReconciler) scaleDown(ctx context.Context, victims []nebulav1alpha1.Sandbox) error { + for i := range victims { + v := &victims[i] + // UID-pinned so a box already replaced by a same-named recreate is never + // clobbered; an already-gone box is success. + preconditions := metav1.Preconditions{UID: &v.UID} + if err := r.Delete(ctx, v, &client.DeleteOptions{Preconditions: &preconditions}); err != nil { + return client.IgnoreNotFound(err) + } + } + return nil +} + +// selectForRemoval picks which n boxes to delete on scale-in, cheapest-to-lose +// first. Scale-in has to name a victim, and the boxes are NOT interchangeable once +// someone is working in one, so the order is chosen to minimise destroyed work: +// +// 1. Terminal boxes — already worthless. pruneTerminal normally removes these +// before we get here, so this rank is a safety net for a box that turned +// terminal between the prune and this call. +// 2. Not-yet-Ready boxes — nobody can have been using a box that was never +// reachable. Youngest first, so the box closest to becoming useful survives. +// 3. Ready boxes — youngest first, on the reasoning that the most recently created +// box is the least likely to have been claimed and worked in. +// +// This is deliberately NOT StatefulSet's highest-ordinal rule, which here would +// mean "kill whichever box happens to sort last" — including one in active use +// while a failed box sits beside it. +// +// The rule we actually want for step 3 is least-recently-USED, so an idle box goes +// before one holding a live session. That needs per-box activity data, which only +// SandD can report and does not yet; "youngest Ready" is the best available proxy +// until it does. +func selectForRemoval(owned []nebulav1alpha1.Sandbox, n int) []nebulav1alpha1.Sandbox { + if n >= len(owned) { + return owned + } + + candidates := make([]nebulav1alpha1.Sandbox, len(owned)) + copy(candidates, owned) + sort.SliceStable(candidates, func(i, j int) bool { + ri, rj := removalRank(&candidates[i]), removalRank(&candidates[j]) + if ri != rj { + return ri < rj + } + // Within a rank, youngest first. + return candidates[i].CreationTimestamp.After(candidates[j].CreationTimestamp.Time) + }) + return candidates[:n] +} + +// removalRank orders boxes by how little it costs to lose them: lower goes first. +func removalRank(sbx *nebulav1alpha1.Sandbox) int { + switch sbx.Status.Phase { + case nebulav1alpha1.SandboxFailed, nebulav1alpha1.SandboxExpired: + return 0 // dead already + case nebulav1alpha1.SandboxReady: + return 2 // possibly in use — last resort + default: + return 1 // still coming up: nobody has used it yet + } +} + +// setStatus rolls the owned boxes up into the set's status, including the selector +// the /scale subresource needs. It skips the write when nothing changed, so a +// steady-state set does not generate an update per resync. +func (r *SandboxSetReconciler) setStatus(ctx context.Context, set *nebulav1alpha1.SandboxSet, owned []nebulav1alpha1.Sandbox) error { + before := set.Status.DeepCopy() + + var ready int32 + names := make([]string, 0, len(owned)) + for i := range owned { + names = append(names, owned[i].Name) + if owned[i].Status.Phase == nebulav1alpha1.SandboxReady { + ready++ + } + } + + set.Status.Replicas = int32(len(owned)) + set.Status.ReadyReplicas = ready + set.Status.Sandboxes = names + // The /scale subresource requires the selector as a serialized string; HPA reads + // it from here to find the set's members, so autoscaling silently does nothing + // without it. + set.Status.Selector = labels.SelectorFromSet(labels.Set{ + nebulav1alpha1.SandboxSetLabel: set.Name, + }).String() + + reason, msg, condStatus := nebulav1alpha1.ReasonSandboxSetProgressing, + "waiting for sandboxes to become ready", metav1.ConditionFalse + switch { + case set.Spec.Replicas == 0: + reason, msg = nebulav1alpha1.ReasonSandboxSetScaledToZero, "scaled to zero" + case ready == set.Spec.Replicas: + reason, msg, condStatus = nebulav1alpha1.ReasonSandboxSetReady, + "all sandboxes are ready", metav1.ConditionTrue + } + apimeta.SetStatusCondition(&set.Status.Conditions, metav1.Condition{ + Type: nebulav1alpha1.SandboxSetConditionReady, + Status: condStatus, + Reason: reason, + Message: msg, + ObservedGeneration: set.Generation, + }) + + if apiequality.Semantic.DeepEqual(before, &set.Status) { + return nil + } + return r.Status().Update(ctx, set) +} + +// SetupWithManager wires the controller. It owns Sandboxes, so a box becoming +// ready or failing re-reconciles the set immediately — which is what makes +// self-healing prompt: a Failed box is removed and replaced on that same event +// rather than at the next resync. +func (r *SandboxSetReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&nebulav1alpha1.SandboxSet{}). + Owns(&nebulav1alpha1.Sandbox{}). + Named("sandboxset"). + Complete(r) +} From 31b992e8dd16280937297cf098a10fd13a84559c Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 09:35:38 +0100 Subject: [PATCH 12/22] controller: set SandD as the Pod's command The Pod is the single source of truth for what runs on the instance (the rule ProvisionRequest already documents for image/env/resources), so the command belongs here, not duplicated in each provider bootstrap. Every adapter already reads it off the Pod, so this needs no per-provider code and cannot drift between providers. SanddPath is shared because it is a two-ended contract: the controller writes it as the command, and each bootstrap must make the binary appear there, since the user's image does not contain it. Co-Authored-By: Claude Opus 5 --- api/v1alpha1/groupversion_info.go | 12 +++++++++++ api/v1alpha1/sandbox_types.go | 17 ++++++++------- .../bases/nebula.inftyai.com_sandboxes.yaml | 17 ++++++++------- .../bases/nebula.inftyai.com_sandboxsets.yaml | 17 ++++++++------- config/samples/nebula_v1alpha1_sandbox.yaml | 9 ++++---- internal/controller/sandbox_controller.go | 21 ++++++++++++++++--- 6 files changed, 64 insertions(+), 29 deletions(-) diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index 2dad0df..0566901 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -132,6 +132,18 @@ const ( // flows the other way — VK writes it for operators/tooling to read. EndpointAnnotation = "nebula.inftyai.com/endpoint" + // SanddPath is where the SandD binary is found INSIDE a Nebula-provisioned + // container, and therefore the command every synthesized workload Pod runs. It + // is a shared constant rather than a per-adapter string because it is a contract + // with two ends that must agree exactly: the controller writes it as the Pod's + // container command, and every provider bootstrap must make the binary appear at + // this path (the AWS adapter bind-mounts it from the host into the container). + // + // The path lives under /nebula rather than /usr/local/bin to avoid colliding with + // anything the user's own image ships, since the image is arbitrary and we are + // injecting into it. + SanddPath = "/nebula/sandd" + // TerminateInstanceFinalizer is held by every NodeClaim to guarantee teardown. // The virtual kubelet owns the happy path (DeletePod → provider.Terminate, // keyed on the Pod-derived claim name), but its teardown is edge-triggered and diff --git a/api/v1alpha1/sandbox_types.go b/api/v1alpha1/sandbox_types.go index 5844f34..3f2474d 100644 --- a/api/v1alpha1/sandbox_types.go +++ b/api/v1alpha1/sandbox_types.go @@ -59,13 +59,16 @@ type SandboxSpec struct { // Image is the container image the sandbox runs. // - // It has no command, and setting one is rejected at admission. That is not a - // simplification, it is the process model: SandD runs as PID 1 in the container - // and spawns the workload as its child so it owns the workload's stdout/stderr - // pipes, which is what makes `kubectl logs` possible against an instance in - // another cloud. A user-supplied command would displace SandD and take the log - // and exec paths with it. An interactive sandbox has nothing to run at boot - // anyway — the whole point is that commands arrive later, over exec. + // There is deliberately no command field, and one cannot be set. That is not a + // simplification, it is the process model: the container's command is always + // SandD, which runs as PID 1 and is what makes `kubectl exec` and `kubectl logs` + // work against an instance in another cloud. A user-supplied command would + // displace it and take both with it. + // + // A sandbox has nothing to run at boot anyway — the whole point is that commands + // arrive later, over exec — so SandD spawns no child here; it holds the container + // open and serves requests. Workload classes that do run something get it spawned + // as SandD's child instead, which is how it comes to own their stdout/stderr. // +kubebuilder:validation:MinLength=1 Image string `json:"image"` diff --git a/config/crd/bases/nebula.inftyai.com_sandboxes.yaml b/config/crd/bases/nebula.inftyai.com_sandboxes.yaml index 5fdbe45..e9d8242 100644 --- a/config/crd/bases/nebula.inftyai.com_sandboxes.yaml +++ b/config/crd/bases/nebula.inftyai.com_sandboxes.yaml @@ -229,13 +229,16 @@ spec: description: |- Image is the container image the sandbox runs. - It has no command, and setting one is rejected at admission. That is not a - simplification, it is the process model: SandD runs as PID 1 in the container - and spawns the workload as its child so it owns the workload's stdout/stderr - pipes, which is what makes `kubectl logs` possible against an instance in - another cloud. A user-supplied command would displace SandD and take the log - and exec paths with it. An interactive sandbox has nothing to run at boot - anyway — the whole point is that commands arrive later, over exec. + There is deliberately no command field, and one cannot be set. That is not a + simplification, it is the process model: the container's command is always + SandD, which runs as PID 1 and is what makes `kubectl exec` and `kubectl logs` + work against an instance in another cloud. A user-supplied command would + displace it and take both with it. + + A sandbox has nothing to run at boot anyway — the whole point is that commands + arrive later, over exec — so SandD spawns no child here; it holds the container + open and serves requests. Workload classes that do run something get it spawned + as SandD's child instead, which is how it comes to own their stdout/stderr. minLength: 1 type: string nodePoolRef: diff --git a/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml b/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml index fa384db..e40009f 100644 --- a/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml +++ b/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml @@ -260,13 +260,16 @@ spec: description: |- Image is the container image the sandbox runs. - It has no command, and setting one is rejected at admission. That is not a - simplification, it is the process model: SandD runs as PID 1 in the container - and spawns the workload as its child so it owns the workload's stdout/stderr - pipes, which is what makes `kubectl logs` possible against an instance in - another cloud. A user-supplied command would displace SandD and take the log - and exec paths with it. An interactive sandbox has nothing to run at boot - anyway — the whole point is that commands arrive later, over exec. + There is deliberately no command field, and one cannot be set. That is not a + simplification, it is the process model: the container's command is always + SandD, which runs as PID 1 and is what makes `kubectl exec` and `kubectl logs` + work against an instance in another cloud. A user-supplied command would + displace it and take both with it. + + A sandbox has nothing to run at boot anyway — the whole point is that commands + arrive later, over exec — so SandD spawns no child here; it holds the container + open and serves requests. Workload classes that do run something get it spawned + as SandD's child instead, which is how it comes to own their stdout/stderr. minLength: 1 type: string nodePoolRef: diff --git a/config/samples/nebula_v1alpha1_sandbox.yaml b/config/samples/nebula_v1alpha1_sandbox.yaml index dcf91c2..e4700c9 100644 --- a/config/samples/nebula_v1alpha1_sandbox.yaml +++ b/config/samples/nebula_v1alpha1_sandbox.yaml @@ -3,11 +3,10 @@ # kubectl exec -it sample -- bash # kubectl logs sample # -# Note there is no command: SandD runs as PID 1 in the container and spawns the -# workload as its child, which is what lets it own the stdout/stderr pipes that -# `kubectl logs` reads. Setting a command would displace it, so admission rejects -# one. An interactive box has nothing to run at boot anyway — commands arrive -# later, over exec. +# There is no command field, by design: the container's command is always SandD, +# which runs as PID 1 and is what makes exec and logs work against an instance in +# another cloud. A box has nothing to run at boot anyway — commands arrive later, +# over exec — so SandD holds the container open and serves them. apiVersion: nebula.inftyai.com/v1alpha1 kind: Sandbox metadata: diff --git a/internal/controller/sandbox_controller.go b/internal/controller/sandbox_controller.go index 502ef58..0007056 100644 --- a/internal/controller/sandbox_controller.go +++ b/internal/controller/sandbox_controller.go @@ -194,9 +194,24 @@ func (r *SandboxReconciler) buildPod(sbx *nebulav1alpha1.Sandbox) *corev1.Pod { Containers: []corev1.Container{{ Name: sandboxContainerName, Image: sbx.Spec.Image, - // No Command: SandD is PID 1 in the container and spawns the workload as - // its child, which is what lets it own the stdout/stderr pipes that - // `kubectl logs` reads. A command here would displace it. + // SandD is the command, set HERE rather than in each provider's bootstrap, + // so the Pod stays the single source of truth for what runs on the instance + // — the same rule ProvisionRequest documents for image/env/resources. Every + // adapter already reads the command off the Pod, so this needs no + // per-provider code and cannot drift between providers. + // + // SandD is PID 1 in the container. For a sandbox it spawns nothing: the box + // exists to receive exec calls, so SandD just holds the container open and + // serves them. For workload classes that DO run something, it spawns that as + // its child and owns the stdout/stderr pipes, which is what makes + // `kubectl logs` work against an instance in another cloud. + // + // The user's image does not contain this binary (it is an arbitrary image + // like ubuntu:24.04), so the provider bootstrap must make it appear at + // SanddPath — the contract the shared constant exists to pin down. A + // user-supplied command is rejected at admission, since it would displace + // SandD and take logs and exec with it. + Command: []string{nebulav1alpha1.SanddPath}, Resources: sbx.Spec.Resources, Env: sbx.Spec.Env, }}, From e5651cf81c1fb2a83d20ba3a975429c2993c136f Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 09:51:22 +0100 Subject: [PATCH 13/22] sandbox: test both controllers, default the image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unit tests for the Sandbox and SandboxSet controllers, using the fake-client pattern the NodeClaim/NodePool tests already established. The Sandbox tests pin the synthesized Pod's whole contract, since every field there is what makes the EXISTING placement path pick the Pod up unchanged — the opt-in label, the pool label, SandD as the command, and the accelerator count as an nvidia.com/gpu limit. The rest cover the projection from Pod state to phase (including gated-vs-provisioning, which is what stops a capacity problem from looking like a slow boot), ReadyTime being written exactly once so a status blip cannot restart the TTL clock, TTL deleting the Pod while the object survives as the record, terminal boxes not being resurrected, and a foreign same-named Pod being refused rather than adopted. The SandboxSet tests cover scale-up from the template, the template being unable to overwrite the ownership label and orphan a box, a labelled-but-foreign box being neither counted nor deleted, terminal boxes being pruned AND replaced on one pass, the removal ranking, and the status selector /scale needs. Two fixes fell out: - scaleDown returned from inside its loop on any error, and IgnoreNotFound maps an already-gone box to nil — so one missing victim abandoned every remaining one and reported the scale-in as done, leaving paid instances running. - spec.image now defaults to ubuntu:24.04 and is no longer required. Unlike the accelerator, the image is not a decision a caller must make to get a useful box: exec'ing into a bare distro IS the remote-shell case. Defaulting a GPU shape would be guessing at spend; defaulting a shell is not. It deliberately does not become a CUDA image when an accelerator is requested — a conditional default cannot be expressed in a structural schema and would surprise anyone reading the object back. Co-Authored-By: Claude Opus 5 --- api/v1alpha1/sandbox_types.go | 15 +- .../bases/nebula.inftyai.com_sandboxes.yaml | 13 +- .../bases/nebula.inftyai.com_sandboxsets.yaml | 13 +- config/samples/nebula_v1alpha1_sandbox.yaml | 3 + .../controller/sandbox_controller_test.go | 454 ++++++++++++++++++ internal/controller/sandboxset_controller.go | 7 +- .../controller/sandboxset_controller_test.go | 381 +++++++++++++++ 7 files changed, 879 insertions(+), 7 deletions(-) create mode 100644 internal/controller/sandbox_controller_test.go create mode 100644 internal/controller/sandboxset_controller_test.go diff --git a/api/v1alpha1/sandbox_types.go b/api/v1alpha1/sandbox_types.go index 3f2474d..162d981 100644 --- a/api/v1alpha1/sandbox_types.go +++ b/api/v1alpha1/sandbox_types.go @@ -57,7 +57,16 @@ type SandboxSpec struct { // +kubebuilder:validation:MinLength=1 NodePoolRef string `json:"nodePoolRef"` - // Image is the container image the sandbox runs. + // Image is the container image the sandbox runs. It defaults to a plain Ubuntu, + // because unlike the accelerator the image is not a decision a caller has to make + // to get a useful box: `kubectl exec` into a bare distro is exactly the "give me a + // remote shell" case, and anything else can be installed from inside it. Defaulting + // a paid GPU shape would be guessing at spend; defaulting a shell is not. + // + // Note it deliberately does NOT default to a CUDA image even when an accelerator is + // requested. A conditional default would make the image depend on another field, + // which structural-schema defaulting cannot express and which would surprise anyone + // reading the object back. Ask for a CUDA image explicitly when you want one. // // There is deliberately no command field, and one cannot be set. That is not a // simplification, it is the process model: the container's command is always @@ -70,7 +79,9 @@ type SandboxSpec struct { // open and serves requests. Workload classes that do run something get it spawned // as SandD's child instead, which is how it comes to own their stdout/stderr. // +kubebuilder:validation:MinLength=1 - Image string `json:"image"` + // +kubebuilder:default="ubuntu:24.04" + // +optional + Image string `json:"image,omitempty"` // AcceleratorType is the requested accelerator TYPE (e.g. "a100-40gb", // "h100"), matched case-insensitively against the provider catalog. The COUNT diff --git a/config/crd/bases/nebula.inftyai.com_sandboxes.yaml b/config/crd/bases/nebula.inftyai.com_sandboxes.yaml index e9d8242..dab19e0 100644 --- a/config/crd/bases/nebula.inftyai.com_sandboxes.yaml +++ b/config/crd/bases/nebula.inftyai.com_sandboxes.yaml @@ -226,8 +226,18 @@ spec: type: object type: array image: + default: ubuntu:24.04 description: |- - Image is the container image the sandbox runs. + Image is the container image the sandbox runs. It defaults to a plain Ubuntu, + because unlike the accelerator the image is not a decision a caller has to make + to get a useful box: `kubectl exec` into a bare distro is exactly the "give me a + remote shell" case, and anything else can be installed from inside it. Defaulting + a paid GPU shape would be guessing at spend; defaulting a shell is not. + + Note it deliberately does NOT default to a CUDA image even when an accelerator is + requested. A conditional default would make the image depend on another field, + which structural-schema defaulting cannot express and which would surprise anyone + reading the object back. Ask for a CUDA image explicitly when you want one. There is deliberately no command field, and one cannot be set. That is not a simplification, it is the process model: the container's command is always @@ -324,7 +334,6 @@ spec: sandbox, which is a deliberate choice rather than the default. type: string required: - - image - nodePoolRef type: object status: diff --git a/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml b/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml index e40009f..e112c72 100644 --- a/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml +++ b/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml @@ -257,8 +257,18 @@ spec: type: object type: array image: + default: ubuntu:24.04 description: |- - Image is the container image the sandbox runs. + Image is the container image the sandbox runs. It defaults to a plain Ubuntu, + because unlike the accelerator the image is not a decision a caller has to make + to get a useful box: `kubectl exec` into a bare distro is exactly the "give me a + remote shell" case, and anything else can be installed from inside it. Defaulting + a paid GPU shape would be guessing at spend; defaulting a shell is not. + + Note it deliberately does NOT default to a CUDA image even when an accelerator is + requested. A conditional default would make the image depend on another field, + which structural-schema defaulting cannot express and which would surprise anyone + reading the object back. Ask for a CUDA image explicitly when you want one. There is deliberately no command field, and one cannot be set. That is not a simplification, it is the process model: the container's command is always @@ -355,7 +365,6 @@ spec: sandbox, which is a deliberate choice rather than the default. type: string required: - - image - nodePoolRef type: object required: diff --git a/config/samples/nebula_v1alpha1_sandbox.yaml b/config/samples/nebula_v1alpha1_sandbox.yaml index e4700c9..74b530d 100644 --- a/config/samples/nebula_v1alpha1_sandbox.yaml +++ b/config/samples/nebula_v1alpha1_sandbox.yaml @@ -17,6 +17,9 @@ spec: # The placement policy this box is provisioned against: which providers are # allowed, which capacity tiers, how to rank them. nodePoolRef: sample + # Optional — defaults to ubuntu:24.04, which is all a "give me a remote shell" box + # needs. Note it does NOT become a CUDA image when you ask for an accelerator; name + # one explicitly if you want the toolkit preinstalled. image: ubuntu:24.04 # The accelerator TYPE is a label-shaped field; the COUNT rides on the # nvidia.com/gpu resource below, so exactly one number drives both scheduling diff --git a/internal/controller/sandbox_controller_test.go b/internal/controller/sandbox_controller_test.go new file mode 100644 index 0000000..915c0f1 --- /dev/null +++ b/internal/controller/sandbox_controller_test.go @@ -0,0 +1,454 @@ +/* +Copyright 2026 The InftyAI Team. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + nebulav1alpha1 "github.com/InftyAI/Nebula/api/v1alpha1" + "github.com/InftyAI/Nebula/pkg/util" +) + +const ( + testNS = "team-ml" + testSbxName = "alice" +) + +// newSandboxReconciler builds a reconciler over a fake client seeded with objs. +func newSandboxReconciler(objs ...client.Object) (*SandboxReconciler, client.Client) { + s := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(s) + _ = nebulav1alpha1.AddToScheme(s) + c := fake.NewClientBuilder(). + WithScheme(s). + WithObjects(objs...). + WithStatusSubresource(&nebulav1alpha1.Sandbox{}). + Build() + return &SandboxReconciler{Client: c, Scheme: s}, c +} + +// newSandbox is a Sandbox with the fields the controller reads. The UID is set +// because ownership checks compare it, and the fake client does not assign one. +func newSandbox(mutators ...func(*nebulav1alpha1.Sandbox)) *nebulav1alpha1.Sandbox { + sbx := &nebulav1alpha1.Sandbox{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSbxName, + Namespace: testNS, + UID: types.UID("sbx-uid-1"), + }, + Spec: nebulav1alpha1.SandboxSpec{ + NodePoolRef: "gpu", + Image: "ubuntu:24.04", + AcceleratorType: "a100-40gb", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + util.NvidiaGPUResource: resource.MustParse("1"), + }, + }, + }, + } + for _, m := range mutators { + m(sbx) + } + return sbx +} + +func reconcileSandbox(t *testing.T, r *SandboxReconciler) reconcile.Result { + t.Helper() + res, err := r.Reconcile(context.Background(), reconcile.Request{ + NamespacedName: types.NamespacedName{Namespace: testNS, Name: testSbxName}, + }) + if err != nil { + t.Fatalf("Reconcile: %v", err) + } + return res +} + +func getSandboxPod(t *testing.T, c client.Client) *corev1.Pod { + t.Helper() + var pod corev1.Pod + key := client.ObjectKey{Namespace: testNS, Name: testSbxName} + if err := c.Get(context.Background(), key, &pod); err != nil { + t.Fatalf("get pod: %v", err) + } + return &pod +} + +func getSandbox(t *testing.T, c client.Client) *nebulav1alpha1.Sandbox { + t.Helper() + var sbx nebulav1alpha1.Sandbox + key := client.ObjectKey{Namespace: testNS, Name: testSbxName} + if err := c.Get(context.Background(), key, &sbx); err != nil { + t.Fatalf("get sandbox: %v", err) + } + return &sbx +} + +// TestSandboxSynthesizesPod covers the whole contract the synthesized Pod must +// satisfy for the EXISTING placement path to pick it up unchanged. Each assertion +// here is load-bearing: drop the opt-in label and the Pod is scheduled by vanilla +// Kubernetes and never reaches a provider; drop the pool label and placement has +// no policy to resolve. +func TestSandboxSynthesizesPod(t *testing.T) { + r, c := newSandboxReconciler(newSandbox()) + reconcileSandbox(t, r) + pod := getSandboxPod(t, c) + + if got := pod.Labels[nebulav1alpha1.EnabledLabel]; got != "true" { + t.Errorf("opt-in label = %q, want \"true\" (without it the Pod never reaches a provider)", got) + } + if got := pod.Labels[nebulav1alpha1.PoolLabel]; got != "gpu" { + t.Errorf("pool label = %q, want \"gpu\"", got) + } + if got := pod.Labels[nebulav1alpha1.SandboxLabel]; got != testSbxName { + t.Errorf("sandbox label = %q, want %q", got, testSbxName) + } + if got := pod.Labels[nebulav1alpha1.AcceleratorTypeLabel]; got != "a100-40gb" { + t.Errorf("accelerator label = %q, want \"a100-40gb\"", got) + } + if pod.Spec.RestartPolicy != corev1.RestartPolicyNever { + t.Errorf("restartPolicy = %q, want Never", pod.Spec.RestartPolicy) + } + + if n := len(pod.Spec.Containers); n != 1 { + t.Fatalf("containers = %d, want 1", n) + } + ctr := pod.Spec.Containers[0] + if ctr.Name != sandboxContainerName { + t.Errorf("container name = %q, want %q (kubectl exec defaults to it)", ctr.Name, sandboxContainerName) + } + if ctr.Image != "ubuntu:24.04" { + t.Errorf("image = %q, want ubuntu:24.04", ctr.Image) + } + // SandD must be the command: it is PID 1 and serves exec/logs. A Pod without it + // would run the image's own entrypoint and be unreachable. + if len(ctr.Command) != 1 || ctr.Command[0] != nebulav1alpha1.SanddPath { + t.Errorf("command = %v, want [%s]", ctr.Command, nebulav1alpha1.SanddPath) + } + // The GPU count must survive as a standard resource: placement and the + // scheduler's fit check both read it from here. + if q, ok := ctr.Resources.Limits[util.NvidiaGPUResource]; !ok || q.Value() != 1 { + t.Errorf("nvidia.com/gpu limit = %v (present=%v), want 1", q.Value(), ok) + } + + // Controller-owned, so garbage collection releases the instance when the + // Sandbox is deleted. + ref := metav1.GetControllerOf(pod) + if ref == nil || ref.Kind != "Sandbox" || ref.Name != testSbxName { + t.Errorf("controller ref = %+v, want the Sandbox", ref) + } +} + +// TestSandboxCPUOnlyOmitsAcceleratorLabel: a CPU-only box must not carry an empty +// accelerator label, which would make placement look for an accelerator named "". +func TestSandboxCPUOnlyOmitsAcceleratorLabel(t *testing.T) { + sbx := newSandbox(func(s *nebulav1alpha1.Sandbox) { + s.Spec.AcceleratorType = "" + s.Spec.Resources = corev1.ResourceRequirements{} + }) + r, c := newSandboxReconciler(sbx) + reconcileSandbox(t, r) + + if _, present := getSandboxPod(t, c).Labels[nebulav1alpha1.AcceleratorTypeLabel]; present { + t.Error("CPU-only sandbox must not set the accelerator-type label") + } +} + +// TestSandboxIsIdempotent: a second reconcile must not create a second Pod nor +// error. Controllers are re-run constantly, so this is the baseline invariant. +func TestSandboxIsIdempotent(t *testing.T) { + r, c := newSandboxReconciler(newSandbox()) + reconcileSandbox(t, r) + reconcileSandbox(t, r) + + var pods corev1.PodList + if err := c.List(context.Background(), &pods, client.InNamespace(testNS)); err != nil { + t.Fatalf("list pods: %v", err) + } + if len(pods.Items) != 1 { + t.Fatalf("pods = %d, want 1 (reconcile must be idempotent)", len(pods.Items)) + } +} + +// TestSandboxPhaseFromPod checks the projection from Pod state to Sandbox phase. +// The distinction that matters most is inside PodPending: a gated Pod means "no +// provider can serve this box" while an ungated one means "provisioning is under +// way" — conflating them would make a capacity problem look like a slow boot. +func TestSandboxPhaseFromPod(t *testing.T) { + gated := func(p *corev1.Pod) { + p.Spec.SchedulingGates = []corev1.PodSchedulingGate{ + {Name: nebulav1alpha1.ProviderSelectionGate}, + } + } + tests := []struct { + name string + mutate func(*corev1.Pod) + want nebulav1alpha1.SandboxPhase + wantRdy bool + }{ + { + name: "gated pod is Pending, not Provisioning", + mutate: func(p *corev1.Pod) { gated(p); p.Status.Phase = corev1.PodPending }, + want: nebulav1alpha1.SandboxPending, + }, + { + name: "ungated pending pod is Provisioning", + mutate: func(p *corev1.Pod) { p.Status.Phase = corev1.PodPending }, + want: nebulav1alpha1.SandboxProvisioning, + }, + { + name: "pending with Initializing reason is Initializing", + mutate: func(p *corev1.Pod) { + p.Status.Phase = corev1.PodPending + p.Status.Reason = podReasonInitializing + }, + want: nebulav1alpha1.SandboxInitializing, + }, + { + name: "running but not ready is Initializing", + mutate: func(p *corev1.Pod) { + p.Status.Phase = corev1.PodRunning + }, + want: nebulav1alpha1.SandboxInitializing, + }, + { + name: "running and ready is Ready", + mutate: func(p *corev1.Pod) { + p.Status.Phase = corev1.PodRunning + p.Status.Conditions = []corev1.PodCondition{ + {Type: corev1.PodReady, Status: corev1.ConditionTrue}, + } + }, + want: nebulav1alpha1.SandboxReady, + wantRdy: true, + }, + { + name: "failed pod is Failed", + mutate: func(p *corev1.Pod) { p.Status.Phase = corev1.PodFailed }, + want: nebulav1alpha1.SandboxFailed, + }, + { + // SandD only exits when the box goes away, so a Succeeded Pod still means + // the instance is gone — not that the sandbox completed successfully. + name: "succeeded pod is Failed too", + mutate: func(p *corev1.Pod) { p.Status.Phase = corev1.PodSucceeded }, + want: nebulav1alpha1.SandboxFailed, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sbx := newSandbox() + r, c := newSandboxReconciler(sbx) + reconcileSandbox(t, r) // creates the Pod + + pod := getSandboxPod(t, c) + tc.mutate(pod) + // Spec and status must be written separately, and the status has to be + // re-applied after the spec write: the fake client treats Pod status as a + // subresource, so Update copies the STORED status back over the object it + // was handed, discarding what mutate just set. + want := pod.Status + if err := c.Update(context.Background(), pod); err != nil { + t.Fatalf("update pod: %v", err) + } + pod.Status = want + if err := c.Status().Update(context.Background(), pod); err != nil { + t.Fatalf("update pod status: %v", err) + } + + reconcileSandbox(t, r) + got := getSandbox(t, c) + if got.Status.Phase != tc.want { + t.Errorf("phase = %q, want %q", got.Status.Phase, tc.want) + } + ready := false + for _, cond := range got.Status.Conditions { + if cond.Type == nebulav1alpha1.SandboxConditionReady { + ready = cond.Status == metav1.ConditionTrue + } + } + if ready != tc.wantRdy { + t.Errorf("Ready condition = %v, want %v", ready, tc.wantRdy) + } + }) + } +} + +// TestSandboxEndpointAndReadyTime: the endpoint must be mirrored from the Pod +// annotation (it is the only way to reach the box), and ReadyTime must be stamped +// once so TTL has a stable anchor. +func TestSandboxEndpointAndReadyTime(t *testing.T) { + r, c := newSandboxReconciler(newSandbox(func(s *nebulav1alpha1.Sandbox) { + s.Spec.TTL = &metav1.Duration{Duration: time.Hour} + })) + reconcileSandbox(t, r) + markPodReady(t, c, "ec2-1-2-3-4.compute.amazonaws.com") + reconcileSandbox(t, r) + + sbx := getSandbox(t, c) + if sbx.Status.Endpoint != "ec2-1-2-3-4.compute.amazonaws.com" { + t.Errorf("endpoint = %q, want the Pod's annotation value", sbx.Status.Endpoint) + } + if sbx.Status.ReadyTime == nil { + t.Fatal("ReadyTime must be stamped on the first transition to Ready") + } + if sbx.Status.ExpiryTime == nil { + t.Fatal("ExpiryTime must be derived from ReadyTime + TTL") + } + firstReady := sbx.Status.ReadyTime.DeepCopy() + + // A later reconcile must NOT move ReadyTime: it anchors TTL, so re-deriving it + // would let a status blip silently restart the user's clock. + reconcileSandbox(t, r) + if got := getSandbox(t, c).Status.ReadyTime; !got.Equal(firstReady) { + t.Errorf("ReadyTime moved from %v to %v; it must be written exactly once", firstReady, got) + } +} + +// TestSandboxTTLReleasesInstance: once the deadline passes the Pod must be +// deleted — that is what triggers VK teardown and the NodeClaim finalizer behind +// it — while the Sandbox object survives as the record of why the box went away. +func TestSandboxTTLReleasesInstance(t *testing.T) { + r, c := newSandboxReconciler(newSandbox(func(s *nebulav1alpha1.Sandbox) { + s.Spec.TTL = &metav1.Duration{Duration: time.Hour} + })) + reconcileSandbox(t, r) + markPodReady(t, c, "1.2.3.4") + reconcileSandbox(t, r) + + // Backdate the expiry rather than sleeping. + sbx := getSandbox(t, c) + past := metav1.NewTime(time.Now().Add(-time.Minute)) + sbx.Status.ExpiryTime = &past + if err := c.Status().Update(context.Background(), sbx); err != nil { + t.Fatalf("update status: %v", err) + } + + reconcileSandbox(t, r) + + var pod corev1.Pod + err := c.Get(context.Background(), client.ObjectKey{Namespace: testNS, Name: testSbxName}, &pod) + if !apierrors.IsNotFound(err) { + t.Errorf("Pod must be deleted on expiry so the instance is released; get err = %v", err) + } + if got := getSandbox(t, c).Status.Phase; got != nebulav1alpha1.SandboxExpired { + t.Errorf("phase = %q, want Expired", got) + } +} + +// TestSandboxTerminalDoesNotRecreatePod: an expired or failed box must stay dead. +// Recreating the Pod would hand the user a DIFFERENT box (empty filesystem, new +// endpoint) under the same name — the single most confusing thing this controller +// could do. +func TestSandboxTerminalDoesNotRecreatePod(t *testing.T) { + for _, phase := range []nebulav1alpha1.SandboxPhase{ + nebulav1alpha1.SandboxExpired, + nebulav1alpha1.SandboxFailed, + } { + t.Run(string(phase), func(t *testing.T) { + sbx := newSandbox() + sbx.Status.Phase = phase + r, c := newSandboxReconciler(sbx) + + reconcileSandbox(t, r) + + var pods corev1.PodList + if err := c.List(context.Background(), &pods, client.InNamespace(testNS)); err != nil { + t.Fatalf("list pods: %v", err) + } + if len(pods.Items) != 0 { + t.Errorf("pods = %d, want 0: a terminal sandbox must not be resurrected", len(pods.Items)) + } + }) + } +} + +// TestSandboxRefusesForeignPod: a Pod of the required name that belongs to someone +// else must NOT be adopted — adopting would subject an unrelated workload to this +// Sandbox's lifecycle, including deletion on TTL expiry. +func TestSandboxRefusesForeignPod(t *testing.T) { + foreign := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSbxName, + Namespace: testNS, + Labels: map[string]string{"app": "someone-elses-thing"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "app", Image: "nginx"}}, + }, + } + r, c := newSandboxReconciler(newSandbox(), foreign) + reconcileSandbox(t, r) + + sbx := getSandbox(t, c) + if sbx.Status.Phase != nebulav1alpha1.SandboxPending { + t.Errorf("phase = %q, want Pending", sbx.Status.Phase) + } + var reason string + for _, cond := range sbx.Status.Conditions { + if cond.Type == nebulav1alpha1.SandboxConditionReady { + reason = cond.Reason + } + } + if reason != nebulav1alpha1.ReasonPodConflict { + t.Errorf("condition reason = %q, want %q", reason, nebulav1alpha1.ReasonPodConflict) + } + + // The foreign Pod must be untouched. + pod := getSandboxPod(t, c) + if pod.Labels["app"] != "someone-elses-thing" { + t.Error("the foreign Pod was mutated; it must be left alone") + } + if len(pod.Spec.Containers) != 1 || pod.Spec.Containers[0].Image != "nginx" { + t.Error("the foreign Pod's spec was overwritten") + } +} + +// markPodReady drives the Sandbox's Pod to Running+Ready with an endpoint, the way +// the virtual kubelet would once the provider reports the instance up. +func markPodReady(t *testing.T, c client.Client, endpoint string) { + t.Helper() + pod := getSandboxPod(t, c) + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + pod.Annotations[nebulav1alpha1.EndpointAnnotation] = endpoint + if err := c.Update(context.Background(), pod); err != nil { + t.Fatalf("update pod: %v", err) + } + pod.Status.Phase = corev1.PodRunning + pod.Status.Conditions = []corev1.PodCondition{ + {Type: corev1.PodReady, Status: corev1.ConditionTrue}, + } + if err := c.Status().Update(context.Background(), pod); err != nil { + t.Fatalf("update pod status: %v", err) + } +} diff --git a/internal/controller/sandboxset_controller.go b/internal/controller/sandboxset_controller.go index 03b5a34..4d7cf19 100644 --- a/internal/controller/sandboxset_controller.go +++ b/internal/controller/sandboxset_controller.go @@ -232,7 +232,12 @@ func (r *SandboxSetReconciler) scaleDown(ctx context.Context, victims []nebulav1 // clobbered; an already-gone box is success. preconditions := metav1.Preconditions{UID: &v.UID} if err := r.Delete(ctx, v, &client.DeleteOptions{Preconditions: &preconditions}); err != nil { - return client.IgnoreNotFound(err) + // An already-gone box is success, and must NOT end the loop: returning here + // would abandon the remaining victims while reporting the scale-in as done, + // leaving paid instances running. + if err = client.IgnoreNotFound(err); err != nil { + return err + } } } return nil diff --git a/internal/controller/sandboxset_controller_test.go b/internal/controller/sandboxset_controller_test.go new file mode 100644 index 0000000..690785d --- /dev/null +++ b/internal/controller/sandboxset_controller_test.go @@ -0,0 +1,381 @@ +/* +Copyright 2026 The InftyAI Team. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + nebulav1alpha1 "github.com/InftyAI/Nebula/api/v1alpha1" +) + +const testSetName = "workers" + +func newSetReconciler(objs ...client.Object) (*SandboxSetReconciler, client.Client) { + s := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(s) + _ = nebulav1alpha1.AddToScheme(s) + c := fake.NewClientBuilder(). + WithScheme(s). + WithObjects(objs...). + WithStatusSubresource(&nebulav1alpha1.SandboxSet{}, &nebulav1alpha1.Sandbox{}). + Build() + return &SandboxSetReconciler{Client: c, Scheme: s}, c +} + +func newSandboxSet(replicas int32) *nebulav1alpha1.SandboxSet { + return &nebulav1alpha1.SandboxSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSetName, + Namespace: testNS, + UID: types.UID("set-uid-1"), + }, + Spec: nebulav1alpha1.SandboxSetSpec{ + Replicas: replicas, + Template: nebulav1alpha1.SandboxTemplateSpec{ + Metadata: nebulav1alpha1.SandboxTemplateMetadata{ + Labels: map[string]string{"tier": "agent"}, + }, + Spec: nebulav1alpha1.SandboxSpec{ + NodePoolRef: "gpu", + Image: "ubuntu:24.04", + }, + }, + }, + } +} + +func reconcileSet(t *testing.T, r *SandboxSetReconciler) { + t.Helper() + if _, err := r.Reconcile(context.Background(), reconcile.Request{ + NamespacedName: types.NamespacedName{Namespace: testNS, Name: testSetName}, + }); err != nil { + t.Fatalf("Reconcile: %v", err) + } +} + +func listSandboxes(t *testing.T, c client.Client) []nebulav1alpha1.Sandbox { + t.Helper() + var list nebulav1alpha1.SandboxList + if err := c.List(context.Background(), &list, client.InNamespace(testNS)); err != nil { + t.Fatalf("list sandboxes: %v", err) + } + return list.Items +} + +func getSet(t *testing.T, c client.Client) *nebulav1alpha1.SandboxSet { + t.Helper() + var set nebulav1alpha1.SandboxSet + key := client.ObjectKey{Namespace: testNS, Name: testSetName} + if err := c.Get(context.Background(), key, &set); err != nil { + t.Fatalf("get sandboxset: %v", err) + } + return &set +} + +// ownedSandbox is a member box as the set would have created it, with the phase +// and age a test needs. The UID is explicit because ownership is matched on it. +func ownedSandbox(name string, phase nebulav1alpha1.SandboxPhase, ageMinutes int) *nebulav1alpha1.Sandbox { + yes := true + return &nebulav1alpha1.Sandbox{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNS, + UID: types.UID("uid-" + name), + CreationTimestamp: metav1.NewTime( + time.Now().Add(-time.Duration(ageMinutes) * time.Minute)), + Labels: map[string]string{nebulav1alpha1.SandboxSetLabel: testSetName}, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: nebulav1alpha1.GroupVersion.String(), + Kind: "SandboxSet", + Name: testSetName, + UID: types.UID("set-uid-1"), + Controller: &yes, + }}, + }, + Spec: nebulav1alpha1.SandboxSpec{NodePoolRef: "gpu", Image: "ubuntu:24.04"}, + Status: nebulav1alpha1.SandboxStatus{Phase: phase}, + } +} + +// TestSandboxSetScalesUpFromTemplate: the set must create exactly Replicas boxes, +// each stamped from the template and labelled so the set can find it again. +func TestSandboxSetScalesUpFromTemplate(t *testing.T) { + r, c := newSetReconciler(newSandboxSet(3)) + reconcileSet(t, r) + + boxes := listSandboxes(t, c) + if len(boxes) != 3 { + t.Fatalf("sandboxes = %d, want 3", len(boxes)) + } + for i := range boxes { + sbx := &boxes[i] + if sbx.Spec.Image != "ubuntu:24.04" || sbx.Spec.NodePoolRef != "gpu" { + t.Errorf("%s: spec not stamped from the template: %+v", sbx.Name, sbx.Spec) + } + if got := sbx.Labels[nebulav1alpha1.SandboxSetLabel]; got != testSetName { + t.Errorf("%s: ownership label = %q, want %q", sbx.Name, got, testSetName) + } + if got := sbx.Labels["tier"]; got != "agent" { + t.Errorf("%s: template label lost: tier = %q", sbx.Name, got) + } + if ref := metav1.GetControllerOf(sbx); ref == nil || ref.Kind != "SandboxSet" { + t.Errorf("%s: controller ref = %+v, want the SandboxSet", sbx.Name, ref) + } + } +} + +// TestSandboxSetTemplateCannotOrphanBox: the ownership label is applied after the +// template's, so a template that sets it cannot detach the box from its own set — +// which would leave an unowned box billing forever, invisible to the set. +func TestSandboxSetTemplateCannotOrphanBox(t *testing.T) { + set := newSandboxSet(1) + set.Spec.Template.Metadata.Labels[nebulav1alpha1.SandboxSetLabel] = "somewhere-else" + r, c := newSetReconciler(set) + reconcileSet(t, r) + + boxes := listSandboxes(t, c) + if len(boxes) != 1 { + t.Fatalf("sandboxes = %d, want 1", len(boxes)) + } + if got := boxes[0].Labels[nebulav1alpha1.SandboxSetLabel]; got != testSetName { + t.Errorf("ownership label = %q, want %q: the template must not override it", got, testSetName) + } +} + +// TestSandboxSetIsIdempotent: re-reconciling a satisfied set must not create more +// boxes. Each box is a paid instance, so a leak here is a bill, not just a bug. +func TestSandboxSetIsIdempotent(t *testing.T) { + r, c := newSetReconciler(newSandboxSet(2)) + reconcileSet(t, r) + reconcileSet(t, r) + reconcileSet(t, r) + + if n := len(listSandboxes(t, c)); n != 2 { + t.Errorf("sandboxes = %d, want 2: reconcile must be idempotent", n) + } +} + +// TestSandboxSetIgnoresForeignSandbox: a box carrying the set's label but owned by +// someone else must be neither counted nor deleted. Counting it would starve the +// set; deleting it would let anyone destroy a box they do not own by labelling it. +func TestSandboxSetIgnoresForeignSandbox(t *testing.T) { + foreign := ownedSandbox("imposter", nebulav1alpha1.SandboxReady, 5) + foreign.OwnerReferences = nil + + r, c := newSetReconciler(newSandboxSet(1), foreign) + reconcileSet(t, r) + + boxes := listSandboxes(t, c) + if len(boxes) != 2 { + t.Fatalf("sandboxes = %d, want 2 (the imposter plus one real box)", len(boxes)) + } + var found bool + for i := range boxes { + if boxes[i].Name == "imposter" { + found = true + } + } + if !found { + t.Error("the foreign Sandbox was deleted; a label alone must not grant the set authority over it") + } + if got := getSet(t, c).Status.Replicas; got != 1 { + t.Errorf("status.replicas = %d, want 1: the foreign box must not be counted", got) + } +} + +// TestSandboxSetReplacesTerminalBox is the self-healing path: a terminal box is +// deleted AND replaced on the same pass. Without the prune the set would sit at +// "3 replicas, 2 usable" forever, because the Sandbox controller deliberately never +// resurrects a dead box. +func TestSandboxSetReplacesTerminalBox(t *testing.T) { + for _, phase := range []nebulav1alpha1.SandboxPhase{ + nebulav1alpha1.SandboxFailed, + nebulav1alpha1.SandboxExpired, + } { + t.Run(string(phase), func(t *testing.T) { + dead := ownedSandbox("workers-dead", phase, 30) + alive := ownedSandbox("workers-alive", nebulav1alpha1.SandboxReady, 20) + + r, c := newSetReconciler(newSandboxSet(2), dead, alive) + reconcileSet(t, r) + + boxes := listSandboxes(t, c) + if len(boxes) != 2 { + t.Fatalf("sandboxes = %d, want 2", len(boxes)) + } + for i := range boxes { + if boxes[i].Name == "workers-dead" { + t.Error("the terminal box was not pruned; the set can never return to 2 usable boxes") + } + } + if got := getSet(t, c).Status.Replicas; got != 2 { + t.Errorf("status.replicas = %d, want 2", got) + } + }) + } +} + +// TestSandboxSetScalesDown checks scale-in removes exactly the excess. +func TestSandboxSetScalesDown(t *testing.T) { + objs := []client.Object{newSandboxSet(1)} + for i := range 3 { + objs = append(objs, ownedSandbox(fmt.Sprintf("workers-%d", i), + nebulav1alpha1.SandboxReady, 10+i)) + } + r, c := newSetReconciler(objs...) + reconcileSet(t, r) + + boxes := listSandboxes(t, c) + if len(boxes) != 1 { + t.Fatalf("sandboxes = %d, want 1", len(boxes)) + } + // Youngest-first within the Ready rank: workers-0 is the youngest (10m) and + // workers-2 the oldest (12m), so the OLDEST box is the survivor. + if boxes[0].Name != "workers-2" { + t.Errorf("survivor = %q, want workers-2 (the oldest Ready box)", boxes[0].Name) + } +} + +// TestSandboxSetScaleDownRemovesAllExcess: scale-in from 3 to 0 must delete every +// box in ONE pass. Bailing out early would leave paid instances running while the +// set reported the scale-in as done. +func TestSandboxSetScaleDownRemovesAllExcess(t *testing.T) { + objs := []client.Object{newSandboxSet(0)} + for i := range 3 { + objs = append(objs, ownedSandbox(fmt.Sprintf("workers-%d", i), + nebulav1alpha1.SandboxReady, 10+i)) + } + r, c := newSetReconciler(objs...) + reconcileSet(t, r) + + if n := len(listSandboxes(t, c)); n != 0 { + t.Errorf("sandboxes = %d, want 0: every excess box must be deleted in one pass", n) + } + set := getSet(t, c) + if set.Status.Replicas != 0 { + t.Errorf("status.replicas = %d, want 0", set.Status.Replicas) + } + if reason := readyReason(set.Status.Conditions); reason != nebulav1alpha1.ReasonSandboxSetScaledToZero { + t.Errorf("condition reason = %q, want %q", reason, nebulav1alpha1.ReasonSandboxSetScaledToZero) + } +} + +// TestSelectForRemovalOrder pins the victim order directly, since it decides whose +// work gets destroyed: dead boxes first, then boxes nobody could have used, and a +// possibly-in-use Ready box only as a last resort. +func TestSelectForRemovalOrder(t *testing.T) { + ready := *ownedSandbox("ready", nebulav1alpha1.SandboxReady, 30) + provisioning := *ownedSandbox("provisioning", nebulav1alpha1.SandboxProvisioning, 20) + failed := *ownedSandbox("failed", nebulav1alpha1.SandboxFailed, 10) + owned := []nebulav1alpha1.Sandbox{ready, provisioning, failed} + + got := selectForRemoval(owned, 2) + if len(got) != 2 { + t.Fatalf("victims = %d, want 2", len(got)) + } + if got[0].Name != "failed" { + t.Errorf("first victim = %q, want failed (a dead box costs nothing to lose)", got[0].Name) + } + if got[1].Name != "provisioning" { + t.Errorf("second victim = %q, want provisioning (nobody can have used it)", got[1].Name) + } + + // Asking for more than exists must return everything, not panic on a slice bound. + if n := len(selectForRemoval(owned, 5)); n != 3 { + t.Errorf("victims = %d, want 3 when n exceeds the population", n) + } +} + +// TestSandboxSetStatusSelector: /scale requires the selector as a serialized +// string, and HPA reads it from status to find the set's members — autoscaling +// silently does nothing if it is wrong. +func TestSandboxSetStatusSelector(t *testing.T) { + r, c := newSetReconciler(newSandboxSet(1)) + reconcileSet(t, r) + + want := nebulav1alpha1.SandboxSetLabel + "=" + testSetName + if got := getSet(t, c).Status.Selector; got != want { + t.Errorf("status.selector = %q, want %q", got, want) + } +} + +// TestSandboxSetReadyRollup: the set is Ready only when every box is, and the +// reported names must be the boxes that actually exist. +func TestSandboxSetReadyRollup(t *testing.T) { + a := ownedSandbox("workers-a", nebulav1alpha1.SandboxReady, 10) + b := ownedSandbox("workers-b", nebulav1alpha1.SandboxProvisioning, 5) + r, c := newSetReconciler(newSandboxSet(2), a, b) + reconcileSet(t, r) + + set := getSet(t, c) + if set.Status.Replicas != 2 || set.Status.ReadyReplicas != 1 { + t.Errorf("replicas/ready = %d/%d, want 2/1", set.Status.Replicas, set.Status.ReadyReplicas) + } + if readyCondStatus(set.Status.Conditions) != metav1.ConditionFalse { + t.Error("Ready must be False while one box is still coming up") + } + if len(set.Status.Sandboxes) != 2 { + t.Errorf("status.sandboxes = %v, want both box names", set.Status.Sandboxes) + } + + // Bring the laggard up: the set must flip to Ready. + b.Status.Phase = nebulav1alpha1.SandboxReady + if err := c.Status().Update(context.Background(), b); err != nil { + t.Fatalf("update sandbox status: %v", err) + } + reconcileSet(t, r) + + set = getSet(t, c) + if set.Status.ReadyReplicas != 2 { + t.Errorf("readyReplicas = %d, want 2", set.Status.ReadyReplicas) + } + if readyCondStatus(set.Status.Conditions) != metav1.ConditionTrue { + t.Error("Ready must be True once every box is ready") + } + if reason := readyReason(set.Status.Conditions); reason != nebulav1alpha1.ReasonSandboxSetReady { + t.Errorf("condition reason = %q, want %q", reason, nebulav1alpha1.ReasonSandboxSetReady) + } +} + +func readyCondStatus(conds []metav1.Condition) metav1.ConditionStatus { + for _, c := range conds { + if c.Type == nebulav1alpha1.SandboxSetConditionReady { + return c.Status + } + } + return "" +} + +func readyReason(conds []metav1.Condition) string { + for _, c := range conds { + if c.Type == nebulav1alpha1.SandboxSetConditionReady { + return c.Reason + } + } + return "" +} From 72b5280b716e99045402b16a0b6726a16410f5bf Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 10:05:45 +0100 Subject: [PATCH 14/22] sandbox: validate at admission, no webhook needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task was "add the Sandbox validating webhook", because the API and controller both claimed a user-set command is rejected at admission with nothing enforcing it. Checking the claim: SandboxSpec has no command field, and the CRD is a structural schema, so `command:` is already rejected as an unknown field by the apiserver itself. The docs were describing a guarantee we get for free; they now say where it comes from, and an envtest spec pins it — if the schema ever stopped rejecting the field (a stray x-kubernetes-preserve-unknown-fields would do it) the command would be silently PRUNED instead, surfacing as "exec does not work" rather than as a rejected object. The real admission gap was elsewhere: util.AcceleratorRequest treats an nvidia.com/gpu count with no acceleratorType as an error, so such a Sandbox was admitted and then failed at PLACEMENT — minutes later, reported on the synthesized Pod rather than on the object the user wrote. A CEL rule now rejects it up front, covering requests as well as limits since gpuCount reads either. The inverse stays legal: a type with no count means one accelerator. The rule propagates into the SandboxSet template for free. CEL is the right mechanism here rather than a webhook: no certs to provision, no availability to depend on, and it cannot be bypassed by a webhook outage. Envtest specs cover the CEL rule both ways, image defaulting, `image: ""` still failing MinLength (via unstructured — the field is omitempty, so a Go zero value never reaches the wire and would be defaulted instead of rejected), and the required nodePoolRef. Verified by mutation: neutering the CEL rule fails exactly the two specs that assert it and no others. Co-Authored-By: Claude Opus 5 --- api/v1alpha1/sandbox_types.go | 12 +- .../bases/nebula.inftyai.com_sandboxes.yaml | 16 +- .../bases/nebula.inftyai.com_sandboxsets.yaml | 9 +- internal/controller/sandbox_controller.go | 6 +- .../controller/sandbox_validation_test.go | 174 ++++++++++++++++++ 5 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 internal/controller/sandbox_validation_test.go diff --git a/api/v1alpha1/sandbox_types.go b/api/v1alpha1/sandbox_types.go index 162d981..09ec832 100644 --- a/api/v1alpha1/sandbox_types.go +++ b/api/v1alpha1/sandbox_types.go @@ -49,6 +49,14 @@ import ( // would fork the vocabulary and, worse, fork the source of truth for the // accelerator COUNT — which placement and the scheduler's fit check both read // from the container's nvidia.com/gpu limit (see util.AcceleratorRequest). +// +// The CEL rule below rejects a GPU count with no accelerator type. That pair is +// contradictory rather than merely incomplete — util.AcceleratorRequest returns an +// error for it — so without the rule the object is admitted and then fails at +// PLACEMENT, minutes later and one object removed from the mistake. Note the +// inverse is fine and deliberately allowed: a type with no count means one +// accelerator. +// +kubebuilder:validation:XValidation:rule="has(self.acceleratorType) || !has(self.resources) || ((!has(self.resources.limits) || !('nvidia.com/gpu' in self.resources.limits)) && (!has(self.resources.requests) || !('nvidia.com/gpu' in self.resources.requests)))",message="nvidia.com/gpu requires acceleratorType to be set" type SandboxSpec struct { // NodePoolRef names the NodePool whose policy places this sandbox: which // providers are allowed, which capacity tiers, how to rank them. Required — @@ -68,7 +76,9 @@ type SandboxSpec struct { // which structural-schema defaulting cannot express and which would surprise anyone // reading the object back. Ask for a CUDA image explicitly when you want one. // - // There is deliberately no command field, and one cannot be set. That is not a + // There is deliberately no command field, and one cannot be set: the CRD is a + // structural schema, so `command:` in a Sandbox spec is rejected as an unknown + // field by the apiserver itself — no webhook required. That is not a // simplification, it is the process model: the container's command is always // SandD, which runs as PID 1 and is what makes `kubectl exec` and `kubectl logs` // work against an instance in another cloud. A user-supplied command would diff --git a/config/crd/bases/nebula.inftyai.com_sandboxes.yaml b/config/crd/bases/nebula.inftyai.com_sandboxes.yaml index dab19e0..e982561 100644 --- a/config/crd/bases/nebula.inftyai.com_sandboxes.yaml +++ b/config/crd/bases/nebula.inftyai.com_sandboxes.yaml @@ -90,6 +90,13 @@ spec: would fork the vocabulary and, worse, fork the source of truth for the accelerator COUNT — which placement and the scheduler's fit check both read from the container's nvidia.com/gpu limit (see util.AcceleratorRequest). + + The CEL rule below rejects a GPU count with no accelerator type. That pair is + contradictory rather than merely incomplete — util.AcceleratorRequest returns an + error for it — so without the rule the object is admitted and then fails at + PLACEMENT, minutes later and one object removed from the mistake. Note the + inverse is fine and deliberately allowed: a type with no count means one + accelerator. properties: acceleratorType: description: |- @@ -239,7 +246,9 @@ spec: which structural-schema defaulting cannot express and which would surprise anyone reading the object back. Ask for a CUDA image explicitly when you want one. - There is deliberately no command field, and one cannot be set. That is not a + There is deliberately no command field, and one cannot be set: the CRD is a + structural schema, so `command:` in a Sandbox spec is rejected as an unknown + field by the apiserver itself — no webhook required. That is not a simplification, it is the process model: the container's command is always SandD, which runs as PID 1 and is what makes `kubectl exec` and `kubectl logs` work against an instance in another cloud. A user-supplied command would @@ -336,6 +345,11 @@ spec: required: - nodePoolRef type: object + x-kubernetes-validations: + - message: nvidia.com/gpu requires acceleratorType to be set + rule: has(self.acceleratorType) || !has(self.resources) || ((!has(self.resources.limits) + || !('nvidia.com/gpu' in self.resources.limits)) && (!has(self.resources.requests) + || !('nvidia.com/gpu' in self.resources.requests))) status: description: SandboxStatus is the observed state, projected from the synthesized Pod. diff --git a/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml b/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml index e112c72..dfc1e17 100644 --- a/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml +++ b/config/crd/bases/nebula.inftyai.com_sandboxsets.yaml @@ -270,7 +270,9 @@ spec: which structural-schema defaulting cannot express and which would surprise anyone reading the object back. Ask for a CUDA image explicitly when you want one. - There is deliberately no command field, and one cannot be set. That is not a + There is deliberately no command field, and one cannot be set: the CRD is a + structural schema, so `command:` in a Sandbox spec is rejected as an unknown + field by the apiserver itself — no webhook required. That is not a simplification, it is the process model: the container's command is always SandD, which runs as PID 1 and is what makes `kubectl exec` and `kubectl logs` work against an instance in another cloud. A user-supplied command would @@ -367,6 +369,11 @@ spec: required: - nodePoolRef type: object + x-kubernetes-validations: + - message: nvidia.com/gpu requires acceleratorType to be set + rule: has(self.acceleratorType) || !has(self.resources) || ((!has(self.resources.limits) + || !('nvidia.com/gpu' in self.resources.limits)) && (!has(self.resources.requests) + || !('nvidia.com/gpu' in self.resources.requests))) required: - spec type: object diff --git a/internal/controller/sandbox_controller.go b/internal/controller/sandbox_controller.go index 0007056..b973ac5 100644 --- a/internal/controller/sandbox_controller.go +++ b/internal/controller/sandbox_controller.go @@ -208,9 +208,9 @@ func (r *SandboxReconciler) buildPod(sbx *nebulav1alpha1.Sandbox) *corev1.Pod { // // The user's image does not contain this binary (it is an arbitrary image // like ubuntu:24.04), so the provider bootstrap must make it appear at - // SanddPath — the contract the shared constant exists to pin down. A - // user-supplied command is rejected at admission, since it would displace - // SandD and take logs and exec with it. + // SanddPath — the contract the shared constant exists to pin down. This is + // never in tension with a user-supplied command: SandboxSpec has no command + // field, so the structural schema rejects one outright. Command: []string{nebulav1alpha1.SanddPath}, Resources: sbx.Spec.Resources, Env: sbx.Spec.Env, diff --git a/internal/controller/sandbox_validation_test.go b/internal/controller/sandbox_validation_test.go new file mode 100644 index 0000000..8999dd0 --- /dev/null +++ b/internal/controller/sandbox_validation_test.go @@ -0,0 +1,174 @@ +/* +Copyright 2026 The InftyAI Team. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + + nebulav1alpha1 "github.com/InftyAI/Nebula/api/v1alpha1" + "github.com/InftyAI/Nebula/pkg/util" +) + +// These specs exercise Sandbox admission — the CEL rule on SandboxSpec and the +// schema-level guarantees (image defaulting, and the absence of a command field). +// None of it is enforced by the fake client the unit tests use: it runs neither +// x-kubernetes-validations nor structural-schema defaulting/pruning, so a real +// apiserver is the only thing that can prove these hold. Requires envtest binaries; +// the whole suite is skipped in BeforeSuite when they are absent. +var _ = Describe("Sandbox admission", func() { + newSandbox := func(name string) *nebulav1alpha1.Sandbox { + return &nebulav1alpha1.Sandbox{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default"}, + Spec: nebulav1alpha1.SandboxSpec{ + NodePoolRef: "sample", + }, + } + } + gpuLimit := func(sbx *nebulav1alpha1.Sandbox, n string) { + sbx.Spec.Resources = corev1.ResourceRequirements{ + Limits: corev1.ResourceList{util.NvidiaGPUResource: resource.MustParse(n)}, + } + } + + It("rejects a GPU count with no acceleratorType", func() { + // The contradictory pair: util.AcceleratorRequest errors on it, so admitting + // this would defer the failure to placement — minutes later, and reported on the + // Pod rather than on the object the user actually wrote. + sbx := newSandbox("gpu-no-type") + gpuLimit(sbx, "1") + + err := k8sClient.Create(ctx, sbx) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("nvidia.com/gpu requires acceleratorType to be set")) + }) + + It("rejects a GPU request (not just a limit) with no acceleratorType", func() { + // gpuCount reads limits OR requests, so the rule has to cover both; a + // requests-only spec would otherwise slip through and fail at placement. + sbx := newSandbox("gpu-request-no-type") + sbx.Spec.Resources = corev1.ResourceRequirements{ + Requests: corev1.ResourceList{util.NvidiaGPUResource: resource.MustParse("1")}, + } + + err := k8sClient.Create(ctx, sbx) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("nvidia.com/gpu requires acceleratorType to be set")) + }) + + It("admits a GPU count together with an acceleratorType", func() { + sbx := newSandbox("gpu-with-type") + sbx.Spec.AcceleratorType = "a100-40gb" + gpuLimit(sbx, "1") + + Expect(k8sClient.Create(ctx, sbx)).To(Succeed()) + Expect(k8sClient.Delete(ctx, sbx)).To(Succeed()) + }) + + It("admits an acceleratorType with no count (which means one accelerator)", func() { + sbx := newSandbox("type-no-count") + sbx.Spec.AcceleratorType = "h100" + + Expect(k8sClient.Create(ctx, sbx)).To(Succeed()) + Expect(k8sClient.Delete(ctx, sbx)).To(Succeed()) + }) + + It("admits a CPU-only sandbox with non-GPU resources", func() { + sbx := newSandbox("cpu-only") + sbx.Spec.Resources = corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("4")}, + } + + Expect(k8sClient.Create(ctx, sbx)).To(Succeed()) + Expect(k8sClient.Delete(ctx, sbx)).To(Succeed()) + }) + + It("defaults the image so a bare spec is usable", func() { + sbx := newSandbox("default-image") + Expect(k8sClient.Create(ctx, sbx)).To(Succeed()) + defer func() { Expect(k8sClient.Delete(ctx, sbx)).To(Succeed()) }() + + var got nebulav1alpha1.Sandbox + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(sbx), &got)).To(Succeed()) + Expect(got.Spec.Image).To(Equal("ubuntu:24.04")) + }) + + It("rejects an explicitly empty image rather than defaulting it", func() { + // `image: ""` is a mistake, not a request for the default, and MinLength must + // still bite — otherwise the box would boot something its author never named. + // + // This has to go through unstructured: the field is `omitempty`, so a Go zero + // value is dropped before the request is sent and would be DEFAULTED instead of + // rejected. Only an explicit empty string on the wire reaches MinLength, which is + // exactly the distinction the apiserver draws between unset and empty. + sbx := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": nebulav1alpha1.GroupVersion.String(), + "kind": "Sandbox", + "metadata": map[string]any{"name": "empty-image", "namespace": "default"}, + "spec": map[string]any{ + "nodePoolRef": "sample", + "image": "", + }, + }} + + err := k8sClient.Create(ctx, sbx) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("should be at least 1 chars long")) + }) + + It("rejects a spec with no nodePoolRef", func() { + // Placing a paid GPU instance against a guessed policy is not a safe default, + // so the field is required rather than defaulted. + sbx := newSandbox("no-pool") + sbx.Spec.NodePoolRef = "" + + err := k8sClient.Create(ctx, sbx) + Expect(err).To(HaveOccurred()) + }) + + It("rejects a user-supplied command", func() { + // The process model depends on SandD being PID 1 — it is what serves exec and + // logs — so a command must never reach the container. SandboxSpec simply has no + // command field, and because the CRD is a structural schema the apiserver rejects + // the unknown field itself. This spec exists because that guarantee is the reason + // no validating webhook was written: if the schema ever stopped rejecting it (a + // stray x-kubernetes-preserve-unknown-fields would do it), the command would be + // silently pruned instead, and the failure would surface as "exec does not work" + // rather than as a rejected object. + sbx := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": nebulav1alpha1.GroupVersion.String(), + "kind": "Sandbox", + "metadata": map[string]any{"name": "with-command", "namespace": "default"}, + "spec": map[string]any{ + "nodePoolRef": "sample", + "command": []any{"/bin/sleep", "infinity"}, + }, + }} + + // Strict field validation is what turns "unknown field" from a silent prune into + // an error; kubectl applies it by default, so this matches what a user sees. + err := k8sClient.Create(ctx, sbx, client.FieldValidation(metav1.FieldValidationStrict)) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unknown field")) + }) +}) From aa4b1d5898a01dd1822c1ce70eb38d825f840c14 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 11:11:49 +0100 Subject: [PATCH 15/22] cert: provision the webhook cert in-process, drop cert-manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The webhook needs a TLS keypair the manager serves from and that cert's CA in the MutatingWebhookConfiguration caBundle, and the two must agree exactly. Both prior ways of getting them were worse: - cert-manager: rotates correctly, but is a whole second operator the user has to install before Nebula works at all. It also made the e2e suite install cert-manager in BeforeSuite, so that suite could not run without network access to fetch its manifests — the concrete failure that prompted this. - hack/gen-webhook-cert.sh: no dependency, but the cert is minted by a shell script at deploy time and NEVER rotates. Valid for CERT_DAYS (default 3650), so its expiry is a silent time bomb that fires years later, when nobody remembers the script exists. pkg/cert does both jobs in-process via open-policy-agent/cert-controller: it mints the keypair into a Secret, writes it to the dir the webhook server reads, patches the caBundle, and keeps RENEWING it before expiry. Because the CA is derived from the cert just written, the served cert and the trusted CA cannot drift. Rotation is deliberately NOT leader-elected: CertDir is each pod's local disk and webhook serving is not leader-elected either, so a non-leader would serve with no keypair. The Secret is the shared source of truth, so later replicas find a valid cert there rather than minting a competing one. Controller and webhook registration now waits on certsReady, in a goroutine because the rotator cannot mint until the manager is STARTED (it is a Runnable needing a synced cache), so blocking in main would deadlock. The controllers wait too, not just the webhook: they create Pods, and a Pod admitted while the webhook is untrusted would either be rejected (failurePolicy=Fail) or, worse, admitted ungated and scheduled by vanilla Kubernetes — silently bypassing placement. The cert volume becomes an emptyDir rather than a Secret projection, since the rotator writes to that path and a Secret volume is read-only. No --webhook-cert-path either: it builds a certwatcher that fails at STARTUP when the files are absent, which they always are on a first install. cert-controller is pinned to v0.14.0 specifically: it requires controller-runtime v0.21.0 exactly, so the pin holds (v0.16.0 would drag controller-runtime to 0.23.3, which the virtual-kubelet v1.11 pin cannot take). Only k8s.io deps move, 0.33.3 -> 0.33.4. Co-Authored-By: Claude Opus 5 --- Makefile | 4 +- cmd/main.go | 166 ++++++++++++++++------ config/default/kustomization.yaml | 18 +-- config/default/manager_webhook_patch.yaml | 48 ++++--- config/manager/manager.yaml | 9 ++ config/rbac/role.yaml | 20 ++- docs/deploy.md | 81 +++++------ go.mod | 18 +-- go.sum | 46 +++--- hack/deploy.sh | 24 ++-- hack/gen-webhook-cert.sh | 116 --------------- pkg/cert/cert.go | 120 ++++++++++++++++ test/e2e/e2e_suite_test.go | 46 ++---- test/e2e/e2e_test.go | 17 +-- test/utils/utils.go | 66 +-------- 15 files changed, 416 insertions(+), 383 deletions(-) delete mode 100755 hack/gen-webhook-cert.sh create mode 100644 pkg/cert/cert.go diff --git a/Makefile b/Makefile index a648480..a31997c 100644 --- a/Makefile +++ b/Makefile @@ -95,8 +95,8 @@ test: manifests generate fmt vet setup-envtest ## Run tests. # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. -# CertManager is installed by default; skip with: -# - CERT_MANAGER_INSTALL_SKIP=true +# No cert-manager is needed: the manager provisions its own webhook serving cert +# in-process (pkg/cert), so the suite only needs a Kind cluster. KIND_CLUSTER ?= nebula-test-e2e .PHONY: setup-test-e2e diff --git a/cmd/main.go b/cmd/main.go index 2cb490f..82e3fff 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -20,6 +20,7 @@ import ( "context" "crypto/tls" "flag" + "fmt" "os" "path/filepath" @@ -46,6 +47,7 @@ import ( nebulav1alpha1 "github.com/InftyAI/Nebula/api/v1alpha1" "github.com/InftyAI/Nebula/internal/controller" webhookv1 "github.com/InftyAI/Nebula/internal/webhook/v1" + nebulacert "github.com/InftyAI/Nebula/pkg/cert" "github.com/InftyAI/Nebula/pkg/failover" "github.com/InftyAI/Nebula/pkg/provider" awsprovider "github.com/InftyAI/Nebula/pkg/provider/aws" @@ -55,6 +57,10 @@ import ( // +kubebuilder:scaffold:imports ) +// defaultNamespace is where the manager is installed by config/default. It is only +// a fallback for managerNamespace when POD_NAMESPACE is unset. +const defaultNamespace = "nebula-system" + var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") @@ -216,6 +222,27 @@ func main() { os.Exit(1) } + // Provision the webhook serving cert in-process, replacing both cert-manager and + // the out-of-band hack/gen-webhook-cert.sh. The rotator mints the keypair into a + // Secret, writes it where the webhook server reads it, patches the caBundle into + // the MutatingWebhookConfiguration, and keeps renewing it before expiry — the one + // thing neither prior approach did (the script's cert simply expired years later). + // + // certsReady closes once the cert is on disk AND the caBundle is patched. The + // webhook must not register before that: with failurePolicy=Fail, serving on a + // missing keypair means every Pod CREATE in the cluster fails admission. + certsReady := make(chan struct{}) + if enableWebhooks() { + if err := nebulacert.CertsManager(mgr, managerNamespace(), certsReady); err != nil { + setupLog.Error(err, "unable to set up cert rotation") + os.Exit(1) + } + } else { + // Nothing will close the channel, so close it here or the goroutine below would + // block forever and no controller would ever start. + close(certsReady) + } + // Register provider backends into the process-wide registry that both // reconcilers resolve through (their Providers field defaults to // provider.Get). Done before SetupWithManager so a pool/claim reconciled at @@ -231,27 +258,107 @@ func main() { // both sides rather than persisted. blocklist := failover.New() + // Controller and webhook registration is deferred until the cert exists, so it + // runs in a goroutine: the cert cannot be minted until the manager is STARTED + // (the rotator is a Runnable and needs a synced cache), so blocking here would + // deadlock. controller-runtime supports Add after Start — a Runnable registered + // then is started immediately — which is what makes this safe. + // + // The controllers wait too, not just the webhook. They create Pods, and every Pod + // CREATE goes through the defaulting webhook that injects the provider-selection + // gate; a Pod created while that webhook is untrusted would either be rejected + // (failurePolicy=Fail) or, worse, admitted ungated and scheduled by vanilla + // Kubernetes — silently bypassing placement and never reaching a provider. + go func() { + setupLog.Info("waiting for the webhook certificate to be ready") + <-certsReady + setupLog.Info("webhook certificate ready") + + if err := setupControllers(mgr, blocklist); err != nil { + setupLog.Error(err, "unable to set up controllers") + os.Exit(1) + } + }() + + if metricsCertWatcher != nil { + setupLog.Info("Adding metrics certificate watcher to manager") + if err := mgr.Add(metricsCertWatcher); err != nil { + setupLog.Error(err, "unable to add metrics certificate watcher to manager") + os.Exit(1) + } + } + + if webhookCertWatcher != nil { + setupLog.Info("Adding webhook certificate watcher to manager") + if err := mgr.Add(webhookCertWatcher); err != nil { + setupLog.Error(err, "unable to add webhook certificate watcher to manager") + os.Exit(1) + } + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +// enableWebhooks reports whether the Pod defaulting webhook (and therefore the cert +// rotator that makes it trustable) should run. It is off only when explicitly +// disabled, which is how the local `make run` and tests avoid needing a cert and a +// reachable Service. +func enableWebhooks() bool { + // nolint:goconst + return os.Getenv("ENABLE_WEBHOOKS") != "false" +} + +// managerNamespace is the namespace the manager runs in, which scopes both the +// webhook cert Secret and the cert's DNS name. It is read from POD_NAMESPACE +// (projected via fieldRef in config/manager/manager.yaml) rather than hardcoded, so +// an install into a non-default namespace still gets a cert the API server accepts. +// The fallback only matters for an out-of-cluster run, where the webhook is +// typically disabled anyway. +func managerNamespace() string { + if ns := os.Getenv("POD_NAMESPACE"); ns != "" { + return ns + } + setupLog.Info("POD_NAMESPACE is unset; falling back to the default install namespace", + "namespace", defaultNamespace) + return defaultNamespace +} + +// setupControllers registers every controller, the virtual nodes and the webhook. +// It runs only after the webhook serving cert is ready (see main), which is why it +// is a function rather than inline: everything here depends on Pod admission +// working, so none of it may be registered before the API server trusts the webhook. +func setupControllers(mgr ctrl.Manager, blocklist *failover.Blocklist) error { if err := (&controller.NodePoolReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "NodePool") - os.Exit(1) + return fmt.Errorf("unable to create NodePool controller: %w", err) } if err := (&controller.NodeClaimReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "NodeClaim") - os.Exit(1) + return fmt.Errorf("unable to create NodeClaim controller: %w", err) } if err := (&controller.PodPlacementReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Blocklist: blocklist, }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "PodPlacement") - os.Exit(1) + return fmt.Errorf("unable to create PodPlacement controller: %w", err) } // The workload controllers sit on top of the provisioning core above: each @@ -262,15 +369,13 @@ func main() { Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Sandbox") - os.Exit(1) + return fmt.Errorf("unable to create Sandbox controller: %w", err) } if err := (&controller.SandboxSetReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "SandboxSet") - os.Exit(1) + return fmt.Errorf("unable to create SandboxSet controller: %w", err) } // Start one virtual node per registered provider. The virtual kubelet owns @@ -279,48 +384,15 @@ func main() { // virtual node materializes an external instance. Each Runner is a // manager.Runnable, so it shares the manager's lifecycle and leader election. if err := setupVirtualNodes(mgr, blocklist); err != nil { - setupLog.Error(err, "unable to set up virtual nodes") - os.Exit(1) + return fmt.Errorf("unable to set up virtual nodes: %w", err) } - // nolint:goconst - if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if enableWebhooks() { if err := webhookv1.SetupPodWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "Pod") - os.Exit(1) + return fmt.Errorf("unable to create Pod webhook: %w", err) } } // +kubebuilder:scaffold:builder - - if metricsCertWatcher != nil { - setupLog.Info("Adding metrics certificate watcher to manager") - if err := mgr.Add(metricsCertWatcher); err != nil { - setupLog.Error(err, "unable to add metrics certificate watcher to manager") - os.Exit(1) - } - } - - if webhookCertWatcher != nil { - setupLog.Info("Adding webhook certificate watcher to manager") - if err := mgr.Add(webhookCertWatcher); err != nil { - setupLog.Error(err, "unable to add webhook certificate watcher to manager") - os.Exit(1) - } - } - - if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { - setupLog.Error(err, "unable to set up health check") - os.Exit(1) - } - if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { - setupLog.Error(err, "unable to set up ready check") - os.Exit(1) - } - - setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { - setupLog.Error(err, "problem running manager") - os.Exit(1) - } + return nil } // setupVirtualNodes adds a vnode.Runner to the manager for every registered diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 6e42628..ef51a10 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -24,11 +24,13 @@ resources: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - ../webhook -# [CERTMANAGER] cert-manager is intentionally NOT used. The webhook serving -# cert is generated as a self-signed cert by hack/gen-webhook-cert.sh (run via -# `make deploy-all`), which also injects the CA into the webhook config. To -# switch back to cert-manager, re-add `- ../certmanager` here and re-enable the -# CERTMANAGER replacements blocks below. See docs/deploy.md. +# [CERTMANAGER] cert-manager is intentionally NOT used, and neither is any +# out-of-band cert step. The manager provisions its own webhook serving cert +# in-process at startup (pkg/cert): it mints the keypair into a Secret, writes it +# to disk, patches the caBundle into the webhook config, and RENEWS it before +# expiry. To switch to cert-manager, re-add `- ../certmanager` here, re-enable the +# CERTMANAGER replacements blocks below, and drop the CertsManager call from +# cmd/main.go. See docs/deploy.md. #- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus @@ -130,7 +132,7 @@ replacements: # index: 1 # create: true -# [CERTMANAGER] Disabled — self-signed cert via hack/gen-webhook-cert.sh instead. +# [CERTMANAGER] Disabled — the cert is minted in-process by pkg/cert instead. # - source: # Uncomment the following block if you have any webhook # kind: Service # version: v1 @@ -199,8 +201,8 @@ replacements: # index: 1 # create: true -# [CERTMANAGER] Disabled — the CA is injected into the MutatingWebhookConfiguration -# by hack/gen-webhook-cert.sh (kubectl patch caBundle) instead of this annotation. +# [CERTMANAGER] Disabled — the CA is patched into the MutatingWebhookConfiguration +# by the in-process cert rotator (pkg/cert) instead of this annotation. # - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) # kind: Certificate # group: cert-manager.io diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml index 963c8a4..d49092b 100644 --- a/config/default/manager_webhook_patch.yaml +++ b/config/default/manager_webhook_patch.yaml @@ -1,18 +1,22 @@ -# This patch ensures the webhook certificates are properly mounted in the manager container. -# It configures the necessary arguments, volumes, volume mounts, and container ports. - -# Add the --webhook-cert-path argument for configuring the webhook certificate path -- op: add - path: /spec/template/spec/containers/0/args/- - value: --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs - -# Add the volumeMount for the webhook certificates -- op: add - path: /spec/template/spec/containers/0/volumeMounts/- - value: - mountPath: /tmp/k8s-webhook-server/serving-certs - name: webhook-certs - readOnly: true +# This patch wires up the webhook server: its port, and the directory it serves its +# TLS keypair from. +# +# The keypair is provisioned IN-PROCESS by the cert rotator (pkg/cert), not by +# cert-manager and not by an out-of-band script. That changes the volume this patch +# needs in two ways worth spelling out, because the kubebuilder default is wrong here: +# +# 1. It is an emptyDir, NOT a Secret projection. The rotator WRITES the keypair to +# this path, so a Secret volume — which is read-only, and whose kubelet refresh +# would fight the rotator — cannot work. The Secret still exists and is still the +# shared source of truth across replicas; the rotator reads it from the API and +# writes it here, rather than the kubelet projecting it. +# +# 2. There is no --webhook-cert-path argument. Passing it makes main build a +# certwatcher over the path, and the watcher fails at STARTUP if the files are not +# there yet — which they never are on a first install, because the rotator cannot +# mint them until the manager is running. controller-runtime's webhook server +# already reads this exact directory by default and picks up rotations, so the +# watcher is redundant as well as harmful. # Add the port configuration for the webhook server - op: add @@ -22,10 +26,18 @@ name: webhook-server protocol: TCP -# Add the volume configuration for the webhook certificates +# The directory the rotator writes the keypair to and the webhook server serves from. +# Must match certDir in pkg/cert (controller-runtime's default path). +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + mountPath: /tmp/k8s-webhook-server/serving-certs + name: webhook-certs + +# emptyDir, because the rotator writes here (see above). The container's root +# filesystem is read-only, so without this volume the write would fail outright. - op: add path: /spec/template/spec/volumes/- value: name: webhook-certs - secret: - secretName: webhook-server-cert + emptyDir: {} diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index f8b02c0..444da50 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -72,6 +72,15 @@ spec: # embedded in the binary. - name: NEBULA_CATALOG_DIR value: /etc/nebula/catalog + # The namespace the manager runs in, needed by the webhook cert rotator + # (pkg/cert): it scopes both the cert Secret and the cert's DNS name + # (..svc). Projected via fieldRef rather than hardcoded + # so an install into a non-default namespace still gets a cert the API + # server accepts. + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace envFrom: # Provider credentials live in a per-provider Secret, one secretRef per # provider — NOT a single shared secret. This matches the "creds-absent → diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 1371607..6c6b589 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -8,7 +8,6 @@ rules: - "" resources: - configmaps - - secrets - services verbs: - get @@ -43,6 +42,25 @@ rules: - get - patch - update +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - get + - list + - update + - watch +- apiGroups: + - admissionregistration.k8s.io + resources: + - mutatingwebhookconfigurations + verbs: + - get + - list + - update + - watch - apiGroups: - coordination.k8s.io resources: diff --git a/docs/deploy.md b/docs/deploy.md index c2bc8a5..a40a3fb 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -68,40 +68,44 @@ cluster) is passed as `make` variables, never committed to `.env`. Nebula runs a mutating webhook (it injects the scheduling gate into gated Pods), and the API server requires TLS to call it. **cert-manager is intentionally not -used** — there is no cert-manager prerequisite to install. Instead, -`hack/gen-webhook-cert.sh` provisions a self-signed certificate. It does the two -things cert-manager would otherwise automate: - -1. **Serving cert** — generates a self-signed cert for the webhook Service DNS - name (`nebula-webhook-service.nebula-system.svc`) and stores it in the - `webhook-server-cert` TLS Secret the manager mounts. -2. **CA trust** — injects that cert's CA into the `MutatingWebhookConfiguration` - `caBundle`, so the API server trusts the webhook. The caBundle is always read - back *from the Secret*, so the served cert and the trusted CA cannot drift. - -**No manager restart is involved.** `deploy-all` orders things so the manager -boots already-correct: the cert Secret is created *before* `make deploy` (it is a -required volume mount, and a running pod would otherwise need remounting), while -the caBundle patch happens *after* (it edits only the webhook config, which only -the API server reads — the manager never sees it). - -Re-running is safe: an existing cert Secret is kept as-is, and the caBundle is -re-derived to match. - -> **No auto-rotation.** This is the one thing cert-manager gives you that the -> self-signed cert does not. The cert is valid 10 years (`CERT_DAYS`, default -> `3650`). To rotate/renew, regenerate the cert and re-inject the CA: -> ```bash -> FORCE_REGEN=true hack/gen-webhook-cert.sh secret -> hack/gen-webhook-cert.sh cabundle -> kubectl rollout restart deployment/nebula-controller-manager -n nebula-system -> ``` -> The restart here is only because you are *replacing* the cert under a -> already-running manager — the initial deploy needs none. - -To switch back to cert-manager, re-add `- ../certmanager` and re-enable the +used**, and neither is any out-of-band setup step: the manager provisions its own +serving certificate in-process at startup (`pkg/cert`, built on +[cert-controller](https://github.com/open-policy-agent/cert-controller)). There is +nothing to install and nothing to run before `make deploy`. + +It does the three things that have to agree with each other: + +1. **Serving cert** — mints a self-signed cert for the webhook Service DNS name + (`nebula-webhook-service..svc`), stores it in the + `webhook-server-cert` Secret, and writes it to + `/tmp/k8s-webhook-server/serving-certs` where the webhook server reads it. +2. **CA trust** — patches that cert's CA into the `MutatingWebhookConfiguration` + `caBundle`, so the API server trusts the webhook. It is derived from the cert + just written, so the served cert and the trusted CA cannot drift. +3. **Renewal** — rotates the cert before it expires. This is the part neither + cert-manager-free alternative had: a script-minted cert simply expires, years + later, when nobody remembers a script was involved. + +The Secret is the shared source of truth across replicas — a second replica finds +the existing cert there and writes it to its own disk rather than minting a +competing one. Rotation is *not* leader-elected, because webhook serving is not +either: every replica needs the keypair on its own local disk, and the API server +will call a non-leader. + +**Startup ordering.** Nothing that depends on Pod admission is registered until the +cert is ready, so the first seconds of a fresh install log +`waiting for the webhook certificate to be ready` and reconcile nothing. That is +deliberate: with `failurePolicy: Fail` a Pod created before the webhook is trusted +would be rejected, and without the gate it would be scheduled by vanilla Kubernetes +— silently bypassing placement and never reaching a provider. + +The cert volume is an `emptyDir`, not a Secret projection, because the rotator +*writes* to that path; a Secret volume is read-only and its kubelet refresh would +fight the rotator. + +To switch to cert-manager instead, re-add `- ../certmanager` and re-enable the `CERTMANAGER` replacements blocks in `config/default/kustomization.yaml`, install -cert-manager, and drop the `gen-webhook-cert.sh` calls from `hack/deploy.sh`. +cert-manager, and drop the `CertsManager` call from `cmd/main.go`. --- @@ -151,23 +155,20 @@ Non-secret config, passed as `make` variables: If you don't want the script (e.g. you manage Secrets via sealed-secrets or a GitOps pipeline), do the same steps by hand. Order matters — create the Secrets -before deploying so the manager boots configured, and inject the CA after: +before deploying so the manager boots configured: ```bash -# 1. Namespace + webhook serving cert Secret (before deploy — it's a volume mount). +# 1. Namespace. kubectl create namespace nebula-system --dry-run=client -o yaml | kubectl apply -f - -hack/gen-webhook-cert.sh secret # 2. Modal credential Secret (before deploy — read as env at pod startup). kubectl create secret generic nebula-modal-credentials -n nebula-system \ --from-literal=MODAL_TOKEN_ID=ak-... \ --from-literal=MODAL_TOKEN_SECRET=as-... -# 3. Deploy CRDs + manager. The pod mounts the cert and reads creds on first boot. +# 3. Deploy CRDs + manager. The pod reads creds on first boot, and provisions its +# own webhook cert + caBundle at startup — no cert step of your own. make deploy IMG=/nebula: - -# 4. Inject the webhook CA (server-side; needs the webhook config to exist). -hack/gen-webhook-cert.sh cabundle ``` No restart is needed — everything the manager consumes exists before it boots. diff --git a/go.mod b/go.mod index 6b222b3..77c3fb7 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,12 @@ require ( github.com/modal-labs/modal-client/go v0.9.0 github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 + github.com/open-policy-agent/cert-controller v0.14.0 github.com/prometheus/client_model v0.6.1 github.com/virtual-kubelet/virtual-kubelet v1.11.0 - k8s.io/api v0.33.3 - k8s.io/apimachinery v0.33.3 - k8s.io/client-go v0.33.3 + k8s.io/api v0.33.4 + k8s.io/apimachinery v0.33.4 + k8s.io/client-go v0.33.4 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 sigs.k8s.io/controller-runtime v0.21.0 ) @@ -42,7 +43,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/djherbis/buffer v1.2.0 // indirect github.com/djherbis/nio/v3 v3.0.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -51,7 +52,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -94,6 +95,7 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.4.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect @@ -115,9 +117,9 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.33.0 // indirect - k8s.io/apiserver v0.33.0 // indirect - k8s.io/component-base v0.33.0 // indirect + k8s.io/apiextensions-apiserver v0.33.4 // indirect + k8s.io/apiserver v0.33.4 // indirect + k8s.io/component-base v0.33.4 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect diff --git a/go.sum b/go.sum index 83be65a..7ce5be5 100644 --- a/go.sum +++ b/go.sum @@ -54,7 +54,6 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -64,8 +63,8 @@ github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE= github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4= github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= +github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -93,12 +92,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -166,11 +163,8 @@ github.com/kisielk/og-rek v1.3.0 h1:lTXdQXqFETZKA//FWH4RBNAuiJ/dofxIwHAidoUZoMk= github.com/kisielk/og-rek v1.3.0/go.mod h1:4at7oxyfBTDilURhNCf7irHWtosJlJl9uyqUqAkrP4w= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -198,6 +192,10 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/open-policy-agent/cert-controller v0.14.0 h1:TPc19BOHOs4tARruTT5o4bzir7Ed6FF+j3EXP/nmZBs= +github.com/open-policy-agent/cert-controller v0.14.0/go.mod h1:UhE/FU54DnKo+Rt0Yf3r+oKjgy6kqSH8Vsjo+5bGrSo= +github.com/open-policy-agent/frameworks/constraint v0.0.0-20241101234656-e78c8abd754a h1:gQtOJ50XFyL2Xh3lDD9zP4KQ2PY4mZKQ9hDcWc81Sp8= +github.com/open-policy-agent/frameworks/constraint v0.0.0-20241101234656-e78c8abd754a/go.mod h1:tI7nc6H6os2UYZRvSm9Y7bq4oMoXqhwA0WfnqKpoAgc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -272,6 +270,8 @@ go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6 go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -387,20 +387,22 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= -k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= -k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= -k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= -k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= -k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc= -k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8= -k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= -k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= -k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk= -k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU= +k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk= +k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc= +k8s.io/apiextensions-apiserver v0.33.4 h1:rtq5SeXiDbXmSwxsF0MLe2Mtv3SwprA6wp+5qh/CrOU= +k8s.io/apiextensions-apiserver v0.33.4/go.mod h1:mWXcZQkQV1GQyxeIjYApuqsn/081hhXPZwZ2URuJeSs= +k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s= +k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.4 h1:6N0TEVA6kASUS3owYDIFJjUH6lgN8ogQmzZvaFFj1/Y= +k8s.io/apiserver v0.33.4/go.mod h1:8ODgXMnOoSPLMUg1aAzMFx+7wTJM+URil+INjbTZCok= +k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw= +k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY= +k8s.io/component-base v0.33.4 h1:Jvb/aw/tl3pfgnJ0E0qPuYLT0NwdYs1VXXYQmSuxJGY= +k8s.io/component-base v0.33.4/go.mod h1:567TeSdixWW2Xb1yYUQ7qk5Docp2kNznKL87eygY8Rc= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-aggregator v0.33.4 h1:TdIJKHb0/bLpby7FblXIaVEzyA1jGEjzt/n9cRvwq8U= +k8s.io/kube-aggregator v0.33.4/go.mod h1:wZuctdRvGde5bwzxkZRs0GYj2KOpCNgx8rRGVoNb62k= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= diff --git a/hack/deploy.sh b/hack/deploy.sh index ec4a4a9..55adb42 100755 --- a/hack/deploy.sh +++ b/hack/deploy.sh @@ -164,10 +164,12 @@ fi log "ensuring namespace ${NAMESPACE}" "${KUBECTL}" create namespace "${NAMESPACE}" --dry-run=client -o yaml | "${KUBECTL}" apply -f - -# Webhook serving cert (no cert-manager): generate the self-signed cert into the -# Secret now. The caBundle is injected later, after the webhook config exists. -log "provisioning webhook serving certificate Secret (self-signed)" -NAMESPACE="${NAMESPACE}" KUBECTL="${KUBECTL}" hack/gen-webhook-cert.sh secret +# No webhook cert step here, deliberately. The manager provisions its own serving +# cert in-process at startup (pkg/cert) — it mints the keypair, stores it in a Secret, +# writes it to disk and patches the caBundle into the MutatingWebhookConfiguration, +# then keeps RENEWING it before expiry. That last part is why it replaced the previous +# hack/gen-webhook-cert.sh: a script-minted cert never rotates, so its expiry is a +# time bomb that fires years later when nobody remembers the script exists. # Provider credential Secrets, one per provider (blank required keys → skipped). for row in "${PROVIDER_SECRETS[@]}"; do @@ -181,11 +183,15 @@ done log "installing CRDs and deploying the manager" make deploy IMG="${IMG}" -# --- 5. inject the webhook CA bundle (server-side, no manager restart) ------ -# This edits only the MutatingWebhookConfiguration, which just got created by -# `make deploy`. Only the API server reads caBundle, so the manager is untouched. -log "injecting webhook CA bundle" -NAMESPACE="${NAMESPACE}" KUBECTL="${KUBECTL}" hack/gen-webhook-cert.sh cabundle +# No CA-bundle injection step either: the manager patches its own caBundle once it +# starts, from the same cert it just wrote — so the served cert and the trusted CA +# cannot drift, which two separate steps could never fully guarantee. +# +# One consequence worth knowing when watching a first install: the manager registers +# NO controllers or webhook until that cert is ready, so the first few seconds of logs +# show "waiting for the webhook certificate to be ready" and nothing reconciles yet. +# That ordering is deliberate — a Pod admitted while the webhook is untrusted would be +# scheduled by vanilla Kubernetes and silently bypass placement. log "done. Check status with:" printf ' %s -n %s get pods\n' "${KUBECTL}" "${NAMESPACE}" diff --git a/hack/gen-webhook-cert.sh b/hack/gen-webhook-cert.sh deleted file mode 100755 index f5902a4..0000000 --- a/hack/gen-webhook-cert.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env bash -# -# gen-webhook-cert.sh — provision the webhook serving cert WITHOUT cert-manager. -# -# The webhook needs two things cert-manager would otherwise automate: -# 1. a TLS Secret (webhook-server-cert) the manager mounts and serves from; -# 2. that cert's CA injected into the MutatingWebhookConfiguration caBundle, -# so the API server trusts the webhook when it calls it. -# -# These two steps have DIFFERENT ordering requirements, so this script exposes -# them as separate actions (see usage) and a running manager never needs a -# restart: -# - the Secret must exist BEFORE the manager pod starts (it is a required -# volume mount), so create it before `make deploy`; -# - the caBundle patch is server-side (only the API server reads it), so it -# runs AFTER `make deploy` creates the webhook config. The manager is -# untouched by it. -# -# The caBundle is always derived from the cert already in the Secret, so the -# served cert and the trusted CA can never drift, even across re-runs. -# -# Usage: -# hack/gen-webhook-cert.sh secret # ensure the TLS Secret exists (generate if absent) -# hack/gen-webhook-cert.sh cabundle # inject the Secret's cert into the webhook config -# hack/gen-webhook-cert.sh all # both, in order (default; standalone use) -# -# Config (env / make flags): -# NAMESPACE namespace the manager runs in (default nebula-system) -# SERVICE webhook Service name (default nebula-webhook-service) -# SECRET TLS Secret the manager mounts (default webhook-server-cert) -# WEBHOOK_CONFIG MutatingWebhookConfiguration name (default nebula-mutating-webhook-configuration) -# CERT_DAYS certificate validity in days (default 3650) -# FORCE_REGEN if "true", regenerate even if the Secret exists (default false) -# KUBECTL kubectl binary (default kubectl) -# -# NOTE: the self-signed cert does NOT auto-rotate (that is cert-manager's main -# advantage). It is valid CERT_DAYS days; re-run with FORCE_REGEN=true to renew. -set -euo pipefail - -NAMESPACE="${NAMESPACE:-nebula-system}" -SERVICE="${SERVICE:-nebula-webhook-service}" -SECRET="${SECRET:-webhook-server-cert}" -WEBHOOK_CONFIG="${WEBHOOK_CONFIG:-nebula-mutating-webhook-configuration}" -CERT_DAYS="${CERT_DAYS:-3650}" -FORCE_REGEN="${FORCE_REGEN:-false}" -KUBECTL="${KUBECTL:-kubectl}" -ACTION="${1:-all}" - -log() { printf '\033[36m==>\033[0m %s\n' "$*"; } -die() { printf '\033[31mERROR:\033[0m %s\n' "$*" >&2; exit 1; } - -command -v "${KUBECTL}" >/dev/null 2>&1 || die "kubectl not found on PATH" - -CN="${SERVICE}.${NAMESPACE}.svc" - -# ensure_secret — create the TLS Secret if absent (or if FORCE_REGEN=true). -# Leaves an existing Secret untouched so re-runs don't needlessly rotate the -# cert (which would otherwise require remounting on the manager). -ensure_secret() { - if [[ "${FORCE_REGEN}" != "true" ]] && \ - "${KUBECTL}" get secret "${SECRET}" -n "${NAMESPACE}" >/dev/null 2>&1; then - log "Secret ${SECRET} already exists in ${NAMESPACE}; keeping it (FORCE_REGEN=true to rotate)" - return 0 - fi - - command -v openssl >/dev/null 2>&1 || die "openssl not found on PATH" - - local tmp - tmp="$(mktemp -d)" - # shellcheck disable=SC2064 - trap "rm -rf '${tmp}'" RETURN - - log "generating self-signed cert for ${CN} (valid ${CERT_DAYS}d)" - # The cert is its own CA: it both serves TLS and is trusted via caBundle. - # SANs cover both DNS forms the webhook may be addressed by. - openssl req -x509 -newkey rsa:2048 -nodes \ - -keyout "${tmp}/tls.key" -out "${tmp}/tls.crt" \ - -days "${CERT_DAYS}" -subj "/CN=${CN}" \ - -addext "subjectAltName=DNS:${CN},DNS:${CN}.cluster.local" >/dev/null 2>&1 - - log "applying TLS Secret ${SECRET} in ${NAMESPACE}" - "${KUBECTL}" create secret tls "${SECRET}" \ - --cert="${tmp}/tls.crt" --key="${tmp}/tls.key" \ - --namespace "${NAMESPACE}" \ - --dry-run=client -o yaml | "${KUBECTL}" apply -f - -} - -# inject_cabundle — read tls.crt from the Secret and set it as the webhook's -# caBundle. Deriving from the Secret guarantees the trusted CA matches the -# served cert. Requires the MutatingWebhookConfiguration to already exist. -inject_cabundle() { - "${KUBECTL}" get secret "${SECRET}" -n "${NAMESPACE}" >/dev/null 2>&1 \ - || die "Secret ${SECRET} not found in ${NAMESPACE}; run '$0 secret' first" - "${KUBECTL}" get mutatingwebhookconfiguration "${WEBHOOK_CONFIG}" >/dev/null 2>&1 \ - || die "${WEBHOOK_CONFIG} not found; deploy the manager (make deploy) before injecting the CA" - - # tls.crt in the Secret is already base64-encoded, which is exactly the form - # caBundle wants — no re-encoding needed. - local ca_b64 - ca_b64="$("${KUBECTL}" get secret "${SECRET}" -n "${NAMESPACE}" -o jsonpath='{.data.tls\.crt}')" - [[ -n "${ca_b64}" ]] || die "Secret ${SECRET} has no tls.crt" - - log "injecting CA bundle into ${WEBHOOK_CONFIG}" - # JSON Patch "add" on an existing member replaces it, so this is correct on - # both first run (caBundle absent) and re-runs (caBundle present). - "${KUBECTL}" patch mutatingwebhookconfiguration "${WEBHOOK_CONFIG}" \ - --type=json \ - -p="[{\"op\":\"add\",\"path\":\"/webhooks/0/clientConfig/caBundle\",\"value\":\"${ca_b64}\"}]" -} - -case "${ACTION}" in - secret) ensure_secret ;; - cabundle) inject_cabundle ;; - all) ensure_secret; inject_cabundle ;; - *) die "unknown action '${ACTION}' (want: secret | cabundle | all)" ;; -esac diff --git a/pkg/cert/cert.go b/pkg/cert/cert.go new file mode 100644 index 0000000..fde3f9d --- /dev/null +++ b/pkg/cert/cert.go @@ -0,0 +1,120 @@ +/* +Copyright 2026 The InftyAI Team. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package cert provisions the webhook serving certificate in-process, so Nebula +// has no cert-manager dependency and no out-of-band setup step. +// +// The webhook needs two things that must agree exactly: a TLS keypair the manager +// serves from, and that cert's CA in the MutatingWebhookConfiguration's caBundle +// so the API server trusts the webhook when it calls it. There were two prior ways +// to get them, and both are worse: +// +// - cert-manager: rotates correctly, but is a whole second operator the user must +// install before Nebula works at all. It also made the e2e suite install +// cert-manager in BeforeSuite, which is why that suite could not run without +// network access to fetch its manifests. +// - hack/gen-webhook-cert.sh: no dependency, but the cert is minted by a shell +// script at deploy time and NEVER rotates. It is valid for CERT_DAYS (default +// 3650) and renewing means re-running the script with FORCE_REGEN — i.e. the +// expiry is a silent time bomb that fires years later, when nobody remembers +// the script exists. +// +// The rotator does both jobs in-process: it mints the keypair into a Secret, writes +// it to CertDir for the webhook server to serve, patches the caBundle, and then +// keeps renewing before expiry. Because the CA is patched from the same cert that +// was just written, the served cert and the trusted CA cannot drift. +package cert + +import ( + "fmt" + + rotator "github.com/open-policy-agent/cert-controller/pkg/rotator" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + // serviceName is the webhook Service the cert is issued for. The rotator needs + // it to build the DNS name the API server dials, so it must match + // config/webhook/service.yaml AFTER kustomize applies the nebula- namePrefix. + serviceName = "nebula-webhook-service" + + // secretName is the Secret the keypair is stored in. The manager also MOUNTS + // this Secret (config/default/manager_webhook_patch.yaml), which is why the + // name must match there too. + secretName = "webhook-server-cert" + + // certDir is where the rotator writes the keypair and where controller-runtime's + // webhook server reads it from. It is the controller-runtime default, and the + // same path the manager mounts the Secret at. + certDir = "/tmp/k8s-webhook-server/serving-certs" + + // mutatingWebhookConfName is the MutatingWebhookConfiguration whose caBundle + // gets patched — the nebula- prefixed name from config/webhook/manifests.yaml. + // + // There is deliberately no ValidatingWebhookConfiguration here: Nebula has only + // the Pod defaulter (see internal/webhook/v1), and Sandbox validation is done + // with CEL in the CRD rather than a webhook. Naming a config that does not exist + // would make the rotator fail to patch it on every reconcile. + mutatingWebhookConfName = "nebula-mutating-webhook-configuration" + + caName = "nebula-ca" + caOrg = "nebula" +) + +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update +// +kubebuilder:rbac:groups="admissionregistration.k8s.io",resources=mutatingwebhookconfigurations,verbs=get;list;watch;update + +// CertsManager registers the cert rotator with the manager. It closes setupFinish +// once the cert is on disk and the caBundle is patched. +// +// Nothing that depends on the webhook may start before that channel closes: the +// webhook server would otherwise serve on a missing keypair and the API server +// would reject the call, which — with failurePolicy=Fail — means every Pod CREATE +// in the cluster fails admission. The caller runs controller setup in a goroutine +// blocked on this channel (see cmd/main.go). +// +// namespace must be the namespace the manager actually runs in, since it scopes +// both the Secret and the cert's DNS name; it is read from POD_NAMESPACE rather +// than hardcoded so a non-default install namespace still gets a valid cert. +func CertsManager(mgr ctrl.Manager, namespace string, setupFinish chan struct{}) error { + // The DNS name the API server dials, and therefore the name the cert must be + // valid for: ..svc. + dnsName := fmt.Sprintf("%s.%s.svc", serviceName, namespace) + + return rotator.AddRotator(mgr, &rotator.CertRotator{ + SecretKey: types.NamespacedName{ + Namespace: namespace, + Name: secretName, + }, + CertDir: certDir, + CAName: caName, + CAOrganization: caOrg, + DNSName: dnsName, + IsReady: setupFinish, + Webhooks: []rotator.WebhookInfo{{ + Type: rotator.Mutating, + Name: mutatingWebhookConfName, + }}, + // RequireLeaderElection is deliberately left false. The rotator must run in + // EVERY replica, not just the leader: CertDir is each pod's own local disk, and a + // replica that never wrote the keypair there cannot serve the webhook — and + // webhook serving is not leader-elected, so the API server will call a + // non-leader. The Secret is the shared source of truth, so replicas after the + // first find a valid cert there and simply write it to their own disk rather than + // minting a competing one. + }) +} diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 5ef295b..b8a0ed7 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -18,7 +18,6 @@ package e2e import ( "fmt" - "os" "os/exec" "testing" @@ -28,24 +27,18 @@ import ( "github.com/InftyAI/Nebula/test/utils" ) -var ( - // Optional Environment Variables: - // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup. - // These variables are useful if CertManager is already installed, avoiding - // re-installation and conflicts. - skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" - // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster - isCertManagerAlreadyInstalled = false +// projectImage is the name of the image which will be build and loaded +// with the code source changes to be tested. +const projectImage = "example.com/nebula:v0.0.1" - // projectImage is the name of the image which will be build and loaded - // with the code source changes to be tested. - projectImage = "example.com/nebula:v0.0.1" -) +// NOTE: there is deliberately no cert-manager setup here. The manager provisions its +// own webhook serving cert in-process (pkg/cert), so this suite needs neither a +// cert-manager install nor the network access to fetch its manifests — which is what +// used to make BeforeSuite fail on a machine without them. // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, // temporary environment to validate project changes with the purpose of being used in CI jobs. -// The default setup requires Kind, builds/loads the Manager Docker image locally, and installs -// CertManager. +// The default setup requires Kind and builds/loads the Manager Docker image locally. func TestE2E(t *testing.T) { RegisterFailHandler(Fail) _, _ = fmt.Fprintf(GinkgoWriter, "Starting nebula integration test suite\n") @@ -63,27 +56,4 @@ var _ = BeforeSuite(func() { By("loading the manager(Operator) image on Kind") err = utils.LoadImageToKindClusterWithName(projectImage) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind") - - // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing. - // To prevent errors when tests run in environments with CertManager already installed, - // we check for its presence before execution. - // Setup CertManager before the suite if not skipped and if not already installed - if !skipCertManagerInstall { - By("checking if cert manager is installed already") - isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled() - if !isCertManagerAlreadyInstalled { - _, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n") - Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") - } else { - _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n") - } - } -}) - -var _ = AfterSuite(func() { - // Teardown CertManager after the suite if not skipped and if it was not already installed - if !skipCertManagerInstall && !isCertManagerAlreadyInstalled { - _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n") - utils.UninstallCertManager() - } }) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index f5b2d29..2c15106 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -81,15 +81,10 @@ var _ = Describe("Manager", Ordered, func() { _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") - // The manager mounts webhook-server-cert as a REQUIRED volume, so the - // self-signed cert Secret must exist before the pod starts, or it wedges - // in ContainerCreating (Pending) forever. Nebula does not use cert-manager - // (see config/default/kustomization.yaml); hack/gen-webhook-cert.sh is the - // source of truth, matching the ordering hack/deploy.sh uses in prod. - By("provisioning the webhook serving certificate Secret") - cmd = exec.Command("hack/gen-webhook-cert.sh", "secret") - _, err = utils.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to provision the webhook serving cert Secret") + // No cert step here: the manager mints its own webhook serving cert at + // startup (pkg/cert) into an emptyDir, so there is nothing to pre-create — + // the same ordering hack/deploy.sh uses in prod. The cert only exists once + // the manager is RUNNING, so the assertions below must be Eventually. // Deploy via the e2e overlay, which bakes NEBULA_ENABLE_FAKE_PROVIDER=true // into the manager env so the in-memory fake provider registers at first @@ -306,8 +301,8 @@ var _ = Describe("Manager", Ordered, func() { }) It("should have the self-signed webhook serving cert Secret", func() { - // Nebula does not use cert-manager; hack/gen-webhook-cert.sh creates this - // self-signed Secret in the e2e BeforeAll (and hack/deploy.sh in prod). + // The manager's own cert rotator (pkg/cert) creates this Secret after it + // starts — nothing pre-creates it, so this asserts the rotator ran. By("validating that the webhook serving cert Secret exists") verifyCertSecret := func(g Gomega) { cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace) diff --git a/test/utils/utils.go b/test/utils/utils.go index 8ae13e7..0d33551 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -31,9 +31,6 @@ const ( prometheusOperatorVersion = "v0.77.1" prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + "releases/download/%s/bundle.yaml" - - certmanagerVersion = "v1.16.3" - certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" ) func warnError(err error) { @@ -106,66 +103,9 @@ func IsPrometheusCRDsInstalled() bool { return false } -// UninstallCertManager uninstalls the cert manager -func UninstallCertManager() { - url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) - cmd := exec.Command("kubectl", "delete", "-f", url) - if _, err := Run(cmd); err != nil { - warnError(err) - } -} - -// InstallCertManager installs the cert manager bundle. -func InstallCertManager() error { - url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) - cmd := exec.Command("kubectl", "apply", "-f", url) - if _, err := Run(cmd); err != nil { - return err - } - // Wait for cert-manager-webhook to be ready, which can take time if cert-manager - // was re-installed after uninstalling on a cluster. - cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", - "--for", "condition=Available", - "--namespace", "cert-manager", - "--timeout", "5m", - ) - - _, err := Run(cmd) - return err -} - -// IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed -// by verifying the existence of key CRDs related to Cert Manager. -func IsCertManagerCRDsInstalled() bool { - // List of common Cert Manager CRDs - certManagerCRDs := []string{ - "certificates.cert-manager.io", - "issuers.cert-manager.io", - "clusterissuers.cert-manager.io", - "certificaterequests.cert-manager.io", - "orders.acme.cert-manager.io", - "challenges.acme.cert-manager.io", - } - - // Execute the kubectl command to get all CRDs - cmd := exec.Command("kubectl", "get", "crds") - output, err := Run(cmd) - if err != nil { - return false - } - - // Check if any of the Cert Manager CRDs are present - crdList := GetNonEmptyLines(output) - for _, crd := range certManagerCRDs { - for _, line := range crdList { - if strings.Contains(line, crd) { - return true - } - } - } - - return false -} +// No cert-manager helpers here, deliberately: the manager provisions its own +// webhook serving cert in-process (pkg/cert), so the e2e suite has nothing to +// install or wait for before deploying. // LoadImageToKindClusterWithName loads a local docker image to the kind cluster func LoadImageToKindClusterWithName(name string) error { From f4e6dc37dc69d8dd99ca9d758de870ca4c148e67 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 11:16:37 +0100 Subject: [PATCH 16/22] chore: ignore locally built command binaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `go build ./cmd/...` writes manager/keybroker into the repo ROOT, which bin/* does not cover. They are multi-MB, so an 8.4MB binary can ride along in a `git add -A` unnoticed — which is exactly what nearly happened. Co-Authored-By: Claude Opus 5 --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 04b6cc1..1c7af81 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,12 @@ bin/* Dockerfile.cross +# Locally built command binaries. `go build ./cmd/...` drops these in the repo +# ROOT, not bin/, so they are not covered above — and they are multi-MB, which is +# exactly the kind of thing that sneaks into a commit unnoticed. +/manager +/keybroker + # Test binary, build with `go test -c` *.test From 99bc2a0725c56f6bbeebea21b4985598442b31d3 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 11:57:40 +0100 Subject: [PATCH 17/22] lint: fix the three findings this branch introduced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main lints clean; all three came from this branch. goconst: the "true" opt-in value appeared in four places (placement controller and helpers, the Pod webhook, the sandbox controller) because the sandbox work added a fourth. It is now nebulav1alpha1.EnabledValue, next to EnabledLabel — worth a named constant beyond silencing the linter, since the label doubles as the webhook's objectSelector and the API server matches it LITERALLY. A Pod labelled "True" is not opted in, and having the value written out per call site is how the controllers and the selector could come to disagree about which Pods are Nebula's. unparam: sandboxStatusFromPod took a *Sandbox it never read. Dropped rather than used — the projection must depend on nothing but observed Pod state, or a stale value already on Sandbox.Status could feed back into the next projection and latch. Noted in the doc comment so it does not get "fixed" by adding a use. lll: a kubebuilder RBAC marker in pkg/cert exceeded 120 chars and cannot be wrapped — controller-gen parses one marker per comment line and has no continuation syntax. Excluded by SOURCE pattern rather than by path, so the exemption covers exactly the lines that are physically unwrappable instead of turning lll off for a whole directory. Co-Authored-By: Claude Opus 5 --- .golangci.yml | 6 ++++++ api/v1alpha1/groupversion_info.go | 7 +++++++ internal/controller/pod_placement_controller.go | 2 +- internal/controller/pod_placement_helpers.go | 2 +- internal/controller/sandbox_controller.go | 10 +++++++--- internal/webhook/v1/pod_webhook.go | 2 +- 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index d25815d..d582d45 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -32,6 +32,12 @@ linters: - linters: - lll path: api/* + # kubebuilder markers are single-line BY PROTOCOL — controller-gen parses one + # marker per comment line and there is no continuation syntax, so a long + # groups/resources/verbs triple simply cannot be wrapped to satisfy lll. + - linters: + - lll + source: '^\s*//\s*\+kubebuilder:' - linters: - dupl - lll diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go index 0566901..bd4dbb4 100644 --- a/api/v1alpha1/groupversion_info.go +++ b/api/v1alpha1/groupversion_info.go @@ -44,6 +44,13 @@ const ( // objectSelector so only opted-in Pods ever hit the mutating webhook. EnabledLabel = "nebula.inftyai.com/enabled" + // EnabledValue is the only value of EnabledLabel that opts a Pod in. The + // comparison is exact, so a Pod labelled "True" or "1" is NOT opted in — the + // label is the webhook's objectSelector, and the API server matches it + // literally, so anything else would make the controllers and the selector + // disagree about which Pods are Nebula's. + EnabledValue = "true" + // ProviderSelectionGate is the scheduling gate the webhook injects at Pod // CREATE. The placement controller removes it once it has chosen a // provider (by adding a provider nodeSelector), releasing the Pod to the diff --git a/internal/controller/pod_placement_controller.go b/internal/controller/pod_placement_controller.go index c201a4a..6ca65ca 100644 --- a/internal/controller/pod_placement_controller.go +++ b/internal/controller/pod_placement_controller.go @@ -199,7 +199,7 @@ func needsPlacement(pod *corev1.Pod) bool { if !pod.DeletionTimestamp.IsZero() { return false } - if pod.Labels[nebulav1alpha1.EnabledLabel] != "true" { + if pod.Labels[nebulav1alpha1.EnabledLabel] != nebulav1alpha1.EnabledValue { return false } if pod.Spec.NodeName != "" { diff --git a/internal/controller/pod_placement_helpers.go b/internal/controller/pod_placement_helpers.go index 0077795..a12c823 100644 --- a/internal/controller/pod_placement_helpers.go +++ b/internal/controller/pod_placement_helpers.go @@ -333,7 +333,7 @@ func removeGate(gates []corev1.PodSchedulingGate, name string) []corev1.PodSched // Delete is UID-pinned so a Pod already replaced by a same-name recreate is not // clobbered, and a NotFound (already gone) is treated as success. func (r *PodPlacementReconciler) reapTerminalPod(ctx context.Context, pod *corev1.Pod) (bool, error) { - if pod.Labels[nebulav1alpha1.EnabledLabel] != "true" { + if pod.Labels[nebulav1alpha1.EnabledLabel] != nebulav1alpha1.EnabledValue { return false, nil } if !pod.DeletionTimestamp.IsZero() { diff --git a/internal/controller/sandbox_controller.go b/internal/controller/sandbox_controller.go index b973ac5..4d2982e 100644 --- a/internal/controller/sandbox_controller.go +++ b/internal/controller/sandbox_controller.go @@ -109,7 +109,7 @@ func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, err } - st := sandboxStatusFromPod(&sbx, pod) + st := sandboxStatusFromPod(pod) if err := r.setStatus(ctx, &sbx, st); err != nil { return ctrl.Result{}, err } @@ -164,7 +164,7 @@ func (r *SandboxReconciler) buildPod(sbx *nebulav1alpha1.Sandbox) *corev1.Pod { labels := map[string]string{ // EnabledLabel is the opt-in the mutating webhook selects on: without it the // Pod would be scheduled by vanilla Kubernetes and never reach a provider. - nebulav1alpha1.EnabledLabel: "true", + nebulav1alpha1.EnabledLabel: nebulav1alpha1.EnabledValue, nebulav1alpha1.ManagedByLabel: nebulav1alpha1.ManagedByValue, nebulav1alpha1.PoolLabel: sbx.Spec.NodePoolRef, nebulav1alpha1.SandboxLabel: sbx.Name, @@ -236,11 +236,15 @@ type sandboxStatus struct { // is doing, so this reads it rather than tracking instance state independently — // two sources for one fact is how they drift. // +// It deliberately takes only the Pod, not the Sandbox: the projection must depend +// on nothing but observed Pod state, or a stale value already on Sandbox.Status +// could feed back into the next projection and latch. +// // The mapping keys off the Pod's status REASON, not just its phase, because the // interesting distinction for a user is inside PodPending: "cannot get capacity" // (Provisioning) versus "capacity granted, still booting" (Initializing). The // vnode stamps those reasons (see pkg/vnode/status.go). -func sandboxStatusFromPod(sbx *nebulav1alpha1.Sandbox, pod *corev1.Pod) sandboxStatus { +func sandboxStatusFromPod(pod *corev1.Pod) sandboxStatus { endpoint := pod.Annotations[nebulav1alpha1.EndpointAnnotation] switch pod.Status.Phase { diff --git a/internal/webhook/v1/pod_webhook.go b/internal/webhook/v1/pod_webhook.go index 74a75dc..3c43a80 100644 --- a/internal/webhook/v1/pod_webhook.go +++ b/internal/webhook/v1/pod_webhook.go @@ -74,7 +74,7 @@ func (d *PodCustomDefaulter) Default(_ context.Context, obj runtime.Object) erro return fmt.Errorf("expected a Pod object but got %T", obj) } - if pod.Labels[nebulav1alpha1.EnabledLabel] != "true" { + if pod.Labels[nebulav1alpha1.EnabledLabel] != nebulav1alpha1.EnabledValue { return nil // not opted in; leave the Pod untouched } if pod.Spec.NodeName != "" { From 65ee48d8f8b58862ce8d4ca51f6b5c3a9f2cb677 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 13:14:53 +0100 Subject: [PATCH 18/22] fix: make the in-process webhook cert actually reach the manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The self-signed cert work landed with three wiring bugs that together meant a fresh install never came up. Each one hid the next, so they are fixed together. 1. The Secret was never created. cert-controller's rotator does a Get followed by an Update on its Secret and never a Create, so an absent Secret is a fatal startup error ("acquiring secret to update certificates: not found") that crash-loops the manager. Ship it empty from config/webhook, the way gatekeeper does — nil Data is exactly what triggers minting. 2. The name did not match what kustomize renders. pkg/cert looked for "webhook-server-cert" while config/default's namePrefix produces "nebula-webhook-server-cert", so even with the Secret in place the rotator looked for one no overlay creates. secretName was the odd one out here; serviceName and mutatingWebhookConfName already carried the prefix. 3. certDir was an emptyDir, so the keypair never reached disk. This was the subtle one: the rotator does NOT write files — cert-controller has no disk writes at all, and CertDir is a path it only os.Stat()s to decide readiness (ensureCertsMounted). The kubelet is what puts the keypair there, via a Secret projection. With an emptyDir, tls.crt never appeared, IsReady never closed, and since controller/webhook registration waits on that channel, NOTHING ever started: no reconcilers, no virtual nodes, no webhook. The manager stayed 1/1 Running and looked healthy the whole time, which is what made this hard to see — it surfaced only as the e2e placement spec timing out because no virtual node ever registered. Also drop the stale hack/gen-webhook-cert.sh cabundle step from the e2e suite (the manager patches its own caBundle now) and fix the Secret name in docs. e2e: 5 Passed | 0 Failed, down from 160s to 47s — the suite was previously spending its time waiting on timeouts that can no longer happen. Co-Authored-By: Claude Opus 5 --- config/default/manager_webhook_patch.yaml | 42 ++++++++++++++--------- config/webhook/kustomization.yaml | 3 ++ config/webhook/secret.yaml | 18 ++++++++++ docs/deploy.md | 4 +-- hack/deploy.sh | 16 ++++----- pkg/cert/cert.go | 15 +++++--- test/e2e/e2e_test.go | 13 +++---- 7 files changed, 72 insertions(+), 39 deletions(-) create mode 100644 config/webhook/secret.yaml diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml index d49092b..cbabe0c 100644 --- a/config/default/manager_webhook_patch.yaml +++ b/config/default/manager_webhook_patch.yaml @@ -2,21 +2,27 @@ # TLS keypair from. # # The keypair is provisioned IN-PROCESS by the cert rotator (pkg/cert), not by -# cert-manager and not by an out-of-band script. That changes the volume this patch -# needs in two ways worth spelling out, because the kubebuilder default is wrong here: +# cert-manager and not by an out-of-band script. Two consequences worth spelling out, +# because they are easy to get backwards: # -# 1. It is an emptyDir, NOT a Secret projection. The rotator WRITES the keypair to -# this path, so a Secret volume — which is read-only, and whose kubelet refresh -# would fight the rotator — cannot work. The Secret still exists and is still the -# shared source of truth across replicas; the rotator reads it from the API and -# writes it here, rather than the kubelet projecting it. +# 1. The keypair reaches this directory as a SECRET PROJECTION, not an emptyDir. +# The rotator does not write files: it mints the keypair into the Secret and +# nothing else — cert-controller has no disk writes at all, and its CertDir is a +# path it only ever os.Stat()s to decide readiness (see ensureCertsMounted in +# rotator.go). So the kubelet is what puts the keypair on disk. With an emptyDir +# the files would never appear, the rotator's IsReady channel would never close, +# and — because controller and webhook registration waits on it (cmd/main.go) — +# NOTHING would ever start: no reconcilers, no virtual nodes, no webhook. The +# manager stays Running and looks healthy, which is what makes that failure mode +# nasty. This is also the second reason config/webhook ships the empty Secret: +# a non-optional Secret volume blocks the pod from starting until it exists. # # 2. There is no --webhook-cert-path argument. Passing it makes main build a # certwatcher over the path, and the watcher fails at STARTUP if the files are not -# there yet — which they never are on a first install, because the rotator cannot -# mint them until the manager is running. controller-runtime's webhook server -# already reads this exact directory by default and picks up rotations, so the -# watcher is redundant as well as harmful. +# there yet — which they are not on a first install, because the Secret is empty +# until the rotator mints into it. controller-runtime's webhook server already +# reads this exact directory by default, and it is only registered after the +# rotator reports ready, so by then the files are there. # Add the port configuration for the webhook server - op: add @@ -26,18 +32,22 @@ name: webhook-server protocol: TCP -# The directory the rotator writes the keypair to and the webhook server serves from. -# Must match certDir in pkg/cert (controller-runtime's default path). +# The directory the keypair is projected into and the webhook server serves from. +# Must match certDir in pkg/cert (controller-runtime's default path), which is also +# the path the rotator polls to decide readiness. - op: add path: /spec/template/spec/containers/0/volumeMounts/- value: mountPath: /tmp/k8s-webhook-server/serving-certs name: webhook-certs + readOnly: true -# emptyDir, because the rotator writes here (see above). The container's root -# filesystem is read-only, so without this volume the write would fail outright. +# The rotator's Secret, projected read-only (see above: the kubelet, not the rotator, +# writes these files). Name must match secretName in pkg/cert. NOT optional: the pod +# should refuse to start rather than run with a directory that never fills. - op: add path: /spec/template/spec/volumes/- value: name: webhook-certs - emptyDir: {} + secret: + secretName: nebula-webhook-server-cert diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml index 051676b..e74dbed 100644 --- a/config/webhook/kustomization.yaml +++ b/config/webhook/kustomization.yaml @@ -1,6 +1,9 @@ resources: - manifests.yaml - service.yaml +# The (empty) Secret the rotator populates. Required — the rotator Gets/Updates it +# but never Creates it, so without this the manager crash-loops. See secret.yaml. +- secret.yaml # Narrow the generated Pod webhook to opt-in Pods outside system namespaces. patches: diff --git a/config/webhook/secret.yaml b/config/webhook/secret.yaml new file mode 100644 index 0000000..35bf863 --- /dev/null +++ b/config/webhook/secret.yaml @@ -0,0 +1,18 @@ +# The Secret the webhook serving keypair lives in. +# +# It is created EMPTY on purpose, and that is not a placeholder — it is required. +# The cert rotator (pkg/cert) does a Get followed by an Update on this Secret and +# never a Create, so an absent Secret is a fatal startup error ("acquiring secret to +# update certificates: not found") that crash-loops the manager. An empty Secret is +# exactly what it expects: `secret.Data == nil` is the trigger for minting the CA and +# server certs and writing them back here. +# +# So the contents are owned entirely by the manager at runtime — never commit key +# material, and do not hand-populate it. It is also the shared source of truth across +# replicas: the first pod to run mints the keypair into this Secret, and later pods +# find it already valid and just write it to their own local disk. +apiVersion: v1 +kind: Secret +metadata: + name: webhook-server-cert + namespace: system diff --git a/docs/deploy.md b/docs/deploy.md index a40a3fb..2f27467 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -77,7 +77,7 @@ It does the three things that have to agree with each other: 1. **Serving cert** — mints a self-signed cert for the webhook Service DNS name (`nebula-webhook-service..svc`), stores it in the - `webhook-server-cert` Secret, and writes it to + `nebula-webhook-server-cert` Secret, and writes it to `/tmp/k8s-webhook-server/serving-certs` where the webhook server reads it. 2. **CA trust** — patches that cert's CA into the `MutatingWebhookConfiguration` `caBundle`, so the API server trusts the webhook. It is derived from the cert @@ -202,7 +202,7 @@ kubectl -n nebula-system logs deploy/nebula-controller-manager | grep -i provide kubectl get nodes -l nebula.inftyai.com/provider # Webhook TLS is wired: the caBundle matches the serving cert Secret. -diff <(kubectl get secret webhook-server-cert -n nebula-system -o jsonpath='{.data.tls\.crt}') \ +diff <(kubectl get secret nebula-webhook-server-cert -n nebula-system -o jsonpath='{.data.tls\.crt}') \ <(kubectl get mutatingwebhookconfiguration nebula-mutating-webhook-configuration \ -o jsonpath='{.webhooks[0].clientConfig.caBundle}') \ && echo "webhook caBundle matches serving cert" diff --git a/hack/deploy.sh b/hack/deploy.sh index 55adb42..3dada9f 100755 --- a/hack/deploy.sh +++ b/hack/deploy.sh @@ -151,13 +151,11 @@ else make docker-buildx IMG="${IMG}" fi -# --- 3. Secrets FIRST, so the manager mounts them on its very first boot ---- -# Ordering matters and lets us avoid any manager restart: -# - the webhook cert Secret is a REQUIRED volume mount, so it must exist -# before the pod starts; -# - provider credentials are read from the environment at process start. -# Both are consumed only at pod startup, so creating them before `make deploy` -# means the manager comes up already correct — no restart, no race. +# --- 3. Secrets FIRST, so the manager reads them on its very first boot ----- +# Provider credentials are read from the environment at process start, so creating +# them before `make deploy` means the manager comes up already correct — no restart, +# no race. (The webhook cert is NOT in this list: the manager mints it itself once +# running, see below.) # # Secrets need the namespace, which `make deploy` would create — so create it # up front (idempotent; kustomize re-applies it harmlessly during deploy). @@ -178,8 +176,8 @@ for row in "${PROVIDER_SECRETS[@]}"; do done # --- 4. install CRDs + deploy the manager ---------------------------------- -# The pod mounts the cert Secret and reads provider creds at boot — both already -# exist, so the manager comes up fully configured with no restart needed. +# The pod reads provider creds at boot and they already exist, so the manager comes +# up fully configured with no restart needed. log "installing CRDs and deploying the manager" make deploy IMG="${IMG}" diff --git a/pkg/cert/cert.go b/pkg/cert/cert.go index fde3f9d..91c86d4 100644 --- a/pkg/cert/cert.go +++ b/pkg/cert/cert.go @@ -52,10 +52,17 @@ const ( // config/webhook/service.yaml AFTER kustomize applies the nebula- namePrefix. serviceName = "nebula-webhook-service" - // secretName is the Secret the keypair is stored in. The manager also MOUNTS - // this Secret (config/default/manager_webhook_patch.yaml), which is why the - // name must match there too. - secretName = "webhook-server-cert" + // secretName is the Secret the keypair is stored in. Like the two names above it + // carries the nebula- prefix, because that is what config/default's namePrefix + // actually renders ("webhook-server-cert" here would look for a Secret no overlay + // creates). + // + // The Secret MUST already exist when the manager starts, which is why + // config/webhook ships it empty: the rotator Gets this Secret and then Updates it, + // but never Creates it, so an absent Secret is a fatal startup error that + // crash-loops the manager rather than something it recovers from. Empty is all it + // needs — nil Data is exactly the condition that triggers minting. + secretName = "nebula-webhook-server-cert" // certDir is where the rotator writes the keypair and where controller-runtime's // webhook server reads it from. It is the controller-runtime default, and the diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 2c15106..e26b402 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -99,13 +99,10 @@ var _ = Describe("Manager", Ordered, func() { _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") - // caBundle injection is server-side (only the API server reads it) and - // requires the MutatingWebhookConfiguration created by the deploy, so it - // runs after the deploy. The manager pod is untouched — no restart needed. - By("injecting the webhook CA bundle") - cmd = exec.Command("hack/gen-webhook-cert.sh", "cabundle") - _, err = utils.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to inject the webhook CA bundle") + // No caBundle injection step: the manager patches the + // MutatingWebhookConfiguration itself once its cert rotator runs (pkg/cert), + // from the same cert it just wrote. The "CA injection" spec below asserts that + // happened, so this is covered by an Eventually rather than a deploy step. }) // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs, @@ -305,7 +302,7 @@ var _ = Describe("Manager", Ordered, func() { // starts — nothing pre-creates it, so this asserts the rotator ran. By("validating that the webhook serving cert Secret exists") verifyCertSecret := func(g Gomega) { - cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace) + cmd := exec.Command("kubectl", "get", "secrets", "nebula-webhook-server-cert", "-n", namespace) _, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) } From ee10250b58683aeee2cc25ca4fc8402a599e54f7 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 13:32:23 +0100 Subject: [PATCH 19/22] docs: drop the superseded proposal and its mesh diagram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/proposal/architecture.md is the original 114-line design sketch, long superseded by the 616-line docs/architecture.md that describes what was actually built. tailnet.png is the headscale mesh diagram — the mesh does not reach the node counts Nebula targets and is no longer part of the design. Neither file is referenced from anywhere. Co-Authored-By: Claude Opus 5 --- docs/proposal/architecture.md | 114 ---------------------------------- docs/proposal/tailnet.png | Bin 299772 -> 0 bytes 2 files changed, 114 deletions(-) delete mode 100644 docs/proposal/architecture.md delete mode 100644 docs/proposal/tailnet.png diff --git a/docs/proposal/architecture.md b/docs/proposal/architecture.md deleted file mode 100644 index d9590da..0000000 --- a/docs/proposal/architecture.md +++ /dev/null @@ -1,114 +0,0 @@ -# Nebula Architecture - -This is the design for where Nebula is going: one Kubernetes API surface over every -GPU workload class: - -- sandboxes and agents, notebooks -- inference and services -- training and fine-tuning jobs - -All running on remote instances across NeoClouds and hyperscalers. - -Contents: - -- [Goals and non-goals](#goals-and-non-goals) -- [Workload classes](#workload-classes) -- [Alternatives](#alternatives) - - [1. Dial-out, not a mesh](#1-dial-out-not-a-mesh) -- [Roadmap](#roadmap) - ---- - -## Goals and non-goals - -**Goals** - -- Support AWS, Modal as starting providers, with more to come. -- `kubectl logs` / `exec` / `attach` against a remote instance, natively. - offer no native exec API and no inbound reachability. -- In-cluster clients can reach inference and services -- Per-provider code stays small: adding a provider must not mean re-implementing - logs, exec, or stats. - -**Non-goals (for now)** - -- Gang scheduling and intra-gang connectivity — multi-instance workloads that - provision all-or-nothing and can reach each other. Deferred, not abandoned: it is - what training and fine-tuning need. -- More providers than AWS and Modal. The architecture should be provider-agnostic, but - the implementation will be provider-specific. - -**Non-goals** - -- Multi-container Pods, init containers, and image-level restart on remote - instances. These need container-runtime access, which not every provider grants. -- An overlay network of any kind (see [Decision 1](#1-dial-out-not-a-mesh)). -- In-place migration of a running workload between providers. - -## Workload classes - -Three classes, three phases. - -| Class | CRD | Inbound to instance | Group | Lifetime | Phase | -|---|---|---|---|---|---| -| sandbox, agent, shell | `Sandbox` | no | 1 | minutes–days | 1 | -| inference, services | Deployment + Service | **yes**, from arbitrary clients | N fungible | long | 2 | -| training, fine-tuning, jobs | Batch Jobs | **yes**, from peers only | N ordinal, all-or-nothing | bounded | deferred | - ---- - -## Implementation Details - -### Authentication - -### SandD - -### Virtual Kubelet - -## Alternatives - -### 1. Dial-out, not a mesh - -**Decision.** The in-container SandD opens **one long-lived WSS connection per -instance** to a stable public endpoint, authenticated by a short-lived JWT whose -audience is the workload identity. No overlay network, for any class. - -``` - instance (any cloud, behind NAT) your cluster - ┌─────────────────────────────┐ - │ container │ - │ SandD (PID 1) ──────────────┼──outbound TLS 443──▶ LB ──SNI──▶ WS server - │ └─ workload as child │ map[claim]conn - └─────────────────────────────┘ -``` - -**Why the connection must be persistent.** The SandD daemon should already have an established connection when a request arrives, ensuring synchronous operations like `kubectl exec` work seamlessly. - -Cost: one idle TLS connection per instance, carrying zero traffic until someone -runs a command. 10k of them is an ordinary WebSocket server in a single process. - -**Why not a mesh (headscale/Tailscale).** A mesh solves the same reachability -problem and needs less development work since coordinator already exists, but headscale has a performance issue when reaching around 500 nodes, see [headscale#1656](https://github.com/juanfont/headscale/issues/1656). - -![tailnet](./tailnet.png) - -**Revisit if** dial-in becomes a product feature: operator SSH into any box, a -pull-based metrics scraper, or instance-to-instance traffic *across* providers. - ---- - -## Roadmap - -Each phase builds the seam the next one needs. Nothing is built twice. - -### Phase 1 — Sandbox, agent, shell - -*Goal: `kubectl exec`/`logs` work against a remote box, driven by a CRD.* Support AWS as the first provider. - -### Phase 2 — Inference and services - -*Goal: `my-llm.default.svc` resolves to an off-cluster instance.* Support Modal as the first provider. - -### Phase 3 — Training, fine-tuning, jobs - -*Goal: multi-instance, all-or-nothing, mutually reachable.* diff --git a/docs/proposal/tailnet.png b/docs/proposal/tailnet.png deleted file mode 100644 index cbdbcdce3d43b1054a59cc9450fd85f2bf104793..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 299772 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

M*z~i|e@PCh=9dE)>z-sRg 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

3k7 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 Mq>@D0bK|%FAINwNhyVZp From 845ab814441737aeeb6fc4e92ce5af325112ac66 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 13:32:43 +0100 Subject: [PATCH 20/22] docs: drop the stale build-status table The table claimed the optimizer was the next milestone and listed components as DONE/PLANNED, which has to be hand-updated on every change and was already wrong (Sandbox/SandboxSet are not in it at all). Git history and the doc body already say what exists; a second, manually-maintained source of that fact only drifts. Co-Authored-By: Claude Opus 5 --- docs/architecture.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 463c4b4..fddafac 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -614,25 +614,3 @@ manager owns reconciliation and the VK node leases (the VK nodes run inside the manager as `Runnable`s — option A). --- - -## Build status - -| Component | Package | Status | -|---|---|---| -| API types (NodePool, NodeClaim) | `api/v1alpha1` | DONE | -| Provider interface + registry | `pkg/provider` | DONE | -| Price catalog (CSV + ConfigMap) | `pkg/provider/catalog` | DONE | -| Modal adapter | `pkg/provider/modal` | DONE | -| NodeClaim controller (teardown backstop) | `internal/controller` | DONE | -| NodePool controller | `internal/controller` | DONE | -| Scheduling-gate webhook | `internal/webhook/v1` | DONE | -| Provider wiring in manager | `cmd/main.go` | DONE | -| Virtual Kubelet node (VK owns provisioning) | `pkg/vnode` | DONE | -| Placement controller (first-matching-provider) | `internal/controller` | DONE | -| **Optimizer (price/weighted, capacity fallback, blocklist)** | — | **PLANNED** | -| Other adapters (RunPod next) | `pkg/provider/*` | PLANNED | - -The critical path — gated Pod → placed → bound → provisioned → torn down — is now -closed end to end. The next milestone is the **optimizer**: replacing the v1 -"first matching provider" policy behind `selectPlacement` with price/weighted -ranking, capacity-tier fallback, and the failover blocklist. From faa80efc544dfb2457ab69b3fa03a7c3a3160d73 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 13:32:54 +0100 Subject: [PATCH 21/22] config: drop ttl from the samples, expand the patch-target comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The samples no longer set spec.ttl, so a copied sample creates an unbounded box rather than one that expires after 8h. (ttl remains a valid optional field on both types — this only changes what the samples demonstrate.) Also spell out WHY the patch targets name the manager: without a name kustomize applies the patch to every Deployment in the build, so a second one added later would silently inherit the manager-only --metrics-bind-address flag (and the webhook port and cert mount) and fail to start. Co-Authored-By: Claude Opus 5 --- config/default/kustomization.yaml | 11 +++++++---- config/samples/nebula_v1alpha1_sandbox.yaml | 4 ---- config/samples/nebula_v1alpha1_sandboxset.yaml | 3 --- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index ef51a10..f169c70 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -49,9 +49,11 @@ patches: - path: manager_metrics_patch.yaml target: kind: Deployment - # Scope to the manager ONLY (matches the pre-prefix name), so the - # manager-only --metrics-bind-address flag is not injected into any other - # Deployment in the build. + # Scope to the manager ONLY (matches the pre-prefix name). The manager is + # currently the only Deployment in the build, so this is defensive rather than + # load-bearing: without a name, kustomize applies the patch to EVERY Deployment, + # so the day an overlay adds a second one it would silently receive the + # manager-only --metrics-bind-address flag and fail to start on an unknown flag. name: controller-manager # Uncomment the patches line if you enable Metrics and CertManager @@ -66,7 +68,8 @@ patches: - path: manager_webhook_patch.yaml target: kind: Deployment - # Scope to the manager ONLY (see the metrics patch above). + # Scope to the manager ONLY (see the metrics patch above) — otherwise a second + # Deployment added later would also get the webhook port, volume and cert mount. name: controller-manager # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. diff --git a/config/samples/nebula_v1alpha1_sandbox.yaml b/config/samples/nebula_v1alpha1_sandbox.yaml index 74b530d..b8b3436 100644 --- a/config/samples/nebula_v1alpha1_sandbox.yaml +++ b/config/samples/nebula_v1alpha1_sandbox.yaml @@ -31,7 +31,3 @@ spec: memory: 64Gi limits: nvidia.com/gpu: "1" - # Measured from the moment the box becomes READY, not from creation, so a slow - # provision does not eat into the user's time. Omit for an unbounded box — - # a deliberate choice, since an abandoned GPU box bills until someone notices. - ttl: 8h diff --git a/config/samples/nebula_v1alpha1_sandboxset.yaml b/config/samples/nebula_v1alpha1_sandboxset.yaml index 3254929..481ca4f 100644 --- a/config/samples/nebula_v1alpha1_sandboxset.yaml +++ b/config/samples/nebula_v1alpha1_sandboxset.yaml @@ -34,6 +34,3 @@ spec: memory: 64Gi limits: nvidia.com/gpu: "1" - # Per box, from its own ready time. In a set this acts as a RECYCLE interval: - # an expired box is pruned and the set creates a fresh one to replace it. - ttl: 8h From 31802d21b003ca34c8920c29fa232c5f0cbe11a2 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 8 Aug 2026 15:21:16 +0100 Subject: [PATCH 22/22] docs: correct the "rotator writes the keypair to disk" claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 65ee48d fixed the volume (emptyDir → Secret projection) but left comments in three other places still saying the rotator writes the keypair to certDir. It does not: cert-controller performs no filesystem writes at all, and CertDir is a path it only os.Stat()s to decide readiness (ensureCertsMounted). The kubelet puts the files there by projecting the Secret. This mattered in review: a reader going by these comments concludes the read-only Secret mount must be a bug and that an emptyDir is the fix — which is exactly the configuration that hangs the manager (files never appear, IsReady never closes, no controller or webhook ever registers, pod still reports Running). Verified on Kind: emptyDir → 0 virtual nodes, Secret projection → 3. Co-Authored-By: Claude Opus 5 --- docs/deploy.md | 12 +++++++----- hack/deploy.sh | 12 +++++++----- pkg/cert/cert.go | 27 ++++++++++++++++++++------- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/docs/deploy.md b/docs/deploy.md index 2f27467..c25de13 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -76,9 +76,11 @@ nothing to install and nothing to run before `make deploy`. It does the three things that have to agree with each other: 1. **Serving cert** — mints a self-signed cert for the webhook Service DNS name - (`nebula-webhook-service..svc`), stores it in the - `nebula-webhook-server-cert` Secret, and writes it to - `/tmp/k8s-webhook-server/serving-certs` where the webhook server reads it. + (`nebula-webhook-service..svc`) and stores it in the + `nebula-webhook-server-cert` Secret. That Secret is projected into the manager at + `/tmp/k8s-webhook-server/serving-certs`, where the webhook server reads it — the + rotator writes only the Secret, never the filesystem, so the volume at that path + must be the Secret rather than an `emptyDir`. 2. **CA trust** — patches that cert's CA into the `MutatingWebhookConfiguration` `caBundle`, so the API server trusts the webhook. It is derived from the cert just written, so the served cert and the trusted CA cannot drift. @@ -87,8 +89,8 @@ It does the three things that have to agree with each other: later, when nobody remembers a script was involved. The Secret is the shared source of truth across replicas — a second replica finds -the existing cert there and writes it to its own disk rather than minting a -competing one. Rotation is *not* leader-elected, because webhook serving is not +the existing cert there rather than minting a competing one, and the kubelet projects +it into that pod too. Rotation is *not* leader-elected, because webhook serving is not either: every replica needs the keypair on its own local disk, and the API server will call a non-leader. diff --git a/hack/deploy.sh b/hack/deploy.sh index 3dada9f..8024978 100755 --- a/hack/deploy.sh +++ b/hack/deploy.sh @@ -163,11 +163,13 @@ log "ensuring namespace ${NAMESPACE}" "${KUBECTL}" create namespace "${NAMESPACE}" --dry-run=client -o yaml | "${KUBECTL}" apply -f - # No webhook cert step here, deliberately. The manager provisions its own serving -# cert in-process at startup (pkg/cert) — it mints the keypair, stores it in a Secret, -# writes it to disk and patches the caBundle into the MutatingWebhookConfiguration, -# then keeps RENEWING it before expiry. That last part is why it replaced the previous -# hack/gen-webhook-cert.sh: a script-minted cert never rotates, so its expiry is a -# time bomb that fires years later when nobody remembers the script exists. +# cert in-process at startup (pkg/cert) — it mints the keypair into a Secret (which +# kustomize ships empty, and which the kubelet then projects into the pod for the +# webhook server to serve) and patches the caBundle into the +# MutatingWebhookConfiguration, then keeps RENEWING it before expiry. That last part is +# why it replaced the previous hack/gen-webhook-cert.sh: a script-minted cert never +# rotates, so its expiry is a time bomb that fires years later when nobody remembers +# the script exists. # Provider credential Secrets, one per provider (blank required keys → skipped). for row in "${PROVIDER_SECRETS[@]}"; do diff --git a/pkg/cert/cert.go b/pkg/cert/cert.go index 91c86d4..eeaa4fb 100644 --- a/pkg/cert/cert.go +++ b/pkg/cert/cert.go @@ -32,10 +32,15 @@ limitations under the License. // expiry is a silent time bomb that fires years later, when nobody remembers // the script exists. // -// The rotator does both jobs in-process: it mints the keypair into a Secret, writes -// it to CertDir for the webhook server to serve, patches the caBundle, and then -// keeps renewing before expiry. Because the CA is patched from the same cert that -// was just written, the served cert and the trusted CA cannot drift. +// The rotator does both jobs in-process: it mints the keypair into a Secret, patches +// the caBundle, and then keeps renewing before expiry. Because the CA is patched from +// the same cert that was just written, the served cert and the trusted CA cannot drift. +// +// The Secret is the ONLY thing it writes. It never touches the filesystem — the +// keypair reaches the webhook server's certDir because the manager projects that +// Secret there as a volume, and the rotator merely polls the path to know when the +// kubelet has done so. See certDir below; getting this backwards (an emptyDir at +// certDir) silently prevents the whole manager from starting. package cert import ( @@ -64,9 +69,17 @@ const ( // needs — nil Data is exactly the condition that triggers minting. secretName = "nebula-webhook-server-cert" - // certDir is where the rotator writes the keypair and where controller-runtime's - // webhook server reads it from. It is the controller-runtime default, and the - // same path the manager mounts the Secret at. + // certDir is where the keypair lands on disk and where controller-runtime's + // webhook server reads it from. It is the controller-runtime default, and the path + // the manager projects the Secret above at. + // + // The rotator does NOT write here, despite the name: cert-controller performs no + // filesystem writes at all, and CertDir is a path it only os.Stat()s to decide + // readiness (ensureCertsMounted). The KUBELET puts the files here by projecting the + // Secret — so the volume must be that Secret, not an emptyDir. With an emptyDir the + // files never appear, IsReady never closes, and since controller and webhook + // registration waits on it (see CertsManager), nothing ever starts while the manager + // still reports Running. certDir = "/tmp/k8s-webhook-server/serving-certs" // mutatingWebhookConfName is the MutatingWebhookConfiguration whose caBundle

M*z~i|e@PCh=9dE)>z-sRg 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