diff --git a/crates/openshell-driver-kubernetes/src/config.rs b/crates/openshell-driver-kubernetes/src/config.rs index 28c04deb3..3f7888af6 100644 --- a/crates/openshell-driver-kubernetes/src/config.rs +++ b/crates/openshell-driver-kubernetes/src/config.rs @@ -7,6 +7,9 @@ use serde::{Deserialize, Serialize}; /// Default Kubernetes namespace for sandbox resources. pub const DEFAULT_K8S_NAMESPACE: &str = "openshell"; +/// Default storage size for the workspace PVC. +pub const DEFAULT_WORKSPACE_STORAGE_SIZE: &str = "2Gi"; + /// How the supervisor binary is delivered into sandbox pods. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] @@ -64,6 +67,7 @@ pub struct KubernetesComputeConfig { pub client_tls_secret_name: String, pub host_gateway_ip: String, pub enable_user_namespaces: bool, + pub workspace_default_storage_size: String, } impl Default for KubernetesComputeConfig { @@ -84,6 +88,7 @@ impl Default for KubernetesComputeConfig { client_tls_secret_name: String::new(), host_gateway_ip: String::new(), enable_user_namespaces: false, + workspace_default_storage_size: DEFAULT_WORKSPACE_STORAGE_SIZE.to_string(), } } } @@ -94,3 +99,26 @@ fn default_sandbox_image() -> String { openshell_core::image::DEFAULT_COMMUNITY_REGISTRY ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_workspace_storage_size_is_2gi() { + let cfg = KubernetesComputeConfig::default(); + assert_eq!( + cfg.workspace_default_storage_size, + DEFAULT_WORKSPACE_STORAGE_SIZE + ); + } + + #[test] + fn serde_override_workspace_storage_size() { + let json = serde_json::json!({ + "workspace_default_storage_size": "10Gi" + }); + let cfg: KubernetesComputeConfig = serde_json::from_value(json).unwrap(); + assert_eq!(cfg.workspace_default_storage_size, "10Gi"); + } +} diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index 21ec7f5bf..a624f787e 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -3,7 +3,9 @@ //! Kubernetes compute driver. -use crate::config::{KubernetesComputeConfig, SupervisorSideloadMethod}; +use crate::config::{ + DEFAULT_WORKSPACE_STORAGE_SIZE, KubernetesComputeConfig, SupervisorSideloadMethod, +}; use futures::{Stream, StreamExt, TryStreamExt}; use k8s_openapi::api::core::v1::{Event as KubeEventObj, Node}; use kube::api::{Api, ApiResource, DeleteParams, ListParams, PostParams}; @@ -106,9 +108,6 @@ const WORKSPACE_INIT_MOUNT_PATH: &str = "/workspace-pvc"; /// Name of the init container that seeds the workspace PVC. const WORKSPACE_INIT_CONTAINER_NAME: &str = "workspace-init"; -/// Default storage request for the workspace PVC. -const WORKSPACE_DEFAULT_STORAGE: &str = "2Gi"; - /// Sentinel file written by the init container after copying the image's /// `/sandbox` contents. Subsequent pod starts skip the copy. const WORKSPACE_SENTINEL: &str = ".workspace-initialized"; @@ -327,6 +326,7 @@ impl KubernetesComputeDriver { client_tls_secret_name: &self.config.client_tls_secret_name, host_gateway_ip: &self.config.host_gateway_ip, enable_user_namespaces: self.config.enable_user_namespaces, + workspace_default_storage_size: &self.config.workspace_default_storage_size, }; obj.data = sandbox_to_k8s_spec(sandbox.spec.as_ref(), ¶ms); let api = self.api(); @@ -1025,7 +1025,12 @@ fn apply_workspace_persistence( /// /// Provides a single PVC named "workspace" that backs the `/sandbox` /// directory. The init container seeds it from the image on first use. -fn default_workspace_volume_claim_templates() -> serde_json::Value { +fn default_workspace_volume_claim_templates(storage_size: &str) -> serde_json::Value { + let size = if storage_size.is_empty() { + DEFAULT_WORKSPACE_STORAGE_SIZE + } else { + storage_size + }; serde_json::json!([{ "metadata": { "name": WORKSPACE_VOLUME_NAME @@ -1034,7 +1039,7 @@ fn default_workspace_volume_claim_templates() -> serde_json::Value { "accessModes": ["ReadWriteOnce"], "resources": { "requests": { - "storage": WORKSPACE_DEFAULT_STORAGE + "storage": size } } } @@ -1056,6 +1061,7 @@ struct SandboxPodParams<'a> { client_tls_secret_name: &'a str, host_gateway_ip: &'a str, enable_user_namespaces: bool, + workspace_default_storage_size: &'a str, } fn spec_pod_env(spec: Option<&SandboxSpec>) -> std::collections::HashMap { @@ -1112,7 +1118,7 @@ fn sandbox_to_k8s_spec( if inject_workspace { root.insert( "volumeClaimTemplates".to_string(), - default_workspace_volume_claim_templates(), + default_workspace_volume_claim_templates(params.workspace_default_storage_size), ); } @@ -2572,4 +2578,18 @@ mod tests { assert_eq!(tolerations[0]["operator"], "Exists"); assert_eq!(tolerations[0]["effect"], "NoSchedule"); } + + #[test] + fn default_workspace_vct_uses_provided_storage_size() { + let vct = default_workspace_volume_claim_templates("5Gi"); + let storage = &vct[0]["spec"]["resources"]["requests"]["storage"]; + assert_eq!(storage, "5Gi"); + } + + #[test] + fn default_workspace_vct_falls_back_to_const_when_empty() { + let vct = default_workspace_volume_claim_templates(""); + let storage = &vct[0]["spec"]["resources"]["requests"]["storage"]; + assert_eq!(storage, DEFAULT_WORKSPACE_STORAGE_SIZE); + } } diff --git a/crates/openshell-driver-kubernetes/src/lib.rs b/crates/openshell-driver-kubernetes/src/lib.rs index 7975ca788..433d62353 100644 --- a/crates/openshell-driver-kubernetes/src/lib.rs +++ b/crates/openshell-driver-kubernetes/src/lib.rs @@ -5,6 +5,8 @@ pub mod config; pub mod driver; pub mod grpc; -pub use config::{KubernetesComputeConfig, SupervisorSideloadMethod}; +pub use config::{ + DEFAULT_WORKSPACE_STORAGE_SIZE, KubernetesComputeConfig, SupervisorSideloadMethod, +}; pub use driver::{KubernetesComputeDriver, KubernetesDriverError}; pub use grpc::ComputeDriverService; diff --git a/crates/openshell-driver-kubernetes/src/main.rs b/crates/openshell-driver-kubernetes/src/main.rs index a170b5785..37f8c08f8 100644 --- a/crates/openshell-driver-kubernetes/src/main.rs +++ b/crates/openshell-driver-kubernetes/src/main.rs @@ -93,6 +93,12 @@ async fn main() -> Result<()> { client_tls_secret_name: args.client_tls_secret_name.unwrap_or_default(), host_gateway_ip: args.host_gateway_ip.unwrap_or_default(), enable_user_namespaces: args.enable_user_namespaces, + workspace_default_storage_size: std::env::var( + "OPENSHELL_K8S_WORKSPACE_DEFAULT_STORAGE_SIZE", + ) + .unwrap_or_else(|_| { + openshell_driver_kubernetes::DEFAULT_WORKSPACE_STORAGE_SIZE.to_string() + }), }) .await .into_diagnostic()?; diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 834bb4857..796655c13 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -583,7 +583,10 @@ async fn build_compute_runtime( match driver { ComputeDriverKind::Kubernetes => { - let k8s = kubernetes_config_from_file(file)?; + let mut k8s = kubernetes_config_from_file(file)?; + if let Ok(size) = std::env::var("OPENSHELL_K8S_WORKSPACE_DEFAULT_STORAGE_SIZE") { + k8s.workspace_default_storage_size = size; + } ComputeRuntime::new_kubernetes( k8s, store, diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index c5484684b..390571062 100644 --- a/deploy/helm/openshell/README.md +++ b/deploy/helm/openshell/README.md @@ -149,6 +149,7 @@ cert-manager alternative. | server.tls.certSecretName | string | `"openshell-server-tls"` | K8s secret (type kubernetes.io/tls) with tls.crt and tls.key for the server. | | server.tls.clientCaSecretName | string | `"openshell-server-client-ca"` | K8s secret with ca.crt for client certificate verification (mTLS). Set to "" to disable mTLS and run HTTPS-only (use OIDC for auth instead). | | server.tls.clientTlsSecretName | string | `"openshell-client-tls"` | K8s secret mounted into sandbox pods for mTLS to the server. | +| server.workspaceDefaultStorageSize | string | `""` | Default storage size for the workspace PVC in sandbox pods. Uses Kubernetes quantity syntax (e.g. "2Gi", "10Gi", "500Mi"). Empty = built-in default (2Gi). | | service.healthPort | int | `8081` | Gateway health service port. | | service.metricsPort | int | `9090` | Gateway metrics service port. | | service.port | int | `8080` | Gateway gRPC/HTTP service port. | diff --git a/deploy/helm/openshell/templates/gateway-config.yaml b/deploy/helm/openshell/templates/gateway-config.yaml index 9d95e45c1..a3d7b3411 100644 --- a/deploy/helm/openshell/templates/gateway-config.yaml +++ b/deploy/helm/openshell/templates/gateway-config.yaml @@ -90,6 +90,9 @@ data: {{- if .Values.server.sandboxImagePullPolicy }} image_pull_policy = {{ .Values.server.sandboxImagePullPolicy | quote }} {{- end }} + {{- if .Values.server.workspaceDefaultStorageSize }} + workspace_default_storage_size = {{ .Values.server.workspaceDefaultStorageSize | quote }} + {{- end }} {{- if .Values.supervisor.image.pullPolicy }} supervisor_image_pull_policy = {{ .Values.supervisor.image.pullPolicy | quote }} {{- end }} diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index fd689c8bf..26ba1b5b5 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -138,6 +138,10 @@ server: # (Always for :latest, IfNotPresent otherwise). Set to "Always" for dev # clusters so new images are picked up without manual eviction. sandboxImagePullPolicy: "" + # -- Default storage size for the workspace PVC in sandbox pods. + # Uses Kubernetes quantity syntax (e.g. "2Gi", "10Gi", "500Mi"). + # Empty = built-in default (2Gi). + workspaceDefaultStorageSize: "" # -- gRPC endpoint sandboxes call back into the gateway. Leave empty to derive # it from the chart fullname, release namespace, service port, and # disableTls flag, for example https://openshell.openshell.svc.cluster.local:8080.