Skip to content

Commit 2d129ac

Browse files
committed
docs(kubernetes): clarify Agent Sandbox extension CRD prerequisite
Signed-off-by: Gordon Sim <gsim@redhat.com>
1 parent e5bc002 commit 2d129ac

7 files changed

Lines changed: 185 additions & 43 deletions

File tree

‎.agents/skills/debug-openshell-cluster/SKILL.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ openshell logs <sandbox-name>
572572
| Kubernetes gateway pod pending | PVC unbound, taint, selector, or insufficient resources | `kubectl -n openshell describe pod <pod>` |
573573
| Kubernetes sandbox pod stuck pending, workspace PVC unbound | Cluster has no default `StorageClass` and OpenShell does not set `storageClassName` on the workspace PVC (clusters with a default `StorageClass` bind fine without it) | `kubectl -n openshell describe pvc`; set `server.workspaceStorageClass` (gateway config `workspace_storage_class`) to a valid `StorageClass` |
574574
| Kubernetes gateway pod crash loops | Missing secret, bad DB URL, bad TLS config | `kubectl -n openshell logs deployment/openshell -c openshell-gateway` or `kubectl -n openshell logs statefulset/openshell -c openshell-gateway` |
575+
| Gateway logs `Agent Sandbox extension APIs are unavailable` and Kubernetes creates cold sandboxes | Agent Sandbox was installed with `manifest.yaml` only while OpenShell warm pooling defaults are enabled | Apply Agent Sandbox `extensions.yaml`, or set `server.warmPooling.enabled=false`; then restart the gateway so the driver revalidates `SandboxClaim`, `SandboxTemplate`, and `SandboxWarmPool` APIs |
575576
| CLI TLS error | Local mTLS bundle does not match server cert/CA | Check `~/.config/openshell/gateways/<name>/mtls/` |
576577
| Edge or OIDC gateway returns `Unauthenticated` | Stored login expired, audience/scopes mismatch, or gateway auth configuration changed | `openshell gateway info`, `openshell gateway login <name>`, gateway auth logs |
577578
| Gateway fails before serving health after enabling an interceptor | Interceptor endpoint unavailable or manifest/binding validation failed | Gateway and interceptor logs; interceptor socket; `binding_policy`, phases, and failure policy |

‎crates/openshell-driver-kubernetes/src/driver.rs‎

Lines changed: 152 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ pub struct KubernetesComputeDriver {
618618
watch_client: Client,
619619
sandbox_api_version: Arc<OnceCell<&'static str>>,
620620
warm_pool_cache: WarmPoolCache,
621+
warm_pool_extension_apis_available: bool,
621622
config: KubernetesComputeConfig,
622623
operator_allowlist: Option<OperatorNamespaceAllowlist>,
623624
}
@@ -699,15 +700,42 @@ impl KubernetesComputeDriver {
699700
watch_client,
700701
sandbox_api_version: Arc::new(OnceCell::new()),
701702
warm_pool_cache: WarmPoolCache::default(),
703+
warm_pool_extension_apis_available: false,
702704
config,
703705
operator_allowlist,
704706
};
707+
let warm_pool_extension_apis_available = if driver.config.warm_pooling.enabled {
708+
match driver.validate_warm_pool_extension_apis().await {
709+
Ok(true) => true,
710+
Ok(false) => {
711+
warn!(
712+
namespace = %driver.config.namespace,
713+
"Agent Sandbox extension APIs are unavailable; Kubernetes warm pooling and template reconciliation are disabled. Install Agent Sandbox extensions.yaml or disable warm_pooling."
714+
);
715+
false
716+
}
717+
Err(err) => {
718+
warn!(
719+
namespace = %driver.config.namespace,
720+
error = %err,
721+
"Could not validate Agent Sandbox extension APIs; Kubernetes warm pooling and template reconciliation are disabled"
722+
);
723+
false
724+
}
725+
}
726+
} else {
727+
false
728+
};
729+
let driver = Self {
730+
warm_pool_extension_apis_available,
731+
..driver
732+
};
705733

