diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 000000000..e1eb28a62 --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1,9 @@ +# Patterns to ignore when building packages. +.DS_Store +.git/ +.gitignore +.vscode/ +.idea/ +*.swp +*.bak +*.tmp diff --git a/helm/ARCHITECTURE.md b/helm/ARCHITECTURE.md new file mode 100644 index 000000000..cb9c72777 --- /dev/null +++ b/helm/ARCHITECTURE.md @@ -0,0 +1,472 @@ +# nao Helm Chart — Architecture & Values Reference + +This document explains how the nao Helm chart is structured, how its components interact at runtime, and provides a complete reference for every configurable value. + +--- + +## Table of contents + +- [Application architecture](#application-architecture) +- [Development architecture (docker-compose)](#development-architecture-docker-compose) +- [Docker build (multi-stage)](#docker-build-multi-stage) +- [Kubernetes resources](#kubernetes-resources) +- [Git sync initContainer](#git-sync-initcontainer) +- [Context modes](#context-modes) +- [Secret vs ConfigMap split](#secret-vs-configmap-split) +- [Complete values reference](#complete-values-reference) + - [General](#general) + - [Image](#image) + - [Service account](#service-account) + - [Application config (ConfigMap)](#application-config-configmap) + - [Secrets](#secrets) + - [Service](#service) + - [Resources](#resources) + - [Probes](#probes) + - [Autoscaling — HPA](#autoscaling--hpa) + - [Pod Disruption Budget](#pod-disruption-budget) + - [Node scheduling](#node-scheduling) + - [Persistence — local mode](#persistence--local-mode) + - [Projects volume — api mode](#projects-volume--api-mode) + - [PostgreSQL subchart](#postgresql-subchart) + +--- + +## Runtime architecture + +The nao Docker image bundles **two processes** managed by `supervisord`. Both run inside a single container in the same Pod. + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Kubernetes Pod │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ supervisord │ │ +│ │ │ │ +│ │ ┌──────────────────────────┐ ┌───────────────────────────┐ │ │ +│ │ │ Fastify / Bun │ │ FastAPI (Python) │ │ │ +│ │ │ backend │ │ analytics sidecar │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ • REST API │ │ • LLM orchestration │ │ │ +│ │ │ • tRPC │ │ • SQL generation │ │ │ +│ │ │ • Serves frontend SPA │ │ • Tool execution │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ port 5005 ◄────────────┼──┼── exposed via Service │ │ │ +│ │ │ │ │ port 8005 (internal) │ │ │ +│ │ └──────────────────────────┘ └───────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ +│ Volumes (conditional on context mode) │ +│ ┌─────────────────────────┐ ┌──────────────────────────────┐ │ +│ │ context PVC │ │ projects PVC │ │ +│ │ mountPath: contextPath │ │ mountPath: /app/projects │ │ +│ │ (local mode) │ │ (api mode) │ │ +│ └─────────────────────────┘ └──────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ port 5005 + ▼ + ┌────────────────────┐ + │ Service │ + │ ClusterIP / LB │ + └────────────────────┘ + + ┌────────────────────────────────────────────────────────────┐ + │ PostgreSQL (bitnami subchart, StatefulSet) │ + │ or external DB supplied via secrets.dbUri │ + └────────────────────────────────────────────────────────────┘ +``` + +| Process | Port | Visibility | Role | +|---------|------|-----------|------| +| **Fastify / Bun** | 5005 | External (Kubernetes Service) | REST API, tRPC, serves the React SPA | +| **FastAPI** | 8005 | Internal (pod-local only) | Agent execution, SQL generation, LLM tool calls | + +Key runtime properties: +- `supervisord` starts as **root** (PID 1) and drops privileges to the **`nao`** user (uid/gid 1000) for child processes. The pod security context must allow root startup — see the [security context note](#general) below. +- `supervisord` automatically restarts either process on crash. +- Port **8005** is never exposed outside the pod — all external traffic enters on port **5005**. + +--- + +## Development architecture (docker-compose) + +For local development, `docker-compose.yml` runs two services: + +``` +┌──────────────────┐ ┌──────────────────────────┐ +│ postgres │ │ nao (from Dockerfile) │ +│ :5432 │◄───────┤ :5005 │ +│ │ DB_URI │ Fastify + FastAPI │ +│ PostgreSQL 16 │ │ supervisord │ +└──────────────────┘ └──────────────────────────┘ +``` + +```bash +# Start all services +npm run dev + +# Or individually: +npm run dev:backend # Fastify + Bun (port 5005) +npm run dev:frontend # Vite + React (port 3000) +npm run dev:fastapi # FastAPI sidecar (port 8005) +``` + +The `DB_URI` connects the backend to the `postgres` service by Docker network name. + +--- + +## Docker build (multi-stage) + +The Docker image is built via a **multi-stage Dockerfile** with 5 stages: + +``` +Stage 1: base → Node.js 24-slim + Bun +Stage 2: deps → npm/bun install for all workspaces +Stage 3: frontend → Vite build of React SPA +Stage 4: python → uv install of nao-core Python package +Stage 5: runtime → Final image combining all artifacts +``` + +The runtime image includes: +- Node.js + Bun (copied from base stage) +- Python + nao-core (from python-builder) +- Backend source + node_modules (from deps) +- Frontend build output (from frontend-builder) +- Example project directory (fallback for local mode) +- `supervisord` configuration + +--- + +## Kubernetes resources + +The chart creates the following resources. Resources marked *(conditional)* are only created when the corresponding value is enabled. + +| Resource | Name | Condition | +|----------|------|-----------| +| `Deployment` | `-nao` | Always | +| `Service` | `-nao` | Always | +| `ConfigMap` | `-nao` | Always | +| `Secret` | `-nao` | Always | +| `ServiceAccount` | `-nao` | `serviceAccount.create=true` | +| `PersistentVolumeClaim` (context) | `-nao-context` | `contextSource=local` + `persistence.enabled=true` + no `existingClaim` | +| `PersistentVolumeClaim` (projects) | `-nao-projects` | `contextSource=api` + `projectsPersistence.enabled=true` + no `existingClaim` | +| `HorizontalPodAutoscaler` | `-nao` | `autoscaling.enabled=true` | +| `PodDisruptionBudget` | `-nao` | `podDisruptionBudget.enabled=true` | +| `Pod` (helm test) | `-nao-test-connection` | `helm test` only | + +When `postgresql.enabled=true`, the bitnami subchart additionally creates: a `StatefulSet`, two `Services` (one headless), a `Secret`, a `ServiceAccount`, a `NetworkPolicy`, and a `PodDisruptionBudget` for the database. + +### Git sync initContainer + +When `contextSource=local`, `persistence.enabled=true`, and `gitSync.url` is set, a `git-sync` initContainer runs **before** the main container to automatically clone (or pull) the context repository into the PVC. + +- Uses HTTPS with token authentication (`oauth2:@host`) — **no SSH keys needed** +- On pod restart/crash, the initContainer automatically re-clones/pulls + +--- + +## Context modes + +nao needs a **project directory** containing a `nao_config.yaml` that describes the data sources, schemas, and LLM tools available to the agent. The chart supports three modes, selected via `config.contextSource`. + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ "local" │ +│ ─────────────────────────────────────────────────────────────────────── │ +│ A PVC is mounted at config.contextPath (/app/project by default). │ +│ You are responsible for pre-populating that volume with a valid │ +│ nao project before the pod starts. │ +│ │ +│ Required values: │ +│ persistence.enabled: true │ +│ persistence.size: │ +│ │ +│ Best for: static configurations, on-prem environments. │ +├──────────────────────────────────────────────────────────────────────────┤ +│ "git" │ +│ ─────────────────────────────────────────────────────────────────────── │ +│ The entrypoint clones (or pulls) a git repository into contextPath at │ +│ every pod startup. An optional cron schedule (refreshSchedule) can │ +│ periodically re-pull the repository while the pod is running. │ +│ │ +│ Required values: │ +│ config.contextGitUrl: │ +│ secrets.contextGitToken: ← only for private repos │ +│ │ +│ Best for: version-controlled configurations, GitOps workflows. │ +├──────────────────────────────────────────────────────────────────────────┤ +│ "api" │ +│ ─────────────────────────────────────────────────────────────────────── │ +│ Projects are deployed dynamically after the pod is running, using │ +│ the `nao deploy` CLI command against the running instance. │ +│ Projects are stored in a writable PVC at /app/projects. │ +│ │ +│ Required values: │ +│ projectsPersistence.enabled: true │ +│ │ +│ Best for: multi-project / multi-tenant deployments, SaaS setups. │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Secret vs ConfigMap split + +Environment variables are split between a `ConfigMap` (non-sensitive) and a `Secret` (sensitive). Both are injected into the container via `envFrom`. + +**ConfigMap** — `helm/templates/configmap.yaml`: +- `SERVER_PORT`, `FASTAPI_PORT`, `NODE_ENV`, `MODE`, `DOCKER` +- `BETTER_AUTH_URL` +- `NAO_CONTEXT_SOURCE`, `NAO_DEFAULT_PROJECT_PATH` +- `NAO_CONTEXT_GIT_URL`, `NAO_CONTEXT_GIT_BRANCH`, `NAO_REFRESH_SCHEDULE` +- `POSTHOG_KEY`, `POSTHOG_HOST`, `POSTHOG_DISABLED` + +**Secret** — `helm/templates/secret.yaml`: +- `BETTER_AUTH_SECRET` +- `DB_URI` (auto-built from subchart values when `postgresql.enabled=true`) +- All LLM provider keys: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, Azure, AWS Bedrock +- `NAO_CONTEXT_GIT_TOKEN` +- SMTP credentials, OAuth client secrets (Google, GitHub) +- `NOTION_API_KEY` + +> In production, manage Secret values with an external secret manager (External Secrets Operator, AWS Secrets Manager, Sealed Secrets) rather than hardcoding them in values files. + +--- + +## Complete values reference + +### General + +| Key | Default | Description | +|-----|---------|-------------| +| `nameOverride` | `""` | Override the chart name used in resource names | +| `fullnameOverride` | `""` | Override the fully qualified resource name | +| `replicaCount` | `1` | Number of nao pod replicas. Ignored when `autoscaling.enabled=true`. | +| `imagePullSecrets` | `[]` | List of `{name: secret-name}` entries for pulling from private registries | +| `podAnnotations` | `{}` | Extra annotations added to each Pod spec | +| `podSecurityContext` | `{}` | Pod-level security context. **Do not set `runAsNonRoot: true` or `runAsUser`** — supervisord must start as root (uid 0) to drop to the `nao` user for child processes. You may set `fsGroup: 1000` if PVC write access requires it. | +| `securityContext` | `{}` | Container-level security context. **Do not set `allowPrivilegeEscalation: false` or drop capabilities** — they conflict with supervisord's privilege-dropping mechanism. | + +### Image + +| Key | Default | Description | +|-----|---------|-------------| +| `image.repository` | `ghcr.io/getnao/nao` | Container image repository — the official nao image published to GitHub Container Registry | +| `image.tag` | `""` | Image tag. Empty string defaults to `.Chart.AppVersion`. | +| `image.pullPolicy` | `IfNotPresent` | Pull policy: `Always`, `IfNotPresent`, or `Never` | + +### Service account + +| Key | Default | Description | +|-----|---------|-------------| +| `serviceAccount.create` | `true` | Create a dedicated `ServiceAccount` for the pod | +| `serviceAccount.annotations` | `{}` | Annotations on the `ServiceAccount` (e.g. `eks.amazonaws.com/role-arn` for IRSA) | +| `serviceAccount.name` | `""` | Override the service account name; defaults to the release fullname | + +### Application config (ConfigMap) + +These values are rendered into the `ConfigMap` and injected as environment variables. + +| Key | Default | Env var | Description | +|-----|---------|---------|-------------| +| `config.serverPort` | `"5005"` | `SERVER_PORT` | Fastify backend listening port | +| `config.fastapiPort` | `"8005"` | `FASTAPI_PORT` | Internal FastAPI sidecar port — not exposed outside the pod | +| `config.betterAuthUrl` | `"http://localhost:5005"` | `BETTER_AUTH_URL` | **Public URL** of the app. Must match the URL users access. Used for OAuth callbacks and trusted-origin checks. | +| `config.nodeEnv` | `"production"` | `NODE_ENV` | Node.js environment | +| `config.contextSource` | `"local"` | `NAO_CONTEXT_SOURCE` | Context loading mode: `local` \| `git` \| `api` | +| `config.contextPath` | `"/app/project"` | `NAO_DEFAULT_PROJECT_PATH` | Absolute path inside the container where the nao project is mounted or cloned | +| `config.contextGitUrl` | `""` | `NAO_CONTEXT_GIT_URL` | Git repository URL (`git` mode) | +| `config.contextGitBranch` | `"main"` | `NAO_CONTEXT_GIT_BRANCH` | Branch to clone (`git` mode) | +| `config.refreshSchedule` | `""` | `NAO_REFRESH_SCHEDULE` | Cron expression for periodic git pulls, e.g. `"0 * * * *"`. Empty = disabled. | +| `config.gitSync.url` | `""` | — | HTTPS URL of the context repo (initContainer git-sync, `local` mode only) | +| `config.gitSync.branch` | `"main"` | — | Branch to sync (initContainer git-sync) | +| `config.gitSync.image` | `"alpine/git:latest"` | — | Container image for the git-sync initContainer | +| `config.posthogKey` | `""` | `POSTHOG_KEY` | PostHog project API key (optional usage analytics) | +| `config.posthogHost` | `"https://eu.i.posthog.com"` | `POSTHOG_HOST` | PostHog ingest endpoint | +| `config.posthogDisabled` | `"false"` | `POSTHOG_DISABLED` | Set to `"true"` to opt out of usage analytics entirely (recommended for on-prem deployments to prevent outbound connections to external analytics) | +| `config.openaiBaseUrl` | `""` | `OPENAI_BASE_URL` | Custom OpenAI API base URL (e.g. for proxy or self-hosted models) | +| `config.anthropicBaseUrl` | `""` | `ANTHROPIC_BASE_URL` | Custom Anthropic API base URL | +| `config.geminiBaseUrl` | `""` | `GEMINI_BASE_URL` | Custom Gemini API base URL | +| `config.mistralBaseUrl` | `""` | `MISTRAL_BASE_URL` | Custom Mistral API base URL | +| `config.openrouterBaseUrl` | `""` | `OPENROUTER_BASE_URL` | Custom OpenRouter API base URL | +| `config.ollamaBaseUrl` | `""` | `OLLAMA_BASE_URL` | Custom Ollama API base URL | + +### Secrets + +All values under `secrets` are stored in a Kubernetes `Secret` object. In production, inject sensitive values via an external secrets manager rather than committing them in values files. + +#### Authentication + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.betterAuthSecret` | `BETTER_AUTH_SECRET` | **Required.** Signs user sessions. Generate with `openssl rand -base64 32`. Changing this value invalidates all active sessions. | + +#### Database + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.dbUri` | `DB_URI` | Database connection URI. When `postgresql.enabled=true` this is built automatically from the subchart values — leave empty. Accepted formats: `postgres://user:pass@host:5432/db` or `sqlite:./db.sqlite` | + +#### LLM providers — provide at least one + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.openaiApiKey` | `OPENAI_API_KEY` | OpenAI API key (`sk-...`) | +| `secrets.anthropicApiKey` | `ANTHROPIC_API_KEY` | Anthropic Claude API key | +| `secrets.azureApiKey` | `AZURE_API_KEY` | Azure OpenAI service key | +| `secrets.azureResourceName` | `AZURE_RESOURCE_NAME` | Azure OpenAI resource name (mutually exclusive with `azureOpenaiBaseUrl`) | +| `secrets.azureOpenaiBaseUrl` | `AZURE_OPENAI_BASE_URL` | Azure OpenAI base URL (mutually exclusive with `azureResourceName`) | +| `secrets.azureApiVersion` | `AZURE_API_VERSION` | Azure OpenAI API version string | +| `secrets.awsBearerTokenBedrock` | `AWS_BEARER_TOKEN_BEDROCK` | AWS Bedrock bearer token | +| `secrets.awsRegion` | `AWS_REGION` | AWS region for Bedrock (`us-east-1`, etc.) | +| `secrets.awsAccessKeyId` | `AWS_ACCESS_KEY_ID` | AWS IAM access key ID | +| `secrets.awsSecretAccessKey` | `AWS_SECRET_ACCESS_KEY` | AWS IAM secret access key | + +#### Context & enterprise + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.contextGitToken` | `NAO_CONTEXT_GIT_TOKEN` | Personal access token or deploy key for cloning a private git repository (`git` mode) | +#### Email — SMTP (optional) + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.smtpHost` | `SMTP_HOST` | SMTP server hostname | +| `secrets.smtpPort` | `SMTP_PORT` | SMTP server port (typically `587` for STARTTLS, `465` for SSL) | +| `secrets.smtpSsl` | `SMTP_SSL` | `"true"` to enable SSL, `"false"` otherwise | +| `secrets.smtpMailFrom` | `SMTP_MAIL_FROM` | Sender address used for outgoing emails | +| `secrets.smtpPassword` | `SMTP_PASSWORD` | SMTP account password | + +#### OAuth providers (optional) + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.googleClientId` | `GOOGLE_CLIENT_ID` | Google OAuth 2.0 client ID | +| `secrets.googleClientSecret` | `GOOGLE_CLIENT_SECRET` | Google OAuth 2.0 client secret | +| `secrets.googleAuthDomains` | `GOOGLE_AUTH_DOMAINS` | Comma-separated list of allowed Google Workspace domains | +| `secrets.githubClientId` | `GITHUB_CLIENT_ID` | GitHub OAuth app client ID | +| `secrets.githubClientSecret` | `GITHUB_CLIENT_SECRET` | GitHub OAuth app client secret | +| `secrets.githubAllowedUsers` | `GITHUB_ALLOWED_USERS` | Comma-separated list of GitHub usernames allowed to sign in | + +#### Integrations (optional) + +| Key | Env var | Description | +|-----|---------|-------------| +| `secrets.notionApiKey` | `NOTION_API_KEY` | Notion integration API key | + +### Service + +| Key | Default | Description | +|-----|---------|-------------| +| `service.type` | `ClusterIP` | Kubernetes Service type: `ClusterIP`, `NodePort`, or `LoadBalancer` | +| `service.port` | `80` | Port exposed on the Service | +| `service.targetPort` | `5005` | Container port the Service routes traffic to | +| `service.annotations` | `{}` | Service annotations (e.g. AWS NLB annotations) | + +### Resources + +| Key | Default | Description | +|-----|---------|-------------| +| `resources.requests.cpu` | `500m` | CPU request | +| `resources.requests.memory` | `512Mi` | Memory request | +| `resources.limits.cpu` | `"2"` | CPU limit | +| `resources.limits.memory` | `2Gi` | Memory limit | + +The FastAPI process is the most CPU and memory intensive (LLM calls, SQL execution). Increase limits for large datasets or heavy concurrent usage. + +### Probes + +The nao backend does not expose a `/health` HTTP endpoint. The chart uses **TCP socket probes** on port 5005. + +| Key | Default | Description | +|-----|---------|-------------| +| `livenessProbe.tcpSocket.port` | `5005` | TCP port checked by the liveness probe | +| `livenessProbe.initialDelaySeconds` | `30` | Seconds to wait before the first liveness check (allow supervisord + processes to start) | +| `livenessProbe.periodSeconds` | `15` | Interval between liveness checks | +| `livenessProbe.failureThreshold` | `3` | Consecutive failures before the pod is restarted | +| `readinessProbe.tcpSocket.port` | `5005` | TCP port checked by the readiness probe | +| `readinessProbe.initialDelaySeconds` | `15` | Seconds to wait before the first readiness check | +| `readinessProbe.periodSeconds` | `10` | Interval between readiness checks | +| `readinessProbe.failureThreshold` | `3` | Consecutive failures before the pod is removed from the Service endpoints | + +### Autoscaling — HPA + +| Key | Default | Description | +|-----|---------|-------------| +| `autoscaling.enabled` | `false` | Create a `HorizontalPodAutoscaler` | +| `autoscaling.minReplicas` | `1` | Minimum number of replicas | +| `autoscaling.maxReplicas` | `5` | Maximum number of replicas | +| `autoscaling.targetCPUUtilizationPercentage` | `80` | Target average CPU utilization (%) across all pods | +| `autoscaling.targetMemoryUtilizationPercentage` | `80` | Target average memory utilization (%) across all pods | + +When `autoscaling.enabled=true`, the `replicaCount` value is ignored. + +### Pod Disruption Budget + +| Key | Default | Description | +|-----|---------|-------------| +| `podDisruptionBudget.enabled` | `false` | Create a `PodDisruptionBudget` | +| `podDisruptionBudget.minAvailable` | `1` | Minimum number of pods that must remain available during voluntary disruptions (node drains, rolling updates) | + +### Node scheduling + +| Key | Default | Description | +|-----|---------|-------------| +| `nodeSelector` | `{}` | Key/value node label selector — restricts pod scheduling to matching nodes | +| `tolerations` | `[]` | List of pod tolerations for tainted nodes | +| `affinity` | `{}` | Affinity / anti-affinity rules (pod or node level) | + +### Persistence — local mode + +Only relevant when `config.contextSource=local`. A `PersistentVolumeClaim` is created and mounted at `config.contextPath` inside the container. + +| Key | Default | Description | +|-----|---------|-------------| +| `persistence.enabled` | `false` | Create and mount a PVC for the nao project context | +| `persistence.storageClass` | `""` | StorageClass name. Empty string uses the cluster default. | +| `persistence.accessMode` | `ReadWriteOnce` | PVC access mode | +| `persistence.size` | `1Gi` | PVC storage size | +| `persistence.existingClaim` | `""` | Name of an existing PVC to use instead of creating a new one | +| `persistence.annotations` | `{}` | Extra annotations on the PVC | + +> You must pre-populate the PVC with a valid nao project (a directory containing `nao_config.yaml`) before the pod starts. The entrypoint will fail if the file is missing. + +### Projects volume — api mode + +Only relevant when `config.contextSource=api`. A `PersistentVolumeClaim` is created and mounted at `/app/projects`. + +| Key | Default | Description | +|-----|---------|-------------| +| `projectsPersistence.enabled` | `false` | Create and mount a PVC for dynamically deployed projects | +| `projectsPersistence.storageClass` | `""` | StorageClass name | +| `projectsPersistence.accessMode` | `ReadWriteOnce` | PVC access mode | +| `projectsPersistence.size` | `5Gi` | PVC storage size | +| `projectsPersistence.existingClaim` | `""` | Name of an existing PVC to reuse | +| `projectsPersistence.annotations` | `{}` | Extra annotations on the PVC | + +### PostgreSQL subchart + +nao uses a PostgreSQL database to store chat history and user sessions. The [bitnami/postgresql](https://github.com/bitnami/charts/tree/main/bitnami/postgresql) subchart is bundled and enabled by default. + +Disable it and set `secrets.dbUri` to connect to an external database instead. + +> **SQLite alternative**: For lightweight or test deployments, set `postgresql.enabled=false` and `secrets.dbUri=sqlite:./db.sqlite`. SQLite requires no additional infrastructure — data is stored inside the pod (ephemeral unless a PVC is configured). + +> **Docker Hub note**: Bitnami PostgreSQL images on Docker Hub are only available for older releases. If your cluster has restricted Docker Hub access or the required image tag is not available, either set `postgresql.image.tag` to a locally mirrored version, or disable the subchart and use an external PostgreSQL instance. + +| Key | Default | Description | +|-----|---------|-------------| +| `postgresql.enabled` | `true` | Deploy PostgreSQL as a subchart alongside nao | +| `postgresql.auth.database` | `nao` | Database name created at init time | +| `postgresql.auth.username` | `nao` | Database user | +| `postgresql.auth.password` | `""` | Password for the nao database user — **change in production** | +| `postgresql.auth.postgresPassword` | `""` | Password for the `postgres` superuser — **change in production** | +| `postgresql.primary.persistence.enabled` | `true` | Persist PostgreSQL data to a PVC | +| `postgresql.primary.persistence.size` | `8Gi` | PostgreSQL PVC size | + +When `postgresql.enabled=true`, the chart automatically constructs `DB_URI` as: + +``` +postgres://:@-nao-postgresql:5432/ +``` + +For the full list of PostgreSQL subchart values, see the [bitnami/postgresql documentation](https://github.com/bitnami/charts/tree/main/bitnami/postgresql#parameters). diff --git a/helm/Chart.lock b/helm/Chart.lock new file mode 100644 index 000000000..f85a61fd9 --- /dev/null +++ b/helm/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 16.7.14 +digest: sha256:3d12513cb378249e854df4b9edc49e63c5ab55842efdcc1b3ca6512b371ac811 +generated: "2026-04-28T16:34:47.673475258+02:00" diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 000000000..0857d77c3 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,29 @@ +apiVersion: v2 +name: nao +description: Helm chart for nao — an open-source analytics agent that transforms natural language into SQL queries. +type: application +version: 0.1.0 +appVersion: "latest" + +keywords: + - analytics + - ai + - sql + - text-to-sql + - agent + +home: https://github.com/mfournioux/nao +sources: + - https://github.com/mfournioux/nao + +maintainers: + - name: Maxime FOURNIOUX + url: https://github.com/mfournioux/nao + +kubeVersion: ">=1.24.0" + +dependencies: + - name: postgresql + version: "16.7.14" + repository: "https://charts.bitnami.com/bitnami" + condition: postgresql.enabled diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 000000000..50183f2c2 --- /dev/null +++ b/helm/README.md @@ -0,0 +1,203 @@ +# nao Helm Chart + +Helm chart for deploying nao — an open-source analytics agent that transforms natural language into SQL queries — on Kubernetes. + +## Prerequisites + +- (Optional) A running PostgreSQL instance, or enable the bundled bitnami/postgresql subchart + +## Chart structure + +``` +helm/ +├── Chart.yaml # Chart metadata & bitnami/postgresql dependency +├── values.yaml # Default configuration values +├── .helmignore +└── templates/ + ├── NOTES.txt # Post-install instructions + ├── _helpers.tpl # Template helpers + ├── configmap.yaml # Non-sensitive environment variables + ├── secret.yaml # Sensitive values (API keys, auth secret, DB URI) + ├── serviceaccount.yaml + ├── deployment.yaml # Main nao workload + ├── service.yaml + ├── pvc.yaml # Conditional (local / api context modes) + ├── hpa.yaml # Conditional + ├── pdb.yaml # Conditional + └── tests/ + └── test-connection.yaml # helm test pod +``` + +## Installation + +### 1. Add the bitnami repository (for the PostgreSQL dependency) + +```bash +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update +``` + +### 2. Install dependencies + +```bash +helm dependency update ./helm +``` + +### 3. Install the chart + +```bash +helm install nao ./helm \ + --namespace nao --create-namespace \ + --set secrets.betterAuthSecret="$(openssl rand -base64 32)" \ + --set secrets.openaiApiKey="" +``` + +## Context modes + +nao supports three ways to load its project configuration, controlled by `config.contextSource`. + +### `local` (default) + +The nao project directory is mounted as a Kubernetes PersistentVolume. + +```yaml +# values-local.yaml +config: + contextSource: local + contextPath: /app/project + +persistence: + enabled: true + size: 1Gi + # existingClaim: my-nao-context-pvc # use an existing PVC +``` + +> **Note:** You must pre-populate the PVC with a valid nao project (a directory containing `nao_config.yaml`). + +### `git` + +The project is cloned from a Git repository at pod startup. + +```yaml +# values-git.yaml +config: + contextSource: git + contextPath: /app/project + contextGitUrl: https://github.com/your-org/your-nao-project.git + contextGitBranch: main + refreshSchedule: "0 * * * *" # optional: pull every hour + +secrets: + contextGitToken: ghp_... # required for private repositories +``` + +### `api` + +Projects are deployed dynamically via the `nao deploy` CLI. A writable volume is created at `/app/projects`. + +```yaml +# values-api.yaml +config: + contextSource: api + +projectsPersistence: + enabled: true + size: 5Gi +``` + +## Values reference + +| Key | Default | Description | +|-----|---------|-------------| +| `replicaCount` | `1` | Number of pod replicas | +| `image.repository` | `ghcr.io/getnao/nao` | Container image repository | +| `image.tag` | `""` (→ appVersion) | Image tag | +| `image.pullPolicy` | `IfNotPresent` | Image pull policy | +| `config.serverPort` | `"5005"` | Fastify backend listening port | +| `config.betterAuthUrl` | `"http://localhost:5005"` | Public URL for auth callbacks | +| `config.contextSource` | `"local"` | Context mode: `local` \| `git` \| `api` | +| `config.contextPath` | `"/app/project"` | Mount path for the nao project | +| `config.contextGitUrl` | `""` | Git repo URL (git mode) | +| `config.contextGitBranch` | `"main"` | Git branch to clone (git mode) | +| `config.refreshSchedule` | `""` | Cron for periodic git pull | +| `secrets.betterAuthSecret` | `""` | **Required.** Auth session secret | +| `secrets.openaiApiKey` | `""` | OpenAI API key | +| `secrets.anthropicApiKey` | `""` | Anthropic API key | +| `secrets.dbUri` | `""` | Database URI (overridden when `postgresql.enabled=true`) | +| `secrets.contextGitToken` | `""` | Git token for private repos | +| `service.type` | `ClusterIP` | Kubernetes service type | +| `service.port` | `80` | Service port | +| `resources.requests.cpu` | `500m` | CPU request | +| `resources.requests.memory` | `512Mi` | Memory request | +| `resources.limits.cpu` | `2` | CPU limit | +| `resources.limits.memory` | `2Gi` | Memory limit | +| `autoscaling.enabled` | `false` | Enable HPA | +| `autoscaling.minReplicas` | `1` | Minimum replicas | +| `autoscaling.maxReplicas` | `5` | Maximum replicas | +| `podDisruptionBudget.enabled` | `false` | Enable PDB | +| `persistence.enabled` | `false` | Enable context PVC (local mode) | +| `persistence.size` | `1Gi` | Context PVC size | +| `persistence.existingClaim` | `""` | Use an existing PVC | +| `projectsPersistence.enabled` | `false` | Enable projects PVC (api mode) | +| `projectsPersistence.size` | `5Gi` | Projects PVC size | +| `postgresql.enabled` | `true` | Deploy bundled PostgreSQL | +| `postgresql.auth.database` | `nao` | PostgreSQL database name | +| `postgresql.auth.username` | `nao` | PostgreSQL username | +| `postgresql.auth.password` | `""` | PostgreSQL password (**change in production**) | + +For the full list of values and their descriptions, see [`values.yaml`](./values.yaml). + +## Using an external database + +Set `postgresql.enabled=false` and provide a connection URI: + +```yaml +postgresql: + enabled: false + +secrets: + dbUri: "postgres://user:password@my-postgres-host:5432/nao" +``` + +SQLite is also supported for single-node / testing deployments: + +```yaml +postgresql: + enabled: false + +secrets: + dbUri: "sqlite:./db.sqlite" +``` + +## Upgrade + +```bash +helm upgrade nao ./helm --namespace nao -f my-values.yaml +``` + +## Rollback + +```bash +# List revision history +helm history nao --namespace nao + +# Roll back to a previous revision +helm rollback nao --namespace nao +``` + +## Running chart tests + +```bash +helm test nao --namespace nao +``` + +## Uninstall + +```bash +helm uninstall nao --namespace nao +``` + +> **Note:** PersistentVolumeClaims are not deleted automatically. Remove them manually if no longer needed: +> ```bash +> kubectl delete pvc -l app.kubernetes.io/instance=nao --namespace nao +> ``` diff --git a/helm/charts/postgresql-16.7.14.tgz b/helm/charts/postgresql-16.7.14.tgz new file mode 100644 index 000000000..a0c0ae9f1 Binary files /dev/null and b/helm/charts/postgresql-16.7.14.tgz differ diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt new file mode 100644 index 000000000..4b79179ff --- /dev/null +++ b/helm/templates/NOTES.txt @@ -0,0 +1,24 @@ +Thank you for installing {{ .Chart.Name }} {{ .Chart.AppVersion }}. + +Release name : {{ .Release.Name }} +Namespace : {{ .Release.Namespace }} + +To access nao locally, run: + + kubectl port-forward --namespace {{ .Release.Namespace }} \ + svc/{{ include "nao.fullname" . }} 5005:{{ .Values.service.port }} + +Then open http://127.0.0.1:5005 in your browser. + +Context mode: {{ .Values.config.contextSource }} +{{- if eq .Values.config.contextSource "local" }} + → Make sure a PVC containing a valid nao project is mounted at {{ .Values.config.contextPath }}. +{{- else if eq .Values.config.contextSource "git" }} + → The project will be cloned from {{ .Values.config.contextGitUrl }} (branch: {{ .Values.config.contextGitBranch }}). +{{- else if eq .Values.config.contextSource "api" }} + → Deploy projects via: nao deploy --url http:// +{{- end }} + +To check the release status: + helm status {{ .Release.Name }} --namespace {{ .Release.Namespace }} + helm get all {{ .Release.Name }} --namespace {{ .Release.Namespace }} diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl new file mode 100644 index 000000000..26746cf87 --- /dev/null +++ b/helm/templates/_helpers.tpl @@ -0,0 +1,109 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nao.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this. +*/}} +{{- define "nao.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nao.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nao.labels" -}} +helm.sh/chart: {{ include "nao.chart" . }} +{{ include "nao.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Common annotations — applied at metadata.annotations on top-level objects +(Deployment, StatefulSet, etc.), as required by org-wide admission policies. +*/}} +{{- define "nao.annotations" -}} +{{- with .Values.commonAnnotations }} +{{- toYaml . }} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nao.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nao.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Service account name +*/}} +{{- define "nao.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "nao.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Full image reference (repository:tag) +*/}} +{{- define "nao.image" -}} +{{- $tag := .Values.image.tag | default .Chart.AppVersion -}} +{{- printf "%s:%s" .Values.image.repository $tag -}} +{{- end }} + +{{/* +PostgreSQL host — uses the bitnami subchart service name when postgresql.enabled=true, +otherwise falls back to a user-supplied host embedded in secrets.dbUri. +*/}} +{{- define "nao.postgresqlHost" -}} +{{- if .Values.postgresql.enabled -}} +{{- printf "%s-postgresql" (include "nao.fullname" .) -}} +{{- end -}} +{{- end }} +{{/* +Build the DB_URI from subchart values when postgresql.enabled=true. +Does not support auth.existingSecret — when using existingSecret, +set secrets.dbUri manually with the full connection string. +*/}} +{{- define "nao.dbUri" -}} +{{- if .Values.postgresql.enabled -}} +{{- if not .Values.postgresql.auth.existingSecret }} +{{- printf "postgres://%s:%s@%s:5432/%s" + .Values.postgresql.auth.username + .Values.postgresql.auth.password + (include "nao.postgresqlHost" .) + .Values.postgresql.auth.database -}} +{{- else }} +{{- .Values.secrets.dbUri }} +{{- end }} +{{- else -}} +{{- .Values.secrets.dbUri -}} +{{- end -}} +{{- end }} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml new file mode 100644 index 000000000..30ea0c8b5 --- /dev/null +++ b/helm/templates/configmap.yaml @@ -0,0 +1,62 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with (include "nao.annotations" .) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +data: + SERVER_PORT: {{ .Values.config.serverPort | quote }} + FASTAPI_PORT: {{ .Values.config.fastapiPort | quote }} + NODE_ENV: {{ .Values.config.nodeEnv | quote }} + MODE: "prod" + DOCKER: "1" + BETTER_AUTH_URL: {{ .Values.config.betterAuthUrl | quote }} + NAO_CONTEXT_SOURCE: {{ .Values.config.contextSource | quote }} + {{- if eq .Values.config.contextSource "api" }} + NAO_PROJECTS_DIR: /app/projects + {{- else }} + NAO_DEFAULT_PROJECT_PATH: {{ .Values.config.contextPath | quote }} + {{- end }} + {{- if eq .Values.config.contextSource "git" }} + NAO_CONTEXT_GIT_URL: {{ .Values.config.contextGitUrl | quote }} + NAO_CONTEXT_GIT_BRANCH: {{ .Values.config.contextGitBranch | quote }} + {{- end }} + {{- if .Values.config.refreshSchedule }} + NAO_REFRESH_SCHEDULE: {{ .Values.config.refreshSchedule | quote }} + {{- end }} + {{- if .Values.config.posthogKey }} + POSTHOG_KEY: {{ .Values.config.posthogKey | quote }} + POSTHOG_HOST: {{ .Values.config.posthogHost | quote }} + {{- end }} + {{- /* + NAO falls back to the editors' own public PostHog key/host when + POSTHOG_KEY is unset (see apps/shared/src/posthog.ts) — so this needs + to be sent unconditionally, not just when posthogKey is provided, + otherwise analytics tracking silently fires toward eu.i.posthog.com + even with no key configured here, which fails on networks without + direct internet egress (FailedToOpenSocket). + */}} + POSTHOG_DISABLED: {{ .Values.config.posthogDisabled | quote }} + {{- if .Values.config.openaiBaseUrl }} + OPENAI_BASE_URL: {{ .Values.config.openaiBaseUrl | quote }} + {{- end }} + {{- if .Values.config.anthropicBaseUrl }} + ANTHROPIC_BASE_URL: {{ .Values.config.anthropicBaseUrl | quote }} + {{- end }} + {{- if .Values.config.geminiBaseUrl }} + GEMINI_BASE_URL: {{ .Values.config.geminiBaseUrl | quote }} + {{- end }} + {{- if .Values.config.mistralBaseUrl }} + MISTRAL_BASE_URL: {{ .Values.config.mistralBaseUrl | quote }} + {{- end }} + {{- if .Values.config.openrouterBaseUrl }} + OPENROUTER_BASE_URL: {{ .Values.config.openrouterBaseUrl | quote }} + {{- end }} + {{- if .Values.config.ollamaBaseUrl }} + OLLAMA_BASE_URL: {{ .Values.config.ollamaBaseUrl | quote }} + {{- end }} \ No newline at end of file diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 000000000..b1c32d0d1 --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,127 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with (include "nao.annotations" .) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "nao.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "nao.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nao.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if and (eq .Values.config.contextSource "local") .Values.persistence.enabled .Values.config.gitSync.url }} + initContainers: + - name: git-sync + image: {{ .Values.config.gitSync.image | quote }} + env: + - name: GIT_BRANCH + value: {{ .Values.config.gitSync.branch | quote }} + - name: GIT_URL + value: {{ .Values.config.gitSync.url | quote }} + {{- if .Values.secrets.contextGitToken }} + - name: GIT_TOKEN + valueFrom: + secretKeyRef: + name: {{ include "nao.fullname" . }} + key: NAO_CONTEXT_GIT_TOKEN + - name: GIT_ASKPASS + value: /bin/true + - name: GIT_TERMINAL_PROMPT + value: '0' + {{- end }} + command: + - sh + - -c + - | + set -eu + rm -rf "{{ .Values.config.contextPath }}"/* + rm -rf "{{ .Values.config.contextPath }}"/.[!.]* "{{ .Values.config.contextPath }}"/..?* + # Configure credential helper so the token is never embedded in .git/config + if [ -n "${GIT_TOKEN:-}" ]; then + git config --global credential.helper '!f() { echo "username=oauth2"; echo "password='"${GIT_TOKEN}"'"; }; f' + fi + # Clone using GIT_ASKPASS so the token is never stored in .git/config + git clone --depth 1 --branch "${GIT_BRANCH}" "${GIT_URL}" {{ .Values.config.contextPath }} + echo "Context repository cloned successfully" + volumeMounts: + - name: context + mountPath: {{ .Values.config.contextPath }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: {{ include "nao.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.config.serverPort | int }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "nao.fullname" . }} + - secretRef: + name: {{ include "nao.fullname" . }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + {{- if and (eq .Values.config.contextSource "local") .Values.persistence.enabled }} + - name: context + mountPath: {{ .Values.config.contextPath }} + {{- end }} + {{- if and (eq .Values.config.contextSource "api") .Values.projectsPersistence.enabled }} + - name: projects + mountPath: /app/projects + {{- end }} + volumes: + {{- if and (eq .Values.config.contextSource "local") .Values.persistence.enabled }} + - name: context + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (printf "%s-context" (include "nao.fullname" .)) }} + {{- end }} + {{- if and (eq .Values.config.contextSource "api") .Values.projectsPersistence.enabled }} + - name: projects + persistentVolumeClaim: + claimName: {{ .Values.projectsPersistence.existingClaim | default (printf "%s-projects" (include "nao.fullname" .)) }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml new file mode 100644 index 000000000..609b61b45 --- /dev/null +++ b/helm/templates/hpa.yaml @@ -0,0 +1,37 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with (include "nao.annotations" .) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "nao.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm/templates/pdb.yaml b/helm/templates/pdb.yaml new file mode 100644 index 000000000..83c3e6ee0 --- /dev/null +++ b/helm/templates/pdb.yaml @@ -0,0 +1,18 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with (include "nao.annotations" .) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + selector: + matchLabels: + {{- include "nao.selectorLabels" . | nindent 6 }} +{{- end }} \ No newline at end of file diff --git a/helm/templates/pvc.yaml b/helm/templates/pvc.yaml new file mode 100644 index 000000000..7d2f4815c --- /dev/null +++ b/helm/templates/pvc.yaml @@ -0,0 +1,45 @@ +{{- if and (eq .Values.config.contextSource "local") .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "nao.fullname" . }}-context + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with .Values.persistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size }} +{{- end }} +{{- if and (eq .Values.config.contextSource "api") .Values.projectsPersistence.enabled (not .Values.projectsPersistence.existingClaim) }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "nao.fullname" . }}-projects + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with .Values.projectsPersistence.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + - {{ .Values.projectsPersistence.accessMode }} + {{- if .Values.projectsPersistence.storageClass }} + storageClassName: {{ .Values.projectsPersistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.projectsPersistence.size }} +{{- end }} diff --git a/helm/templates/secret.yaml b/helm/templates/secret.yaml new file mode 100644 index 000000000..209bd0956 --- /dev/null +++ b/helm/templates/secret.yaml @@ -0,0 +1,72 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- with (include "nao.annotations" .) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +type: Opaque +stringData: + BETTER_AUTH_SECRET: {{ .Values.secrets.betterAuthSecret | quote }} + DB_URI: {{ include "nao.dbUri" . | quote }} + {{- if .Values.secrets.openaiApiKey }} + OPENAI_API_KEY: {{ .Values.secrets.openaiApiKey | quote }} + {{- end }} + {{- if .Values.secrets.anthropicApiKey }} + ANTHROPIC_API_KEY: {{ .Values.secrets.anthropicApiKey | quote }} + {{- end }} + {{- if .Values.secrets.azureApiKey }} + AZURE_API_KEY: {{ .Values.secrets.azureApiKey | quote }} + {{- end }} + {{- if .Values.secrets.azureResourceName }} + AZURE_RESOURCE_NAME: {{ .Values.secrets.azureResourceName | quote }} + {{- end }} + {{- if .Values.secrets.azureOpenaiBaseUrl }} + AZURE_OPENAI_BASE_URL: {{ .Values.secrets.azureOpenaiBaseUrl | quote }} + {{- end }} + {{- if .Values.secrets.azureApiVersion }} + AZURE_API_VERSION: {{ .Values.secrets.azureApiVersion | quote }} + {{- end }} + {{- if .Values.secrets.awsBearerTokenBedrock }} + AWS_BEARER_TOKEN_BEDROCK: {{ .Values.secrets.awsBearerTokenBedrock | quote }} + {{- end }} + {{- if .Values.secrets.awsRegion }} + AWS_REGION: {{ .Values.secrets.awsRegion | quote }} + {{- end }} + {{- if .Values.secrets.awsAccessKeyId }} + AWS_ACCESS_KEY_ID: {{ .Values.secrets.awsAccessKeyId | quote }} + {{- end }} + {{- if .Values.secrets.awsSecretAccessKey }} + AWS_SECRET_ACCESS_KEY: {{ .Values.secrets.awsSecretAccessKey | quote }} + {{- end }} + {{- if .Values.secrets.contextGitToken }} + NAO_CONTEXT_GIT_TOKEN: {{ .Values.secrets.contextGitToken | quote }} + {{- end }} + {{- if .Values.secrets.smtpHost }} + SMTP_HOST: {{ .Values.secrets.smtpHost | quote }} + SMTP_PORT: {{ .Values.secrets.smtpPort | quote }} + SMTP_SSL: {{ .Values.secrets.smtpSsl | quote }} + SMTP_MAIL_FROM: {{ .Values.secrets.smtpMailFrom | quote }} + SMTP_PASSWORD: {{ .Values.secrets.smtpPassword | quote }} + {{- end }} + {{- if .Values.secrets.googleClientId }} + GOOGLE_CLIENT_ID: {{ .Values.secrets.googleClientId | quote }} + GOOGLE_CLIENT_SECRET: {{ .Values.secrets.googleClientSecret | quote }} + {{- end }} + {{- if .Values.secrets.googleAuthDomains }} + GOOGLE_AUTH_DOMAINS: {{ .Values.secrets.googleAuthDomains | quote }} + {{- end }} + {{- if .Values.secrets.githubClientId }} + GITHUB_CLIENT_ID: {{ .Values.secrets.githubClientId | quote }} + GITHUB_CLIENT_SECRET: {{ .Values.secrets.githubClientSecret | quote }} + {{- end }} + {{- if .Values.secrets.githubAllowedUsers }} + GITHUB_ALLOWED_USERS: {{ .Values.secrets.githubAllowedUsers | quote }} + {{- end }} + {{- if .Values.secrets.notionApiKey }} + NOTION_API_KEY: {{ .Values.secrets.notionApiKey | quote }} + {{- end }} \ No newline at end of file diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 000000000..7bc2a08e8 --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nao.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- $ann := dict }} + {{- with include "nao.annotations" . }} + {{- with . }} + {{- $ann = merge $ann (fromYaml .) }} + {{- end }} + {{- end }} + {{- with .Values.service.annotations }} + {{- $ann = merge . $ann }} + {{- end }} + {{- with $ann }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: http + selector: + {{- include "nao.selectorLabels" . | nindent 4 }} diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..289a87de1 --- /dev/null +++ b/helm/templates/serviceaccount.yaml @@ -0,0 +1,22 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "nao.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nao.labels" . | nindent 4 }} + {{- $ann := dict }} + {{- with include "nao.annotations" . }} + {{- with . }} + {{- $ann = merge $ann (fromYaml .) }} + {{- end }} + {{- end }} + {{- with .Values.serviceAccount.annotations }} + {{- $ann = merge . $ann }} + {{- end }} + {{- with $ann }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm/templates/tests/test-connection.yaml b/helm/templates/tests/test-connection.yaml new file mode 100644 index 000000000..baeb3a89e --- /dev/null +++ b/helm/templates/tests/test-connection.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "nao.fullname" . }}-test-connection" + labels: + {{- include "nao.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + containers: + - name: curl + image: curlimages/curl:8.12.1 + command: + - curl + - --fail + - --silent + - --max-time + - "10" + - "http://{{ include "nao.fullname" . }}:{{ .Values.service.port }}" diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 000000000..95d0d060d --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,316 @@ +# ============================================================================= +# nao Helm Chart — values.yaml (SINGLE FILE, ready for helm install) +# Target namespace: nao +# +# METHOD — automatic git-sync via initContainer (HTTPS + Git access token): +# config.gitSync.url points to the nao/example repo. The initContainer +# clones (or pulls if already cloned) into the PVC `context` on every pod +# startup — no manual action required after that. +# +# Prerequisites before `helm install`/`helm upgrade` : +# 1. Create a Git access token (scope: read_repository) for the repo +# (GitHub, GitLab, etc.), or use a dedicated service account. +# 2. Set secrets.contextGitToken with this token (see below). +# 3. Ensure config.gitSync.url is the HTTPS URL of the repo +# (https://..., NOT git@... — no SSH here; the token is injected +# directly into the HTTPS URL by the initContainer). +# +# If the pod restarts/crashes, the initContainer automatically re-clones/pulls +# on the next startup — no manual copy needed. +# ============================================================================= + +# -- Required because postgresql.image points to bitnamilegacy/postgresql (due to +# Bitnami's October 2025 migration, see comment in the postgresql block below). +# The subchart blocks any image outside its expected "official" registry by default — +# this flag lifts that restriction. +# Safe here: same tag/image, just a different Docker Hub repository. +global: + security: + # Required: bitnamilegacy/postgresql is an archived image (see values.yaml:303). + # Set to false and update image.repository to the official postgres image + # when the chart migrates away from the Bitnami postgresql subchart. + allowInsecureImages: true + +# -- Override chart name +nameOverride: "" +# -- Override fully qualified app name +fullnameOverride: "" + +# -- Annotations applied to metadata.annotations of every top-level object +# this chart creates (Deployment, Service, ConfigMap, Secret, PVC, etc.). +# Required by the Kyverno component annotation check. +# IMPORTANT: also set the identical value under postgresql.commonAnnotations +# below — Helm does not propagate top-level values to subcharts automatically. +commonAnnotations: {} + +# -- Number of replicas +replicaCount: 1 + +# -- Container image configuration +image: + repository: "ghcr.io/getnao/nao" # official image from GitHub Container Registry + pullPolicy: IfNotPresent + tag: "" # empty defaults to .Chart.AppVersion + +# -- Image pull secrets (e.g. for private registries) +imagePullSecrets: [] + +# -- Service account configuration +serviceAccount: + create: true + # -- commonAnnotations (above) is already merged automatically here by the chart. + # Leave empty unless you need an annotation specific to the ServiceAccount only. + annotations: {} + name: "" + +# -- Extra annotations added to the Pod template (spec.template.metadata). +# Does NOT cover metadata.annotations of the Deployment itself — see +# commonAnnotations above for that (required by Kyverno). +podAnnotations: {} + +# -- Pod-level security context. +# NOTE: supervisord inside the image starts as root (uid 0) and drops privileges +# to the "nao" user (uid 1000). Leave empty unless you have a custom image. +podSecurityContext: {} + +# -- Container-level security context +securityContext: {} + +# ============================================================================= +# Application configuration +# ============================================================================= +config: + # -- Listening port for the Fastify backend + serverPort: "5005" + # -- Internal FastAPI sidecar port (not exposed externally) + fastapiPort: "8005" + # -- Node environment + nodeEnv: "production" + # -- Public URL of the app — used for auth callbacks and trusted origins. + # Matches the backend default (zod validation fallback). + # Override with the URL users access in their browser. + betterAuthUrl: "http://localhost:5005" + + # contextSource: local => project mounted as a volume at contextPath, + # populated by the gitSync initContainer (see gitSync below). + contextSource: "local" + + # -- Path inside the container where the nao project is located. + # Matches the official `docker run` doc convention + # (-v /path/to/your/nao-project:/app/project -e NAO_DEFAULT_PROJECT_PATH=/app/project). + contextPath: "/app/project" + + # -- Native git mode fields (only used when contextSource=git) + contextGitUrl: "" + contextGitBranch: "main" + refreshSchedule: "" + + # --------------------------------------------------------------------------- + # Git sync — populates the `context` PVC via an initContainer git-clone. + # Set this to the HTTPS URL of your nao context repository. + # --------------------------------------------------------------------------- + # --------------------------------------------------------------------------- + # Git sync — DISABLED by default (use kubectl cp for manual population). + # Empty url => the chart does not generate a git-sync initContainer (see condition + # in deployment.yaml line 35); the PVC `context` is created EMPTY. + # + # To re-enable git-sync later, set: + # url: "https://github.com/your-org/your-nao-project.git" + # --------------------------------------------------------------------------- + # --------------------------------------------------------------------------- + # Git sync — initContainer git-clone, HTTPS + access token. + # No SSH here: the initContainer injects the token directly into + # the HTTPS URL (https://oauth2:@host/...), handled automatically via + # secrets.contextGitToken below. No SSH keys or known_hosts to manage. + # The URL should be the "Clone with HTTPS" URL of the repo (not git@...). + # --------------------------------------------------------------------------- + gitSync: + url: "" + branch: "main" + image: "alpine/git:latest" + + # -- Optional PostHog analytics key + posthogKey: "" + posthogHost: "https://eu.i.posthog.com" + # -- "true" disables PostHog tracking entirely (editor analytics for NAO). + # Required in internal deployments: without a provided key, NAO defaults to + # their own public PostHog key hardcoded in apps/shared/src/posthog.ts, which + # triggers an outbound connection to eu.i.posthog.com — + # blocked on internal networks without direct internet access + # (FailedToOpenSocket error seen in logs). + posthogDisabled: "true" + + # --------------------------------------------------------------------------- + # LLM provider base URLs + # --------------------------------------------------------------------------- + openaiBaseUrl: "" + anthropicBaseUrl: "" + geminiBaseUrl: "" + mistralBaseUrl: "" + openrouterBaseUrl: "" + ollamaBaseUrl: "" + +# ============================================================================= +# Secrets +# ============================================================================= +secrets: + # -- Required. Generate with: openssl rand -base64 32 + betterAuthSecret: "" + + # -- LLM provider API keys + openaiApiKey: "" # Set your OpenAI API key here + anthropicApiKey: "" + + # -- Azure OpenAI (optional) + azureApiKey: "" + azureResourceName: "" + azureOpenaiBaseUrl: "" + azureApiVersion: "" + + # -- AWS Bedrock (optional) + awsBearerTokenBedrock: "" + awsRegion: "" + awsAccessKeyId: "" + awsSecretAccessKey: "" + + # -- Git token for private repositories (used by the initContainer git-sync) + # -- Git access token (minimal scope: read_repository) used by + # the initContainer git-sync to clone the context repo via HTTPS. + # Generate this in your Git provider: Settings > Access Tokens (at the project + # level or a dedicated service account), then paste the value below. + contextGitToken: "" + + + # -- SMTP credentials (optional) + smtpHost: "" + smtpPort: "" + smtpSsl: "false" + smtpMailFrom: "" + smtpPassword: "" + + # -- Google OAuth (optional) + googleClientId: "" + googleClientSecret: "" + googleAuthDomains: "" + + # -- GitHub OAuth (optional) + githubClientId: "" + githubClientSecret: "" + githubAllowedUsers: "" + + # -- Notion integration (optional) + notionApiKey: "" + + # -- Database URI. Leave empty: managed automatically via postgresql.enabled=true. + dbUri: "" + +# ============================================================================= +# Kubernetes Service +# ============================================================================= +service: + type: ClusterIP + port: 80 + targetPort: 5005 + annotations: {} + +# ============================================================================= +# Resources +# ============================================================================= +resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + +# ============================================================================= +# Probes +# ============================================================================= +livenessProbe: + tcpSocket: + port: 5005 + initialDelaySeconds: 30 + periodSeconds: 15 + failureThreshold: 3 + +readinessProbe: + tcpSocket: + port: 5005 + initialDelaySeconds: 15 + periodSeconds: 10 + failureThreshold: 3 + +# ============================================================================= +# Autoscaling (HPA) +# ============================================================================= +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + +# ============================================================================= +# Pod Disruption Budget +# ============================================================================= +podDisruptionBudget: + enabled: false + minAvailable: 1 + +# ============================================================================= +# Node scheduling +# ============================================================================= +nodeSelector: {} +tolerations: [] +affinity: {} + +# ============================================================================= +# Persistence — used when config.contextSource = "local" +# Populated at pod startup by the gitSync initContainer. +# ============================================================================= +persistence: + enabled: false + storageClass: "" + accessMode: ReadWriteOnce + size: 1Gi + existingClaim: "" + annotations: {} + +# ============================================================================= +# Projects volume — used when config.contextSource = "api" +# ============================================================================= +projectsPersistence: + enabled: false + storageClass: "" + accessMode: ReadWriteOnce + size: 5Gi + existingClaim: "" + annotations: {} + +# ============================================================================= +# PostgreSQL subchart (bitnami/postgresql) +# ============================================================================= +postgresql: + enabled: true + # -- IMPORTANT: identical to commonAnnotations at the top of this file — Helm does + # not automatically propagate parent chart values to subcharts. + # This resolves the Kyverno error on the nao-postgresql StatefulSet. + commonAnnotations: {} + # -- Bitnami migrated their versioned images from docker.io/bitnami to + # docker.io/bitnamilegacy (October 2025). Same tag/image, just a + # different repository. This will be replaced with the official + # postgres image or a paid Secure Images subscription in the next + # major chart version. + image: + repository: bitnamilegacy/postgresql + tag: 17.5.0-debian-12-r12 + auth: + database: nao + username: nao + password: "" # Change in production + postgresPassword: "" # Change in production + primary: + persistence: + enabled: true + size: 8Gi \ No newline at end of file