From 5dc03d01aa2460da4e5cc547d84afbba3bd0cc57 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Sat, 21 Mar 2026 02:38:08 -0400 Subject: [PATCH] feat: add Helm chart for Kubernetes deployment (#280) * feat: add Helm chart for Kubernetes deployment Comprehensive Helm chart for deploying Huly to Kubernetes: - All core services (front, account, transactor, collaborator, etc.) - Infrastructure (CockroachDB, Redpanda, Elasticsearch, MinIO) - Optional AI bot with MongoDB - Automated backup CronJobs to S3-compatible storage - Configurable image registry and version pinning - Auto-generated secrets with persistence across upgrades - NGINX ingress with TLS via cert-manager - CI workflow for lint, template validation, and OCI publish Chart lives at helm/ (outside kube/) to avoid conflict with the existing kubectl-based CI that recursively applies kube/*.yaml. Signed-off-by: Daniel Kendall Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Don Kendall * feat(helm): add optional GitHub integration service Deploy pod-github for bidirectional sync of issues, PRs, and comments between Huly and GitHub. Gated behind githubIntegration.enabled (false by default). Adds: - templates/github/deployment.yaml + service.yaml - GITHUB_URL to configmap and front deployment - GitHub App credentials to shared secret - /_github ingress backend - README documentation for GitHub integration and AI bot Signed-off-by: Daniel Kendall Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Don Kendall --------- Signed-off-by: Don Kendall Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/helm-chart.yml | 52 +++ helm/huly/.gitignore | 1 + helm/huly/Chart.yaml | 13 + helm/huly/README.md | 393 ++++++++++++++++++ helm/huly/templates/NOTES.txt | 19 + helm/huly/templates/_helpers.tpl | 156 +++++++ helm/huly/templates/account/deployment.yaml | 75 ++++ helm/huly/templates/account/service.yaml | 14 + helm/huly/templates/aibot/deployment.yaml | 82 ++++ helm/huly/templates/aibot/service.yaml | 16 + .../templates/backup/cronjob-cockroachdb.yaml | 122 ++++++ helm/huly/templates/backup/cronjob-files.yaml | 107 +++++ .../templates/backup/cronjob-mongodb.yaml | 84 ++++ helm/huly/templates/backup/secret.yaml | 43 ++ helm/huly/templates/cockroach/deployment.yaml | 62 +++ helm/huly/templates/cockroach/pvc.yaml | 17 + helm/huly/templates/cockroach/service.yaml | 20 + .../templates/collaborator/deployment.yaml | 48 +++ helm/huly/templates/collaborator/service.yaml | 14 + helm/huly/templates/configmap.yaml | 60 +++ helm/huly/templates/elastic/deployment.yaml | 82 ++++ helm/huly/templates/elastic/pvc.yaml | 17 + helm/huly/templates/elastic/service.yaml | 16 + helm/huly/templates/front/deployment.yaml | 73 ++++ helm/huly/templates/front/service.yaml | 14 + helm/huly/templates/fulltext/deployment.yaml | 54 +++ helm/huly/templates/fulltext/service.yaml | 14 + helm/huly/templates/github/deployment.yaml | 61 +++ helm/huly/templates/github/service.yaml | 16 + helm/huly/templates/ingress.yaml | 98 +++++ helm/huly/templates/kvs/deployment.yaml | 49 +++ helm/huly/templates/kvs/service.yaml | 16 + helm/huly/templates/minio/deployment.yaml | 61 +++ helm/huly/templates/minio/pvc.yaml | 17 + helm/huly/templates/minio/service.yaml | 19 + helm/huly/templates/mongodb/deployment.yaml | 50 +++ helm/huly/templates/mongodb/pvc.yaml | 17 + helm/huly/templates/mongodb/service.yaml | 17 + helm/huly/templates/redpanda/deployment.yaml | 87 ++++ helm/huly/templates/redpanda/pvc.yaml | 17 + helm/huly/templates/redpanda/service.yaml | 19 + helm/huly/templates/rekoni/deployment.yaml | 43 ++ helm/huly/templates/rekoni/service.yaml | 14 + helm/huly/templates/secret.yaml | 124 ++++++ helm/huly/templates/stats/deployment.yaml | 45 ++ helm/huly/templates/stats/service.yaml | 14 + .../huly/templates/transactor/deployment.yaml | 61 +++ helm/huly/templates/transactor/service.yaml | 14 + helm/huly/templates/workspace/deployment.yaml | 41 ++ helm/huly/values.yaml | 292 +++++++++++++ 50 files changed, 2860 insertions(+) create mode 100644 .github/workflows/helm-chart.yml create mode 100644 helm/huly/.gitignore create mode 100644 helm/huly/Chart.yaml create mode 100644 helm/huly/README.md create mode 100644 helm/huly/templates/NOTES.txt create mode 100644 helm/huly/templates/_helpers.tpl create mode 100644 helm/huly/templates/account/deployment.yaml create mode 100644 helm/huly/templates/account/service.yaml create mode 100644 helm/huly/templates/aibot/deployment.yaml create mode 100644 helm/huly/templates/aibot/service.yaml create mode 100644 helm/huly/templates/backup/cronjob-cockroachdb.yaml create mode 100644 helm/huly/templates/backup/cronjob-files.yaml create mode 100644 helm/huly/templates/backup/cronjob-mongodb.yaml create mode 100644 helm/huly/templates/backup/secret.yaml create mode 100644 helm/huly/templates/cockroach/deployment.yaml create mode 100644 helm/huly/templates/cockroach/pvc.yaml create mode 100644 helm/huly/templates/cockroach/service.yaml create mode 100644 helm/huly/templates/collaborator/deployment.yaml create mode 100644 helm/huly/templates/collaborator/service.yaml create mode 100644 helm/huly/templates/configmap.yaml create mode 100644 helm/huly/templates/elastic/deployment.yaml create mode 100644 helm/huly/templates/elastic/pvc.yaml create mode 100644 helm/huly/templates/elastic/service.yaml create mode 100644 helm/huly/templates/front/deployment.yaml create mode 100644 helm/huly/templates/front/service.yaml create mode 100644 helm/huly/templates/fulltext/deployment.yaml create mode 100644 helm/huly/templates/fulltext/service.yaml create mode 100644 helm/huly/templates/github/deployment.yaml create mode 100644 helm/huly/templates/github/service.yaml create mode 100644 helm/huly/templates/ingress.yaml create mode 100644 helm/huly/templates/kvs/deployment.yaml create mode 100644 helm/huly/templates/kvs/service.yaml create mode 100644 helm/huly/templates/minio/deployment.yaml create mode 100644 helm/huly/templates/minio/pvc.yaml create mode 100644 helm/huly/templates/minio/service.yaml create mode 100644 helm/huly/templates/mongodb/deployment.yaml create mode 100644 helm/huly/templates/mongodb/pvc.yaml create mode 100644 helm/huly/templates/mongodb/service.yaml create mode 100644 helm/huly/templates/redpanda/deployment.yaml create mode 100644 helm/huly/templates/redpanda/pvc.yaml create mode 100644 helm/huly/templates/redpanda/service.yaml create mode 100644 helm/huly/templates/rekoni/deployment.yaml create mode 100644 helm/huly/templates/rekoni/service.yaml create mode 100644 helm/huly/templates/secret.yaml create mode 100644 helm/huly/templates/stats/deployment.yaml create mode 100644 helm/huly/templates/stats/service.yaml create mode 100644 helm/huly/templates/transactor/deployment.yaml create mode 100644 helm/huly/templates/transactor/service.yaml create mode 100644 helm/huly/templates/workspace/deployment.yaml create mode 100644 helm/huly/values.yaml diff --git a/.github/workflows/helm-chart.yml b/.github/workflows/helm-chart.yml new file mode 100644 index 0000000..d7207f2 --- /dev/null +++ b/.github/workflows/helm-chart.yml @@ -0,0 +1,52 @@ +name: Helm Chart + +on: + push: + branches: [main] + paths: + - "helm/**" + pull_request: + paths: + - "helm/**" + +permissions: + contents: read + packages: write + +env: + CHART_PATH: helm/huly + OCI_REGISTRY: oci://ghcr.io/${{ github.repository_owner }}/charts + +jobs: + lint: + name: Lint & Template + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: azure/setup-helm@v4 + - run: helm lint ${{ env.CHART_PATH }} + - run: helm template huly ${{ env.CHART_PATH }} --set domain=ci.example.com + + publish: + name: Package & Push OCI + needs: lint + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: azure/setup-helm@v4 + + - name: Extract chart version + id: chart + run: | + version=$(grep '^version:' ${{ env.CHART_PATH }}/Chart.yaml | awk '{print $2}') + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Login to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Package chart + run: helm package ${{ env.CHART_PATH }} -d /tmp/charts + + - name: Push to OCI registry + run: helm push /tmp/charts/huly-${{ steps.chart.outputs.version }}.tgz ${{ env.OCI_REGISTRY }} diff --git a/helm/huly/.gitignore b/helm/huly/.gitignore new file mode 100644 index 0000000..21c65e1 --- /dev/null +++ b/helm/huly/.gitignore @@ -0,0 +1 @@ +values-ledoweb.yaml diff --git a/helm/huly/Chart.yaml b/helm/huly/Chart.yaml new file mode 100644 index 0000000..de1c0e3 --- /dev/null +++ b/helm/huly/Chart.yaml @@ -0,0 +1,13 @@ +apiVersion: v2 +name: huly +description: Huly — open-source project management platform +type: application +version: 0.1.0 +appVersion: "0.7.382" +home: https://huly.io +sources: + - https://github.com/hcengineering/huly-selfhost +keywords: + - huly + - project-management + - collaboration diff --git a/helm/huly/README.md b/helm/huly/README.md new file mode 100644 index 0000000..140e440 --- /dev/null +++ b/helm/huly/README.md @@ -0,0 +1,393 @@ +# Huly Helm Chart + +Deploy [Huly](https://huly.io) — an open-source project management platform — on Kubernetes with a single command. + +## Prerequisites + +- Kubernetes 1.25+ +- Helm 3.10+ +- [NGINX Ingress Controller](https://kubernetes.github.io/ingress-nginx/) +- [cert-manager](https://cert-manager.io/) (if using TLS) + +## Quick Start + +```bash +helm install huly ./helm/huly \ + --set domain=huly.mysite.com +``` + +All secrets (server secret, CockroachDB password, MinIO credentials) are auto-generated on first install and preserved across `helm upgrade`. + +## Authentication + +At least one auth provider should be configured. Without one, users cannot sign in. + +### Google OAuth + +```bash +helm install huly ./helm/huly \ + --set domain=huly.mysite.com \ + --set auth.google.clientId=YOUR_CLIENT_ID \ + --set auth.google.clientSecret=YOUR_CLIENT_SECRET +``` + +### GitHub OAuth + +```bash +helm install huly ./helm/huly \ + --set domain=huly.mysite.com \ + --set auth.github.clientId=YOUR_CLIENT_ID \ + --set auth.github.clientSecret=YOUR_CLIENT_SECRET +``` + +### OpenID Connect (OIDC) + +```bash +helm install huly ./helm/huly \ + --set domain=huly.mysite.com \ + --set auth.oidc.clientId=YOUR_CLIENT_ID \ + --set auth.oidc.clientSecret=YOUR_CLIENT_SECRET \ + --set auth.oidc.issuer=https://accounts.google.com +``` + +### Disable Public Signup + +```bash +--set auth.disableSignup=true +``` + +## External S3 Storage + +By default, the chart deploys a built-in MinIO instance. To use external S3-compatible storage instead: + +```bash +helm install huly ./helm/huly \ + --set domain=huly.mysite.com \ + --set storage.type=s3 \ + --set storage.s3.endpoint=https://s3.amazonaws.com \ + --set storage.s3.region=us-east-1 \ + --set storage.s3.accessKey=YOUR_ACCESS_KEY \ + --set storage.s3.secretKey=YOUR_SECRET_KEY \ + --set storage.s3.rootBucket=huly-data +``` + +Setting `storage.type=s3` automatically disables the built-in MinIO deployment and PVC. + +**Bucket modes:** +- `rootBucket` — all workspaces share one bucket, isolated by workspace-ID prefix (recommended) +- `bucketPrefix` — each workspace gets its own bucket, prefixed with this string + +## External Infrastructure + +Each built-in infra service (CockroachDB, Redpanda, Elasticsearch) can be replaced with an external instance. + +### External CockroachDB / PostgreSQL + +```bash +helm install huly ./helm/huly \ + --set domain=huly.mysite.com \ + --set cockroach.enabled=false \ + --set secrets.crDbUrl='postgres://user:pass@db.example.com:26257/huly' +``` + +### External Redpanda / Kafka + +```bash +--set redpanda.enabled=false \ +--set external.redpanda=kafka.example.com:9092 +``` + +### External Elasticsearch + +```bash +--set elastic.enabled=false \ +--set external.elastic=https://es.example.com:9200 +``` + +## GitHub Integration + +Bidirectional sync of issues, PRs, and comments between Huly and GitHub. Requires a [GitHub App](https://docs.github.com/en/apps/creating-github-apps). + +```bash +helm install huly ./helm/huly \ + --set domain=huly.mysite.com \ + --set githubIntegration.enabled=true \ + --set githubIntegration.appId=123456 \ + --set githubIntegration.clientId=Iv1.abc123 \ + --set githubIntegration.clientSecret=YOUR_SECRET \ + --set-file githubIntegration.privateKey=path/to/private-key.pem \ + --set githubIntegration.webhookSecret=YOUR_WEBHOOK_SECRET \ + --set githubIntegration.botName="your-app-name[bot]" +``` + +**GitHub App settings:** + +| Field | Value | +|-------|-------| +| Callback URL | `https:///github` | +| Setup URL | `https:///github?op=installation` | +| Webhook URL | `https:///_github/api/webhook` | +| Permissions | Issues R/W, PRs R/W, Contents R, Metadata R | +| Events | Issues, Issue comment, Pull request, PR review, PR review comment, PR review thread | + +## AI Bot + +Optional AI assistant powered by OpenAI. Requires an OpenAI API key. + +```bash +helm install huly ./helm/huly \ + --set domain=huly.mysite.com \ + --set aibot.enabled=true \ + --set secrets.openaiApiKey=sk-... +``` + +## Admin Configuration + +```bash +--set appSettings.adminEmails="admin@example.com,ops@example.com" +``` + +## Upgrading + +### Version bumps + +Update `hulyVersion` to the desired release tag: + +```bash +helm upgrade huly ./helm/huly \ + --reuse-values \ + --set hulyVersion=v0.7.400 +``` + +All app service pods restart automatically (via checksum annotations) when the chart version or config changes. Infrastructure services (CockroachDB, Redpanda, Elasticsearch) are **not** restarted on config changes to avoid data-layer disruption. + +### From MinIO to S3 + +When switching storage backends, set the new storage type explicitly — it overrides any previously persisted config: + +```bash +helm upgrade huly ./helm/huly \ + --reuse-values \ + --set storage.type=s3 \ + --set storage.s3.endpoint=https://s3.example.com \ + --set storage.s3.region=us-east-1 \ + --set storage.s3.accessKey=KEY \ + --set storage.s3.secretKey=SECRET \ + --set storage.s3.rootBucket=huly-data +``` + +After confirming S3 works, clean up the orphaned MinIO PVC: + +```bash +kubectl delete pvc minio-data -n +``` + +## Troubleshooting + +### Services fail to start with connection errors + +App services (account, transactor, workspace, fulltext) depend on CockroachDB and Redpanda. The chart includes init containers that wait for these services to accept connections before starting. If you see `ECONNREFUSED` or `ENOTFOUND` errors in logs, restart the affected deployments: + +```bash +kubectl rollout restart deployment/account deployment/transactor \ + deployment/workspace deployment/fulltext -n +``` + +### CockroachDB user/password issues + +When running CockroachDB with `--insecure` (the default), authentication is disabled. The `COCKROACH_PASSWORD` env var creates the user during init but is not enforced for connections. If using the `root` user, set: + +```bash +--set cockroach.username=root \ +--set secrets.crDbUrl='postgres://root@cockroach:26257/defaultdb?sslmode=disable' +``` + +### Checking pod health + +```bash +kubectl get pods -l app.kubernetes.io/part-of=huly -n +kubectl logs deployment/ -n --tail=20 +``` + +## Values Reference + +### Required + +| Key | Description | Default | +|-----|-------------|---------| +| `domain` | Your Huly domain (e.g. `huly.mysite.com`) | `huly.example` | + +### Authentication + +| Key | Description | Default | +|-----|-------------|---------| +| `auth.google.clientId` | Google OAuth client ID | `""` | +| `auth.google.clientSecret` | Google OAuth client secret | `""` | +| `auth.github.clientId` | GitHub OAuth client ID | `""` | +| `auth.github.clientSecret` | GitHub OAuth client secret | `""` | +| `auth.oidc.clientId` | OIDC client ID | `""` | +| `auth.oidc.clientSecret` | OIDC client secret | `""` | +| `auth.oidc.issuer` | OIDC issuer URL | `""` | +| `auth.disableSignup` | Prevent new user registration | `false` | + +### Storage + +| Key | Description | Default | +|-----|-------------|---------| +| `storage.type` | `minio` (built-in) or `s3` (external) | `minio` | +| `storage.s3.endpoint` | S3 endpoint URL | `""` | +| `storage.s3.region` | S3 region | `""` | +| `storage.s3.accessKey` | S3 access key | `""` | +| `storage.s3.secretKey` | S3 secret key | `""` | +| `storage.s3.rootBucket` | Single bucket for all workspaces (prefixed by workspace ID) | `""` | +| `storage.s3.bucketPrefix` | Prefix for per-workspace bucket names (if rootBucket empty) | `""` | + +### Secrets + +All secrets are auto-generated if left empty. They persist across `helm upgrade` via Kubernetes secret lookup. + +| Key | Description | Default | +|-----|-------------|---------| +| `secrets.serverSecret` | Shared JWT signing secret | auto | +| `secrets.storageConfig` | Full storage connection string override | auto | +| `secrets.cockroachPassword` | CockroachDB password | auto | +| `secrets.redpandaPassword` | Redpanda superuser password | auto | +| `secrets.crDbUrl` | CockroachDB connection URL | auto | + +### Ingress + +| Key | Description | Default | +|-----|-------------|---------| +| `ingress.enabled` | Enable ingress resources | `true` | +| `ingress.className` | Ingress class | `nginx` | +| `ingress.annotations` | Extra annotations for all ingress resources | `{}` | +| `ingress.tls.enabled` | Enable TLS via cert-manager | `true` | +| `ingress.tls.clusterIssuer` | cert-manager ClusterIssuer name | `letsencrypt-prod` | + +### App Settings + +| Key | Description | Default | +|-----|-------------|---------| +| `appSettings.title` | Browser title | `Huly Self Host` | +| `appSettings.defaultLanguage` | Default UI language | `en` | +| `appSettings.lastNameFirst` | Display last name first | `true` | +| `appSettings.modelEnabled` | Enabled platform models | `*` | +| `appSettings.adminEmails` | Comma-separated admin emails | `""` | +| `appSettings.desktopChannel` | Desktop update channel | `selfhost` | + +### Infrastructure + +Each infra service can be disabled to use an external instance. When disabled, provide connection details via the corresponding external/secret keys. + +| Key | Description | Default | +|-----|-------------|---------| +| `cockroach.enabled` | Deploy built-in CockroachDB | `true` | +| `cockroach.image` | CockroachDB image | `cockroachdb/cockroach:latest-v24.2` | +| `cockroach.storage` | Data PVC size | `10Gi` | +| `cockroach.storageClassName` | PVC storage class (empty = cluster default) | `""` | +| `cockroach.database` | Database name | `defaultdb` | +| `cockroach.username` | Database user | `selfhost` | +| `redpanda.enabled` | Deploy built-in Redpanda | `true` | +| `redpanda.image` | Redpanda image | `docker.redpanda.com/...` | +| `redpanda.storage` | Data PVC size | `5Gi` | +| `redpanda.storageClassName` | PVC storage class | `""` | +| `elastic.enabled` | Deploy built-in Elasticsearch | `true` | +| `elastic.image` | Elasticsearch image | `elasticsearch:7.14.2` | +| `elastic.storage` | Data PVC size | `10Gi` | +| `elastic.storageClassName` | PVC storage class | `""` | +| `elastic.javaOpts` | JVM heap options | `-Xms1024m -Xmx1024m` | +| `minio.enabled` | Deploy built-in MinIO | `true` | +| `minio.image` | MinIO image | `minio/minio` | +| `minio.storage` | Data PVC size | `50Gi` | +| `minio.storageClassName` | PVC storage class | `""` | + +### External Infrastructure + +| Key | Description | Default | +|-----|-------------|---------| +| `external.redpanda` | Kafka-compatible broker address (when `redpanda.enabled=false`) | `""` | +| `external.elastic` | Elasticsearch URL (when `elastic.enabled=false`) | `""` | + +### Application Services + +All app services share these overridable keys: `.replicas`, `.resources`. + +| Key | Description | Default | +|-----|-------------|---------| +| `hulyVersion` | Image tag for all Huly services | `v0.7.382` | +| `kvs.enabled` | Deploy KVS (key-value store) service | `true` | + +### GitHub Integration + +| Key | Description | Default | +|-----|-------------|---------| +| `githubIntegration.enabled` | Deploy GitHub integration service | `false` | +| `githubIntegration.replicas` | Replica count | `1` | +| `githubIntegration.botName` | Bot display name (must match GitHub App slug + `[bot]`) | `""` | +| `githubIntegration.appId` | GitHub App ID | `""` | +| `githubIntegration.clientId` | GitHub App Client ID | `""` | +| `githubIntegration.clientSecret` | GitHub App Client Secret | `""` | +| `githubIntegration.privateKey` | GitHub App Private Key (PEM) | `""` | +| `githubIntegration.webhookSecret` | GitHub App Webhook Secret | `""` | + +### AI Bot + +| Key | Description | Default | +|-----|-------------|---------| +| `aibot.enabled` | Deploy AI bot service | `false` | +| `aibot.replicas` | Replica count | `1` | +| `aibot.firstName` | Bot display first name | `Huly` | +| `aibot.lastName` | Bot display last name | `AI` | +| `secrets.openaiApiKey` | OpenAI API key (required when enabled) | `""` | +| `secrets.openaiBaseUrl` | OpenAI API base URL override | `""` | + +### Global Pod Settings + +| Key | Description | Default | +|-----|-------------|---------| +| `global.nodeSelector` | Node selector for all pods | `{}` | +| `global.tolerations` | Tolerations for all pods | `[]` | +| `global.affinity` | Affinity rules for all pods | `{}` | + +## Architecture + +The chart deploys 13+ services (optional services marked with *): + +| Service | Port | Description | +|---------|------|-------------| +| **cockroach** | 26257 | SQL database (CockroachDB) | +| **redpanda** | 9092 | Message queue (Kafka-compatible) | +| **elastic** | 9200 | Full-text search (Elasticsearch) | +| **minio** | 9000 | Object storage (S3-compatible) | +| **front** | 8080 | Web UI | +| **account** | 3000 | Authentication & user management | +| **transactor** | 3333 | Core transaction engine | +| **collaborator** | 3078 | Real-time collaboration (WebSocket) | +| **workspace** | — | Workspace lifecycle (background worker) | +| **fulltext** | 4700 | Search indexing | +| **rekoni** | 4004 | Content intelligence | +| **stats** | 4900 | Metrics collection | +| **kvs** | 8094 | Key-value store | +| **github*** | 3500 | GitHub integration (bidirectional sync) | +| **aibot*** | 4010 | AI assistant (requires OpenAI key) | +| **mongodb*** | 27017 | Document database (for aibot) | + +All services are exposed under a single domain via path-based NGINX ingress routing: +- `/` → front +- `/_accounts` → account +- `/_transactor` → transactor (WebSocket) +- `/_collaborator` → collaborator (WebSocket) +- `/_rekoni` → rekoni +- `/_stats` → stats +- `/_github` → github * +- `/_aibot` → aibot * + +## CI / OCI Registry + +On merge to `main`, the GitHub Actions workflow packages and pushes the chart to GHCR: + +```bash +helm install huly oci://ghcr.io/hcengineering/charts/huly \ + --version 0.1.0 \ + --set domain=huly.mysite.com +``` diff --git a/helm/huly/templates/NOTES.txt b/helm/huly/templates/NOTES.txt new file mode 100644 index 0000000..4cff801 --- /dev/null +++ b/helm/huly/templates/NOTES.txt @@ -0,0 +1,19 @@ +Huly has been installed! + +{{- $proto := ternary "https" "http" (and .Values.ingress.enabled .Values.ingress.tls.enabled) }} + +Your Huly instance will be available at: + + {{ $proto }}://{{ .Values.domain }} + +{{- if .Values.ingress.enabled }} + +Ingress is enabled with class "{{ .Values.ingress.className }}". +{{- if .Values.ingress.tls.enabled }} +TLS is enabled via cluster-issuer "{{ .Values.ingress.tls.clusterIssuer }}". +{{- end }} +{{- end }} + +To check the rollout status: + + kubectl get pods -l app.kubernetes.io/part-of=huly -n {{ .Release.Namespace }} diff --git a/helm/huly/templates/_helpers.tpl b/helm/huly/templates/_helpers.tpl new file mode 100644 index 0000000..ec20b0b --- /dev/null +++ b/helm/huly/templates/_helpers.tpl @@ -0,0 +1,156 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "huly.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "huly.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 }} + +{{/* +Secret resource name. +*/}} +{{- define "huly.secretName" -}} +{{- printf "%s-secret" (include "huly.fullname" .) }} +{{- end }} + +{{/* +ConfigMap resource name. +*/}} +{{- define "huly.configName" -}} +{{- printf "%s-config" (include "huly.fullname" .) }} +{{- end }} + +{{/* +Common labels applied to every resource. +*/}} +{{- define "huly.labels" -}} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: huly +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} + +{{/* +Global scheduling (nodeSelector, tolerations, affinity). +*/}} +{{- define "huly.scheduling" -}} +{{- with .Values.global.nodeSelector }} +nodeSelector: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- with .Values.global.tolerations }} +tolerations: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- with .Values.global.affinity }} +affinity: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- end }} + +{{/* +Effective MinIO enabled (disabled if storage.type == s3). +*/}} +{{- define "huly.minioEnabled" -}} +{{- if and .Values.minio.enabled (eq .Values.storage.type "minio") }}true{{- else }}false{{- end }} +{{- end }} + +{{/* +Checksum annotations — triggers pod restart when secret/configmap changes. +Usage: {{- include "huly.checksumAnnotations" . | nindent 8 }} +*/}} +{{- define "huly.checksumAnnotations" -}} +checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} +checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} +{{- end }} + +{{/* +Init container that waits for CockroachDB to accept connections. +*/}} +{{- define "huly.waitForCockroach" -}} +{{- if .Values.cockroach.enabled }} +- name: wait-cockroach + image: busybox:1.36 + command: ['sh', '-c', 'until nc -z cockroach 26257; do echo "waiting for cockroach..."; sleep 2; done'] +{{- end }} +{{- end }} + +{{/* +Init container that waits for MongoDB to accept connections. +*/}} +{{- define "huly.waitForMongodb" -}} +{{- if .Values.mongodb.enabled }} +- name: wait-mongodb + image: busybox:1.36 + command: ['sh', '-c', 'until nc -z mongodb 27017; do echo "waiting for mongodb..."; sleep 2; done'] +{{- end }} +{{- end }} + +{{/* +Init container that waits for Redpanda to accept connections. +*/}} +{{- define "huly.waitForRedpanda" -}} +{{- if .Values.redpanda.enabled }} +- name: wait-redpanda + image: busybox:1.36 + command: ['sh', '-c', 'until nc -z redpanda 9092; do echo "waiting for redpanda..."; sleep 2; done'] +{{- end }} +{{- end }} + +{{/* +Backup secret resource name. +*/}} +{{- define "huly.backupSecretName" -}} +{{- printf "%s-backup-secret" (include "huly.fullname" .) }} +{{- end }} + +{{/* +Env var from backup Secret helper. +Usage: {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_ENDPOINT" "key" "BACKUP_S3_ENDPOINT" "root" .) }} +*/}} +{{- define "huly.envBackupSecret" -}} +- name: {{ .name }} + valueFrom: + secretKeyRef: + name: {{ include "huly.backupSecretName" .root }} + key: {{ .key }} +{{- end }} + +{{/* +Env var from Secret helper — reduces boilerplate. +Usage: {{- include "huly.envSecret" (dict "name" "SERVER_SECRET" "key" "SERVER_SECRET" "root" .) }} +*/}} +{{- define "huly.envSecret" -}} +- name: {{ .name }} + valueFrom: + secretKeyRef: + name: {{ include "huly.secretName" .root }} + key: {{ .key }} +{{- end }} + +{{/* +Env var from ConfigMap helper — reduces boilerplate. +Usage: {{- include "huly.envConfig" (dict "name" "FRONT_URL" "key" "FRONT_URL" "root" .) }} +*/}} +{{- define "huly.envConfig" -}} +- name: {{ .name }} + valueFrom: + configMapKeyRef: + name: {{ include "huly.configName" .root }} + key: {{ .key }} +{{- end }} diff --git a/helm/huly/templates/account/deployment.yaml b/helm/huly/templates/account/deployment.yaml new file mode 100644 index 0000000..8805967 --- /dev/null +++ b/helm/huly/templates/account/deployment.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: account + labels: + {{- include "huly.labels" . | nindent 4 }} + app: account +spec: + replicas: {{ .Values.account.replicas }} + selector: + matchLabels: + app: account + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: account + spec: + {{- include "huly.scheduling" . | nindent 6 }} + initContainers: + {{- include "huly.waitForCockroach" . | nindent 8 }} + {{- include "huly.waitForRedpanda" . | nindent 8 }} + containers: + - name: account + image: {{ .Values.hulyRegistry }}/account:{{ .Values.hulyVersion }} + ports: + - name: http + containerPort: 3000 + env: + - name: ACCOUNT_PORT + value: "3000" + - name: SERVER_PORT + value: "3000" + {{- include "huly.envSecret" (dict "name" "SERVER_SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "DB_URL" "key" "CR_DB_URL" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "STORAGE_CONFIG" "key" "STORAGE_CONFIG" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "ACCOUNTS_URL" "key" "ACCOUNTS_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "FRONT_URL" "key" "FRONT_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "STATS_URL" "key" "STATS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "MODEL_ENABLED" "key" "MODEL_ENABLED" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "TRANSACTOR_URL" "key" "TRANSACTOR_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "QUEUE_CONFIG" "key" "QUEUE_CONFIG" "root" .) | nindent 12 }} + {{- if .Values.auth.disableSignup }} + - name: DISABLE_SIGNUP + value: "true" + {{- end }} + {{- if .Values.auth.google.clientId }} + {{- include "huly.envSecret" (dict "name" "GOOGLE_CLIENT_ID" "key" "GOOGLE_CLIENT_ID" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "GOOGLE_CLIENT_SECRET" "key" "GOOGLE_CLIENT_SECRET" "root" .) | nindent 12 }} + {{- end }} + {{- if .Values.auth.github.clientId }} + {{- include "huly.envSecret" (dict "name" "GITHUB_CLIENT_ID" "key" "GITHUB_CLIENT_ID" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "GITHUB_CLIENT_SECRET" "key" "GITHUB_CLIENT_SECRET" "root" .) | nindent 12 }} + {{- end }} + {{- if .Values.auth.oidc.clientId }} + {{- include "huly.envSecret" (dict "name" "OPENID_CLIENT_ID" "key" "OPENID_CLIENT_ID" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "OPENID_CLIENT_SECRET" "key" "OPENID_CLIENT_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "OPENID_ISSUER" "key" "OPENID_ISSUER" "root" .) | nindent 12 }} + {{- end }} + readinessProbe: + tcpSocket: + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 30 + {{- with .Values.account.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/helm/huly/templates/account/service.yaml b/helm/huly/templates/account/service.yaml new file mode 100644 index 0000000..752e20c --- /dev/null +++ b/helm/huly/templates/account/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: account + labels: + {{- include "huly.labels" . | nindent 4 }} + app: account +spec: + selector: + app: account + ports: + - name: http + port: 3000 + targetPort: 3000 diff --git a/helm/huly/templates/aibot/deployment.yaml b/helm/huly/templates/aibot/deployment.yaml new file mode 100644 index 0000000..ae746ac --- /dev/null +++ b/helm/huly/templates/aibot/deployment.yaml @@ -0,0 +1,82 @@ +{{- if .Values.aibot.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: aibot + labels: + {{- include "huly.labels" . | nindent 4 }} + app: aibot +spec: + replicas: {{ .Values.aibot.replicas }} + selector: + matchLabels: + app: aibot + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: aibot + spec: + {{- include "huly.scheduling" . | nindent 6 }} + initContainers: + {{- include "huly.waitForCockroach" . | nindent 8 }} + {{- include "huly.waitForMongodb" . | nindent 8 }} + containers: + - name: aibot + image: {{ .Values.hulyRegistry }}/ai-bot:{{ .Values.hulyVersion }} + ports: + - name: http + containerPort: 4010 + env: + - name: SERVER_PORT + value: "4010" + - name: FIRST_NAME + value: {{ .Values.aibot.firstName | quote }} + - name: LAST_NAME + value: {{ .Values.aibot.lastName | quote }} + {{- include "huly.envSecret" (dict "name" "SERVER_SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "STORAGE_CONFIG" "key" "STORAGE_CONFIG" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "DB_URL" "key" "CR_DB_URL" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "OPENAI_API_KEY" "key" "OPENAI_API_KEY" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "PASSWORD" "key" "AIBOT_PASSWORD" "root" .) | nindent 12 }} + {{- if .Values.secrets.openaiBaseUrl }} + {{- include "huly.envSecret" (dict "name" "OPENAI_BASE_URL" "key" "OPENAI_BASE_URL" "root" .) | nindent 12 }} + {{- end }} + {{- include "huly.envConfig" (dict "name" "ACCOUNTS_URL" "key" "ACCOUNTS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "MONGO_URL" "key" "MONGO_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "STATS_URL" "key" "STATS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "QUEUE_CONFIG" "key" "QUEUE_CONFIG" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "LOVE_ENDPOINT" "key" "LOVE_ENDPOINT" "root" .) | nindent 12 }} + {{- with .Values.aibot.openaiModel }} + - name: OPENAI_MODEL + value: {{ . | quote }} + {{- end }} + {{- with .Values.aibot.openaiEmbeddingModel }} + - name: OPENAI_EMBEDDING_MODEL + value: {{ . | quote }} + {{- end }} + {{- with .Values.aibot.openaiTranslateModel }} + - name: AI_OPENAI_TRANSLATE_MODEL + value: {{ . | quote }} + {{- end }} + {{- with .Values.aibot.openaiSummaryModel }} + - name: AI_OPENAI_SUMMARY_MODEL + value: {{ . | quote }} + {{- end }} + readinessProbe: + tcpSocket: + port: 4010 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 4010 + initialDelaySeconds: 15 + periodSeconds: 30 + {{- with .Values.aibot.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} +{{- end }} diff --git a/helm/huly/templates/aibot/service.yaml b/helm/huly/templates/aibot/service.yaml new file mode 100644 index 0000000..d54a6dc --- /dev/null +++ b/helm/huly/templates/aibot/service.yaml @@ -0,0 +1,16 @@ +{{- if .Values.aibot.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: aibot + labels: + {{- include "huly.labels" . | nindent 4 }} + app: aibot +spec: + selector: + app: aibot + ports: + - name: http + port: 4010 + targetPort: 4010 +{{- end }} diff --git a/helm/huly/templates/backup/cronjob-cockroachdb.yaml b/helm/huly/templates/backup/cronjob-cockroachdb.yaml new file mode 100644 index 0000000..01a4594 --- /dev/null +++ b/helm/huly/templates/backup/cronjob-cockroachdb.yaml @@ -0,0 +1,122 @@ +{{- if and .Values.backup.enabled .Values.backup.cockroachdb.enabled .Values.cockroach.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "huly.fullname" . }}-backup-cockroachdb + labels: + {{- include "huly.labels" . | nindent 4 }} + app: backup-cockroachdb +spec: + schedule: {{ .Values.backup.cockroachdb.schedule | default .Values.backup.schedule | quote }} + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + template: + metadata: + labels: + {{- include "huly.labels" . | nindent 12 }} + app: backup-cockroachdb + spec: + {{- include "huly.scheduling" . | nindent 10 }} + restartPolicy: OnFailure + volumes: + - name: backup + emptyDir: {} + initContainers: + - name: crdb-dump + image: cockroachdb/cockroach:latest-v24.2 + volumeMounts: + - name: backup + mountPath: /backup + command: + - sh + - -c + - | + set -e + STAMP=$(date +%Y%m%d-%H%M%S) + DEST="/backup/cockroachdb_${STAMP}.sql.gz" + CRDB="cockroach sql --insecure --host=cockroach:26257 -d {{ .Values.cockroach.database }}" + + echo "=== CockroachDB backup started at $(date -u) ===" + + { + # 1. Schema DDL + echo "-- Schema dump" + $CRDB --format=raw -e "SHOW CREATE ALL TABLES" + + # 2. Per-table data as CSV (COPY format) + TABLES=$($CRDB --format=csv -e " + SELECT schema_name || '.' || table_name + FROM [SHOW TABLES FROM {{ .Values.cockroach.database }}] + WHERE schema_name NOT IN ('crdb_internal','pg_catalog','pg_extension','information_schema') + ORDER BY schema_name, table_name + " | tail -n +2) + + for tbl in $TABLES; do + ROW_COUNT=$($CRDB --format=csv -e "SELECT count(*) FROM ${tbl}" | tail -1) + if [ "$ROW_COUNT" -gt 0 ] 2>/dev/null; then + echo "" + echo "-- Data for ${tbl} (${ROW_COUNT} rows)" + $CRDB --format=raw -e "SELECT + 'INSERT INTO ${tbl} (' || + array_to_string( + (SELECT array_agg(column_name ORDER BY ordinal_position) + FROM information_schema.columns + WHERE table_schema = split_part('${tbl}', '.', 1) + AND table_name = split_part('${tbl}', '.', 2)), + ', ' + ) || ') VALUES' AS header" 2>/dev/null || true + + # Export as CSV, can be loaded with IMPORT INTO + echo "-- CSV data for IMPORT INTO ${tbl}:" + echo "COPY ${tbl} FROM stdin WITH CSV HEADER;" + $CRDB --format=csv -e "TABLE ${tbl}" + echo "\\." + fi + done + } | gzip > "$DEST" + + SIZE=$(ls -lh "$DEST" | awk '{print $5}') + echo "=== Dump complete: ${DEST} (${SIZE}) ===" + containers: + - name: rclone-upload + image: {{ .Values.backup.rcloneImage }} + volumeMounts: + - name: backup + mountPath: /backup + env: + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_ENDPOINT" "key" "BACKUP_S3_ENDPOINT" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_REGION" "key" "BACKUP_S3_REGION" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_BUCKET" "key" "BACKUP_S3_BUCKET" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_PATH_PREFIX" "key" "BACKUP_S3_PATH_PREFIX" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_ACCESS_KEY" "key" "BACKUP_S3_ACCESS_KEY" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_SECRET_KEY" "key" "BACKUP_S3_SECRET_KEY" "root" .) | nindent 16 }} + command: + - sh + - -c + - | + set -e + # Configure rclone remote + rclone config create backup s3 \ + provider=Other \ + env_auth=false \ + access_key_id="$BACKUP_S3_ACCESS_KEY" \ + secret_access_key="$BACKUP_S3_SECRET_KEY" \ + endpoint="$BACKUP_S3_ENDPOINT" \ + region="$BACKUP_S3_REGION" \ + --non-interactive + + REMOTE_PATH="backup:${BACKUP_S3_BUCKET}/${BACKUP_S3_PATH_PREFIX}/cockroachdb/" + + echo "Uploading to ${REMOTE_PATH}..." + rclone copy /backup/ "$REMOTE_PATH" --include "*.sql.gz" -v + + echo "Cleaning up backups older than {{ .Values.backup.retentionDays }} days..." + rclone delete "$REMOTE_PATH" --min-age {{ .Values.backup.retentionDays }}d -v + + echo "Backup complete." + rclone ls "$REMOTE_PATH" | tail -5 +{{- end }} diff --git a/helm/huly/templates/backup/cronjob-files.yaml b/helm/huly/templates/backup/cronjob-files.yaml new file mode 100644 index 0000000..57fdd4e --- /dev/null +++ b/helm/huly/templates/backup/cronjob-files.yaml @@ -0,0 +1,107 @@ +{{- if and .Values.backup.enabled .Values.backup.files.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "huly.fullname" . }}-backup-files + labels: + {{- include "huly.labels" . | nindent 4 }} + app: backup-files +spec: + schedule: {{ .Values.backup.files.schedule | default .Values.backup.schedule | quote }} + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + template: + metadata: + labels: + {{- include "huly.labels" . | nindent 12 }} + app: backup-files + spec: + {{- include "huly.scheduling" . | nindent 10 }} + restartPolicy: OnFailure + containers: + - name: rclone-sync + image: {{ .Values.backup.rcloneImage }} + env: + {{- include "huly.envSecret" (dict "name" "STORAGE_CONFIG" "key" "STORAGE_CONFIG" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_ENDPOINT" "key" "BACKUP_S3_ENDPOINT" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_REGION" "key" "BACKUP_S3_REGION" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_BUCKET" "key" "BACKUP_S3_BUCKET" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_PATH_PREFIX" "key" "BACKUP_S3_PATH_PREFIX" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_ACCESS_KEY" "key" "BACKUP_S3_ACCESS_KEY" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_SECRET_KEY" "key" "BACKUP_S3_SECRET_KEY" "root" .) | nindent 16 }} + command: + - sh + - -c + - | + set -e + + # Parse STORAGE_CONFIG to extract source S3 details. + # Format: s3|https://endpoint?accessKey=X&secretKey=Y®ion=Z&rootBucket=B + # or: minio|minio?accessKey=X&secretKey=Y + + PROTO=$(echo "$STORAGE_CONFIG" | cut -d'|' -f1) + REST=$(echo "$STORAGE_CONFIG" | cut -d'|' -f2) + + if [ "$PROTO" = "minio" ]; then + # MinIO: host is "minio", creds in query string + SRC_ENDPOINT="http://minio:9000" + SRC_PROVIDER="Minio" + else + # External S3: endpoint is the URL before '?' + SRC_ENDPOINT=$(echo "$REST" | cut -d'?' -f1) + SRC_PROVIDER="Other" + fi + + PARAMS=$(echo "$REST" | cut -d'?' -f2) + SRC_ACCESS_KEY=$(echo "$PARAMS" | tr '&' '\n' | grep '^accessKey=' | cut -d= -f2) + SRC_SECRET_KEY=$(echo "$PARAMS" | tr '&' '\n' | grep '^secretKey=' | cut -d= -f2) + SRC_REGION=$(echo "$PARAMS" | tr '&' '\n' | grep '^region=' | cut -d= -f2) + SRC_ROOT_BUCKET=$(echo "$PARAMS" | tr '&' '\n' | grep '^rootBucket=' | cut -d= -f2) + SRC_BUCKET_PREFIX=$(echo "$PARAMS" | tr '&' '\n' | grep '^bucketPrefix=' | cut -d= -f2) + + # Configure source remote + rclone config create source s3 \ + provider="$SRC_PROVIDER" \ + env_auth=false \ + access_key_id="$SRC_ACCESS_KEY" \ + secret_access_key="$SRC_SECRET_KEY" \ + endpoint="$SRC_ENDPOINT" \ + region="${SRC_REGION:-us-east-1}" \ + --non-interactive + + # Configure backup remote + rclone config create backup s3 \ + provider=Other \ + env_auth=false \ + access_key_id="$BACKUP_S3_ACCESS_KEY" \ + secret_access_key="$BACKUP_S3_SECRET_KEY" \ + endpoint="$BACKUP_S3_ENDPOINT" \ + region="$BACKUP_S3_REGION" \ + --non-interactive + + # Determine source path + if [ -n "$SRC_ROOT_BUCKET" ]; then + SRC_PATH="source:${SRC_ROOT_BUCKET}" + elif [ -n "$SRC_BUCKET_PREFIX" ]; then + echo "Warning: bucketPrefix mode — syncing all buckets with prefix '${SRC_BUCKET_PREFIX}'" + SRC_PATH="source:${SRC_BUCKET_PREFIX}" + else + echo "Error: cannot determine source bucket from STORAGE_CONFIG" + exit 1 + fi + + DEST_PATH="backup:${BACKUP_S3_BUCKET}/${BACKUP_S3_PATH_PREFIX}/files/" + + echo "Syncing files from ${SRC_PATH} to ${DEST_PATH}..." + rclone sync "$SRC_PATH" "$DEST_PATH" \ + --transfers 8 \ + --checkers 16 \ + --fast-list \ + -v + + echo "File sync complete." +{{- end }} diff --git a/helm/huly/templates/backup/cronjob-mongodb.yaml b/helm/huly/templates/backup/cronjob-mongodb.yaml new file mode 100644 index 0000000..8bfd3e9 --- /dev/null +++ b/helm/huly/templates/backup/cronjob-mongodb.yaml @@ -0,0 +1,84 @@ +{{- if and .Values.backup.enabled .Values.backup.mongodb.enabled .Values.aibot.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "huly.fullname" . }}-backup-mongodb + labels: + {{- include "huly.labels" . | nindent 4 }} + app: backup-mongodb +spec: + schedule: {{ .Values.backup.mongodb.schedule | default .Values.backup.schedule | quote }} + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 2 + template: + metadata: + labels: + {{- include "huly.labels" . | nindent 12 }} + app: backup-mongodb + spec: + {{- include "huly.scheduling" . | nindent 10 }} + restartPolicy: OnFailure + volumes: + - name: backup + emptyDir: {} + initContainers: + - name: mongodump + image: {{ .Values.mongodb.image }} + volumeMounts: + - name: backup + mountPath: /backup + env: + {{- include "huly.envConfig" (dict "name" "MONGO_URL" "key" "MONGO_URL" "root" .) | nindent 16 }} + command: + - sh + - -c + - | + set -e + STAMP=$(date +%Y%m%d-%H%M%S) + DEST="/backup/mongodb_${STAMP}.gz" + echo "Dumping MongoDB to ${DEST}..." + mongodump --uri="$MONGO_URL" --archive="$DEST" --gzip + ls -lh "$DEST" + echo "mongodump complete." + containers: + - name: rclone-upload + image: {{ .Values.backup.rcloneImage }} + volumeMounts: + - name: backup + mountPath: /backup + env: + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_ENDPOINT" "key" "BACKUP_S3_ENDPOINT" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_REGION" "key" "BACKUP_S3_REGION" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_BUCKET" "key" "BACKUP_S3_BUCKET" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_PATH_PREFIX" "key" "BACKUP_S3_PATH_PREFIX" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_ACCESS_KEY" "key" "BACKUP_S3_ACCESS_KEY" "root" .) | nindent 16 }} + {{- include "huly.envBackupSecret" (dict "name" "BACKUP_S3_SECRET_KEY" "key" "BACKUP_S3_SECRET_KEY" "root" .) | nindent 16 }} + command: + - sh + - -c + - | + set -e + rclone config create backup s3 \ + provider=Other \ + env_auth=false \ + access_key_id="$BACKUP_S3_ACCESS_KEY" \ + secret_access_key="$BACKUP_S3_SECRET_KEY" \ + endpoint="$BACKUP_S3_ENDPOINT" \ + region="$BACKUP_S3_REGION" \ + --non-interactive + + REMOTE_PATH="backup:${BACKUP_S3_BUCKET}/${BACKUP_S3_PATH_PREFIX}/mongodb/" + + echo "Uploading to ${REMOTE_PATH}..." + rclone copy /backup/ "$REMOTE_PATH" --include "*.gz" -v + + echo "Cleaning up backups older than {{ .Values.backup.retentionDays }} days..." + rclone delete "$REMOTE_PATH" --min-age {{ .Values.backup.retentionDays }}d -v + + echo "Backup complete." + rclone ls "$REMOTE_PATH" | tail -5 +{{- end }} diff --git a/helm/huly/templates/backup/secret.yaml b/helm/huly/templates/backup/secret.yaml new file mode 100644 index 0000000..4d89659 --- /dev/null +++ b/helm/huly/templates/backup/secret.yaml @@ -0,0 +1,43 @@ +{{- if .Values.backup.enabled }} +{{- $secretName := include "huly.backupSecretName" . -}} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName -}} +{{- $hasExisting := not (empty $existing) -}} + +{{- /* Resolve active credential set */ -}} +{{- $accessKey := "" -}} +{{- $secretKey := "" -}} +{{- if eq .Values.backup.s3.activeCredential "secondary" -}} + {{- $accessKey = .Values.backup.s3.secondaryAccessKey -}} + {{- $secretKey = .Values.backup.s3.secondarySecretKey -}} +{{- else -}} + {{- $accessKey = .Values.backup.s3.accessKey -}} + {{- $secretKey = .Values.backup.s3.secretKey -}} +{{- end -}} + +{{- /* Fall back to existing secret if keys are empty */ -}} +{{- if and (not $accessKey) $hasExisting (hasKey $existing.data "BACKUP_S3_ACCESS_KEY") -}} + {{- $accessKey = index $existing.data "BACKUP_S3_ACCESS_KEY" | b64dec -}} +{{- end -}} +{{- if and (not $secretKey) $hasExisting (hasKey $existing.data "BACKUP_S3_SECRET_KEY") -}} + {{- $secretKey = index $existing.data "BACKUP_S3_SECRET_KEY" | b64dec -}} +{{- end -}} + +{{- if or (not $accessKey) (not $secretKey) -}} + {{- fail "backup.s3.accessKey and backup.s3.secretKey are required when backup.enabled=true" }} +{{- end -}} + +apiVersion: v1 +kind: Secret +metadata: + name: {{ $secretName }} + labels: + {{- include "huly.labels" . | nindent 4 }} +type: Opaque +data: + BACKUP_S3_ENDPOINT: {{ .Values.backup.s3.endpoint | b64enc | quote }} + BACKUP_S3_REGION: {{ .Values.backup.s3.region | b64enc | quote }} + BACKUP_S3_BUCKET: {{ .Values.backup.s3.bucket | b64enc | quote }} + BACKUP_S3_PATH_PREFIX: {{ .Values.backup.s3.pathPrefix | b64enc | quote }} + BACKUP_S3_ACCESS_KEY: {{ $accessKey | b64enc | quote }} + BACKUP_S3_SECRET_KEY: {{ $secretKey | b64enc | quote }} +{{- end }} diff --git a/helm/huly/templates/cockroach/deployment.yaml b/helm/huly/templates/cockroach/deployment.yaml new file mode 100644 index 0000000..40788bf --- /dev/null +++ b/helm/huly/templates/cockroach/deployment.yaml @@ -0,0 +1,62 @@ +{{- if .Values.cockroach.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cockroach + labels: + {{- include "huly.labels" . | nindent 4 }} + app: cockroach +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: cockroach + template: + metadata: + labels: + {{- include "huly.labels" . | nindent 8 }} + app: cockroach + spec: + {{- include "huly.scheduling" . | nindent 6 }} + containers: + - name: cockroach + image: {{ .Values.cockroach.image }} + args: + - start-single-node + - --insecure + ports: + - name: grpc + containerPort: 26257 + - name: http + containerPort: 8080 + env: + - name: COCKROACH_DATABASE + value: {{ .Values.cockroach.database }} + - name: COCKROACH_USER + value: {{ .Values.cockroach.username }} + {{- include "huly.envSecret" (dict "name" "COCKROACH_PASSWORD" "key" "COCKROACH_PASSWORD" "root" .) | nindent 12 }} + readinessProbe: + exec: + command: ['sh', '-c', 'curl -sf http://localhost:8080/health?ready=1'] + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 30 + volumeMounts: + - name: data + mountPath: /cockroach/cockroach-data + {{- with .Values.cockroach.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: data + persistentVolumeClaim: + claimName: cockroach-data +{{- end }} diff --git a/helm/huly/templates/cockroach/pvc.yaml b/helm/huly/templates/cockroach/pvc.yaml new file mode 100644 index 0000000..a4f85ff --- /dev/null +++ b/helm/huly/templates/cockroach/pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.cockroach.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: cockroach-data + labels: + {{- include "huly.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.cockroach.storageClassName }} + storageClassName: {{ .Values.cockroach.storageClassName }} + {{- end }} + resources: + requests: + storage: {{ .Values.cockroach.storage }} +{{- end }} diff --git a/helm/huly/templates/cockroach/service.yaml b/helm/huly/templates/cockroach/service.yaml new file mode 100644 index 0000000..b8189aa --- /dev/null +++ b/helm/huly/templates/cockroach/service.yaml @@ -0,0 +1,20 @@ +{{- if .Values.cockroach.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: cockroach + labels: + {{- include "huly.labels" . | nindent 4 }} + app: cockroach +spec: + clusterIP: None + selector: + app: cockroach + ports: + - name: grpc + port: 26257 + targetPort: 26257 + - name: http + port: 8080 + targetPort: 8080 +{{- end }} diff --git a/helm/huly/templates/collaborator/deployment.yaml b/helm/huly/templates/collaborator/deployment.yaml new file mode 100644 index 0000000..8cf04cc --- /dev/null +++ b/helm/huly/templates/collaborator/deployment.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: collaborator + labels: + {{- include "huly.labels" . | nindent 4 }} + app: collaborator +spec: + replicas: {{ .Values.collaborator.replicas }} + selector: + matchLabels: + app: collaborator + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: collaborator + spec: + {{- include "huly.scheduling" . | nindent 6 }} + containers: + - name: collaborator + image: {{ .Values.hulyRegistry }}/collaborator:{{ .Values.hulyVersion }} + ports: + - name: http + containerPort: 3078 + env: + - name: COLLABORATOR_PORT + value: "3078" + {{- include "huly.envSecret" (dict "name" "SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "STORAGE_CONFIG" "key" "STORAGE_CONFIG" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "ACCOUNTS_URL" "key" "ACCOUNTS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "STATS_URL" "key" "STATS_URL_INTERNAL" "root" .) | nindent 12 }} + readinessProbe: + tcpSocket: + port: 3078 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 3078 + initialDelaySeconds: 30 + periodSeconds: 30 + {{- with .Values.collaborator.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/helm/huly/templates/collaborator/service.yaml b/helm/huly/templates/collaborator/service.yaml new file mode 100644 index 0000000..0bccc5b --- /dev/null +++ b/helm/huly/templates/collaborator/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: collaborator + labels: + {{- include "huly.labels" . | nindent 4 }} + app: collaborator +spec: + selector: + app: collaborator + ports: + - name: http + port: 3078 + targetPort: 3078 diff --git a/helm/huly/templates/configmap.yaml b/helm/huly/templates/configmap.yaml new file mode 100644 index 0000000..8c143b2 --- /dev/null +++ b/helm/huly/templates/configmap.yaml @@ -0,0 +1,60 @@ +{{- $proto := ternary "https" "http" (and .Values.ingress.enabled .Values.ingress.tls.enabled) -}} +{{- $wsProto := ternary "wss" "ws" (and .Values.ingress.enabled .Values.ingress.tls.enabled) -}} + +{{- /* Resolve infra endpoints: built-in pod name or external URL */ -}} +{{- $elasticUrl := ternary "http://elastic:9200" .Values.external.elastic .Values.elastic.enabled -}} +{{- $queueConfig := ternary "redpanda:9092" .Values.external.redpanda .Values.redpanda.enabled -}} + +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "huly.configName" . }} + labels: + {{- include "huly.labels" . | nindent 4 }} +data: + # External URLs (via ingress) + ACCOUNTS_URL: "{{ $proto }}://{{ .Values.domain }}/_accounts" + COLLABORATOR_URL: "{{ $wsProto }}://{{ .Values.domain }}/_collaborator" + FRONT_URL: "{{ $proto }}://{{ .Values.domain }}" + REKONI_URL: "{{ $proto }}://{{ .Values.domain }}/_rekoni" + STATS_URL: "{{ $proto }}://{{ .Values.domain }}/_stats" + TRANSACTOR_URL: "ws://transactor:3333;{{ $wsProto }}://{{ .Values.domain }}/_transactor" + + # Internal URLs (pod-to-pod) + ELASTIC_URL: {{ $elasticUrl | quote }} + ELASTIC_INDEX_NAME: "huly_storage_index" + FULLTEXT_URL: "http://fulltext:4700" + ACCOUNTS_URL_INTERNAL: "http://account:3000" + REKONI_URL_INTERNAL: "http://rekoni:4004" + STATS_URL_INTERNAL: "http://stats:4900" + QUEUE_CONFIG: {{ $queueConfig | quote }} + + # App settings + MODEL_ENABLED: {{ .Values.appSettings.modelEnabled | quote }} + LAST_NAME_FIRST: {{ .Values.appSettings.lastNameFirst | quote }} + TITLE: {{ .Values.appSettings.title | quote }} + DEFAULT_LANGUAGE: {{ .Values.appSettings.defaultLanguage | quote }} + UPLOAD_URL: "/files" + DESKTOP_UPDATES_CHANNEL: {{ .Values.appSettings.desktopChannel | quote }} + {{- if .Values.appSettings.adminEmails }} + ADMIN_EMAILS: {{ .Values.appSettings.adminEmails | quote }} + {{- end }} + {{- if .Values.auth.disableSignup }} + DISABLE_SIGNUP: "true" + {{- end }} + + # Optional integration URLs (required by front even if unused) + GMAIL_URL: "{{ $proto }}://{{ .Values.domain }}/_gmail" + TELEGRAM_URL: "{{ $proto }}://{{ .Values.domain }}/_telegram" + CALENDAR_URL: "{{ $proto }}://{{ .Values.domain }}/_calendar" + LOVE_ENDPOINT: "{{ $proto }}://{{ .Values.domain }}/_love" + + {{- if .Values.githubIntegration.enabled }} + GITHUB_URL: "{{ $proto }}://{{ .Values.domain }}/_github" + {{- end }} + + {{- if .Values.aibot.enabled }} + AI_URL: "{{ $proto }}://{{ .Values.domain }}/_aibot" + AI_BOT_URL: "http://aibot:4010" + MONGO_URL: {{ ternary "mongodb://mongodb:27017" .Values.external.mongodb .Values.mongodb.enabled | quote }} + {{- end }} diff --git a/helm/huly/templates/elastic/deployment.yaml b/helm/huly/templates/elastic/deployment.yaml new file mode 100644 index 0000000..f367e92 --- /dev/null +++ b/helm/huly/templates/elastic/deployment.yaml @@ -0,0 +1,82 @@ +{{- if .Values.elastic.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: elastic + labels: + {{- include "huly.labels" . | nindent 4 }} + app: elastic +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: elastic + template: + metadata: + labels: + {{- include "huly.labels" . | nindent 8 }} + app: elastic + spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + {{- include "huly.scheduling" . | nindent 6 }} + containers: + - name: elastic + image: {{ .Values.elastic.image }} + args: + - /bin/sh + - -c + - | + bin/elasticsearch-plugin install --batch ingest-attachment; + /usr/local/bin/docker-entrypoint.sh elasticsearch + ports: + - name: http + containerPort: 9200 + env: + - name: BITNAMI_DEBUG + value: "true" + - name: ELASTICSEARCH_PORT_NUMBER + value: "9200" + - name: ES_JAVA_OPTS + value: {{ .Values.elastic.javaOpts | quote }} + - name: discovery.type + value: single-node + - name: http.cors.enabled + value: "true" + - name: http.cors.allow-origin + value: "http://localhost:8082" + livenessProbe: + exec: + command: + - /bin/sh + - -c + - | + curl -sf http://localhost:9200/_cluster/health | grep -vq '"status":"red"' + initialDelaySeconds: 60 + periodSeconds: 20 + failureThreshold: 10 + readinessProbe: + exec: + command: + - /bin/sh + - -c + - | + curl -sf http://localhost:9200/_cluster/health + initialDelaySeconds: 30 + periodSeconds: 10 + volumeMounts: + - name: data + mountPath: /usr/share/elasticsearch/data + {{- with .Values.elastic.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: data + persistentVolumeClaim: + claimName: elastic-data +{{- end }} diff --git a/helm/huly/templates/elastic/pvc.yaml b/helm/huly/templates/elastic/pvc.yaml new file mode 100644 index 0000000..773c77b --- /dev/null +++ b/helm/huly/templates/elastic/pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.elastic.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: elastic-data + labels: + {{- include "huly.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.elastic.storageClassName }} + storageClassName: {{ .Values.elastic.storageClassName }} + {{- end }} + resources: + requests: + storage: {{ .Values.elastic.storage }} +{{- end }} diff --git a/helm/huly/templates/elastic/service.yaml b/helm/huly/templates/elastic/service.yaml new file mode 100644 index 0000000..5a30e1a --- /dev/null +++ b/helm/huly/templates/elastic/service.yaml @@ -0,0 +1,16 @@ +{{- if .Values.elastic.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: elastic + labels: + {{- include "huly.labels" . | nindent 4 }} + app: elastic +spec: + selector: + app: elastic + ports: + - name: http + port: 9200 + targetPort: 9200 +{{- end }} diff --git a/helm/huly/templates/front/deployment.yaml b/helm/huly/templates/front/deployment.yaml new file mode 100644 index 0000000..bb8c4b0 --- /dev/null +++ b/helm/huly/templates/front/deployment.yaml @@ -0,0 +1,73 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: front + labels: + {{- include "huly.labels" . | nindent 4 }} + app: front +spec: + replicas: {{ .Values.front.replicas }} + selector: + matchLabels: + app: front + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: front + spec: + {{- include "huly.scheduling" . | nindent 6 }} + containers: + - name: front + image: {{ .Values.hulyRegistry }}/front:{{ .Values.hulyVersion }} + ports: + - name: http + containerPort: 8080 + env: + - name: SERVER_PORT + value: "8080" + {{- include "huly.envSecret" (dict "name" "SERVER_SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "STORAGE_CONFIG" "key" "STORAGE_CONFIG" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "ACCOUNTS_URL" "key" "ACCOUNTS_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "ACCOUNTS_URL_INTERNAL" "key" "ACCOUNTS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "COLLABORATOR_URL" "key" "COLLABORATOR_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "REKONI_URL" "key" "REKONI_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "ELASTIC_URL" "key" "ELASTIC_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "STATS_URL" "key" "STATS_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "DEFAULT_LANGUAGE" "key" "DEFAULT_LANGUAGE" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "TITLE" "key" "TITLE" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "UPLOAD_URL" "key" "UPLOAD_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "LAST_NAME_FIRST" "key" "LAST_NAME_FIRST" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "DESKTOP_UPDATES_CHANNEL" "key" "DESKTOP_UPDATES_CHANNEL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "CALENDAR_URL" "key" "CALENDAR_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "GMAIL_URL" "key" "GMAIL_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "TELEGRAM_URL" "key" "TELEGRAM_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "LOVE_ENDPOINT" "key" "LOVE_ENDPOINT" "root" .) | nindent 12 }} + {{- if .Values.githubIntegration.enabled }} + {{- include "huly.envConfig" (dict "name" "GITHUB_URL" "key" "GITHUB_URL" "root" .) | nindent 12 }} + {{- end }} + {{- if .Values.aibot.enabled }} + {{- include "huly.envConfig" (dict "name" "AI_URL" "key" "AI_URL" "root" .) | nindent 12 }} + {{- end }} + {{- if .Values.auth.disableSignup }} + - name: DISABLE_SIGNUP + value: "true" + {{- end }} + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 30 + {{- with .Values.front.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/helm/huly/templates/front/service.yaml b/helm/huly/templates/front/service.yaml new file mode 100644 index 0000000..31d474f --- /dev/null +++ b/helm/huly/templates/front/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: front + labels: + {{- include "huly.labels" . | nindent 4 }} + app: front +spec: + selector: + app: front + ports: + - name: http + port: 8080 + targetPort: 8080 diff --git a/helm/huly/templates/fulltext/deployment.yaml b/helm/huly/templates/fulltext/deployment.yaml new file mode 100644 index 0000000..e98e1e8 --- /dev/null +++ b/helm/huly/templates/fulltext/deployment.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fulltext + labels: + {{- include "huly.labels" . | nindent 4 }} + app: fulltext +spec: + replicas: {{ .Values.fulltext.replicas }} + selector: + matchLabels: + app: fulltext + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: fulltext + spec: + {{- include "huly.scheduling" . | nindent 6 }} + initContainers: + {{- include "huly.waitForCockroach" . | nindent 8 }} + {{- include "huly.waitForRedpanda" . | nindent 8 }} + containers: + - name: fulltext + image: {{ .Values.hulyRegistry }}/fulltext:{{ .Values.hulyVersion }} + ports: + - name: http + containerPort: 4700 + env: + {{- include "huly.envSecret" (dict "name" "SERVER_SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "DB_URL" "key" "CR_DB_URL" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "STORAGE_CONFIG" "key" "STORAGE_CONFIG" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "FULLTEXT_DB_URL" "key" "ELASTIC_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "ELASTIC_INDEX_NAME" "key" "ELASTIC_INDEX_NAME" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "REKONI_URL" "key" "REKONI_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "ACCOUNTS_URL" "key" "ACCOUNTS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "STATS_URL" "key" "STATS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "QUEUE_CONFIG" "key" "QUEUE_CONFIG" "root" .) | nindent 12 }} + readinessProbe: + tcpSocket: + port: 4700 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 4700 + initialDelaySeconds: 30 + periodSeconds: 30 + {{- with .Values.fulltext.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/helm/huly/templates/fulltext/service.yaml b/helm/huly/templates/fulltext/service.yaml new file mode 100644 index 0000000..ccf9b4f --- /dev/null +++ b/helm/huly/templates/fulltext/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: fulltext + labels: + {{- include "huly.labels" . | nindent 4 }} + app: fulltext +spec: + selector: + app: fulltext + ports: + - name: http + port: 4700 + targetPort: 4700 diff --git a/helm/huly/templates/github/deployment.yaml b/helm/huly/templates/github/deployment.yaml new file mode 100644 index 0000000..96bf835 --- /dev/null +++ b/helm/huly/templates/github/deployment.yaml @@ -0,0 +1,61 @@ +{{- if .Values.githubIntegration.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: github + labels: + {{- include "huly.labels" . | nindent 4 }} + app: github +spec: + replicas: {{ .Values.githubIntegration.replicas }} + selector: + matchLabels: + app: github + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: github + spec: + {{- include "huly.scheduling" . | nindent 6 }} + containers: + - name: github + image: {{ .Values.hulyRegistry }}/github:{{ .Values.hulyVersion }} + ports: + - name: http + containerPort: 3500 + env: + - name: PORT + value: "3500" + {{- include "huly.envSecret" (dict "name" "SERVER_SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "STORAGE_CONFIG" "key" "STORAGE_CONFIG" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "APP_ID" "key" "GITHUB_APP_ID" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "CLIENT_ID" "key" "GITHUB_APP_CLIENT_ID" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "CLIENT_SECRET" "key" "GITHUB_APP_CLIENT_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "PRIVATE_KEY" "key" "GITHUB_APP_PRIVATE_KEY" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "WEBHOOK_SECRET" "key" "GITHUB_APP_WEBHOOK_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "ACCOUNTS_URL" "key" "ACCOUNTS_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "FRONT_URL" "key" "FRONT_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "COLLABORATOR_URL" "key" "COLLABORATOR_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "QUEUE_CONFIG" "key" "QUEUE_CONFIG" "root" .) | nindent 12 }} + - name: BOT_NAME + value: {{ .Values.githubIntegration.botName | quote }} + - name: SERVICE_ID + value: "github-service" + readinessProbe: + tcpSocket: + port: 3500 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 3500 + initialDelaySeconds: 30 + periodSeconds: 30 + {{- with .Values.githubIntegration.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} +{{- end }} diff --git a/helm/huly/templates/github/service.yaml b/helm/huly/templates/github/service.yaml new file mode 100644 index 0000000..8ec18ad --- /dev/null +++ b/helm/huly/templates/github/service.yaml @@ -0,0 +1,16 @@ +{{- if .Values.githubIntegration.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: github + labels: + {{- include "huly.labels" . | nindent 4 }} + app: github +spec: + selector: + app: github + ports: + - name: http + port: 3500 + targetPort: 3500 +{{- end }} diff --git a/helm/huly/templates/ingress.yaml b/helm/huly/templates/ingress.yaml new file mode 100644 index 0000000..5d821bf --- /dev/null +++ b/helm/huly/templates/ingress.yaml @@ -0,0 +1,98 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullname := include "huly.fullname" . -}} +{{- $domain := .Values.domain -}} +{{- $tlsEnabled := .Values.ingress.tls.enabled -}} +{{- $clusterIssuer := .Values.ingress.tls.clusterIssuer -}} +{{- $className := .Values.ingress.className -}} +{{- $extraAnnotations := .Values.ingress.annotations -}} + +{{- /* Backend services exposed via path-based routing */ -}} +{{- $backends := list + (dict "name" "accounts" "svc" "account" "port" 3000 "ws" false) + (dict "name" "transactor" "svc" "transactor" "port" 3333 "ws" true) + (dict "name" "collaborator" "svc" "collaborator" "port" 3078 "ws" true) + (dict "name" "rekoni" "svc" "rekoni" "port" 4004 "ws" false) + (dict "name" "stats" "svc" "stats" "port" 4900 "ws" false) +-}} +{{- if .Values.aibot.enabled }} +{{- $backends = append $backends (dict "name" "aibot" "svc" "aibot" "port" 4010 "ws" true) }} +{{- end -}} +{{- if .Values.githubIntegration.enabled }} +{{- $backends = append $backends (dict "name" "github" "svc" "github" "port" 3500 "ws" false) }} +{{- end -}} + +{{- range $backends }} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullname }}-{{ .name }} + labels: + {{- include "huly.labels" $ | nindent 4 }} + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 + {{- if .ws }} + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-http-version: "1.1" + {{- end }} + {{- with $extraAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + ingressClassName: {{ $className }} + {{- if $tlsEnabled }} + tls: + - hosts: + - {{ $domain }} + secretName: {{ $fullname }}-tls + {{- end }} + rules: + - host: {{ $domain }} + http: + paths: + - path: /_{{ .name }}(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: {{ .svc }} + port: + number: {{ .port }} +{{- end }} + +--- +# Front catch-all — also owns the TLS certificate +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullname }}-front + labels: + {{- include "huly.labels" . | nindent 4 }} + annotations: + {{- if $tlsEnabled }} + cert-manager.io/cluster-issuer: {{ $clusterIssuer }} + {{- end }} + {{- with $extraAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + ingressClassName: {{ $className }} + {{- if $tlsEnabled }} + tls: + - hosts: + - {{ $domain }} + secretName: {{ $fullname }}-tls + {{- end }} + rules: + - host: {{ $domain }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: front + port: + number: 8080 +{{- end }} diff --git a/helm/huly/templates/kvs/deployment.yaml b/helm/huly/templates/kvs/deployment.yaml new file mode 100644 index 0000000..ae348a1 --- /dev/null +++ b/helm/huly/templates/kvs/deployment.yaml @@ -0,0 +1,49 @@ +{{- if .Values.kvs.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kvs + labels: + {{- include "huly.labels" . | nindent 4 }} + app: kvs +spec: + replicas: {{ .Values.kvs.replicas }} + selector: + matchLabels: + app: kvs + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: kvs + spec: + {{- include "huly.scheduling" . | nindent 6 }} + initContainers: + {{- include "huly.waitForCockroach" . | nindent 8 }} + containers: + - name: kvs + image: {{ .Values.hulyRegistry }}/hulykvs:{{ .Values.kvs.version | default .Values.hulyVersion }} + imagePullPolicy: Always + ports: + - name: http + containerPort: 8094 + env: + {{- include "huly.envSecret" (dict "name" "HULY_DB_CONNECTION" "key" "CR_DB_URL" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "HULY_TOKEN_SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + readinessProbe: + tcpSocket: + port: 8094 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 8094 + initialDelaySeconds: 15 + periodSeconds: 30 + {{- with .Values.kvs.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} +{{- end }} diff --git a/helm/huly/templates/kvs/service.yaml b/helm/huly/templates/kvs/service.yaml new file mode 100644 index 0000000..1c115b4 --- /dev/null +++ b/helm/huly/templates/kvs/service.yaml @@ -0,0 +1,16 @@ +{{- if .Values.kvs.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: kvs + labels: + {{- include "huly.labels" . | nindent 4 }} + app: kvs +spec: + selector: + app: kvs + ports: + - name: http + port: 8094 + targetPort: 8094 +{{- end }} diff --git a/helm/huly/templates/minio/deployment.yaml b/helm/huly/templates/minio/deployment.yaml new file mode 100644 index 0000000..02d78c0 --- /dev/null +++ b/helm/huly/templates/minio/deployment.yaml @@ -0,0 +1,61 @@ +{{- if eq (include "huly.minioEnabled" .) "true" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: minio + labels: + {{- include "huly.labels" . | nindent 4 }} + app: minio +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: minio + template: + metadata: + labels: + {{- include "huly.labels" . | nindent 8 }} + app: minio + spec: + {{- include "huly.scheduling" . | nindent 6 }} + containers: + - name: minio + image: {{ .Values.minio.image }} + args: + - server + - /data + - --address + - ":9000" + - --console-address + - ":9001" + ports: + - name: api + containerPort: 9000 + - name: console + containerPort: 9001 + readinessProbe: + httpGet: + path: /minio/health/ready + port: 9000 + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /minio/health/live + port: 9000 + initialDelaySeconds: 30 + periodSeconds: 20 + volumeMounts: + - name: data + mountPath: /data + {{- with .Values.minio.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: data + persistentVolumeClaim: + claimName: minio-data +{{- end }} diff --git a/helm/huly/templates/minio/pvc.yaml b/helm/huly/templates/minio/pvc.yaml new file mode 100644 index 0000000..5790a07 --- /dev/null +++ b/helm/huly/templates/minio/pvc.yaml @@ -0,0 +1,17 @@ +{{- if eq (include "huly.minioEnabled" .) "true" }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: minio-data + labels: + {{- include "huly.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.minio.storageClassName }} + storageClassName: {{ .Values.minio.storageClassName }} + {{- end }} + resources: + requests: + storage: {{ .Values.minio.storage }} +{{- end }} diff --git a/helm/huly/templates/minio/service.yaml b/helm/huly/templates/minio/service.yaml new file mode 100644 index 0000000..424a9df --- /dev/null +++ b/helm/huly/templates/minio/service.yaml @@ -0,0 +1,19 @@ +{{- if eq (include "huly.minioEnabled" .) "true" }} +apiVersion: v1 +kind: Service +metadata: + name: minio + labels: + {{- include "huly.labels" . | nindent 4 }} + app: minio +spec: + selector: + app: minio + ports: + - name: api + port: 9000 + targetPort: 9000 + - name: console + port: 9001 + targetPort: 9001 +{{- end }} diff --git a/helm/huly/templates/mongodb/deployment.yaml b/helm/huly/templates/mongodb/deployment.yaml new file mode 100644 index 0000000..032269d --- /dev/null +++ b/helm/huly/templates/mongodb/deployment.yaml @@ -0,0 +1,50 @@ +{{- if and .Values.aibot.enabled .Values.mongodb.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mongodb + labels: + {{- include "huly.labels" . | nindent 4 }} + app: mongodb +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: mongodb + template: + metadata: + labels: + {{- include "huly.labels" . | nindent 8 }} + app: mongodb + spec: + {{- include "huly.scheduling" . | nindent 6 }} + containers: + - name: mongodb + image: {{ .Values.mongodb.image }} + ports: + - name: mongo + containerPort: 27017 + readinessProbe: + tcpSocket: + port: 27017 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 27017 + initialDelaySeconds: 15 + periodSeconds: 30 + volumeMounts: + - name: data + mountPath: /data/db + {{- with .Values.mongodb.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: data + persistentVolumeClaim: + claimName: mongodb-data +{{- end }} diff --git a/helm/huly/templates/mongodb/pvc.yaml b/helm/huly/templates/mongodb/pvc.yaml new file mode 100644 index 0000000..ea5101f --- /dev/null +++ b/helm/huly/templates/mongodb/pvc.yaml @@ -0,0 +1,17 @@ +{{- if and .Values.aibot.enabled .Values.mongodb.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mongodb-data + labels: + {{- include "huly.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.mongodb.storageClassName }} + storageClassName: {{ .Values.mongodb.storageClassName }} + {{- end }} + resources: + requests: + storage: {{ .Values.mongodb.storage }} +{{- end }} diff --git a/helm/huly/templates/mongodb/service.yaml b/helm/huly/templates/mongodb/service.yaml new file mode 100644 index 0000000..3452e2b --- /dev/null +++ b/helm/huly/templates/mongodb/service.yaml @@ -0,0 +1,17 @@ +{{- if and .Values.aibot.enabled .Values.mongodb.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: mongodb + labels: + {{- include "huly.labels" . | nindent 4 }} + app: mongodb +spec: + clusterIP: None + selector: + app: mongodb + ports: + - name: mongo + port: 27017 + targetPort: 27017 +{{- end }} diff --git a/helm/huly/templates/redpanda/deployment.yaml b/helm/huly/templates/redpanda/deployment.yaml new file mode 100644 index 0000000..2ae091e --- /dev/null +++ b/helm/huly/templates/redpanda/deployment.yaml @@ -0,0 +1,87 @@ +{{- if .Values.redpanda.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redpanda + labels: + {{- include "huly.labels" . | nindent 4 }} + app: redpanda +spec: + replicas: 1 + selector: + matchLabels: + app: redpanda + template: + metadata: + labels: + {{- include "huly.labels" . | nindent 8 }} + app: redpanda + spec: + {{- include "huly.scheduling" . | nindent 6 }} + initContainers: + - name: set-datadir-ownership + image: busybox:1.36 + command: ["sh", "-c", "chown -R 101:101 /var/lib/redpanda/data"] + volumeMounts: + - name: data + mountPath: /var/lib/redpanda/data + containers: + - name: redpanda + image: {{ .Values.redpanda.image }} + args: + - redpanda + - start + - --kafka-addr + - internal://0.0.0.0:9092,external://0.0.0.0:19092 + - --advertise-kafka-addr + - internal://redpanda:9092,external://localhost:19092 + - --pandaproxy-addr + - internal://0.0.0.0:8082,external://0.0.0.0:18082 + - --advertise-pandaproxy-addr + - internal://redpanda:8082,external://localhost:18082 + - --schema-registry-addr + - internal://0.0.0.0:8081,external://0.0.0.0:18081 + - --rpc-addr + - 0.0.0.0:33145 + - --advertise-rpc-addr + - redpanda:33145 + - --mode + - dev-container + - --smp + - "1" + - --default-log-level=info + ports: + - name: kafka + containerPort: 9092 + - name: rpc + containerPort: 33145 + env: + - name: REDPANDA_SUPERUSER_USERNAME + value: superuser + - name: REDPANDA_SUPERUSER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "huly.secretName" . }} + key: REDPANDA_SUPERUSER_PASSWORD + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + readinessProbe: + exec: + command: ["/bin/sh", "-c", "rpk cluster info"] + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 5 + volumeMounts: + - name: data + mountPath: /var/lib/redpanda/data + {{- with .Values.redpanda.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: data + persistentVolumeClaim: + claimName: redpanda-data +{{- end }} diff --git a/helm/huly/templates/redpanda/pvc.yaml b/helm/huly/templates/redpanda/pvc.yaml new file mode 100644 index 0000000..05e94a0 --- /dev/null +++ b/helm/huly/templates/redpanda/pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.redpanda.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: redpanda-data + labels: + {{- include "huly.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.redpanda.storageClassName }} + storageClassName: {{ .Values.redpanda.storageClassName }} + {{- end }} + resources: + requests: + storage: {{ .Values.redpanda.storage }} +{{- end }} diff --git a/helm/huly/templates/redpanda/service.yaml b/helm/huly/templates/redpanda/service.yaml new file mode 100644 index 0000000..1f859ca --- /dev/null +++ b/helm/huly/templates/redpanda/service.yaml @@ -0,0 +1,19 @@ +{{- if .Values.redpanda.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: redpanda + labels: + {{- include "huly.labels" . | nindent 4 }} + app: redpanda +spec: + selector: + app: redpanda + ports: + - name: kafka + port: 9092 + targetPort: 9092 + - name: rpc + port: 33145 + targetPort: 33145 +{{- end }} diff --git a/helm/huly/templates/rekoni/deployment.yaml b/helm/huly/templates/rekoni/deployment.yaml new file mode 100644 index 0000000..6edde66 --- /dev/null +++ b/helm/huly/templates/rekoni/deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rekoni + labels: + {{- include "huly.labels" . | nindent 4 }} + app: rekoni +spec: + replicas: {{ .Values.rekoni.replicas }} + selector: + matchLabels: + app: rekoni + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: rekoni + spec: + {{- include "huly.scheduling" . | nindent 6 }} + containers: + - name: rekoni + image: {{ .Values.hulyRegistry }}/rekoni-service:{{ .Values.hulyVersion }} + ports: + - name: http + containerPort: 4004 + env: + {{- include "huly.envSecret" (dict "name" "SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + readinessProbe: + tcpSocket: + port: 4004 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 4004 + initialDelaySeconds: 15 + periodSeconds: 30 + {{- with .Values.rekoni.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/helm/huly/templates/rekoni/service.yaml b/helm/huly/templates/rekoni/service.yaml new file mode 100644 index 0000000..663e751 --- /dev/null +++ b/helm/huly/templates/rekoni/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: rekoni + labels: + {{- include "huly.labels" . | nindent 4 }} + app: rekoni +spec: + selector: + app: rekoni + ports: + - name: http + port: 4004 + targetPort: 4004 diff --git a/helm/huly/templates/secret.yaml b/helm/huly/templates/secret.yaml new file mode 100644 index 0000000..1c8ba53 --- /dev/null +++ b/helm/huly/templates/secret.yaml @@ -0,0 +1,124 @@ +{{- $secretName := include "huly.secretName" . -}} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName -}} +{{- $hasExisting := not (empty $existing) -}} + +{{- /* SERVER_SECRET */ -}} +{{- $serverSecret := "" -}} +{{- if .Values.secrets.serverSecret -}} + {{- $serverSecret = .Values.secrets.serverSecret -}} +{{- else if and $hasExisting (hasKey $existing.data "SERVER_SECRET") -}} + {{- $serverSecret = index $existing.data "SERVER_SECRET" | b64dec -}} +{{- else -}} + {{- $serverSecret = randAlphaNum 32 -}} +{{- end -}} + +{{- /* COCKROACH_PASSWORD */ -}} +{{- $cockroachPwd := "" -}} +{{- if .Values.secrets.cockroachPassword -}} + {{- $cockroachPwd = .Values.secrets.cockroachPassword -}} +{{- else if and $hasExisting (hasKey $existing.data "COCKROACH_PASSWORD") -}} + {{- $cockroachPwd = index $existing.data "COCKROACH_PASSWORD" | b64dec -}} +{{- else -}} + {{- $cockroachPwd = randAlphaNum 24 -}} +{{- end -}} + +{{- /* REDPANDA_SUPERUSER_PASSWORD */ -}} +{{- $redpandaPwd := "" -}} +{{- if .Values.secrets.redpandaPassword -}} + {{- $redpandaPwd = .Values.secrets.redpandaPassword -}} +{{- else if and $hasExisting (hasKey $existing.data "REDPANDA_SUPERUSER_PASSWORD") -}} + {{- $redpandaPwd = index $existing.data "REDPANDA_SUPERUSER_PASSWORD" | b64dec -}} +{{- else -}} + {{- $redpandaPwd = randAlphaNum 24 -}} +{{- end -}} + +{{- /* STORAGE_CONFIG — explicit values > storage.type config > existing secret > auto-generate */ -}} +{{- $storageConfig := "" -}} +{{- if .Values.secrets.storageConfig -}} + {{- $storageConfig = .Values.secrets.storageConfig -}} +{{- else if eq .Values.storage.type "s3" -}} + {{- $s3Params := printf "accessKey=%s&secretKey=%s®ion=%s" .Values.storage.s3.accessKey .Values.storage.s3.secretKey .Values.storage.s3.region -}} + {{- if .Values.storage.s3.rootBucket -}} + {{- $s3Params = printf "%s&rootBucket=%s" $s3Params .Values.storage.s3.rootBucket -}} + {{- end -}} + {{- if .Values.storage.s3.bucketPrefix -}} + {{- $s3Params = printf "%s&bucketPrefix=%s" $s3Params .Values.storage.s3.bucketPrefix -}} + {{- end -}} + {{- $storageConfig = printf "s3|%s?%s" .Values.storage.s3.endpoint $s3Params -}} +{{- else if and $hasExisting (hasKey $existing.data "STORAGE_CONFIG") -}} + {{- $storageConfig = index $existing.data "STORAGE_CONFIG" | b64dec -}} +{{- else -}} + {{- $storageConfig = printf "minio|minio?accessKey=%s&secretKey=%s" (randAlphaNum 20) (randAlphaNum 40) -}} +{{- end -}} + +{{- /* CR_DB_URL */ -}} +{{- $crDbUrl := "" -}} +{{- if .Values.secrets.crDbUrl -}} + {{- $crDbUrl = .Values.secrets.crDbUrl -}} +{{- else if and $hasExisting (hasKey $existing.data "CR_DB_URL") -}} + {{- $crDbUrl = index $existing.data "CR_DB_URL" | b64dec -}} +{{- else -}} + {{- $crDbUrl = printf "postgres://%s:%s@cockroach:26257/%s" .Values.cockroach.username $cockroachPwd .Values.cockroach.database -}} +{{- end -}} + +{{- /* AIBOT_PASSWORD */ -}} +{{- $aibotPwd := "" -}} +{{- if .Values.secrets.aibotPassword -}} + {{- $aibotPwd = .Values.secrets.aibotPassword -}} +{{- else if and $hasExisting (hasKey $existing.data "AIBOT_PASSWORD") -}} + {{- $aibotPwd = index $existing.data "AIBOT_PASSWORD" | b64dec -}} +{{- else -}} + {{- $aibotPwd = randAlphaNum 24 -}} +{{- end -}} + +apiVersion: v1 +kind: Secret +metadata: + name: {{ $secretName }} + labels: + {{- include "huly.labels" . | nindent 4 }} +type: Opaque +data: + SERVER_SECRET: {{ $serverSecret | b64enc | quote }} + COCKROACH_PASSWORD: {{ $cockroachPwd | b64enc | quote }} + REDPANDA_SUPERUSER_PASSWORD: {{ $redpandaPwd | b64enc | quote }} + STORAGE_CONFIG: {{ $storageConfig | b64enc | quote }} + CR_DB_URL: {{ $crDbUrl | b64enc | quote }} + AIBOT_PASSWORD: {{ $aibotPwd | b64enc | quote }} + {{- if .Values.aibot.enabled }} + {{- if not .Values.secrets.openaiApiKey }} + {{- fail "secrets.openaiApiKey is required when aibot.enabled=true" }} + {{- end }} + OPENAI_API_KEY: {{ .Values.secrets.openaiApiKey | b64enc | quote }} + {{- with .Values.secrets.openaiBaseUrl }} + OPENAI_BASE_URL: {{ . | b64enc | quote }} + {{- end }} + {{- end }} + {{- with .Values.auth.google.clientId }} + GOOGLE_CLIENT_ID: {{ . | b64enc | quote }} + {{- end }} + {{- with .Values.auth.google.clientSecret }} + GOOGLE_CLIENT_SECRET: {{ . | b64enc | quote }} + {{- end }} + {{- with .Values.auth.github.clientId }} + GITHUB_CLIENT_ID: {{ . | b64enc | quote }} + {{- end }} + {{- with .Values.auth.github.clientSecret }} + GITHUB_CLIENT_SECRET: {{ . | b64enc | quote }} + {{- end }} + {{- with .Values.auth.oidc.clientId }} + OPENID_CLIENT_ID: {{ . | b64enc | quote }} + {{- end }} + {{- with .Values.auth.oidc.clientSecret }} + OPENID_CLIENT_SECRET: {{ . | b64enc | quote }} + {{- end }} + {{- with .Values.auth.oidc.issuer }} + OPENID_ISSUER: {{ . | b64enc | quote }} + {{- end }} + {{- if .Values.githubIntegration.enabled }} + GITHUB_APP_ID: {{ .Values.githubIntegration.appId | toString | b64enc | quote }} + GITHUB_APP_CLIENT_ID: {{ .Values.githubIntegration.clientId | b64enc | quote }} + GITHUB_APP_CLIENT_SECRET: {{ .Values.githubIntegration.clientSecret | b64enc | quote }} + GITHUB_APP_PRIVATE_KEY: {{ .Values.githubIntegration.privateKey | b64enc | quote }} + GITHUB_APP_WEBHOOK_SECRET: {{ .Values.githubIntegration.webhookSecret | b64enc | quote }} + {{- end }} diff --git a/helm/huly/templates/stats/deployment.yaml b/helm/huly/templates/stats/deployment.yaml new file mode 100644 index 0000000..8c57626 --- /dev/null +++ b/helm/huly/templates/stats/deployment.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: stats + labels: + {{- include "huly.labels" . | nindent 4 }} + app: stats +spec: + replicas: {{ .Values.stats.replicas }} + selector: + matchLabels: + app: stats + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: stats + spec: + {{- include "huly.scheduling" . | nindent 6 }} + containers: + - name: stats + image: {{ .Values.hulyRegistry }}/stats:{{ .Values.hulyVersion }} + ports: + - name: http + containerPort: 4900 + env: + - name: PORT + value: "4900" + {{- include "huly.envSecret" (dict "name" "SERVER_SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + readinessProbe: + tcpSocket: + port: 4900 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 4900 + initialDelaySeconds: 15 + periodSeconds: 30 + {{- with .Values.stats.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/helm/huly/templates/stats/service.yaml b/helm/huly/templates/stats/service.yaml new file mode 100644 index 0000000..29b4546 --- /dev/null +++ b/helm/huly/templates/stats/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: stats + labels: + {{- include "huly.labels" . | nindent 4 }} + app: stats +spec: + selector: + app: stats + ports: + - name: http + port: 4900 + targetPort: 4900 diff --git a/helm/huly/templates/transactor/deployment.yaml b/helm/huly/templates/transactor/deployment.yaml new file mode 100644 index 0000000..5a85c16 --- /dev/null +++ b/helm/huly/templates/transactor/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: transactor + labels: + {{- include "huly.labels" . | nindent 4 }} + app: transactor +spec: + replicas: {{ .Values.transactor.replicas }} + selector: + matchLabels: + app: transactor + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: transactor + spec: + {{- include "huly.scheduling" . | nindent 6 }} + initContainers: + {{- include "huly.waitForCockroach" . | nindent 8 }} + {{- include "huly.waitForRedpanda" . | nindent 8 }} + containers: + - name: transactor + image: {{ .Values.hulyRegistry }}/transactor:{{ .Values.hulyVersion }} + ports: + - name: http + containerPort: 3333 + env: + - name: SERVER_PORT + value: "3333" + {{- include "huly.envSecret" (dict "name" "SERVER_SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "DB_URL" "key" "CR_DB_URL" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "STORAGE_CONFIG" "key" "STORAGE_CONFIG" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "FRONT_URL" "key" "FRONT_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "ACCOUNTS_URL" "key" "ACCOUNTS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "FULLTEXT_URL" "key" "FULLTEXT_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "STATS_URL" "key" "STATS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "QUEUE_CONFIG" "key" "QUEUE_CONFIG" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "LAST_NAME_FIRST" "key" "LAST_NAME_FIRST" "root" .) | nindent 12 }} + {{- if .Values.aibot.enabled }} + {{- include "huly.envConfig" (dict "name" "AI_BOT_URL" "key" "AI_BOT_URL" "root" .) | nindent 12 }} + {{- end }} + - name: SERVER_CURSOR_MAXTIMEMS + value: "30000" + readinessProbe: + tcpSocket: + port: 3333 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 3333 + initialDelaySeconds: 30 + periodSeconds: 30 + {{- with .Values.transactor.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/helm/huly/templates/transactor/service.yaml b/helm/huly/templates/transactor/service.yaml new file mode 100644 index 0000000..1802510 --- /dev/null +++ b/helm/huly/templates/transactor/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: transactor + labels: + {{- include "huly.labels" . | nindent 4 }} + app: transactor +spec: + selector: + app: transactor + ports: + - name: http + port: 3333 + targetPort: 3333 diff --git a/helm/huly/templates/workspace/deployment.yaml b/helm/huly/templates/workspace/deployment.yaml new file mode 100644 index 0000000..a6a0d86 --- /dev/null +++ b/helm/huly/templates/workspace/deployment.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workspace + labels: + {{- include "huly.labels" . | nindent 4 }} + app: workspace +spec: + replicas: {{ .Values.workspace.replicas }} + selector: + matchLabels: + app: workspace + template: + metadata: + annotations: + {{- include "huly.checksumAnnotations" . | nindent 8 }} + labels: + {{- include "huly.labels" . | nindent 8 }} + app: workspace + spec: + {{- include "huly.scheduling" . | nindent 6 }} + initContainers: + {{- include "huly.waitForCockroach" . | nindent 8 }} + {{- include "huly.waitForRedpanda" . | nindent 8 }} + containers: + - name: workspace + image: {{ .Values.hulyRegistry }}/workspace:{{ .Values.hulyVersion }} + env: + {{- include "huly.envSecret" (dict "name" "SERVER_SECRET" "key" "SERVER_SECRET" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "DB_URL" "key" "CR_DB_URL" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "ACCOUNTS_DB_URL" "key" "CR_DB_URL" "root" .) | nindent 12 }} + {{- include "huly.envSecret" (dict "name" "STORAGE_CONFIG" "key" "STORAGE_CONFIG" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "ACCOUNTS_URL" "key" "ACCOUNTS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "STATS_URL" "key" "STATS_URL_INTERNAL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "TRANSACTOR_URL" "key" "TRANSACTOR_URL" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "MODEL_ENABLED" "key" "MODEL_ENABLED" "root" .) | nindent 12 }} + {{- include "huly.envConfig" (dict "name" "QUEUE_CONFIG" "key" "QUEUE_CONFIG" "root" .) | nindent 12 }} + {{- with .Values.workspace.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} diff --git a/helm/huly/values.yaml b/helm/huly/values.yaml new file mode 100644 index 0000000..9e770f1 --- /dev/null +++ b/helm/huly/values.yaml @@ -0,0 +1,292 @@ +# ────────────────────────────────────────────────────────────── +# REQUIRED — set this to your domain +# ────────────────────────────────────────────────────────────── +domain: huly.example + +# Override the chart name / full release name +nameOverride: "" +fullnameOverride: "" + +# ────────────────────────────────────────────────────────────── +# Global image tag for all Huly services +# ────────────────────────────────────────────────────────────── +hulyVersion: v0.7.382 + +# Docker registry prefix for Huly service images. +# Default "hardcoreeng" pulls from Docker Hub (hardcoreeng/front, etc.). +# Override to use a private registry, e.g. europe-west3-docker.pkg.dev/kendall-ledo/docker/huly +hulyRegistry: hardcoreeng + +# ────────────────────────────────────────────────────────────── +# Secrets — leave empty to auto-generate (persists across upgrades) +# ────────────────────────────────────────────────────────────── +secrets: + # Shared JWT signing secret (auto-generated if empty) + serverSecret: "" + # Full storage config string override. + # When empty, auto-derived from storage.type + storage.s3.* (or random MinIO creds). + # Format: s3|https://s3.example.com?accessKey=X&secretKey=Y®ion=us-east-1&rootBucket=data + # or: minio|minio?accessKey=&secretKey= + storageConfig: "" + # CockroachDB password (auto-generated if empty) + cockroachPassword: "" + # Redpanda superuser password (auto-generated if empty) + redpandaPassword: "" + # CockroachDB connection URL (auto-derived from cockroachPassword if empty) + # Set this when using an external database (cockroach.enabled=false). + # Format: postgres://:@:26257/ + crDbUrl: "" + # OpenAI API key (required when aibot.enabled=true) + openaiApiKey: "" + # OpenAI base URL override (optional, e.g. for Azure OpenAI or proxies) + openaiBaseUrl: "" + # AI bot password (auto-generated if empty) + aibotPassword: "" + +# ────────────────────────────────────────────────────────────── +# Authentication providers (set on the account service) +# At least one provider should be configured for user login. +# ────────────────────────────────────────────────────────────── +auth: + # Google OAuth 2.0 + google: + clientId: "" + clientSecret: "" + # GitHub OAuth + github: + clientId: "" + clientSecret: "" + # OpenID Connect (generic OIDC provider) + oidc: + clientId: "" + clientSecret: "" + issuer: "" # e.g. https://accounts.google.com + # Set to true to prevent new user registration + disableSignup: false + +# ────────────────────────────────────────────────────────────── +# Storage backend — built-in MinIO or external S3 +# ────────────────────────────────────────────────────────────── +storage: + # "minio" = use built-in MinIO (default) + # "s3" = use external S3-compatible storage (disables built-in MinIO) + type: minio + s3: + endpoint: "" # e.g. https://s3.amazonaws.com + region: "" # e.g. us-east-1 + accessKey: "" + secretKey: "" + # rootBucket: store all workspaces as prefixes inside a single bucket + rootBucket: "" # e.g. huly-data + # bucketPrefix: prefix prepended to per-workspace bucket names (only used if rootBucket is empty) + bucketPrefix: "" # e.g. huly- + +# ────────────────────────────────────────────────────────────── +# Ingress +# ────────────────────────────────────────────────────────────── +ingress: + enabled: true + className: nginx + # Extra annotations merged into every ingress resource + annotations: {} + tls: + enabled: true + clusterIssuer: letsencrypt-prod + +# ────────────────────────────────────────────────────────────── +# App settings (ConfigMap) +# ────────────────────────────────────────────────────────────── +appSettings: + modelEnabled: "*" + lastNameFirst: "true" + title: "Huly Self Host" + defaultLanguage: "en" + # Comma-separated list of admin email addresses + adminEmails: "" + # Desktop update channel + desktopChannel: "selfhost" + +# ────────────────────────────────────────────────────────────── +# Global pod settings (applied to all services) +# ────────────────────────────────────────────────────────────── +global: + nodeSelector: {} + tolerations: [] + affinity: {} + +# ────────────────────────────────────────────────────────────── +# Infrastructure — set enabled: false to use external services +# +# When disabled, provide connection details via: +# cockroach: secrets.crDbUrl +# redpanda: external.redpanda +# elastic: external.elastic +# minio: storage.type=s3 + storage.s3.* +# mongodb: external.mongodb (only when aibot.enabled=true) +# ────────────────────────────────────────────────────────────── +cockroach: + enabled: true + image: cockroachdb/cockroach:latest-v24.2 + database: defaultdb + username: selfhost + storage: 10Gi + storageClassName: "" + resources: {} + +redpanda: + enabled: true + image: docker.redpanda.com/redpandadata/redpanda:v24.3.6 + storage: 5Gi + storageClassName: "" + resources: + limits: + memory: 512Mi + +elastic: + enabled: true + image: elasticsearch:7.14.2 + storage: 10Gi + storageClassName: "" + javaOpts: "-Xms1024m -Xmx1024m" + resources: {} + +minio: + # Automatically disabled when storage.type=s3 + enabled: true + image: minio/minio + storage: 50Gi + storageClassName: "" + resources: {} + +mongodb: + # Built-in MongoDB — only deployed when aibot.enabled=true + enabled: true + image: mongo:7 + storage: 5Gi + storageClassName: "" + resources: {} + +# ────────────────────────────────────────────────────────────── +# External infrastructure endpoints +# Used when the corresponding built-in service is disabled. +# ────────────────────────────────────────────────────────────── +external: + # Used when redpanda.enabled=false + redpanda: "" # e.g. kafka.example.com:9092 + # Used when elastic.enabled=false + elastic: "" # e.g. https://elasticsearch.example.com:9200 + # Used when mongodb.enabled=false (requires aibot.enabled=true) + mongodb: "" # e.g. mongodb://user:pass@host:27017 + +# ────────────────────────────────────────────────────────────── +# Application services +# ────────────────────────────────────────────────────────────── +front: + replicas: 1 + resources: {} + +account: + replicas: 1 + resources: + limits: + memory: 512Mi + +transactor: + replicas: 1 + resources: {} + +collaborator: + replicas: 1 + resources: + limits: + memory: 512Mi + +workspace: + replicas: 1 + resources: + limits: + memory: 512Mi + +fulltext: + replicas: 1 + resources: + limits: + memory: 512Mi + +rekoni: + replicas: 1 + resources: + limits: + memory: 500Mi + +stats: + replicas: 1 + resources: + limits: + memory: 500Mi + +kvs: + enabled: true + replicas: 1 + resources: {} + +githubIntegration: + # GitHub App integration — syncs issues, PRs, and comments with GitHub + enabled: false + replicas: 1 + # Bot name shown on GitHub (must match the GitHub App's slug + [bot]) + botName: "" + # GitHub App credentials (stored in the shared secret) + appId: "" + clientId: "" + clientSecret: "" + privateKey: "" # PEM-encoded private key (use multiline YAML |) + webhookSecret: "" + resources: + limits: + memory: 512Mi + +aibot: + # AI bot service — requires OpenAI API key + enabled: false + replicas: 1 + firstName: "Huly" + lastName: "AI" + # Override default models (leave empty for upstream defaults) + openaiModel: "" + openaiEmbeddingModel: "" + openaiTranslateModel: "" + openaiSummaryModel: "" + resources: + limits: + memory: 512Mi + +# ────────────────────────────────────────────────────────────── +# Backup — nightly CronJobs to S3-compatible storage +# ────────────────────────────────────────────────────────────── +backup: + enabled: false + schedule: "0 2 * * *" # 2 AM daily + retentionDays: 30 + rcloneImage: rclone/rclone:latest + s3: + endpoint: "" # e.g. https://nbg1.your-objectstorage.com + region: "" + bucket: "" # e.g. ledo-backups + pathPrefix: "" # e.g. huly-ledo + accessKey: "" + secretKey: "" + # Secondary credentials for rotation (optional) + secondaryAccessKey: "" + secondarySecretKey: "" + # Which credential set is active: "primary" or "secondary" + activeCredential: "primary" + cockroachdb: + enabled: true + schedule: "" # override global schedule + files: + enabled: true + schedule: "" + mongodb: + enabled: true # only runs when aibot.enabled=true + schedule: ""