diff --git a/charts/retool/Chart.lock b/charts/retool/Chart.lock index 832b6f5..7a8aeec 100644 --- a/charts/retool/Chart.lock +++ b/charts/retool/Chart.lock @@ -5,5 +5,5 @@ dependencies: - name: retool-temporal-services-helm repository: "" version: 1.1.5 -digest: sha256:6b027cb2d661c436127fe34c4a5e14c820c691d4ec9e0c08609f416e6fe5af21 -generated: "2024-03-26T15:39:11.463027-04:00" +digest: sha256:7b9440db4914c56407c98faace390fd00374820b0f87987903912de7ac899ce8 +generated: "2026-04-22T17:14:51.109299-07:00" diff --git a/charts/retool/Chart.yaml b/charts/retool/Chart.yaml index 771605b..dbedb80 100644 --- a/charts/retool/Chart.yaml +++ b/charts/retool/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: retool description: A Helm chart for Kubernetes type: application -version: 6.10.6 +version: 6.11.0 maintainers: - name: Retool Engineering email: engineering+helm@retool.com @@ -13,4 +13,4 @@ dependencies: condition: postgresql.enabled - name: retool-temporal-services-helm version: 1.1.5 - condition: retool-temporal-services-helm.enabled,workflows.enabled + condition: retool-temporal-services-helm.enabled diff --git a/charts/retool/ci/test-agent-sandbox-deviceplugin-no-priorityclass-option.yaml b/charts/retool/ci/test-agent-sandbox-deviceplugin-no-priorityclass-option.yaml new file mode 100644 index 0000000..75b0ac6 --- /dev/null +++ b/charts/retool/ci/test-agent-sandbox-deviceplugin-no-priorityclass-option.yaml @@ -0,0 +1,27 @@ +rr: + + # Agent Sandbox — device-plugin DaemonSet with priorityClassName opted OUT. + # + # Regression guard for the GKE path documented in values.yaml: GKE doesn't allow + # the `system-node-critical` PriorityClass in user namespaces, so operators set + # rr.agentSandbox.devicePlugin.priorityClassName: null + # to drop it. The DaemonSet template must OMIT the priorityClassName field in that + # case — a bare `{{ ... }}` on a nil value would render the literal ``, + # producing an invalid manifest that the kubelet rejects (and that kubeconform + # rejects here in CI). + # + # Overlaid on test-install-values.yaml. + agentSandbox: + enabled: true + + externalSecret: + name: agent-sandbox-secrets + + sandboxNetwork: + enabled: true + devicePlugin: true + deployDaemonSet: true + + devicePlugin: + # The GKE opt-out — DaemonSet must render with no priorityClassName field. + priorityClassName: null diff --git a/charts/retool/ci/test-agent-sandbox-enabled-option.yaml b/charts/retool/ci/test-agent-sandbox-enabled-option.yaml new file mode 100644 index 0000000..32ba7f6 --- /dev/null +++ b/charts/retool/ci/test-agent-sandbox-enabled-option.yaml @@ -0,0 +1,79 @@ +rr: + + # Agent Sandbox — external secret + dedicated proxy domain WITH ingress. + # + # This is the "max surface" scenario: controller + proxy deployments, the + # job-template ConfigMap, RBAC, headless/proxy services, proxy ingress + TLS, + # the image-prepuller + seccomp DaemonSets, the smarter-device-manager device + # plugin DaemonSet, the NetworkPolicies, and both PDBs. + # + # Secret/Postgres sourcing here uses externalSecret.name (Postgres OPTION 4: + # the secret's postgres-url key). The other secret/Postgres precedence paths and + # the same-origin (no-ingress) proxy mode are covered by sibling files: + # - test-agent-sandbox-inline-secrets-option.yaml (inline secrets, plaintext DSN, same-origin/no ingress, hostPath tun) + # - test-agent-sandbox-postgres-fields-option.yaml (assemble DSN from fields + PGPASSWORD secret) + # - test-agent-sandbox-postgres-url-secret-option.yaml (full DSN from an existing secret) + # - test-agent-sandbox-inherit-postgres-option.yaml (zero-config inherit of the backend Postgres) + # Overlaid on test-install-values.yaml. + agentSandbox: + enabled: true + + image: + repository: tryretool/agent-sandbox-service + tag: 3.123.4 + pullPolicy: IfNotPresent + + # Reference a pre-existing K8s Secret (the production-recommended path) rather + # than inlining JWT/encryption material into the chart. With externalSecret.name + # set, secret-backed env vars — including the ones injected into the sandbox + # job-template — resolve via secretKeyRef instead of plaintext. + externalSecret: + name: agent-sandbox-secrets + + postgres: + schema: agent_executor + poolMax: 10 + + sandboxNetwork: + enabled: true + devicePlugin: true + deployDaemonSet: true + + snapshotStorage: + s3Bucket: retool-agent-sandbox-snapshots + s3Endpoint: https://s3.us-east-1.amazonaws.com + s3Region: us-east-1 + credentialsSecretName: agent-sandbox-s3-credentials + + # replicaCount > 1 renders the controller PodDisruptionBudget. + controller: + replicaCount: 2 + + proxy: + replicaCount: 2 + allowedDomains: api.example.com,example.com + backendDomainSuffixes: .example.com + sandboxProxyTimeoutMs: "3600000" + service: + type: ClusterIP + # Dedicated proxy domain → renders the proxy Ingress. + ingress: + enabled: true + ingressClassName: nginx + annotations: + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + host: sandbox.example.com + tls: + - secretName: agent-sandbox-tls + hosts: + - sandbox.example.com + frontendWsProxyDomain: https://sandbox.example.com + + # Restrict sandbox/controller/proxy traffic → renders the NetworkPolicies. + networkPolicy: + enabled: true + +# Exercise the proxy PodDisruptionBudget branch. +podDisruptionBudget: + maxUnavailable: 1 diff --git a/charts/retool/ci/test-agent-sandbox-inherit-postgres-option.yaml b/charts/retool/ci/test-agent-sandbox-inherit-postgres-option.yaml new file mode 100644 index 0000000..c5d09a4 --- /dev/null +++ b/charts/retool/ci/test-agent-sandbox-inherit-postgres-option.yaml @@ -0,0 +1,22 @@ +rr: + + # Agent Sandbox — Postgres sourcing OPTION 5 (default): inherit the backend's + # Postgres connection. With agentSandbox.postgres left entirely unset, the + # controller/proxy reuse config.postgresql / the postgresql subchart (same + # instance and database, separate schema) — the zero-config path for enabling + # the sandbox on an existing deployment. PGPASSWORD mirrors the backend's + # POSTGRES_PASSWORD secretKeyRef, and the DSN is assembled from the postgresql + # helpers. The base test-install-values.yaml enables the postgresql subchart, + # which is what makes inheritance resolve. + # + # Only the (required) JWT secrets are provided; everything else is left default. + agentSandbox: + enabled: true + + image: + repository: tryretool/agent-sandbox-service + tag: 3.123.4 + pullPolicy: IfNotPresent + + jwtPublicKey: '-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEljtqa2nhBwe/PqNhWgPHhj0jv8AI\nY+QUCicYtfv9wLGcEGPQuXoBQtuoIuOwXOdbEWgrQyLdIEb0YjegAW3miA==\n-----END PUBLIC KEY-----' + jwtPrivateKey: '-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMFXLiN/YsJv89D2YkEZ6/Dj5fujghENmYTOilwdChU3oAoGCCqGSM49\nAwEHoUQDQgAEljtqa2nhBwe/PqNhWgPHhj0jv8AIY+QUCicYtfv9wLGcEGPQuXoB\nQtuoIuOwXOdbEWgrQyLdIEb0YjegAW3miA==\n-----END EC PRIVATE KEY-----' diff --git a/charts/retool/ci/test-agent-sandbox-inline-secrets-option.yaml b/charts/retool/ci/test-agent-sandbox-inline-secrets-option.yaml new file mode 100644 index 0000000..35c172a --- /dev/null +++ b/charts/retool/ci/test-agent-sandbox-inline-secrets-option.yaml @@ -0,0 +1,61 @@ +rr: + + # Agent Sandbox — inline secrets + plaintext DSN + same-origin proxy (no ingress). + # + # Complements test-agent-sandbox-enabled-option.yaml (external secret + dedicated + # proxy ingress). Here we exercise the *other* halves of those branches: + # - Secrets inline (no externalSecret.name) → the chart renders its own Secret + # (jwt-public-key / jwt-private-key / encryption-key / api-secret). jwtPublicKey + # is injected into the sandbox job-template JSON via `toJson`, so a genuine + # multi-line PEM (real newlines, as below) is escaped correctly — no need to + # pre-flatten it to a single `\n`-escaped line. + # - Postgres sourcing OPTION 1: plaintext DSN via postgres.url. + # - Same-origin proxy: no dedicated proxy domain and no proxy ingress — the + # backend reverse-proxies /sandbox/* (frontendWsProxyDomain left empty). + # - sandboxNetwork.devicePlugin=false → sandbox pods get /dev/net/tun via + # hostPath (the non-device-plugin branch), and no device-manager DaemonSet. + # - networkPolicy disabled. + agentSandbox: + enabled: true + + image: + repository: tryretool/agent-sandbox-service + tag: 3.123.4 + pullPolicy: IfNotPresent + + # Real multi-line PEM (block scalar) — exercises the toJson newline escaping in + # the job-template JSON. A raw "{{ . }}" would produce invalid JSON here. + jwtPublicKey: |- + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEljtqa2nhBwe/PqNhWgPHhj0jv8AI + Y+QUCicYtfv9wLGcEGPQuXoBQtuoIuOwXOdbEWgrQyLdIEb0YjegAW3miA== + -----END PUBLIC KEY----- + jwtPrivateKey: |- + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIMFXLiN/YsJv89D2YkEZ6/Dj5fujghENmYTOilwdChU3oAoGCCqGSM49 + AwEHoUQDQgAEljtqa2nhBwe/PqNhWgPHhj0jv8AIY+QUCicYtfv9wLGcEGPQuXoB + QtuoIuOwXOdbEWgrQyLdIEb0YjegAW3miA== + -----END EC PRIVATE KEY----- + encryptionKey: a12b01429fe0fe69a80da94e9e837ab2f1e9bda378ed8a25905a238f6fea6b7a + apiSecret: test-agent-sandbox-api-secret + + # Option 1: plaintext DSN. + postgres: + url: postgres://retool:retool@agent-sandbox-db.example.internal:5432/agent_sandbox + schema: agent_executor + poolMax: 10 + + sandboxNetwork: + enabled: true + devicePlugin: false + deployDaemonSet: false + + proxy: + # Same-origin: ClusterIP service, no ingress. + service: + type: ClusterIP + ingress: + enabled: false + + networkPolicy: + enabled: false diff --git a/charts/retool/ci/test-agent-sandbox-postgres-fields-option.yaml b/charts/retool/ci/test-agent-sandbox-postgres-fields-option.yaml new file mode 100644 index 0000000..2b4e25b --- /dev/null +++ b/charts/retool/ci/test-agent-sandbox-postgres-fields-option.yaml @@ -0,0 +1,41 @@ +rr: + + # Agent Sandbox — Postgres sourcing OPTION 2: assemble the DSN from discrete + # fields, with the password supplied via PGPASSWORD from a pre-existing Secret + # (never embedded in the URL, so any password characters are safe). + # + # Also exercises: + # - An Azure-style "user@servername" username, which validateSecrets allows + # (the parser splits userinfo on the last '@'). + # - sandboxNetwork with devicePlugin=true but deployDaemonSet=false (a + # smarter-device-manager already runs on the nodes, managed elsewhere) → + # sandbox pods request smarter-devices/net_tun but no DS is rendered. + # - networkPolicy enabled. + agentSandbox: + enabled: true + + image: + repository: tryretool/agent-sandbox-service + tag: 3.123.4 + pullPolicy: IfNotPresent + + jwtPublicKey: '-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEljtqa2nhBwe/PqNhWgPHhj0jv8AI\nY+QUCicYtfv9wLGcEGPQuXoBQtuoIuOwXOdbEWgrQyLdIEb0YjegAW3miA==\n-----END PUBLIC KEY-----' + jwtPrivateKey: '-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMFXLiN/YsJv89D2YkEZ6/Dj5fujghENmYTOilwdChU3oAoGCCqGSM49\nAwEHoUQDQgAEljtqa2nhBwe/PqNhWgPHhj0jv8AIY+QUCicYtfv9wLGcEGPQuXoB\nQtuoIuOwXOdbEWgrQyLdIEb0YjegAW3miA==\n-----END EC PRIVATE KEY-----' + + # Option 2: host + user + database, password via PGPASSWORD secretKeyRef. + postgres: + host: agentdb-prod.postgres.database.azure.com + port: 5432 + database: agent_sandbox + user: retool@agentdb-prod + passwordSecretName: agent-sandbox-db-password + passwordSecretKey: password + schema: agent_executor + + sandboxNetwork: + enabled: true + devicePlugin: true + deployDaemonSet: false + + networkPolicy: + enabled: true diff --git a/charts/retool/ci/test-agent-sandbox-postgres-url-secret-option.yaml b/charts/retool/ci/test-agent-sandbox-postgres-url-secret-option.yaml new file mode 100644 index 0000000..7e0db28 --- /dev/null +++ b/charts/retool/ci/test-agent-sandbox-postgres-url-secret-option.yaml @@ -0,0 +1,31 @@ +rr: + + # Agent Sandbox — Postgres sourcing OPTION 3: the full DSN comes from a + # pre-existing Secret (postgres.urlSecretName / urlSecretKey), while the JWT/ + # encryption secrets are provided inline. This is the "BYO DB secret, chart- + # managed app secrets" combination. + # + # Also exercises S3 snapshot storage WITHOUT a dedicated credentialsSecretName, + # so the sandbox AWS creds fall back to the default (chart-rendered) Secret. + agentSandbox: + enabled: true + + image: + repository: tryretool/agent-sandbox-service + tag: 3.123.4 + pullPolicy: IfNotPresent + + jwtPublicKey: '-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEljtqa2nhBwe/PqNhWgPHhj0jv8AI\nY+QUCicYtfv9wLGcEGPQuXoBQtuoIuOwXOdbEWgrQyLdIEb0YjegAW3miA==\n-----END PUBLIC KEY-----' + jwtPrivateKey: '-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMFXLiN/YsJv89D2YkEZ6/Dj5fujghENmYTOilwdChU3oAoGCCqGSM49\nAwEHoUQDQgAEljtqa2nhBwe/PqNhWgPHhj0jv8AIY+QUCicYtfv9wLGcEGPQuXoB\nQtuoIuOwXOdbEWgrQyLdIEb0YjegAW3miA==\n-----END EC PRIVATE KEY-----' + encryptionKey: a12b01429fe0fe69a80da94e9e837ab2f1e9bda378ed8a25905a238f6fea6b7a + + # Option 3: full DSN from an existing Secret. + postgres: + urlSecretName: agent-sandbox-db-dsn + urlSecretKey: connection-string + schema: agent_executor + + snapshotStorage: + s3Bucket: retool-agent-sandbox-snapshots + s3Endpoint: https://s3.us-west-2.amazonaws.com + s3Region: us-west-2 diff --git a/charts/retool/ci/test-js-executor-enabled-option.yaml b/charts/retool/ci/test-js-executor-enabled-option.yaml new file mode 100644 index 0000000..4de597c --- /dev/null +++ b/charts/retool/ci/test-js-executor-enabled-option.yaml @@ -0,0 +1,39 @@ +rr: + + # Exercises the JS executor workload (deployment_js_executor.yaml + + # configmap_js_executor.yaml). Overlaid on top of test-install-values.yaml. + jsExecutor: + enabled: true + replicaCount: 2 + image: + repository: tryretool/js-executor-service + tag: 3.123.4 + # Deliberately differs from the global image.pullPolicy (IfNotPresent in the + # base values) so the rendered deployment proves the per-workload override is + # honored rather than the global value. + pullPolicy: Always + # JS-executor-specific env (not inherited from top-level .Values.env). + env: + LOG_LEVEL: info + # Exercise the per-workload secretKeyRef branch. + environmentSecrets: + - name: JS_EXECUTOR_TOKEN + secretKeyRef: + name: js-executor-secrets + key: token + environmentVariables: + - name: JS_EXECUTOR_TEST_OPTION + value: "true" + # Memory request and limit are kept equal: JSE rejects requests at 80% of + # its limit, so the request must reserve the full amount. + resources: + limits: + cpu: 4000m + memory: 4Gi + requests: + cpu: 4000m + memory: 4Gi + +# Exercise the PDB branch shared by the JS executor deployment. +podDisruptionBudget: + maxUnavailable: 1 diff --git a/charts/retool/ci/test-mcp-enabled-option.yaml b/charts/retool/ci/test-mcp-enabled-option.yaml new file mode 100644 index 0000000..95a3d38 --- /dev/null +++ b/charts/retool/ci/test-mcp-enabled-option.yaml @@ -0,0 +1,19 @@ +mcp: + enabled: true + replicaCount: 2 + config: + oauthMainDomain: https://oauth.example.com + oauthIntrospectionAuthToken: test-oauth-introspection-token + enabledToolsets: + - apps + - resources + maxTransportSessions: 50 + sessionIdleTimeoutMs: 600000 + environmentVariables: + - name: MCP_TEST_OPTION + value: "true" + service: + internalPort: + +httpRoute: + enabled: true diff --git a/charts/retool/ci/test-rr-agent-enabled-option.yaml b/charts/retool/ci/test-rr-agent-enabled-option.yaml new file mode 100644 index 0000000..678c467 --- /dev/null +++ b/charts/retool/ci/test-rr-agent-enabled-option.yaml @@ -0,0 +1,27 @@ +rr: + + # Exercises the RR Agent worker (the server-side agent loop worker rendered via + # _workers.tpl as SERVICE_TYPE=R2_AGENT_TEMPORAL_WORKER on healthcheck port 3016). + # Renders a Deployment + Service, plus a PodDisruptionBudget when one is set. + # Overlaid on test-install-values.yaml. + agent: + enabled: true + config: + nodeOptions: "--max_old_space_size=2048" + worker: + replicaCount: 2 + resources: + limits: + cpu: 2000m + memory: 4096Mi + requests: + cpu: 1000m + memory: 2048Mi + labels: + test-pod-label: "true" + annotations: + test-pod-annotation: "true" + +# Exercise the worker PodDisruptionBudget branch. +podDisruptionBudget: + maxUnavailable: 1 diff --git a/charts/retool/ci/test-rr-enabled-option.yaml b/charts/retool/ci/test-rr-enabled-option.yaml new file mode 100644 index 0000000..aae0c9d --- /dev/null +++ b/charts/retool/ci/test-rr-enabled-option.yaml @@ -0,0 +1,30 @@ +# RR master switch — single flag turns on the whole RR stack. +# +# Exercises the `rr.enabled: true` inherit path: agent, jsExecutor, and +# agentSandbox all leave their own `enabled` unset (null) and inherit the master +# switch. This guards `retool.rr.componentEnabled` and the helper-routed +# cross-component env wiring (backend/workflows/jobs/workers read +# effective-enabled, not the raw per-component flag). mcp is intentionally NOT +# part of the master switch (it is opt-in via mcp.enabled, since it needs its +# own OAuth config), so it must not render from rr.enabled alone. +# +# Secrets/config below are only what each component requires to template when +# enabled; none of them set `*.enabled`, so enablement comes solely from rr. +rr: + enabled: true + + agentSandbox: + # jwtPublicKey is injected raw into the sandbox job-template JSON, so it MUST + # be single-line (\n-escaped) or templating breaks. + jwtPublicKey: '-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEljtqa2nhBwe/PqNhWgPHhj0jv8AIY+QUCicYtfv9wLGcEGPQuXoBQtuoIuOwXOdbEWgrQyLdIEb0YjegAW3miA==\n-----END PUBLIC KEY-----' + jwtPrivateKey: '-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMFXLiN/YsJv89D2YkEZ6/Dj5fujghENmYTOilwdChU3oAoGCCqGSM49AwEHoUQDQgAEljtqa2nhBwe/PqNhWgPHhj0jv8AIY+QUCicYtfv9wLGcEGPQuXoBQtuoIuOwXOdbEWgrQyLdIEb0YjegAW3miA==\n-----END EC PRIVATE KEY-----' + postgres: + url: postgres://retool:retool@agent-sandbox-db.example.internal:5432/agent_sandbox + schema: agent_executor + proxy: + service: + type: ClusterIP + ingress: + enabled: false + networkPolicy: + enabled: false diff --git a/charts/retool/ci/test-rr-git-server-separate-option.yaml b/charts/retool/ci/test-rr-git-server-separate-option.yaml new file mode 100644 index 0000000..1c2e23a --- /dev/null +++ b/charts/retool/ci/test-rr-git-server-separate-option.yaml @@ -0,0 +1,41 @@ +podDisruptionBudget: + minAvailable: 1 + +rr: + + gitServer: + enabled: true + repackThreshold: 200 + separate: + enabled: true + replicaCount: 2 + port: 3010 + resources: + requests: + cpu: 250m + memory: 512Mi + annotations: + test-pod-annotation: "true" + labels: + test-pod-label: "true" + service: + annotations: + test-service-annotation: "true" + labels: + test-service-label: "true" + + + blobStorage: + s3: + bucket: test-rr-bucket + region: us-east-1 + accessKeyId: AKIATEST + secretAccessKeySecretName: rr-blob-storage + secretAccessKeySecretKey: secret-access-key + +# Exercise the MCP auto-wiring to the standalone git server service. +mcp: + enabled: true + config: + oauthMainDomain: https://oauth.example.com + oauthIntrospectionAuthToken: test-oauth-introspection-token diff --git a/charts/retool/files/gvisor-seccomp.json b/charts/retool/files/gvisor-seccomp.json new file mode 100644 index 0000000..120950f --- /dev/null +++ b/charts/retool/files/gvisor-seccomp.json @@ -0,0 +1,447 @@ +{ + "comment": "Docker default seccomp profile extended with syscalls required by gVisor runsc (systrap platform, rootless mode). Use with: docker run --security-opt seccomp=gvisor-seccomp.json", + "defaultAction": "SCMP_ACT_ERRNO", + "defaultErrnoRet": 38, + "archMap": [ + { + "architecture": "SCMP_ARCH_X86_64", + "subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"] + }, + { + "architecture": "SCMP_ARCH_AARCH64", + "subArchitectures": [] + } + ], + "syscalls": [ + { + "comment": "Docker default allowlist (Docker 27.x, x86_64 + aarch64)", + "names": [ + "_llseek", + "_newselect", + "accept", + "accept4", + "access", + "acct", + "adjtimex", + "alarm", + "arch_prctl", + "bind", + "bpf", + "brk", + "cachestat", + "capget", + "capset", + "chdir", + "chmod", + "chown", + "chown32", + "chroot", + "clock_adjtime", + "clock_adjtime64", + "clock_getres", + "clock_getres_time64", + "clock_gettime", + "clock_gettime64", + "clock_nanosleep", + "clock_nanosleep_time64", + "clock_settime", + "clock_settime64", + "close", + "close_range", + "connect", + "copy_file_range", + "creat", + "delete_module", + "dup", + "dup2", + "dup3", + "epoll_create", + "epoll_create1", + "epoll_ctl", + "epoll_ctl_old", + "epoll_pwait", + "epoll_pwait2", + "epoll_wait", + "epoll_wait_old", + "eventfd", + "eventfd2", + "execve", + "execveat", + "exit", + "exit_group", + "faccessat", + "faccessat2", + "fadvise64", + "fadvise64_64", + "fallocate", + "fanotify_init", + "fanotify_mark", + "fchdir", + "fchmod", + "fchmodat", + "fchmodat2", + "fchown", + "fchown32", + "fchownat", + "fcntl", + "fcntl64", + "fdatasync", + "fgetxattr", + "finit_module", + "flistxattr", + "flock", + "fork", + "fremovexattr", + "fsconfig", + "fsetxattr", + "fsmount", + "fsopen", + "fspick", + "fstat", + "fstat64", + "fstatat64", + "fstatfs", + "fstatfs64", + "fsync", + "ftruncate", + "ftruncate64", + "futex", + "futex_requeue", + "futex_time64", + "futex_wait", + "futex_waitv", + "futex_wake", + "futimesat", + "get_mempolicy", + "get_robust_list", + "get_thread_area", + "getcpu", + "getcwd", + "getdents", + "getdents64", + "getegid", + "getegid32", + "geteuid", + "geteuid32", + "getgid", + "getgid32", + "getgroups", + "getgroups32", + "getitimer", + "getpeername", + "getpgid", + "getpgrp", + "getpid", + "getppid", + "getpriority", + "getrandom", + "getresgid", + "getresgid32", + "getresuid", + "getresuid32", + "getrlimit", + "getrusage", + "getsid", + "getsockname", + "getsockopt", + "gettid", + "gettimeofday", + "getuid", + "getuid32", + "getxattr", + "init_module", + "inotify_add_watch", + "inotify_init", + "inotify_init1", + "inotify_rm_watch", + "io_cancel", + "io_destroy", + "io_getevents", + "io_pgetevents", + "io_pgetevents_time64", + "io_setup", + "io_submit", + "io_uring_enter", + "io_uring_register", + "io_uring_setup", + "ioctl", + "ioperm", + "iopl", + "ioprio_get", + "ioprio_set", + "ipc", + "kcmp", + "kill", + "landlock_add_rule", + "landlock_create_ruleset", + "landlock_restrict_self", + "lchown", + "lchown32", + "lgetxattr", + "link", + "linkat", + "listen", + "listxattr", + "llistxattr", + "lookup_dcookie", + "lremovexattr", + "lseek", + "lsetxattr", + "lstat", + "lstat64", + "madvise", + "map_shadow_stack", + "mbind", + "membarrier", + "memfd_create", + "memfd_secret", + "mincore", + "mkdir", + "mkdirat", + "mknod", + "mknodat", + "mlock", + "mlock2", + "mlockall", + "mmap", + "mmap2", + "modify_ldt", + "mount_setattr", + "move_mount", + "mprotect", + "mq_getsetattr", + "mq_notify", + "mq_open", + "mq_timedreceive", + "mq_timedreceive_time64", + "mq_timedsend", + "mq_timedsend_time64", + "mq_unlink", + "mremap", + "msgctl", + "msgget", + "msgrcv", + "msgsnd", + "msync", + "munlock", + "munlockall", + "munmap", + "name_to_handle_at", + "nanosleep", + "newfstatat", + "open", + "open_by_handle_at", + "open_tree", + "openat", + "openat2", + "pause", + "perf_event_open", + "pidfd_getfd", + "pidfd_open", + "pidfd_send_signal", + "pipe", + "pipe2", + "pkey_alloc", + "pkey_free", + "pkey_mprotect", + "poll", + "ppoll", + "ppoll_time64", + "prctl", + "pread64", + "preadv", + "preadv2", + "prlimit64", + "process_madvise", + "process_mrelease", + "process_vm_readv", + "process_vm_writev", + "pselect6", + "pselect6_time64", + "pwrite64", + "pwritev", + "pwritev2", + "quotactl", + "quotactl_fd", + "read", + "readahead", + "readlink", + "readlinkat", + "readv", + "reboot", + "recv", + "recvfrom", + "recvmmsg", + "recvmmsg_time64", + "recvmsg", + "remap_file_pages", + "removexattr", + "rename", + "renameat", + "renameat2", + "restart_syscall", + "rmdir", + "rseq", + "rt_sigaction", + "rt_sigpending", + "rt_sigprocmask", + "rt_sigqueueinfo", + "rt_sigreturn", + "rt_sigsuspend", + "rt_sigtimedwait", + "rt_sigtimedwait_time64", + "rt_tgsigqueueinfo", + "sched_get_priority_max", + "sched_get_priority_min", + "sched_getaffinity", + "sched_getattr", + "sched_getparam", + "sched_getscheduler", + "sched_rr_get_interval", + "sched_rr_get_interval_time64", + "sched_setaffinity", + "sched_setattr", + "sched_setparam", + "sched_setscheduler", + "sched_yield", + "seccomp", + "select", + "semctl", + "semget", + "semop", + "semtimedop", + "semtimedop_time64", + "send", + "sendfile", + "sendfile64", + "sendmmsg", + "sendmsg", + "sendto", + "set_mempolicy", + "set_mempolicy_home_node", + "set_robust_list", + "set_thread_area", + "set_tid_address", + "set_tls", + "setdomainname", + "setfsgid", + "setfsgid32", + "setfsuid", + "setfsuid32", + "setgid", + "setgid32", + "setgroups", + "setgroups32", + "setitimer", + "setpgid", + "setpriority", + "setregid", + "setregid32", + "setresgid", + "setresgid32", + "setresuid", + "setresuid32", + "setreuid", + "setreuid32", + "setrlimit", + "setsid", + "setsockopt", + "settimeofday", + "setuid", + "setuid32", + "setxattr", + "shmat", + "shmctl", + "shmdt", + "shmget", + "shutdown", + "sigaltstack", + "signalfd", + "signalfd4", + "sigprocmask", + "sigreturn", + "socket", + "socketcall", + "socketpair", + "splice", + "stat", + "stat64", + "statfs", + "statfs64", + "statx", + "stime", + "symlink", + "symlinkat", + "sync", + "sync_file_range", + "sync_file_range2", + "syncfs", + "sysinfo", + "syslog", + "tee", + "tgkill", + "time", + "timer_create", + "timer_delete", + "timer_getoverrun", + "timer_gettime", + "timer_gettime64", + "timer_settime", + "timer_settime64", + "timerfd_create", + "timerfd_gettime", + "timerfd_gettime64", + "timerfd_settime", + "timerfd_settime64", + "times", + "tkill", + "truncate", + "truncate64", + "ugetrlimit", + "umask", + "umount", + "uname", + "unlink", + "unlinkat", + "utime", + "utimensat", + "utimensat_time64", + "utimes", + "vfork", + "vhangup", + "vmsplice", + "wait4", + "waitid", + "waitpid", + "write", + "writev" + ], + "action": "SCMP_ACT_ALLOW" + }, + { + "comment": "gVisor + pasta: namespace creation and entry (clone/unshare with CLONE_NEW* flags, setns to join namespaces)", + "names": ["clone", "clone3", "unshare", "setns"], + "action": "SCMP_ACT_ALLOW" + }, + { + "comment": "pasta: set hostname inside namespace (cosmetic, avoids warning)", + "names": ["sethostname"], + "action": "SCMP_ACT_ALLOW" + }, + { + "comment": "gVisor: sandbox filesystem setup (tmpfs, proc, bind mounts)", + "names": ["mount", "umount2"], + "action": "SCMP_ACT_ALLOW" + }, + { + "comment": "gVisor: filesystem root isolation for sentry and gofer", + "names": ["pivot_root"], + "action": "SCMP_ACT_ALLOW" + }, + { + "comment": "gVisor systrap platform: workload executor thread initialization", + "names": ["ptrace"], + "action": "SCMP_ACT_ALLOW" + } + ] +} diff --git a/charts/retool/files/nsjail-seccomp.json b/charts/retool/files/nsjail-seccomp.json new file mode 100644 index 0000000..a9c55e9 --- /dev/null +++ b/charts/retool/files/nsjail-seccomp.json @@ -0,0 +1,736 @@ +{ + "defaultAction": "SCMP_ACT_ERRNO", + "defaultErrnoRet": 1, + "archMap": [ + { + "architecture": "SCMP_ARCH_X86_64", + "subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"] + }, + { + "architecture": "SCMP_ARCH_AARCH64", + "subArchitectures": ["SCMP_ARCH_ARM"] + }, + { + "architecture": "SCMP_ARCH_MIPS64", + "subArchitectures": ["SCMP_ARCH_MIPS", "SCMP_ARCH_MIPS64N32"] + }, + { + "architecture": "SCMP_ARCH_MIPS64N32", + "subArchitectures": ["SCMP_ARCH_MIPS", "SCMP_ARCH_MIPS64"] + }, + { + "architecture": "SCMP_ARCH_MIPSEL64", + "subArchitectures": ["SCMP_ARCH_MIPSEL", "SCMP_ARCH_MIPSEL64N32"] + }, + { + "architecture": "SCMP_ARCH_MIPSEL64N32", + "subArchitectures": ["SCMP_ARCH_MIPSEL", "SCMP_ARCH_MIPSEL64"] + }, + { + "architecture": "SCMP_ARCH_S390X", + "subArchitectures": ["SCMP_ARCH_S390"] + }, + { + "architecture": "SCMP_ARCH_RISCV64", + "subArchitectures": null + } + ], + "syscalls": [ + { + "names": [ + "accept", + "accept4", + "access", + "adjtimex", + "alarm", + "bind", + "brk", + "cachestat", + "capget", + "capset", + "chdir", + "chmod", + "chown", + "chown32", + "clock_adjtime", + "clock_adjtime64", + "clock_getres", + "clock_getres_time64", + "clock_gettime", + "clock_gettime64", + "clock_nanosleep", + "clock_nanosleep_time64", + "close", + "close_range", + "connect", + "copy_file_range", + "creat", + "dup", + "dup2", + "dup3", + "epoll_create", + "epoll_create1", + "epoll_ctl", + "epoll_ctl_old", + "epoll_pwait", + "epoll_pwait2", + "epoll_wait", + "epoll_wait_old", + "eventfd", + "eventfd2", + "execve", + "execveat", + "exit", + "exit_group", + "faccessat", + "faccessat2", + "fadvise64", + "fadvise64_64", + "fallocate", + "fanotify_mark", + "fchdir", + "fchmod", + "fchmodat", + "fchmodat2", + "fchown", + "fchown32", + "fchownat", + "fcntl", + "fcntl64", + "fdatasync", + "fgetxattr", + "flistxattr", + "flock", + "fork", + "fremovexattr", + "fsetxattr", + "fstat", + "fstat64", + "fstatat64", + "fstatfs", + "fstatfs64", + "fsync", + "ftruncate", + "ftruncate64", + "futex", + "futex_requeue", + "futex_time64", + "futex_wait", + "futex_waitv", + "futex_wake", + "futimesat", + "getcpu", + "getcwd", + "getdents", + "getdents64", + "getegid", + "getegid32", + "geteuid", + "geteuid32", + "getgid", + "getgid32", + "getgroups", + "getgroups32", + "getitimer", + "getpeername", + "getpgid", + "getpgrp", + "getpid", + "getppid", + "getpriority", + "getrandom", + "getresgid", + "getresgid32", + "getresuid", + "getresuid32", + "getrlimit", + "get_robust_list", + "getrusage", + "getsid", + "getsockname", + "getsockopt", + "get_thread_area", + "gettid", + "gettimeofday", + "getuid", + "getuid32", + "getxattr", + "getxattrat", + "inotify_add_watch", + "inotify_init", + "inotify_init1", + "inotify_rm_watch", + "io_cancel", + "ioctl", + "io_destroy", + "io_getevents", + "io_pgetevents", + "io_pgetevents_time64", + "ioprio_get", + "ioprio_set", + "io_setup", + "io_submit", + "ipc", + "kill", + "landlock_add_rule", + "landlock_create_ruleset", + "landlock_restrict_self", + "lchown", + "lchown32", + "lgetxattr", + "link", + "linkat", + "listen", + "listmount", + "listxattr", + "listxattrat", + "llistxattr", + "_llseek", + "lremovexattr", + "lseek", + "lsetxattr", + "lstat", + "lstat64", + "madvise", + "map_shadow_stack", + "membarrier", + "memfd_create", + "memfd_secret", + "mincore", + "mkdir", + "mkdirat", + "mknod", + "mknodat", + "mlock", + "mlock2", + "mlockall", + "mmap", + "mmap2", + "mprotect", + "mq_getsetattr", + "mq_notify", + "mq_open", + "mq_timedreceive", + "mq_timedreceive_time64", + "mq_timedsend", + "mq_timedsend_time64", + "mq_unlink", + "mremap", + "mseal", + "msgctl", + "msgget", + "msgrcv", + "msgsnd", + "msync", + "munlock", + "munlockall", + "munmap", + "name_to_handle_at", + "nanosleep", + "newfstatat", + "_newselect", + "open", + "openat", + "openat2", + "pause", + "pidfd_open", + "pidfd_send_signal", + "pipe", + "pipe2", + "pkey_alloc", + "pkey_free", + "pkey_mprotect", + "poll", + "ppoll", + "ppoll_time64", + "prctl", + "pread64", + "preadv", + "preadv2", + "prlimit64", + "process_mrelease", + "pselect6", + "pselect6_time64", + "pwrite64", + "pwritev", + "pwritev2", + "read", + "readahead", + "readlink", + "readlinkat", + "readv", + "recv", + "recvfrom", + "recvmmsg", + "recvmmsg_time64", + "recvmsg", + "remap_file_pages", + "removexattr", + "removexattrat", + "rename", + "renameat", + "renameat2", + "restart_syscall", + "riscv_hwprobe", + "rmdir", + "rseq", + "rt_sigaction", + "rt_sigpending", + "rt_sigprocmask", + "rt_sigqueueinfo", + "rt_sigreturn", + "rt_sigsuspend", + "rt_sigtimedwait", + "rt_sigtimedwait_time64", + "rt_tgsigqueueinfo", + "sched_getaffinity", + "sched_getattr", + "sched_getparam", + "sched_get_priority_max", + "sched_get_priority_min", + "sched_getscheduler", + "sched_rr_get_interval", + "sched_rr_get_interval_time64", + "sched_setaffinity", + "sched_setattr", + "sched_setparam", + "sched_setscheduler", + "sched_yield", + "seccomp", + "select", + "semctl", + "semget", + "semop", + "semtimedop", + "semtimedop_time64", + "send", + "sendfile", + "sendfile64", + "sendmmsg", + "sendmsg", + "sendto", + "setfsgid", + "setfsgid32", + "setfsuid", + "setfsuid32", + "setgid", + "setgid32", + "setgroups", + "setgroups32", + "setitimer", + "setpgid", + "setpriority", + "setregid", + "setregid32", + "setresgid", + "setresgid32", + "setresuid", + "setresuid32", + "setreuid", + "setreuid32", + "setrlimit", + "set_robust_list", + "setsid", + "setsockopt", + "set_thread_area", + "set_tid_address", + "setuid", + "setuid32", + "setxattr", + "setxattrat", + "shmat", + "shmctl", + "shmdt", + "shmget", + "shutdown", + "sigaltstack", + "signalfd", + "signalfd4", + "sigprocmask", + "sigreturn", + "socketcall", + "socketpair", + "splice", + "stat", + "stat64", + "statfs", + "statfs64", + "statmount", + "statx", + "symlink", + "symlinkat", + "sync", + "sync_file_range", + "syncfs", + "sysinfo", + "tee", + "tgkill", + "time", + "timer_create", + "timer_delete", + "timer_getoverrun", + "timer_gettime", + "timer_gettime64", + "timer_settime", + "timer_settime64", + "timerfd_create", + "timerfd_gettime", + "timerfd_gettime64", + "timerfd_settime", + "timerfd_settime64", + "times", + "tkill", + "truncate", + "truncate64", + "ugetrlimit", + "umask", + "uname", + "unlink", + "unlinkat", + "uretprobe", + "utime", + "utimensat", + "utimensat_time64", + "utimes", + "vfork", + "vmsplice", + "wait4", + "waitid", + "waitpid", + "write", + "writev" + ], + "action": "SCMP_ACT_ALLOW" + }, + { + "names": ["process_vm_readv", "process_vm_writev", "ptrace"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "minKernel": "4.8" + } + }, + { + "names": ["socket"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 1, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": ["socket"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 2, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": ["socket"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 10, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": ["socket"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 16, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": ["socket"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 17, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": ["personality"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 0, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": ["personality"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 8, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": ["personality"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 131072, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": ["personality"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 131080, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": ["personality"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 4294967295, + "op": "SCMP_CMP_EQ" + } + ] + }, + { + "names": ["sync_file_range2", "swapcontext"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": ["ppc64le"] + } + }, + { + "names": ["arm_fadvise64_64", "arm_sync_file_range", "sync_file_range2", "breakpoint", "cacheflush", "set_tls"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": ["arm", "arm64"] + } + }, + { + "names": ["arch_prctl"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": ["amd64", "x32"] + } + }, + { + "names": ["modify_ldt"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": ["amd64", "x32", "x86"] + } + }, + { + "names": ["s390_pci_mmio_read", "s390_pci_mmio_write", "s390_runtime_instr"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": ["s390", "s390x"] + } + }, + { + "names": ["riscv_flush_icache"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "arches": ["riscv64"] + } + }, + { + "names": ["open_by_handle_at"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_DAC_READ_SEARCH"] + } + }, + { + "names": ["clone", "clone2", "mount", "pivot_root", "sethostname", "umount2"], + "action": "SCMP_ACT_ALLOW", + "comment": "Retool specific syscalls to enable nsjail sandboxing" + }, + { + "names": [ + "bpf", + "clone", + "clone3", + "fanotify_init", + "fsconfig", + "fsmount", + "fsopen", + "fspick", + "lookup_dcookie", + "lsm_get_self_attr", + "lsm_list_modules", + "lsm_set_self_attr", + "mount", + "mount_setattr", + "move_mount", + "open_tree", + "perf_event_open", + "quotactl", + "quotactl_fd", + "setdomainname", + "sethostname", + "setns", + "syslog", + "umount", + "umount2", + "unshare" + ], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYS_ADMIN"] + } + }, + { + "names": ["clone"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 2114060288, + "op": "SCMP_CMP_MASKED_EQ" + } + ], + "excludes": { + "caps": ["CAP_SYS_ADMIN"], + "arches": ["s390", "s390x"] + } + }, + { + "names": ["clone"], + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 1, + "value": 2114060288, + "op": "SCMP_CMP_MASKED_EQ" + } + ], + "comment": "s390 parameter ordering for clone is different", + "includes": { + "arches": ["s390", "s390x"] + }, + "excludes": { + "caps": ["CAP_SYS_ADMIN"] + } + }, + { + "names": ["clone3"], + "action": "SCMP_ACT_ERRNO", + "errnoRet": 38, + "excludes": { + "caps": ["CAP_SYS_ADMIN"] + } + }, + { + "names": ["reboot"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYS_BOOT"] + } + }, + { + "names": ["chroot"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYS_CHROOT"] + } + }, + { + "names": ["delete_module", "init_module", "finit_module"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYS_MODULE"] + } + }, + { + "names": ["acct"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYS_PACCT"] + } + }, + { + "names": ["kcmp", "pidfd_getfd", "process_madvise", "process_vm_readv", "process_vm_writev", "ptrace"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYS_PTRACE"] + } + }, + { + "names": ["iopl", "ioperm"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYS_RAWIO"] + } + }, + { + "names": ["settimeofday", "stime", "clock_settime", "clock_settime64"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYS_TIME"] + } + }, + { + "names": ["vhangup"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYS_TTY_CONFIG"] + } + }, + { + "names": ["get_mempolicy", "mbind", "set_mempolicy", "set_mempolicy_home_node"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYS_NICE"] + } + }, + { + "names": ["syslog"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_SYSLOG"] + } + }, + { + "names": ["bpf"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_BPF"] + } + }, + { + "names": ["perf_event_open"], + "action": "SCMP_ACT_ALLOW", + "includes": { + "caps": ["CAP_PERFMON"] + } + } + ] +} diff --git a/charts/retool/templates/_helpers.tpl b/charts/retool/templates/_helpers.tpl index 9ee5e45..5d1d787 100644 --- a/charts/retool/templates/_helpers.tpl +++ b/charts/retool/templates/_helpers.tpl @@ -23,6 +23,97 @@ If release name contains chart name it will be used as a full name. {{- end }} {{- end }} +{{/* +Whether MCP routing needs the main Retool Service to expose the backend API +listener in addition to the primary frontend-facing port. +*/}} +{{- define "retool.mcp.needsBackendApi" -}} +{{- $mcp := .Values.mcp | default dict -}} +{{- $mcpIngress := $mcp.ingress | default dict -}} +{{- $mcpHttpRoute := $mcp.httpRoute | default dict -}} +{{- $needsBackendApi := false -}} +{{- if and .Values.ingress.enabled $mcp.enabled $mcpIngress.enabled -}} +{{- range ($mcpIngress.paths | default list) -}} +{{- if eq (.target | default "mcp") "backendApi" -}} +{{- $needsBackendApi = true -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- if and .Values.httpRoute.enabled $mcp.enabled $mcpHttpRoute.enabled -}} +{{- range ($mcpHttpRoute.rules | default list) -}} +{{- if eq (.target | default "mcp") "backendApi" -}} +{{- $needsBackendApi = true -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- if $needsBackendApi -}}true{{- else -}}false{{- end -}} +{{- end }} + +{{/* +Render an MCP-related Ingress path. By default paths route to the MCP service; +target: backendApi routes to the main backend API listener instead. +*/}} +{{- define "retool.ingress.mcpPath" -}} +{{- $root := .root -}} +{{- $path := .path -}} +{{- $target := .target | default ($path.target | default "mcp") -}} +{{- if not (or (eq $target "mcp") (eq $target "backendApi")) -}} +{{- fail (printf "Invalid mcp.ingress.paths target %q for path %q. Valid targets are \"mcp\" and \"backendApi\"." $target $path.path) -}} +{{- end -}} +{{- $mcpService := (($root.Values.mcp).service) | default dict -}} +{{- $serviceName := include "retool.mcp.name" $root -}} +{{- $servicePort := $path.port | default ($mcpService.externalPort | default 4010) -}} +{{- $pathType := $path.pathType | default "ImplementationSpecific" -}} +{{- if eq $target "backendApi" -}} +{{- $serviceName = include "retool.fullname" $root -}} +{{- $servicePort = $path.port | default (.backendApiPort | default 3001) -}} +{{- $pathType = $path.pathType | default "Exact" -}} +{{- end -}} +- path: {{ $path.path }} + {{- if (semverCompare ">=1.18-0" $root.Capabilities.KubeVersion.Version) }} + pathType: {{ $pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $root.Capabilities.KubeVersion.Version }} + service: + name: {{ $serviceName }} + port: + number: {{ $servicePort }} + {{- else }} + serviceName: {{ $serviceName }} + servicePort: {{ $servicePort }} + {{- end }} +{{- end }} + +{{/* +Render an MCP-related HTTPRoute rule. By default rules route to the MCP service; +target: backendApi routes to the main backend API listener instead. +*/}} +{{- define "retool.httpRoute.mcpRule" -}} +{{- $root := .root -}} +{{- $rule := .rule -}} +{{- $target := .target | default ($rule.target | default "mcp") -}} +{{- if not (or (eq $target "mcp") (eq $target "backendApi")) -}} +{{- fail (printf "Invalid mcp.httpRoute.rules target %q for path %q. Valid targets are \"mcp\" and \"backendApi\"." $target $rule.path) -}} +{{- end -}} +{{- $mcpService := (($root.Values.mcp).service) | default dict -}} +{{- $serviceName := include "retool.mcp.name" $root -}} +{{- $servicePort := $rule.port | default ($mcpService.externalPort | default 4010) -}} +{{- $pathType := $rule.pathType | default "PathPrefix" -}} +{{- if eq $target "backendApi" -}} +{{- $serviceName = include "retool.fullname" $root -}} +{{- $servicePort = $rule.port | default (.backendApiPort | default 3001) -}} +{{- $pathType = $rule.pathType | default "Exact" -}} +{{- end -}} +- matches: + - path: + type: {{ $pathType }} + value: {{ $rule.path }} + backendRefs: + - name: {{ $serviceName }} + port: {{ $servicePort }} +{{- end }} + {{/* Create chart name and version as used by the chart label. */}} @@ -125,6 +216,23 @@ app.kubernetes.io/instance: {{ .Release.Name }} telemetry.retool.com/service-name: code-executor {{- end }} +{{/* +Selector labels for js executor. Note changes here will require manual +deployment recreation and incur downtime, so should be avoided. +*/}} +{{- define "retool.jsExecutor.selectorLabels" -}} +retoolService: {{ include "retool.jsExecutor.name" . }} +{{- end }} + +{{/* +Extra (non-selector) labels for js executor. +*/}} +{{- define "retool.jsExecutor.labels" -}} +app.kubernetes.io/name: {{ include "retool.jsExecutor.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +telemetry.retool.com/service-name: js-executor +{{- end }} + {{/* Selector labels for agent worker. Note changes here will require manual deployment recreation and incur downtime, so should be avoided. @@ -170,6 +278,28 @@ Create the name of the service account to use {{- end }} {{- end }} +{{/* +Render map-style env values as Kubernetes EnvVar entries. +Scalar values are always quoted so YAML booleans and numbers become strings. +Map values allow structured EnvVar fields such as valueFrom. +*/}} +{{- define "retool.env" -}} +{{- range $key, $value := . }} +- name: {{ $key | quote }} +{{- if kindIs "map" $value }} +{{- if hasKey $value "value" }} + value: {{ get $value "value" | quote }} +{{- end }} +{{- range $field, $fieldValue := omit $value "value" }} + {{ $field }}: +{{ toYaml $fieldValue | indent 4 }} +{{- end }} +{{- else }} + value: {{ $value | quote }} +{{- end }} +{{- end }} +{{- end }} + {{- define "retool.postgresql.fullname" -}} {{- $name := default "postgresql" .Values.postgresql.nameOverride -}} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} @@ -289,6 +419,40 @@ Usage: (include "retool.agents.enabled" .) {{- $output -}} {{- end -}} +{{/* +Resolve whether an RR component (agent, jsExecutor, agentSandbox) is +enabled. Components are nested under .Values.rr (each ships as a default block +with `enabled: null`). The component's own `enabled` wins when explicitly set to +true/false; when left unset (null) it inherits the shared master switch +.Values.rr.enabled. If the component block itself is absent or explicitly nulled +it is treated as disabled (there is no config to render). A non-mapping value +(e.g. a bare bool) is a misconfiguration and fails loudly. +Usage: (include "retool.rr.componentEnabled" (dict "root" $ "component" "jsExecutor")) +Returns "1" when enabled, "" otherwise. +*/}} +{{- define "retool.rr.componentEnabled" -}} +{{- $rr := .root.Values.rr | default dict -}} +{{- $cfg := index $rr .component -}} +{{- if kindIs "invalid" $cfg -}} + {{/* component block absent or explicitly nulled -> disabled (there is no + config to render, so it cannot inherit the master switch on) */}} +{{- else if kindIs "map" $cfg -}} + {{- if kindIs "invalid" $cfg.enabled -}} + {{- if eq (toString $rr.enabled) "true" -}}1{{- end -}} + {{- else if eq (toString $cfg.enabled) "true" -}}1{{- end -}} +{{- else -}} + {{- fail (printf "rr.%s must be a mapping (got %s). To toggle this component set rr.%s.enabled: true|false; to inherit the rr.enabled master switch, leave rr.%s unset." .component (kindOf $cfg) .component .component) -}} +{{- end -}} +{{- end -}} + +{{/* +Set RR agent worker enabled. Honors the shared RR master switch. +Usage: (include "retool.agent.enabled" .) +*/}} +{{- define "retool.agent.enabled" -}} +{{- include "retool.rr.componentEnabled" (dict "root" . "component" "agent") -}} +{{- end -}} + {{/* Global Temporal configuration */}} {{- define "retool.temporalConfig" -}} {{- .Values.workflows.temporal | default .Values.temporal | toYaml -}} @@ -358,6 +522,13 @@ Set code executor service name {{ template "retool.fullname" . }}-code-executor {{- end -}} +{{/* +Set JS executor service name +*/}} +{{- define "retool.jsExecutor.name" -}} +{{ template "retool.fullname" . }}-js-executor +{{- end -}} + {{/* Set multiplayer service name */}} @@ -379,6 +550,485 @@ Set agent eval worker service name {{ template "retool.fullname" . }}-agent-eval-worker {{- end -}} +{{/* +Set RR agent worker service name +*/}} +{{- define "retool.rrAgentWorker.name" -}} +{{ template "retool.fullname" . }}-r2-agent-worker +{{- end -}} + +{{/* +Selector labels for RR agent worker. Note changes here will require manual +deployment recreation and incur downtime, so should be avoided. +*/}} +{{- define "retool.rrAgentWorker.selectorLabels" -}} +retoolService: {{ include "retool.rrAgentWorker.name" . }} +{{- end }} + +{{/* +Extra (non-selector) labels for RR agent worker. +*/}} +{{- define "retool.rrAgentWorker.labels" -}} +app.kubernetes.io/name: {{ include "retool.rrAgentWorker.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +telemetry.retool.com/service-name: r2-agent-worker +{{- end }} + +{{/* +Set agent sandbox base name +*/}} +{{- define "retool.agentSandbox.name" -}} +{{ template "retool.fullname" . }}-agent-sandbox +{{- end -}} + +{{/* +Set agent sandbox controller name +*/}} +{{- define "retool.agentSandbox.controller.name" -}} +{{ template "retool.fullname" . }}-agent-sandbox-controller +{{- end -}} + +{{/* +Set agent sandbox proxy name +*/}} +{{- define "retool.agentSandbox.proxy.name" -}} +{{ template "retool.fullname" . }}-agent-sandbox-proxy +{{- end -}} + +{{/* +Selector labels for agent sandbox (sandbox pods / headless service). +*/}} +{{- define "retool.agentSandbox.selectorLabels" -}} +retoolService: {{ include "retool.agentSandbox.name" . }} +{{- end -}} + +{{/* +Extra labels for agent sandbox. +*/}} +{{- define "retool.agentSandbox.labels" -}} +app.kubernetes.io/name: {{ include "retool.agentSandbox.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +telemetry.retool.com/service-name: agent-sandbox +{{- end -}} + +{{/* +Selector labels for agent sandbox controller. +*/}} +{{- define "retool.agentSandbox.controller.selectorLabels" -}} +retoolService: {{ include "retool.agentSandbox.controller.name" . }} +{{- end -}} + +{{/* +Extra labels for agent sandbox controller. +*/}} +{{- define "retool.agentSandbox.controller.labels" -}} +app.kubernetes.io/name: {{ include "retool.agentSandbox.controller.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: controller +telemetry.retool.com/service-name: agent-sandbox-controller +{{- end -}} + +{{/* +Selector labels for agent sandbox proxy. +*/}} +{{- define "retool.agentSandbox.proxy.selectorLabels" -}} +retoolService: {{ include "retool.agentSandbox.proxy.name" . }} +{{- end -}} + +{{/* +Extra labels for agent sandbox proxy. +*/}} +{{- define "retool.agentSandbox.proxy.labels" -}} +app.kubernetes.io/name: {{ include "retool.agentSandbox.proxy.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: proxy +telemetry.retool.com/service-name: agent-sandbox-proxy +{{- end -}} + +{{/* +Validate that an enabled agent sandbox has its required secrets supplied. The +controller and proxy fail to boot without a Postgres connection and a JWT +public key, and the Retool backend needs the JWT private key to sign sandbox +tokens. Each may come from a plaintext value, the per-key existing-secret refs, +or the catch-all externalSecret.name. No-op when agentSandbox is disabled. +*/}} +{{- define "retool.agentSandbox.validateSecrets" -}} +{{- if eq (include "retool.rr.componentEnabled" (dict "root" . "component" "agentSandbox")) "1" -}} +{{- $as := .Values.rr.agentSandbox -}} +{{- $ext := $as.externalSecret.name -}} +{{- $explicitPg := or $as.postgres.url $as.postgres.urlSecretName $as.postgres.host $ext -}} +{{- if not $explicitPg -}} +{{- /* No explicit source: inherit the backend's Postgres connection. */ -}} +{{- if not (include "retool.postgresql.host" . | trimAll "\"") -}} +{{- fail "agentSandbox.enabled defaults to reusing the backend's Postgres connection, but config.postgresql resolved no host. Set agentSandbox.postgres.url / .host / .urlSecretName / externalSecret.name, or configure config.postgresql." -}} +{{- end -}} +{{- if not (or .Values.postgresql.enabled .Values.config.postgresql.passwordSecretName (eq (include "shouldIncludeConfigSecretsEnvVars" . | trim) "1")) -}} +{{- fail "agentSandbox.postgres is unset so it would inherit the backend's Postgres password, but that password is supplied via external secrets (envFrom) and cannot be referenced from a separate pod. Set agentSandbox.postgres.url / .urlSecretName / .host (+ passwordSecretName), or agentSandbox.externalSecret.name." -}} +{{- end -}} +{{- end -}} +{{- if $as.postgres.host -}} +{{- if not (and $as.postgres.user $as.postgres.database) -}} +{{- fail "agentSandbox.postgres.host is set, so postgres.user and postgres.database are also required to assemble the DSN." -}} +{{- end -}} +{{- if not (or $as.postgres.password $as.postgres.passwordSecretName) -}} +{{- fail "agentSandbox.postgres.host is set, so a password is required: set postgres.password or postgres.passwordSecretName. For a passwordless connection (e.g. IAM/trust auth), supply the full connection string via postgres.url or postgres.urlSecretName instead." -}} +{{- end -}} +{{- /* + user and database are embedded verbatim in the assembled DSN, so reject the + characters that would break URL parsing. '@' is allowed in user (managed + services like Azure use user@servername; the parser splits on the last '@'), + but ':' '/' and whitespace would be mis-parsed as a password/host/path. For + values needing other characters, supply a full DSN via postgres.url or + postgres.urlSecretName instead. +*/}} +{{- if regexMatch "[\\s:/?#]" ($as.postgres.user | toString) -}} +{{- fail "agentSandbox.postgres.user contains a character that breaks DSN assembly (whitespace, : / ? #). '@' is fine (e.g. Azure user@server); otherwise supply a full DSN via agentSandbox.postgres.url or postgres.urlSecretName." -}} +{{- end -}} +{{- if regexMatch "[\\s:/?#]" ($as.postgres.database | toString) -}} +{{- fail "agentSandbox.postgres.database contains a character that breaks DSN assembly (whitespace, : / ? #); supply a full DSN via agentSandbox.postgres.url or postgres.urlSecretName." -}} +{{- end -}} +{{- end -}} +{{- if not (or $as.jwtPublicKey $ext) -}} +{{- fail "agentSandbox.enabled requires a JWT public key. Set agentSandbox.jwtPublicKey or agentSandbox.externalSecret.name." -}} +{{- end -}} +{{- if not (or $as.jwtPrivateKey $ext) -}} +{{- fail "agentSandbox.enabled requires a JWT private key (the backend signs sandbox tokens with it). Set agentSandbox.jwtPrivateKey or agentSandbox.externalSecret.name." -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Render the AGENT_SANDBOX_POSTGRES_URL env entry for the controller/proxy (plus a +PGPASSWORD entry when assembling from fields). validateSecrets guarantees one of +these applies, in order: postgres.url -> postgres.host -> postgres.urlSecretName +-> externalSecret.name -> inherit the backend's config.postgresql connection +(the default when nothing agent-specific is set). + +For the host path the password is passed via PGPASSWORD rather than embedded in +the URL: node-postgres reads PGPASSWORD when the connection string omits the +password, so it needs no URL escaping. PGPASSWORD is process-global but safe +here because the controller/proxy open exactly one Postgres connection. user and +database are embedded verbatim (percent-encoding doesn't round-trip here -- the +parser decodes userinfo before splitting on ':', and runs the path through +decodeURI); validateSecrets instead rejects the characters that would break +parsing. An Azure-style "user@servername" is fine -- the parser splits on the +last '@'. +Usage: {{- include "retool.agentSandbox.postgresUrlEnv" . | nindent 12 }} +*/}} +{{- define "retool.agentSandbox.postgresUrlEnv" -}} +{{- $pg := .Values.rr.agentSandbox.postgres -}} +{{- $ext := .Values.rr.agentSandbox.externalSecret.name -}} +{{- if $pg.url }} +- name: AGENT_SANDBOX_POSTGRES_URL + value: {{ $pg.url | quote }} +{{- else if $pg.host }} +{{- $port := $pg.port | default 5432 -}} +{{- if $pg.passwordSecretName }} +- name: PGPASSWORD + valueFrom: + secretKeyRef: + name: {{ $pg.passwordSecretName }} + key: {{ $pg.passwordSecretKey | default "password" }} +{{- else if $pg.password }} +- name: PGPASSWORD + value: {{ $pg.password | quote }} +{{- end }} +- name: AGENT_SANDBOX_POSTGRES_URL + value: {{ printf "postgres://%s@%s:%v/%s" $pg.user $pg.host $port $pg.database | quote }} +{{- else if $pg.urlSecretName }} +- name: AGENT_SANDBOX_POSTGRES_URL + valueFrom: + secretKeyRef: + name: {{ $pg.urlSecretName }} + key: {{ $pg.urlSecretKey | default "postgres-url" }} +{{- else if $ext }} +- name: AGENT_SANDBOX_POSTGRES_URL + valueFrom: + secretKeyRef: + name: {{ $ext }} + key: postgres-url +{{- else }} +{{- /* + Default: inherit the backend's Postgres connection (config.postgresql or the + postgresql subchart) -- same instance/database, separate schema. The password + is sourced from the same secret the backend uses; this block mirrors the + POSTGRES_PASSWORD secretKeyRef in deployment_backend.yaml. validateSecrets + rejects the one combination this can't reach (external-secrets mode with no + discrete password key). +*/}} +- name: PGPASSWORD + valueFrom: + secretKeyRef: + {{- if .Values.postgresql.enabled }} + name: {{ template "retool.postgresql.fullname" . }} + {{- if eq .Values.postgresql.auth.username "postgres" }} + key: postgres-password + {{- else }} + key: password + {{- end }} + {{- else if .Values.config.postgresql.passwordSecretName }} + name: {{ .Values.config.postgresql.passwordSecretName }} + key: {{ .Values.config.postgresql.passwordSecretKey | default "postgresql-password" }} + {{- else }} + name: {{ template "retool.fullname" . }} + key: postgresql-password + {{- end }} +- name: AGENT_SANDBOX_POSTGRES_URL + value: {{ printf "postgres://%s@%s:%s/%s" (include "retool.postgresql.user" . | trimAll "\"") (include "retool.postgresql.host" . | trimAll "\"") (include "retool.postgresql.port" . | trimAll "\"" | default "5432") (include "retool.postgresql.database" . | trimAll "\"") | quote }} +{{- end }} +{{- end -}} + +{{/* +Agent sandbox env vars for the Retool backend, workflow backend, and workers. +Outputs env entries that tell the backend how to reach the agent sandbox services. +Usage: {{- include "retool.agentSandbox.backendEnvVars" . | nindent 10 }} +*/}} +{{- define "retool.agentSandbox.backendEnvVars" -}} +{{- if eq (include "retool.rr.componentEnabled" (dict "root" . "component" "agentSandbox")) "1" }} +{{- $defaultSecretName := .Values.rr.agentSandbox.externalSecret.name | default (include "retool.agentSandbox.name" .) -}} +- name: RR_AGENT_PUBSUB_BACKEND + value: "postgres" +- name: AGENT_SANDBOX_CONTROLLER_INGRESS_DOMAIN + value: {{ .Values.rr.agentSandbox.controllerUrl | default (printf "http://%s:%s" (include "retool.agentSandbox.controller.name" .) (toString .Values.rr.agentSandbox.controller.port)) | quote }} +- name: AGENT_SANDBOX_PROXY_INGRESS_DOMAIN + value: {{ .Values.rr.agentSandbox.proxyUrl | default (printf "http://%s:%s" (include "retool.agentSandbox.proxy.name" .) (toString .Values.rr.agentSandbox.proxy.port)) | quote }} +{{- if .Values.rr.agentSandbox.frontendWsProxyDomain }} +- name: AGENT_SANDBOX_FRONTEND_WS_PROXY_DOMAIN + value: {{ .Values.rr.agentSandbox.frontendWsProxyDomain | quote }} +{{- end }} +{{- if .Values.rr.agentSandbox.jwtPrivateKey }} +- name: AGENT_SANDBOX_JWT_PRIVATE_KEY + value: {{ .Values.rr.agentSandbox.jwtPrivateKey | quote }} +{{- else if .Values.rr.agentSandbox.externalSecret.name }} +- name: AGENT_SANDBOX_JWT_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: {{ $defaultSecretName }} + key: jwt-private-key +{{- end }} +{{- if .Values.rr.agentSandbox.jwtPublicKey }} +- name: AGENT_SANDBOX_JWT_PUBLIC_KEY + value: {{ .Values.rr.agentSandbox.jwtPublicKey | quote }} +{{- else if .Values.rr.agentSandbox.externalSecret.name }} +- name: AGENT_SANDBOX_JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ $defaultSecretName }} + key: jwt-public-key +{{- end }} +{{- if .Values.rr.agentSandbox.encryptionKey }} +- name: AGENT_SANDBOX_ENCRYPTION_KEY + value: {{ .Values.rr.agentSandbox.encryptionKey | quote }} +{{- else if .Values.rr.agentSandbox.externalSecret.name }} +- name: AGENT_SANDBOX_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ $defaultSecretName }} + key: encryption-key +{{- end }} +{{- end }} +{{- end -}} + +{{/* +Set MCP server service name +*/}} +{{- define "retool.mcp.name" -}} +{{ template "retool.fullname" . }}-mcp +{{- end -}} + +{{/* +Set git server deployment/service name (only used when rr.gitServer.separate is enabled) +*/}} +{{- define "retool.gitServer.name" -}} +{{ template "retool.fullname" . }}-git-server +{{- end -}} + +{{/* +Returns "1" when the git server should run as its own deployment/service +(rr.gitServer.enabled AND rr.gitServer.separate.enabled), empty otherwise. +*/}} +{{- define "retool.gitServer.separateEnabled" -}} +{{- if and .Values.rr.gitServer.enabled (.Values.rr.gitServer.separate | default dict).enabled -}} +1 +{{- end -}} +{{- end -}} + +{{/* +Port the standalone git server listens on (RR_GIT_SERVER_PORT) and exposes via its service. +*/}} +{{- define "retool.gitServer.port" -}} +{{- (.Values.rr.gitServer.separate | default dict).port | default 3010 -}} +{{- end -}} + +{{/* +In-cluster URL of the standalone git server service, e.g. http://-git-server:3010. +Used to point the MCP server (and any other consumer) at the split-out git server. +*/}} +{{- define "retool.gitServer.url" -}} +http://{{ template "retool.gitServer.name" . }}:{{ include "retool.gitServer.port" . }} +{{- end -}} + +{{/* +Blob-storage + git repack env vars shared by the in-process git server (main +backend) and the standalone git server deployment. git_server stores all +objects/packs in blob storage; the same RR_DEFAULT_* vars are also used by +snapshots. Emits nothing when no blobStorage provider is configured (in which +case the user is expected to plumb RR_BLOB_STORAGE_PROVIDER / RR_DEFAULT_* +directly via environmentVariables / environmentSecrets). +*/}} +{{- define "retool.gitServer.commonEnv" -}} +{{- $bs := .Values.rr.blobStorage | default dict }} +{{- if $bs.s3 }} +- name: RR_BLOB_STORAGE_PROVIDER + value: "s3" +- name: RR_DEFAULT_S3_BUCKET + value: {{ $bs.s3.bucket | quote }} +{{- if $bs.s3.region }} +- name: RR_DEFAULT_S3_REGION + value: {{ $bs.s3.region | quote }} +{{- end }} +{{- if $bs.s3.endpoint }} +- name: RR_DEFAULT_S3_ENDPOINT + value: {{ $bs.s3.endpoint | quote }} +{{- end }} +{{- if $bs.s3.accessKeyId }} +- name: RR_DEFAULT_S3_ACCESS_KEY_ID + value: {{ $bs.s3.accessKeyId | quote }} +{{- end }} +{{- if $bs.s3.secretAccessKeySecretName }} +- name: RR_DEFAULT_S3_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ $bs.s3.secretAccessKeySecretName }} + key: {{ $bs.s3.secretAccessKeySecretKey | default "secret-access-key" }} +{{- else if $bs.s3.secretAccessKey }} +- name: RR_DEFAULT_S3_SECRET_ACCESS_KEY + value: {{ $bs.s3.secretAccessKey | quote }} +{{- end }} +{{- else if $bs.gcs }} +- name: RR_BLOB_STORAGE_PROVIDER + value: "gcs" +- name: RR_DEFAULT_GCS_BUCKET + value: {{ $bs.gcs.bucket | quote }} +{{- if $bs.gcs.credentialsSecretName }} +- name: RR_DEFAULT_GCS_CREDENTIALS + valueFrom: + secretKeyRef: + name: {{ $bs.gcs.credentialsSecretName }} + key: {{ $bs.gcs.credentialsSecretKey | default "credentials.json" }} +{{- else if $bs.gcs.credentials }} +- name: RR_DEFAULT_GCS_CREDENTIALS + value: {{ $bs.gcs.credentials | quote }} +{{- end }} +{{- else if $bs.azure }} +- name: RR_BLOB_STORAGE_PROVIDER + value: "azure" +- name: RR_DEFAULT_AZURE_CONTAINER + value: {{ $bs.azure.container | quote }} +{{- if $bs.azure.connectionStringSecretName }} +- name: RR_DEFAULT_AZURE_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: {{ $bs.azure.connectionStringSecretName }} + key: {{ $bs.azure.connectionStringSecretKey | default "connection-string" }} +{{- else if $bs.azure.connectionString }} +- name: RR_DEFAULT_AZURE_CONNECTION_STRING + value: {{ $bs.azure.connectionString | quote }} +{{- end }} +{{- end }} +{{- if .Values.rr.gitServer.repackThreshold }} +- name: RR_GIT_REPACK_THRESHOLD + value: {{ .Values.rr.gitServer.repackThreshold | quote }} +{{- end }} +{{- end -}} + +{{/* +Validate that exactly one blob-storage provider is configured when rr.gitServer +is enabled. Skipped when the user has plumbed the RR_BLOB_STORAGE_PROVIDER / +RR_DEFAULT_*_* env vars in directly via env/environmentVariables/environmentSecrets, +which is treated as an opt-out from the first-class blobStorage config. +Also skipped entirely when rr.gitServer.skipBlobStorageValidation is true, which +is the escape hatch for sources we cannot inspect at template time (e.g. env +vars injected via envFrom from a Secret/ConfigMap). +No-op when rr.gitServer is disabled. +*/}} +{{- define "retool.gitServer.validateBlobStorage" -}} +{{- if and .Values.rr.gitServer.enabled (not .Values.rr.gitServer.skipBlobStorageValidation) -}} +{{- $hasDirectEnv := false -}} +{{- range $name, $value := .Values.env -}} +{{- if or (hasPrefix "RR_DEFAULT_" $name) (eq $name "RR_BLOB_STORAGE_PROVIDER") -}} +{{- $hasDirectEnv = true -}} +{{- end -}} +{{- end -}} +{{- range .Values.environmentVariables -}} +{{- if or (hasPrefix "RR_DEFAULT_" .name) (eq .name "RR_BLOB_STORAGE_PROVIDER") -}} +{{- $hasDirectEnv = true -}} +{{- end -}} +{{- end -}} +{{- range .Values.environmentSecrets -}} +{{- if or (hasPrefix "RR_DEFAULT_" .name) (eq .name "RR_BLOB_STORAGE_PROVIDER") -}} +{{- $hasDirectEnv = true -}} +{{- end -}} +{{- end -}} +{{- if not $hasDirectEnv -}} +{{- $bs := .Values.rr.blobStorage | default dict -}} +{{- $providers := list -}} +{{- if $bs.s3 }}{{ $providers = append $providers "s3" }}{{ end -}} +{{- if $bs.gcs }}{{ $providers = append $providers "gcs" }}{{ end -}} +{{- if $bs.azure }}{{ $providers = append $providers "azure" }}{{ end -}} +{{- if ne (len $providers) 1 -}} +{{- fail "rr.gitServer.enabled requires exactly one of rr.blobStorage.s3, rr.blobStorage.gcs, rr.blobStorage.azure to be configured, or set RR_BLOB_STORAGE_PROVIDER / RR_DEFAULT_* directly via env / environmentVariables / environmentSecrets. If those vars are supplied another way (e.g. envFrom), set rr.gitServer.skipBlobStorageValidation=true to bypass this check." -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Guard against the pre-rename RR values layout. The whole stack used to be named +"r2" (top-level `r2:` master switch), and its components used to be top-level +keys (jsExecutor, r2Agent, agentSandbox, rrGitServer, blobStorage); everything is +now named "rr" and nested under .Values.rr. A chart upgrade would otherwise +SILENTLY ignore any config still set under the old names — quietly disabling RR — +so fail loudly with the exact key moves instead. + +Two classes of stale config are caught: + 1. Old TOP-LEVEL keys (the master switch and the un-nested components). + 2. Old component LEAF names nested under the new `rr:` block (e.g. someone + who moved config under `rr:` but kept `r2Agent`/`rrAgent`/`rrGitServer`/ + `rrBlobStorage` instead of the renamed `agent`/`gitServer`/`blobStorage`). +*/}} +{{- define "retool.rr.validateLegacyValues" -}} +{{- $found := list -}} +{{/* 1. old top-level keys */}} +{{- $topMoves := list + (list "r2" "rr") + (list "jsExecutor" "rr.jsExecutor") + (list "r2Agent" "rr.agent") + (list "agentSandbox" "rr.agentSandbox") + (list "rrGitServer" "rr.gitServer") + (list "blobStorage" "rr.blobStorage") -}} +{{- range $move := $topMoves -}} +{{- if hasKey $.Values (index $move 0) -}} +{{- $found = append $found (printf " %s: -> %s:" (index $move 0) (index $move 1)) -}} +{{- end -}} +{{- end -}} +{{/* 2. old leaf names nested under rr: */}} +{{- $rr := $.Values.rr | default dict -}} +{{- $childMoves := list + (list "r2Agent" "rr.agent") + (list "rrAgent" "rr.agent") + (list "rrGitServer" "rr.gitServer") + (list "rrBlobStorage" "rr.blobStorage") -}} +{{- range $move := $childMoves -}} +{{- if hasKey $rr (index $move 0) -}} +{{- $found = append $found (printf " rr.%s: -> %s:" (index $move 0) (index $move 1)) -}} +{{- end -}} +{{- end -}} +{{- if $found -}} +{{- fail (printf "\n\nACTION REQUIRED: update your Helm values file.\n\nThe RR (formerly \"r2\") values layout changed: the master switch and every component it needs now live under the top-level `rr:` block. The keys below are still set in your values but are NO LONGER READ, which would silently disable RR. This deploy is blocked until you fix it.\n\nTo fix: edit your values file (values.yaml / your Helm values overrides) and rename / move these keys:\n\n%s\n\nThe master switch is now `rr.enabled`. See the chart's values.yaml for the full new layout." (join "\n" $found)) -}} +{{- end -}} +{{- end -}} + {{/* Set code executor image tag Usage: (template "retool.codeExecutor.image.tag" .) @@ -398,6 +1048,27 @@ Usage: (template "retool.codeExecutor.image.tag" .) {{- end -}} {{- end -}} +{{/* +Set JS executor image tag +Usage: (template "retool.jsExecutor.image.tag" .) +*/}} +{{- define "retool.jsExecutor.image.tag" -}} +{{- if .Values.rr.jsExecutor.image.tag -}} + {{- .Values.rr.jsExecutor.image.tag -}} +{{- else if .Values.image.tag -}} + {{- $valid_retool_version_regexp := "([0-9]+\\.[0-9]+(\\.[0-9]+)?(-[a-zA-Z0-9]+)?)" }} + {{- $semver_version_regexp := "[0-9]+\\.[0-9]+(\\.[0-9]+)?" }} + {{- $retool_version_with_ce := ( and ( regexMatch $valid_retool_version_regexp $.Values.image.tag ) ( semverCompare ">= 3.20.15-0" ( regexFind $semver_version_regexp $.Values.image.tag ) ) ) }} + {{- if $retool_version_with_ce -}} + {{- .Values.image.tag -}} + {{- else -}} + {{- "1.1.0" -}} + {{- end -}} +{{- else -}} + {{- fail "Please set a value for .Values.image.tag or .Values.rr.jsExecutor.image.tag" }} +{{- end -}} +{{- end -}} + {{- define "retool_version_with_java_dbconnector_opt_out" -}} {{- $output := "" -}} {{- $valid_retool_version_regexp := "([0-9]+\\.[0-9]+(\\.[0-9]+)?(-[a-zA-Z0-9]+)?)" }} @@ -415,22 +1086,6 @@ Usage: (template "retool.codeExecutor.image.tag" .) {{/* Checks whether or not ExternalSecret definitions are enabled and can potentially clobber secrets or explicitly allow additional direct secret refs. */}} -{{/* -Render env vars from .Values.env, handling both string values and object values (e.g. valueFrom). -Usage: {{- include "retool.env" .Values.env | nindent 10 }} -*/}} -{{- define "retool.env" -}} -{{- range $key, $value := . }} -{{- if not (kindIs "map" $value) }} -- name: "{{ $key }}" - value: "{{ $value }}" -{{- else }} -- name: "{{ $key }}" -{{ toYaml $value | indent 2 }} -{{- end }} -{{- end }} -{{- end -}} - {{- define "shouldIncludeConfigSecretsEnvVars" -}} {{- $output := "" -}} {{- if or (not (or (.Values.externalSecrets.enabled) (.Values.externalSecrets.externalSecretsOperator.enabled))) .Values.externalSecrets.includeConfigSecrets -}} diff --git a/charts/retool/templates/_workers.tpl b/charts/retool/templates/_workers.tpl index 989bba9..36c1e7e 100644 --- a/charts/retool/templates/_workers.tpl +++ b/charts/retool/templates/_workers.tpl @@ -1,8 +1,18 @@ +{{/* +Worker descriptors. `parent` is the values key holding the worker's config and +also names its enable helper (retool..enabled). `type` selects the +per-worker rendering (resource name, SERVICE_TYPE, taskqueue). `nested`, when +set, is the parent values block the config lives under (e.g. the rr stack +keeps its workers under .Values.rr); omitted means the key is top-level. +*/}} {{- define "retool.workers" -}} - parent: agents type: agent - parent: agents type: agentEval +- parent: agent + type: rrAgent + nested: rr - parent: workflows type: workflow {{- end -}} @@ -13,7 +23,7 @@ {{- range $worker := $workers -}} {{- if eq (include (printf "retool.%s.enabled" $worker.parent) $root) "1" -}} -{{ include "retool.worker.deployment" (dict "root" $root "parent" $worker.parent "workerType" $worker.type) }} +{{ include "retool.worker.deployment" (dict "root" $root "parent" $worker.parent "workerType" $worker.type "nested" $worker.nested) }} {{- end -}} {{- end -}} {{- end -}} @@ -22,7 +32,11 @@ {{- $ := .root -}} {{- $parent := .parent -}} {{- $workerType := .workerType -}} -{{- $parentValues := index $.Values $parent -}} +{{- $owner := $.Values -}} +{{- if .nested -}} +{{- $owner = index $.Values .nested -}} +{{- end -}} +{{- $parentValues := index $owner $parent -}} {{- $workerValues := $parentValues.worker -}} {{- if eq $workerType "agentEval" -}} @@ -36,9 +50,20 @@ {{- end }} {{- end -}} -{{- $healthcheckPort := ternary 3012 3005 (eq $workerType "agentEval") -}} -{{- $serviceType := ternary "AGENT_EVAL_TEMPORAL_WORKER" "WORKFLOW_TEMPORAL_WORKER" (eq $workerType "agentEval") -}} -{{- $taskqueue := ternary "agent-eval" (ternary "agent" "" (eq $workerType "agent")) (eq $workerType "agentEval") -}} +{{- $healthcheckPort := 3005 -}} +{{- $serviceType := "WORKFLOW_TEMPORAL_WORKER" -}} +{{- $taskqueue := "" -}} +{{- if eq $workerType "agentEval" -}} + {{- $healthcheckPort = 3012 -}} + {{- $serviceType = "AGENT_EVAL_TEMPORAL_WORKER" -}} + {{- $taskqueue = "agent-eval" -}} +{{- else if eq $workerType "rrAgent" -}} + {{- $healthcheckPort = 3016 -}} + {{- $serviceType = "R2_AGENT_TEMPORAL_WORKER" -}} + {{- $taskqueue = "r2-agent" -}} +{{- else if eq $workerType "agent" -}} + {{- $taskqueue = "agent" -}} +{{- end -}} {{/* yaml starts here */}} apiVersion: apps/v1 @@ -100,7 +125,7 @@ spec: {{- end }} {{- end }} containers: - - name: {{ if eq $workerType "agentEval" }}agent-eval-worker{{ else }}{{ $workerType }}-worker{{ end }} + - name: {{ if eq $workerType "agentEval" }}agent-eval-worker{{ else if eq $workerType "rrAgent" }}r2-agent-worker{{ else }}{{ $workerType }}-worker{{ end }} image: "{{ $.Values.image.repository }}:{{ required "Please set a value for .Values.image.tag" $.Values.image.tag }}" imagePullPolicy: {{ $.Values.image.pullPolicy }} args: @@ -200,6 +225,11 @@ spec: value: {{ template "retool.postgresql.ssl_enabled" $ }} - name: CODE_EXECUTOR_INGRESS_DOMAIN value: http://{{ template "retool.codeExecutor.name" $ }} + {{- if eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "jsExecutor")) "1" }} + - name: JS_EXECUTOR_INGRESS_DOMAIN + value: http://{{ template "retool.jsExecutor.name" $ }} + {{- end }} + {{- include "retool.agentSandbox.backendEnvVars" $ | nindent 10 }} {{- include "retool.telemetry.includeEnvVars" $ | nindent 10 }} diff --git a/charts/retool/templates/agent_sandbox_device_plugin.yaml b/charts/retool/templates/agent_sandbox_device_plugin.yaml new file mode 100644 index 0000000..aa46d3f --- /dev/null +++ b/charts/retool/templates/agent_sandbox_device_plugin.yaml @@ -0,0 +1,93 @@ +{{- if and (eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "agentSandbox")) "1") .Values.rr.agentSandbox.sandboxNetwork.deployDaemonSet }} +{{- $as := .Values.rr.agentSandbox -}} +{{- $nodeSelector := $as.nodeSelector | default .Values.nodeSelector -}} +{{- $tolerations := $as.tolerations | default .Values.tolerations -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "retool.agentSandbox.name" . }}-device-plugin + labels: + {{- include "retool.agentSandbox.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} + app.kubernetes.io/component: device-plugin +data: + conf.yaml: | + - devicematch: ^net/tun$ + nummaxdevices: {{ $as.devicePlugin.maxDevices | default 130 }} +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "retool.agentSandbox.name" . }}-device-plugin + labels: + {{- include "retool.agentSandbox.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} + app.kubernetes.io/component: device-plugin +spec: + selector: + matchLabels: + retoolService: {{ include "retool.agentSandbox.name" . }}-device-plugin + template: + metadata: + labels: + retoolService: {{ include "retool.agentSandbox.name" . }}-device-plugin + app.kubernetes.io/name: {{ include "retool.agentSandbox.name" . }}-device-plugin + app.kubernetes.io/instance: {{ .Release.Name }} + {{- include "retool.labels" . | nindent 8 }} +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} + spec: + automountServiceAccountToken: false + {{- if $as.devicePlugin.priorityClassName }} + priorityClassName: {{ $as.devicePlugin.priorityClassName }} + {{- end }} +{{- if $nodeSelector }} + nodeSelector: +{{ toYaml $nodeSelector | indent 8 }} +{{- end }} + tolerations: +{{ toYaml $tolerations | indent 8 }} + containers: + - name: smarter-device-manager + image: "{{ $as.devicePlugin.image.repository }}:{{ $as.devicePlugin.image.tag }}" + imagePullPolicy: IfNotPresent + terminationMessagePath: /tmp/termination-log + terminationMessagePolicy: FallbackToLogsOnError + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: device-plugin + mountPath: /var/lib/kubelet/device-plugins + - name: dev + mountPath: /dev + readOnly: true + - name: sys + mountPath: /sys + readOnly: true + - name: config + mountPath: /root/config + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 100m + memory: 32Mi + volumes: + - name: device-plugin + hostPath: + path: /var/lib/kubelet/device-plugins + - name: dev + hostPath: + path: /dev + - name: sys + hostPath: + path: /sys + - name: config + configMap: + name: {{ include "retool.agentSandbox.name" . }}-device-plugin +{{- end }} diff --git a/charts/retool/templates/agent_sandbox_networkpolicy.yaml b/charts/retool/templates/agent_sandbox_networkpolicy.yaml new file mode 100644 index 0000000..4ba72d4 --- /dev/null +++ b/charts/retool/templates/agent_sandbox_networkpolicy.yaml @@ -0,0 +1,216 @@ +{{- if and (eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "agentSandbox")) "1") .Values.rr.agentSandbox.networkPolicy.enabled }} +{{- $as := .Values.rr.agentSandbox -}} +{{- /* +======================================================================= + Sandbox Pod NetworkPolicy — restrict ingress/egress for executor Jobs +======================================================================= +*/}} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "retool.agentSandbox.name" . }} + labels: + {{- include "retool.agentSandbox.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "retool.agentSandbox.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + - Egress + ingress: + - from: + {{- if $as.networkPolicy.ingressFrom }} + {{- toYaml $as.networkPolicy.ingressFrom | nindent 8 }} + {{- else }} + - podSelector: + matchLabels: + {{- include "retool.selectorLabels" . | nindent 14 }} + {{- end }} + ports: + - port: {{ $as.sandbox.port }} + protocol: TCP + - from: + - podSelector: + matchLabels: + {{- include "retool.agentSandbox.controller.selectorLabels" . | nindent 14 }} + ports: + - port: {{ $as.sandbox.port }} + protocol: TCP + - from: + - podSelector: + matchLabels: + {{- include "retool.agentSandbox.proxy.selectorLabels" . | nindent 14 }} + ports: + - port: {{ $as.sandbox.port }} + protocol: TCP + egress: + {{- if $as.networkPolicy.dnsSelector }} + - to: + - namespaceSelector: + {{- toYaml $as.networkPolicy.dnsSelector.namespaceSelector | nindent 12 }} + podSelector: + {{- toYaml $as.networkPolicy.dnsSelector.podSelector | nindent 12 }} + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + {{- end }} + - to: + - podSelector: + matchLabels: + {{- include "retool.agentSandbox.proxy.selectorLabels" . | nindent 14 }} + ports: + - port: {{ $as.proxy.port }} + protocol: TCP + {{- with $as.networkPolicy.extraEgress }} + {{- toYaml . | nindent 4 }} + {{- end }} +--- +{{- /* +======================================================================= + Controller NetworkPolicy +======================================================================= +*/}} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "retool.agentSandbox.controller.name" . }} + labels: + {{- include "retool.agentSandbox.controller.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "retool.agentSandbox.controller.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + {{- include "retool.selectorLabels" . | nindent 14 }} + ports: + - port: {{ $as.controller.port }} + protocol: TCP + egress: + {{- if $as.networkPolicy.dnsSelector }} + - to: + - namespaceSelector: + {{- toYaml $as.networkPolicy.dnsSelector.namespaceSelector | nindent 12 }} + podSelector: + {{- toYaml $as.networkPolicy.dnsSelector.podSelector | nindent 12 }} + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + {{- end }} + - to: + - podSelector: + matchLabels: + {{- include "retool.agentSandbox.selectorLabels" . | nindent 14 }} + ports: + - port: {{ $as.sandbox.port }} + protocol: TCP + - to: + - ipBlock: + cidr: 0.0.0.0/0 + ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP +--- +{{- /* +======================================================================= + Proxy NetworkPolicy +======================================================================= +*/}} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "retool.agentSandbox.proxy.name" . }} + labels: + {{- include "retool.agentSandbox.proxy.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "retool.agentSandbox.proxy.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + {{- include "retool.agentSandbox.selectorLabels" . | nindent 14 }} + ports: + - port: {{ $as.proxy.port }} + protocol: TCP + - from: + - podSelector: + matchLabels: + {{- include "retool.selectorLabels" . | nindent 14 }} + ports: + - port: {{ $as.proxy.port }} + protocol: TCP + {{- if ($as.proxy.ingress).enabled }} + {{- if $as.proxy.ingress.networkPolicy }} + - from: + - podSelector: + matchLabels: + {{- toYaml $as.proxy.ingress.networkPolicy.podSelector | nindent 14 }} + ports: + - port: {{ $as.proxy.port }} + protocol: TCP + {{- end }} + {{- end }} + egress: + {{- if $as.networkPolicy.dnsSelector }} + - to: + - namespaceSelector: + {{- toYaml $as.networkPolicy.dnsSelector.namespaceSelector | nindent 12 }} + podSelector: + {{- toYaml $as.networkPolicy.dnsSelector.podSelector | nindent 12 }} + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + {{- end }} + - to: + - podSelector: + matchLabels: + {{- include "retool.agentSandbox.selectorLabels" . | nindent 14 }} + ports: + - port: {{ $as.sandbox.port }} + protocol: TCP + {{- if $as.networkPolicy.backendAllowlist }} + - to: + {{- range $as.networkPolicy.backendAllowlist }} + - ipBlock: + cidr: {{ . }} + {{- end }} + {{- end }} + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + {{- range $as.networkPolicy.blockedRanges }} + - {{ . }} + {{- end }} + {{- if $as.networkPolicy.blockedRanges6 }} + - to: + - ipBlock: + cidr: ::/0 + except: + {{- range $as.networkPolicy.blockedRanges6 }} + - {{ . }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/retool/templates/agent_sandbox_prepuller.yaml b/charts/retool/templates/agent_sandbox_prepuller.yaml new file mode 100644 index 0000000..817c0ce --- /dev/null +++ b/charts/retool/templates/agent_sandbox_prepuller.yaml @@ -0,0 +1,84 @@ +{{- if eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "agentSandbox")) "1" }} +{{- $as := .Values.rr.agentSandbox -}} +{{- $nodeSelector := $as.nodeSelector | default .Values.nodeSelector -}} +{{- $tolerations := $as.tolerations | default .Values.tolerations -}} +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "retool.agentSandbox.name" . }}-image-prepuller + labels: + {{- include "retool.agentSandbox.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} + app.kubernetes.io/component: image-prepuller +spec: + selector: + matchLabels: + retoolService: {{ include "retool.agentSandbox.name" . }}-image-prepuller + updateStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 100% + template: + metadata: + labels: + retoolService: {{ include "retool.agentSandbox.name" . }}-image-prepuller + app.kubernetes.io/name: {{ include "retool.agentSandbox.name" . }}-image-prepuller + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: image-prepuller + {{- include "retool.labels" . | nindent 8 }} +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} + spec: + automountServiceAccountToken: false +{{- if $nodeSelector }} + nodeSelector: +{{ toYaml $nodeSelector | indent 8 }} +{{- end }} + tolerations: +{{ toYaml $tolerations | indent 8 }} + terminationGracePeriodSeconds: 5 + initContainers: + - name: pull-image + image: "{{ $as.image.repository }}:{{ $as.image.tag | default .Values.image.tag }}" + imagePullPolicy: {{ $as.image.pullPolicy }} + command: ["true"] + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1001 + capabilities: + drop: ["ALL"] + resources: + requests: + cpu: 1m + memory: 4Mi + limits: + cpu: 10m + memory: 16Mi + containers: + - name: pause + image: "{{ $as.initImage.repository }}:{{ $as.initImage.tag }}{{- if $as.initImage.digest }}@{{ $as.initImage.digest }}{{- end }}" + command: ["sleep", "infinity"] + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1001 + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + resources: + requests: + cpu: 1m + memory: 4Mi + limits: + cpu: 10m + memory: 16Mi +{{- if .Values.image.pullSecrets }} + imagePullSecrets: +{{ toYaml .Values.image.pullSecrets | indent 8 }} +{{- end }} +{{- end }} diff --git a/charts/retool/templates/agent_sandbox_seccomp.yaml b/charts/retool/templates/agent_sandbox_seccomp.yaml new file mode 100644 index 0000000..4550dd9 --- /dev/null +++ b/charts/retool/templates/agent_sandbox_seccomp.yaml @@ -0,0 +1,91 @@ +{{- if eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "agentSandbox")) "1" }} +{{- $as := .Values.rr.agentSandbox -}} +{{- $nodeSelector := $as.nodeSelector | default .Values.nodeSelector -}} +{{- $tolerations := $as.tolerations | default .Values.tolerations -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "retool.agentSandbox.name" . }}-seccomp + labels: + {{- include "retool.agentSandbox.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +data: + gvisor-seccomp.json: | + {{- .Files.Get "files/gvisor-seccomp.json" | nindent 4 }} +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "retool.agentSandbox.name" . }}-node-installer + labels: + {{- include "retool.agentSandbox.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} + app.kubernetes.io/component: node-installer +spec: + selector: + matchLabels: + retoolService: {{ include "retool.agentSandbox.name" . }}-node-installer + template: + metadata: + labels: + retoolService: {{ include "retool.agentSandbox.name" . }}-node-installer + app.kubernetes.io/name: {{ include "retool.agentSandbox.name" . }}-node-installer + app.kubernetes.io/instance: {{ .Release.Name }} + {{- include "retool.labels" . | nindent 8 }} +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} + spec: + automountServiceAccountToken: false +{{- if $nodeSelector }} + nodeSelector: +{{ toYaml $nodeSelector | indent 8 }} +{{- end }} + tolerations: +{{ toYaml $tolerations | indent 8 }} + initContainers: + - name: install + image: "{{ $as.initImage.repository }}:{{ $as.initImage.tag }}{{- if $as.initImage.digest }}@{{ $as.initImage.digest }}{{- end }}" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + command: + - /bin/sh + - -c + - | + DEST="/host-seccomp/{{ $as.seccompProfile }}" + mkdir -p "$(dirname "$DEST")" + cp /seccomp-profile/gvisor-seccomp.json "$DEST" + echo "seccomp profile installed at $DEST" + volumeMounts: + - name: seccomp-profile + mountPath: /seccomp-profile + - name: host-seccomp + mountPath: /host-seccomp + containers: + - name: pause + image: "{{ $as.initImage.repository }}:{{ $as.initImage.tag }}{{- if $as.initImage.digest }}@{{ $as.initImage.digest }}{{- end }}" + command: ["sleep", "infinity"] + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + requests: + cpu: 1m + memory: 4Mi + limits: + cpu: 10m + memory: 16Mi + volumes: + - name: seccomp-profile + configMap: + name: {{ include "retool.agentSandbox.name" . }}-seccomp + - name: host-seccomp + hostPath: + path: /var/lib/kubelet/seccomp + type: DirectoryOrCreate +{{- end }} diff --git a/charts/retool/templates/configmap_code_executor.yaml b/charts/retool/templates/configmap_code_executor.yaml new file mode 100644 index 0000000..026ba90 --- /dev/null +++ b/charts/retool/templates/configmap_code_executor.yaml @@ -0,0 +1,13 @@ +{{- if include "retool.workflows.enabled" . }} +{{- if and (not .Values.codeExecutor.securityContext) .Values.codeExecutor.useSeccompProfile }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "retool.fullname" . }}-code-executor-seccomp + labels: + {{- include "retool.labels" . | nindent 4 }} +data: + nsjail-seccomp.json: | + {{- .Files.Get "files/nsjail-seccomp.json" | nindent 4 }} +{{- end }} +{{- end }} diff --git a/charts/retool/templates/configmap_js_executor.yaml b/charts/retool/templates/configmap_js_executor.yaml new file mode 100644 index 0000000..413e675 --- /dev/null +++ b/charts/retool/templates/configmap_js_executor.yaml @@ -0,0 +1,11 @@ +{{- if eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "jsExecutor")) "1" }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "retool.fullname" . }}-js-executor-seccomp + labels: + {{- include "retool.labels" . | nindent 4 }} +data: + nsjail-seccomp.json: | + {{- .Files.Get "files/nsjail-seccomp.json" | nindent 4 }} +{{- end }} diff --git a/charts/retool/templates/deployment_agent_sandbox.yaml b/charts/retool/templates/deployment_agent_sandbox.yaml new file mode 100644 index 0000000..2dd1039 --- /dev/null +++ b/charts/retool/templates/deployment_agent_sandbox.yaml @@ -0,0 +1,714 @@ +{{- if eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "agentSandbox")) "1" }} +{{- include "retool.agentSandbox.validateSecrets" . }} +{{- $as := .Values.rr.agentSandbox -}} +{{- $defaultSecretName := $as.externalSecret.name | default (include "retool.agentSandbox.name" .) -}} +{{- $nodeSelector := $as.nodeSelector | default .Values.nodeSelector -}} +{{- $tolerations := $as.tolerations | default .Values.tolerations -}} +{{- /* +======================================================================= + Secret (skipped when externalSecret.name is set) +======================================================================= +*/}} +{{- if not $as.externalSecret.name }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "retool.agentSandbox.name" . }} + labels: + {{- include "retool.agentSandbox.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +type: Opaque +data: + jwt-public-key: {{ $as.jwtPublicKey | default "" | b64enc | quote }} + jwt-private-key: {{ $as.jwtPrivateKey | default "" | b64enc | quote }} + encryption-key: {{ $as.encryptionKey | default "" | b64enc | quote }} + api-secret: {{ $as.apiSecret | default "" | b64enc | quote }} + {{- if $as.postgres.url }} + postgres-url: {{ $as.postgres.url | b64enc | quote }} + {{- end }} +--- +{{- end }} +{{- /* +======================================================================= + RBAC for the controller (needs to manage Jobs, Pods, ConfigMaps) +======================================================================= +*/}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "retool.agentSandbox.controller.name" . }} + labels: + {{- include "retool.agentSandbox.controller.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "retool.agentSandbox.controller.name" . }} + labels: + {{- include "retool.agentSandbox.controller.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +rules: + - apiGroups: ["apps"] + resources: ["deployments"] + resourceNames: ["{{ include "retool.agentSandbox.name" . }}"] + verbs: ["get", "list", "watch", "update", "patch"] + - apiGroups: ["apps"] + resources: ["deployments/scale"] + resourceNames: ["{{ include "retool.agentSandbox.name" . }}"] + verbs: ["get", "patch"] + - apiGroups: ["apps"] + resources: ["daemonsets"] + verbs: ["get", "list", "watch"] + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "create", "delete"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "retool.agentSandbox.controller.name" . }} + labels: + {{- include "retool.agentSandbox.controller.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "retool.agentSandbox.controller.name" . }} +subjects: + - kind: ServiceAccount + name: {{ include "retool.agentSandbox.controller.name" . }} + namespace: {{ .Release.Namespace }} +--- +{{- /* +======================================================================= + Job Template ConfigMap — defines the K8s Job spec the controller uses + to create sandbox pods. +======================================================================= +*/}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "retool.agentSandbox.name" . }}-job-template + labels: + {{- include "retool.agentSandbox.controller.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +data: + job-template.json: | + { + "apiVersion": "batch/v1", + "kind": "Job", + "metadata": { + "labels": { + "retoolService": "{{ include "retool.agentSandbox.name" . }}", + "app.kubernetes.io/name": "{{ include "retool.agentSandbox.name" . }}" + } + }, + "spec": { + "backoffLimit": 0, + "ttlSecondsAfterFinished": {{ $as.controller.scaling.jobRetentionSeconds }}, + "template": { + "metadata": { + "annotations": { + "karpenter.sh/do-not-disrupt": "true" + }, + "labels": { + "retoolService": "{{ include "retool.agentSandbox.name" . }}", + "app.kubernetes.io/name": "{{ include "retool.agentSandbox.name" . }}" + } + }, + "spec": { + "restartPolicy": "Never", + "subdomain": "{{ include "retool.agentSandbox.name" . }}-pods", + "automountServiceAccountToken": false, + {{- if $nodeSelector }} + "nodeSelector": {{ toJson $nodeSelector }}, + {{- end }} + {{- if $tolerations }} + "tolerations": {{ toJson $tolerations }}, + {{- end }} + "initContainers": [ + { + "name": "rootfs-etc-copy", + "image": "{{ $as.image.repository }}:__IMAGE_TAG__", + "command": ["/bin/sh", "-c", "cp -r /opt/sandbox-env/rootfs/etc/. /mnt/etc/"], + "securityContext": { + "runAsUser": 0, + "allowPrivilegeEscalation": false, + "readOnlyRootFilesystem": true, + "capabilities": {"drop": ["ALL"], "add": ["DAC_READ_SEARCH"]} + }, + "volumeMounts": [ + {"name": "rootfs-etc", "mountPath": "/mnt/etc"} + ], + "resources": { + "requests": {"cpu": "10m", "memory": "16Mi"}, + "limits": {"cpu": "100m", "memory": "32Mi"} + } + } + ], + "containers": [ + { + "name": "agent-sandbox", + "image": "{{ $as.image.repository }}:__IMAGE_TAG__", + "ports": [{"containerPort": {{ $as.sandbox.port }}, "protocol": "TCP"}], + "securityContext": { + "runAsUser": 1001, + "runAsGroup": 1001, + "allowPrivilegeEscalation": false, + "readOnlyRootFilesystem": true, + "capabilities": {"drop": ["ALL"]}, + "seccompProfile": {"type": "Localhost", "localhostProfile": "{{ $as.seccompProfile }}"}, + "appArmorProfile": {"type": "Unconfined"} + }, + "env": [ + {"name": "NODE_ENV", "value": "production"} + ,{"name": "EXECUTOR_PORT", "value": "{{ $as.sandbox.port }}"} + ,{"name": "POD_NAME", "valueFrom": {"fieldRef": {"fieldPath": "metadata.name"}}} + ,{"name": "POD_UID", "valueFrom": {"fieldRef": {"fieldPath": "metadata.uid"}}} + ,{"name": "POD_IP", "valueFrom": {"fieldRef": {"fieldPath": "status.podIP"}}} + ,{"name": "SANDBOX_NETWORK_ENABLED", "value": "{{ $as.sandboxNetwork.enabled }}"} + ,{"name": "SANDBOX_IDLE_TIMEOUT_MS", "value": "{{ $as.sandbox.sandboxIdleTimeoutMs }}"} + ,{"name": "SANDBOX_GLOBAL_LIFETIME_MS", "value": "{{ $as.sandbox.sandboxGlobalLifetimeMs }}"} + ,{"name": "SANDBOX_READY_TIMEOUT_MS", "value": "{{ $as.sandbox.sandboxReadyTimeoutMs }}"} + {{- if $as.jwtPublicKey }} + ,{"name": "AGENT_SANDBOX_JWT_PUBLIC_KEY", "value": {{ $as.jwtPublicKey | toJson }}} + {{- else if $as.externalSecret.name }} + ,{"name": "AGENT_SANDBOX_JWT_PUBLIC_KEY", "valueFrom": {"secretKeyRef": {"name": "{{ $defaultSecretName }}", "key": "jwt-public-key"}}} + {{- end }} + {{- if $as.proxy.backendDomainSuffixes }} + ,{"name": "BACKEND_DOMAIN_SUFFIXES", "value": "{{ $as.proxy.backendDomainSuffixes }}"} + {{- end }} + {{- if $as.sandboxNetwork.enabled }} + ,{"name": "SANDBOX_HTTP_PROXY", "value": "{{ $as.sandboxNetwork.httpProxy | default (printf "http://%s:%s" (include "retool.agentSandbox.proxy.name" .) (toString $as.proxy.port)) }}"} + {{- end }} + {{- if $as.snapshotStorage.s3Bucket }} + ,{"name": "S3_BUCKET", "value": "{{ $as.snapshotStorage.s3Bucket }}"} + ,{"name": "S3_ENDPOINT", "value": "{{ $as.snapshotStorage.s3Endpoint }}"} + ,{"name": "S3_REGION", "value": "{{ $as.snapshotStorage.s3Region }}"} + ,{"name": "AWS_ACCESS_KEY_ID", "valueFrom": {"secretKeyRef": {"name": "{{ $as.snapshotStorage.credentialsSecretName | default $defaultSecretName }}", "key": "awsAccessKeyId"}}} + ,{"name": "AWS_SECRET_ACCESS_KEY", "valueFrom": {"secretKeyRef": {"name": "{{ $as.snapshotStorage.credentialsSecretName | default $defaultSecretName }}", "key": "awsSecretAccessKey"}}} + {{- end }} + {{- range $as.sandbox.extraEnv }} + ,{{ toJson . }} + {{- end }} + ], + "volumeMounts": [ + {{- if and $as.sandboxNetwork.enabled (not $as.sandboxNetwork.devicePlugin) }} + {"name": "dev-tun", "mountPath": "/dev/net/tun"}, + {{- end }} + {"name": "run", "mountPath": "/run"}, + {"name": "tmp", "mountPath": "/tmp"}, + {"name": "rootfs-appjob", "mountPath": "/opt/sandbox-env/rootfs/app/job"}, + {"name": "rootfs-etc", "mountPath": "/opt/sandbox-env/rootfs/etc"} + ], + {{- $res := deepCopy $as.sandbox.resources }} + {{- if $as.sandboxNetwork.devicePlugin }} + {{- $_ := set $res.limits "smarter-devices/net_tun" 1 }} + {{- end }} + "resources": {{ toJson $res }} + } + ], + "volumes": [ + {{- if and $as.sandboxNetwork.enabled (not $as.sandboxNetwork.devicePlugin) }} + {"name": "dev-tun", "hostPath": {"path": "/dev/net/tun", "type": "CharDevice"}}, + {{- end }} + {"name": "run", "emptyDir": {"medium": "Memory", "sizeLimit": "64Mi"}}, + {"name": "tmp", "emptyDir": {"sizeLimit": "{{ $as.sandbox.tmpDirSizeLimit | default "20Gi" }}"}}, + {"name": "rootfs-appjob", "emptyDir": {"sizeLimit": "{{ $as.sandbox.rootfsSizeLimit | default "2Gi" }}"}}, + {"name": "rootfs-etc", "emptyDir": {"medium": "Memory", "sizeLimit": "4Mi"}} + ] + } + } + } + } +--- +{{- /* +======================================================================= + Controller Deployment +======================================================================= +*/}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "retool.agentSandbox.controller.name" . }} + labels: + {{- include "retool.agentSandbox.controller.selectorLabels" . | nindent 4 }} + {{- include "retool.agentSandbox.controller.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +{{- if .Values.deployment.labels }} +{{ toYaml .Values.deployment.labels | indent 4 }} +{{- end }} +{{- if .Values.deployment.annotations }} + annotations: +{{ toYaml .Values.deployment.annotations | indent 4 }} +{{- end }} +spec: + replicas: {{ $as.controller.replicaCount }} + selector: + matchLabels: + {{- include "retool.agentSandbox.controller.selectorLabels" . | nindent 6 }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + template: + metadata: + annotations: +{{- if .Values.podAnnotations }} +{{ toYaml .Values.podAnnotations | indent 8 }} +{{- end }} +{{- if $as.annotations }} +{{ toYaml $as.annotations | indent 8 }} +{{- end }} + labels: + {{- include "retool.agentSandbox.controller.selectorLabels" . | nindent 8 }} + {{- include "retool.agentSandbox.controller.labels" . | nindent 8 }} + {{- include "retool.labels" . | nindent 8 }} +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} +{{- if $as.labels }} +{{ toYaml $as.labels | indent 8 }} +{{- end }} + spec: + serviceAccountName: {{ include "retool.agentSandbox.controller.name" . }} + automountServiceAccountToken: true + {{- if .Values.priorityClassName }} + priorityClassName: "{{ .Values.priorityClassName }}" + {{- end }} + containers: + - name: controller + image: "{{ $as.image.repository }}:{{ $as.image.tag | default .Values.image.tag }}" + imagePullPolicy: {{ $as.image.pullPolicy }} + ports: + - name: http + containerPort: {{ $as.controller.port }} + protocol: TCP + securityContext: + runAsUser: 1001 + runAsGroup: 1001 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + env: + - name: NODE_ENV + value: "production" + - name: AGENT_SANDBOX_ROLE + value: "controller" + - name: CONTROLLER_PORT + value: {{ $as.controller.port | quote }} + - name: STATE_BACKEND + value: "postgres" + {{- include "retool.agentSandbox.postgresUrlEnv" . | nindent 12 }} + - name: AGENT_SANDBOX_POSTGRES_SCHEMA + value: {{ $as.postgres.schema | quote }} + - name: AGENT_SANDBOX_POSTGRES_POOL_MAX + value: {{ $as.postgres.poolMax | quote }} + - name: STATE_SWEEPER_INTERVAL_MS + value: {{ $as.postgres.sweeperIntervalMs | quote }} + - name: K8S_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: JOB_NAME_PREFIX + value: {{ include "retool.agentSandbox.name" . }}-job + - name: JOB_APP_LABEL + value: {{ include "retool.agentSandbox.name" . }} + - name: EXECUTOR_DEPLOYMENT_NAME + value: {{ include "retool.agentSandbox.name" . }} + - name: EXECUTOR_SERVICE_NAME + value: {{ include "retool.agentSandbox.name" . }}-pods + - name: PREWARM_POOL_SIZE + value: {{ $as.controller.scaling.prewarmPoolSize | quote }} + - name: MAX_TOTAL_JOBS + value: {{ $as.controller.scaling.maxTotalJobs | quote }} + - name: MAX_CONCURRENT_CREATES + value: {{ $as.controller.scaling.maxConcurrentCreates | quote }} + - name: JOB_RETENTION_SECONDS + value: {{ $as.controller.scaling.jobRetentionSeconds | quote }} + - name: ASSIGNED_SANDBOX_TTL_SECONDS + value: {{ $as.controller.scaling.assignedSandboxTtlSeconds | quote }} + - name: RECONCILE_INTERVAL_MS + value: {{ $as.controller.scaling.reconcileIntervalMs | quote }} + - name: LEADER_TTL_MS + value: {{ $as.controller.scaling.leaderTtlMs | quote }} + - name: LEADER_RENEW_MS + value: {{ $as.controller.scaling.leaderRenewMs | quote }} + - name: PER_USER_CONCURRENCY_PAID + value: {{ $as.controller.scaling.perUserSandboxLimit | quote }} + - name: DEPLOYED_IMAGE_TAG + value: {{ $as.image.tag | default .Values.image.tag | quote }} + - name: JOB_TEMPLATE_CONFIGMAP + value: {{ include "retool.agentSandbox.name" . }}-job-template + - name: DAEMONSET_NAME + value: {{ include "retool.agentSandbox.name" . }}-image-prepuller + {{- if $as.jwtPublicKey }} + - name: AGENT_SANDBOX_JWT_PUBLIC_KEY + value: {{ $as.jwtPublicKey | quote }} + {{- else if $as.externalSecret.name }} + - name: AGENT_SANDBOX_JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ $defaultSecretName }} + key: jwt-public-key + {{- end }} + livenessProbe: + httpGet: + path: /livez + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 5 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 1 + periodSeconds: 2 + timeoutSeconds: 3 + resources: + {{- toYaml $as.controller.resources | nindent 12 }} +{{- if .Values.image.pullSecrets }} + imagePullSecrets: +{{ toYaml .Values.image.pullSecrets | indent 8 }} +{{- end }} +{{- if $as.affinity }} + affinity: +{{ toYaml $as.affinity | indent 8 }} +{{- end }} +{{- if $nodeSelector }} + nodeSelector: +{{ toYaml $nodeSelector | indent 8 }} +{{- end }} + tolerations: +{{ toYaml $tolerations | indent 8 }} +--- +{{- /* +======================================================================= + Controller Service (ClusterIP) +======================================================================= +*/}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "retool.agentSandbox.controller.name" . }} + labels: + {{- include "retool.agentSandbox.controller.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: {{ $as.controller.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "retool.agentSandbox.controller.selectorLabels" . | nindent 4 }} +--- +{{- /* +======================================================================= + Proxy Deployment +======================================================================= +*/}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "retool.agentSandbox.proxy.name" . }} + labels: + {{- include "retool.agentSandbox.proxy.selectorLabels" . | nindent 4 }} + {{- include "retool.agentSandbox.proxy.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +{{- if .Values.deployment.labels }} +{{ toYaml .Values.deployment.labels | indent 4 }} +{{- end }} +{{- if .Values.deployment.annotations }} + annotations: +{{ toYaml .Values.deployment.annotations | indent 4 }} +{{- end }} +spec: + replicas: {{ $as.proxy.replicaCount }} + selector: + matchLabels: + {{- include "retool.agentSandbox.proxy.selectorLabels" . | nindent 6 }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + template: + metadata: + annotations: +{{- if .Values.podAnnotations }} +{{ toYaml .Values.podAnnotations | indent 8 }} +{{- end }} +{{- if $as.annotations }} +{{ toYaml $as.annotations | indent 8 }} +{{- end }} + labels: + {{- include "retool.agentSandbox.proxy.selectorLabels" . | nindent 8 }} + {{- include "retool.agentSandbox.proxy.labels" . | nindent 8 }} + {{- include "retool.labels" . | nindent 8 }} +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} +{{- if $as.labels }} +{{ toYaml $as.labels | indent 8 }} +{{- end }} + spec: + automountServiceAccountToken: false + {{- if .Values.priorityClassName }} + priorityClassName: "{{ .Values.priorityClassName }}" + {{- end }} + containers: + - name: proxy + image: "{{ $as.image.repository }}:{{ $as.image.tag | default .Values.image.tag }}" + imagePullPolicy: {{ $as.image.pullPolicy }} + ports: + - name: http + containerPort: {{ $as.proxy.port }} + protocol: TCP + securityContext: + runAsUser: 1001 + runAsGroup: 1001 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + env: + - name: NODE_ENV + value: "production" + - name: AGENT_SANDBOX_ROLE + value: "proxy" + - name: PROXY_PORT + value: {{ $as.proxy.port | quote }} + - name: STATE_BACKEND + value: "postgres" + {{- include "retool.agentSandbox.postgresUrlEnv" . | nindent 12 }} + - name: AGENT_SANDBOX_POSTGRES_SCHEMA + value: {{ $as.postgres.schema | quote }} + - name: AGENT_SANDBOX_POSTGRES_POOL_MAX + value: {{ $as.postgres.poolMax | quote }} + - name: STATE_SWEEPER_INTERVAL_MS + value: {{ $as.postgres.sweeperIntervalMs | quote }} + {{- if $as.proxy.allowedDomains }} + - name: ALLOWED_DOMAINS + value: {{ $as.proxy.allowedDomains | quote }} + {{- end }} + - name: BACKEND_URL + value: {{ $as.proxy.backendUrl | default (printf "http://%s:%s" (include "retool.fullname" .) (toString .Values.service.internalPort)) | quote }} + {{- if $as.proxy.backendDomainSuffixes }} + - name: BACKEND_DOMAIN_SUFFIXES + value: {{ $as.proxy.backendDomainSuffixes | quote }} + {{- end }} + {{- if $as.encryptionKey }} + - name: AGENT_SANDBOX_ENCRYPTION_KEY + value: {{ $as.encryptionKey | quote }} + {{- else if $as.externalSecret.name }} + - name: AGENT_SANDBOX_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ $defaultSecretName }} + key: encryption-key + {{- end }} + {{- if $as.jwtPublicKey }} + - name: AGENT_SANDBOX_JWT_PUBLIC_KEY + value: {{ $as.jwtPublicKey | quote }} + {{- else if $as.externalSecret.name }} + - name: AGENT_SANDBOX_JWT_PUBLIC_KEY + valueFrom: + secretKeyRef: + name: {{ $defaultSecretName }} + key: jwt-public-key + {{- end }} + - name: EXECUTOR_PORT + value: {{ $as.sandbox.port | quote }} + - name: EXECUTOR_SERVICE_NAME + value: {{ include "retool.agentSandbox.name" . }}-pods + - name: K8S_NAMESPACE + value: {{ .Release.Namespace | quote }} + {{- if $as.proxy.sandboxProxyTimeoutMs }} + - name: SANDBOX_PROXY_TIMEOUT_MS + value: {{ $as.proxy.sandboxProxyTimeoutMs | quote }} + {{- end }} + livenessProbe: + httpGet: + path: /livez + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 5 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 1 + periodSeconds: 2 + timeoutSeconds: 3 + resources: + {{- toYaml $as.proxy.resources | nindent 12 }} +{{- if .Values.image.pullSecrets }} + imagePullSecrets: +{{ toYaml .Values.image.pullSecrets | indent 8 }} +{{- end }} +{{- if $as.affinity }} + affinity: +{{ toYaml $as.affinity | indent 8 }} +{{- end }} +{{- if $nodeSelector }} + nodeSelector: +{{ toYaml $nodeSelector | indent 8 }} +{{- end }} + tolerations: +{{ toYaml $tolerations | indent 8 }} +--- +{{- /* +======================================================================= + Proxy Service +======================================================================= +*/}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "retool.agentSandbox.proxy.name" . }} + labels: + {{- include "retool.agentSandbox.proxy.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +{{- with ($as.proxy.service).annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} +spec: + type: {{ ($as.proxy.service).type | default "ClusterIP" }} + ports: + - port: {{ $as.proxy.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "retool.agentSandbox.proxy.selectorLabels" . | nindent 4 }} +--- +{{- /* +======================================================================= + Proxy Ingress (optional — exposes proxy to frontend for WebSocket) +======================================================================= +*/}} +{{- if ($as.proxy.ingress).enabled }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.Version }} +apiVersion: networking.k8s.io/v1 +{{- else }} +apiVersion: networking.k8s.io/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ include "retool.agentSandbox.proxy.name" . }} + labels: + {{- include "retool.agentSandbox.proxy.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +{{- with $as.proxy.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} +{{- end }} +spec: + {{- if and $as.proxy.ingress.ingressClassName (semverCompare ">=1.18-0" .Capabilities.KubeVersion.Version) }} + ingressClassName: {{ $as.proxy.ingress.ingressClassName }} + {{- end }} + rules: + - host: {{ $as.proxy.ingress.host | quote }} + http: + paths: + - path: / + {{- if semverCompare ">=1.18-0" .Capabilities.KubeVersion.Version }} + pathType: Prefix + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.Version }} + service: + name: {{ include "retool.agentSandbox.proxy.name" . }} + port: + number: {{ $as.proxy.port }} + {{- else }} + serviceName: {{ include "retool.agentSandbox.proxy.name" . }} + servicePort: {{ $as.proxy.port }} + {{- end }} +{{- with $as.proxy.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} +{{- end }} +--- +{{- end }} +{{- /* +======================================================================= + Headless Service for direct pod addressing (sandbox routing). + Executor Job pods use subdomain to register DNS: + ...svc.cluster.local: +======================================================================= +*/}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "retool.agentSandbox.name" . }}-pods + labels: + {{- include "retool.agentSandbox.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +spec: + clusterIP: None + ports: + - port: {{ $as.sandbox.port }} + targetPort: {{ $as.sandbox.port }} + protocol: TCP + name: http + selector: + {{- include "retool.agentSandbox.selectorLabels" . | nindent 4 }} +--- +{{- /* +======================================================================= + PodDisruptionBudget for controller (when replicas > 1) +======================================================================= +*/}} +{{- if gt (int $as.controller.replicaCount) 1 }} +{{- if semverCompare ">=1.21-0" .Capabilities.KubeVersion.Version -}} +apiVersion: policy/v1 +{{- else -}} +apiVersion: policy/v1beta1 +{{- end }} +kind: PodDisruptionBudget +metadata: + name: {{ include "retool.agentSandbox.controller.name" . }} + labels: + {{- include "retool.agentSandbox.controller.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +spec: + maxUnavailable: 1 + selector: + matchLabels: + {{- include "retool.agentSandbox.controller.selectorLabels" . | nindent 6 }} +--- +{{- end }} +{{- if .Values.podDisruptionBudget }} +{{- if semverCompare ">=1.21-0" .Capabilities.KubeVersion.Version -}} +apiVersion: policy/v1 +{{- else -}} +apiVersion: policy/v1beta1 +{{- end }} +kind: PodDisruptionBudget +metadata: + name: {{ include "retool.agentSandbox.proxy.name" . }} + labels: + {{- include "retool.agentSandbox.proxy.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +spec: + {{- toYaml .Values.podDisruptionBudget | nindent 2 }} + selector: + matchLabels: + {{- include "retool.agentSandbox.proxy.selectorLabels" . | nindent 6 }} +{{- end }} +{{- end }} diff --git a/charts/retool/templates/deployment_backend.yaml b/charts/retool/templates/deployment_backend.yaml index 9a1a6c4..ff1a9e5 100644 --- a/charts/retool/templates/deployment_backend.yaml +++ b/charts/retool/templates/deployment_backend.yaml @@ -1,3 +1,5 @@ +{{- include "retool.rr.validateLegacyValues" . }} +{{- include "retool.gitServer.validateBlobStorage" . }} apiVersion: apps/v1 kind: Deployment metadata: @@ -100,6 +102,13 @@ spec: {{- if not ( include "retool.jobRunner.enabled" . ) }} {{- $serviceType = append $serviceType "JOBS_RUNNER" }} {{- end }} + {{- /* + Run the git server in-process on the main backend unless it has been + split out into its own deployment (rr.gitServer.separate.enabled). + */}} + {{- if and .Values.rr.gitServer.enabled (not (include "retool.gitServer.separateEnabled" .)) }} + {{- $serviceType = append $serviceType "RR_GIT_SERVER" }} + {{- end }} - name: SERVICE_TYPE value: {{ join "," $serviceType }} {{ if and ( not $.Values.dbconnector.enabled ) ( and ( include "retool_version_with_java_dbconnector_opt_out" . ) ( not $.Values.dbconnector.java.enabled ) ) }} @@ -161,6 +170,7 @@ spec: - name: CODE_EXECUTOR_INGRESS_DOMAIN value: http://{{ template "retool.codeExecutor.name" . }} {{- end }} + {{- include "retool.agentSandbox.backendEnvVars" . | nindent 10 }} {{- if ($temporalConfig).sslEnabled }} - name: WORKFLOW_TEMPORAL_TLS_ENABLED value: "true" @@ -179,6 +189,10 @@ spec: {{- end }} {{- end }} {{- end }} + {{- if eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "jsExecutor")) "1" }} + - name: JS_EXECUTOR_INGRESS_DOMAIN + value: http://{{ template "retool.jsExecutor.name" . }} + {{- end }} {{- include "retool.telemetry.includeEnvVars" . | nindent 10 }} @@ -247,6 +261,21 @@ spec: {{- end }} {{- end }} {{- end }} + {{- if .Values.rr.gitServer.enabled }} + {{- if include "retool.gitServer.separateEnabled" . }} + {{- /* + git server runs in its own deployment; point the main backend's + proxy (/api/ai/rr/git/v2/*) at the git-server service instead of + localhost. + */}} + - name: RR_GIT_SERVER_HOST + value: {{ template "retool.gitServer.name" . }} + - name: RR_GIT_SERVER_PORT + value: {{ include "retool.gitServer.port" . | quote }} + {{- else }} + {{- include "retool.gitServer.commonEnv" . | nindent 10 }} + {{- end }} + {{- end }} {{- include "retool.env" .Values.env | nindent 10 }} {{- range .Values.environmentSecrets }} - name: {{ .name }} diff --git a/charts/retool/templates/deployment_code_executor.yaml b/charts/retool/templates/deployment_code_executor.yaml index 6750e61..6c75b3e 100644 --- a/charts/retool/templates/deployment_code_executor.yaml +++ b/charts/retool/templates/deployment_code_executor.yaml @@ -1,4 +1,6 @@ {{- if include "retool.workflows.enabled" . }} +{{- /* Use the less-privileged seccomp sandbox (see codeExecutor.useSeccompProfile in values.yaml) only when it is enabled and no explicit codeExecutor.securityContext is set. */ -}} +{{- $useSecComp := and (not .Values.codeExecutor.securityContext) .Values.codeExecutor.useSeccompProfile -}} apiVersion: apps/v1 kind: Deployment metadata: @@ -23,6 +25,10 @@ spec: template: metadata: annotations: +{{- if $useSecComp }} + checksum/seccomp: {{ .Files.Get "files/nsjail-seccomp.json" | sha256sum }} + container.apparmor.security.beta.kubernetes.io/code-executor: unconfined +{{- end }} {{- if .Values.podAnnotations }} {{ toYaml .Values.podAnnotations | indent 8 }} {{- end }} @@ -44,11 +50,43 @@ spec: {{- if .Values.priorityClassName }} priorityClassName: "{{ .Values.priorityClassName }}" {{- end }} -{{- if .Values.initContainers }} +{{- if $useSecComp }} + hostUsers: false +{{- end }} +{{- if or $useSecComp .Values.initContainers }} initContainers: +{{- if $useSecComp }} + - name: install-seccomp + image: busybox:1.37.0@sha256:b3255e7dfbcd10cb367af0d409747d511aeb66dfac98cf30e97e87e4207dd76f + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + requests: + cpu: 1m + memory: 4Mi + limits: + cpu: 10m + memory: 16Mi + command: + - /bin/sh + - -c + - | + DEST="/host-seccomp/{{ .Values.codeExecutor.seccompLocalhostProfile }}" + mkdir -p "$(dirname "$DEST")" + cp /seccomp-profile/nsjail-seccomp.json "$DEST" + echo "seccomp profile installed at $DEST" + volumeMounts: + - name: seccomp-profile + mountPath: /seccomp-profile + - name: host-seccomp + mountPath: /host-seccomp +{{- end }} {{- range $key, $value := .Values.initContainers }} - - name: "{{ $key }}" -{{ toYaml $value | indent 8 }} + - name: "{{ $key }}" +{{ toYaml $value | indent 10 }} {{- end }} {{- end }} containers: @@ -56,11 +94,18 @@ spec: image: "{{ .Values.codeExecutor.image.repository }}:{{ include "retool.codeExecutor.image.tag" . }}" imagePullPolicy: {{ .Values.image.pullPolicy }} securityContext: - {{ if .Values.codeExecutor.securityContext }} + {{- if .Values.codeExecutor.securityContext }} {{ toYaml .Values.codeExecutor.securityContext | indent 10 }} - {{ else }} + {{- else if $useSecComp }} + capabilities: + add: ["NET_ADMIN"] + procMount: Unmasked + seccompProfile: + type: Localhost + localhostProfile: {{ .Values.codeExecutor.seccompLocalhostProfile }} + {{- else }} privileged: true - {{ end }} + {{- end }} {{- if .Values.securityContext.extraContainerSecurityContext }} {{ toYaml .Values.securityContext.extraContainerSecurityContext | indent 10 }} {{- end }} @@ -128,6 +173,15 @@ spec: {{ tpl . $ | indent 6 }} {{- end }} volumes: +{{- if $useSecComp }} + - name: seccomp-profile + configMap: + name: {{ template "retool.fullname" . }}-code-executor-seccomp + - name: host-seccomp + hostPath: + path: /var/lib/kubelet/seccomp + type: DirectoryOrCreate +{{- end }} {{- if .Values.codeExecutor.volumes }} {{ toYaml .Values.codeExecutor.volumes | indent 8 }} {{- end }} diff --git a/charts/retool/templates/deployment_git_server.yaml b/charts/retool/templates/deployment_git_server.yaml new file mode 100644 index 0000000..af99a6f --- /dev/null +++ b/charts/retool/templates/deployment_git_server.yaml @@ -0,0 +1,296 @@ +{{- if include "retool.gitServer.separateEnabled" . }} +{{- include "retool.gitServer.validateBlobStorage" . }} +{{- $gitServerPort := include "retool.gitServer.port" . }} +{{- $gitServerValues := .Values.rr.gitServer.separate }} +{{- $gitServerService := $gitServerValues.service | default dict }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "retool.gitServer.name" . }} + labels: + {{- include "retool.labels" . | nindent 4 }} + {{- with $gitServerService.labels }} + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + {{- with $gitServerService.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} +spec: + selector: + retoolService: {{ template "retool.gitServer.name" . }} + ports: + - name: http-server + protocol: TCP + port: {{ $gitServerPort }} + targetPort: {{ $gitServerPort }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "retool.gitServer.name" . }} + labels: +{{- include "retool.labels" . | nindent 4 }} +{{- if .Values.deployment.annotations }} + annotations: +{{ toYaml .Values.deployment.annotations | indent 4 }} +{{- end }} +spec: + replicas: {{ $gitServerValues.replicaCount | default 1 }} + selector: + matchLabels: + retoolService: {{ template "retool.gitServer.name" . }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + template: + metadata: + annotations: +{{- if .Values.podAnnotations }} +{{ toYaml .Values.podAnnotations | indent 8 }} +{{- end }} +{{- with $gitServerValues.annotations }} +{{ toYaml . | indent 8 }} +{{- end }} + labels: + {{- include "retool.labels" . | nindent 8 }} + retoolService: {{ template "retool.gitServer.name" . }} + telemetry.retool.com/service-name: rr-git-server +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} +{{- with $gitServerValues.labels }} +{{ toYaml . | indent 8 }} +{{- end }} + spec: + serviceAccountName: {{ template "retool.serviceAccountName" . }} + {{- if .Values.priorityClassName }} + priorityClassName: "{{ .Values.priorityClassName }}" + {{- end }} +{{- if .Values.initContainers }} + initContainers: +{{- range $key, $value := .Values.initContainers }} + - name: "{{ $key }}" +{{ toYaml $value | indent 8 }} +{{- end }} +{{- end }} + containers: + - name: rr-git-server + image: "{{ .Values.image.repository }}:{{ required "Please set a value for .Values.image.tag" .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - bash + - -c + - chmod -R +x ./docker_scripts; sync; ./docker_scripts/wait-for-it.sh -t 0 {{ template "retool.postgresql.host" . }}:{{ template "retool.postgresql.port" . }}; ./docker_scripts/start_api.sh + {{- if .Values.commandline.args }} +{{ toYaml .Values.commandline.args | indent 10 }} + {{- end }} + env: + - name: DEPLOYMENT_TEMPLATE_TYPE + value: {{ template "retool.deploymentTemplateType" . }} + - name: DEPLOYMENT_TEMPLATE_VERSION + value: {{ template "retool.deploymentTemplateVersion" . }} + - name: NODE_ENV + value: production + - name: SERVICE_TYPE + value: RR_GIT_SERVER + - name: RR_GIT_SERVER_PORT + value: {{ $gitServerPort | quote }} + # The standalone git server does not run migrations; the main backend owns them. + - name: DISABLE_DATABASE_MIGRATIONS + value: "true" + - name: COOKIE_INSECURE + value: {{ .Values.config.useInsecureCookies | quote }} + - name: POSTGRES_HOST + value: {{ template "retool.postgresql.host" . }} + - name: POSTGRES_PORT + value: {{ template "retool.postgresql.port" . }} + - name: POSTGRES_DB + value: {{ template "retool.postgresql.database" . }} + - name: POSTGRES_USER + value: {{ template "retool.postgresql.user" . }} + - name: POSTGRES_SSL_ENABLED + value: {{ template "retool.postgresql.ssl_enabled" . }} + + {{- include "retool.telemetry.includeEnvVars" . | nindent 10 }} + + {{- if include "shouldIncludeConfigSecretsEnvVars" . }} + - name: LICENSE_KEY + valueFrom: + secretKeyRef: + {{- if .Values.config.licenseKeySecretName }} + name: {{ .Values.config.licenseKeySecretName }} + key: {{ .Values.config.licenseKeySecretKey | default "license-key" }} + {{- else }} + name: {{ template "retool.fullname" . }} + key: license-key + {{- end }} + - name: JWT_SECRET + valueFrom: + secretKeyRef: + {{- if .Values.config.jwtSecretSecretName }} + name: {{ .Values.config.jwtSecretSecretName }} + key: {{ .Values.config.jwtSecretSecretKey | default "jwt-secret" }} + {{- else }} + name: {{ template "retool.fullname" . }} + key: jwt-secret + {{- end }} + - name: ENCRYPTION_KEY + valueFrom: + secretKeyRef: + {{- if .Values.config.encryptionKeySecretName }} + name: {{ .Values.config.encryptionKeySecretName }} + key: {{ .Values.config.encryptionKeySecretKey | default "encryption-key" }} + {{- else }} + name: {{ template "retool.fullname" . }} + key: encryption-key + {{- end }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + {{- if .Values.postgresql.enabled }} + name: {{ template "retool.postgresql.fullname" . }} + # `postgres` is the default admin username for postgres in the subchart we use, so it needs the admin password + # if a different username is picked, then it needs the custom password instead. + {{- if eq .Values.postgresql.auth.username "postgres" }} + key: postgres-password + {{- else }} + key: password + {{- end }} + {{- else }} + {{- if .Values.config.postgresql.passwordSecretName }} + name: {{ .Values.config.postgresql.passwordSecretName }} + key: {{ .Values.config.postgresql.passwordSecretKey | default "postgresql-password" }} + {{- else }} + name: {{ template "retool.fullname" . }} + key: postgresql-password + {{- end }} + {{- end }} + {{- end }} + {{- include "retool.gitServer.commonEnv" . | nindent 10 }} + {{- include "retool.env" .Values.env | nindent 10 }} + {{- range .Values.environmentSecrets }} + - name: {{ .name }} + valueFrom: + secretKeyRef: + name: {{ .secretKeyRef.name }} + key: {{ .secretKeyRef.key }} + {{- end }} + {{- with .Values.environmentVariables }} +{{ toYaml . | indent 10 }} + {{- end }} + {{- if .Values.externalSecrets.enabled }} + envFrom: + - secretRef: + name: {{ .Values.externalSecrets.name }} + {{- range .Values.externalSecrets.secrets }} + - secretRef: + name: {{ .name }} + {{- end }} + {{- end }} + {{- if .Values.externalSecrets.externalSecretsOperator.enabled }} + envFrom: + {{- range .Values.externalSecrets.externalSecretsOperator.secretRef }} + - secretRef: + name: {{ .name }} + optional: {{ .optional | default false }} + {{- end }} + {{- end }} + ports: + - containerPort: {{ $gitServerPort }} + name: http-server + protocol: TCP + readinessProbe: + tcpSocket: + port: {{ $gitServerPort }} + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: {{ $gitServerPort }} + initialDelaySeconds: 30 + failureThreshold: 10 + timeoutSeconds: 10 + periodSeconds: 20 + resources: +{{ toYaml ($gitServerValues.resources | default .Values.resources) | indent 10 }} + volumeMounts: + {{- range $configFile := (keys .Values.files) }} + - name: {{ template "retool.name" $ }} + mountPath: "/usr/share/retool/config/{{ $configFile }}" + subPath: {{ $configFile }} + {{- end }} + {{if and .Values.persistentVolumeClaim.enabled .Values.persistentVolumeClaim.mountPath }} + - name: retool-pv + mountPath: {{ .Values.persistentVolumeClaim.mountPath }} + {{- end }} +{{- if .Values.extraVolumeMounts }} +{{ toYaml .Values.extraVolumeMounts | indent 8 }} +{{- end }} +{{- if .Values.securityContext.extraContainerSecurityContext }} + securityContext: +{{ toYaml .Values.securityContext.extraContainerSecurityContext | indent 10 }} +{{- end }} +{{- with .Values.extraContainers }} +{{ tpl . $ | indent 6 }} +{{- end }} +{{- range .Values.extraConfigMapMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath }} +{{- end }} + {{- if .Values.image.pullSecrets }} + imagePullSecrets: +{{ toYaml .Values.image.pullSecrets | indent 8 }} + {{- end }} + {{- $affinity := $gitServerValues.affinity | default .Values.affinity }} + {{- if $affinity }} + affinity: +{{ toYaml $affinity | indent 8 }} + {{- end }} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end }} + tolerations: +{{ toYaml .Values.tolerations | indent 8 }} +{{- if .Values.securityContext.enabled }} + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + fsGroup: {{ .Values.securityContext.fsGroup }} +{{- if .Values.securityContext.extraSecurityContext }} +{{ toYaml .Values.securityContext.extraSecurityContext | indent 8 }} +{{- end }} +{{- end }} + volumes: +{{- range .Values.extraConfigMapMounts }} + - name: {{ .name }} + configMap: + name: {{ .configMap }} +{{- end }} + {{- if .Values.persistentVolumeClaim.enabled }} + - name: retool-pv + persistentVolumeClaim: + claimName: {{ default (include "retool.fullname" .) .Values.persistentVolumeClaim.existingClaim }} + {{- end }} +{{- if .Values.extraVolumes }} +{{ toYaml .Values.extraVolumes | indent 8 }} +{{- end }} +{{- if .Values.podDisruptionBudget }} +--- +{{- if semverCompare ">=1.21-0" .Capabilities.KubeVersion.Version }} +apiVersion: policy/v1 +{{- else }} +apiVersion: policy/v1beta1 +{{- end }} +kind: PodDisruptionBudget +metadata: + name: {{ template "retool.gitServer.name" . }} +spec: + {{- toYaml .Values.podDisruptionBudget | nindent 2 }} + selector: + matchLabels: + retoolService: {{ template "retool.gitServer.name" . }} +{{- end }} +{{- end }} diff --git a/charts/retool/templates/deployment_jobs.yaml b/charts/retool/templates/deployment_jobs.yaml index fa08b81..5306800 100644 --- a/charts/retool/templates/deployment_jobs.yaml +++ b/charts/retool/templates/deployment_jobs.yaml @@ -93,6 +93,39 @@ spec: {{- include "retool.telemetry.includeEnvVars" . | nindent 10 }} + {{- if eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "agentSandbox")) "1" }} + - name: RR_AGENT_PUBSUB_BACKEND + value: "postgres" + {{- end }} + + {{- $temporalConfig := (include "retool.temporalConfig" . | fromYaml) }} + {{- if or (index .Values "retool-temporal-services-helm" "enabled") ($temporalConfig).enabled }} + - name: WORKFLOW_TEMPORAL_CLUSTER_FRONTEND_HOST + value: {{ template "retool.temporal.host" . }} + - name: WORKFLOW_TEMPORAL_CLUSTER_FRONTEND_PORT + value: {{ template "retool.temporal.port" . }} + - name: WORKFLOW_TEMPORAL_CLUSTER_NAMESPACE + value: {{ template "retool.temporal.namespace" . }} + {{- end }} + {{- if ($temporalConfig).sslEnabled }} + - name: WORKFLOW_TEMPORAL_TLS_ENABLED + value: "true" + {{- if (and ($temporalConfig).sslCert ($temporalConfig).sslKey) }} + - name: WORKFLOW_TEMPORAL_TLS_CRT + value: {{ $temporalConfig.sslCert }} + - name: WORKFLOW_TEMPORAL_TLS_KEY + valueFrom: + secretKeyRef: + {{- if ($temporalConfig).sslKeySecretName }} + name: {{ $temporalConfig.sslKeySecretName }} + key: {{ ($temporalConfig).sslKeySecretKey | default "temporal-tls-key" }} + {{- else }} + name: {{ template "retool.fullname" . }} + key: "temporal-tls-key" + {{- end }} + {{- end }} + {{- end }} + {{- if include "shouldIncludeConfigSecretsEnvVars" . }} - name: LICENSE_KEY valueFrom: diff --git a/charts/retool/templates/deployment_js_executor.yaml b/charts/retool/templates/deployment_js_executor.yaml new file mode 100644 index 0000000..09a371e --- /dev/null +++ b/charts/retool/templates/deployment_js_executor.yaml @@ -0,0 +1,210 @@ +{{- if eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "jsExecutor")) "1" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "retool.jsExecutor.name" . }} + labels: + {{- include "retool.jsExecutor.selectorLabels" . | nindent 4 }} + {{- include "retool.jsExecutor.labels" . | nindent 4 }} + {{- include "retool.labels" . | nindent 4 }} +{{- if .Values.deployment.labels }} +{{ toYaml .Values.deployment.labels | indent 4 }} +{{- end }} +{{- if .Values.deployment.annotations }} + annotations: +{{ toYaml .Values.deployment.annotations | indent 4 }} +{{- end }} +spec: + replicas: {{ .Values.rr.jsExecutor.replicaCount }} + selector: + matchLabels: + {{- include "retool.jsExecutor.selectorLabels" . | nindent 6 }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + template: + metadata: + annotations: + checksum/seccomp: {{ .Files.Get "files/nsjail-seccomp.json" | sha256sum }} + container.apparmor.security.beta.kubernetes.io/js-executor: unconfined +{{- if .Values.podAnnotations }} +{{ toYaml .Values.podAnnotations | indent 8 }} +{{- end }} +{{- if .Values.rr.jsExecutor.annotations }} +{{ toYaml .Values.rr.jsExecutor.annotations | indent 8 }} +{{- end }} + labels: + {{- include "retool.jsExecutor.selectorLabels" . | nindent 8 }} + {{- include "retool.jsExecutor.labels" . | nindent 8 }} + {{- include "retool.labels" . | nindent 8 }} +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} +{{- if .Values.rr.jsExecutor.labels }} +{{ toYaml .Values.rr.jsExecutor.labels | indent 8 }} +{{- end }} + spec: + serviceAccountName: {{ template "retool.serviceAccountName" . }} + {{- if .Values.priorityClassName }} + priorityClassName: "{{ .Values.priorityClassName }}" + {{- end }} + initContainers: + - name: install-seccomp + image: busybox:1.37.0@sha256:b3255e7dfbcd10cb367af0d409747d511aeb66dfac98cf30e97e87e4207dd76f + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + requests: + cpu: 1m + memory: 4Mi + limits: + cpu: 10m + memory: 16Mi + command: + - /bin/sh + - -c + - | + DEST="/host-seccomp/{{ .Values.rr.jsExecutor.seccompLocalhostProfile }}" + mkdir -p "$(dirname "$DEST")" + cp /seccomp-profile/nsjail-seccomp.json "$DEST" + echo "seccomp profile installed at $DEST" + volumeMounts: + - name: seccomp-profile + mountPath: /seccomp-profile + - name: host-seccomp + mountPath: /host-seccomp +{{- if .Values.initContainers }} +{{- range $key, $value := .Values.initContainers }} + - name: "{{ $key }}" +{{ toYaml $value | indent 10 }} +{{- end }} +{{- end }} + containers: + - name: js-executor + image: "{{ .Values.rr.jsExecutor.image.repository }}:{{ include "retool.jsExecutor.image.tag" . }}" + imagePullPolicy: {{ .Values.rr.jsExecutor.image.pullPolicy | default .Values.image.pullPolicy }} + securityContext: + capabilities: + add: ["NET_ADMIN"] + seccompProfile: + type: Localhost + localhostProfile: {{ .Values.rr.jsExecutor.seccompLocalhostProfile }} + env: + - name: DEPLOYMENT_TEMPLATE_TYPE + value: {{ template "retool.deploymentTemplateType" . }} + - name: DEPLOYMENT_TEMPLATE_VERSION + value: {{ template "retool.deploymentTemplateVersion" . }} + - name: NODE_ENV + value: production + {{- include "retool.telemetry.includeEnvVars" . | nindent 10 }} + {{- include "retool.env" .Values.rr.jsExecutor.env | nindent 10 }} + {{- range .Values.rr.jsExecutor.environmentSecrets }} + - name: {{ .name }} + valueFrom: + secretKeyRef: + name: {{ .secretKeyRef.name }} + key: {{ .secretKeyRef.key }} + {{- end }} + {{- with .Values.rr.jsExecutor.environmentVariables }} +{{ toYaml . | indent 10 }} + {{- end }} + ports: + - containerPort: 3000 + name: http-server + protocol: TCP + livenessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} + timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.livenessProbe.failureThreshold }} + readinessProbe: + httpGet: + path: /api/readiness + port: 3000 + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} + successThreshold: {{ .Values.readinessProbe.successThreshold }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + resources: +{{ toYaml .Values.rr.jsExecutor.resources | indent 10 }} + volumeMounts: +{{- if .Values.rr.jsExecutor.volumeMounts }} +{{ toYaml .Values.rr.jsExecutor.volumeMounts | indent 10 }} +{{- end }} +{{- if .Values.extraVolumeMounts }} +{{ toYaml .Values.extraVolumeMounts | indent 10 }} +{{- end }} +{{- range .Values.extraConfigMapMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath }} +{{- end }} +{{- with .Values.extraContainers }} +{{ tpl . $ | indent 6 }} +{{- end }} + volumes: + - name: seccomp-profile + configMap: + name: {{ template "retool.fullname" . }}-js-executor-seccomp + - name: host-seccomp + hostPath: + path: /var/lib/kubelet/seccomp + type: DirectoryOrCreate +{{- if .Values.rr.jsExecutor.volumes }} +{{ toYaml .Values.rr.jsExecutor.volumes | indent 8 }} +{{- end }} +{{- range .Values.extraConfigMapMounts }} + - name: {{ .name }} + configMap: + name: {{ .configMap }} +{{- end }} +{{- if .Values.extraVolumes }} +{{ toYaml .Values.extraVolumes | indent 8 }} +{{- end }} +{{- if .Values.image.pullSecrets }} + imagePullSecrets: +{{ toYaml .Values.image.pullSecrets | indent 8 }} +{{- end }} +{{- if .Values.rr.jsExecutor.affinity }} + affinity: +{{ toYaml .Values.rr.jsExecutor.affinity | indent 8 }} +{{- end }} +{{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} +{{- end }} + tolerations: +{{ toYaml .Values.tolerations | indent 8 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ template "retool.jsExecutor.name" . }} +spec: + selector: + {{- include "retool.jsExecutor.selectorLabels" . | nindent 4 }} + ports: + - protocol: TCP + port: 80 + targetPort: 3000 + name: http-server +--- +{{- if .Values.podDisruptionBudget }} +{{- if semverCompare ">=1.21-0" .Capabilities.KubeVersion.Version -}} +apiVersion: policy/v1 +{{- else -}} +apiVersion: policy/v1beta1 +{{- end }} +kind: PodDisruptionBudget +metadata: + name: {{ template "retool.jsExecutor.name" . }} +spec: + {{- toYaml .Values.podDisruptionBudget | nindent 2 }} + selector: + matchLabels: + {{- include "retool.jsExecutor.selectorLabels" . | nindent 6 }} +{{- end }} +{{- end }} diff --git a/charts/retool/templates/deployment_mcp.yaml b/charts/retool/templates/deployment_mcp.yaml new file mode 100644 index 0000000..1c37a6e --- /dev/null +++ b/charts/retool/templates/deployment_mcp.yaml @@ -0,0 +1,255 @@ +{{- if .Values.mcp.enabled }} +{{- $mcpConfig := .Values.mcp.config | default dict }} +{{- $hasOAuthIntrospectionAuthTokenEnv := false }} +{{- range .Values.mcp.environmentVariables }} +{{- if eq .name "OAUTH_INTROSPECTION_AUTH_TOKEN" }} +{{- $hasOAuthIntrospectionAuthTokenEnv = true }} +{{- end }} +{{- end }} +{{- if not (or $mcpConfig.oauthIntrospectionAuthTokenSecretName $mcpConfig.oauthIntrospectionAuthToken $hasOAuthIntrospectionAuthTokenEnv) }} +{{- fail "Please set .Values.mcp.config.oauthIntrospectionAuthTokenSecretName, .Values.mcp.config.oauthIntrospectionAuthToken, or an OAUTH_INTROSPECTION_AUTH_TOKEN entry in .Values.mcp.environmentVariables when the MCP server is enabled (.Values.mcp.enabled)" }} +{{- end }} +{{- $mcpInternalPort := .Values.mcp.service.internalPort | default 4010 }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "retool.mcp.name" . }} + labels: + {{- include "retool.labels" . | nindent 4 }} + {{- if .Values.mcp.service.labels }} + {{- range $key, $value := .Values.mcp.service.labels }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + {{- if .Values.mcp.service.annotations }} + {{- with .Values.mcp.service.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + {{- end }} +spec: + selector: + retoolService: {{ template "retool.mcp.name" . }} + ports: + - name: http-server + protocol: TCP + {{- if .Values.mcp.service.externalPort }} + port: {{ .Values.mcp.service.externalPort }} + {{- else }} + port: 4010 + {{- end }} + {{- if .Values.mcp.service.internalPort }} + targetPort: {{ $mcpInternalPort }} + {{- else }} + targetPort: 4010 + {{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "retool.mcp.name" . }} + labels: +{{- include "retool.labels" . | nindent 4 }} +{{- if .Values.deployment.annotations }} + annotations: +{{ toYaml .Values.deployment.annotations | indent 4 }} +{{- end }} +spec: + replicas: {{ .Values.mcp.replicaCount }} + selector: + matchLabels: + retoolService: {{ template "retool.mcp.name" . }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + template: + metadata: + annotations: +{{- if .Values.podAnnotations }} +{{ toYaml .Values.podAnnotations | indent 8 }} +{{- end }} +{{- if .Values.mcp.annotations }} +{{ toYaml .Values.mcp.annotations | indent 8 }} +{{- end }} + labels: + retoolService: {{ template "retool.mcp.name" . }} +{{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} +{{- end }} +{{- if .Values.mcp.labels }} +{{ toYaml .Values.mcp.labels | indent 8 }} +{{- end }} + spec: + serviceAccountName: {{ template "retool.serviceAccountName" . }} + {{- if .Values.priorityClassName }} + priorityClassName: "{{ .Values.priorityClassName }}" + {{- end }} +{{- if .Values.initContainers }} + initContainers: +{{- range $key, $value := .Values.initContainers }} + - name: "{{ $key }}" +{{ toYaml $value | indent 8 }} +{{- end }} +{{- end }} + containers: + - name: mcp + image: "{{ .Values.image.repository }}:{{ required "Please set a value for .Values.image.tag" .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - bash + - -c + - chmod -R +x ./docker_scripts; sync; ./docker_scripts/start_api.sh + {{- if .Values.commandline.args }} +{{ toYaml .Values.commandline.args | indent 10 }} + {{- end }} + env: + - name: DEPLOYMENT_TEMPLATE_TYPE + value: {{ template "retool.deploymentTemplateType" . }} + - name: DEPLOYMENT_TEMPLATE_VERSION + value: {{ template "retool.deploymentTemplateVersion" . }} + - name: NODE_ENV + value: production + - name: SERVICE_TYPE + value: MCP_SERVER + - name: MCP_PORT + value: {{ $mcpInternalPort | quote }} + - name: RETOOL_BACKEND_URL + value: {{ $mcpConfig.retoolBackendUrl | default (printf "http://%s:%v" (include "retool.fullname" .) .Values.service.externalPort) | quote }} + {{- /* + Prefer an explicit mcp.config.retoolGitServerUrl; otherwise, when the + git server is split into its own deployment, auto-point MCP at it. + */}} + {{- $retoolGitServerUrl := $mcpConfig.retoolGitServerUrl }} + {{- if and (not $retoolGitServerUrl) (include "retool.gitServer.separateEnabled" .) }} + {{- $retoolGitServerUrl = include "retool.gitServer.url" . }} + {{- end }} + {{- if $retoolGitServerUrl }} + - name: RETOOL_GIT_SERVER_URL + value: {{ $retoolGitServerUrl | quote }} + {{- end }} + {{- if $mcpConfig.retoolUrl }} + - name: RETOOL_URL + value: {{ $mcpConfig.retoolUrl | quote }} + {{- end }} + {{- if $mcpConfig.oauthMainDomain }} + - name: OAUTH_MAIN_DOMAIN + value: {{ $mcpConfig.oauthMainDomain | quote }} + {{- end }} + {{- if $mcpConfig.oauthIntrospectionAuthTokenSecretName }} + - name: OAUTH_INTROSPECTION_AUTH_TOKEN + valueFrom: + secretKeyRef: + name: {{ $mcpConfig.oauthIntrospectionAuthTokenSecretName }} + key: {{ $mcpConfig.oauthIntrospectionAuthTokenSecretKey | default "oauthIntrospectionAuthToken" }} + {{- else if $mcpConfig.oauthIntrospectionAuthToken }} + - name: OAUTH_INTROSPECTION_AUTH_TOKEN + value: {{ $mcpConfig.oauthIntrospectionAuthToken | quote }} + {{- end }} + {{- if $mcpConfig.nodeOptions }} + - name: NODE_OPTIONS + value: {{ $mcpConfig.nodeOptions | quote }} + {{- end }} + {{- with $mcpConfig.enabledToolsets }} + - name: RETOOL_TOOLSETS + value: {{ join "," . | quote }} + {{- end }} + {{- if hasKey $mcpConfig "maxTransportSessions" }} + - name: MCP_MAX_TRANSPORT_SESSIONS + value: {{ $mcpConfig.maxTransportSessions | quote }} + {{- end }} + {{- if hasKey $mcpConfig "sessionIdleTimeoutMs" }} + - name: MCP_SESSION_IDLE_TIMEOUT_MS + value: {{ $mcpConfig.sessionIdleTimeoutMs | quote }} + {{- end }} + {{- if hasKey $mcpConfig "sessionSweepIntervalMs" }} + - name: MCP_SESSION_SWEEP_INTERVAL_MS + value: {{ $mcpConfig.sessionSweepIntervalMs | quote }} + {{- end }} + {{- if hasKey $mcpConfig "sessionGaugeEmitIntervalMs" }} + - name: MCP_SESSION_GAUGE_EMIT_INTERVAL_MS + value: {{ $mcpConfig.sessionGaugeEmitIntervalMs | quote }} + {{- end }} + {{- with .Values.mcp.environmentVariables }} +{{ toYaml . | indent 10 }} + {{- end }} + ports: + - containerPort: {{ $mcpInternalPort }} + name: http-server + protocol: TCP + readinessProbe: + httpGet: + path: /healthcheck + port: {{ $mcpInternalPort }} + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthcheck + port: {{ $mcpInternalPort }} + initialDelaySeconds: 30 + failureThreshold: 10 + timeoutSeconds: 10 + periodSeconds: 20 + resources: +{{ toYaml .Values.mcp.resources | indent 10 }} + volumeMounts: + {{- range $configFile := (keys .Values.files) }} + - name: {{ template "retool.name" $ }} + mountPath: "/usr/share/retool/config/{{ $configFile }}" + subPath: {{ $configFile }} + {{- end }} + {{if and .Values.persistentVolumeClaim.enabled .Values.persistentVolumeClaim.mountPath }} + - name: retool-pv + mountPath: {{ .Values.persistentVolumeClaim.mountPath }} + {{- end }} +{{- if .Values.extraVolumeMounts }} +{{ toYaml .Values.extraVolumeMounts | indent 8 }} +{{- end }} +{{- if .Values.securityContext.extraContainerSecurityContext }} + securityContext: +{{ toYaml .Values.securityContext.extraContainerSecurityContext | indent 10 }} +{{- end }} +{{- with .Values.extraContainers }} +{{ tpl . $ | indent 6 }} +{{- end }} +{{- range .Values.extraConfigMapMounts }} + - name: {{ .name }} + mountPath: {{ .mountPath }} + subPath: {{ .subPath }} +{{- end }} + {{- if .Values.image.pullSecrets }} + imagePullSecrets: +{{ toYaml .Values.image.pullSecrets | indent 8 }} + {{- end }} + {{- if .Values.affinity }} + affinity: +{{ toYaml .Values.affinity | indent 8 }} + {{- end }} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end }} + tolerations: +{{ toYaml .Values.tolerations | indent 8 }} +{{- if .Values.securityContext.enabled }} + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + fsGroup: {{ .Values.securityContext.fsGroup }} +{{- if .Values.securityContext.extraSecurityContext }} +{{ toYaml .Values.securityContext.extraSecurityContext | indent 8 }} +{{- end }} +{{- end }} + volumes: +{{- range .Values.extraConfigMapMounts }} + - name: {{ .name }} + configMap: + name: {{ .configMap }} +{{- end }} + {{- if .Values.persistentVolumeClaim.enabled }} + - name: retool-pv + persistentVolumeClaim: + claimName: {{ default (include "retool.fullname" .) .Values.persistentVolumeClaim.existingClaim }} + {{- end }} +{{- if .Values.extraVolumes }} +{{ toYaml .Values.extraVolumes | indent 8 }} +{{- end }} +{{- end }} diff --git a/charts/retool/templates/deployment_workflows.yaml b/charts/retool/templates/deployment_workflows.yaml index 35b7484..b66f7c3 100644 --- a/charts/retool/templates/deployment_workflows.yaml +++ b/charts/retool/templates/deployment_workflows.yaml @@ -153,6 +153,10 @@ spec: {{- end }} {{- end }} {{- end }} + {{- if eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "jsExecutor")) "1" }} + - name: JS_EXECUTOR_INGRESS_DOMAIN + value: http://{{ template "retool.jsExecutor.name" . }} + {{- end }} {{- include "retool.telemetry.includeEnvVars" . | nindent 10 }} @@ -176,6 +180,7 @@ spec: value: http://{{ include "retool.workflowBackend.name" . }} - name: CODE_EXECUTOR_INGRESS_DOMAIN value: http://{{ template "retool.codeExecutor.name" . }} + {{- include "retool.agentSandbox.backendEnvVars" . | nindent 10 }} {{- if include "shouldIncludeConfigSecretsEnvVars" . }} - name: LICENSE_KEY valueFrom: diff --git a/charts/retool/templates/httproute.yaml b/charts/retool/templates/httproute.yaml index e880892..dc70064 100644 --- a/charts/retool/templates/httproute.yaml +++ b/charts/retool/templates/httproute.yaml @@ -1,6 +1,11 @@ {{- if .Values.httpRoute.enabled }} {{- $fullName := include "retool.fullname" . -}} {{- $svcPort := .Values.service.externalPort -}} +{{- $mcp := .Values.mcp | default dict -}} +{{- $mcpBackendMetadata := $mcp.backendMetadata | default dict -}} +{{- $mcpHttpRoute := $mcp.httpRoute | default dict -}} +{{- $backendApiService := $mcpBackendMetadata.service | default dict -}} +{{- $backendApiPort := $backendApiService.externalPort | default 3001 -}} apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: @@ -35,6 +40,11 @@ spec: port: {{ .port }} {{- end }} {{- end }} + {{- if ( and ((.Values.mcp).enabled) $mcpHttpRoute.enabled ) }} + {{- range $mcpHttpRoute.rules }} + {{- include "retool.httpRoute.mcpRule" (dict "root" $ "rule" . "backendApiPort" $backendApiPort) | nindent 4 }} + {{- end }} + {{- end }} {{- if .Values.httpRoute.rules }} {{- toYaml .Values.httpRoute.rules | nindent 4 }} {{- else }} @@ -46,4 +56,35 @@ spec: - name: {{ $fullName }} port: {{ $svcPort }} {{- end }} +{{- if and (eq (include "retool.rr.componentEnabled" (dict "root" $ "component" "agentSandbox")) "1") .Values.rr.agentSandbox.frontendWsProxyDomain }} +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "retool.agentSandbox.proxy.name" . }} + labels: + {{- include "retool.labels" . | nindent 4 }} + {{- with .Values.httpRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.httpRoute.parentRefs }} + parentRefs: + {{- toYaml . | nindent 4 }} + {{- end }} + hostnames: + - {{ .Values.rr.agentSandbox.frontendWsProxyDomain | trimPrefix "http://" | trimPrefix "https://" | quote }} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: {{ include "retool.agentSandbox.proxy.name" . }} + port: {{ .Values.rr.agentSandbox.proxy.port }} +{{- end }} {{- end }} diff --git a/charts/retool/templates/ingress.yaml b/charts/retool/templates/ingress.yaml index d63f779..d99d8e6 100644 --- a/charts/retool/templates/ingress.yaml +++ b/charts/retool/templates/ingress.yaml @@ -1,6 +1,11 @@ {{- if .Values.ingress.enabled }} {{- $fullName := include "retool.fullname" . -}} {{- $svcPort := .Values.service.externalPort -}} +{{- $mcp := .Values.mcp | default dict -}} +{{- $mcpBackendMetadata := $mcp.backendMetadata | default dict -}} +{{- $mcpIngress := $mcp.ingress | default dict -}} +{{- $backendApiService := $mcpBackendMetadata.service | default dict -}} +{{- $backendApiPort := $backendApiService.externalPort | default 3001 -}} {{- $pathType := .Values.ingress.pathType -}} {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.Version -}} apiVersion: networking.k8s.io/v1 @@ -44,6 +49,12 @@ spec: number: {{ .port }} {{- end }} {{- end }} + # MCP-related paths must be added before the main path to avoid less specific paths being matched first. + {{- if ( and ((.Values.mcp).enabled) $mcpIngress.enabled ) }} + {{- range $mcpIngress.paths }} + {{- include "retool.ingress.mcpPath" (dict "root" $ "path" . "backendApiPort" $backendApiPort) | nindent 10 }} + {{- end }} + {{- end }} - path: {{- if and $pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.Version) }} pathType: {{ $pathType }} @@ -80,6 +91,12 @@ spec: number: {{ .port }} {{- end }} {{- end }} + # MCP-related paths must be added before the main path to avoid less specific paths being matched first. + {{- if ( and (($.Values.mcp).enabled) $mcpIngress.enabled ) }} + {{- range $mcpIngress.paths }} + {{- include "retool.ingress.mcpPath" (dict "root" $ "path" . "backendApiPort" $backendApiPort) | nindent 10 }} + {{- end }} + {{- end }} {{- range .paths }} - path: {{ .path }} {{- if and $pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.Version) }} diff --git a/charts/retool/templates/service.yaml b/charts/retool/templates/service.yaml index f7c5049..4c0b8bc 100644 --- a/charts/retool/templates/service.yaml +++ b/charts/retool/templates/service.yaml @@ -1,3 +1,10 @@ +{{- $mcpNeedsBackendApi := eq (include "retool.mcp.needsBackendApi" .) "true" -}} +{{- $backendApiService := (((.Values.mcp).backendMetadata).service) | default dict -}} +{{- $servicePortName := .Values.service.portName | default "http" -}} +{{- $backendApiPortName := $backendApiService.portName | default "http-api" -}} +{{- if and $mcpNeedsBackendApi (eq $servicePortName $backendApiPortName) -}} +{{- fail "When MCP backend API routing is enabled, .Values.service.portName and .Values.mcp.backendMetadata.service.portName must be different" -}} +{{- end -}} apiVersion: v1 kind: Service metadata: @@ -28,11 +35,17 @@ spec: - port: {{ .Values.service.externalPort }} targetPort: {{ .Values.service.internalPort }} protocol: TCP -{{ if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }} +{{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }} nodePort: {{ .Values.service.nodePort }} -{{ end }} -{{- if .Values.service.portName }} - name: {{ .Values.service.portName }} +{{- end }} +{{- if or .Values.service.portName $mcpNeedsBackendApi }} + name: {{ $servicePortName }} +{{- end }} +{{- if $mcpNeedsBackendApi }} + - name: {{ $backendApiPortName }} + port: {{ $backendApiService.externalPort | default 3001 }} + targetPort: {{ $backendApiService.internalPort | default 3001 }} + protocol: TCP {{- end }} {{- if .Values.service.externalIPs }} externalIPs: diff --git a/charts/retool/values.yaml b/charts/retool/values.yaml index 8347d2d..0b3b5eb 100644 --- a/charts/retool/values.yaml +++ b/charts/retool/values.yaml @@ -563,6 +563,158 @@ multiplayer: annotations: {} labels: {} +mcp: + # Run Retool's MCP server as a separate deployment. Independent of the + # .Values.rr.enabled master switch (the MCP server needs its own OAuth + # introspection config, so it is opt-in): set true to enable. + enabled: false + + replicaCount: 1 + + # Annotations for MCP server pods + annotations: {} + + # Labels for MCP server pods + labels: {} + + # MCP-specific environment variables. Can include valueFrom entries. + # Use this for MCP-only env vars that are not exposed under mcp.config. + environmentVariables: [] + + # MCP server configuration. + # The chart always sets RETOOL_BACKEND_URL, defaulting to the release's + # internal Retool service, e.g. http://:. + # For other config keys, the chart only emits environment variables when you set + # them. When unset, the MCP service uses its runtime defaults: + # enabledToolsets: all available toolsets + # apps, resources, workflows, folders, environments, users, + # organization, user_invites, feedback + # maxTransportSessions: 1000 + # sessionIdleTimeoutMs: 1800000 (30 minutes) + # sessionSweepIntervalMs: 60000 (1 minute) + # sessionGaugeEmitIntervalMs: 30000 (30 seconds) + config: {} + # Example overrides: + # config: + # # Internal URL used by the MCP server to call the Retool backend. + # # Defaults to the release's internal Retool service when unset. + # retoolBackendUrl: + # + # # Internal URL used by the MCP server to call the Retool git server. + # # Unset by default. + # retoolGitServerUrl: + # + # # Public Retool URL used for links. Set explicitly when the request + # # origin is not the right public URL. + # retoolUrl: + # + # # Public OAuth domain used by MCP OAuth metadata routes. Set explicitly + # # when the request origin is not the right OAuth base URL. + # oauthMainDomain: + # + # # Secret-backed token used by MCP to call /api/oauth2/introspect. + # # Required when mcp.enabled is true unless OAUTH_INTROSPECTION_AUTH_TOKEN + # # is provided directly in mcp.environmentVariables. + # oauthIntrospectionAuthTokenSecretName: + # oauthIntrospectionAuthTokenSecretKey: oauthIntrospectionAuthToken + # + # # Literal token override for development/testing only. Prefer the + # # secret-backed setting above for real deployments. + # # Required when mcp.enabled is true unless OAUTH_INTROSPECTION_AUTH_TOKEN + # # is provided directly in mcp.environmentVariables. + # oauthIntrospectionAuthToken: + # + # # Optional Node.js options for the MCP server process. Unset by default. + # nodeOptions: --max_old_space_size=1024 + # + # # Optional MCP service configuration. If unset, all available toolsets + # # are enabled by default. + # enabledToolsets: + # - apps + # - resources + # maxTransportSessions: 1000 + # sessionIdleTimeoutMs: 1800000 + # sessionSweepIntervalMs: 60000 + # sessionGaugeEmitIntervalMs: 30000 + + # Resources for MCP server pods. MCP runs from the backend image and does not + # horizontally scale yet, so its default memory limit is higher than multiplayer. + resources: + requests: + cpu: "200m" + memory: "256Mi" + limits: + memory: "4096Mi" + + # MCP OAuth metadata routes are served by the main Retool backend, not the MCP + # pod. This config exposes the backend API listener as an additional port on + # the main Retool service when a route uses target: backendApi. + backendMetadata: + service: + # Service port that exposes the backend API listener for metadata paths + # that should not fall through to the static frontend server. + portName: http-api + externalPort: 3001 + internalPort: 3001 + + # Public MCP-related ingress paths. Paths are emitted in order before the main + # Retool route. Use target: backendApi for OAuth metadata routes that must hit + # the main backend API listener; use target: mcp for MCP server routes. + ingress: + # This conditional is dependent on mcp.enabled. + enabled: true + paths: + - path: /.well-known/oauth-authorization-server + pathType: Exact + target: backendApi + port: 3001 + - path: /.well-known/oauth-protected-resource/mcp + pathType: Exact + target: backendApi + port: 3001 + - path: /mcp/.well-known/oauth-protected-resource + pathType: Exact + target: backendApi + port: 3001 + - path: /mcp + target: mcp + port: 4010 + - path: /.well-known/oauth-protected-resource + pathType: Exact + target: mcp + port: 4010 + + # HTTPRoute rules for MCP when using Gateway API instead of ingress. + httpRoute: + # This conditional is dependent on mcp.enabled. + enabled: true + rules: + - path: /.well-known/oauth-authorization-server + pathType: Exact + target: backendApi + port: 3001 + - path: /.well-known/oauth-protected-resource/mcp + pathType: Exact + target: backendApi + port: 3001 + - path: /mcp/.well-known/oauth-protected-resource + pathType: Exact + target: backendApi + port: 3001 + - path: /mcp + target: mcp + port: 4010 + - path: /.well-known/oauth-protected-resource + pathType: Exact + target: mcp + port: 4010 + + service: + externalPort: 4010 + internalPort: 4010 + annotations: {} + labels: {} + codeExecutor: # as of Chart version 6.7.0, code-executor image version must align with the top-level `image` parameters # explicitly set other fields as needed @@ -600,11 +752,457 @@ codeExecutor: cpu: 1000m memory: 1024Mi - # code executor uses nsjail to sandbox code execution. nsjail requires privileged container access. - # If your deployment does not support privileged access, you can set `privileged` to false to not - # use nsjail. Without nsjail, all code is run without sandboxing within your deployment. - securityContext: - privileged: true + # The code executor runs workflow code inside nsjail sandboxes, which require + # elevated privileges to create. By default these are granted by running the + # container as privileged (securityContext.privileged: true). Set + # useSeccompProfile: true to instead grant only what nsjail needs, far more + # granularly than the privileged flag: a slightly relaxed version of Docker's + # default seccomp profile, the NET_ADMIN capability for network isolation, and an + # unmasked /proc for process resource monitoring. This requires Kubernetes 1.33 or + # higher (for the ProcMountType and UserNamespacesSupport feature gates) -- do not + # enable it on older clusters. Pinning codeExecutor.securityContext overrides both + # paths and is used verbatim. + useSeccompProfile: false + seccompLocalhostProfile: profiles/nsjail-seccomp.json + +# === RR (Retool agent runtime) ============================================= +# Master switch for the whole RR stack. Set `rr.enabled: true` to turn on the +# components nested below — jsExecutor, agent, and agentSandbox — with a +# single line. Each component's own `enabled` (left null by default) inherits +# this switch; set a component's `enabled` to true/false to override the master +# for that component only. The gitServer and blobStorage blocks below +# provide the React Retool git server and the object storage the stack needs. +# (The MCP server is configured separately at the top level: it needs its own +# OAuth opt-in and is intentionally independent of this master switch.) +rr: + enabled: false + + # JS Executor + jsExecutor: + # Inherits .Values.rr.enabled when left unset (null); set true/false to override. + enabled: null + + image: + repository: tryretool/js-executor-service + # defaults to top level image.tag + tag: null + pullPolicy: IfNotPresent + + replicaCount: 1 + + seccompLocalhostProfile: profiles/nsjail-seccomp.json + + # JS-executor-specific environment; not inherited from the top-level + # .Values.env / .Values.environmentSecrets / .Values.environmentVariables. + env: {} + environmentSecrets: [] + environmentVariables: [] + + # Annotations for JS executor pods + annotations: {} + + # Labels for JS executor pods + labels: {} + + volumes: {} + volumeMounts: {} + + # Config affinity and anti-affinity rules for the JS executor pods + affinity: {} + + # Resources for the JS executor. Memory request and limit are kept equal: + # JSE reads its memory limit and rejects requests at 80% of it, so the + # request must reserve the full amount to avoid premature rejections. + resources: + limits: + cpu: '2' + memory: 6Gi + requests: + cpu: '2' + memory: 6Gi + + # RR Agent: server-side agent loop worker (independent from agents above). + agent: + # Inherits .Values.rr.enabled when left unset (null); set true/false to override. + enabled: null + + # Labels for RR agent worker pods + labels: {} + + # RR agent configuration + config: {} + + # Annotations for RR agent worker pods + annotations: {} + + # RR agent worker configuration + worker: + replicaCount: 1 + + resources: + limits: + cpu: 2000m + memory: 4096Mi + requests: + cpu: 1000m + memory: 2048Mi + + # Agent Sandbox Service: sandboxed code execution for AI agents. + # Deploys a controller (manages sandbox lifecycle), proxy (HTTP proxy for sandbox egress), + # and ephemeral Job-based sandboxes. Uses Postgres for controller/proxy state. + agentSandbox: + # Inherits .Values.rr.enabled when left unset (null); set true/false to override. + enabled: null + + image: + repository: tryretool/agent-sandbox-service + # defaults to top level image.tag + tag: null + pullPolicy: IfNotPresent + + # Lightweight init image used by the prepuller and seccomp DaemonSets. + # Pinning by digest is recommended for production. + initImage: + repository: busybox + tag: '1.37.0' + # Manifest list digest — set to '' in test environments where images are + # pre-loaded (containerd 2.0 can't resolve digest references for side-loaded images). + digest: '' + + # Annotations for agent sandbox pods + annotations: {} + + # Labels for agent sandbox pods + labels: {} + + # === Secrets ============================================================ + # Provide each secret as a plaintext value below, OR set externalSecret.name + # to a pre-existing Secret with keys jwt-public-key, jwt-private-key, + # encryption-key, api-secret, postgres-url. A plaintext value always wins over + # the external secret for that key. + externalSecret: + name: '' # optional: existing Secret holding all keys below + + jwtPublicKey: '' # REQUIRED (ES256) unless provided via externalSecret + jwtPrivateKey: '' # REQUIRED (ES256) unless provided via externalSecret + encryptionKey: '' # optional: hex 256-bit; must match backend AGENT_SANDBOX_ENCRYPTION_KEY + apiSecret: '' # optional: admin/test endpoints + + # === Postgres state backend ============================================= + # By DEFAULT (all options below left blank) the agent sandbox reuses the + # backend's Postgres connection from config.postgresql / the postgresql + # subchart -- same instance and database, separate schema (see schema below). + # So enabling it on an existing deployment needs nothing here. (Exception: + # if the backend's DB password is supplied via external secrets / envFrom, it + # can't be inherited by a separate pod -- set an option below in that case.) + # To point the sandbox at a different database, set exactly ONE option: + postgres: + # -- Option 1: plaintext DSN -- + url: '' + + # -- Option 2: assemble from fields -- + # The password is passed via PGPASSWORD (never embedded in the URL), so any + # characters are safe and a password-only secret can be reused as-is. + # Set either password or passwordSecretName. + # user/database are embedded in the DSN verbatim (user may contain '@', e.g. + # Azure user@servername); for values with : / ? # use Option 1 or 3. + host: '' + port: 5432 + database: '' + user: '' + password: '' + passwordSecretName: '' + passwordSecretKey: 'password' + + # -- Option 3: existing Secret holding the full DSN -- + urlSecretName: '' + urlSecretKey: 'postgres-url' + + # -- Option 4: reuse externalSecret.name (its postgres-url key) -- + # Selected by setting rr.agentSandbox.externalSecret.name (in the Secrets + # section above), not by anything here. Used when options 1-3 are blank. + # + # If options 1-4 are ALL unset, the default (inherit config.postgresql) + # applies -- see the note at the top of this block. + + # -- Optional tuning (defaults shown) -- + schema: 'agent_executor' + poolMax: 10 + sweeperIntervalMs: 60000 + + # Sandbox network access via pasta userspace networking. + # When enabled, sandboxes get isolated outbound access with L7 filtering. + sandboxNetwork: + enabled: true + # Request smarter-devices/net_tun via resources.limits on sandbox pods. + # When true, the kubelet grants /dev/net/tun device cgroup access without + # privileged mode. Requires smarter-device-manager to be running on each + # node (see deployDaemonSet below). + devicePlugin: true + # Deploy the smarter-device-manager DaemonSet from this Helm release. + # Set to false when another release (or external process) already manages + # the DaemonSet — only one instance should run per node. + deployDaemonSet: true + # HTTP proxy for sandbox egress L7 filtering. Defaults to the in-cluster + # agent-sandbox-proxy service URL when empty. + httpProxy: '' + + # smarter-device-manager: registers /dev/net/tun with the kubelet so sandbox + # pods can request it via resources.limits. + devicePlugin: + image: + repository: ghcr.io/smarter-project/smarter-device-manager + tag: v1.20.12 + # Number of /dev/net/tun device slots to register. + # Set high enough to accommodate maxTotalJobs + prewarm pool. + maxDevices: 130 + + # When possible, we want the devicePlugin daemonset to preempt normal pods. + # Note: in some cases this is inconvenient or unsupported, i.e. in GKE which + # requires a custom ResourceQuota to use the `system-node-critical` + # PriorityClass in user namespaces. In those cases, set this to `null`. + priorityClassName: system-node-critical + + # Seccomp profile path relative to /var/lib/kubelet/seccomp/. + # The seccomp node-installer DaemonSet copies the profile to this path + # on every node automatically. + seccompProfile: retool/gvisor-seccomp.json + + # S3-compatible snapshot storage. + # When s3Bucket is set, snapshots are persisted to S3 and survive pod restarts. + snapshotStorage: + s3Bucket: '' + s3Endpoint: '' + s3Region: 'us-east-1' + # Name of a K8s Secret containing keys awsAccessKeyId and awsSecretAccessKey. + # If empty, falls back to the main agent sandbox secret. + credentialsSecretName: '' + + # Sandbox (Job) configuration + sandbox: + port: 3017 + resources: + requests: + cpu: 1 + memory: 2Gi + limits: + cpu: '2' + memory: 4Gi + # Idle timeout (ms) before an unassigned sandbox self-terminates. + sandboxIdleTimeoutMs: 600000 + # Hard ceiling (ms) on total sandbox lifetime, regardless of activity. When + # reached, the sandbox is destroyed (deferred until the current agent loop + # ends). Defaults to 2.5 hours. + sandboxGlobalLifetimeMs: 9000000 + # Time (ms) the controller waits for a freshly-started sandbox to connect + # before failing the request. Interactive sandbox boot (gVisor + bundle + # load) can take ~20-25s; raise this (e.g. 45000) if you see "did not + # connect within ms" errors on cold starts. + sandboxReadyTimeoutMs: 20000 + tmpDirSizeLimit: 20Gi + # Separate limit for the rootfs-appjob volume — the sandbox root filesystem + # is a static ~600MB extraction, so 2Gi provides headroom without the 20Gi + # allocated for /tmp. + rootfsSizeLimit: 2Gi + # Additional environment variables for sandbox containers. + extraEnv: [] + + # Controller: tracks capacity, assigns sandbox pods, manages scaling + controller: + replicaCount: 1 + port: 3018 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + scaling: + prewarmPoolSize: 5 + maxTotalJobs: 50 + maxConcurrentCreates: 3 + jobRetentionSeconds: 300 + assignedSandboxTtlSeconds: 3600 + reconcileIntervalMs: 5000 + leaderTtlMs: 10000 + leaderRenewMs: 3000 + perUserSandboxLimit: 5 + + # Proxy: HTTP proxy for sandbox egress with credential injection. + # The proxy must be reachable by frontend browsers for WebSocket connections. + proxy: + replicaCount: 1 + port: 3019 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + allowedDomains: '' + # URL the proxy uses to reach the Retool backend for token exchange. + # Defaults to http://:3000 (same-cluster backend service). + backendUrl: '' + backendDomainSuffixes: '' + sandboxProxyTimeoutMs: '180000' # 3 minutes + service: + # Set to LoadBalancer or NodePort to expose the proxy externally. + type: ClusterIP + annotations: {} + # Optional ingress to expose the proxy to frontend browsers for WebSocket connections. + # This is separate from the main Retool ingress since the proxy typically runs on its own domain. + ingress: + enabled: false + # ingressClassName: + annotations: {} + # kubernetes.io/ingress.class: nginx + # nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + # nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + host: '' + # e.g. sandbox.yourdomain.com + tls: [] + # - secretName: sandbox-tls + # hosts: + # - sandbox.yourdomain.com + + # Backend integration: these tell the Retool backend how to reach agent executor. + # controllerUrl and proxyUrl default to internal service URLs when empty. + controllerUrl: '' + proxyUrl: '' + # Public URL for frontend browsers to reach the proxy via WebSocket. + # Leave EMPTY for self-hosted: the backend then serves the sandbox same-origin + # as the editor (your Retool base URL) and the front server reverse-proxies the + # /sandbox/* WS+Vite paths to the in-cluster proxy Service — so no dedicated + # proxy domain or ingress is required, and your catch-all ingress is untouched. + # Only set this (e.g. https://sandbox.yourdomain.com) if you deliberately want + # the proxy on a separate domain, in which case also enable proxy.ingress above. + frontendWsProxyDomain: '' + # Public URL for proxy domain. Defaults to frontendWsProxyDomain if empty. + proxyDomain: '' + + # NetworkPolicy: restrict sandbox, controller, and proxy pod traffic. + # Strongly recommended for production to isolate sandbox egress. + networkPolicy: + enabled: false + # CIDR ranges to block in proxy egress rules. Must stay in sync with + # DEFAULT_BLOCKED_CIDRS in the agent-executor source. + blockedRanges: + - 169.254.0.0/16 # link-local / cloud metadata + - 10.0.0.0/8 # private (RFC 1918) + - 172.16.0.0/12 # private (RFC 1918) + - 192.168.0.0/16 # private (RFC 1918) + - 100.64.0.0/10 # carrier-grade NAT (RFC 6598) + - 127.0.0.0/8 # loopback + - 0.0.0.0/8 # "this network" (RFC 791) + blockedRanges6: + - fc00::/7 # IPv6 unique local addresses + - fe80::/10 # IPv6 link-local + - '::1/128' # IPv6 loopback + # Restrict DNS egress to pods matching this selector (typically kube-dns/coredns). + # Set to empty to allow DNS to any destination (not recommended). + dnsSelector: + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + extraEgress: [] + # CIDRs allowed in proxy egress even if they fall within blockedRanges + # (e.g. private backend endpoint). + backendAllowlist: [] + # Override sandbox ingress source selector (defaults to retool backend pods). + ingressFrom: [] + + # Node placement overrides (falls back to global nodeSelector/tolerations if empty) + nodeSelector: {} + tolerations: [] + affinity: {} + + gitServer: + # Runs the React Retool Git Server in-process on the main backend pod + # (SERVICE_TYPE=...,RR_GIT_SERVER). The main backend internally proxies + # /api/ai/rr/git/v2/* to localhost:RR_GIT_SERVER_PORT, so no extra ingress + # routing is required. Required for the rr / React Retool app pipeline. + # + # When enabled, exactly one of blobStorage.s3, blobStorage.gcs, or + # blobStorage.azure must be configured below — git_server stores all + # objects/packs in blob storage. + enabled: false + + # Optional: number of loose objects before git_server triggers a repack. + # Backend default is 100; unset to inherit it. + repackThreshold: ~ + + # Escape hatch for the blob-storage validation below. The chart can only + # inspect blobStorage, env, environmentVariables, and environmentSecrets at + # template time; it cannot see env vars injected via envFrom (Secret/ConfigMap + # splat). Set this to true to bypass the check when RR_BLOB_STORAGE_PROVIDER / + # RR_DEFAULT_* are provided that way. + skipBlobStorageValidation: false + + # Optionally split the git server out of the main backend into its own + # deployment + service (mirrors how the workload is split in Retool Cloud). + # Requires rr.gitServer.enabled: true. When enabled: + # - a dedicated -git-server Deployment runs SERVICE_TYPE=RR_GIT_SERVER + # - the main backend drops RR_GIT_SERVER from its SERVICE_TYPE and proxies git + # traffic to the service via RR_GIT_SERVER_HOST / RR_GIT_SERVER_PORT + # - the MCP server (if enabled) is auto-pointed at the same service unless + # mcp.config.retoolGitServerUrl is set explicitly + # The blobStorage config below is rendered onto the git-server pod instead of + # the main backend in this mode. + separate: + enabled: false + replicaCount: 1 + # Port the git server listens on (RR_GIT_SERVER_PORT) and that its service exposes. + port: 3010 + # Pod resource requests/limits. Falls back to top-level `resources` if unset. + resources: {} + # Falls back to top-level `affinity` if unset. + affinity: {} + # Annotations/labels applied to the git-server pod template. + annotations: {} + labels: {} + # Annotations/labels applied to the git-server Service (kept separate from + # the pod ones above). + service: + annotations: {} + labels: {} + + # Shared blob-storage config used by git_server (and other features that + # need object storage, e.g. snapshots). Set exactly one of s3, gcs, azure. + # Renders RR_BLOB_STORAGE_PROVIDER + RR_DEFAULT__* env vars on + # the backend deployment. + # + # This block can be omitted entirely if RR_BLOB_STORAGE_PROVIDER and the + # RR_DEFAULT_*_* env vars are provided directly via environmentVariables / + # environmentSecrets above — the chart detects that and skips this guard. + blobStorage: {} + # s3: + # bucket: my-rr-bucket + # region: us-east-1 + # endpoint: "" # optional, for S3-compatible (MinIO, R2, etc.) + # accessKeyId: AKIA... + # # Provide secretAccessKey OR the secretName/secretKey pair below. + # secretAccessKey: "" + # secretAccessKeySecretName: "" + # secretAccessKeySecretKey: secret-access-key + # + # gcs: + # bucket: my-rr-bucket + # # Provide credentials (JSON string) OR the secretName/secretKey pair below. + # credentials: "" + # credentialsSecretName: "" + # credentialsSecretKey: credentials.json + # + # azure: + # container: my-rr-container + # # Provide connectionString OR the secretName/secretKey pair below. + # connectionString: "" + # connectionStringSecretName: "" + # connectionStringSecretKey: connection-string agents: # Enable AI Agents diff --git a/values.yaml b/values.yaml index 8347d2d..0b3b5eb 100644 --- a/values.yaml +++ b/values.yaml @@ -563,6 +563,158 @@ multiplayer: annotations: {} labels: {} +mcp: + # Run Retool's MCP server as a separate deployment. Independent of the + # .Values.rr.enabled master switch (the MCP server needs its own OAuth + # introspection config, so it is opt-in): set true to enable. + enabled: false + + replicaCount: 1 + + # Annotations for MCP server pods + annotations: {} + + # Labels for MCP server pods + labels: {} + + # MCP-specific environment variables. Can include valueFrom entries. + # Use this for MCP-only env vars that are not exposed under mcp.config. + environmentVariables: [] + + # MCP server configuration. + # The chart always sets RETOOL_BACKEND_URL, defaulting to the release's + # internal Retool service, e.g. http://:. + # For other config keys, the chart only emits environment variables when you set + # them. When unset, the MCP service uses its runtime defaults: + # enabledToolsets: all available toolsets + # apps, resources, workflows, folders, environments, users, + # organization, user_invites, feedback + # maxTransportSessions: 1000 + # sessionIdleTimeoutMs: 1800000 (30 minutes) + # sessionSweepIntervalMs: 60000 (1 minute) + # sessionGaugeEmitIntervalMs: 30000 (30 seconds) + config: {} + # Example overrides: + # config: + # # Internal URL used by the MCP server to call the Retool backend. + # # Defaults to the release's internal Retool service when unset. + # retoolBackendUrl: + # + # # Internal URL used by the MCP server to call the Retool git server. + # # Unset by default. + # retoolGitServerUrl: + # + # # Public Retool URL used for links. Set explicitly when the request + # # origin is not the right public URL. + # retoolUrl: + # + # # Public OAuth domain used by MCP OAuth metadata routes. Set explicitly + # # when the request origin is not the right OAuth base URL. + # oauthMainDomain: + # + # # Secret-backed token used by MCP to call /api/oauth2/introspect. + # # Required when mcp.enabled is true unless OAUTH_INTROSPECTION_AUTH_TOKEN + # # is provided directly in mcp.environmentVariables. + # oauthIntrospectionAuthTokenSecretName: + # oauthIntrospectionAuthTokenSecretKey: oauthIntrospectionAuthToken + # + # # Literal token override for development/testing only. Prefer the + # # secret-backed setting above for real deployments. + # # Required when mcp.enabled is true unless OAUTH_INTROSPECTION_AUTH_TOKEN + # # is provided directly in mcp.environmentVariables. + # oauthIntrospectionAuthToken: + # + # # Optional Node.js options for the MCP server process. Unset by default. + # nodeOptions: --max_old_space_size=1024 + # + # # Optional MCP service configuration. If unset, all available toolsets + # # are enabled by default. + # enabledToolsets: + # - apps + # - resources + # maxTransportSessions: 1000 + # sessionIdleTimeoutMs: 1800000 + # sessionSweepIntervalMs: 60000 + # sessionGaugeEmitIntervalMs: 30000 + + # Resources for MCP server pods. MCP runs from the backend image and does not + # horizontally scale yet, so its default memory limit is higher than multiplayer. + resources: + requests: + cpu: "200m" + memory: "256Mi" + limits: + memory: "4096Mi" + + # MCP OAuth metadata routes are served by the main Retool backend, not the MCP + # pod. This config exposes the backend API listener as an additional port on + # the main Retool service when a route uses target: backendApi. + backendMetadata: + service: + # Service port that exposes the backend API listener for metadata paths + # that should not fall through to the static frontend server. + portName: http-api + externalPort: 3001 + internalPort: 3001 + + # Public MCP-related ingress paths. Paths are emitted in order before the main + # Retool route. Use target: backendApi for OAuth metadata routes that must hit + # the main backend API listener; use target: mcp for MCP server routes. + ingress: + # This conditional is dependent on mcp.enabled. + enabled: true + paths: + - path: /.well-known/oauth-authorization-server + pathType: Exact + target: backendApi + port: 3001 + - path: /.well-known/oauth-protected-resource/mcp + pathType: Exact + target: backendApi + port: 3001 + - path: /mcp/.well-known/oauth-protected-resource + pathType: Exact + target: backendApi + port: 3001 + - path: /mcp + target: mcp + port: 4010 + - path: /.well-known/oauth-protected-resource + pathType: Exact + target: mcp + port: 4010 + + # HTTPRoute rules for MCP when using Gateway API instead of ingress. + httpRoute: + # This conditional is dependent on mcp.enabled. + enabled: true + rules: + - path: /.well-known/oauth-authorization-server + pathType: Exact + target: backendApi + port: 3001 + - path: /.well-known/oauth-protected-resource/mcp + pathType: Exact + target: backendApi + port: 3001 + - path: /mcp/.well-known/oauth-protected-resource + pathType: Exact + target: backendApi + port: 3001 + - path: /mcp + target: mcp + port: 4010 + - path: /.well-known/oauth-protected-resource + pathType: Exact + target: mcp + port: 4010 + + service: + externalPort: 4010 + internalPort: 4010 + annotations: {} + labels: {} + codeExecutor: # as of Chart version 6.7.0, code-executor image version must align with the top-level `image` parameters # explicitly set other fields as needed @@ -600,11 +752,457 @@ codeExecutor: cpu: 1000m memory: 1024Mi - # code executor uses nsjail to sandbox code execution. nsjail requires privileged container access. - # If your deployment does not support privileged access, you can set `privileged` to false to not - # use nsjail. Without nsjail, all code is run without sandboxing within your deployment. - securityContext: - privileged: true + # The code executor runs workflow code inside nsjail sandboxes, which require + # elevated privileges to create. By default these are granted by running the + # container as privileged (securityContext.privileged: true). Set + # useSeccompProfile: true to instead grant only what nsjail needs, far more + # granularly than the privileged flag: a slightly relaxed version of Docker's + # default seccomp profile, the NET_ADMIN capability for network isolation, and an + # unmasked /proc for process resource monitoring. This requires Kubernetes 1.33 or + # higher (for the ProcMountType and UserNamespacesSupport feature gates) -- do not + # enable it on older clusters. Pinning codeExecutor.securityContext overrides both + # paths and is used verbatim. + useSeccompProfile: false + seccompLocalhostProfile: profiles/nsjail-seccomp.json + +# === RR (Retool agent runtime) ============================================= +# Master switch for the whole RR stack. Set `rr.enabled: true` to turn on the +# components nested below — jsExecutor, agent, and agentSandbox — with a +# single line. Each component's own `enabled` (left null by default) inherits +# this switch; set a component's `enabled` to true/false to override the master +# for that component only. The gitServer and blobStorage blocks below +# provide the React Retool git server and the object storage the stack needs. +# (The MCP server is configured separately at the top level: it needs its own +# OAuth opt-in and is intentionally independent of this master switch.) +rr: + enabled: false + + # JS Executor + jsExecutor: + # Inherits .Values.rr.enabled when left unset (null); set true/false to override. + enabled: null + + image: + repository: tryretool/js-executor-service + # defaults to top level image.tag + tag: null + pullPolicy: IfNotPresent + + replicaCount: 1 + + seccompLocalhostProfile: profiles/nsjail-seccomp.json + + # JS-executor-specific environment; not inherited from the top-level + # .Values.env / .Values.environmentSecrets / .Values.environmentVariables. + env: {} + environmentSecrets: [] + environmentVariables: [] + + # Annotations for JS executor pods + annotations: {} + + # Labels for JS executor pods + labels: {} + + volumes: {} + volumeMounts: {} + + # Config affinity and anti-affinity rules for the JS executor pods + affinity: {} + + # Resources for the JS executor. Memory request and limit are kept equal: + # JSE reads its memory limit and rejects requests at 80% of it, so the + # request must reserve the full amount to avoid premature rejections. + resources: + limits: + cpu: '2' + memory: 6Gi + requests: + cpu: '2' + memory: 6Gi + + # RR Agent: server-side agent loop worker (independent from agents above). + agent: + # Inherits .Values.rr.enabled when left unset (null); set true/false to override. + enabled: null + + # Labels for RR agent worker pods + labels: {} + + # RR agent configuration + config: {} + + # Annotations for RR agent worker pods + annotations: {} + + # RR agent worker configuration + worker: + replicaCount: 1 + + resources: + limits: + cpu: 2000m + memory: 4096Mi + requests: + cpu: 1000m + memory: 2048Mi + + # Agent Sandbox Service: sandboxed code execution for AI agents. + # Deploys a controller (manages sandbox lifecycle), proxy (HTTP proxy for sandbox egress), + # and ephemeral Job-based sandboxes. Uses Postgres for controller/proxy state. + agentSandbox: + # Inherits .Values.rr.enabled when left unset (null); set true/false to override. + enabled: null + + image: + repository: tryretool/agent-sandbox-service + # defaults to top level image.tag + tag: null + pullPolicy: IfNotPresent + + # Lightweight init image used by the prepuller and seccomp DaemonSets. + # Pinning by digest is recommended for production. + initImage: + repository: busybox + tag: '1.37.0' + # Manifest list digest — set to '' in test environments where images are + # pre-loaded (containerd 2.0 can't resolve digest references for side-loaded images). + digest: '' + + # Annotations for agent sandbox pods + annotations: {} + + # Labels for agent sandbox pods + labels: {} + + # === Secrets ============================================================ + # Provide each secret as a plaintext value below, OR set externalSecret.name + # to a pre-existing Secret with keys jwt-public-key, jwt-private-key, + # encryption-key, api-secret, postgres-url. A plaintext value always wins over + # the external secret for that key. + externalSecret: + name: '' # optional: existing Secret holding all keys below + + jwtPublicKey: '' # REQUIRED (ES256) unless provided via externalSecret + jwtPrivateKey: '' # REQUIRED (ES256) unless provided via externalSecret + encryptionKey: '' # optional: hex 256-bit; must match backend AGENT_SANDBOX_ENCRYPTION_KEY + apiSecret: '' # optional: admin/test endpoints + + # === Postgres state backend ============================================= + # By DEFAULT (all options below left blank) the agent sandbox reuses the + # backend's Postgres connection from config.postgresql / the postgresql + # subchart -- same instance and database, separate schema (see schema below). + # So enabling it on an existing deployment needs nothing here. (Exception: + # if the backend's DB password is supplied via external secrets / envFrom, it + # can't be inherited by a separate pod -- set an option below in that case.) + # To point the sandbox at a different database, set exactly ONE option: + postgres: + # -- Option 1: plaintext DSN -- + url: '' + + # -- Option 2: assemble from fields -- + # The password is passed via PGPASSWORD (never embedded in the URL), so any + # characters are safe and a password-only secret can be reused as-is. + # Set either password or passwordSecretName. + # user/database are embedded in the DSN verbatim (user may contain '@', e.g. + # Azure user@servername); for values with : / ? # use Option 1 or 3. + host: '' + port: 5432 + database: '' + user: '' + password: '' + passwordSecretName: '' + passwordSecretKey: 'password' + + # -- Option 3: existing Secret holding the full DSN -- + urlSecretName: '' + urlSecretKey: 'postgres-url' + + # -- Option 4: reuse externalSecret.name (its postgres-url key) -- + # Selected by setting rr.agentSandbox.externalSecret.name (in the Secrets + # section above), not by anything here. Used when options 1-3 are blank. + # + # If options 1-4 are ALL unset, the default (inherit config.postgresql) + # applies -- see the note at the top of this block. + + # -- Optional tuning (defaults shown) -- + schema: 'agent_executor' + poolMax: 10 + sweeperIntervalMs: 60000 + + # Sandbox network access via pasta userspace networking. + # When enabled, sandboxes get isolated outbound access with L7 filtering. + sandboxNetwork: + enabled: true + # Request smarter-devices/net_tun via resources.limits on sandbox pods. + # When true, the kubelet grants /dev/net/tun device cgroup access without + # privileged mode. Requires smarter-device-manager to be running on each + # node (see deployDaemonSet below). + devicePlugin: true + # Deploy the smarter-device-manager DaemonSet from this Helm release. + # Set to false when another release (or external process) already manages + # the DaemonSet — only one instance should run per node. + deployDaemonSet: true + # HTTP proxy for sandbox egress L7 filtering. Defaults to the in-cluster + # agent-sandbox-proxy service URL when empty. + httpProxy: '' + + # smarter-device-manager: registers /dev/net/tun with the kubelet so sandbox + # pods can request it via resources.limits. + devicePlugin: + image: + repository: ghcr.io/smarter-project/smarter-device-manager + tag: v1.20.12 + # Number of /dev/net/tun device slots to register. + # Set high enough to accommodate maxTotalJobs + prewarm pool. + maxDevices: 130 + + # When possible, we want the devicePlugin daemonset to preempt normal pods. + # Note: in some cases this is inconvenient or unsupported, i.e. in GKE which + # requires a custom ResourceQuota to use the `system-node-critical` + # PriorityClass in user namespaces. In those cases, set this to `null`. + priorityClassName: system-node-critical + + # Seccomp profile path relative to /var/lib/kubelet/seccomp/. + # The seccomp node-installer DaemonSet copies the profile to this path + # on every node automatically. + seccompProfile: retool/gvisor-seccomp.json + + # S3-compatible snapshot storage. + # When s3Bucket is set, snapshots are persisted to S3 and survive pod restarts. + snapshotStorage: + s3Bucket: '' + s3Endpoint: '' + s3Region: 'us-east-1' + # Name of a K8s Secret containing keys awsAccessKeyId and awsSecretAccessKey. + # If empty, falls back to the main agent sandbox secret. + credentialsSecretName: '' + + # Sandbox (Job) configuration + sandbox: + port: 3017 + resources: + requests: + cpu: 1 + memory: 2Gi + limits: + cpu: '2' + memory: 4Gi + # Idle timeout (ms) before an unassigned sandbox self-terminates. + sandboxIdleTimeoutMs: 600000 + # Hard ceiling (ms) on total sandbox lifetime, regardless of activity. When + # reached, the sandbox is destroyed (deferred until the current agent loop + # ends). Defaults to 2.5 hours. + sandboxGlobalLifetimeMs: 9000000 + # Time (ms) the controller waits for a freshly-started sandbox to connect + # before failing the request. Interactive sandbox boot (gVisor + bundle + # load) can take ~20-25s; raise this (e.g. 45000) if you see "did not + # connect within ms" errors on cold starts. + sandboxReadyTimeoutMs: 20000 + tmpDirSizeLimit: 20Gi + # Separate limit for the rootfs-appjob volume — the sandbox root filesystem + # is a static ~600MB extraction, so 2Gi provides headroom without the 20Gi + # allocated for /tmp. + rootfsSizeLimit: 2Gi + # Additional environment variables for sandbox containers. + extraEnv: [] + + # Controller: tracks capacity, assigns sandbox pods, manages scaling + controller: + replicaCount: 1 + port: 3018 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + scaling: + prewarmPoolSize: 5 + maxTotalJobs: 50 + maxConcurrentCreates: 3 + jobRetentionSeconds: 300 + assignedSandboxTtlSeconds: 3600 + reconcileIntervalMs: 5000 + leaderTtlMs: 10000 + leaderRenewMs: 3000 + perUserSandboxLimit: 5 + + # Proxy: HTTP proxy for sandbox egress with credential injection. + # The proxy must be reachable by frontend browsers for WebSocket connections. + proxy: + replicaCount: 1 + port: 3019 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + allowedDomains: '' + # URL the proxy uses to reach the Retool backend for token exchange. + # Defaults to http://:3000 (same-cluster backend service). + backendUrl: '' + backendDomainSuffixes: '' + sandboxProxyTimeoutMs: '180000' # 3 minutes + service: + # Set to LoadBalancer or NodePort to expose the proxy externally. + type: ClusterIP + annotations: {} + # Optional ingress to expose the proxy to frontend browsers for WebSocket connections. + # This is separate from the main Retool ingress since the proxy typically runs on its own domain. + ingress: + enabled: false + # ingressClassName: + annotations: {} + # kubernetes.io/ingress.class: nginx + # nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + # nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + host: '' + # e.g. sandbox.yourdomain.com + tls: [] + # - secretName: sandbox-tls + # hosts: + # - sandbox.yourdomain.com + + # Backend integration: these tell the Retool backend how to reach agent executor. + # controllerUrl and proxyUrl default to internal service URLs when empty. + controllerUrl: '' + proxyUrl: '' + # Public URL for frontend browsers to reach the proxy via WebSocket. + # Leave EMPTY for self-hosted: the backend then serves the sandbox same-origin + # as the editor (your Retool base URL) and the front server reverse-proxies the + # /sandbox/* WS+Vite paths to the in-cluster proxy Service — so no dedicated + # proxy domain or ingress is required, and your catch-all ingress is untouched. + # Only set this (e.g. https://sandbox.yourdomain.com) if you deliberately want + # the proxy on a separate domain, in which case also enable proxy.ingress above. + frontendWsProxyDomain: '' + # Public URL for proxy domain. Defaults to frontendWsProxyDomain if empty. + proxyDomain: '' + + # NetworkPolicy: restrict sandbox, controller, and proxy pod traffic. + # Strongly recommended for production to isolate sandbox egress. + networkPolicy: + enabled: false + # CIDR ranges to block in proxy egress rules. Must stay in sync with + # DEFAULT_BLOCKED_CIDRS in the agent-executor source. + blockedRanges: + - 169.254.0.0/16 # link-local / cloud metadata + - 10.0.0.0/8 # private (RFC 1918) + - 172.16.0.0/12 # private (RFC 1918) + - 192.168.0.0/16 # private (RFC 1918) + - 100.64.0.0/10 # carrier-grade NAT (RFC 6598) + - 127.0.0.0/8 # loopback + - 0.0.0.0/8 # "this network" (RFC 791) + blockedRanges6: + - fc00::/7 # IPv6 unique local addresses + - fe80::/10 # IPv6 link-local + - '::1/128' # IPv6 loopback + # Restrict DNS egress to pods matching this selector (typically kube-dns/coredns). + # Set to empty to allow DNS to any destination (not recommended). + dnsSelector: + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + extraEgress: [] + # CIDRs allowed in proxy egress even if they fall within blockedRanges + # (e.g. private backend endpoint). + backendAllowlist: [] + # Override sandbox ingress source selector (defaults to retool backend pods). + ingressFrom: [] + + # Node placement overrides (falls back to global nodeSelector/tolerations if empty) + nodeSelector: {} + tolerations: [] + affinity: {} + + gitServer: + # Runs the React Retool Git Server in-process on the main backend pod + # (SERVICE_TYPE=...,RR_GIT_SERVER). The main backend internally proxies + # /api/ai/rr/git/v2/* to localhost:RR_GIT_SERVER_PORT, so no extra ingress + # routing is required. Required for the rr / React Retool app pipeline. + # + # When enabled, exactly one of blobStorage.s3, blobStorage.gcs, or + # blobStorage.azure must be configured below — git_server stores all + # objects/packs in blob storage. + enabled: false + + # Optional: number of loose objects before git_server triggers a repack. + # Backend default is 100; unset to inherit it. + repackThreshold: ~ + + # Escape hatch for the blob-storage validation below. The chart can only + # inspect blobStorage, env, environmentVariables, and environmentSecrets at + # template time; it cannot see env vars injected via envFrom (Secret/ConfigMap + # splat). Set this to true to bypass the check when RR_BLOB_STORAGE_PROVIDER / + # RR_DEFAULT_* are provided that way. + skipBlobStorageValidation: false + + # Optionally split the git server out of the main backend into its own + # deployment + service (mirrors how the workload is split in Retool Cloud). + # Requires rr.gitServer.enabled: true. When enabled: + # - a dedicated -git-server Deployment runs SERVICE_TYPE=RR_GIT_SERVER + # - the main backend drops RR_GIT_SERVER from its SERVICE_TYPE and proxies git + # traffic to the service via RR_GIT_SERVER_HOST / RR_GIT_SERVER_PORT + # - the MCP server (if enabled) is auto-pointed at the same service unless + # mcp.config.retoolGitServerUrl is set explicitly + # The blobStorage config below is rendered onto the git-server pod instead of + # the main backend in this mode. + separate: + enabled: false + replicaCount: 1 + # Port the git server listens on (RR_GIT_SERVER_PORT) and that its service exposes. + port: 3010 + # Pod resource requests/limits. Falls back to top-level `resources` if unset. + resources: {} + # Falls back to top-level `affinity` if unset. + affinity: {} + # Annotations/labels applied to the git-server pod template. + annotations: {} + labels: {} + # Annotations/labels applied to the git-server Service (kept separate from + # the pod ones above). + service: + annotations: {} + labels: {} + + # Shared blob-storage config used by git_server (and other features that + # need object storage, e.g. snapshots). Set exactly one of s3, gcs, azure. + # Renders RR_BLOB_STORAGE_PROVIDER + RR_DEFAULT__* env vars on + # the backend deployment. + # + # This block can be omitted entirely if RR_BLOB_STORAGE_PROVIDER and the + # RR_DEFAULT_*_* env vars are provided directly via environmentVariables / + # environmentSecrets above — the chart detects that and skips this guard. + blobStorage: {} + # s3: + # bucket: my-rr-bucket + # region: us-east-1 + # endpoint: "" # optional, for S3-compatible (MinIO, R2, etc.) + # accessKeyId: AKIA... + # # Provide secretAccessKey OR the secretName/secretKey pair below. + # secretAccessKey: "" + # secretAccessKeySecretName: "" + # secretAccessKeySecretKey: secret-access-key + # + # gcs: + # bucket: my-rr-bucket + # # Provide credentials (JSON string) OR the secretName/secretKey pair below. + # credentials: "" + # credentialsSecretName: "" + # credentialsSecretKey: credentials.json + # + # azure: + # container: my-rr-container + # # Provide connectionString OR the secretName/secretKey pair below. + # connectionString: "" + # connectionStringSecretName: "" + # connectionStringSecretKey: connection-string agents: # Enable AI Agents