706734
if driver.workspace_mode() == WorkspaceMode::Shared {
707735
driver.backfill_gateway_id_labels().await?;
708736
}
709737

710-
if driver.config.warm_pooling.enabled {
738+
if driver.config.warm_pooling.enabled && driver.warm_pool_extension_apis_available {
711739
driver.spawn_warm_pool_cache_controller();
712740
}
713741
Ok(driver)
@@ -719,7 +747,9 @@ impl KubernetesComputeDriver {
719747
"kubernetes",
720748
openshell_core::VERSION,
721749
&self.config.default_image,
722-
self.config.warm_pooling.enabled && self.config.warm_pooling.templates.enabled,
750+
self.config.warm_pooling.enabled
751+
&& self.config.warm_pooling.templates.enabled
752+
&& self.warm_pool_extension_apis_available,
723753
),
724754
)
725755
}
@@ -1175,6 +1205,15 @@ impl KubernetesComputeDriver {
11751205
&self,
11761206
template: &DriverSandboxTemplateResource,
11771207
) -> Result<(), KubernetesDriverError> {
1208+
if !self.warm_pool_extension_apis_available {
1209+
debug!(
1210+
template_id = %template.id,
1211+
template_name = %template.name,
1212+
workspace = %template.workspace,
1213+
"Skipping sandbox template warm-pool reconciliation because Agent Sandbox extension APIs are unavailable"
1214+
);
1215+
return Ok(());
1216+
}
11781217
let maybe_rendered = render_warm_pool_template(self.client.clone(), &self.config, template)
11791218
.await
11801219
.map_err(KubernetesDriverError::InvalidArgument)?;
@@ -1211,6 +1250,14 @@ impl KubernetesComputeDriver {
12111250
template_id: &str,
12121251
workspace: &str,
12131252
) -> Result<(), KubernetesDriverError> {
1253+
if !self.warm_pool_extension_apis_available {
1254+
debug!(
1255+
template_id,
1256+
workspace,
1257+
"Skipping sandbox template warm-pool cleanup because Agent Sandbox extension APIs are unavailable"
1258+
);
1259+
return Ok(());
1260+
}
12141261
garbage_collect_warm_pool_template(
12151262
self.client.clone(),
12161263
&self.config,
@@ -1316,6 +1363,68 @@ impl KubernetesComputeDriver {
13161363
Self::namespaced_extension_api(client, namespace, SANDBOX_TEMPLATE_KIND)
13171364
}
13181365

1366+
async fn validate_warm_pool_extension_apis(&self) -> Result<bool, String> {
1367+
for kind in [
1368+
SANDBOX_CLAIM_KIND,
1369+
SANDBOX_TEMPLATE_KIND,
1370+
SANDBOX_WARM_POOL_KIND,
1371+
] {
1372+
if !Self::extension_api_available(self.client.clone(), &self.config.namespace, kind)
1373+
.await?
1374+
{
1375+
debug!(
1376+
namespace = %self.config.namespace,
1377+
kind,
1378+
api_group = EXTENSIONS_GROUP,
1379+
api_version = EXTENSIONS_VERSION_V1BETA1,
1380+
"Agent Sandbox extension API is unavailable"
1381+
);
1382+
return Ok(false);
1383+
}
1384+
}
1385+
Ok(true)
1386+
}
1387+
1388+
async fn extension_api_available(
1389+
client: Client,
1390+
fallback_namespace: &str,
1391+
kind: &str,
1392+
) -> Result<bool, String> {
1393+
let all_api = Self::all_extension_api(client.clone(), kind);
1394+
match tokio::time::timeout(
1395+
KUBE_API_TIMEOUT,
1396+
all_api.api.list(&ListParams::default().limit(1)),
1397+
)
1398+
.await
1399+
{
1400+
Ok(Ok(_)) => Ok(true),
1401+
Ok(Err(err)) if extension_api_unavailable(&err) => Ok(false),
1402+
Ok(Err(KubeError::Api(err))) if err.code == 403 => {
1403+
let namespaced_api =
1404+
Self::namespaced_extension_api(client, fallback_namespace, kind);
1405+
match tokio::time::timeout(
1406+
KUBE_API_TIMEOUT,
1407+
namespaced_api.api.list(&ListParams::default().limit(1)),
1408+
)
1409+
.await
1410+
{
1411+
Ok(Ok(_)) => Ok(true),
1412+
Ok(Err(err)) if extension_api_unavailable(&err) => Ok(false),
1413+
Ok(Err(err)) => Err(err.to_string()),
1414+
Err(_) => Err(format!(
1415+
"timed out after {}s waiting for Kubernetes API",
1416+
KUBE_API_TIMEOUT.as_secs()
1417+
)),
1418+
}
1419+
}
1420+
Ok(Err(err)) => Err(err.to_string()),
1421+
Err(_) => Err(format!(
1422+
"timed out after {}s waiting for Kubernetes API",
1423+
KUBE_API_TIMEOUT.as_secs()
1424+
)),
1425+
}
1426+
}
1427+
13191428
fn spawn_warm_pool_cache_controller(&self) {
13201429
let cache = self.warm_pool_cache.clone();
13211430
let client = self.client.clone();
@@ -1833,7 +1942,7 @@ impl KubernetesComputeDriver {
18331942
sandbox_template: &DriverSandboxTemplateRef,
18341943
request_fingerprint: &str,
18351944
) -> Result<bool, KubernetesDriverError> {
1836-
if !self.config.warm_pooling.enabled {
1945+
if !self.config.warm_pooling.enabled || !self.warm_pool_extension_apis_available {
18371946
return Ok(false);
18381947
}
18391948

@@ -2183,6 +2292,7 @@ impl KubernetesComputeDriver {
21832292
let data = sandbox_to_k8s_spec(sandbox.spec.as_ref(), &params)
21842293
.map_err(KubernetesDriverError::InvalidArgument)?;
21852294
if self.config.warm_pooling.enabled
2295+
&& self.warm_pool_extension_apis_available
21862296
&& let Some(sandbox_template) = sandbox_template
21872297
{
21882298
match sandbox_spec_fingerprint(&data) {
@@ -2572,47 +2682,48 @@ impl KubernetesComputeDriver {
25722682
let mut sandbox_stream = watcher::watcher(agent_sandbox_api.api, watcher_config).boxed();
25732683
let claim_watcher_config = watcher::Config::default()
25742684
.labels(&format!("{LABEL_MANAGED_BY}={LABEL_MANAGED_BY_VALUE}"));
2575-
let claim_api = if self.config.warm_pooling.enabled {
2576-
let all_api = Self::sandbox_claim_api_all(self.watch_client.clone());
2577-
match tokio::time::timeout(
2578-
KUBE_API_TIMEOUT,
2579-
all_api.api.list(&ListParams::default().limit(1)),
2580-
)
2581-
.await
2582-
{
2583-
Ok(Ok(_)) => Some(all_api.api),
2584-
Ok(Err(KubeError::Api(err))) if err.code == 403 => {
2585-
let namespaced_api =
2586-
Self::sandbox_claim_api(self.watch_client.clone(), &namespace);
2587-
match tokio::time::timeout(
2588-
KUBE_API_TIMEOUT,
2589-
namespaced_api.api.list(&ListParams::default().limit(1)),
2590-
)
2591-
.await
2592-
{
2593-
Ok(Ok(_)) => Some(namespaced_api.api),
2594-
Ok(Err(err)) if extension_api_unavailable(&err) => None,
2595-
Ok(Err(err)) => return Err(err.to_string()),
2596-
Err(_) => {
2597-
return Err(format!(
2598-
"timed out after {}s waiting for Kubernetes API",
2599-
KUBE_API_TIMEOUT.as_secs()
2600-
));
2685+
let claim_api =
2686+
if self.config.warm_pooling.enabled && self.warm_pool_extension_apis_available {
2687+
let all_api = Self::sandbox_claim_api_all(self.watch_client.clone());
2688+
match tokio::time::timeout(
2689+
KUBE_API_TIMEOUT,
2690+
all_api.api.list(&ListParams::default().limit(1)),
2691+
)
2692+
.await
2693+
{
2694+
Ok(Ok(_)) => Some(all_api.api),
2695+
Ok(Err(KubeError::Api(err))) if err.code == 403 => {
2696+
let namespaced_api =
2697+
Self::sandbox_claim_api(self.watch_client.clone(), &namespace);
2698+
match tokio::time::timeout(
2699+
KUBE_API_TIMEOUT,
2700+
namespaced_api.api.list(&ListParams::default().limit(1)),
2701+
)
2702+
.await
2703+
{
2704+
Ok(Ok(_)) => Some(namespaced_api.api),
2705+
Ok(Err(err)) if extension_api_unavailable(&err) => None,
2706+
Ok(Err(err)) => return Err(err.to_string()),
2707+
Err(_) => {
2708+
return Err(format!(
2709+
"timed out after {}s waiting for Kubernetes API",
2710+
KUBE_API_TIMEOUT.as_secs()
2711+
));
2712+
}
26012713
}
26022714
}
2715+
Ok(Err(err)) if extension_api_unavailable(&err) => None,
2716+
Ok(Err(err)) => return Err(err.to_string()),
2717+
Err(_) => {
2718+
return Err(format!(
2719+
"timed out after {}s waiting for Kubernetes API",
2720+
KUBE_API_TIMEOUT.as_secs()
2721+
));
2722+
}
26032723
}
2604-
Ok(Err(err)) if extension_api_unavailable(&err) => None,
2605-
Ok(Err(err)) => return Err(err.to_string()),
2606-
Err(_) => {
2607-
return Err(format!(
2608-
"timed out after {}s waiting for Kubernetes API",
2609-
KUBE_API_TIMEOUT.as_secs()
2610-
));
2611-
}
2612-
}
2613-
} else {
2614-
None
2615-
};
2724+
} else {
2725+
None
2726+
};
26162727
let mut claim_stream = claim_api.map_or_else(
26172728
|| stream::pending().boxed(),
26182729
|api| watcher::watcher(api, claim_watcher_config).boxed(),

‎deploy/helm/openshell/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ The Kubernetes Agent Sandbox CRDs and controller must be installed on the cluste
1717
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml
1818
```
1919

20+
The chart defaults Kubernetes warm pooling on. Install the Agent Sandbox extension APIs too, or set `server.warmPooling.enabled=false`:
21+
22+
```shell
23+
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/extensions.yaml
24+
```
25+
2026
## Install on Kubernetes
2127

2228
```shell

‎deploy/helm/openshell/README.md.gotmpl‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ The Kubernetes Agent Sandbox CRDs and controller must be installed on the cluste
1717
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml
1818
```
1919

20+
The chart defaults Kubernetes warm pooling on. Install the Agent Sandbox extension APIs too, or set `server.warmPooling.enabled=false`:
21+
22+
```shell
23+
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/extensions.yaml
24+
```
25+
2026
## Install on Kubernetes
2127

2228
```shell

‎docs/kubernetes/setup.mdx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Make sure the following are in place before you install.
2222
|---|---|---|
2323
| Kubernetes 1.29+ with RBAC enabled | Yes | No additional notes. |
2424
| Helm 3.x | Yes | No additional notes. |
25-
| Agent Sandbox controller and CRDs | Yes | Install before the OpenShell chart. Refer to [Install Agent Sandbox](#install-agent-sandbox). |
25+
| Agent Sandbox controller and CRDs | Yes | Install before the OpenShell chart. Refer to [Install Agent Sandbox](#install-agent-sandbox). Install `extensions.yaml` too when warm pooling is enabled. |
2626
| cert-manager | No | Refer to [Managing Certificates](/kubernetes/managing-certificates). Use cert-manager only if you prefer it over the built-in PKI job. |
2727
| Kubernetes Gateway API | No | Refer to [Ingress](/kubernetes/ingress). Use it only for external access without port-forwarding. |
2828

@@ -38,6 +38,14 @@ kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/lates
3838

3939
This creates the `agent-sandbox-system` namespace, installs the `sandboxes.agents.x-k8s.io` CRD, and starts the controller.
4040

41+
Install the Agent Sandbox extension APIs when Kubernetes warm pooling is enabled. The OpenShell chart defaults warm pooling on, but it does not install the `SandboxClaim`, `SandboxTemplate`, or `SandboxWarmPool` CRDs:
42+
43+
```shell
44+
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/extensions.yaml
45+
```
46+
47+
If you do not install `extensions.yaml`, set `server.warmPooling.enabled=false`. Otherwise OpenShell disables warm pooling and template reconciliation at startup and falls back to direct `Sandbox` creation.
48+
4149
<Note>
4250
**Air-gapped clusters:** mirror the manifest above and the `registry.k8s.io/agent-sandbox/agent-sandbox-controller` image referenced inside it to your internal registry, then point the manifest's image reference at your mirror before applying. You will also need to mirror the OpenShell gateway and sandbox images — see the chart's `image.repository` value for the gateway and `server.sandboxImage` / `server.supervisorImage` for the sandbox runtime.
4351
</Note>
@@ -258,7 +266,7 @@ when `server.warmPooling.enabled=true`:
258266
| `""` | `nodes` | get, list, watch |
259267
| `""` | `namespaces` | get |
260268
| `extensions.agents.x-k8s.io` | `sandboxclaims` | get, list, watch; create, delete when warm pooling is enabled |
261-
| `extensions.agents.x-k8s.io` | `sandboxtemplates`, `sandboxwarmpools` | get, list, watch; create, patch, update, delete when profile reconciliation is enabled |
269+
| `extensions.agents.x-k8s.io` | `sandboxtemplates`, `sandboxwarmpools` | get, list, watch; create, patch, update, delete when template reconciliation is enabled |
262270

263271
To use an existing ServiceAccount instead of creating one, set `serviceAccount.create=false` and supply its name:
264272

‎docs/reference/gateway-config.mdx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@ gateway_pod_selector = { "app.kubernetes.io/name" = "openshell", "app.kubernetes
536536
[openshell.drivers.kubernetes.warm_pooling]
537537
# When enabled, compatible creates may use v1beta1 SandboxClaim resources
538538
# backed by the driver's cache of OpenShell-enabled SandboxWarmPool resources.
539+
# Requires the Agent Sandbox extensions.yaml APIs. If they are unavailable at
540+
# gateway startup, OpenShell disables warm pooling and template reconciliation
541+
# for this driver instance and falls back to direct Sandbox creation.
539542
enabled = true
540543

541544
[openshell.drivers.kubernetes.warm_pooling.templates]

‎docs/reference/sandbox-compute-drivers.mdx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,13 @@ OpenShell workspace. The current shared-namespace mapping selects the configured
462462
`openshell.drivers.kubernetes.warm_pooling.enabled = false` to force direct
463463
`Sandbox` creation.
464464

465+
Warm pooling requires the Agent Sandbox extension APIs from `extensions.yaml`.
466+
The OpenShell chart grants RBAC for those APIs, but it does not install the
467+
`SandboxClaim`, `SandboxTemplate`, or `SandboxWarmPool` CRDs. If the
468+
extension APIs are unavailable when the Kubernetes driver starts, OpenShell
469+
disables warm pooling and template reconciliation for that driver instance and
470+
continues with direct `Sandbox` creation.
471+
465472
When `openshell.drivers.kubernetes.warm_pooling.templates.enabled = true`, the
466473
gateway notifies the Kubernetes driver when OpenShell `SandboxTemplate`
467474
resources are created, deleted, or synced at gateway startup. The driver

0 commit comments

Comments
 (0)