diff --git a/charts/langsmith/Chart.yaml b/charts/langsmith/Chart.yaml index 38063b869..72b1c9a20 100644 --- a/charts/langsmith/Chart.yaml +++ b/charts/langsmith/Chart.yaml @@ -5,5 +5,5 @@ maintainers: email: ankush@langchain.dev description: Helm chart to deploy the langsmith application and all services it depends on. type: application -version: 0.16.0-rc.25 +version: 0.16.0-rc.26 appVersion: "0.16.30rc1" diff --git a/charts/langsmith/README.md b/charts/langsmith/README.md index 291d43aac..5da370902 100644 --- a/charts/langsmith/README.md +++ b/charts/langsmith/README.md @@ -1,6 +1,6 @@ # langsmith -![Version: 0.16.0-rc.25](https://img.shields.io/badge/Version-0.16.0--rc.25-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.16.30rc1](https://img.shields.io/badge/AppVersion-0.16.30rc1-informational?style=flat-square) +![Version: 0.16.0-rc.26](https://img.shields.io/badge/Version-0.16.0--rc.26-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.16.30rc1](https://img.shields.io/badge/AppVersion-0.16.30rc1-informational?style=flat-square) Helm chart to deploy the langsmith application and all services it depends on. @@ -568,7 +568,7 @@ The chart-managed frontend owns public API route rewrites for LangSmith services | images.imagePullSecrets | list | `[]` | Secrets with credentials to pull images from a private registry. Specified as name: value. | | images.juicefsCSIImage | object | `{"pullPolicy":"IfNotPresent","repository":"docker.io/juicedata/juicefs-csi-driver","tag":"v0.31.4"}` | JuiceFS CSI driver image. Only used when config.sandboxes.enabled is true. | | images.juicefsCSINodeDriverRegistrarImage | object | `{"pullPolicy":"IfNotPresent","repository":"registry.k8s.io/sig-storage/csi-node-driver-registrar","tag":"v2.9.0"}` | JuiceFS CSI node-driver-registrar sidecar image. Only used when config.sandboxes.enabled is true. | -| images.juicefsMountImage | object | `{"pullPolicy":"IfNotPresent","repository":"docker.io/juicedata/mount","tag":""}` | Optional JuiceFS CE mount image used by runtime mount pods. Set this when mirroring images for restricted networks. | +| images.juicefsMountImage | object | `{"pullPolicy":"IfNotPresent","repository":"docker.io/juicedata/mount","tag":"ce-v1.3.0"}` | Optional JuiceFS CE mount image used by runtime mount pods. Set this when mirroring images for restricted networks. | | images.operatorImage.pullPolicy | string | `"IfNotPresent"` | | | images.operatorImage.repository | string | `"docker.io/langchain/langgraph-operator"` | | | images.operatorImage.tag | string | `"0.1.47"` | | @@ -873,6 +873,63 @@ The chart-managed frontend owns public API route rewrites for LangSmith services | polly.redis.statefulSet.volumeMounts | list | `[]` | | | polly.redis.statefulSet.volumes | list | `[]` | | | preInstallManifests | list | `[]` | annotations, ExternalSecret-only validation, idempotency rules, and caveats. Example: preInstallManifests: - apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: langsmith-app spec: refreshInterval: 1h secretStoreRef: name: vault-backend kind: ClusterSecretStore target: name: langsmith-app-secret creationPolicy: Orphan data: - secretKey: langsmith_license_key remoteRef: key: secret/langsmith/app property: langsmith_license_key | +| sandboxes.callbackSigningJwk | string | `""` | Private JWK signing sandbox callbacks, or key `sandbox_callback_signing_jwk` in config.existingSecretName. Needs config.hostname for the issuer, or signing fails closed. | +| sandboxes.enabled | bool | `false` | | +| sandboxes.juicefs.bucket | string | `""` | Object storage bucket/root URL used by JuiceFS. For AWS S3, use a region-explicit endpoint such as `https://bucket-name.s3.us-west-2.amazonaws.com`; do not use the `s3://bucket-name` shorthand because JuiceFS then infers region with GetBucketLocation. For GCS, use `gs://bucket-name`. Use `sandboxes.juicefs.name` for JuiceFS volume isolation instead of deployment-specific bucket paths. | +| sandboxes.juicefs.csi.configSecretName | string | `"juicefs-csi-config"` | Name of the chart-managed JuiceFS CSI config Secret. The bundled JuiceFS CSI setup is intended only for JuiceFS PVs in the namespace where this chart is installed. Chart-managed Secret/config changes roll only the JuiceFS CSI controller/node pods through checksum annotations. Helm cannot hash externally managed Secret contents or directly refresh CSI-created JuiceFS mount pods; delete affected JuiceFS mount pods so they are recreated with the updated Secret. | +| sandboxes.juicefs.csi.controller.annotations | object | `{}` | Annotations applied to the JuiceFS CSI controller StatefulSet and pod template. | +| sandboxes.juicefs.csi.controller.serviceAccount.annotations | object | `{}` | Annotations applied to the JuiceFS CSI controller ServiceAccount. | +| sandboxes.juicefs.csi.existingSecretName | string | `""` | Existing Secret containing JuiceFS CSI config keys `name`, `metaurl`, `storage`, and `bucket`. When set, this chart does not render the CSI config Secret and `sandboxes.juicefs.name`, `sandboxes.juicefs.storage`, `sandboxes.juicefs.bucket`, and `sandboxes.juicefs.redis.metaURL` are not used for the CSI config. | +| sandboxes.juicefs.csi.install | bool | `true` | Install the bundled JuiceFS CSI driver. Set to false when the cluster already runs one; the chart then renders only the sandbox PV/PVCs and CSI config Secret, bound to the existing driver. | +| sandboxes.juicefs.csi.mountPodPatch | list | `[{"mountOptions":["cache-dir=/var/cache/juicefs-csi","cache-size=51200","buffer-size=300","prefetch=3","metrics=0.0.0.0:9567"],"resources":{"limits":{"cpu":"2","memory":"4Gi"},"requests":{"cpu":"2","memory":"4Gi"}}},{"mountOptions":["cache-dir=/var/cache/juicefs-csi","cache-size=307200"],"pvcSelector":{"matchLabels":{"juicefs.langsmith.com/cache":"ssd"}}}]` | Mount pod patches used by the JuiceFS CSI driver for sandbox volumes. Override only when tuning JuiceFS mount behavior or observability. | +| sandboxes.juicefs.csi.node.annotations | object | `{}` | Annotations applied to the JuiceFS CSI node DaemonSet and pod template. | +| sandboxes.juicefs.csi.node.serviceAccount.annotations | object | `{}` | Annotations applied to the JuiceFS CSI node ServiceAccount. Use this for workload identity annotations such as AWS IRSA or GCP Workload Identity. | +| sandboxes.juicefs.csi.pvName | string | `"smithbox-juicefs-csi"` | | +| sandboxes.juicefs.csi.pvcName | string | `"smithbox-juicefs-csi"` | | +| sandboxes.juicefs.name | string | `"sandbox-juicefs"` | JuiceFS volume name. Use a flat DNS-label-style name only; slashes and object-store subpaths are not supported here. JuiceFS stores objects under `/` inside the configured bucket. Also used to scope JuiceFS CSI RBAC for the generated mount Secret, so keep it aligned with the `name` key when using an existing CSI config Secret. | +| sandboxes.juicefs.redis.metaURL | string | `""` | JuiceFS Redis metadata URL. Redis metadata engines must use maxmemory-policy noeviction. For Redis Cluster, the `/DB` path is used by JuiceFS as a hash-tag key prefix rather than a Redis logical database. | +| sandboxes.juicefs.storage | string | `"s3"` | Object storage backend used by JuiceFS for sandboxes. Currently supported values are `s3` for AWS/EKS and `gs` for GCP/GKE. Azure-backed sandbox storage is not supported yet. | +| sandboxes.proxyCa.existingSecretName | string | `""` | Existing TLS Secret containing tls.crt and tls.key for the sandbox proxy CA. This Secret can be created manually, by cert-manager, or by another external process. | +| sandboxes.proxyCa.mode | string | `"generatedSecret"` | generatedSecret creates a self-signed CA Secret with Helm and reuses it on live upgrades via lookup. In pure render/GitOps workflows where lookup cannot read the cluster, generatedSecret produces different cert material on each render; use existingSecret for deterministic manifests. | +| sandboxes.proxyCa.secretName | string | `"smithbox-proxy-ca"` | | +| sandboxes.quotas.maxCpuCores | int | `16` | | +| sandboxes.quotas.maxEphemeralStorageGib | int | `100` | | +| sandboxes.quotas.maxMemoryGb | int | `64` | | +| sandboxes.quotas.maxSandboxes | int | `1000` | | +| sandboxes.quotas.minEphemeralStorageGb | int | `1` | | +| sandboxes.sandboxHost.autoscaling | object | `{"enabled":false,"headroomHosts":1,"maxReplicas":10,"minReplicas":1,"scaleDownStabilizationSeconds":300,"targetUtilizationPercent":70}` | Sandbox host pool autoscaling. There is no HPA or KEDA object: the elected sandbox-host resizes the Deployment itself. Unmanaged `sandbox-host.smith.langchain.com/autoscale-*` annotations override these live. | +| sandboxes.sandboxHost.autoscaling.headroomHosts | int | `1` | Spare empty hosts kept above current demand so a sandbox create rarely waits for a cold node. | +| sandboxes.sandboxHost.autoscaling.scaleDownStabilizationSeconds | int | `300` | Hold the replica target at its recent peak this long before shrinking, so load dips do not thrash the pool. | +| sandboxes.sandboxHost.autoscaling.targetUtilizationPercent | int | `70` | Target share of pool CPU capacity committed to sandboxes before scaling up. | +| sandboxes.sandboxHost.deployment.annotations | object | `{}` | | +| sandboxes.sandboxHost.deployment.extraEnv | list | `[]` | | +| sandboxes.sandboxHost.deployment.initContainers | list | `[]` | | +| sandboxes.sandboxHost.deployment.labels | object | `{}` | | +| sandboxes.sandboxHost.deployment.nodeSelector | object | `{}` | Node selector for sandbox-host pods. Required: host pods must land on KVM-capable nodes, and a toleration alone does not attract them there. | +| sandboxes.sandboxHost.deployment.podAnnotations | object | `{}` | | +| sandboxes.sandboxHost.deployment.podSecurityContext | object | `{}` | | +| sandboxes.sandboxHost.deployment.priorityClassName | string | `""` | | +| sandboxes.sandboxHost.deployment.readinessProbe | object | `{"failureThreshold":3,"initialDelaySeconds":5,"periodSeconds":10,"tcpSocket":{"port":"http"},"timeoutSeconds":3}` | Readiness probe. The host binds its listener last, so accepting connections gates the rollout. No liveness probe: a restart mid-suspend destroys running microVMs. | +| sandboxes.sandboxHost.deployment.replicas | int | `1` | Number of sandbox-host replicas, i.e. sandbox nodes backing the pool. One host per node, so do not exceed the node count. Ignored when autoscaling is enabled. | +| sandboxes.sandboxHost.deployment.resources.requests.cpu | string | `"2"` | | +| sandboxes.sandboxHost.deployment.resources.requests.memory | string | `"2Gi"` | | +| sandboxes.sandboxHost.deployment.securityContext | object | `{"privileged":true}` | Container security context. Must stay privileged, with a writable root: the host needs netns, iptables and mount propagation, and unpacks the guest tools image. | +| sandboxes.sandboxHost.deployment.sidecars | list | `[]` | | +| sandboxes.sandboxHost.deployment.terminationGracePeriodSeconds | int | `300` | | +| sandboxes.sandboxHost.deployment.tolerations | list | `[{"effect":"NoSchedule","key":"sandbox.langsmith.com/host","operator":"Equal","value":"true"}]` | Tolerations for sandbox-host pods. The default expects sandbox-host nodes tainted `sandbox.langsmith.com/host=true:NoSchedule`; override this if your sandbox node pool uses different taints. | +| sandboxes.sandboxHost.deployment.volumeMounts | list | `[]` | | +| sandboxes.sandboxHost.deployment.volumes | list | `[]` | | +| sandboxes.sandboxHost.name | string | `"sandbox-host"` | Component name for sandbox-host. Resources are named `-`, which the chart passes to the host as SANDBOX_HOST_DEPLOYMENT_NAME. | +| sandboxes.sandboxHost.pdb | object | `{"annotations":{},"enabled":false,"labels":{},"maxUnavailable":1}` | Disruption budget for sandbox-host, capping concurrent evictions since each drain suspends every microVM on that host. maxUnavailable keeps a small pool drainable; setting minAvailable overrides it. | +| sandboxes.sandboxHost.rbac.annotations | object | `{}` | | +| sandboxes.sandboxHost.rbac.create | bool | `true` | | +| sandboxes.sandboxHost.rbac.labels | object | `{}` | | +| sandboxes.sandboxHost.serviceAccount.annotations | object | `{}` | | +| sandboxes.sandboxHost.serviceAccount.automountServiceAccountToken | bool | `true` | | +| sandboxes.sandboxHost.serviceAccount.create | bool | `true` | | +| sandboxes.sandboxHost.serviceAccount.labels | object | `{}` | | +| sandboxes.sandboxHost.serviceAccount.name | string | `""` | | +| sandboxes.serviceUrlBaseUrl | string | `""` | Base URL for reaching HTTP services inside sandboxes. Needs wildcard DNS and TLS for `*.`; with ingress.enabled the chart adds the wildcard rule. http(s) origin only, no path. | | smithdb.clusterManager.containerGrpcPort | int | `8091` | | | smithdb.clusterManager.containerPort | int | `8090` | | | smithdb.clusterManager.deployment.affinity | object | `{}` | | @@ -1277,25 +1334,7 @@ The chart-managed frontend owns public API route rewrites for LangSmith services | Key | Type | Default | Description | |-----|------|---------|-------------| -| config.agentBuilder.agent.extraEnv | object | `{}` | | -| config.agentBuilder.agent.resources.cpu | int | `2` | | -| config.agentBuilder.agent.resources.cpuLimit | int | `4` | | -| config.agentBuilder.agent.resources.maxScale | int | `5` | | -| config.agentBuilder.agent.resources.memoryLimitMb | int | `8192` | | -| config.agentBuilder.agent.resources.memoryMb | int | `4096` | | -| config.agentBuilder.agent.resources.minScale | int | `1` | | -| config.agentBuilder.enabled | bool | `false` | | -| config.agentBuilder.encryptionKey | string | `""` | | -| config.agentBuilder.oauth.githubOAuthProvider | string | `""` | | -| config.agentBuilder.oauth.googleOAuthProvider | string | `""` | | -| config.agentBuilder.oauth.linearOAuthProvider | string | `""` | | -| config.agentBuilder.oauth.linkedinOAuthProvider | string | `""` | | -| config.agentBuilder.oauth.microsoftOAuthProvider | string | `""` | | -| config.agentBuilder.oauth.salesforceOAuthProvider | string | `""` | | -| config.agentBuilder.oauth.slackBotId | string | `""` | | -| config.agentBuilder.oauth.slackOAuthProvider | string | `""` | | -| config.agentBuilder.oauth.slackSigningSecret | string | `""` | | -| config.agentBuilder.oauthProviderOrgId | string | `""` | | +| config.agentBuilder | object | `{"agent":{"extraEnv":{},"resources":{"cpu":2,"cpuLimit":4,"maxScale":5,"memoryLimitMb":8192,"memoryMb":4096,"minScale":1}},"enabled":false,"encryptionKey":"","oauth":{"githubOAuthProvider":"","googleOAuthProvider":"","linearOAuthProvider":"","linkedinOAuthProvider":"","microsoftOAuthProvider":"","salesforceOAuthProvider":"","slackBotId":"","slackOAuthProvider":"","slackSigningSecret":""},"oauthProviderOrgId":""}` | LangSmith Sandboxes configuration. Disabled by default. When enabled, this installs sandbox runtime resources in the same namespace as the LangSmith release and wires the LangSmith API/frontend services to expose sandbox functionality. This supports the same-cluster sandbox-host architecture only. Self-hosted sandboxes are currently supported on AWS/EKS and GCP/GKE. Azure/AKS is supported by the base LangSmith chart, but not by sandboxes yet. Enabling sandboxes also installs the JuiceFS CSI driver. The CSI driver creates cluster-scoped resources, so only one sandbox-enabled LangSmith release should manage it per Kubernetes cluster. Do not enable sandboxes in multiple releases in the same cluster, or alongside an independently managed JuiceFS CSI installation, unless you have verified ownership of those cluster-scoped resources. | | config.apiKeySalt | string | `""` | Salt used to generate the API key. Should be a random string. | | config.authType | string | `""` | Must be 'oauth' for OAuth with PKCE, 'mixed' for basic auth or OAuth without PKCE | | config.basePath | string | `""` | Base path for the LangSmith installation. Used to serve the app under a subpath like example.com/langsmith. WARNING: Changing basePath after LangSmith Deployments have been created will break existing deployments. Existing deployments will need to be recreated for the new basePath to take effect. | @@ -1353,29 +1392,6 @@ The chart-managed frontend owns public API route rewrites for LangSmith services | config.observability.tracing.useTls | bool | `true` | Use TLS for OTLP export. | | config.orgAdminsInstallationUsageExportEnabled | bool | `false` | When true, any org admin can use the usage backfill export. | | config.personalOrgsDisabled | bool | `true` | Disable personal orgs. | -| config.sandboxes | object | `{"callbackSigningJwk":"","clusterName":"","enabled":false,"juicefs":{"bucket":"","csi":{"configSecretName":"juicefs-csi-config","controller":{"annotations":{},"serviceAccount":{"annotations":{}}},"existingSecretName":"","mountPath":"/juicefs","mountPodPatch":[{"mountOptions":["cache-dir=/var/cache/juicefs-csi","cache-size=51200","buffer-size=300","prefetch=3","metrics=0.0.0.0:9567"],"resources":{"limits":{"cpu":"2","memory":"4Gi"},"requests":{"cpu":"2","memory":"4Gi"}}},{"mountOptions":["cache-dir=/var/cache/juicefs-csi","cache-size=307200"],"pvcSelector":{"matchLabels":{"juicefs.langsmith.com/cache":"ssd"}}}],"node":{"annotations":{},"serviceAccount":{"annotations":{}}},"pvName":"smithbox-juicefs-csi","pvcName":"smithbox-juicefs-csi"},"name":"sandbox-juicefs","redis":{"metaURL":""},"storage":"s3"},"limits":{"maxCpuCores":16,"maxEphemeralStorageGb":100,"maxMemoryGb":64,"maxSandboxes":1000,"minEphemeralStorageGb":1},"proxyCa":{"existingSecretName":"","mode":"generatedSecret","secretName":"smithbox-proxy-ca"},"sandboxHost":{"allowPrivateIPCallbacks":false,"containerPort":19190,"deployment":{"annotations":{},"labels":{},"nodeSelector":{},"podAnnotations":{},"replicas":null,"resources":{"requests":{"cpu":"2","memory":"2Gi"}},"securityContext":{"privileged":true},"terminationGracePeriodSeconds":300,"tolerations":[{"effect":"NoSchedule","key":"sandbox.langsmith.com/host","operator":"Equal","value":"true"}]},"name":"sandbox-host","rbac":{"annotations":{},"create":true,"labels":{}},"serviceAccount":{"annotations":{},"automountServiceAccountToken":true,"create":true,"labels":{},"name":""}},"serviceUrlBaseUrl":"","xServiceAuthJwtSecret":"","xServiceAuthJwtSecretPrevious":""}` | LangSmith Sandboxes configuration. Disabled by default. When enabled, this installs sandbox runtime resources in the same namespace as the LangSmith release and wires the LangSmith API/frontend services to expose sandbox functionality. This supports the same-cluster sandbox-host architecture only. Self-hosted sandboxes are currently supported on AWS/EKS and GCP/GKE. Azure/AKS is supported by the base LangSmith chart, but not by sandboxes yet. Enabling sandboxes also installs the JuiceFS CSI driver. The CSI driver creates cluster-scoped resources, so only one sandbox-enabled LangSmith release should manage it per Kubernetes cluster. Do not enable sandboxes in multiple releases in the same cluster, or alongside an independently managed JuiceFS CSI installation, unless you have verified ownership of those cluster-scoped resources. | -| config.sandboxes.callbackSigningJwk | string | `""` | Required private JWK for sandbox callback signing when this chart creates the LangSmith app Secret. When config.existingSecretName is set, add key `sandbox_callback_signing_jwk` to that Secret instead. | -| config.sandboxes.clusterName | string | `""` | Logical Kubernetes cluster name sent to LangSmith sandbox workers. | -| config.sandboxes.juicefs.bucket | string | `""` | Object storage bucket/root URL used by JuiceFS. For AWS S3, use a region-explicit endpoint such as `https://bucket-name.s3.us-west-2.amazonaws.com`; do not use the `s3://bucket-name` shorthand because JuiceFS then infers region with GetBucketLocation. For GCS, use `gs://bucket-name`. Use `config.sandboxes.juicefs.name` for JuiceFS volume isolation instead of deployment-specific bucket paths. | -| config.sandboxes.juicefs.csi.configSecretName | string | `"juicefs-csi-config"` | Name of the chart-managed JuiceFS CSI config Secret. The bundled JuiceFS CSI setup is intended only for JuiceFS PVs in the namespace where this chart is installed. Chart-managed Secret/config changes roll only the JuiceFS CSI controller/node pods through checksum annotations. Helm cannot hash externally managed Secret contents or directly refresh CSI-created JuiceFS mount pods; delete affected JuiceFS mount pods so they are recreated with the updated Secret. | -| config.sandboxes.juicefs.csi.controller.annotations | object | `{}` | Annotations applied to the JuiceFS CSI controller StatefulSet and pod template. | -| config.sandboxes.juicefs.csi.controller.serviceAccount.annotations | object | `{}` | Annotations applied to the JuiceFS CSI controller ServiceAccount. | -| config.sandboxes.juicefs.csi.existingSecretName | string | `""` | Existing Secret containing JuiceFS CSI config keys `name`, `metaurl`, `storage`, and `bucket`. When set, this chart does not render the CSI config Secret and `config.sandboxes.juicefs.name`, `config.sandboxes.juicefs.storage`, `config.sandboxes.juicefs.bucket`, and `config.sandboxes.juicefs.redis.metaURL` are not used for the CSI config. | -| config.sandboxes.juicefs.csi.mountPodPatch | list | `[{"mountOptions":["cache-dir=/var/cache/juicefs-csi","cache-size=51200","buffer-size=300","prefetch=3","metrics=0.0.0.0:9567"],"resources":{"limits":{"cpu":"2","memory":"4Gi"},"requests":{"cpu":"2","memory":"4Gi"}}},{"mountOptions":["cache-dir=/var/cache/juicefs-csi","cache-size=307200"],"pvcSelector":{"matchLabels":{"juicefs.langsmith.com/cache":"ssd"}}}]` | Mount pod patches used by the JuiceFS CSI driver for sandbox volumes. Override only when tuning JuiceFS mount behavior or observability. | -| config.sandboxes.juicefs.csi.node.annotations | object | `{}` | Annotations applied to the JuiceFS CSI node DaemonSet and pod template. | -| config.sandboxes.juicefs.csi.node.serviceAccount.annotations | object | `{}` | Annotations applied to the JuiceFS CSI node ServiceAccount. Use this for workload identity annotations such as AWS IRSA or GCP Workload Identity. | -| config.sandboxes.juicefs.name | string | `"sandbox-juicefs"` | JuiceFS volume name. Use a flat DNS-label-style name only; slashes and object-store subpaths are not supported here. JuiceFS stores objects under `/` inside the configured bucket. Also used to scope JuiceFS CSI RBAC for the generated mount Secret, so keep it aligned with the `name` key when using an existing CSI config Secret. | -| config.sandboxes.juicefs.redis.metaURL | string | `""` | JuiceFS Redis metadata URL. Redis metadata engines must use maxmemory-policy noeviction. For Redis Cluster, the `/DB` path is used by JuiceFS as a hash-tag key prefix rather than a Redis logical database. | -| config.sandboxes.juicefs.storage | string | `"s3"` | Object storage backend used by JuiceFS for sandboxes. Currently supported values are `s3` for AWS/EKS and `gs` for GCP/GKE. Azure-backed sandbox storage is not supported yet. | -| config.sandboxes.proxyCa.existingSecretName | string | `""` | Existing TLS Secret containing tls.crt and tls.key for the sandbox proxy CA. This Secret can be created manually, by cert-manager, or by another external process. | -| config.sandboxes.proxyCa.mode | string | `"generatedSecret"` | generatedSecret creates a self-signed CA Secret with Helm and reuses it on live upgrades via lookup. In pure render/GitOps workflows where lookup cannot read the cluster, generatedSecret produces different cert material on each render; use existingSecret for deterministic manifests. | -| config.sandboxes.sandboxHost.allowPrivateIPCallbacks | bool | `false` | Whether sandbox callback validation should allow private IP and localhost targets. This renders SSRF_ALLOW_PRIVATE_IPS_CALLBACKS for sandbox-host. | -| config.sandboxes.sandboxHost.deployment.nodeSelector | object | `{}` | Node selector for sandbox-host pods. Empty uses the default selector `sandbox.langsmith.com/host=true`; set this map to replace the default if your sandbox node pool uses different labels. | -| config.sandboxes.sandboxHost.deployment.replicas | string | `nil` | Optional initial sandbox-host Deployment replica count. Leave null to let Kubernetes default the initial replica count and avoid Helm reconciling runtime scaling. Multiple sandbox-host replicas are kept one per node by chart-managed required pod anti-affinity on `kubernetes.io/hostname` plus the sandbox-host `hostPort`. The chart also uses a no-surge rolling update strategy (`maxSurge: 0`, `maxUnavailable: 1`) so image updates roll hosts one node at a time instead of creating unschedulable surge pods. | -| config.sandboxes.sandboxHost.deployment.tolerations | list | `[{"effect":"NoSchedule","key":"sandbox.langsmith.com/host","operator":"Equal","value":"true"}]` | Tolerations for sandbox-host pods. The default expects sandbox-host nodes tainted `sandbox.langsmith.com/host=true:NoSchedule`; override this if your sandbox node pool uses different taints. | -| config.sandboxes.serviceUrlBaseUrl | string | `""` | Optional base URL used to generate browser/programmatic service URLs for HTTP services running inside sandboxes. When set, the chart renders SANDBOX_SERVICE_URLS for config.sandboxes.clusterName. Requires wildcard DNS and TLS for `*.`. When ingress.enabled is true, the chart also adds a wildcard ingress rule to the LangSmith platform backend. Must be an http:// or https:// URL without a path, query string, or fragment. When ingress.enabled is true, the host must be a DNS hostname without a port. | -| config.sandboxes.xServiceAuthJwtSecret | string | `""` | Shared service-auth secret override for LangSmith <-> sandbox runtime calls when this chart creates the LangSmith app Secret. If unset, the chart uses the configured API key salt or LangSmith license key as the chart-managed value. When config.existingSecretName is set, add key `sandbox_x_service_auth_jwt_secret` to that Secret instead. | -| config.sandboxes.xServiceAuthJwtSecretPrevious | string | `""` | Optional previous service-auth secret used during rotation. Used only when this chart creates the LangSmith app Secret. When config.existingSecretName is set, add optional key `sandbox_x_service_auth_jwt_secret_previous` to that Secret to enable rotation. | | config.security | object | `{"cors":{"allowedOrigins":"*","allowedOriginsRegex":"","alwaysAllowPathsRegex":""}}` | Security configuration for CORS, headers, and other security-related settings. These settings control cross-origin access and help protect against common web vulnerabilities. | | config.security.cors | object | `{"allowedOrigins":"*","allowedOriginsRegex":"","alwaysAllowPathsRegex":""}` | CORS (Cross-Origin Resource Sharing) configuration. Controls which origins can make requests to the LangSmith API. By default, CORS is permissive. For production deployments, you should restrict this. | | config.security.cors.allowedOrigins | string | `"*"` | Comma-separated list of allowed origins. Use "*" to allow all origins (not recommended for production). Example: "https://app.example.com,https://admin.example.com" If allowedOriginsRegex is set, this value is ignored. | diff --git a/charts/langsmith/templates/_helpers.tpl b/charts/langsmith/templates/_helpers.tpl index ebbf2d854..523abd70e 100644 --- a/charts/langsmith/templates/_helpers.tpl +++ b/charts/langsmith/templates/_helpers.tpl @@ -497,6 +497,18 @@ Template containing common environment variables that are used by several servic key: polly_encryption_key optional: false {{- end }} +{{- if .Values.sandboxes.enabled }} +- name: SANDBOX_FEATURE_ENABLED + value: "true" +- name: SANDBOX_RUNTIME_V2 + value: "always" +- name: SANDBOX_X_SERVICE_AUTH_JWT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "langsmith.secretsName" . }} + key: api_key_salt + optional: {{ .Values.config.disableSecretCreation }} +{{- end }} {{- end }} {{/* @@ -1245,11 +1257,11 @@ Falls back to config.hostname when config.frontendHostname is unset. {{- end -}} {{/* -Host portion of config.sandboxes.serviceUrlBaseUrl. +Host portion of sandboxes.serviceUrlBaseUrl. */}} {{- define "langsmith.sandboxes.serviceUrlHost" -}} -{{- if .Values.config.sandboxes.serviceUrlBaseUrl -}} -{{- regexReplaceAll "^https?://" .Values.config.sandboxes.serviceUrlBaseUrl "" -}} +{{- if .Values.sandboxes.serviceUrlBaseUrl -}} +{{- regexReplaceAll "^https?://" .Values.sandboxes.serviceUrlBaseUrl "" -}} {{- end -}} {{- end -}} @@ -1257,32 +1269,18 @@ Host portion of config.sandboxes.serviceUrlBaseUrl. Sandbox proxy CA secret name in the LangSmith release namespace. */}} {{- define "langsmith.sandboxes.proxyCaSecretName" -}} -{{- if eq .Values.config.sandboxes.proxyCa.mode "existingSecret" -}} -{{- .Values.config.sandboxes.proxyCa.existingSecretName -}} +{{- if eq .Values.sandboxes.proxyCa.mode "existingSecret" -}} +{{- .Values.sandboxes.proxyCa.existingSecretName -}} {{- else -}} -{{- default "smithbox-proxy-ca" .Values.config.sandboxes.proxyCa.secretName -}} +{{- default "smithbox-proxy-ca" .Values.sandboxes.proxyCa.secretName -}} {{- end -}} {{- end -}} -{{/* -Sandbox service auth secret material for chart-created app/runtime Secrets. -*/}} -{{- define "langsmith.sandboxes.xServiceAuthJwtSecretValue" -}} -{{- default (default .Values.config.langsmithLicenseKey .Values.config.apiKeySalt) .Values.config.sandboxes.xServiceAuthJwtSecret -}} -{{- end -}} - -{{/* -Internal LangSmith platform endpoint used by sandbox runtime callbacks. -*/}} -{{- define "langsmith.sandboxes.langsmithInternalEndpoint" -}} -{{- printf "http://%s-%s.%s.svc.%s:%v" (include "langsmith.fullname" .) .Values.platformBackend.name (.Values.namespace | default .Release.Namespace) .Values.clusterDomain .Values.platformBackend.service.port -}} -{{- end -}} - {{/* Name for the JuiceFS CSI config Secret. */}} {{- define "langsmith.sandboxes.juicefsCSIConfigSecretName" -}} -{{- default .Values.config.sandboxes.juicefs.csi.configSecretName .Values.config.sandboxes.juicefs.csi.existingSecretName -}} +{{- default .Values.sandboxes.juicefs.csi.configSecretName .Values.sandboxes.juicefs.csi.existingSecretName -}} {{- end -}} {{/* @@ -1292,8 +1290,8 @@ cannot be scoped by resourceNames, but read/update/delete verbs can. {{- define "langsmith.sandboxes.juicefsCSISecretResourceNames" -}} {{- $names := list (include "langsmith.sandboxes.juicefsCSIConfigSecretName" .) - (printf "juicefs-%s-secret" .Values.config.sandboxes.juicefs.name) - (printf "juicefs-%s-secret" .Values.config.sandboxes.juicefs.csi.pvName) + (printf "juicefs-%s-secret" .Values.sandboxes.juicefs.name) + (printf "juicefs-%s-secret" .Values.sandboxes.juicefs.csi.pvName) (printf "juicefs-%s-secret" (include "langsmith.sandboxes.juicefsHostPVName" .)) -}} {{- $resourceNames := list -}} @@ -1307,11 +1305,11 @@ cannot be scoped by resourceNames, but read/update/delete verbs can. Rendered JuiceFS CSI config Secret data for chart-managed sandbox volumes. */}} {{- define "langsmith.sandboxes.juicefsCSIConfigSecretData" -}} -{{- $juicefsRedis := .Values.config.sandboxes.juicefs.redis | default dict -}} -name: {{ .Values.config.sandboxes.juicefs.name | quote }} +{{- $juicefsRedis := .Values.sandboxes.juicefs.redis | default dict -}} +name: {{ .Values.sandboxes.juicefs.name | quote }} metaurl: {{ $juicefsRedis.metaURL | quote }} -storage: {{ .Values.config.sandboxes.juicefs.storage | quote }} -bucket: {{ .Values.config.sandboxes.juicefs.bucket | quote }} +storage: {{ .Values.sandboxes.juicefs.storage | quote }} +bucket: {{ .Values.sandboxes.juicefs.bucket | quote }} {{- end -}} {{/* @@ -1319,7 +1317,7 @@ Checksum for the JuiceFS CSI config Secret known to Helm. Existing Secrets use the Secret name only because Helm cannot safely hash live external Secret data. */}} {{- define "langsmith.sandboxes.juicefsCSIConfigSecretChecksum" -}} -{{- if .Values.config.sandboxes.juicefs.csi.existingSecretName -}} +{{- if .Values.sandboxes.juicefs.csi.existingSecretName -}} {{- printf "existing:%s" (include "langsmith.sandboxes.juicefsCSIConfigSecretName" .) | sha256sum -}} {{- else -}} {{- include "langsmith.sandboxes.juicefsCSIConfigSecretData" . | sha256sum -}} @@ -1330,11 +1328,11 @@ the Secret name only because Helm cannot safely hash live external Secret data. Derived JuiceFS CSI PV/PVC names for the sandbox-host mount. */}} {{- define "langsmith.sandboxes.juicefsHostPVName" -}} -{{- printf "%s-host" .Values.config.sandboxes.juicefs.csi.pvName -}} +{{- printf "%s-host" .Values.sandboxes.juicefs.csi.pvName -}} {{- end -}} {{- define "langsmith.sandboxes.juicefsHostPVCName" -}} -{{- printf "%s-host" .Values.config.sandboxes.juicefs.csi.pvcName -}} +{{- printf "%s-host" .Values.sandboxes.juicefs.csi.pvcName -}} {{- end -}} {{/* @@ -1391,46 +1389,21 @@ Rendered JuiceFS CSI driver config file. {{- define "langsmith.sandboxes.juicefsCSIDriverConfig" -}} enableNodeSelector: false mountPodPatch: -{{- toYaml .Values.config.sandboxes.juicefs.csi.mountPodPatch | nindent 2 }} +{{- toYaml .Values.sandboxes.juicefs.csi.mountPodPatch | nindent 2 }} {{- end -}} {{- define "langsmith.sandboxes.juicefsCSIDriverConfigChecksum" -}} {{- include "langsmith.sandboxes.juicefsCSIDriverConfig" . | sha256sum -}} {{- end -}} -{{/* -Creates an optional image reference without falling back to Chart.AppVersion. -*/}} -{{- define "langsmith.optionalImage" -}} -{{- $imageConfig := index .Values.images .component -}} -{{- if and $imageConfig.repository $imageConfig.tag -}} -{{- if .Values.images.registry -}} -{{ .Values.images.registry }}/{{ $imageConfig.repository }}:{{ $imageConfig.tag }} -{{- else -}} -{{ $imageConfig.repository }}:{{ $imageConfig.tag }} -{{- end -}} -{{- end -}} -{{- end -}} - {{/* Sandbox service account names. */}} {{- define "langsmith.sandboxes.sandboxHostServiceAccountName" -}} -{{- if .Values.config.sandboxes.sandboxHost.serviceAccount.create -}} -{{- default .Values.config.sandboxes.sandboxHost.name .Values.config.sandboxes.sandboxHost.serviceAccount.name | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- default "default" .Values.config.sandboxes.sandboxHost.serviceAccount.name -}} -{{- end -}} -{{- end -}} - -{{/* -Node selector for sandbox-host pods. -*/}} -{{- define "langsmith.sandboxes.sandboxHostNodeSelector" -}} -{{- if .Values.config.sandboxes.sandboxHost.deployment.nodeSelector -}} -{{- toYaml .Values.config.sandboxes.sandboxHost.deployment.nodeSelector -}} +{{- if .Values.sandboxes.sandboxHost.serviceAccount.create -}} +{{- default (printf "%s-%s" (include "langsmith.fullname" .) .Values.sandboxes.sandboxHost.name) .Values.sandboxes.sandboxHost.serviceAccount.name | trunc 63 | trimSuffix "-" -}} {{- else -}} -sandbox.langsmith.com/host: "true" +{{- default "default" .Values.sandboxes.sandboxHost.serviceAccount.name -}} {{- end -}} {{- end -}} @@ -1452,26 +1425,6 @@ which default to http:// for local development. {{- end -}} {{- end -}} -{{/* -Public API endpoint used by sandbox runtime-v2 dataplane URLs. -When hostname is unset, keep this relative so browser callers use the same origin. -*/}} -{{- define "langsmith.sandboxes.platformEndpoint" -}} -{{- $basePath := trimAll "/" (default "" .Values.config.basePath) -}} -{{- if .Values.config.hostname -}} - {{- $baseURL := include "langsmith.hostnameWithProtocol" . | trimSuffix "/" -}} - {{- if $basePath -}} - {{- printf "%s/%s/api" $baseURL $basePath -}} - {{- else -}} - {{- printf "%s/api" $baseURL -}} - {{- end -}} -{{- else if $basePath -}} - {{- printf "/%s/api" $basePath -}} -{{- else -}} - /api -{{- end -}} -{{- end -}} - {{/* OAuth Authorization Server issuer URL advertised to remote MCP clients. Derived from [/basePath]/api. The /api path is fixed because the @@ -1587,3 +1540,19 @@ Served through the frontend at /mcp (or //mcp). optional: {{ .Values.config.disableSecretCreation }} {{- end }} {{- end -}} + +{{/* +In-cluster platform-backend URL. Also rendered as GO_ENDPOINT in the shared ConfigMap. +*/}} +{{- define "langsmith.platformBackendEndpoint" -}} +{{- printf "http://%s-%s.%s.svc.%s:%v" (include "langsmith.fullname" .) .Values.platformBackend.name (.Values.namespace | default .Release.Namespace) .Values.clusterDomain .Values.platformBackend.service.port -}} +{{- end -}} + +{{/* +Public API base: scheme, host, any base path, and the /api prefix. Derived from +config.hostname; relative when unset so browser callers use their own origin. Equal to the +OAuth issuer today, but kept separate so either can change on its own. +*/}} +{{- define "langsmith.publicApiEndpoint" -}} +{{- include "langsmith.hostnameWithProtocol" . }}{{- with .Values.config.basePath }}/{{ . }}{{- end }}/api +{{- end -}} diff --git a/charts/langsmith/templates/config-map.yaml b/charts/langsmith/templates/config-map.yaml index 2ecddccd7..1663920cd 100644 --- a/charts/langsmith/templates/config-map.yaml +++ b/charts/langsmith/templates/config-map.yaml @@ -35,10 +35,10 @@ data: {{- end }} LANGCHAIN_ENV: "local_kubernetes" LANGCHAIN_ENDPOINT: "/{{ if .Values.config.basePath }}{{ .Values.config.basePath }}/{{ end }}api/v1" - {{- if .Values.config.sandboxes.enabled }} - LANGCHAIN_PLATFORM_ENDPOINT: {{ include "langsmith.sandboxes.platformEndpoint" . | quote }} + {{- if .Values.sandboxes.enabled }} {{- end }} - GO_ENDPOINT: "http://{{ include "langsmith.fullname" . }}-{{ .Values.platformBackend.name }}.{{ .Values.namespace | default .Release.Namespace }}.svc.{{ .Values.clusterDomain }}:{{ .Values.platformBackend.service.port }}" + GO_ENDPOINT: {{ include "langsmith.platformBackendEndpoint" . | quote }} + LANGSMITH_PUBLIC_API_ENDPOINT: {{ include "langsmith.publicApiEndpoint" . | quote }} {{- if .Values.engine.enabled }} FORGE_AGENT_LANGGRAPH_URL: "http://{{ include "langsmith.agentFeatures.fullname" (dict "root" . "product" "engineInsightsAgent") }}-{{ .Values.engineInsightsAgent.apiServer.name }}.{{ .Values.namespace | default .Release.Namespace }}.svc.{{ .Values.clusterDomain }}:{{ .Values.engineInsightsAgent.apiServer.service.httpPort }}" {{- end }} diff --git a/charts/langsmith/templates/frontend/config-map.yaml b/charts/langsmith/templates/frontend/config-map.yaml index 2db13bc9e..8072adde2 100644 --- a/charts/langsmith/templates/frontend/config-map.yaml +++ b/charts/langsmith/templates/frontend/config-map.yaml @@ -137,7 +137,7 @@ data: proxy_pass http://{{ include "langsmith.fullname" . }}-{{ .Values.platformBackend.name }}.{{ .Values.namespace | default .Release.Namespace }}.svc.{{ .Values.clusterDomain }}:{{ .Values.platformBackend.service.port }}; } - {{- if .Values.config.sandboxes.enabled }} + {{- if .Values.sandboxes.enabled }} location /{{ .Values.config.basePath }}/api/v2/sandboxes/ { rewrite /{{ .Values.config.basePath }}/api/v2/sandboxes/(.*) /v2/sandboxes/$1 break; proxy_set_header Upgrade $http_upgrade; @@ -772,7 +772,7 @@ data: proxy_pass http://{{ include "langsmith.fullname" . }}-{{ .Values.platformBackend.name }}.{{ .Values.namespace | default .Release.Namespace }}.svc.{{ .Values.clusterDomain }}:{{ .Values.platformBackend.service.port }}; } - {{- if .Values.config.sandboxes.enabled }} + {{- if .Values.sandboxes.enabled }} location /api/v2/sandboxes/ { rewrite /api/v2/sandboxes/(.*) /v2/sandboxes/$1 break; proxy_set_header Upgrade $http_upgrade; diff --git a/charts/langsmith/templates/ingest-queue/deployment.yaml b/charts/langsmith/templates/ingest-queue/deployment.yaml index 81c500cf0..7225f0258 100644 --- a/charts/langsmith/templates/ingest-queue/deployment.yaml +++ b/charts/langsmith/templates/ingest-queue/deployment.yaml @@ -4,21 +4,9 @@ - name: "QUEUE_PORT" value: {{ .Values.ingestQueue.containerPort | quote }} {{ include "langsmith.engine.smithGoEnv" . }} -{{- if .Values.config.sandboxes.enabled }} -- name: SANDBOX_FEATURE_ENABLED - value: "true" -- name: SANDBOX_RUNTIME_V2 - value: "always" -- name: SANDBOX_K8S_CLUSTER_NAME - value: {{ .Values.config.sandboxes.clusterName | quote }} +{{- if .Values.sandboxes.enabled }} - name: SANDBOX_QUEUE_PRIORITY value: "1" -- name: SANDBOX_X_SERVICE_AUTH_JWT_SECRET - valueFrom: - secretKeyRef: - name: {{ include "langsmith.secretsName" . }} - key: sandbox_x_service_auth_jwt_secret - optional: {{ .Values.config.disableSecretCreation }} {{- end }} {{- end -}} {{- if .Values.ingestQueue.enabled }} diff --git a/charts/langsmith/templates/ingress.yaml b/charts/langsmith/templates/ingress.yaml index 8ae0d4918..2dd68c865 100644 --- a/charts/langsmith/templates/ingress.yaml +++ b/charts/langsmith/templates/ingress.yaml @@ -34,7 +34,7 @@ spec: name: {{ include "langsmith.fullname" . }}-{{ .Values.frontend.name }} port: number: {{ .Values.frontend.service.httpPort }} - {{- if and .Values.config.sandboxes.enabled .Values.config.sandboxes.serviceUrlBaseUrl }} + {{- if and .Values.sandboxes.enabled .Values.sandboxes.serviceUrlBaseUrl }} - host: {{ printf "*.%s" (include "langsmith.sandboxes.serviceUrlHost" .) | quote }} http: paths: diff --git a/charts/langsmith/templates/platform-backend/deployment.yaml b/charts/langsmith/templates/platform-backend/deployment.yaml index ba29f9e17..cf55fbe43 100644 --- a/charts/langsmith/templates/platform-backend/deployment.yaml +++ b/charts/langsmith/templates/platform-backend/deployment.yaml @@ -14,44 +14,25 @@ key: langsmith_signing_jwks optional: true {{ include "langsmith.engine.smithGoEnv" . }} -{{- if .Values.config.sandboxes.enabled }} -- name: SANDBOX_FEATURE_ENABLED - value: "true" +{{- if .Values.sandboxes.enabled }} - name: SANDBOX_FRONTEND_ENABLED value: "true" -- name: SANDBOX_RUNTIME_V2 - value: "always" -- name: SANDBOX_K8S_CLUSTER_NAME - value: {{ .Values.config.sandboxes.clusterName | quote }} - name: SANDBOX_MAX_CPU_CORES - value: {{ .Values.config.sandboxes.limits.maxCpuCores | quote }} + value: {{ .Values.sandboxes.quotas.maxCpuCores | quote }} - name: SANDBOX_MAX_MEMORY_GB - value: {{ .Values.config.sandboxes.limits.maxMemoryGb | quote }} + value: {{ .Values.sandboxes.quotas.maxMemoryGb | quote }} - name: SANDBOX_MIN_EPHEMERAL_STORAGE_GB - value: {{ .Values.config.sandboxes.limits.minEphemeralStorageGb | quote }} + value: {{ .Values.sandboxes.quotas.minEphemeralStorageGb | quote }} - name: SANDBOX_MAX_EPHEMERAL_STORAGE_GIB - value: {{ .Values.config.sandboxes.limits.maxEphemeralStorageGb | quote }} + value: {{ .Values.sandboxes.quotas.maxEphemeralStorageGib | quote }} - name: SANDBOX_MAX_SANDBOXES - value: {{ .Values.config.sandboxes.limits.maxSandboxes | quote }} -{{- if .Values.config.sandboxes.serviceUrlBaseUrl }} + value: {{ .Values.sandboxes.quotas.maxSandboxes | quote }} +{{- if .Values.sandboxes.serviceUrlBaseUrl }} +{{- /* Keyed by the name the runtime resolves and stores on each sandbox. */}} - name: SANDBOX_SERVICE_URLS - value: {{ dict .Values.config.sandboxes.clusterName .Values.config.sandboxes.serviceUrlBaseUrl | toJson | quote }} -{{- end }} -- name: SANDBOX_X_SERVICE_AUTH_JWT_SECRET - valueFrom: - secretKeyRef: - name: {{ include "langsmith.secretsName" . }} - key: sandbox_x_service_auth_jwt_secret - optional: {{ .Values.config.disableSecretCreation }} -{{- if or (not (empty .Values.config.existingSecretName)) .Values.config.sandboxes.xServiceAuthJwtSecretPrevious }} -- name: SANDBOX_X_SERVICE_AUTH_JWT_SECRET_PREVIOUS - valueFrom: - secretKeyRef: - name: {{ include "langsmith.secretsName" . }} - key: sandbox_x_service_auth_jwt_secret_previous - optional: {{ or .Values.config.disableSecretCreation (not (empty .Values.config.existingSecretName)) }} + value: {{ dict "incluster" .Values.sandboxes.serviceUrlBaseUrl | toJson | quote }} {{- end }} -{{- if or (not (empty .Values.config.existingSecretName)) .Values.config.sandboxes.callbackSigningJwk }} +{{- if or (not (empty .Values.config.existingSecretName)) .Values.sandboxes.callbackSigningJwk }} - name: LANGSMITH_SANDBOX_CALLBACK_SIGNING_JWK valueFrom: secretKeyRef: diff --git a/charts/langsmith/templates/sandboxes/juicefs-csi-driver/configmap.yaml b/charts/langsmith/templates/sandboxes/juicefs-csi-driver/configmap.yaml index f93af9775..8a7839674 100644 --- a/charts/langsmith/templates/sandboxes/juicefs-csi-driver/configmap.yaml +++ b/charts/langsmith/templates/sandboxes/juicefs-csi-driver/configmap.yaml @@ -1,4 +1,5 @@ -{{- if .Values.config.sandboxes.enabled }} +{{- /* Vendored from the upstream juicefs-csi-driver chart; re-sync when bumping images.juicefsCSIImage. */}} +{{- if and .Values.sandboxes.enabled .Values.sandboxes.juicefs.csi.install }} {{- $namespace := .Values.namespace | default .Release.Namespace }} apiVersion: v1 kind: ConfigMap @@ -9,7 +10,6 @@ metadata: app.kubernetes.io/component: config {{- include "langsmith.sandboxes.juicefsCSILabels" . | nindent 4 }} annotations: - helm.sh/resource-policy: keep {{- include "langsmith.sandboxes.juicefsCSIAnnotations" . | nindent 4 }} data: config.yaml: |- diff --git a/charts/langsmith/templates/sandboxes/juicefs-csi-driver/controller-statefulset.yaml b/charts/langsmith/templates/sandboxes/juicefs-csi-driver/controller-statefulset.yaml index bc2244967..a794545c7 100644 --- a/charts/langsmith/templates/sandboxes/juicefs-csi-driver/controller-statefulset.yaml +++ b/charts/langsmith/templates/sandboxes/juicefs-csi-driver/controller-statefulset.yaml @@ -1,7 +1,8 @@ -{{- if .Values.config.sandboxes.enabled }} +{{- /* Vendored from the upstream juicefs-csi-driver chart; re-sync when bumping images.juicefsCSIImage. */}} +{{- if and .Values.sandboxes.enabled .Values.sandboxes.juicefs.csi.install }} {{- $namespace := .Values.namespace | default .Release.Namespace }} -{{- $mountImage := include "langsmith.optionalImage" (dict "Values" .Values "Chart" .Chart "component" "juicefsMountImage") }} -{{- $controllerAnnotations := .Values.config.sandboxes.juicefs.csi.controller.annotations }} +{{- $mountImage := include "langsmith.image" (dict "Values" .Values "Chart" .Chart "component" "juicefsMountImage") }} +{{- $controllerAnnotations := .Values.sandboxes.juicefs.csi.controller.annotations }} {{- $podAnnotations := mergeOverwrite (dict) (default dict .Values.commonPodAnnotations) $controllerAnnotations }} apiVersion: apps/v1 kind: StatefulSet @@ -70,10 +71,8 @@ spec: value: /var/lib/juicefs/volume - name: JUICEFS_CONFIG_PATH value: /var/lib/juicefs/config - {{- if $mountImage }} - name: JUICEFS_CE_MOUNT_IMAGE value: {{ $mountImage | quote }} - {{- end }} - name: JUICEFS_CSI_WEB_PORT value: "9567" - name: DRIVER_NAME diff --git a/charts/langsmith/templates/sandboxes/juicefs-csi-driver/csidriver.yaml b/charts/langsmith/templates/sandboxes/juicefs-csi-driver/csidriver.yaml index 10ffb6824..f9f53f234 100644 --- a/charts/langsmith/templates/sandboxes/juicefs-csi-driver/csidriver.yaml +++ b/charts/langsmith/templates/sandboxes/juicefs-csi-driver/csidriver.yaml @@ -1,4 +1,5 @@ -{{- if .Values.config.sandboxes.enabled }} +{{- /* Vendored from the upstream juicefs-csi-driver chart; re-sync when bumping images.juicefsCSIImage. */}} +{{- if and .Values.sandboxes.enabled .Values.sandboxes.juicefs.csi.install }} apiVersion: storage.k8s.io/v1 kind: CSIDriver metadata: @@ -7,7 +8,6 @@ metadata: app.kubernetes.io/component: driver {{- include "langsmith.sandboxes.juicefsCSILabels" . | nindent 4 }} annotations: - helm.sh/resource-policy: keep {{- include "langsmith.sandboxes.juicefsCSIAnnotations" . | nindent 4 }} spec: attachRequired: false diff --git a/charts/langsmith/templates/sandboxes/juicefs-csi-driver/node-daemonset.yaml b/charts/langsmith/templates/sandboxes/juicefs-csi-driver/node-daemonset.yaml index 515b48f01..c554b1922 100644 --- a/charts/langsmith/templates/sandboxes/juicefs-csi-driver/node-daemonset.yaml +++ b/charts/langsmith/templates/sandboxes/juicefs-csi-driver/node-daemonset.yaml @@ -1,7 +1,8 @@ -{{- if .Values.config.sandboxes.enabled }} +{{- /* Vendored from the upstream juicefs-csi-driver chart; re-sync when bumping images.juicefsCSIImage. */}} +{{- if and .Values.sandboxes.enabled .Values.sandboxes.juicefs.csi.install }} {{- $namespace := .Values.namespace | default .Release.Namespace }} -{{- $mountImage := include "langsmith.optionalImage" (dict "Values" .Values "Chart" .Chart "component" "juicefsMountImage") }} -{{- $nodeAnnotations := .Values.config.sandboxes.juicefs.csi.node.annotations }} +{{- $mountImage := include "langsmith.image" (dict "Values" .Values "Chart" .Chart "component" "juicefsMountImage") }} +{{- $nodeAnnotations := .Values.sandboxes.juicefs.csi.node.annotations }} {{- $podAnnotations := mergeOverwrite (dict) (default dict .Values.commonPodAnnotations) $nodeAnnotations }} apiVersion: apps/v1 kind: DaemonSet @@ -77,10 +78,8 @@ spec: value: /var/lib/juicefs/config - name: FS_SHARE_MOUNT value: "true" - {{- if $mountImage }} - name: JUICEFS_CE_MOUNT_IMAGE value: {{ $mountImage | quote }} - {{- end }} - name: JUICEFS_CSI_WEB_PORT value: "9567" - name: DRIVER_NAME @@ -172,7 +171,7 @@ spec: name: registration-dir dnsPolicy: ClusterFirstWithHostNet priorityClassName: system-node-critical - {{- with .Values.config.sandboxes.sandboxHost.deployment.tolerations }} + {{- with .Values.sandboxes.sandboxHost.deployment.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/langsmith/templates/sandboxes/juicefs-csi-driver/rbac.yaml b/charts/langsmith/templates/sandboxes/juicefs-csi-driver/rbac.yaml index 9ae994da5..27783f19a 100644 --- a/charts/langsmith/templates/sandboxes/juicefs-csi-driver/rbac.yaml +++ b/charts/langsmith/templates/sandboxes/juicefs-csi-driver/rbac.yaml @@ -1,7 +1,8 @@ -{{- if .Values.config.sandboxes.enabled }} +{{- /* Vendored from the upstream juicefs-csi-driver chart; re-sync when bumping images.juicefsCSIImage. */}} +{{- if and .Values.sandboxes.enabled .Values.sandboxes.juicefs.csi.install }} {{- $namespace := .Values.namespace | default .Release.Namespace }} -{{- $controllerServiceAccountAnnotations := mergeOverwrite (dict) (include "langsmith.sandboxes.juicefsCSIAnnotations" . | fromYaml) (default dict .Values.config.sandboxes.juicefs.csi.controller.serviceAccount.annotations) }} -{{- $nodeServiceAccountAnnotations := mergeOverwrite (dict) (include "langsmith.sandboxes.juicefsCSIAnnotations" . | fromYaml) (default dict .Values.config.sandboxes.juicefs.csi.node.serviceAccount.annotations) }} +{{- $controllerServiceAccountAnnotations := mergeOverwrite (dict) (include "langsmith.sandboxes.juicefsCSIAnnotations" . | fromYaml) (default dict .Values.sandboxes.juicefs.csi.controller.serviceAccount.annotations) }} +{{- $nodeServiceAccountAnnotations := mergeOverwrite (dict) (include "langsmith.sandboxes.juicefsCSIAnnotations" . | fromYaml) (default dict .Values.sandboxes.juicefs.csi.node.serviceAccount.annotations) }} apiVersion: v1 kind: ServiceAccount metadata: diff --git a/charts/langsmith/templates/sandboxes/juicefs-csi.yaml b/charts/langsmith/templates/sandboxes/juicefs-csi.yaml index 11100cdff..b7c75f37d 100644 --- a/charts/langsmith/templates/sandboxes/juicefs-csi.yaml +++ b/charts/langsmith/templates/sandboxes/juicefs-csi.yaml @@ -1,11 +1,11 @@ -{{- if .Values.config.sandboxes.enabled }} +{{- if .Values.sandboxes.enabled }} {{- $namespace := .Values.namespace | default .Release.Namespace }} -{{- $juicefsCSI := .Values.config.sandboxes.juicefs.csi | default dict }} +{{- $juicefsCSI := .Values.sandboxes.juicefs.csi | default dict }} {{- if not $juicefsCSI.existingSecretName }} apiVersion: v1 kind: Secret metadata: - name: {{ .Values.config.sandboxes.juicefs.csi.configSecretName }} + name: {{ .Values.sandboxes.juicefs.csi.configSecretName }} namespace: {{ $namespace }} labels: {{- include "langsmith.labels" . | nindent 4 }} @@ -20,7 +20,7 @@ stringData: apiVersion: v1 kind: PersistentVolume metadata: - name: {{ .Values.config.sandboxes.juicefs.csi.pvName }} + name: {{ .Values.sandboxes.juicefs.csi.pvName }} labels: app: smithbox juicefs.langsmith.com/mount: csi @@ -34,13 +34,16 @@ spec: storageClassName: "" claimRef: namespace: {{ $namespace }} - name: {{ .Values.config.sandboxes.juicefs.csi.pvcName }} + name: {{ .Values.sandboxes.juicefs.csi.pvcName }} csi: driver: {{ include "langsmith.sandboxes.juicefsCSIDriverName" . }} - volumeHandle: {{ .Values.config.sandboxes.juicefs.csi.pvName }} + volumeHandle: {{ .Values.sandboxes.juicefs.csi.pvName }} fsType: juicefs + {{- /* An external driver uses its own mount ServiceAccount. */}} + {{- if .Values.sandboxes.juicefs.csi.install }} volumeAttributes: juicefs/mount-service-account: {{ include "langsmith.sandboxes.juicefsCSINodeServiceAccountName" . }} + {{- end }} nodePublishSecretRef: name: {{ include "langsmith.sandboxes.juicefsCSIConfigSecretName" . }} namespace: {{ $namespace }} @@ -48,14 +51,14 @@ spec: apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: {{ .Values.config.sandboxes.juicefs.csi.pvcName }} + name: {{ .Values.sandboxes.juicefs.csi.pvcName }} namespace: {{ $namespace }} spec: accessModes: - ReadWriteMany volumeMode: Filesystem storageClassName: "" - volumeName: {{ .Values.config.sandboxes.juicefs.csi.pvName }} + volumeName: {{ .Values.sandboxes.juicefs.csi.pvName }} resources: requests: storage: 10Pi @@ -86,8 +89,10 @@ spec: driver: {{ include "langsmith.sandboxes.juicefsCSIDriverName" . }} volumeHandle: {{ include "langsmith.sandboxes.juicefsHostPVName" . }} fsType: juicefs + {{- if .Values.sandboxes.juicefs.csi.install }} volumeAttributes: juicefs/mount-service-account: {{ include "langsmith.sandboxes.juicefsCSINodeServiceAccountName" . }} + {{- end }} nodePublishSecretRef: name: {{ include "langsmith.sandboxes.juicefsCSIConfigSecretName" . }} namespace: {{ $namespace }} diff --git a/charts/langsmith/templates/sandboxes/proxy-ca.yaml b/charts/langsmith/templates/sandboxes/proxy-ca.yaml index 76b60c747..9cbd869ba 100644 --- a/charts/langsmith/templates/sandboxes/proxy-ca.yaml +++ b/charts/langsmith/templates/sandboxes/proxy-ca.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.config.sandboxes.enabled (eq .Values.config.sandboxes.proxyCa.mode "generatedSecret") }} +{{- if and .Values.sandboxes.enabled (eq .Values.sandboxes.proxyCa.mode "generatedSecret") }} {{- $name := include "langsmith.sandboxes.proxyCaSecretName" . -}} {{- $namespace := .Values.namespace | default .Release.Namespace -}} {{- $existing := lookup "v1" "Secret" $namespace $name -}} diff --git a/charts/langsmith/templates/sandboxes/sandbox-host-deployment.yaml b/charts/langsmith/templates/sandboxes/sandbox-host-deployment.yaml index a3f229f54..37bbdcb4d 100644 --- a/charts/langsmith/templates/sandboxes/sandbox-host-deployment.yaml +++ b/charts/langsmith/templates/sandboxes/sandbox-host-deployment.yaml @@ -1,27 +1,34 @@ -{{- if .Values.config.sandboxes.enabled }} +{{- if .Values.sandboxes.enabled }} {{- $namespace := .Values.namespace | default .Release.Namespace }} +{{- $autoscaling := .Values.sandboxes.sandboxHost.autoscaling }} +{{- /* Chart labels last so they win; the selector depends on them. */}} +{{- $labels := mergeOverwrite (dict "app" .Values.sandboxes.sandboxHost.name) + (default dict .Values.sandboxes.sandboxHost.deployment.labels) + (include "langsmith.labels" . | fromYaml) + (dict "app.kubernetes.io/component" (printf "%s-%s" (include "langsmith.fullname" .) .Values.sandboxes.sandboxHost.name)) }} +{{- $tlsVolumes := include "langsmith.tlsVolumes" . | fromYamlArray }} +{{- $tlsVolumeMounts := include "langsmith.tlsVolumeMounts" . | fromYamlArray }} +{{- $extraVolumes := concat .Values.commonVolumes .Values.sandboxes.sandboxHost.deployment.volumes }} +{{- $extraVolumeMounts := concat .Values.commonVolumeMounts .Values.sandboxes.sandboxHost.deployment.volumeMounts }} apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Values.config.sandboxes.sandboxHost.name }} + name: {{ include "langsmith.fullname" . }}-{{ .Values.sandboxes.sandboxHost.name }} namespace: {{ $namespace }} labels: - app: sandbox-host - {{- include "langsmith.labels" . | nindent 4 }} - {{- with .Values.config.sandboxes.sandboxHost.deployment.labels }} - {{- toYaml . | nindent 4 }} - {{- end }} + {{- toYaml $labels | nindent 4 }} annotations: {{- include "langsmith.annotations" . | nindent 4 }} - {{- with .Values.config.sandboxes.sandboxHost.deployment.annotations }} + {{- with .Values.sandboxes.sandboxHost.deployment.annotations }} {{- toYaml . | nindent 4 }} {{- end }} spec: selector: matchLabels: - app: sandbox-host - {{- if not (kindIs "invalid" .Values.config.sandboxes.sandboxHost.deployment.replicas) }} - replicas: {{ .Values.config.sandboxes.sandboxHost.deployment.replicas }} + {{- include "langsmith.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: {{ include "langsmith.fullname" . }}-{{ .Values.sandboxes.sandboxHost.name }} + {{- if not $autoscaling.enabled }} + replicas: {{ .Values.sandboxes.sandboxHost.deployment.replicas }} {{- end }} strategy: type: RollingUpdate @@ -31,41 +38,55 @@ spec: template: metadata: labels: - app: sandbox-host - {{- include "langsmith.labels" . | nindent 8 }} + {{- toYaml $labels | nindent 8 }} annotations: {{- include "langsmith.commonPodAnnotations" . | nindent 8 }} - {{- with .Values.config.sandboxes.sandboxHost.deployment.podAnnotations }} + {{- with .Values.sandboxes.sandboxHost.deployment.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} spec: + {{- with include "langsmith.podSecurityContext" (dict "Values" .Values "componentSecurityContext" .Values.sandboxes.sandboxHost.deployment.podSecurityContext) }} + securityContext: + {{- . | nindent 8 }} + {{- end }} hostNetwork: true dnsPolicy: ClusterFirstWithHostNet serviceAccountName: {{ include "langsmith.sandboxes.sandboxHostServiceAccountName" . }} affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: + {{- /* One host per node; hostPort enforces it regardless. */}} - labelSelector: matchLabels: - app: sandbox-host + {{- include "langsmith.selectorLabels" . | nindent 18 }} + app.kubernetes.io/component: {{ include "langsmith.fullname" . }}-{{ .Values.sandboxes.sandboxHost.name }} topologyKey: kubernetes.io/hostname {{- with .Values.images.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} + {{- with .Values.sandboxes.sandboxHost.deployment.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + {{- with .Values.sandboxes.sandboxHost.deployment.nodeSelector }} nodeSelector: - {{- include "langsmith.sandboxes.sandboxHostNodeSelector" . | nindent 8 }} - {{- with .Values.config.sandboxes.sandboxHost.deployment.tolerations }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.sandboxes.sandboxHost.deployment.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{- with concat .Values.commonInitContainers .Values.sandboxes.sandboxHost.deployment.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} containers: - name: sandbox-host image: {{ include "langsmith.image" (dict "Values" .Values "Chart" .Chart "component" "sandboxHostImage") | quote }} imagePullPolicy: {{ .Values.images.sandboxHostImage.pullPolicy }} env: - name: LANGCHAIN_ENV - value: {{ .Values.config.langchainEnv | quote }} + value: "local_kubernetes" - name: POD_NAME valueFrom: fieldRef: @@ -86,11 +107,15 @@ spec: valueFrom: secretKeyRef: name: {{ include "langsmith.secretsName" . }} - key: sandbox_x_service_auth_jwt_secret + key: api_key_salt optional: {{ .Values.config.disableSecretCreation }} - name: SANDBOX_HOST_PROXY_CA_DIR value: /certs - {{- if or (not (empty .Values.config.existingSecretName)) .Values.config.sandboxes.callbackSigningJwk }} + {{- if and .Values.config.customCa.secretName .Values.config.customCa.secretKey }} + - name: SSL_CERT_FILE + value: /etc/ssl/certs/custom-ca-certificates.crt + {{- end }} + {{- if or (not (empty .Values.config.existingSecretName)) .Values.sandboxes.callbackSigningJwk }} - name: LANGSMITH_SANDBOX_CALLBACK_SIGNING_JWK valueFrom: secretKeyRef: @@ -98,33 +123,68 @@ spec: key: sandbox_callback_signing_jwk optional: true {{- end }} + {{- /* Receivers fetch the key from /.well-known/jwks.json. */}} + {{- if .Values.config.hostname }} - name: OAUTH_AS_ISSUER - value: {{ include "langsmith.sandboxes.langsmithInternalEndpoint" . | quote }} + value: {{ include "langsmith.oauthAsIssuer" . | quote }} + {{- end }} + {{- /* Self-hosted reaches private addresses; SaaS is what blocks them. */}} - name: SSRF_ALLOW_PRIVATE_IPS_CALLBACKS - value: {{ .Values.config.sandboxes.sandboxHost.allowPrivateIPCallbacks | quote }} + value: "true" - name: SANDBOX_CALLBACK_LOOPBACK_REWRITE value: "false" - name: LANGSMITH_SANDBOX_INTERNAL_ENDPOINT - value: {{ include "langsmith.sandboxes.langsmithInternalEndpoint" . | quote }} + value: {{ include "langsmith.platformBackendEndpoint" . | quote }} + - name: SANDBOX_HOST_DEPLOYMENT_NAME + value: {{ printf "%s-%s" (include "langsmith.fullname" .) .Values.sandboxes.sandboxHost.name | quote }} - name: SANDBOX_HOST_TERMINATION_GRACE_SECONDS - value: {{ .Values.config.sandboxes.sandboxHost.deployment.terminationGracePeriodSeconds | quote }} + value: {{ .Values.sandboxes.sandboxHost.deployment.terminationGracePeriodSeconds | quote }} + {{- /* Baseline; autoscale-* annotations override it live. */}} + - name: SANDBOX_HOST_AUTOSCALE_ENABLED + value: {{ $autoscaling.enabled | quote }} + {{- if $autoscaling.enabled }} + - name: SANDBOX_HOST_AUTOSCALE_MIN_REPLICAS + value: {{ $autoscaling.minReplicas | quote }} + - name: SANDBOX_HOST_AUTOSCALE_MAX_REPLICAS + value: {{ $autoscaling.maxReplicas | quote }} + - name: SANDBOX_HOST_AUTOSCALE_TARGET_UTILIZATION_PERCENT + value: {{ $autoscaling.targetUtilizationPercent | quote }} + - name: SANDBOX_HOST_AUTOSCALE_HEADROOM_HOSTS + value: {{ $autoscaling.headroomHosts | quote }} + - name: SANDBOX_HOST_AUTOSCALE_SCALE_DOWN_STABILIZATION_SECONDS + value: {{ $autoscaling.scaleDownStabilizationSeconds | quote }} + {{- end }} + {{- with .Values.sandboxes.sandboxHost.deployment.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- /* Fixed: the host's own defaults. SANDBOX_HOST_LISTEN_ADDR is not set. */}} ports: - name: http - containerPort: {{ .Values.config.sandboxes.sandboxHost.containerPort }} - hostPort: {{ .Values.config.sandboxes.sandboxHost.containerPort }} + containerPort: 19190 + hostPort: 19190 protocol: TCP + {{- with .Values.sandboxes.sandboxHost.deployment.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} securityContext: - {{- toYaml .Values.config.sandboxes.sandboxHost.deployment.securityContext | nindent 12 }} + {{- toYaml .Values.sandboxes.sandboxHost.deployment.securityContext | nindent 12 }} resources: - {{- toYaml .Values.config.sandboxes.sandboxHost.deployment.resources | nindent 12 }} + {{- toYaml .Values.sandboxes.sandboxHost.deployment.resources | nindent 12 }} volumeMounts: - name: juicefs - mountPath: {{ .Values.config.sandboxes.juicefs.csi.mountPath }} + mountPath: /juicefs - name: host-bin mountPath: /host-bin - name: proxy-ca mountPath: /certs readOnly: true + {{- with concat $tlsVolumeMounts $extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.sandboxes.sandboxHost.deployment.sidecars }} + {{- toYaml . | nindent 8 }} + {{- end }} volumes: - name: juicefs persistentVolumeClaim: @@ -136,5 +196,8 @@ spec: hostPath: path: /var/lib/sandbox-host type: DirectoryOrCreate - terminationGracePeriodSeconds: {{ .Values.config.sandboxes.sandboxHost.deployment.terminationGracePeriodSeconds }} + {{- with concat $tlsVolumes $extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: {{ .Values.sandboxes.sandboxHost.deployment.terminationGracePeriodSeconds }} {{- end }} diff --git a/charts/langsmith/templates/sandboxes/sandbox-host-pdb.yaml b/charts/langsmith/templates/sandboxes/sandbox-host-pdb.yaml new file mode 100644 index 000000000..035933a9b --- /dev/null +++ b/charts/langsmith/templates/sandboxes/sandbox-host-pdb.yaml @@ -0,0 +1,28 @@ +{{- if and .Values.sandboxes.enabled .Values.sandboxes.sandboxHost.pdb.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "langsmith.fullname" . }}-{{ .Values.sandboxes.sandboxHost.name }} + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + {{- include "langsmith.labels" . | nindent 4 }} + {{- with .Values.sandboxes.sandboxHost.pdb.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + annotations: + {{- include "langsmith.annotations" . | nindent 4 }} + {{- with .Values.sandboxes.sandboxHost.pdb.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "langsmith.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: {{ include "langsmith.fullname" . }}-{{ .Values.sandboxes.sandboxHost.name }} + {{- /* Only one may be set, so an explicit minAvailable wins over the default maxUnavailable. */}} + {{- if hasKey .Values.sandboxes.sandboxHost.pdb "minAvailable" }} + minAvailable: {{ .Values.sandboxes.sandboxHost.pdb.minAvailable }} + {{- else if hasKey .Values.sandboxes.sandboxHost.pdb "maxUnavailable" }} + maxUnavailable: {{ .Values.sandboxes.sandboxHost.pdb.maxUnavailable }} + {{- end }} +{{- end }} diff --git a/charts/langsmith/templates/sandboxes/sandbox-host-rbac.yaml b/charts/langsmith/templates/sandboxes/sandbox-host-rbac.yaml index c771b3f62..393d4e06c 100644 --- a/charts/langsmith/templates/sandboxes/sandbox-host-rbac.yaml +++ b/charts/langsmith/templates/sandboxes/sandbox-host-rbac.yaml @@ -1,60 +1,64 @@ -{{- if .Values.config.sandboxes.enabled }} +{{- if and .Values.sandboxes.enabled .Values.sandboxes.sandboxHost.rbac.create }} {{- $namespace := .Values.namespace | default .Release.Namespace }} -{{- if .Values.config.sandboxes.sandboxHost.serviceAccount.create }} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "langsmith.sandboxes.sandboxHostServiceAccountName" . }} - namespace: {{ $namespace }} - labels: - {{- include "langsmith.labels" . | nindent 4 }} - {{- with .Values.config.sandboxes.sandboxHost.serviceAccount.labels }} - {{- toYaml . | nindent 4 }} - {{- end }} - annotations: - {{- include "langsmith.annotations" . | nindent 4 }} - {{- with .Values.config.sandboxes.sandboxHost.serviceAccount.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.config.sandboxes.sandboxHost.serviceAccount.automountServiceAccountToken }} -{{- if .Values.config.sandboxes.sandboxHost.rbac.create }} ---- -{{- end }} -{{- end }} -{{- if .Values.config.sandboxes.sandboxHost.rbac.create }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - name: sandbox-host-lease + name: {{ include "langsmith.fullname" . }}-{{ .Values.sandboxes.sandboxHost.name }} namespace: {{ $namespace }} labels: {{- include "langsmith.labels" . | nindent 4 }} - {{- with .Values.config.sandboxes.sandboxHost.rbac.labels }} + {{- with .Values.sandboxes.sandboxHost.rbac.labels }} {{- toYaml . | nindent 4 }} {{- end }} annotations: {{- include "langsmith.annotations" . | nindent 4 }} - {{- with .Values.config.sandboxes.sandboxHost.rbac.annotations }} + {{- with .Values.sandboxes.sandboxHost.rbac.annotations }} {{- toYaml . | nindent 4 }} {{- end }} rules: - - apiGroups: ["coordination.k8s.io"] - resources: ["leases"] - verbs: ["create", "get", "list", "watch", "update", "delete"] +# Host liveness leases + leader election. +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - update + - watch +# The elected host reads and resizes its own Deployment. +- apiGroups: + - apps + resources: + - deployments + resourceNames: + - {{ include "langsmith.fullname" . }}-{{ .Values.sandboxes.sandboxHost.name }} + verbs: + - get + - patch +# Pod deletion cost, so the emptiest host is evicted first. +- apiGroups: + - "" + resources: + - pods + verbs: + - patch --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: - name: sandbox-host-lease + name: {{ include "langsmith.fullname" . }}-{{ .Values.sandboxes.sandboxHost.name }} namespace: {{ $namespace }} labels: {{- include "langsmith.labels" . | nindent 4 }} - {{- with .Values.config.sandboxes.sandboxHost.rbac.labels }} + {{- with .Values.sandboxes.sandboxHost.rbac.labels }} {{- toYaml . | nindent 4 }} {{- end }} annotations: {{- include "langsmith.annotations" . | nindent 4 }} - {{- with .Values.config.sandboxes.sandboxHost.rbac.annotations }} + {{- with .Values.sandboxes.sandboxHost.rbac.annotations }} {{- toYaml . | nindent 4 }} {{- end }} subjects: @@ -62,8 +66,7 @@ subjects: name: {{ include "langsmith.sandboxes.sandboxHostServiceAccountName" . }} namespace: {{ $namespace }} roleRef: - apiGroup: rbac.authorization.k8s.io kind: Role - name: sandbox-host-lease -{{- end }} + name: {{ include "langsmith.fullname" . }}-{{ .Values.sandboxes.sandboxHost.name }} + apiGroup: rbac.authorization.k8s.io {{- end }} diff --git a/charts/langsmith/templates/sandboxes/sandbox-host-service-account.yaml b/charts/langsmith/templates/sandboxes/sandbox-host-service-account.yaml new file mode 100644 index 000000000..f0c88f048 --- /dev/null +++ b/charts/langsmith/templates/sandboxes/sandbox-host-service-account.yaml @@ -0,0 +1,18 @@ +{{- if and .Values.sandboxes.enabled .Values.sandboxes.sandboxHost.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "langsmith.sandboxes.sandboxHostServiceAccountName" . }} + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + {{- include "langsmith.labels" . | nindent 4 }} + {{- with .Values.sandboxes.sandboxHost.serviceAccount.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + annotations: + {{- include "langsmith.annotations" . | nindent 4 }} + {{- with .Values.sandboxes.sandboxHost.serviceAccount.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.sandboxes.sandboxHost.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/charts/langsmith/templates/secrets.yaml b/charts/langsmith/templates/secrets.yaml index 3b97b50e7..e7e3a365a 100644 --- a/charts/langsmith/templates/secrets.yaml +++ b/charts/langsmith/templates/secrets.yaml @@ -28,9 +28,7 @@ data: engine_encryption_key: {{ .Values.engine.encryptionKey | b64enc | quote }} engine_encryption_key_previous: {{ .Values.engine.encryptionKeyPrevious | b64enc | quote }} polly_encryption_key: {{ .Values.polly.encryptionKey | b64enc | quote }} - {{- if .Values.config.sandboxes.enabled }} - sandbox_x_service_auth_jwt_secret: {{ include "langsmith.sandboxes.xServiceAuthJwtSecretValue" . | b64enc | quote }} - sandbox_x_service_auth_jwt_secret_previous: {{ .Values.config.sandboxes.xServiceAuthJwtSecretPrevious | b64enc | quote }} - sandbox_callback_signing_jwk: {{ .Values.config.sandboxes.callbackSigningJwk | b64enc | quote }} + {{- if .Values.sandboxes.enabled }} + sandbox_callback_signing_jwk: {{ .Values.sandboxes.callbackSigningJwk | b64enc | quote }} {{- end }} {{- end }} diff --git a/charts/langsmith/templates/validate.yaml b/charts/langsmith/templates/validate.yaml index 8c22007f9..b76767492 100644 --- a/charts/langsmith/templates/validate.yaml +++ b/charts/langsmith/templates/validate.yaml @@ -164,90 +164,104 @@ AWS Marketplace Helm template verification). {{- end -}} {{- /* Validate sandbox configuration */ -}} -{{- if .Values.config.sandboxes.enabled -}} +{{- /* config.sandboxes moved to a top-level sandboxes: block. Fail loudly rather than + silently ignoring the old path, which would disable sandboxes on upgrade. */}} +{{- if .Values.config.sandboxes -}} +{{- fail "config.sandboxes has moved to a top-level sandboxes: block. Move these values to sandboxes: (limits is now quotas, maxEphemeralStorageGb is now maxEphemeralStorageGib, and clusterName / xServiceAuthJwtSecret / xServiceAuthJwtSecretPrevious / sandboxHost.allowPrivateIPCallbacks / sandboxHost.containerPort are no longer used)." -}} +{{- end -}} +{{- if .Values.sandboxes.enabled -}} {{- if not .Values.images.sandboxHostImage.repository -}} -{{- fail "images.sandboxHostImage.repository is required when config.sandboxes.enabled is true." -}} +{{- fail "images.sandboxHostImage.repository is required when sandboxes.enabled is true." -}} {{- end -}} {{- if not .Values.images.sandboxHostImage.tag -}} -{{- fail "images.sandboxHostImage.tag is required when config.sandboxes.enabled is true." -}} +{{- fail "images.sandboxHostImage.tag is required when sandboxes.enabled is true." -}} {{- end -}} +{{- if .Values.sandboxes.juicefs.csi.install -}} {{- range $imageName := list "juicefsCSIImage" "juicefsCSINodeDriverRegistrarImage" -}} {{- $image := index $.Values.images $imageName -}} {{- if not $image.repository -}} -{{- fail (printf "images.%s.repository is required when config.sandboxes.enabled is true." $imageName) -}} +{{- fail (printf "images.%s.repository is required when sandboxes.enabled is true." $imageName) -}} {{- end -}} {{- if not $image.tag -}} -{{- fail (printf "images.%s.tag is required when config.sandboxes.enabled is true." $imageName) -}} +{{- fail (printf "images.%s.tag is required when sandboxes.enabled is true." $imageName) -}} {{- end -}} {{- end -}} {{- if and .Values.images.juicefsMountImage.tag (not .Values.images.juicefsMountImage.repository) -}} {{- fail "images.juicefsMountImage.repository is required when images.juicefsMountImage.tag is set." -}} {{- end -}} -{{- if not .Values.config.sandboxes.clusterName -}} -{{- fail "config.sandboxes.clusterName is required when config.sandboxes.enabled is true." -}} {{- end -}} -{{- $serviceUrlBaseUrl := .Values.config.sandboxes.serviceUrlBaseUrl | default "" -}} +{{- /* The host needs netns, iptables and mount propagation. Fail here, not at CrashLoop. */ -}} +{{- if and (hasKey .Values.sandboxes.sandboxHost.deployment.securityContext "privileged") (not .Values.sandboxes.sandboxHost.deployment.securityContext.privileged) -}} +{{- fail "sandboxes.sandboxHost.deployment.securityContext.privileged must be true: sandbox-host manages microVMs, network namespaces and mounts." -}} +{{- end -}} +{{- if .Values.sandboxes.sandboxHost.deployment.securityContext.readOnlyRootFilesystem -}} +{{- fail "sandboxes.sandboxHost.deployment.securityContext.readOnlyRootFilesystem must not be true: sandbox-host unpacks the guest tools image on startup." -}} +{{- end -}} +{{- if not .Values.sandboxes.sandboxHost.deployment.nodeSelector -}} +{{- fail "sandboxes.sandboxHost.deployment.nodeSelector is required when sandboxes.enabled is true: sandbox-host must be pinned to KVM-capable nodes, and its toleration alone does not attract it there." -}} +{{- end -}} +{{- $autoscaling := .Values.sandboxes.sandboxHost.autoscaling -}} +{{- if $autoscaling.enabled -}} +{{- if lt (int $autoscaling.minReplicas) 1 -}} +{{- fail "sandboxes.sandboxHost.autoscaling.minReplicas must be at least 1." -}} +{{- end -}} +{{- if lt (int $autoscaling.maxReplicas) (int $autoscaling.minReplicas) -}} +{{- fail "sandboxes.sandboxHost.autoscaling.maxReplicas must be greater than or equal to minReplicas." -}} +{{- end -}} +{{- end -}} +{{- $serviceUrlBaseUrl := .Values.sandboxes.serviceUrlBaseUrl | default "" -}} {{- if $serviceUrlBaseUrl -}} {{- if not (regexMatch "^https?://" $serviceUrlBaseUrl) -}} -{{- fail "config.sandboxes.serviceUrlBaseUrl must start with http:// or https://." -}} +{{- fail "sandboxes.serviceUrlBaseUrl must start with http:// or https://." -}} {{- end -}} {{- if not (regexMatch "^https?://[^/?#]+$" $serviceUrlBaseUrl) -}} -{{- fail "config.sandboxes.serviceUrlBaseUrl must not include a path, query string, or fragment." -}} +{{- fail "sandboxes.serviceUrlBaseUrl must not include a path, query string, or fragment." -}} {{- end -}} {{- $serviceUrlHost := regexReplaceAll "^https?://" $serviceUrlBaseUrl "" -}} {{- if and .Values.ingress.enabled (not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$" $serviceUrlHost)) -}} -{{- fail "config.sandboxes.serviceUrlBaseUrl host must be a DNS hostname without a port when ingress.enabled is true." -}} +{{- fail "sandboxes.serviceUrlBaseUrl host must be a DNS hostname without a port when ingress.enabled is true." -}} {{- end -}} {{- end -}} -{{- $juicefsCSI := .Values.config.sandboxes.juicefs.csi | default dict -}} +{{- $juicefsCSI := .Values.sandboxes.juicefs.csi | default dict -}} {{- if not $juicefsCSI.existingSecretName -}} -{{- $juicefsRedis := .Values.config.sandboxes.juicefs.redis | default dict -}} +{{- $juicefsRedis := .Values.sandboxes.juicefs.redis | default dict -}} {{- if not $juicefsRedis.metaURL -}} -{{- fail "config.sandboxes.juicefs.redis.metaURL is required when config.sandboxes.enabled is true and config.sandboxes.juicefs.csi.existingSecretName is not set." -}} +{{- fail "sandboxes.juicefs.redis.metaURL is required when sandboxes.enabled is true and sandboxes.juicefs.csi.existingSecretName is not set." -}} {{- end -}} -{{- $juicefsName := .Values.config.sandboxes.juicefs.name | default "" -}} +{{- $juicefsName := .Values.sandboxes.juicefs.name | default "" -}} {{- if not $juicefsName -}} -{{- fail "config.sandboxes.juicefs.name is required when config.sandboxes.enabled is true and config.sandboxes.juicefs.csi.existingSecretName is not set." -}} +{{- fail "sandboxes.juicefs.name is required when sandboxes.enabled is true and sandboxes.juicefs.csi.existingSecretName is not set." -}} {{- end -}} {{- if not (regexMatch "^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$" $juicefsName) -}} -{{- fail "config.sandboxes.juicefs.name must be a DNS-label-style JuiceFS volume name: 3-63 characters, lowercase letters, numbers, and hyphens only. Do not use slashes or object-store subpaths here." -}} +{{- fail "sandboxes.juicefs.name must be a DNS-label-style JuiceFS volume name: 3-63 characters, lowercase letters, numbers, and hyphens only. Do not use slashes or object-store subpaths here." -}} {{- end -}} -{{- if not .Values.config.sandboxes.juicefs.bucket -}} -{{- fail "config.sandboxes.juicefs.bucket is required when config.sandboxes.enabled is true and config.sandboxes.juicefs.csi.existingSecretName is not set." -}} +{{- if not .Values.sandboxes.juicefs.bucket -}} +{{- fail "sandboxes.juicefs.bucket is required when sandboxes.enabled is true and sandboxes.juicefs.csi.existingSecretName is not set." -}} {{- end -}} -{{- $juicefsStorage := .Values.config.sandboxes.juicefs.storage | default "" -}} -{{- $juicefsBucket := .Values.config.sandboxes.juicefs.bucket | default "" -}} +{{- $juicefsStorage := .Values.sandboxes.juicefs.storage | default "" -}} +{{- $juicefsBucket := .Values.sandboxes.juicefs.bucket | default "" -}} {{- if not (or (eq $juicefsStorage "s3") (eq $juicefsStorage "gs")) -}} -{{- fail "config.sandboxes.juicefs.storage currently supports only s3 for AWS/EKS and gs for GCP/GKE. Azure-backed sandbox storage is not supported yet." -}} +{{- fail "sandboxes.juicefs.storage currently supports only s3 for AWS/EKS and gs for GCP/GKE. Azure-backed sandbox storage is not supported yet." -}} {{- end -}} {{- if and (eq $juicefsStorage "s3") (hasPrefix "s3://" $juicefsBucket) -}} -{{- fail "config.sandboxes.juicefs.bucket must use a region-explicit S3 endpoint URL such as https://bucket-name.s3.us-west-2.amazonaws.com when config.sandboxes.juicefs.storage is s3. The s3://bucket shorthand makes JuiceFS infer region with GetBucketLocation and is not supported." -}} +{{- fail "sandboxes.juicefs.bucket must use a region-explicit S3 endpoint URL such as https://bucket-name.s3.us-west-2.amazonaws.com when sandboxes.juicefs.storage is s3. The s3://bucket shorthand makes JuiceFS infer region with GetBucketLocation and is not supported." -}} {{- end -}} {{- if and (eq $juicefsStorage "s3") (not (regexMatch "^https?://" $juicefsBucket)) -}} -{{- fail "config.sandboxes.juicefs.bucket must use an HTTP(S) S3 endpoint URL when config.sandboxes.juicefs.storage is s3." -}} -{{- end -}} -{{- end -}} -{{- if and (not .Values.config.existingSecretName) (not .Values.config.disableSecretCreation) (not (include "langsmith.sandboxes.xServiceAuthJwtSecretValue" .)) -}} -{{- fail "config.sandboxes.xServiceAuthJwtSecret is required when config.sandboxes.enabled is true and no chart-managed app secret value is available." -}} -{{- end -}} -{{- if and (not .Values.config.existingSecretName) (not .Values.config.disableSecretCreation) (not .Values.config.sandboxes.callbackSigningJwk) -}} -{{- fail "config.sandboxes.callbackSigningJwk is required when config.sandboxes.enabled is true and the chart manages the LangSmith app Secret. When config.existingSecretName is set, add key sandbox_callback_signing_jwk to the existing Secret instead." -}} +{{- fail "sandboxes.juicefs.bucket must use an HTTP(S) S3 endpoint URL when sandboxes.juicefs.storage is s3." -}} {{- end -}} -{{- if and .Values.config.existingSecretName .Values.config.sandboxes.xServiceAuthJwtSecret -}} -{{- fail "config.sandboxes.xServiceAuthJwtSecret cannot be used when config.existingSecretName is set. Add key sandbox_x_service_auth_jwt_secret to the existing Secret instead." -}} {{- end -}} -{{- if and .Values.config.existingSecretName .Values.config.sandboxes.xServiceAuthJwtSecretPrevious -}} -{{- fail "config.sandboxes.xServiceAuthJwtSecretPrevious cannot be used when config.existingSecretName is set. Add key sandbox_x_service_auth_jwt_secret_previous to the existing Secret instead." -}} +{{- if and (not .Values.config.existingSecretName) (not .Values.config.disableSecretCreation) (not .Values.sandboxes.callbackSigningJwk) -}} +{{- fail "sandboxes.callbackSigningJwk is required when sandboxes.enabled is true and the chart manages the LangSmith app Secret. When config.existingSecretName is set, add key sandbox_callback_signing_jwk to the existing Secret instead." -}} {{- end -}} -{{- if and .Values.config.existingSecretName .Values.config.sandboxes.callbackSigningJwk -}} -{{- fail "config.sandboxes.callbackSigningJwk cannot be used when config.existingSecretName is set. Add key sandbox_callback_signing_jwk to the existing Secret instead." -}} +{{- if and .Values.config.existingSecretName .Values.sandboxes.callbackSigningJwk -}} +{{- fail "sandboxes.callbackSigningJwk cannot be used when config.existingSecretName is set. Add key sandbox_callback_signing_jwk to the existing Secret instead." -}} {{- end -}} -{{- $proxyCaMode := .Values.config.sandboxes.proxyCa.mode -}} +{{- $proxyCaMode := .Values.sandboxes.proxyCa.mode -}} {{- if not (or (eq $proxyCaMode "generatedSecret") (eq $proxyCaMode "existingSecret")) -}} -{{- fail "config.sandboxes.proxyCa.mode must be one of generatedSecret or existingSecret." -}} +{{- fail "sandboxes.proxyCa.mode must be one of generatedSecret or existingSecret." -}} {{- end -}} -{{- if and (eq $proxyCaMode "existingSecret") (not .Values.config.sandboxes.proxyCa.existingSecretName) -}} -{{- fail "config.sandboxes.proxyCa.existingSecretName is required when config.sandboxes.proxyCa.mode is existingSecret." -}} +{{- if and (eq $proxyCaMode "existingSecret") (not .Values.sandboxes.proxyCa.existingSecretName) -}} +{{- fail "sandboxes.proxyCa.existingSecretName is required when sandboxes.proxyCa.mode is existingSecret." -}} {{- end -}} {{- end -}} @@ -532,8 +546,8 @@ AWS Marketplace Helm template verification). {{- if not .Values.engine.intelligenceBaseUrl }} {{- fail "engine.intelligenceBaseUrl is required when engine.enabled is true: the self-hosted Engine has no ANTHROPIC_API_KEY and routes model calls through the LSI gateway for its cloud/region (e.g. AWS: https://beacon.aws.langchain.com/intelligence)." }} {{- end }} -{{- if not .Values.config.sandboxes.enabled }} -{{- fail "engine.enabled is true but config.sandboxes.enabled is false: every Engine run executes in a sandbox, so Engine cannot run without them." }} +{{- if not .Values.sandboxes.enabled }} +{{- fail "engine.enabled is true but sandboxes.enabled is false: every Engine run executes in a sandbox, so Engine cannot run without them." }} {{- end }} {{- if not .Values.config.hostname }} {{- fail "engine.enabled is true but config.hostname is not set: the sandbox runs the langsmith CLI against an externally reachable LangSmith URL, which is derived from it." }} diff --git a/charts/langsmith/tests/engine_insights_agent_test.yaml b/charts/langsmith/tests/engine_insights_agent_test.yaml index 7f31505e0..0bdf769f7 100644 --- a/charts/langsmith/tests/engine_insights_agent_test.yaml +++ b/charts/langsmith/tests/engine_insights_agent_test.yaml @@ -45,6 +45,7 @@ tests: values: - ./values/sandboxes-enabled.yaml set: + config.hostname: langsmith.example.com engine.enabled: true engine.encryptionKey: test-key engine.intelligenceBaseUrl: https://beacon.aws.langchain.com/intelligence @@ -57,7 +58,8 @@ tests: values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.enabled: false + config.hostname: langsmith.example.com + sandboxes.enabled: false engine.enabled: true engine.encryptionKey: test-key engine.intelligenceBaseUrl: https://beacon.aws.langchain.com/intelligence @@ -65,7 +67,7 @@ tests: images.engineInsightsAgentImage.repository: docker.io/langchain/langsmith-insights-engine asserts: - failedTemplate: - errorMessage: "engine.enabled is true but config.sandboxes.enabled is false: every Engine run executes in a sandbox, so Engine cannot run without them." + errorMessage: "engine.enabled is true but sandboxes.enabled is false: every Engine run executes in a sandbox, so Engine cannot run without them." - it: should fail when engine is enabled without a hostname values: diff --git a/charts/langsmith/tests/sandbox_juicefs_csi_driver_test.yaml b/charts/langsmith/tests/sandbox_juicefs_csi_driver_test.yaml index 1dd1d644f..cfe23270d 100644 --- a/charts/langsmith/tests/sandbox_juicefs_csi_driver_test.yaml +++ b/charts/langsmith/tests/sandbox_juicefs_csi_driver_test.yaml @@ -137,7 +137,7 @@ tests: values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.juicefs.csi.controller.serviceAccount.annotations.iam\.gke\.io/gcp-service-account: juicefs-controller@example.iam.gserviceaccount.com + sandboxes.juicefs.csi.controller.serviceAccount.annotations.iam\.gke\.io/gcp-service-account: juicefs-controller@example.iam.gserviceaccount.com template: sandboxes/juicefs-csi-driver/rbac.yaml documentSelector: path: $[?(@.kind == "ServiceAccount")].metadata.name @@ -156,7 +156,7 @@ tests: values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.juicefs.csi.node.serviceAccount.annotations.eks\.amazonaws\.com/role-arn: arn:aws:iam::123456789012:role/juicefs-csi-node + sandboxes.juicefs.csi.node.serviceAccount.annotations.eks\.amazonaws\.com/role-arn: arn:aws:iam::123456789012:role/juicefs-csi-node template: sandboxes/juicefs-csi-driver/rbac.yaml documentSelector: path: $[?(@.kind == "ServiceAccount")].metadata.name @@ -415,7 +415,7 @@ tests: template: validate.yaml asserts: - failedTemplate: - errorMessage: "images.juicefsCSIImage.tag is required when config.sandboxes.enabled is true." + errorMessage: "images.juicefsCSIImage.tag is required when sandboxes.enabled is true." - it: should render JuiceFS CSI node observability annotations values: diff --git a/charts/langsmith/tests/sandboxes_legacy_values_test.yaml b/charts/langsmith/tests/sandboxes_legacy_values_test.yaml new file mode 100644 index 000000000..c6096682f --- /dev/null +++ b/charts/langsmith/tests/sandboxes_legacy_values_test.yaml @@ -0,0 +1,29 @@ +suite: legacy config.sandboxes values +templates: + - validate.yaml +tests: + - it: should fail when sandbox values are still under config.sandboxes + set: + config.existingSecretName: langsmith-secrets + config.oauth.enabled: true + config.authType: oauth + config.sandboxes.enabled: true + asserts: + - failedTemplate: + errorPattern: "config\\.sandboxes has moved to a top-level sandboxes: block" + + - it: should fail even when the legacy block only carries unrelated keys + set: + config.existingSecretName: langsmith-secrets + config.oauth.enabled: true + config.authType: oauth + config.sandboxes.juicefs.bucket: https://b.s3.us-west-2.amazonaws.com + asserts: + - failedTemplate: + errorPattern: "config\\.sandboxes has moved to a top-level sandboxes: block" + + - it: should not fail when sandbox values use the new top-level path + values: + - ./values/sandboxes-enabled.yaml + asserts: + - notFailedTemplate: {} diff --git a/charts/langsmith/tests/sandboxes_test.yaml b/charts/langsmith/tests/sandboxes_test.yaml index 6723f018f..c573fb763 100644 --- a/charts/langsmith/tests/sandboxes_test.yaml +++ b/charts/langsmith/tests/sandboxes_test.yaml @@ -14,7 +14,14 @@ templates: - sandboxes/proxy-ca.yaml - sandboxes/juicefs-csi.yaml - sandboxes/sandbox-host-rbac.yaml + - sandboxes/sandbox-host-service-account.yaml - sandboxes/sandbox-host-deployment.yaml + - sandboxes/sandbox-host-pdb.yaml + - sandboxes/juicefs-csi-driver/csidriver.yaml + - sandboxes/juicefs-csi-driver/rbac.yaml + - sandboxes/juicefs-csi-driver/controller-statefulset.yaml + - sandboxes/juicefs-csi-driver/node-daemonset.yaml + - sandboxes/juicefs-csi-driver/configmap.yaml tests: - it: should not render sandbox runtime resources by default set: @@ -25,6 +32,7 @@ tests: - sandboxes/proxy-ca.yaml - sandboxes/juicefs-csi.yaml - sandboxes/sandbox-host-rbac.yaml + - sandboxes/sandbox-host-service-account.yaml - sandboxes/sandbox-host-deployment.yaml asserts: - hasDocuments: @@ -55,11 +63,11 @@ tests: content: name: LANGSMITH_LICENSE_REQUIRED_CLAIMS value: sandbox_enabled - - contains: + - notContains: path: spec.template.spec.containers[0].env content: name: SANDBOX_K8S_CLUSTER_NAME - value: self-hosted-test + any: true - contains: path: spec.template.spec.containers[0].env content: @@ -67,7 +75,7 @@ tests: valueFrom: secretKeyRef: name: langsmith-secrets - key: sandbox_x_service_auth_jwt_secret + key: api_key_salt optional: false - notContains: path: spec.template.spec.containers[0].env @@ -78,14 +86,14 @@ tests: values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com + sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com template: platform-backend/deployment.yaml asserts: - contains: path: spec.template.spec.containers[0].env content: name: SANDBOX_SERVICE_URLS - value: '{"self-hosted-test":"https://sandbox-services.example.com"}' + value: '{"incluster":"https://sandbox-services.example.com"}' - it: should not add a sandbox service URL ingress rule by default values: @@ -106,7 +114,7 @@ tests: - ./values/sandboxes-enabled.yaml set: config.hostname: langsmith.example.com - config.sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com + sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com ingress.enabled: true ingress.ingressClassName: nginx template: ingress.yaml @@ -127,20 +135,11 @@ tests: path: spec.rules[1].http.paths[0].backend.service.port.number value: 1986 - - it: should wire optional sandbox keys from an existing LangSmith secret into platform backend + - it: should wire the sandbox callback signing JWK from an existing LangSmith secret into platform backend values: - ./values/sandboxes-enabled.yaml template: platform-backend/deployment.yaml asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: SANDBOX_X_SERVICE_AUTH_JWT_SECRET_PREVIOUS - valueFrom: - secretKeyRef: - name: langsmith-secrets - key: sandbox_x_service_auth_jwt_secret_previous - optional: true - contains: path: spec.template.spec.containers[0].env content: @@ -197,24 +196,21 @@ tests: - it: should derive a relative platform endpoint for sandbox dataplane URLs values: - ./values/sandboxes-enabled.yaml - set: - config.hostname: "" template: config-map.yaml asserts: - equal: - path: data.LANGCHAIN_PLATFORM_ENDPOINT + path: data.LANGSMITH_PUBLIC_API_ENDPOINT value: /api - it: should derive a basePath-aware relative platform endpoint for sandbox dataplane URLs values: - ./values/sandboxes-enabled.yaml set: - config.hostname: "" config.basePath: langsmith template: config-map.yaml asserts: - equal: - path: data.LANGCHAIN_PLATFORM_ENDPOINT + path: data.LANGSMITH_PUBLIC_API_ENDPOINT value: /langsmith/api - it: should derive an absolute platform endpoint for sandbox dataplane URLs when hostname is set @@ -225,7 +221,7 @@ tests: template: config-map.yaml asserts: - equal: - path: data.LANGCHAIN_PLATFORM_ENDPOINT + path: data.LANGSMITH_PUBLIC_API_ENDPOINT value: https://langsmith.example.com/api - it: should allow sandboxes when the chart frontend is disabled @@ -244,12 +240,11 @@ tests: config.apiKeySalt: test-api-key-salt config.oauth.enabled: true config.authType: oauth - config.sandboxes.enabled: true - config.sandboxes.clusterName: self-hosted-test - config.sandboxes.xServiceAuthJwtSecret: test-sandbox-jwt - config.sandboxes.callbackSigningJwk: test-callback-jwk - config.sandboxes.juicefs.redis.metaURL: redis://langsmith-redis.default.svc.cluster.local:6379/1 - config.sandboxes.juicefs.bucket: https://langsmith-sandboxes-test.s3.us-west-2.amazonaws.com + sandboxes.enabled: true + sandboxes.sandboxHost.deployment.nodeSelector.sandbox\.langsmith\.com/host: "true" + sandboxes.callbackSigningJwk: test-callback-jwk + sandboxes.juicefs.redis.metaURL: redis://langsmith-redis.default.svc.cluster.local:6379/1 + sandboxes.juicefs.bucket: https://langsmith-sandboxes-test.s3.us-west-2.amazonaws.com images.sandboxHostImage.repository: example.com/langsmith/sandbox-host images.sandboxHostImage.tag: test-sandbox-host insights.encryptionKey: test-insights-encryption-key @@ -260,8 +255,10 @@ tests: path: metadata.name value: RELEASE-NAME-langsmith-secrets - equal: + path: data.api_key_salt + value: dGVzdC1hcGkta2V5LXNhbHQ= + - notExists: path: data.sandbox_x_service_auth_jwt_secret - value: dGVzdC1zYW5kYm94LWp3dA== - equal: path: data.sandbox_callback_signing_jwk value: dGVzdC1jYWxsYmFjay1qd2s= @@ -269,8 +266,6 @@ tests: - it: should keep local_kubernetes LANGCHAIN_ENV in the shared config map values: - ./values/sandboxes-enabled.yaml - set: - config.langchainEnv: chart-env template: config-map.yaml asserts: - equal: @@ -302,76 +297,49 @@ tests: name: SANDBOX_QUEUE_PRIORITY value: "1" - - it: should use configured LANGCHAIN_ENV for sandbox-host + - it: should use the same LANGCHAIN_ENV for sandbox-host as the shared config map values: - ./values/sandboxes-enabled.yaml - set: - config.langchainEnv: chart-env template: sandboxes/sandbox-host-deployment.yaml asserts: - contains: path: spec.template.spec.containers[0].env content: name: LANGCHAIN_ENV - value: chart-env + value: local_kubernetes - - it: should render sandbox-host with host network and rolling update rollout + - it: should sign sandbox callbacks with the public issuer when hostname is set values: - ./values/sandboxes-enabled.yaml + set: + config.hostname: langsmith.example.com template: sandboxes/sandbox-host-deployment.yaml asserts: - - isKind: - of: Deployment - - equal: - path: metadata.name - value: sandbox-host - - equal: - path: metadata.namespace - value: NAMESPACE - - equal: - path: spec.strategy.type - value: RollingUpdate - - equal: - path: spec.strategy.rollingUpdate.maxSurge - value: 0 - - equal: - path: spec.strategy.rollingUpdate.maxUnavailable - value: 1 - - equal: - path: spec.template.spec.hostNetwork - value: true - - equal: - path: spec.template.spec.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution[0].labelSelector.matchLabels.app - value: sandbox-host - - equal: - path: spec.template.spec.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution[0].topologyKey - value: kubernetes.io/hostname - - equal: - path: spec.template.spec.containers[0].ports[0].hostPort - value: 19190 - - notExists: - path: spec.template.metadata.annotations["checksum/juicefs-csi-config"] - - notExists: - path: spec.template.metadata.annotations["checksum/juicefs-csi-driver-config"] - - notExists: - path: spec.template.spec.containers[0].command - - equal: - path: spec.template.spec.nodeSelector - value: - sandbox.langsmith.com/host: "true" - - equal: - path: spec.template.spec.tolerations - value: - - key: sandbox.langsmith.com/host - operator: Equal - value: "true" - effect: NoSchedule - contains: - path: spec.template.spec.containers[0].volumeMounts + path: spec.template.spec.containers[0].env content: - name: proxy-ca - mountPath: /certs - readOnly: true + name: OAUTH_AS_ISSUER + value: https://langsmith.example.com/api + + - it: should omit the callback issuer when no public hostname is configured + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: OAUTH_AS_ISSUER + any: true + + - it: should source sandbox service auth from the shared api key salt + values: + - ./values/sandboxes-enabled.yaml + templates: + - sandboxes/sandbox-host-deployment.yaml + - platform-backend/deployment.yaml + - ingest-queue/deployment.yaml + asserts: - contains: path: spec.template.spec.containers[0].env content: @@ -379,28 +347,125 @@ tests: valueFrom: secretKeyRef: name: langsmith-secrets - key: sandbox_x_service_auth_jwt_secret + key: api_key_salt optional: false + - notContains: + path: spec.template.spec.containers[0].env + content: + name: SANDBOX_X_SERVICE_AUTH_JWT_SECRET_PREVIOUS + any: true + + - it: should append sandbox-host extraEnv + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.deployment.extraEnv: + - name: SANDBOX_HOST_JUICEFS_CLONE_CONCURRENCY + value: "40" + template: sandboxes/sandbox-host-deployment.yaml + asserts: - contains: path: spec.template.spec.containers[0].env content: - name: LANGSMITH_SANDBOX_CALLBACK_SIGNING_JWK - valueFrom: - secretKeyRef: - name: langsmith-secrets - key: sandbox_callback_signing_jwk - optional: true + name: SANDBOX_HOST_JUICEFS_CLONE_CONCURRENCY + value: "40" + + - it: should hardcode replicas and disable pool autoscaling by default + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: spec.replicas + value: 1 - contains: path: spec.template.spec.containers[0].env content: - name: SSRF_ALLOW_PRIVATE_IPS_CALLBACKS + name: SANDBOX_HOST_AUTOSCALE_ENABLED value: "false" + - notContains: + path: spec.template.spec.containers[0].env + content: + name: SANDBOX_HOST_AUTOSCALE_MIN_REPLICAS + any: true + + - it: should drop replicas and render autoscale env when pool autoscaling is enabled + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 8 + targetUtilizationPercent: 60 + headroomHosts: 2 + scaleDownStabilizationSeconds: 600 + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - isNull: + path: spec.replicas + - contains: + path: spec.template.spec.containers[0].env + content: + name: SANDBOX_HOST_AUTOSCALE_ENABLED + value: "true" + - contains: + path: spec.template.spec.containers[0].env + content: + name: SANDBOX_HOST_AUTOSCALE_MIN_REPLICAS + value: "2" + - contains: + path: spec.template.spec.containers[0].env + content: + name: SANDBOX_HOST_AUTOSCALE_MAX_REPLICAS + value: "8" + - contains: + path: spec.template.spec.containers[0].env + content: + name: SANDBOX_HOST_AUTOSCALE_TARGET_UTILIZATION_PERCENT + value: "60" + - contains: + path: spec.template.spec.containers[0].env + content: + name: SANDBOX_HOST_AUTOSCALE_HEADROOM_HOSTS + value: "2" + - contains: + path: spec.template.spec.containers[0].env + content: + name: SANDBOX_HOST_AUTOSCALE_SCALE_DOWN_STABILIZATION_SECONDS + value: "600" - - it: should allow enabling private IP sandbox callbacks on sandbox-host + - it: should let the elected host read and scale its own Deployment + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-rbac.yaml + documentIndex: 0 + asserts: + - isKind: + of: Role + - contains: + path: rules + content: + apiGroups: ["apps"] + resources: ["deployments"] + resourceNames: ["RELEASE-NAME-langsmith-sandbox-host"] + verbs: ["get", "patch"] + + - it: should fail when autoscaling maxReplicas is below minReplicas values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.sandboxHost.allowPrivateIPCallbacks: true + sandboxes.sandboxHost.autoscaling.enabled: true + sandboxes.sandboxHost.autoscaling.minReplicas: 5 + sandboxes.sandboxHost.autoscaling.maxReplicas: 2 + template: validate.yaml + asserts: + - failedTemplate: + errorMessage: "sandboxes.sandboxHost.autoscaling.maxReplicas must be greater than or equal to minReplicas." + + - it: should always allow private IP sandbox callbacks + values: + - ./values/sandboxes-enabled.yaml template: sandboxes/sandbox-host-deployment.yaml asserts: - contains: @@ -413,10 +478,10 @@ tests: values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.sandboxHost.rbac.labels.review: requested - config.sandboxes.sandboxHost.rbac.annotations.review: requested + sandboxes.sandboxHost.rbac.labels.review: requested + sandboxes.sandboxHost.rbac.annotations.review: requested template: sandboxes/sandbox-host-rbac.yaml - documentIndex: 1 + documentIndex: 0 asserts: - equal: path: metadata.labels.review @@ -429,15 +494,15 @@ tests: values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.sandboxHost.rbac.create: false + sandboxes.sandboxHost.rbac.create: false template: sandboxes/sandbox-host-rbac.yaml asserts: - hasDocuments: - count: 1 + count: 0 - it: should render custom sandbox-host scheduling on sandbox-host values: - - ./values/sandboxes-enabled.yaml + - ./values/sandboxes-enabled-no-scheduling.yaml - ./values/sandboxes-custom-scheduling.yaml template: sandboxes/sandbox-host-deployment.yaml asserts: @@ -542,9 +607,9 @@ tests: values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.juicefs.redis.metaURL: "" - config.sandboxes.juicefs.bucket: "" - config.sandboxes.juicefs.csi.existingSecretName: external-juicefs-csi-config + sandboxes.juicefs.redis.metaURL: "" + sandboxes.juicefs.bucket: "" + sandboxes.juicefs.csi.existingSecretName: external-juicefs-csi-config template: sandboxes/juicefs-csi.yaml documentIndex: 2 asserts: @@ -564,9 +629,9 @@ tests: values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.juicefs.redis.metaURL: "" - config.sandboxes.juicefs.bucket: "" - config.sandboxes.juicefs.csi.existingSecretName: external-juicefs-csi-config + sandboxes.juicefs.redis.metaURL: "" + sandboxes.juicefs.bucket: "" + sandboxes.juicefs.csi.existingSecretName: external-juicefs-csi-config template: validate.yaml asserts: - hasDocuments: @@ -586,52 +651,53 @@ tests: config.existingSecretName: langsmith-secrets config.oauth.enabled: true config.authType: oauth - config.sandboxes.enabled: true - config.sandboxes.juicefs.redis.metaURL: redis://langsmith-redis.default.svc.cluster.local:6379/1 - config.sandboxes.juicefs.bucket: s3://langsmith-sandboxes-test + sandboxes.enabled: true + sandboxes.sandboxHost.deployment.nodeSelector.sandbox\.langsmith\.com/host: "true" + sandboxes.juicefs.redis.metaURL: redis://langsmith-redis.default.svc.cluster.local:6379/1 + sandboxes.juicefs.bucket: s3://langsmith-sandboxes-test images.sandboxHostImage.repository: example.com/langsmith/sandbox-host template: validate.yaml asserts: - failedTemplate: - errorMessage: "images.sandboxHostImage.tag is required when config.sandboxes.enabled is true." + errorMessage: "images.sandboxHostImage.tag is required when sandboxes.enabled is true." - it: should fail when JuiceFS name is invalid values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.juicefs.name: Sandbox_JuiceFS + sandboxes.juicefs.name: Sandbox_JuiceFS template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.juicefs.name must be a DNS-label-style JuiceFS volume name: 3-63 characters, lowercase letters, numbers, and hyphens only. Do not use slashes or object-store subpaths here." + errorMessage: "sandboxes.juicefs.name must be a DNS-label-style JuiceFS volume name: 3-63 characters, lowercase letters, numbers, and hyphens only. Do not use slashes or object-store subpaths here." - it: should fail when JuiceFS name contains a slash values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.juicefs.name: sandbox/juicefs + sandboxes.juicefs.name: sandbox/juicefs template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.juicefs.name must be a DNS-label-style JuiceFS volume name: 3-63 characters, lowercase letters, numbers, and hyphens only. Do not use slashes or object-store subpaths here." + errorMessage: "sandboxes.juicefs.name must be a DNS-label-style JuiceFS volume name: 3-63 characters, lowercase letters, numbers, and hyphens only. Do not use slashes or object-store subpaths here." - it: should fail when AWS JuiceFS bucket uses s3 shorthand values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.juicefs.storage: s3 - config.sandboxes.juicefs.bucket: s3://langsmith-sandboxes-test + sandboxes.juicefs.storage: s3 + sandboxes.juicefs.bucket: s3://langsmith-sandboxes-test template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.juicefs.bucket must use a region-explicit S3 endpoint URL such as https://bucket-name.s3.us-west-2.amazonaws.com when config.sandboxes.juicefs.storage is s3. The s3://bucket shorthand makes JuiceFS infer region with GetBucketLocation and is not supported." + errorMessage: "sandboxes.juicefs.bucket must use a region-explicit S3 endpoint URL such as https://bucket-name.s3.us-west-2.amazonaws.com when sandboxes.juicefs.storage is s3. The s3://bucket shorthand makes JuiceFS infer region with GetBucketLocation and is not supported." - it: should allow GCP JuiceFS storage values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.juicefs.storage: gs - config.sandboxes.juicefs.bucket: gs://langsmith-sandboxes-test + sandboxes.juicefs.storage: gs + sandboxes.juicefs.bucket: gs://langsmith-sandboxes-test template: validate.yaml asserts: - hasDocuments: @@ -641,92 +707,72 @@ tests: values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.juicefs.storage: azure + sandboxes.juicefs.storage: azure template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.juicefs.storage currently supports only s3 for AWS/EKS and gs for GCP/GKE. Azure-backed sandbox storage is not supported yet." + errorMessage: "sandboxes.juicefs.storage currently supports only s3 for AWS/EKS and gs for GCP/GKE. Azure-backed sandbox storage is not supported yet." - it: should fail when sandbox service URL base URL does not use HTTP or HTTPS values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.serviceUrlBaseUrl: sandbox-services.example.com + sandboxes.serviceUrlBaseUrl: sandbox-services.example.com template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.serviceUrlBaseUrl must start with http:// or https://." + errorMessage: "sandboxes.serviceUrlBaseUrl must start with http:// or https://." - it: should fail when sandbox service URL base URL contains a path values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com/path + sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com/path template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.serviceUrlBaseUrl must not include a path, query string, or fragment." + errorMessage: "sandboxes.serviceUrlBaseUrl must not include a path, query string, or fragment." - it: should fail when sandbox service URL base URL contains a query string values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com?foo=bar + sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com?foo=bar template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.serviceUrlBaseUrl must not include a path, query string, or fragment." + errorMessage: "sandboxes.serviceUrlBaseUrl must not include a path, query string, or fragment." - it: should fail when sandbox service URL base URL contains a fragment values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com#fragment + sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com#fragment template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.serviceUrlBaseUrl must not include a path, query string, or fragment." + errorMessage: "sandboxes.serviceUrlBaseUrl must not include a path, query string, or fragment." - it: should fail when chart ingress would render an invalid sandbox service URL host values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com:8443 + sandboxes.serviceUrlBaseUrl: https://sandbox-services.example.com:8443 ingress.enabled: true template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.serviceUrlBaseUrl host must be a DNS hostname without a port when ingress.enabled is true." - - - it: should fail when existing app Secret is used with chart-provided sandbox service auth secret - values: - - ./values/sandboxes-enabled.yaml - set: - config.sandboxes.xServiceAuthJwtSecret: test-sandbox-jwt - template: validate.yaml - asserts: - - failedTemplate: - errorMessage: "config.sandboxes.xServiceAuthJwtSecret cannot be used when config.existingSecretName is set. Add key sandbox_x_service_auth_jwt_secret to the existing Secret instead." - - - it: should fail when existing app Secret is used with chart-provided previous sandbox service auth secret - values: - - ./values/sandboxes-enabled.yaml - set: - config.sandboxes.xServiceAuthJwtSecretPrevious: test-previous-sandbox-jwt - template: validate.yaml - asserts: - - failedTemplate: - errorMessage: "config.sandboxes.xServiceAuthJwtSecretPrevious cannot be used when config.existingSecretName is set. Add key sandbox_x_service_auth_jwt_secret_previous to the existing Secret instead." + errorMessage: "sandboxes.serviceUrlBaseUrl host must be a DNS hostname without a port when ingress.enabled is true." - it: should fail when existing app Secret is used with chart-provided sandbox callback signing JWK values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.callbackSigningJwk: test-callback-jwk + sandboxes.callbackSigningJwk: test-callback-jwk template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.callbackSigningJwk cannot be used when config.existingSecretName is set. Add key sandbox_callback_signing_jwk to the existing Secret instead." + errorMessage: "sandboxes.callbackSigningJwk cannot be used when config.existingSecretName is set. Add key sandbox_callback_signing_jwk to the existing Secret instead." - it: should fail when chart-managed app Secret is missing sandbox callback signing JWK set: @@ -734,11 +780,10 @@ tests: config.apiKeySalt: test-api-key-salt config.oauth.enabled: true config.authType: oauth - config.sandboxes.enabled: true - config.sandboxes.clusterName: self-hosted-test - config.sandboxes.xServiceAuthJwtSecret: test-sandbox-jwt - config.sandboxes.juicefs.redis.metaURL: redis://langsmith-redis.default.svc.cluster.local:6379/1 - config.sandboxes.juicefs.bucket: https://langsmith-sandboxes-test.s3.us-west-2.amazonaws.com + sandboxes.enabled: true + sandboxes.sandboxHost.deployment.nodeSelector.sandbox\.langsmith\.com/host: "true" + sandboxes.juicefs.redis.metaURL: redis://langsmith-redis.default.svc.cluster.local:6379/1 + sandboxes.juicefs.bucket: https://langsmith-sandboxes-test.s3.us-west-2.amazonaws.com images.sandboxHostImage.repository: example.com/langsmith/sandbox-host images.sandboxHostImage.tag: test-sandbox-host insights.encryptionKey: test-insights-encryption-key @@ -746,17 +791,17 @@ tests: template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.callbackSigningJwk is required when config.sandboxes.enabled is true and the chart manages the LangSmith app Secret. When config.existingSecretName is set, add key sandbox_callback_signing_jwk to the existing Secret instead." + errorMessage: "sandboxes.callbackSigningJwk is required when sandboxes.enabled is true and the chart manages the LangSmith app Secret. When config.existingSecretName is set, add key sandbox_callback_signing_jwk to the existing Secret instead." - it: should fail when sandbox proxy CA mode is unsupported values: - ./values/sandboxes-enabled.yaml set: - config.sandboxes.proxyCa.mode: certManager + sandboxes.proxyCa.mode: certManager template: validate.yaml asserts: - failedTemplate: - errorMessage: "config.sandboxes.proxyCa.mode must be one of generatedSecret or existingSecret." + errorMessage: "sandboxes.proxyCa.mode must be one of generatedSecret or existingSecret." - it: should fail when raw sandbox env vars conflict with chart-managed platform env values: @@ -766,3 +811,424 @@ tests: asserts: - failedTemplate: errorMessage: "Duplicate keys detected: [SANDBOX_RUNTIME_V2]" + + - it: should gate sandbox-host readiness on the host listener without a liveness probe + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.tcpSocket.port + value: http + - isNull: + path: spec.template.spec.containers[0].livenessProbe + + - it: should not render a sandbox-host PDB by default + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-pdb.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should cap concurrent sandbox-host evictions when the PDB is enabled + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.pdb.enabled: true + template: sandboxes/sandbox-host-pdb.yaml + asserts: + - isKind: + of: PodDisruptionBudget + - equal: + path: metadata.name + value: RELEASE-NAME-langsmith-sandbox-host + - equal: + path: spec.maxUnavailable + value: 1 + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/component"] + value: RELEASE-NAME-langsmith-sandbox-host + + - it: should let minAvailable replace maxUnavailable in the sandbox-host PDB + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.pdb: + enabled: true + minAvailable: 2 + template: sandboxes/sandbox-host-pdb.yaml + asserts: + - equal: + path: spec.minAvailable + value: 2 + - isNull: + path: spec.maxUnavailable + + - it: should skip the bundled JuiceFS CSI driver when the cluster already runs one + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.juicefs.csi.install: false + templates: + - sandboxes/juicefs-csi-driver/csidriver.yaml + - sandboxes/juicefs-csi-driver/rbac.yaml + - sandboxes/juicefs-csi-driver/controller-statefulset.yaml + - sandboxes/juicefs-csi-driver/node-daemonset.yaml + - sandboxes/juicefs-csi-driver/configmap.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should still render sandbox volumes when the bundled JuiceFS CSI driver is skipped + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.juicefs.csi.install: false + template: sandboxes/juicefs-csi.yaml + asserts: + - hasDocuments: + count: 5 + + - it: should not keep the CSIDriver behind on uninstall + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/juicefs-csi-driver/csidriver.yaml + asserts: + - isKind: + of: CSIDriver + - isNull: + path: metadata.annotations["helm.sh/resource-policy"] + + - it: should release-prefix the sandbox-host Deployment and tell the host its name + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.name: hosts + templates: + - sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-langsmith-hosts + - contains: + path: spec.template.spec.containers[0].env + content: + name: SANDBOX_HOST_DEPLOYMENT_NAME + value: RELEASE-NAME-langsmith-hosts + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/component"] + value: RELEASE-NAME-langsmith-hosts + - equal: + path: spec.template.spec.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution[0].labelSelector.matchLabels["app.kubernetes.io/component"] + value: RELEASE-NAME-langsmith-hosts + + - it: should scope the sandbox-host autoscaler RBAC to the release-prefixed Deployment + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.name: hosts + template: sandboxes/sandbox-host-rbac.yaml + documentIndex: 0 + asserts: + - contains: + path: rules + content: + apiGroups: ["apps"] + resources: ["deployments"] + resourceNames: ["RELEASE-NAME-langsmith-hosts"] + verbs: ["get", "patch"] + + - it: should name the sandbox-host Role after the component like other rbac templates + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-rbac.yaml + documentIndex: 0 + asserts: + - isKind: + of: Role + - equal: + path: metadata.name + value: RELEASE-NAME-langsmith-sandbox-host + - equal: + path: metadata.namespace + value: NAMESPACE + + - it: should bind the sandbox-host Role to its ServiceAccount + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-rbac.yaml + documentIndex: 1 + asserts: + - isKind: + of: RoleBinding + - equal: + path: roleRef.name + value: RELEASE-NAME-langsmith-sandbox-host + - equal: + path: subjects[0].name + value: RELEASE-NAME-langsmith-sandbox-host + - equal: + path: subjects[0].namespace + value: NAMESPACE + + - it: should render the sandbox-host ServiceAccount from its own template + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-service-account.yaml + asserts: + - isKind: + of: ServiceAccount + - equal: + path: metadata.name + value: RELEASE-NAME-langsmith-sandbox-host + - equal: + path: automountServiceAccountToken + value: true + + - it: should skip the sandbox-host ServiceAccount when create is false + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.serviceAccount.create: false + template: sandboxes/sandbox-host-service-account.yaml + asserts: + - hasDocuments: + count: 0 + + + - it: should trust a custom CA bundle in sandbox-host + values: + - ./values/sandboxes-enabled.yaml + set: + config.customCa.secretName: my-ca + config.customCa.secretKey: ca.crt + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: SSL_CERT_FILE + value: /etc/ssl/certs/custom-ca-certificates.crt + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: langsmith-custom-ca + mountPath: /etc/ssl/certs/custom-ca-certificates.crt + subPath: ca-certificates.crt + readOnly: true + - contains: + path: spec.template.spec.volumes + content: + name: langsmith-custom-ca + secret: + secretName: my-ca + items: + - key: ca.crt + path: ca-certificates.crt + + - it: should not mount a custom CA in sandbox-host by default + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: SSL_CERT_FILE + any: true + - notContains: + path: spec.template.spec.volumes + content: + name: langsmith-custom-ca + any: true + + - it: should put arbitrary sandbox-host labels on the Deployment and its pods + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.deployment.labels: + app: my-sandbox-host + team: infra + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: metadata.labels.app + value: my-sandbox-host + - equal: + path: spec.template.metadata.labels.app + value: my-sandbox-host + - equal: + path: spec.template.metadata.labels.team + value: infra + # The selector and anti-affinity must keep using the chart's own labels. + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/component"] + value: RELEASE-NAME-langsmith-sandbox-host + - equal: + path: spec.selector.matchLabels["app.kubernetes.io/component"] + value: RELEASE-NAME-langsmith-sandbox-host + + - it: should not let user labels clobber the chart labels the selector depends on + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.deployment.labels: + app.kubernetes.io/component: hijacked + app.kubernetes.io/name: hijacked + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/component"] + value: RELEASE-NAME-langsmith-sandbox-host + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/name"] + value: langsmith + + - it: should label sandbox-host pods app: like the other sandbox templates + values: + - ./values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: metadata.labels.app + value: sandbox-host + - equal: + path: spec.template.metadata.labels.app + value: sandbox-host + + - it: should follow the component name into the app label + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.name: hosts + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: spec.template.metadata.labels.app + value: hosts + + - it: should pin the sandbox-host port and JuiceFS mount, ignoring stale overrides + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.containerPort: 19200 + sandboxes.juicefs.csi.mountPath: /elsewhere + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].ports[0].containerPort + value: 19190 + - equal: + path: spec.template.spec.containers[0].ports[0].hostPort + value: 19190 + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: juicefs + mountPath: /juicefs + + - it: should render the public API endpoint even with sandboxes off + set: + config.existingSecretName: langsmith-secrets + config.oauth.enabled: true + config.authType: oauth + config.hostname: langsmith.example.com + template: config-map.yaml + asserts: + - equal: + path: data.LANGSMITH_PUBLIC_API_ENDPOINT + value: https://langsmith.example.com/api + + - it: should accept the standard sandbox-host deployment knobs + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.deployment.podSecurityContext: + fsGroup: 2000 + sandboxes.sandboxHost.deployment.volumes: + - name: extra + emptyDir: {} + sandboxes.sandboxHost.deployment.volumeMounts: + - name: extra + mountPath: /extra + sandboxes.sandboxHost.deployment.initContainers: + - name: warm + image: busybox + sandboxes.sandboxHost.deployment.sidecars: + - name: shipper + image: busybox + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 2000 + - contains: + path: spec.template.spec.volumes + content: + name: extra + emptyDir: {} + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: extra + mountPath: /extra + - equal: + path: spec.template.spec.initContainers[0].name + value: warm + - equal: + path: spec.template.spec.containers[1].name + value: shipper + + - it: should let the readiness probe be tuned + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.deployment.readinessProbe.initialDelaySeconds: 60 + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].readinessProbe.initialDelaySeconds + value: 60 + - equal: + path: spec.template.spec.containers[0].readinessProbe.tcpSocket.port + value: http + + - it: should refuse to run sandbox-host unprivileged + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.deployment.securityContext.privileged: false + template: validate.yaml + asserts: + - failedTemplate: + errorMessage: "sandboxes.sandboxHost.deployment.securityContext.privileged must be true: sandbox-host manages microVMs, network namespaces and mounts." + + - it: should refuse a read-only root filesystem for sandbox-host + values: + - ./values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.deployment.securityContext.readOnlyRootFilesystem: true + template: validate.yaml + asserts: + - failedTemplate: + errorMessage: "sandboxes.sandboxHost.deployment.securityContext.readOnlyRootFilesystem must not be true: sandbox-host unpacks the guest tools image on startup." + + - it: does not render the sandbox-host priorityClassName by default + values: + - values/sandboxes-enabled.yaml + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - notExists: + path: spec.template.spec.priorityClassName + + - it: renders a configured sandbox-host priorityClassName + values: + - values/sandboxes-enabled.yaml + set: + sandboxes.sandboxHost.deployment.priorityClassName: system-node-critical + template: sandboxes/sandbox-host-deployment.yaml + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: system-node-critical diff --git a/charts/langsmith/tests/values/sandboxes-custom-scheduling.yaml b/charts/langsmith/tests/values/sandboxes-custom-scheduling.yaml index 2e034c5ea..79efc0712 100644 --- a/charts/langsmith/tests/values/sandboxes-custom-scheduling.yaml +++ b/charts/langsmith/tests/values/sandboxes-custom-scheduling.yaml @@ -1,11 +1,10 @@ -config: - sandboxes: - sandboxHost: - deployment: - nodeSelector: - sandbox.example.com/pool: hosts - tolerations: - - key: sandbox.example.com/pool - operator: Equal - value: hosts - effect: NoSchedule +sandboxes: + sandboxHost: + deployment: + nodeSelector: + sandbox.example.com/pool: hosts + tolerations: + - key: sandbox.example.com/pool + operator: Equal + value: hosts + effect: NoSchedule diff --git a/charts/langsmith/tests/values/sandboxes-enabled-no-scheduling.yaml b/charts/langsmith/tests/values/sandboxes-enabled-no-scheduling.yaml new file mode 100644 index 000000000..da881b69d --- /dev/null +++ b/charts/langsmith/tests/values/sandboxes-enabled-no-scheduling.yaml @@ -0,0 +1,27 @@ +config: + existingSecretName: langsmith-secrets + oauth: + enabled: true + authType: oauth + +images: + sandboxHostImage: + repository: example.com/langsmith/sandbox-host + tag: test-sandbox-host + +sandboxes: + enabled: true + quotas: + maxCpuCores: 16 + maxMemoryGb: 64 + minEphemeralStorageGb: 1 + maxEphemeralStorageGib: 100 + maxSandboxes: 1000 + proxyCa: + mode: generatedSecret + juicefs: + name: sandbox-juicefs + storage: s3 + bucket: https://langsmith-sandboxes-test.s3.us-west-2.amazonaws.com + redis: + metaURL: redis://langsmith-redis.default.svc.cluster.local:6379/1 diff --git a/charts/langsmith/tests/values/sandboxes-enabled.yaml b/charts/langsmith/tests/values/sandboxes-enabled.yaml index 6138b9ab7..c27160b11 100644 --- a/charts/langsmith/tests/values/sandboxes-enabled.yaml +++ b/charts/langsmith/tests/values/sandboxes-enabled.yaml @@ -1,28 +1,31 @@ config: existingSecretName: langsmith-secrets - hostname: langsmith.example.com oauth: enabled: true authType: oauth - sandboxes: - enabled: true - clusterName: self-hosted-test - limits: - maxCpuCores: 16 - maxMemoryGb: 64 - minEphemeralStorageGb: 1 - maxEphemeralStorageGb: 100 - maxSandboxes: 1000 - proxyCa: - mode: generatedSecret - juicefs: - name: sandbox-juicefs - storage: s3 - bucket: https://langsmith-sandboxes-test.s3.us-west-2.amazonaws.com - redis: - metaURL: redis://langsmith-redis.default.svc.cluster.local:6379/1 images: sandboxHostImage: repository: example.com/langsmith/sandbox-host tag: test-sandbox-host + +sandboxes: + enabled: true + sandboxHost: + deployment: + nodeSelector: + sandbox.langsmith.com/host: "true" + quotas: + maxCpuCores: 16 + maxMemoryGb: 64 + minEphemeralStorageGb: 1 + maxEphemeralStorageGib: 100 + maxSandboxes: 1000 + proxyCa: + mode: generatedSecret + juicefs: + name: sandbox-juicefs + storage: s3 + bucket: https://langsmith-sandboxes-test.s3.us-west-2.amazonaws.com + redis: + metaURL: redis://langsmith-redis.default.svc.cluster.local:6379/1 diff --git a/charts/langsmith/tests/values/sandboxes-juicefs-observability.yaml b/charts/langsmith/tests/values/sandboxes-juicefs-observability.yaml index 4cdf25d34..f476a50e1 100644 --- a/charts/langsmith/tests/values/sandboxes-juicefs-observability.yaml +++ b/charts/langsmith/tests/values/sandboxes-juicefs-observability.yaml @@ -1,26 +1,25 @@ -config: - sandboxes: - juicefs: - csi: - controller: +sandboxes: + juicefs: + csi: + controller: + annotations: + prometheus.io/scrape: "true" + prometheus.io/path: /metrics + prometheus.io/port: "9567" + node: + annotations: + prometheus.io/scrape: "true" + prometheus.io/path: /metrics + prometheus.io/port: "9567" + mountPodPatch: + - labels: + admission.datadoghq.com/enabled: "false" annotations: + ad.datadoghq.com/juicefs.logs: '[{"source":"juicefs","service":"juicefs-mount"}]' prometheus.io/scrape: "true" prometheus.io/path: /metrics prometheus.io/port: "9567" - node: - annotations: - prometheus.io/scrape: "true" - prometheus.io/path: /metrics - prometheus.io/port: "9567" - mountPodPatch: - - labels: - admission.datadoghq.com/enabled: "false" - annotations: - ad.datadoghq.com/juicefs.logs: '[{"source":"juicefs","service":"juicefs-mount"}]' - prometheus.io/scrape: "true" - prometheus.io/path: /metrics - prometheus.io/port: "9567" - mountOptions: - - cache-dir=/var/cache/juicefs-csi - - cache-size=51200 - - metrics=0.0.0.0:9567 + mountOptions: + - cache-dir=/var/cache/juicefs-csi + - cache-size=51200 + - metrics=0.0.0.0:9567 diff --git a/charts/langsmith/values.yaml b/charts/langsmith/values.yaml index d9e39c48a..c5e67b3bb 100644 --- a/charts/langsmith/values.yaml +++ b/charts/langsmith/values.yaml @@ -137,7 +137,7 @@ images: juicefsMountImage: repository: "docker.io/juicedata/mount" pullPolicy: IfNotPresent - tag: "" + tag: "ce-v1.3.0" ingress: enabled: false @@ -164,6 +164,144 @@ istioGateway: labels: {} +sandboxes: + enabled: false + # -- Private JWK signing sandbox callbacks, or key `sandbox_callback_signing_jwk` in config.existingSecretName. Needs config.hostname for the issuer, or signing fails closed. + callbackSigningJwk: "" + # -- Base URL for reaching HTTP services inside sandboxes. Needs wildcard DNS and TLS for `*.`; with ingress.enabled the chart adds the wildcard rule. http(s) origin only, no path. + serviceUrlBaseUrl: "" + quotas: + maxCpuCores: 16 + maxMemoryGb: 64 + minEphemeralStorageGb: 1 + maxEphemeralStorageGib: 100 + maxSandboxes: 1000 + proxyCa: + # -- generatedSecret creates a self-signed CA Secret with Helm and reuses it on live upgrades via lookup. In pure render/GitOps workflows where lookup cannot read the cluster, generatedSecret produces different cert material on each render; use existingSecret for deterministic manifests. + mode: "generatedSecret" + secretName: "smithbox-proxy-ca" + # -- Existing TLS Secret containing tls.crt and tls.key for the sandbox proxy CA. This Secret can be created manually, by cert-manager, or by another external process. + existingSecretName: "" + juicefs: + # -- JuiceFS volume name. Use a flat DNS-label-style name only; slashes and object-store subpaths are not supported here. JuiceFS stores objects under `/` inside the configured bucket. Also used to scope JuiceFS CSI RBAC for the generated mount Secret, so keep it aligned with the `name` key when using an existing CSI config Secret. + name: "sandbox-juicefs" + # -- Object storage backend used by JuiceFS for sandboxes. Currently supported values are `s3` for AWS/EKS and `gs` for GCP/GKE. Azure-backed sandbox storage is not supported yet. + storage: "s3" + # -- Object storage bucket/root URL used by JuiceFS. For AWS S3, use a region-explicit endpoint such as `https://bucket-name.s3.us-west-2.amazonaws.com`; do not use the `s3://bucket-name` shorthand because JuiceFS then infers region with GetBucketLocation. For GCS, use `gs://bucket-name`. Use `sandboxes.juicefs.name` for JuiceFS volume isolation instead of deployment-specific bucket paths. + bucket: "" + redis: + # -- JuiceFS Redis metadata URL. Redis metadata engines must use maxmemory-policy noeviction. For Redis Cluster, the `/DB` path is used by JuiceFS as a hash-tag key prefix rather than a Redis logical database. + metaURL: "" + csi: + # -- Install the bundled JuiceFS CSI driver. Set to false when the cluster already runs one; the chart then renders only the sandbox PV/PVCs and CSI config Secret, bound to the existing driver. + install: true + # -- Name of the chart-managed JuiceFS CSI config Secret. The bundled JuiceFS CSI setup is intended only for JuiceFS PVs in the namespace where this chart is installed. Chart-managed Secret/config changes roll only the JuiceFS CSI controller/node pods through checksum annotations. Helm cannot hash externally managed Secret contents or directly refresh CSI-created JuiceFS mount pods; delete affected JuiceFS mount pods so they are recreated with the updated Secret. + configSecretName: "juicefs-csi-config" + # -- Existing Secret containing JuiceFS CSI config keys `name`, `metaurl`, `storage`, and `bucket`. When set, this chart does not render the CSI config Secret and `sandboxes.juicefs.name`, `sandboxes.juicefs.storage`, `sandboxes.juicefs.bucket`, and `sandboxes.juicefs.redis.metaURL` are not used for the CSI config. + existingSecretName: "" + controller: + # -- Annotations applied to the JuiceFS CSI controller StatefulSet and pod template. + annotations: {} + serviceAccount: + # -- Annotations applied to the JuiceFS CSI controller ServiceAccount. + annotations: {} + node: + # -- Annotations applied to the JuiceFS CSI node DaemonSet and pod template. + annotations: {} + serviceAccount: + # -- Annotations applied to the JuiceFS CSI node ServiceAccount. Use this for workload identity annotations such as AWS IRSA or GCP Workload Identity. + annotations: {} + # -- Mount pod patches used by the JuiceFS CSI driver for sandbox volumes. Override only when tuning JuiceFS mount behavior or observability. + mountPodPatch: + - mountOptions: + - cache-dir=/var/cache/juicefs-csi + - cache-size=51200 + - buffer-size=300 + - prefetch=3 + - metrics=0.0.0.0:9567 + resources: + requests: + cpu: "2" + memory: 4Gi + limits: + cpu: "2" + memory: 4Gi + - pvcSelector: + matchLabels: + juicefs.langsmith.com/cache: ssd + mountOptions: + - cache-dir=/var/cache/juicefs-csi + - cache-size=307200 + pvName: "smithbox-juicefs-csi" + pvcName: "smithbox-juicefs-csi" + sandboxHost: + # -- Component name for sandbox-host. Resources are named `-`, which the chart passes to the host as SANDBOX_HOST_DEPLOYMENT_NAME. + name: "sandbox-host" + deployment: + labels: {} + annotations: {} + podAnnotations: {} + # -- Number of sandbox-host replicas, i.e. sandbox nodes backing the pool. One host per node, so do not exceed the node count. Ignored when autoscaling is enabled. + replicas: 1 + terminationGracePeriodSeconds: 300 + priorityClassName: "" + # -- Node selector for sandbox-host pods. Required: host pods must land on KVM-capable nodes, and a toleration alone does not attract them there. + nodeSelector: {} + # -- Tolerations for sandbox-host pods. The default expects sandbox-host nodes tainted `sandbox.langsmith.com/host=true:NoSchedule`; override this if your sandbox node pool uses different taints. + tolerations: + - key: sandbox.langsmith.com/host + operator: Equal + value: "true" + effect: NoSchedule + resources: + requests: + cpu: "2" + memory: 2Gi + # -- Container security context. Must stay privileged, with a writable root: the host needs netns, iptables and mount propagation, and unpacks the guest tools image. + securityContext: + privileged: true + podSecurityContext: {} + extraEnv: [] + volumes: [] + volumeMounts: [] + initContainers: [] + sidecars: [] + # -- Readiness probe. The host binds its listener last, so accepting connections gates the rollout. No liveness probe: a restart mid-suspend destroys running microVMs. + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + # -- Sandbox host pool autoscaling. There is no HPA or KEDA object: the elected sandbox-host resizes the Deployment itself. Unmanaged `sandbox-host.smith.langchain.com/autoscale-*` annotations override these live. + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 10 + # -- Target share of pool CPU capacity committed to sandboxes before scaling up. + targetUtilizationPercent: 70 + # -- Spare empty hosts kept above current demand so a sandbox create rarely waits for a cold node. + headroomHosts: 1 + # -- Hold the replica target at its recent peak this long before shrinking, so load dips do not thrash the pool. + scaleDownStabilizationSeconds: 300 + # -- Disruption budget for sandbox-host, capping concurrent evictions since each drain suspends every microVM on that host. maxUnavailable keeps a small pool drainable; setting minAvailable overrides it. + pdb: + enabled: false + maxUnavailable: 1 + labels: {} + annotations: {} + serviceAccount: + create: true + name: "" + labels: {} + annotations: {} + automountServiceAccountToken: true + rbac: + create: true + labels: {} + annotations: {} + fleet: enabled: false namePrefix: "standalone-fleet" @@ -938,131 +1076,6 @@ config: # This supports the same-cluster sandbox-host architecture only. # Self-hosted sandboxes are currently supported on AWS/EKS and GCP/GKE. Azure/AKS is supported by the base LangSmith chart, but not by sandboxes yet. # Enabling sandboxes also installs the JuiceFS CSI driver. The CSI driver creates cluster-scoped resources, so only one sandbox-enabled LangSmith release should manage it per Kubernetes cluster. Do not enable sandboxes in multiple releases in the same cluster, or alongside an independently managed JuiceFS CSI installation, unless you have verified ownership of those cluster-scoped resources. - sandboxes: - enabled: false - # -- Logical Kubernetes cluster name sent to LangSmith sandbox workers. - clusterName: "" - # -- Shared service-auth secret override for LangSmith <-> sandbox runtime calls when this chart creates the LangSmith app Secret. If unset, the chart uses the configured API key salt or LangSmith license key as the chart-managed value. - # When config.existingSecretName is set, add key `sandbox_x_service_auth_jwt_secret` to that Secret instead. - xServiceAuthJwtSecret: "" - # -- Optional previous service-auth secret used during rotation. Used only when this chart creates the LangSmith app Secret. - # When config.existingSecretName is set, add optional key `sandbox_x_service_auth_jwt_secret_previous` to that Secret to enable rotation. - xServiceAuthJwtSecretPrevious: "" - # -- Required private JWK for sandbox callback signing when this chart creates the LangSmith app Secret. - # When config.existingSecretName is set, add key `sandbox_callback_signing_jwk` to that Secret instead. - callbackSigningJwk: "" - # -- Optional base URL used to generate browser/programmatic service URLs for HTTP services running inside sandboxes. - # When set, the chart renders SANDBOX_SERVICE_URLS for config.sandboxes.clusterName. - # Requires wildcard DNS and TLS for `*.`. When ingress.enabled is true, the chart also adds a wildcard ingress rule to the LangSmith platform backend. - # Must be an http:// or https:// URL without a path, query string, or fragment. When ingress.enabled is true, the host must be a DNS hostname without a port. - serviceUrlBaseUrl: "" - limits: - maxCpuCores: 16 - maxMemoryGb: 64 - minEphemeralStorageGb: 1 - maxEphemeralStorageGb: 100 - maxSandboxes: 1000 - proxyCa: - # -- generatedSecret creates a self-signed CA Secret with Helm and reuses it on live upgrades via lookup. In pure render/GitOps workflows where lookup cannot read the cluster, generatedSecret produces different cert material on each render; use existingSecret for deterministic manifests. - mode: "generatedSecret" - secretName: "smithbox-proxy-ca" - # -- Existing TLS Secret containing tls.crt and tls.key for the sandbox proxy CA. This Secret can be created manually, by cert-manager, or by another external process. - existingSecretName: "" - juicefs: - # -- JuiceFS volume name. Use a flat DNS-label-style name only; slashes and object-store subpaths are not supported here. JuiceFS stores objects under `/` inside the configured bucket. Also used to scope JuiceFS CSI RBAC for the generated mount Secret, so keep it aligned with the `name` key when using an existing CSI config Secret. - name: "sandbox-juicefs" - # -- Object storage backend used by JuiceFS for sandboxes. Currently supported values are `s3` for AWS/EKS and `gs` for GCP/GKE. Azure-backed sandbox storage is not supported yet. - storage: "s3" - # -- Object storage bucket/root URL used by JuiceFS. For AWS S3, use a region-explicit endpoint such as `https://bucket-name.s3.us-west-2.amazonaws.com`; do not use the `s3://bucket-name` shorthand because JuiceFS then infers region with GetBucketLocation. For GCS, use `gs://bucket-name`. Use `config.sandboxes.juicefs.name` for JuiceFS volume isolation instead of deployment-specific bucket paths. - bucket: "" - redis: - # -- JuiceFS Redis metadata URL. Redis metadata engines must use maxmemory-policy noeviction. For Redis Cluster, the `/DB` path is used by JuiceFS as a hash-tag key prefix rather than a Redis logical database. - metaURL: "" - csi: - # -- Name of the chart-managed JuiceFS CSI config Secret. The bundled JuiceFS CSI setup is intended only for JuiceFS PVs in the namespace where this chart is installed. Chart-managed Secret/config changes roll only the JuiceFS CSI controller/node pods through checksum annotations. Helm cannot hash externally managed Secret contents or directly refresh CSI-created JuiceFS mount pods; delete affected JuiceFS mount pods so they are recreated with the updated Secret. - configSecretName: "juicefs-csi-config" - # -- Existing Secret containing JuiceFS CSI config keys `name`, `metaurl`, `storage`, and `bucket`. When set, this chart does not render the CSI config Secret and `config.sandboxes.juicefs.name`, `config.sandboxes.juicefs.storage`, `config.sandboxes.juicefs.bucket`, and `config.sandboxes.juicefs.redis.metaURL` are not used for the CSI config. - existingSecretName: "" - controller: - # -- Annotations applied to the JuiceFS CSI controller StatefulSet and pod template. - annotations: {} - serviceAccount: - # -- Annotations applied to the JuiceFS CSI controller ServiceAccount. - annotations: {} - node: - # -- Annotations applied to the JuiceFS CSI node DaemonSet and pod template. - annotations: {} - serviceAccount: - # -- Annotations applied to the JuiceFS CSI node ServiceAccount. Use this for workload identity annotations such as AWS IRSA or GCP Workload Identity. - annotations: {} - # -- Mount pod patches used by the JuiceFS CSI driver for sandbox volumes. Override only when tuning JuiceFS mount behavior or observability. - mountPodPatch: - - mountOptions: - - cache-dir=/var/cache/juicefs-csi - - cache-size=51200 - - buffer-size=300 - - prefetch=3 - - metrics=0.0.0.0:9567 - resources: - requests: - cpu: "2" - memory: 4Gi - limits: - cpu: "2" - memory: 4Gi - - pvcSelector: - matchLabels: - juicefs.langsmith.com/cache: ssd - mountOptions: - - cache-dir=/var/cache/juicefs-csi - - cache-size=307200 - mountPath: "/juicefs" - pvName: "smithbox-juicefs-csi" - pvcName: "smithbox-juicefs-csi" - sandboxHost: - name: "sandbox-host" - containerPort: 19190 - # -- Whether sandbox callback validation should allow private IP and localhost targets. This renders SSRF_ALLOW_PRIVATE_IPS_CALLBACKS for sandbox-host. - allowPrivateIPCallbacks: false - deployment: - labels: {} - annotations: {} - podAnnotations: {} - # -- Optional initial sandbox-host Deployment replica count. Leave null to let Kubernetes default the initial replica count and avoid Helm reconciling runtime scaling. - # Multiple sandbox-host replicas are kept one per node by chart-managed required pod anti-affinity on `kubernetes.io/hostname` plus the sandbox-host `hostPort`. - # The chart also uses a no-surge rolling update strategy (`maxSurge: 0`, `maxUnavailable: 1`) so image updates roll hosts one node at a time instead of creating unschedulable surge pods. - replicas: null - terminationGracePeriodSeconds: 300 - # -- Node selector for sandbox-host pods. Empty uses the default selector `sandbox.langsmith.com/host=true`; set this map to replace the default if your sandbox node pool uses different labels. - nodeSelector: {} - # -- Tolerations for sandbox-host pods. The default expects sandbox-host nodes tainted `sandbox.langsmith.com/host=true:NoSchedule`; override this if your sandbox node pool uses different taints. - tolerations: - - key: sandbox.langsmith.com/host - operator: Equal - value: "true" - effect: NoSchedule - resources: - requests: - cpu: "2" - memory: 2Gi - securityContext: - privileged: true - serviceAccount: - create: true - name: "" - labels: {} - annotations: {} - automountServiceAccountToken: true - rbac: - create: true - labels: {} - annotations: {} - - # DEPRECATED: use the top-level fleet block (fleet/fleetToolServer/ - # fleetTriggerServer) instead. These keys are retained only as fallback - # defaults for the fleet block and may be removed in a future chart version. - # Agent Builder configuration - UI for creating and managing agents - # Requires config.deployment.enabled to be true agentBuilder: enabled: false # Fernet encryption key for Agent Builder secrets. Required when enabled.