feat(kubernetes): create SandboxClaims for matching warm pools - #2460
feat(kubernetes): create SandboxClaims for matching warm pools#2460grs wants to merge 15 commits into
Conversation
cdfe4be to
45f2384
Compare
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
…eation Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
rhuss
left a comment
There was a problem hiding this comment.
Review
Well-engineered PR with sound security design. No critical issues. The registration registry, activation flow, and claim lifecycle are all thoughtfully designed. The K8s-specific code refactor out of the gateway (~856 lines removed from k8s_sa.rs) is a major improvement.
What's done well:
- Auth trust chain is sound.
SupervisorBootstrapprincipal locked to a single RPC, explicitly rejected everywhere else. - Session-ID concurrency scheme in the registration registry is clean and well-tested.
- Claim creation is idempotent with deterministic naming and preconditioned deletes.
- Level-triggered activation with periodic relist, concurrency caps, and dedup by claim UID.
- Clean abstraction boundary: no K8s types leak through to the gateway.
- Bootstrap security: pod UID cross-checked against SA token extras, ownerReference chain validated, multi-version CRD support.
- Fail-safe behavior throughout: cache not-ready skips allocation, ambiguous matches skip allocation, GC failure creates safe ambiguity.
Inline comments below cover the specific findings.
| // The initial trial activates already-bound cold pods immediately. Later | ||
| // warm-pool stages keep this stream pending until a SandboxClaim adopts the | ||
| // registered pod. | ||
| rpc RegisterSupervisorPod(RegisterSupervisorPodRequest) |
There was a problem hiding this comment.
naming: RegisterSupervisorPod and PodActivationMessage sit in the driver-agnostic gateway proto. "Pod" leaks K8s terminology into the public API. The internal abstractions already use driver-neutral naming (instance_id, instance_name in SupervisorBootstrapIdentity). Consider renaming to RegisterSupervisor / SupervisorActivationMessage to match the existing ConnectSupervisor pattern. Docker warm containers or pre-booted VMs are plausible future extensions.
| # v1beta1 SandboxClaim resources when a compatible OpenShell-enabled | ||
| # SandboxWarmPool exists in the target namespace. | ||
| warmPooling: | ||
| enabled: true |
There was a problem hiding this comment.
defaults: Both warmPooling.enabled and profiles.enabled default to true. A fresh install on a cluster without Agent Sandbox CRDs will start watching extension CRDs that may not exist, causing noisy error logs. Consider defaulting to false (opt-in), or at minimum profiles.enabled: false while keeping warm-pool consumption enabled.
| - apiGroups: | ||
| - extensions.agents.x-k8s.io | ||
| resources: | ||
| - sandboxclaims |
There was a problem hiding this comment.
RBAC: create/delete on sandboxclaims is granted unconditionally, even when warmPooling.enabled: false. Contrast with sandboxtemplates/sandboxwarmpools below which correctly gate write verbs on profiles.enabled. Consider gating claim write verbs on warmPooling.enabled.
| - apiGroups: | ||
| - extensions.agents.x-k8s.io | ||
| resources: | ||
| - sandboxtemplates |
There was a problem hiding this comment.
RBAC scope: When profiles are enabled, create/patch/update/delete on sandboxtemplates and sandboxwarmpools applies cluster-wide. The profile reconciler only operates in one namespace. A namespaced Role would be more minimal. If the cluster-wide scope is intentional (warm pools spanning namespaces), a comment explaining why would help.
| ) -> Option<Arc<SandboxClaimActivationController>> { | ||
| std::env::var_os("KUBERNETES_SERVICE_HOST")?; | ||
|
|
||
| match kube::Client::try_default().await { |
There was a problem hiding this comment.
duplicate clients: Both kubernetes_supervisor_bootstrap_identity_provider (line 256) and kubernetes_sandbox_claim_activation_controller (here) call kube::Client::try_default() independently, creating two HTTP connection pools to the apiserver. Consider creating one client and sharing it.
| self.sandbox_index.remove_sandbox(sandbox.object_id()); | ||
| Err(Status::failed_precondition(status.message().to_string())) | ||
| } | ||
| Err(status) if status.code() == Code::Unavailable => { |
There was a problem hiding this comment.
orphaned records: When the driver returns Unavailable, the gateway preserves the Provisioning record. If the backend create actually failed, this could leak sandbox names in the index. Is there a staleness reconciler for provisioning records, or do they rely on the sandbox watcher to clean up?
| } | ||
| } | ||
|
|
||
| Err(Status::aborted( |
There was a problem hiding this comment.
observability: After exhausting all 3 retry attempts, this returns Status::aborted but doesn't log. A warn! here would provide operational visibility into rapid registration churn.
| activator: Arc<dyn SupervisorBootstrapActivator>, | ||
| claim: DynamicObject, | ||
| ) { | ||
| if tasks.len() >= ACTIVATION_MAX_CONCURRENCY { |
There was a problem hiding this comment.
observability: When the JoinSet is at ACTIVATION_MAX_CONCURRENCY (32), the claim event is silently dropped, relying on the 15-second resync to retry. A debug! log here would help operators diagnose delayed activations under load.
| data: | ||
| warm-pool.toml: | | ||
| version = 1 | ||
| workspace = "openshell" |
There was a problem hiding this comment.
consistency: All three example ConfigMaps use workspace = "openshell", but E2E tests and docs reference use workspace = "default". Users copying these examples verbatim will create warm pools that don't match sandbox creates in the default workspace. Either align the examples with the default workspace name, or add a comment explaining the workspace must match the operational workspace.
Summary
Add transparent Kubernetes warm-pool allocation by matching sandbox create requests to OpenShell-enabled
SandboxWarmPooltemplates and creatingSandboxClaimresources when a compatible pool exists. The PRalso moves Kubernetes supervisor pod bootstrap and claim activation into the Kubernetes driver path so warm pods can register before assignment and be activated once a claim selects them.
Related Issue
Closes #2157
Changes
IssueSandboxTokento the streamingRegisterSupervisorPodRPC so warm pods can establish a gateway-held registration before they are assigned to a sandbox. This means supervisor path is the same for warm or cold initialisation.SandboxWarmPoolandSandboxTemplatefingerprints.SandboxClaimresources instead of directSandboxresources when exactly one compatible warm pool exists.SandboxClaimactivation handling that validates the selectedSandboxand pod before issuing a sandbox token.SandboxWarmPooland associatedSandboxTemplatein response to presence of labeledConfigMapwith toml describing the sandbox parameters it should match. Included some exampleConfigMaps.Examples
The easiest way to try it out is to use one (or more) of the examples under
examples/kubernetes-warm-pool-config/:default-configmap.yamlwill create a warm pool that will be matched by sandbox create requests with no explicit arguments,env-foo-configmap.yamlby requests with--env FOO=barandcpu-0-2-configmap.yamlby requests with--cpu 0.2. Obviously you can also modify these for other settings. The examples set the pool size to 1; if you want to test rapid creation of multiple sandboxes you will need to increase that.Testing
mise run pre-commitpassesChecklist