From afd54079f26118f8d08ffed91839544ba0e0f13a Mon Sep 17 00:00:00 2001 From: ihaardik Date: Mon, 6 Apr 2026 20:23:13 +0530 Subject: [PATCH] Add migration app scaffolding for ArgoCD --- argocd/applicationsets/05-migration.yaml | 52 ++ .../applicationsets/05-migration.yaml | 55 +++ argocd/customers/reference/migration.yaml | 11 + charts/countly-migration/Chart.yaml | 29 ++ charts/countly-migration/README.md | 417 ++++++++++++++++ .../examples/argocd-application.yaml | 32 ++ .../examples/values-development.yaml | 23 + .../examples/values-multipod.yaml | 37 ++ .../examples/values-production.yaml | 70 +++ charts/countly-migration/templates/NOTES.txt | 87 ++++ .../countly-migration/templates/_helpers.tpl | 149 ++++++ .../templates/configmap.yaml | 28 ++ .../templates/deployment.yaml | 117 +++++ .../templates/external-secret.yaml | 33 ++ .../countly-migration/templates/ingress.yaml | 41 ++ .../templates/networkpolicy.yaml | 27 + charts/countly-migration/templates/pdb.yaml | 13 + .../countly-migration/templates/secret.yaml | 44 ++ .../countly-migration/templates/service.yaml | 22 + .../templates/serviceaccount.yaml | 16 + .../templates/servicemonitor.yaml | 21 + .../templates/tests/test-health.yaml | 16 + charts/countly-migration/values.schema.json | 229 +++++++++ charts/countly-migration/values.yaml | 284 +++++++++++ charts/noop/Chart.yaml | 6 + .../reference/credentials-migration.yaml | 40 ++ environments/reference/migration.yaml | 63 +++ scripts/new-argocd-customer.sh | 467 ++++++++++++++++++ 28 files changed, 2429 insertions(+) create mode 100644 argocd/applicationsets/05-migration.yaml create mode 100644 argocd/countly-hosted/applicationsets/05-migration.yaml create mode 100644 argocd/customers/reference/migration.yaml create mode 100644 charts/countly-migration/Chart.yaml create mode 100644 charts/countly-migration/README.md create mode 100644 charts/countly-migration/examples/argocd-application.yaml create mode 100644 charts/countly-migration/examples/values-development.yaml create mode 100644 charts/countly-migration/examples/values-multipod.yaml create mode 100644 charts/countly-migration/examples/values-production.yaml create mode 100644 charts/countly-migration/templates/NOTES.txt create mode 100644 charts/countly-migration/templates/_helpers.tpl create mode 100644 charts/countly-migration/templates/configmap.yaml create mode 100644 charts/countly-migration/templates/deployment.yaml create mode 100644 charts/countly-migration/templates/external-secret.yaml create mode 100644 charts/countly-migration/templates/ingress.yaml create mode 100644 charts/countly-migration/templates/networkpolicy.yaml create mode 100644 charts/countly-migration/templates/pdb.yaml create mode 100644 charts/countly-migration/templates/secret.yaml create mode 100644 charts/countly-migration/templates/service.yaml create mode 100644 charts/countly-migration/templates/serviceaccount.yaml create mode 100644 charts/countly-migration/templates/servicemonitor.yaml create mode 100644 charts/countly-migration/templates/tests/test-health.yaml create mode 100644 charts/countly-migration/values.schema.json create mode 100644 charts/countly-migration/values.yaml create mode 100644 charts/noop/Chart.yaml create mode 100644 environments/reference/credentials-migration.yaml create mode 100644 environments/reference/migration.yaml create mode 100755 scripts/new-argocd-customer.sh diff --git a/argocd/applicationsets/05-migration.yaml b/argocd/applicationsets/05-migration.yaml new file mode 100644 index 0000000..2198388 --- /dev/null +++ b/argocd/applicationsets/05-migration.yaml @@ -0,0 +1,52 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: migration + namespace: argocd +spec: + goTemplate: true + goTemplateOptions: + - missingkey=error + generators: + - git: + repoURL: https://github.com/Countly/helm.git + revision: main + files: + - path: argocd/customers/migration/*.yaml + template: + metadata: + name: "{{ .customer }}-migration" + annotations: + argocd.argoproj.io/sync-wave: "20" + spec: + project: "{{ .project }}" + source: + repoURL: https://github.com/Countly/helm.git + targetRevision: main + path: charts/countly-migration + helm: + releaseName: countly-migration + valueFiles: + - "../../environments/{{ .environment }}/global.yaml" + - "../../environments/{{ .environment }}/migration.yaml" + - "../../environments/{{ .environment }}/credentials-migration.yaml" + parameters: + - name: argocd.enabled + value: "true" + destination: + server: "{{ .server }}" + namespace: countly-migration + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - RespectIgnoreDifferences=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m diff --git a/argocd/countly-hosted/applicationsets/05-migration.yaml b/argocd/countly-hosted/applicationsets/05-migration.yaml new file mode 100644 index 0000000..176e749 --- /dev/null +++ b/argocd/countly-hosted/applicationsets/05-migration.yaml @@ -0,0 +1,55 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: countly-migration + namespace: argocd +spec: + goTemplate: true + goTemplateOptions: + - missingkey=error + generators: + - git: + repoURL: https://github.com/Countly/countly-deployment.git + revision: main + files: + - path: customers/migration/*.yaml + template: + metadata: + name: "{{ .customer }}-migration" + annotations: + argocd.argoproj.io/sync-wave: "20" + spec: + project: "{{ .project }}" + sources: + - repoURL: https://github.com/Countly/helm.git + targetRevision: main + path: charts/countly-migration + helm: + releaseName: countly-migration + valueFiles: + - "$values/environments/{{ .environment }}/global.yaml" + - "$values/environments/{{ .environment }}/migration.yaml" + - "$values/environments/{{ .environment }}/credentials-migration.yaml" + parameters: + - name: argocd.enabled + value: "true" + - repoURL: https://github.com/Countly/countly-deployment.git + targetRevision: main + ref: values + destination: + server: "{{ .server }}" + namespace: countly-migration + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - RespectIgnoreDifferences=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m diff --git a/argocd/customers/reference/migration.yaml b/argocd/customers/reference/migration.yaml new file mode 100644 index 0000000..4024a41 --- /dev/null +++ b/argocd/customers/reference/migration.yaml @@ -0,0 +1,11 @@ +# Copy this file to: +# argocd/customers/migration/.yaml +# +# Create that file only when the migration app should be enabled for a customer. +# The values should usually match the base customer metadata in: +# argocd/customers/.yaml + +customer: example-customer +environment: example-customer +project: countly-customers +server: https://kubernetes.default.svc diff --git a/charts/countly-migration/Chart.yaml b/charts/countly-migration/Chart.yaml new file mode 100644 index 0000000..3632797 --- /dev/null +++ b/charts/countly-migration/Chart.yaml @@ -0,0 +1,29 @@ +apiVersion: v2 +name: countly-migration +description: MongoDB to ClickHouse batch migration service for Countly drill events +type: application +version: 0.1.0 +appVersion: "1.0.0" +home: https://countly.com +icon: https://count.ly/images/logos/countly-logo.svg +sources: + - https://github.com/Countly/countly-server +keywords: + - migration + - clickhouse + - mongodb + - countly + - batch-migration +maintainers: + - name: Countly + url: https://countly.com +dependencies: + - name: redis + version: ">=25.0.0" + repository: https://charts.bitnami.com/bitnami + condition: redis.enabled +annotations: + artifacthub.io/license: AGPL-3.0 + artifacthub.io/links: | + - name: Documentation + url: https://github.com/Countly/helm diff --git a/charts/countly-migration/README.md b/charts/countly-migration/README.md new file mode 100644 index 0000000..f88b56e --- /dev/null +++ b/charts/countly-migration/README.md @@ -0,0 +1,417 @@ +# Countly Migration Helm Chart + +Deploys the MongoDB-to-ClickHouse batch migration service for Countly drill events. Reads `drill_events*` collections from MongoDB, transforms documents, and inserts them into the ClickHouse `drill_events` table. Includes a bundled Redis instance for migration state tracking. + +**Chart version:** 0.1.0 +**App version:** 1.0.0 + +--- + +## Architecture + +```mermaid +flowchart LR + subgraph source["Source"] + mongo["MongoDB\ncountly_drill.drill_events*"] + end + + subgraph migration["countly-migration namespace"] + svc["Migration Service\n:8080"] + redis["Redis\n:6379"] + end + + subgraph target["Target"] + ch["ClickHouse\ncountly_drill.drill_events"] + end + + mongo -->|read batches| svc + svc -->|insert rows| ch + svc <-->|hot state, bitmaps,\nerror buffers| redis + svc -->|run manifests| mongo +``` + +The migration service is a **singleton Deployment** with `Recreate` strategy. It processes collections sequentially in batches, with full crash recovery and resume support. State is stored in MongoDB (run manifests) and Redis (hot state, processed document bitmaps, error buffers). + +--- + +## Quick Start + +**Alongside sibling charts** (default — auto-discovers MongoDB and ClickHouse via DNS): + +```bash +helm install countly-migration ./charts/countly-migration \ + -n countly-migration --create-namespace \ + --set backingServices.mongodb.password="YOUR_MONGODB_APP_PASSWORD" \ + --set backingServices.clickhouse.password="YOUR_CLICKHOUSE_PASSWORD" +``` + +Only two values required. Everything else is auto-detected from the sibling `countly-mongodb` and `countly-clickhouse` charts. Redis is bundled automatically. + +**Standalone** (external MongoDB and ClickHouse): + +```bash +helm install countly-migration ./charts/countly-migration \ + -n countly-migration --create-namespace \ + --set backingServices.mongodb.mode=external \ + --set backingServices.mongodb.uri="mongodb://app:PASSWORD@mongodb-host:27017/admin?replicaSet=rs0&ssl=false" \ + --set backingServices.clickhouse.mode=external \ + --set backingServices.clickhouse.url="http://clickhouse-host:8123" \ + --set backingServices.clickhouse.password="PASSWORD" +``` + +> **Production deployment:** Use the profile-based approach from the [root README](../../README.md#manual-installation-without-helmfile) instead of `--set` flags. + +The migration service auto-discovers all `drill_events*` collections in the source MongoDB database and begins migrating. + +--- + +## Prerequisites + +- **MongoDB** — Source database with `drill_events*` collections in `countly_drill` database +- **ClickHouse** — Target with `drill_events` table in `countly_drill` database +- **Redis** — Bundled by default (Bitnami subchart), or provide an external URL + +If deploying alongside other Countly charts, MongoDB and ClickHouse are already available via their respective namespaces. + +--- + +## Configuration + +### Backing Services + +The chart connects to MongoDB, ClickHouse, and Redis. Each can be configured in **bundled** or **external** mode. + +#### MongoDB + +| Mode | Description | +|------|-------------| +| `bundled` (default) | Auto-constructs URI from sibling `countly-mongodb` chart using in-cluster DNS | +| `external` | Provide a full connection URI via `backingServices.mongodb.uri` | + +```yaml +# Bundled mode (default — alongside countly-mongodb chart) +backingServices: + mongodb: + password: "app-user-password" # Only required field + +# External mode +backingServices: + mongodb: + mode: external + uri: "mongodb://app:pass@host:27017/admin?replicaSet=rs0&ssl=false" +``` + +In bundled mode, the chart constructs the URI as: +`mongodb://app:{password}@{releaseName}-mongodb-svc.{namespace}.svc.cluster.local:27017/admin?replicaSet={releaseName}-mongodb` + +Override `releaseName` if your sibling charts use a non-standard prefix (default: `"countly"`). + +#### ClickHouse + +| Mode | Description | +|------|-------------| +| `bundled` (default) | Auto-constructs URL from sibling `countly-clickhouse` chart using in-cluster DNS | +| `external` | Provide a full HTTP URL via `backingServices.clickhouse.url` | + +```yaml +# Bundled mode (default — alongside countly-clickhouse chart) +backingServices: + clickhouse: + password: "default-password" # Only required field + +# External mode +backingServices: + clickhouse: + mode: external + url: "http://clickhouse-host:8123" + password: "default-password" +``` + +In bundled mode, the chart constructs the URL as: +`http://{releaseName}-clickhouse-clickhouse-headless.{namespace}.svc:8123` + +#### Redis + +Redis is **enabled by default** as a Bitnami subchart with AOF persistence. + +```yaml +# Default: bundled Redis (already enabled) +redis: + enabled: true + +# External Redis: disable subchart and provide URL +redis: + enabled: false +backingServices: + redis: + url: "redis://my-external-redis:6379" +``` + +### Redis Configuration + +The bundled Redis defaults: + +| Setting | Default | Description | +|---------|---------|-------------| +| `redis.architecture` | `standalone` | Single-node Redis | +| `redis.auth.enabled` | `false` | No password (internal cluster traffic) | +| `redis.master.persistence.enabled` | `true` | Persistent volume for data | +| `redis.master.persistence.size` | `8Gi` | PVC size | +| `redis.commonConfiguration` | AOF + RDB | `appendonly yes`, `appendfsync everysec`, RDB snapshots | +| `redis.master.resources.requests.cpu` | `500m` | CPU request | +| `redis.master.resources.requests.memory` | `2Gi` | Memory request | +| `redis.master.resources.limits.cpu` | `1` | CPU limit | +| `redis.master.resources.limits.memory` | `2Gi` | Memory limit | + +To disable persistence (dev/test only): + +```yaml +redis: + master: + persistence: + enabled: false +``` + +### Secrets + +Three modes for managing credentials: + +| Mode | Description | Use Case | +|------|-------------|----------| +| `values` (default) | Secret created from Helm values | Development, testing | +| `existingSecret` | Reference a pre-created Kubernetes Secret | Production with manual secret management | +| `externalSecret` | External Secrets Operator (AWS SM, Azure KV) | Production with vault integration | + +The Secret must contain these keys: `MONGO_URI`, `CLICKHOUSE_URL`, `CLICKHOUSE_PASSWORD`, `REDIS_URL`. + +```yaml +# Production: use pre-created secret +secrets: + mode: existingSecret + existingSecret: + name: countly-migration-secrets +``` + +### Migration Config + +Key environment variables (set via `config.*`): + +| Variable | Default | Description | +|----------|---------|-------------| +| `RERUN_MODE` | `resume` | `resume` (crash recovery), `new-run`, `clone-run` | +| `LOG_LEVEL` | `info` | `fatal`, `error`, `warn`, `info`, `debug`, `trace` | +| `MONGO_DB` | `countly_drill` | Source MongoDB database | +| `MONGO_COLLECTION_PREFIX` | `drill_events` | Collection name prefix to discover | +| `MONGO_BATCH_ROWS_TARGET` | `10000` | Documents per batch | +| `CLICKHOUSE_DB` | `countly_drill` | Target ClickHouse database | +| `CLICKHOUSE_TABLE` | `drill_events` | Target table | +| `CLICKHOUSE_USE_DEDUP_TOKEN` | `true` | Deduplication on insert | +| `BACKPRESSURE_ENABLED` | `true` | Monitor ClickHouse compaction pressure | +| `GC_ENABLED` | `true` | Automatic garbage collection | +| `GC_RSS_SOFT_LIMIT_MB` | `1536` | Trigger GC at this RSS | +| `GC_RSS_HARD_LIMIT_MB` | `2048` | Force exit at this RSS | + +### ArgoCD Integration + +Enable sync-wave annotations and external progress link: + +```yaml +argocd: + enabled: true + +externalLink: + enabled: true + url: "https://migration.example.internal/runs/current" +``` + +Sync-wave ordering (within this chart): +- Wave 0: Redis subchart resources, ConfigMap +- Wave 1: Secret +- Wave 10: Deployment, Service, Ingress, ServiceMonitor + +At the **stack level** (in `countly-argocd`), migration deploys **last** at wave 20 — after all other charts (databases, Kafka, Countly, observability) are healthy. This ensures the full system is stable before migration begins. + +Namespace is created by the ArgoCD Application (`CreateNamespace=true`), not by the chart. + +--- + +## Endpoints + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `/healthz` | Liveness probe — always returns 200 | +| GET | `/readyz` | Readiness probe — checks MongoDB, ClickHouse, Redis, ManifestStore, BatchRunner | +| GET | `/stats` | Comprehensive JSON stats (throughput, integrity, memory, backpressure) | +| GET | `/runs/current` | Current active run details | +| GET | `/runs` | Paginated list of all runs (`?status=active\|completed\|failed&limit=20`) | +| GET | `/runs/:id` | Single run details | +| GET | `/runs/:id/batches` | Batches for a run (`?status=done\|failed&limit=50`) | +| GET | `/runs/:id/failures` | Failure analysis (errors, mismatches, retries) | +| GET | `/runs/:id/timeline` | Historical timeline snapshots | +| GET | `/runs/:id/coverage` | Coverage percentage and batch counts | +| POST | `/control/pause` | Pause after current batch | +| POST | `/control/resume` | Resume processing | +| POST | `/control/stop-after-batch` | Graceful stop | +| POST | `/control/gc` | Trigger garbage collection (`{"mode":"now\|after-batch\|force"}`) | +| DELETE | `/runs/:id/cache` | Cleanup Redis cache for a completed run | + +--- + +## Verifying the Deployment + +### 1. Check pods are running + +```bash +kubectl get pods -n countly-migration +``` + +Expected: migration pod `1/1 Running`, redis-master pod `1/1 Running`. + +### 2. Check health + +```bash +kubectl exec -n countly-migration deploy/-countly-migration -- \ + node -e "fetch('http://localhost:8080/healthz').then(r=>r.text()).then(console.log)" +``` + +Expected: `{"status":"alive"}` + +### 3. Check readiness + +```bash +kubectl exec -n countly-migration deploy/-countly-migration -- \ + node -e "fetch('http://localhost:8080/readyz').then(r=>r.text()).then(console.log)" +``` + +Expected: `{"ready":true,"checks":{"mongo":true,"clickhouse":true,"redis":true,"manifestStore":true,"batchRunner":true}}` + +### 4. Check migration stats + +```bash +kubectl exec -n countly-migration deploy/-countly-migration -- \ + node -e "fetch('http://localhost:8080/stats').then(r=>r.json()).then(d=>console.log(JSON.stringify(d,null,2)))" +``` + +### 5. Check run status + +```bash +kubectl exec -n countly-migration deploy/-countly-migration -- \ + node -e "fetch('http://localhost:8080/runs?limit=5').then(r=>r.text()).then(console.log)" +``` + +### 6. Verify data in ClickHouse + +```bash +kubectl exec -n clickhouse -- \ + clickhouse-client --password \ + --query "SELECT count() FROM countly_drill.drill_events" +``` + +### 7. Port-forward for browser access + +```bash +kubectl port-forward -n countly-migration svc/-countly-migration 8080:8080 +# Then open: http://localhost:8080/stats +``` + +--- + +## Operations + +### Pause / Resume + +```bash +# Pause after current batch completes +kubectl exec -n countly-migration deploy/-countly-migration -- \ + node -e "fetch('http://localhost:8080/control/pause',{method:'POST'}).then(r=>r.text()).then(console.log)" + +# Resume +kubectl exec -n countly-migration deploy/-countly-migration -- \ + node -e "fetch('http://localhost:8080/control/resume',{method:'POST'}).then(r=>r.text()).then(console.log)" +``` + +### Check failures + +```bash +kubectl exec -n countly-migration deploy/-countly-migration -- \ + node -e "fetch('http://localhost:8080/runs/RUNID/failures').then(r=>r.text()).then(console.log)" +``` + +### Cleanup Redis cache after a completed run + +```bash +kubectl exec -n countly-migration deploy/-countly-migration -- \ + node -e "fetch('http://localhost:8080/runs/RUNID/cache',{method:'DELETE'}).then(r=>r.text()).then(console.log)" +``` + +### View logs + +```bash +kubectl logs -n countly-migration -l app.kubernetes.io/name=countly-migration -f +``` + +--- + +## Multi-Pod Mode + +Scale the migration across multiple pods for faster throughput. Pods coordinate via Redis-based collection locking and range splitting. + +```yaml +deployment: + replicas: 3 + strategy: + type: RollingUpdate + +pdb: + enabled: true + minAvailable: 1 +``` + +When `replicas > 1`, the chart automatically: +- Switches to `RollingUpdate` strategy support +- Adds pod anti-affinity (spread across nodes) +- Injects `POD_ID` from pod name for coordination +- Configures preStop drain hook + +### Worker settings + +| Value | Default | Description | +|-------|---------|-------------| +| `worker.enabled` | `true` | Enable multi-pod coordination | +| `worker.lockTtlSec` | `300` | Collection lock TTL (seconds) | +| `worker.lockRenewMs` | `60000` | Lock renewal interval (ms) | +| `worker.podHeartbeatMs` | `30000` | Heartbeat interval (ms) | +| `worker.podDeadAfterSec` | `180` | Dead pod threshold (seconds) | +| `worker.rangeParallelThreshold` | `500000` | Doc count to trigger range splitting | +| `worker.rangeCount` | `100` | Time ranges per collection | +| `worker.rangeLeaseTtlSec` | `300` | Range lease TTL (seconds) | + +For a comprehensive guide on multi-pod operations, scaling, and troubleshooting, see [docs/migration-guide.md](../../docs/migration-guide.md#multi-pod-mode). + +--- + +## Schema Guardrails + +The chart includes `values.schema.json` that enforces: + +- **`deployment.replicas`** must be `>= 1` — set to 1 for single-pod, or higher for multi-pod +- **`deployment.strategy.type`** must be `Recreate` or `RollingUpdate` — use `RollingUpdate` for multi-pod +- **`secrets.mode`** must be one of: `values`, `existingSecret`, `externalSecret` +- **Worker settings** have minimum value constraints (e.g., `lockTtlSec >= 30`, `podHeartbeatMs >= 1000`) + +--- + +## Examples + +See the `examples/` directory: + +- **`values-development.yaml`** — Minimal development setup with bundled backing services +- **`values-production.yaml`** — Production setup with `existingSecret` mode +- **`values-multipod.yaml`** — Multi-pod setup with 3 replicas, RollingUpdate, PDB +- **`argocd-application.yaml`** — ArgoCD Application manifest with `CreateNamespace=true` + +--- + +## Full Documentation + +For architecture details, configuration reference, operations playbook, API reference, and troubleshooting, see [docs/migration-guide.md](../../docs/migration-guide.md). diff --git a/charts/countly-migration/examples/argocd-application.yaml b/charts/countly-migration/examples/argocd-application.yaml new file mode 100644 index 0000000..6b36665 --- /dev/null +++ b/charts/countly-migration/examples/argocd-application.yaml @@ -0,0 +1,32 @@ +# Example ArgoCD Application for countly-migration. +# Namespace is created by ArgoCD (CreateNamespace=true), not by the chart. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: countly-migration + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/Countly/helm.git + targetRevision: main + path: charts/countly-migration + helm: + releaseName: countly-migration + valueFiles: + - ../../environments/prod/migration.yaml + parameters: + - name: argocd.enabled + value: "true" + destination: + server: https://kubernetes.default.svc + namespace: countly-migration + syncPolicy: + syncOptions: + - CreateNamespace=true + - ApplyOutOfSyncOnly=true + managedNamespaceMetadata: + labels: + app.kubernetes.io/part-of: countly + annotations: + owner: data-platform diff --git a/charts/countly-migration/examples/values-development.yaml b/charts/countly-migration/examples/values-development.yaml new file mode 100644 index 0000000..aae85bf --- /dev/null +++ b/charts/countly-migration/examples/values-development.yaml @@ -0,0 +1,23 @@ +# Development values example for countly-migration. +# Bundled mode (default) — auto-discovers sibling chart endpoints via DNS. +# Only passwords are required; everything else uses sane defaults. + +# Backing service passwords (must match sibling charts) +backingServices: + mongodb: + password: "devpassword" + clickhouse: + password: "devpassword" + +# Redis is bundled automatically — no config needed. + +config: + LOG_LEVEL: "debug" + +resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "1" + memory: "2Gi" diff --git a/charts/countly-migration/examples/values-multipod.yaml b/charts/countly-migration/examples/values-multipod.yaml new file mode 100644 index 0000000..b930935 --- /dev/null +++ b/charts/countly-migration/examples/values-multipod.yaml @@ -0,0 +1,37 @@ +# Multi-pod values example for countly-migration. +# Runs 3 replicas with Redis-based collection locking and range splitting. + +deployment: + replicas: 3 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + +pdb: + enabled: true + minAvailable: 1 + +# Backing service passwords (must match sibling charts) +backingServices: + mongodb: + password: "your-mongodb-password" + clickhouse: + password: "your-clickhouse-password" + +# Worker coordination settings (defaults are sane for most deployments) +worker: + enabled: true + lockTtlSec: 300 + podDeadAfterSec: 180 + rangeParallelThreshold: 500000 + rangeCount: 100 + +resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "2" + memory: "3Gi" diff --git a/charts/countly-migration/examples/values-production.yaml b/charts/countly-migration/examples/values-production.yaml new file mode 100644 index 0000000..9f97e00 --- /dev/null +++ b/charts/countly-migration/examples/values-production.yaml @@ -0,0 +1,70 @@ +# Production values example for countly-migration. +# Uses existingSecret mode — credentials are pre-created or managed externally. + +image: + repository: registry.example.com/countly/countly-migration + tag: "1.0.0" + +argocd: + enabled: true + +deployment: + replicas: 1 + strategy: + type: Recreate + terminationGracePeriodSeconds: 90 + +# Uncomment for multi-pod mode: +# deployment: +# replicas: 3 +# strategy: +# type: RollingUpdate +# pdb: +# enabled: true +# minAvailable: 1 + +service: + port: 8080 + +ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8,192.168.0.0/16" + argocd.argoproj.io/ignore-default-links: "true" + hosts: + - host: migration.example.internal + paths: + - path: / + pathType: Prefix + +externalLink: + enabled: true + url: "https://migration.example.internal/runs/current" + +# Reference pre-created secret containing MONGO_URI, CLICKHOUSE_URL, CLICKHOUSE_PASSWORD, REDIS_URL +secrets: + mode: existingSecret + keep: true + existingSecret: + name: countly-migration-secrets + +config: + SERVICE_NAME: countly-migration + SERVICE_PORT: "8080" + RERUN_MODE: "resume" + LOG_LEVEL: "info" + MONGO_DB: "countly_drill" + MONGO_COLLECTION_PREFIX: "drill_events" + CLICKHOUSE_DB: "countly_drill" + CLICKHOUSE_TABLE: "drill_events" + MANIFEST_DB: "countly_drill" + REDIS_KEY_PREFIX: "mig" + +resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "2" + memory: "3Gi" diff --git a/charts/countly-migration/templates/NOTES.txt b/charts/countly-migration/templates/NOTES.txt new file mode 100644 index 0000000..0354751 --- /dev/null +++ b/charts/countly-migration/templates/NOTES.txt @@ -0,0 +1,87 @@ +Countly Migration service has been deployed! + +=== Service Endpoints === + + Health: GET /healthz (liveness probe) + Ready: GET /readyz (readiness — checks mongo, clickhouse, redis) + Stats: GET /stats (throughput, memory, backpressure) + Runs: GET /runs/current (active run details) + Control: POST /control/{pause,resume,stop-after-batch} + +=== Access === +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }} +{{- end }} +{{- else }} + + kubectl port-forward svc/{{ include "countly-migration.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} -n {{ .Release.Namespace }} + # Then open: http://localhost:{{ .Values.service.port }}/stats +{{- end }} +{{- if and .Values.externalLink.enabled .Values.externalLink.url }} + + ArgoCD progress link: {{ .Values.externalLink.url }} +{{- end }} + +=== Redis === +{{- if .Values.redis.enabled }} + + Bundled Redis (Bitnami subchart): + URL: redis://{{ include "countly-migration.fullname" . }}-redis-master:6379 + Persistence: {{ if .Values.redis.master.persistence.enabled }}enabled ({{ .Values.redis.master.persistence.size }}){{ else }}disabled{{ end }} + Auth: {{ if .Values.redis.auth.enabled }}enabled{{ else }}disabled{{ end }} +{{- else if .Values.backingServices.redis.url }} + + External Redis: {{ .Values.backingServices.redis.url }} +{{- else }} + + WARNING: No Redis configured. The migration service requires Redis for state tracking. + Either enable the bundled Redis (redis.enabled=true) or provide backingServices.redis.url. +{{- end }} + +=== Secrets === + + Mode: {{ .Values.secrets.mode }} +{{- if eq .Values.secrets.mode "existingSecret" }} + Secret: {{ .Values.secrets.existingSecret.name }} +{{- end }} +{{- if eq .Values.secrets.mode "externalSecret" }} + External Secrets Operator will provision the secret. +{{- end }} + +=== Verify Deployment === + + 1. Check pods: + kubectl get pods -n {{ .Release.Namespace }} + + 2. Check health: + kubectl exec -n {{ .Release.Namespace }} deploy/{{ include "countly-migration.fullname" . }} -- \ + node -e "fetch('http://localhost:{{ .Values.service.port }}/healthz').then(r=>r.text()).then(console.log)" + + 3. Check readiness (all backing services connected): + kubectl exec -n {{ .Release.Namespace }} deploy/{{ include "countly-migration.fullname" . }} -- \ + node -e "fetch('http://localhost:{{ .Values.service.port }}/readyz').then(r=>r.text()).then(console.log)" + + 4. Check migration progress: + kubectl exec -n {{ .Release.Namespace }} deploy/{{ include "countly-migration.fullname" . }} -- \ + node -e "fetch('http://localhost:{{ .Values.service.port }}/runs?limit=5').then(r=>r.text()).then(console.log)" + + 5. View logs: + kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/name=countly-migration -f +{{- if gt (int .Values.deployment.replicas) 1 }} + +=== Multi-Pod Mode ({{ .Values.deployment.replicas }} replicas) === + + Global control (affects all pods): + POST /control/global/pause Pause all pods + POST /control/global/resume Resume all pods + POST /control/global/stop Stop all pods + + Coordination: + GET /control/locks List collection locks + GET /control/pods List all pods and status + POST /control/drain Graceful drain (preStop calls this) + + Scale up/down: + kubectl scale deploy/{{ include "countly-migration.fullname" . }} -n {{ .Release.Namespace }} --replicas=N +{{- end }} diff --git a/charts/countly-migration/templates/_helpers.tpl b/charts/countly-migration/templates/_helpers.tpl new file mode 100644 index 0000000..8c3da74 --- /dev/null +++ b/charts/countly-migration/templates/_helpers.tpl @@ -0,0 +1,149 @@ +{{/* +Whether multi-pod mode is active (replicas > 1). +*/}} +{{- define "countly-migration.isMultiPod" -}} +{{- if gt (int .Values.deployment.replicas) 1 }}true{{- end }} +{{- end }} + +{{/* +Expand the name of the chart. +*/}} +{{- define "countly-migration.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "countly-migration.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 }} + +{{/* +Common labels +*/}} +{{- define "countly-migration.labels" -}} +helm.sh/chart: {{ include "countly-migration.chart" . }} +{{ include "countly-migration.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Chart label +*/}} +{{- define "countly-migration.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "countly-migration.selectorLabels" -}} +app.kubernetes.io/name: {{ include "countly-migration.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Service account name +*/}} +{{- define "countly-migration.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "countly-migration.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +ArgoCD sync-wave annotation (only when argocd.enabled). +Usage: include "countly-migration.syncWave" (dict "wave" "0" "root" .) +*/}} +{{- define "countly-migration.syncWave" -}} +{{- if ((.root.Values.argocd).enabled) }} +argocd.argoproj.io/sync-wave: {{ .wave | quote }} +{{- end }} +{{- end -}} + +{{/* +Secret name resolution across three modes. +*/}} +{{- define "countly-migration.secretName" -}} +{{- if eq (.Values.secrets.mode | default "values") "existingSecret" }} +{{- required "secrets.existingSecret.name is required when secrets.mode=existingSecret" .Values.secrets.existingSecret.name }} +{{- else }} +{{- include "countly-migration.fullname" . }} +{{- end }} +{{- end }} + +{{/* +MongoDB URI computation. +External mode: use provided URI directly. +Bundled mode: construct from sibling countly-mongodb chart DNS. +*/}} +{{- define "countly-migration.mongoUri" -}} +{{- $bs := .Values.backingServices.mongodb -}} +{{- if eq ($bs.mode | default "bundled") "external" -}} +{{- required "backingServices.mongodb.uri is required when mode=external" $bs.uri -}} +{{- else -}} +{{- $prefix := $bs.releaseName | default "countly" -}} +{{- $host := $bs.host | default (printf "%s-mongodb-svc.%s.svc.cluster.local" $prefix ($bs.namespace | default "mongodb")) -}} +{{- $port := $bs.port | default "27017" -}} +{{- $user := $bs.username | default "app" -}} +{{- $pass := $bs.password | default "" -}} +{{- $db := $bs.database | default "admin" -}} +{{- $rs := $bs.replicaSet | default (printf "%s-mongodb" $prefix) -}} +mongodb://{{ $user }}:{{ $pass }}@{{ $host }}:{{ $port }}/{{ $db }}?replicaSet={{ $rs }}&ssl=false +{{- end -}} +{{- end -}} + +{{/* +ClickHouse URL computation. +External mode: use provided URL directly. +Bundled mode: construct from sibling countly-clickhouse chart DNS. +*/}} +{{- define "countly-migration.clickhouseUrl" -}} +{{- $bs := .Values.backingServices.clickhouse -}} +{{- if eq ($bs.mode | default "bundled") "external" -}} +{{- required "backingServices.clickhouse.url is required when mode=external" $bs.url -}} +{{- else -}} +{{- $prefix := $bs.releaseName | default "countly" -}} +{{- $host := $bs.host | default (printf "%s-clickhouse-clickhouse-headless.%s.svc" $prefix ($bs.namespace | default "clickhouse")) -}} +{{- $port := $bs.port | default "8123" -}} +{{- $tls := $bs.tls | default "false" -}} +{{- $scheme := ternary "https" "http" (eq (toString $tls) "true") -}} +{{- $scheme }}://{{ $host }}:{{ $port }} +{{- end -}} +{{- end -}} + +{{/* +Redis URL computation. +If backingServices.redis.url is set, use it. +If redis subchart is enabled, auto-wire to the subchart service. +*/}} +{{- define "countly-migration.redisUrl" -}} +{{- if .Values.backingServices.redis.url -}} +{{- .Values.backingServices.redis.url -}} +{{- else if .Values.redis.enabled -}} +redis://{{ include "countly-migration.fullname" . }}-redis-master:6379 +{{- end -}} +{{- end -}} + +{{/* +Image reference with tag defaulting to "latest". +*/}} +{{- define "countly-migration.image" -}} +{{- $tag := .Values.image.tag | default "latest" -}} +{{ .Values.image.repository }}:{{ $tag }} +{{- end }} diff --git a/charts/countly-migration/templates/configmap.yaml b/charts/countly-migration/templates/configmap.yaml new file mode 100644 index 0000000..efdad7a --- /dev/null +++ b/charts/countly-migration/templates/configmap.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "countly-migration.fullname" . }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} + {{- if ((.Values.argocd).enabled) }} + annotations: + {{- include "countly-migration.syncWave" (dict "wave" "0" "root" .) | nindent 4 }} + {{- end }} +data: + {{- range $k, $v := .Values.config }} + {{ $k }}: {{ $v | quote }} + {{- end }} + {{- /* Multi-pod worker configuration */ -}} + {{- with .Values.worker }} + MULTI_POD_ENABLED: {{ .enabled | quote }} + LOCK_TTL_SECONDS: {{ .lockTtlSec | quote }} + LOCK_RENEW_MS: {{ .lockRenewMs | quote }} + POD_HEARTBEAT_MS: {{ .podHeartbeatMs | quote }} + POD_DEAD_AFTER_SEC: {{ .podDeadAfterSec | quote }} + RANGE_PARALLEL_THRESHOLD: {{ .rangeParallelThreshold | quote }} + RANGE_COUNT: {{ .rangeCount | quote }} + RANGE_LEASE_TTL_SEC: {{ .rangeLeaseTtlSec | quote }} + PROGRESS_UPDATE_MS: {{ .progressUpdateMs | quote }} + ASYNC_WRITE_FLUSH_INTERVAL_MS: {{ .asyncWriteFlushIntervalMs | quote }} + ASYNC_WRITE_FLUSH_BATCH_SIZE: {{ .asyncWriteFlushBatchSize | quote }} + {{- end }} diff --git a/charts/countly-migration/templates/deployment.yaml b/charts/countly-migration/templates/deployment.yaml new file mode 100644 index 0000000..8ac7549 --- /dev/null +++ b/charts/countly-migration/templates/deployment.yaml @@ -0,0 +1,117 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "countly-migration.fullname" . }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} + {{- if or (and .Values.externalLink.enabled .Values.externalLink.url) ((.Values.argocd).enabled) }} + annotations: + {{- if and .Values.externalLink.enabled .Values.externalLink.url }} + link.argocd.argoproj.io/external-link: {{ .Values.externalLink.url | quote }} + {{- end }} + {{- include "countly-migration.syncWave" (dict "wave" "10" "root" .) | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.deployment.replicas }} + strategy: + type: {{ .Values.deployment.strategy.type }} + {{- if eq .Values.deployment.strategy.type "RollingUpdate" }} + rollingUpdate: + maxSurge: {{ .Values.deployment.strategy.rollingUpdate.maxSurge | default 1 }} + maxUnavailable: {{ .Values.deployment.strategy.rollingUpdate.maxUnavailable | default 0 }} + {{- end }} + selector: + matchLabels: + {{- include "countly-migration.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "countly-migration.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "countly-migration.serviceAccountName" . }} + terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.image.pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ include "countly-migration.name" . }} + image: {{ include "countly-migration.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "countly-migration.fullname" . }} + - secretRef: + name: {{ include "countly-migration.secretName" . }} + env: + - name: POD_ID + valueFrom: + fieldRef: + fieldPath: metadata.name + lifecycle: + preStop: + httpGet: + path: /control/drain + port: http + livenessProbe: + httpGet: + path: {{ .Values.probes.liveness.path }} + port: http + initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.liveness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }} + failureThreshold: {{ .Values.probes.liveness.failureThreshold }} + readinessProbe: + httpGet: + path: {{ .Values.probes.readiness.path }} + port: http + initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.readiness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }} + failureThreshold: {{ .Values.probes.readiness.failureThreshold }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.affinity }} + affinity: + {{- toYaml .Values.affinity | nindent 8 }} + {{- else if gt (int .Values.deployment.replicas) 1 }} + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + {{- include "countly-migration.selectorLabels" . | nindent 20 }} + topologyKey: kubernetes.io/hostname + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/countly-migration/templates/external-secret.yaml b/charts/countly-migration/templates/external-secret.yaml new file mode 100644 index 0000000..14b81da --- /dev/null +++ b/charts/countly-migration/templates/external-secret.yaml @@ -0,0 +1,33 @@ +{{- if eq (.Values.secrets.mode | default "values") "externalSecret" }} +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ include "countly-migration.fullname" . }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} + {{- if ((.Values.argocd).enabled) }} + annotations: + {{- include "countly-migration.syncWave" (dict "wave" "0" "root" .) | nindent 4 }} + {{- end }} +spec: + refreshInterval: {{ .Values.secrets.externalSecret.refreshInterval | default "1h" }} + secretStoreRef: + name: {{ required "secrets.externalSecret.secretStoreRef.name is required when secrets.mode=externalSecret" .Values.secrets.externalSecret.secretStoreRef.name }} + kind: {{ .Values.secrets.externalSecret.secretStoreRef.kind | default "ClusterSecretStore" }} + target: + name: {{ include "countly-migration.fullname" . }} + creationPolicy: Owner + data: + - secretKey: MONGO_URI + remoteRef: + key: {{ required "secrets.externalSecret.remoteRefs.mongoUri is required" .Values.secrets.externalSecret.remoteRefs.mongoUri }} + - secretKey: CLICKHOUSE_URL + remoteRef: + key: {{ required "secrets.externalSecret.remoteRefs.clickhouseUrl is required" .Values.secrets.externalSecret.remoteRefs.clickhouseUrl }} + - secretKey: CLICKHOUSE_PASSWORD + remoteRef: + key: {{ required "secrets.externalSecret.remoteRefs.clickhousePassword is required" .Values.secrets.externalSecret.remoteRefs.clickhousePassword }} + - secretKey: REDIS_URL + remoteRef: + key: {{ required "secrets.externalSecret.remoteRefs.redisUrl is required" .Values.secrets.externalSecret.remoteRefs.redisUrl }} +{{- end }} diff --git a/charts/countly-migration/templates/ingress.yaml b/charts/countly-migration/templates/ingress.yaml new file mode 100644 index 0000000..aa8a6e3 --- /dev/null +++ b/charts/countly-migration/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "countly-migration.fullname" . }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} + {{- if or (and .Values.externalLink.enabled .Values.externalLink.url) ((.Values.argocd).enabled) .Values.ingress.annotations }} + annotations: + {{- if and .Values.externalLink.enabled .Values.externalLink.url }} + link.argocd.argoproj.io/external-link: {{ .Values.externalLink.url | quote }} + {{- end }} + {{- include "countly-migration.syncWave" (dict "wave" "10" "root" .) | nindent 4 }} + {{- with .Values.ingress.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "countly-migration.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/countly-migration/templates/networkpolicy.yaml b/charts/countly-migration/templates/networkpolicy.yaml new file mode 100644 index 0000000..5b8959d --- /dev/null +++ b/charts/countly-migration/templates/networkpolicy.yaml @@ -0,0 +1,27 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "countly-migration.fullname" . }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} + {{- if ((.Values.argocd).enabled) }} + annotations: + {{- include "countly-migration.syncWave" (dict "wave" "0" "root" .) | nindent 4 }} + {{- end }} +spec: + podSelector: + matchLabels: + {{- include "countly-migration.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + - Egress + {{- with .Values.networkPolicy.ingress }} + ingress: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.networkPolicy.egress }} + egress: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/countly-migration/templates/pdb.yaml b/charts/countly-migration/templates/pdb.yaml new file mode 100644 index 0000000..2196a5f --- /dev/null +++ b/charts/countly-migration/templates/pdb.yaml @@ -0,0 +1,13 @@ +{{- if .Values.pdb.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "countly-migration.fullname" . }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} +spec: + minAvailable: {{ .Values.pdb.minAvailable }} + selector: + matchLabels: + {{- include "countly-migration.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/charts/countly-migration/templates/secret.yaml b/charts/countly-migration/templates/secret.yaml new file mode 100644 index 0000000..4a8294f --- /dev/null +++ b/charts/countly-migration/templates/secret.yaml @@ -0,0 +1,44 @@ +{{- if eq (.Values.secrets.mode | default "values") "values" }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "countly-migration.fullname" . }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} + {{- if or .Values.secrets.keep ((.Values.argocd).enabled) }} + annotations: + {{- if .Values.secrets.keep }} + helm.sh/resource-policy: keep + {{- end }} + {{- include "countly-migration.syncWave" (dict "wave" "1" "root" .) | nindent 4 }} + {{- end }} +type: Opaque +data: + {{- $secretName := include "countly-migration.fullname" . }} + {{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName }} + {{- if and (not .Values.backingServices.mongodb.password) (eq (.Values.backingServices.mongodb.mode | default "bundled") "bundled") }} + {{- if and $existing (index $existing.data "MONGO_URI") }} + MONGO_URI: {{ index $existing.data "MONGO_URI" }} + {{- else }} + {{- fail "backingServices.mongodb.password is required on first install when mode=bundled. Set the value or provide secrets.existingSecret." }} + {{- end }} + {{- else }} + MONGO_URI: {{ include "countly-migration.mongoUri" . | b64enc }} + {{- end }} + CLICKHOUSE_URL: {{ include "countly-migration.clickhouseUrl" . | b64enc }} + {{- if .Values.backingServices.clickhouse.password }} + CLICKHOUSE_PASSWORD: {{ .Values.backingServices.clickhouse.password | b64enc }} + {{- else if and $existing (index $existing.data "CLICKHOUSE_PASSWORD") }} + CLICKHOUSE_PASSWORD: {{ index $existing.data "CLICKHOUSE_PASSWORD" }} + {{- else }} + CLICKHOUSE_PASSWORD: {{ "" | b64enc }} + {{- end }} + {{- $redisUrl := include "countly-migration.redisUrl" . }} + {{- if $redisUrl }} + REDIS_URL: {{ $redisUrl | b64enc }} + {{- else if and $existing (index $existing.data "REDIS_URL") }} + REDIS_URL: {{ index $existing.data "REDIS_URL" }} + {{- else }} + REDIS_URL: {{ "" | b64enc }} + {{- end }} +{{- end }} diff --git a/charts/countly-migration/templates/service.yaml b/charts/countly-migration/templates/service.yaml new file mode 100644 index 0000000..d18d7c2 --- /dev/null +++ b/charts/countly-migration/templates/service.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "countly-migration.fullname" . }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} + {{- if or ((.Values.argocd).enabled) .Values.service.annotations }} + annotations: + {{- include "countly-migration.syncWave" (dict "wave" "10" "root" .) | nindent 4 }} + {{- with .Values.service.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "countly-migration.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/charts/countly-migration/templates/serviceaccount.yaml b/charts/countly-migration/templates/serviceaccount.yaml new file mode 100644 index 0000000..fc9d0e1 --- /dev/null +++ b/charts/countly-migration/templates/serviceaccount.yaml @@ -0,0 +1,16 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "countly-migration.serviceAccountName" . }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} + {{- if or ((.Values.argocd).enabled) .Values.serviceAccount.annotations }} + annotations: + {{- include "countly-migration.syncWave" (dict "wave" "0" "root" .) | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} +automountServiceAccountToken: false +{{- end }} diff --git a/charts/countly-migration/templates/servicemonitor.yaml b/charts/countly-migration/templates/servicemonitor.yaml new file mode 100644 index 0000000..6299115 --- /dev/null +++ b/charts/countly-migration/templates/servicemonitor.yaml @@ -0,0 +1,21 @@ +{{- if .Values.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "countly-migration.fullname" . }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} + {{- if ((.Values.argocd).enabled) }} + annotations: + {{- include "countly-migration.syncWave" (dict "wave" "10" "root" .) | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "countly-migration.selectorLabels" . | nindent 6 }} + endpoints: + - port: http + path: {{ .Values.serviceMonitor.path }} + interval: {{ .Values.serviceMonitor.interval }} + scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} +{{- end }} diff --git a/charts/countly-migration/templates/tests/test-health.yaml b/charts/countly-migration/templates/tests/test-health.yaml new file mode 100644 index 0000000..1c4142c --- /dev/null +++ b/charts/countly-migration/templates/tests/test-health.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "countly-migration.fullname" . }}-test-health + namespace: {{ .Release.Namespace }} + labels: + {{- include "countly-migration.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + restartPolicy: Never + containers: + - name: test + image: busybox:1.35 + command: ['sh', '-c', 'wget -qO- --timeout=10 http://{{ include "countly-migration.fullname" . }}:{{ .Values.service.port }}/healthz'] diff --git a/charts/countly-migration/values.schema.json b/charts/countly-migration/values.schema.json new file mode 100644 index 0000000..a2e6ec3 --- /dev/null +++ b/charts/countly-migration/values.schema.json @@ -0,0 +1,229 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "countly-migration values", + "type": "object", + "properties": { + "image": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "minLength": 1, + "description": "Container image repository" + }, + "tag": { + "type": "string", + "description": "Image tag (defaults to appVersion)" + }, + "pullPolicy": { + "type": "string", + "enum": ["Always", "IfNotPresent", "Never"] + } + }, + "required": ["repository"] + }, + "deployment": { + "type": "object", + "properties": { + "replicas": { + "type": "integer", + "minimum": 1, + "description": "Number of migration pods (>1 enables multi-pod coordination)" + }, + "strategy": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["Recreate", "RollingUpdate"], + "description": "Deployment strategy (use RollingUpdate for multi-pod)" + } + }, + "required": ["type"] + }, + "terminationGracePeriodSeconds": { + "type": "integer", + "minimum": 30 + } + }, + "required": ["replicas", "strategy"] + }, + "service": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["ClusterIP", "NodePort", "LoadBalancer"] + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + }, + "required": ["port"] + }, + "secrets": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["values", "existingSecret", "externalSecret"], + "description": "Secret provisioning mode" + }, + "keep": { + "type": "boolean" + }, + "existingSecret": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "externalSecret": { + "type": "object", + "properties": { + "refreshInterval": { + "type": "string" + }, + "secretStoreRef": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "kind": { + "type": "string" + } + } + }, + "remoteRefs": { + "type": "object", + "properties": { + "mongoUri": { "type": "string" }, + "clickhouseUrl": { "type": "string" }, + "clickhousePassword": { "type": "string" }, + "redisUrl": { "type": "string" } + } + } + } + } + }, + "required": ["mode"] + }, + "backingServices": { + "type": "object", + "properties": { + "mongodb": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["external", "bundled"] + }, + "releaseName": { + "type": "string", + "description": "Release name prefix of the sibling countly-mongodb chart" + } + } + }, + "clickhouse": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["external", "bundled"] + }, + "releaseName": { + "type": "string", + "description": "Release name prefix of the sibling countly-clickhouse chart" + } + } + }, + "redis": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + } + } + } + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Non-secret environment variables" + }, + "probes": { + "type": "object", + "properties": { + "liveness": { + "type": "object", + "properties": { + "path": { "type": "string" }, + "initialDelaySeconds": { "type": "integer", "minimum": 0 }, + "periodSeconds": { "type": "integer", "minimum": 1 }, + "timeoutSeconds": { "type": "integer", "minimum": 1 }, + "failureThreshold": { "type": "integer", "minimum": 1 } + } + }, + "readiness": { + "type": "object", + "properties": { + "path": { "type": "string" }, + "initialDelaySeconds": { "type": "integer", "minimum": 0 }, + "periodSeconds": { "type": "integer", "minimum": 1 }, + "timeoutSeconds": { "type": "integer", "minimum": 1 }, + "failureThreshold": { "type": "integer", "minimum": 1 } + } + } + } + }, + "resources": { + "type": "object", + "properties": { + "requests": { "type": "object" }, + "limits": { "type": "object" } + } + }, + "externalLink": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "url": { "type": "string" } + } + }, + "serviceMonitor": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "interval": { "type": "string" }, + "scrapeTimeout": { "type": "string" }, + "path": { "type": "string" } + } + }, + "worker": { + "type": "object", + "description": "Multi-pod worker coordination settings", + "properties": { + "enabled": { "type": "boolean", "description": "Enable multi-pod coordination mode" }, + "lockTtlSec": { "type": "integer", "minimum": 30, "description": "Collection lock TTL in seconds" }, + "lockRenewMs": { "type": "integer", "minimum": 1000, "description": "Lock renewal interval in ms" }, + "podHeartbeatMs": { "type": "integer", "minimum": 1000, "description": "Pod heartbeat interval in ms" }, + "podDeadAfterSec": { "type": "integer", "minimum": 30, "description": "Consider pod dead after N seconds" }, + "rangeParallelThreshold": { "type": "integer", "minimum": 0, "description": "Doc count to trigger range splitting" }, + "rangeCount": { "type": "integer", "minimum": 1, "description": "Number of time ranges" }, + "rangeLeaseTtlSec": { "type": "integer", "minimum": 30, "description": "Range lease TTL in seconds" }, + "progressUpdateMs": { "type": "integer", "minimum": 1000, "description": "Progress report interval in ms" }, + "asyncWriteFlushIntervalMs": { "type": "integer", "minimum": 1000, "description": "Async batch flush interval in ms" }, + "asyncWriteFlushBatchSize": { "type": "integer", "minimum": 1, "description": "Async batch flush size" } + } + } + }, + "required": ["image", "deployment", "service", "secrets"] +} diff --git a/charts/countly-migration/values.yaml b/charts/countly-migration/values.yaml new file mode 100644 index 0000000..29cf0d9 --- /dev/null +++ b/charts/countly-migration/values.yaml @@ -0,0 +1,284 @@ +# -- Override the chart name used in resource names +nameOverride: "" +# -- Override the full resource name +fullnameOverride: "" + +# -- Container image configuration +image: + repository: countly/countly-migration + # -- Defaults to "latest" when empty + tag: "" + pullPolicy: IfNotPresent + pullSecrets: [] + +# -- Service account configuration +serviceAccount: + create: true + name: "" + annotations: {} + +# -- ArgoCD integration +argocd: + enabled: false + +# -- Deployment configuration +# For multi-pod mode: set replicas > 1 and strategy.type to RollingUpdate +deployment: + replicas: 1 + strategy: + type: Recreate + # RollingUpdate settings (used when strategy.type is RollingUpdate) + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + terminationGracePeriodSeconds: 90 + +# -- Additional pod annotations +podAnnotations: {} +# -- Additional pod labels +podLabels: {} + +# -- Service configuration +service: + type: ClusterIP + port: 8080 + annotations: {} + +# -- Ingress configuration (optional) +ingress: + enabled: false + className: "" + annotations: {} + hosts: [] + # - host: migration.example.internal + # paths: + # - path: / + # pathType: Prefix + tls: [] + +# -- ArgoCD external link on the Deployment resource +externalLink: + enabled: false + # -- URL shown in ArgoCD UI (e.g. https://migration.example.internal/runs/current) + url: "" + +# -- Backing services (credential sources for MongoDB, ClickHouse, Redis) +backingServices: + mongodb: + # -- bundled: auto-discover from sibling countly-mongodb chart; external: provide full URI + mode: bundled + # -- Full MongoDB connection string (external mode only) + uri: "" + # -- Release name prefix of the sibling countly-mongodb chart (bundled mode) + releaseName: "countly" + # -- MongoDB host override (bundled mode; auto-constructed from releaseName if empty) + host: "" + port: "27017" + username: "app" + # -- MongoDB app user password (must match countly-mongodb chart) + password: "" + database: "admin" + replicaSet: "" + # -- Namespace where countly-mongodb chart is deployed + namespace: mongodb + clickhouse: + # -- bundled: auto-discover from sibling countly-clickhouse chart; external: provide full URL + mode: bundled + # -- Full ClickHouse HTTP URL (external mode only) + url: "" + # -- Release name prefix of the sibling countly-clickhouse chart (bundled mode) + releaseName: "countly" + # -- ClickHouse host override (bundled mode; auto-constructed from releaseName if empty) + host: "" + port: "8123" + tls: "false" + username: "default" + # -- ClickHouse default user password (must match countly-clickhouse chart) + password: "" + # -- Namespace where countly-clickhouse chart is deployed + namespace: clickhouse + redis: + # -- Full Redis connection URL (e.g. redis://redis:6379) + url: "" + +# -- Secrets management +secrets: + # -- values: create Secret from values; existingSecret: reference pre-created; externalSecret: use ESO + mode: values + # -- Preserve secrets on helm uninstall/upgrade + keep: true + existingSecret: + # -- Name of pre-created Secret containing MONGO_URI, CLICKHOUSE_URL, CLICKHOUSE_PASSWORD, REDIS_URL + name: "" + externalSecret: + refreshInterval: "1h" + secretStoreRef: + name: "" + kind: ClusterSecretStore + remoteRefs: + mongoUri: "" + clickhouseUrl: "" + clickhousePassword: "" + redisUrl: "" + +# -- Application config (non-secret environment variables) +config: + SERVICE_NAME: countly-migration + SERVICE_PORT: "8080" + SERVICE_HOST: "0.0.0.0" + GRACEFUL_SHUTDOWN_TIMEOUT_MS: "60000" + RERUN_MODE: "resume" + LOG_LEVEL: "info" + + # MongoDB source + MONGO_DB: "countly_drill" + MONGO_COLLECTION_PREFIX: "drill_events" + MONGO_READ_PREFERENCE: "primary" + MONGO_READ_CONCERN: "majority" + MONGO_RETRY_READS: "true" + MONGO_APP_NAME: "countly-migration" + MONGO_BATCH_ROWS_TARGET: "10000" + MONGO_CURSOR_BATCH_SIZE: "2000" + MONGO_MAX_TIME_MS: "120000" + + # Transform + TRANSFORM_VERSION: "v1" + + # ClickHouse target + CLICKHOUSE_DB: "countly_drill" + CLICKHOUSE_TABLE: "drill_events" + CLICKHOUSE_USERNAME: "default" + CLICKHOUSE_QUERY_TIMEOUT_MS: "120000" + CLICKHOUSE_MAX_RETRIES: "8" + CLICKHOUSE_RETRY_BASE_DELAY_MS: "1000" + CLICKHOUSE_RETRY_MAX_DELAY_MS: "30000" + CLICKHOUSE_USE_DEDUP_TOKEN: "true" + + # Backpressure + BACKPRESSURE_ENABLED: "true" + BACKPRESSURE_PARTS_TO_THROW_INSERT: "300" + BACKPRESSURE_MAX_PARTS_IN_TOTAL: "500" + BACKPRESSURE_PARTITION_PCT_HIGH: "0.50" + BACKPRESSURE_PARTITION_PCT_LOW: "0.35" + BACKPRESSURE_TOTAL_PCT_HIGH: "0.50" + BACKPRESSURE_TOTAL_PCT_LOW: "0.40" + BACKPRESSURE_POLL_INTERVAL_MS: "15000" + BACKPRESSURE_MAX_PAUSE_EPISODE_MS: "180000" + + # State management + MANIFEST_DB: "countly_drill" + REDIS_KEY_PREFIX: "mig" + TIMELINE_SNAPSHOT_INTERVAL: "10" + + # Garbage collection + GC_ENABLED: "true" + GC_RSS_SOFT_LIMIT_MB: "1536" + GC_RSS_HARD_LIMIT_MB: "2048" + GC_HEAP_USED_RATIO: "0.70" + GC_EVERY_N_BATCHES: "10" + +# -- Health probes +probes: + liveness: + path: /healthz + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 6 + readiness: + path: /readyz + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + +# -- Resource requests and limits +resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "2" + memory: "3Gi" + +# -- Pod-level security context +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + +# -- Container-level security context +containerSecurityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + +# -- Node selector +nodeSelector: {} +# -- Tolerations +tolerations: [] +# -- Affinity rules +affinity: {} + +# -- Network policy (optional) +networkPolicy: + enabled: false + ingress: {} + egress: {} + +# -- Multi-pod worker coordination settings +# These take effect when deployment.replicas > 1. +worker: + enabled: true # MULTI_POD_ENABLED + lockTtlSec: 300 # LOCK_TTL_SECONDS — collection lock expiration + lockRenewMs: 60000 # LOCK_RENEW_MS — lock renewal interval + podHeartbeatMs: 30000 # POD_HEARTBEAT_MS — heartbeat interval + podDeadAfterSec: 180 # POD_DEAD_AFTER_SEC — dead pod threshold + rangeParallelThreshold: 500000 # RANGE_PARALLEL_THRESHOLD — split large collections + rangeCount: 100 # RANGE_COUNT — number of time ranges + rangeLeaseTtlSec: 300 # RANGE_LEASE_TTL_SEC — range lease expiration + progressUpdateMs: 5000 # PROGRESS_UPDATE_MS — progress report interval + asyncWriteFlushIntervalMs: 5000 # ASYNC_WRITE_FLUSH_INTERVAL_MS + asyncWriteFlushBatchSize: 10 # ASYNC_WRITE_FLUSH_BATCH_SIZE + +# -- Pod disruption budget (recommended when replicas > 1) +pdb: + enabled: false + minAvailable: 1 + +# -- Prometheus ServiceMonitor (optional) +serviceMonitor: + enabled: false + interval: "30s" + scrapeTimeout: "10s" + path: /stats + +# -- Bundled Redis subchart (Bitnami) +# Deploys Redis alongside the migration service. +# When enabled and backingServices.redis.url is empty, the chart auto-wires the URL. +# Set redis.enabled=false and provide backingServices.redis.url to use an external Redis. +redis: + enabled: true + architecture: standalone + # -- Explicit sync-wave ensures Redis is healthy before the migration Deployment (wave 10) + commonAnnotations: + argocd.argoproj.io/sync-wave: "0" + auth: + enabled: false + master: + resources: + requests: + cpu: "500m" + memory: "2Gi" + limits: + cpu: "1" + memory: "2Gi" + persistence: + enabled: true + size: 8Gi + commonConfiguration: |- + appendonly yes + appendfsync everysec + save 900 1 + save 300 10 + save 60 10000 diff --git a/charts/noop/Chart.yaml b/charts/noop/Chart.yaml new file mode 100644 index 0000000..0969edd --- /dev/null +++ b/charts/noop/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: noop +description: No-op chart used when a GitOps-managed component is intentionally disabled. +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/environments/reference/credentials-migration.yaml b/environments/reference/credentials-migration.yaml new file mode 100644 index 0000000..20e8936 --- /dev/null +++ b/environments/reference/credentials-migration.yaml @@ -0,0 +1,40 @@ +# Reference secrets for the optional countly-migration app. +# +# Default pattern: values mode with bundled MongoDB + ClickHouse and bundled +# Redis. Only the MongoDB app password and ClickHouse default-user password are +# required in that mode. +# +# If you use External Secrets, switch to the commented block below and provide +# full connection secret refs for MONGO_URI, CLICKHOUSE_URL, CLICKHOUSE_PASSWORD, +# and REDIS_URL. +# - mongoUri: usually taken from the MongoDB chart's app connection string secret +# or built from the app user, replica set, and service DNS. +# - clickhouseUrl: usually points to the ClickHouse HTTP endpoint, for example +# http://countly-clickhouse-clickhouse-headless.clickhouse.svc.cluster.local:8123 +# - clickhousePassword: reuse the existing customer ClickHouse password secret; +# no separate migration password secret is needed. +# - redisUrl: if migration uses bundled Redis, point this to the in-cluster Redis +# service, for example redis://countly-migration-redis-master:6379 + +secrets: + mode: values + +backingServices: + mongodb: + password: "" # REQUIRED when mode=bundled + clickhouse: + password: "" # REQUIRED when mode=bundled + +# External Secret example: +# secrets: +# mode: externalSecret +# externalSecret: +# refreshInterval: "1h" +# secretStoreRef: +# name: gcp-secrets +# kind: ClusterSecretStore +# remoteRefs: +# mongoUri: "-mongodb-connection-string" +# clickhouseUrl: "-migration-clickhouse-url" +# clickhousePassword: "-clickhouse-password" +# redisUrl: "-migration-redis-url" diff --git a/environments/reference/migration.yaml b/environments/reference/migration.yaml new file mode 100644 index 0000000..6d6fccc --- /dev/null +++ b/environments/reference/migration.yaml @@ -0,0 +1,63 @@ +# Reference values for the optional countly-migration app. +# +# Keep this file for every customer even when migration is disabled. +# When a customer later adds argocd/customers/migration/.yaml, this +# file already shows the expected non-secret knobs and bundled-vs-external +# service modes. + +image: + repository: countly/countly-migration + tag: "" + pullPolicy: IfNotPresent + pullSecrets: [] + +deployment: + replicas: 3 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + +backingServices: + mongodb: + mode: bundled + releaseName: "countly" + namespace: mongodb + username: "app" + database: admin + replicaSet: "" + # External mode example: + # mode: external + # uri: "mongodb://app:password@mongodb.example:27017/admin?replicaSet=rs0&ssl=false" + clickhouse: + mode: bundled + releaseName: "countly" + namespace: clickhouse + username: "default" + tls: "false" + # External mode example: + # mode: external + # url: "http://clickhouse.example:8123" + redis: + url: "" + # External mode example: + # url: "redis://redis.example:6379" + +config: + RERUN_MODE: "resume" + LOG_LEVEL: "info" + +resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "2" + memory: "3Gi" + +worker: + enabled: true + +redis: + enabled: true diff --git a/scripts/new-argocd-customer.sh b/scripts/new-argocd-customer.sh new file mode 100755 index 0000000..c009232 --- /dev/null +++ b/scripts/new-argocd-customer.sh @@ -0,0 +1,467 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + scripts/new-argocd-customer.sh [--secret-mode values|gcp-secrets] [project] + +Example: + scripts/new-argocd-customer.sh acme https://1.2.3.4 acme.count.ly + scripts/new-argocd-customer.sh --secret-mode gcp-secrets acme https://1.2.3.4 acme.count.ly + +This command: + 1. copies environments/reference to environments/ + 2. updates environments//global.yaml with the hostname and default profiles + 3. writes credentials files for either direct values or GCP Secret Manager + 4. creates argocd/customers/.yaml for the ApplicationSets + +Defaults: + project countly-customers + secretMode values + sizing production + security open + tls letsencrypt + observability full + kafkaConnect balanced + kafkaConnectSizing auto + gcpSA set after scaffold for External Secrets Workload Identity +EOF +} + +secret_mode="values" +positionals=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --secret-mode) + if [[ $# -lt 2 ]]; then + echo "Missing value for --secret-mode" >&2 + exit 1 + fi + secret_mode="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + while [[ $# -gt 0 ]]; do + positionals+=("$1") + shift + done + ;; + -*) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + *) + positionals+=("$1") + shift + ;; + esac +done + +case "${secret_mode}" in + values|direct) + secret_mode="values" + ;; + gcp-secrets) + ;; + *) + echo "Unsupported --secret-mode: ${secret_mode}" >&2 + echo "Supported values: values, gcp-secrets" >&2 + exit 1 + ;; +esac + +if [[ ${#positionals[@]} -lt 3 || ${#positionals[@]} -gt 4 ]]; then + usage + exit 1 +fi + +customer="${positionals[0]}" +server="${positionals[1]}" +hostname="${positionals[2]}" +project="${positionals[3]:-countly-customers}" + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +env_dir="${repo_root}/environments/${customer}" +customer_file="${repo_root}/argocd/customers/${customer}.yaml" +migration_customer_file="${repo_root}/argocd/customers/migration/${customer}.yaml" + +if [[ -e "${env_dir}" ]]; then + echo "Environment already exists: ${env_dir}" >&2 + exit 1 +fi + +if [[ -e "${customer_file}" ]]; then + echo "Customer metadata already exists: ${customer_file}" >&2 + exit 1 +fi + +mkdir -p "$(dirname "${customer_file}")" +mkdir -p "$(dirname "${migration_customer_file}")" + +cp -R "${repo_root}/environments/reference" "${env_dir}" + +cat > "${env_dir}/global.yaml" < "${env_dir}/kafka.yaml" <<'EOF' +# Customer-specific Kafka overrides only. +# Leave this file minimal so sizing / kafka-connect / observability / security profiles apply cleanly. +EOF + +cat > "${env_dir}/clickhouse.yaml" <<'EOF' +# Customer-specific ClickHouse overrides only. +# Leave this file minimal so sizing / security profiles apply cleanly. +EOF + +cat > "${env_dir}/mongodb.yaml" <<'EOF' +# Customer-specific MongoDB overrides only. +# Leave this file minimal so sizing / security profiles apply cleanly. +EOF + +cat > "${env_dir}/observability.yaml" <<'EOF' +# Customer-specific observability overrides only. +EOF + +cat > "${env_dir}/migration.yaml" <<'EOF' +# Customer-specific migration overrides. +# Keep this file even when migration is disabled so future enablement only +# requires filling the matching credentials file and creating the matching +# migration metadata file. + +image: + repository: countly/countly-migration + tag: "" + pullPolicy: IfNotPresent + pullSecrets: [] + +deployment: + replicas: 3 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + +backingServices: + mongodb: + mode: bundled + releaseName: "countly" + namespace: mongodb + username: "app" + database: admin + replicaSet: "" + clickhouse: + mode: bundled + releaseName: "countly" + namespace: clickhouse + username: "default" + tls: "false" + redis: + url: "" + +config: + RERUN_MODE: "resume" + LOG_LEVEL: "info" + +resources: + requests: + cpu: "500m" + memory: "1Gi" + limits: + cpu: "2" + memory: "3Gi" + +worker: + enabled: true + +redis: + enabled: true +EOF + +if [[ "${secret_mode}" == "gcp-secrets" ]]; then + cat > "${env_dir}/countly.yaml" <<'EOF' +# Customer-specific Countly overrides only. +# TLS Secret Manager support is prewired below and becomes active only when: +# - argocd/customers/.yaml sets tls: provided +# - the shared Secret Manager keys exist +# By default this reuses one shared certificate for every customer: +# - countly-prod-tls-crt +# - countly-prod-tls-key +# Override these remoteRefs only if a specific customer needs its own cert. +ingress: + tls: + externalSecret: + enabled: true + refreshInterval: "1h" + secretStoreRef: + name: gcp-secrets + kind: ClusterSecretStore + remoteRefs: + tlsCrt: countly-prod-tls-crt + tlsKey: countly-prod-tls-key +EOF + + cat > "${env_dir}/credentials-countly.yaml" < "${env_dir}/credentials-kafka.yaml" < "${env_dir}/credentials-clickhouse.yaml" < "${env_dir}/credentials-mongodb.yaml" < "${env_dir}/credentials-migration.yaml" < "${env_dir}/countly.yaml" <<'EOF' +# Customer-specific Countly overrides only. +# Leave this file minimal so sizing / TLS / observability / security profiles apply cleanly. +EOF + + cat > "${env_dir}/credentials-countly.yaml" <<'EOF' +# Countly secrets — FILL IN before first deploy +# Passwords must match across charts (see secrets.example.yaml) +secrets: + mode: values + common: + encryptionReportsKey: "" # REQUIRED: min 8 chars + webSessionSecret: "" # REQUIRED: min 8 chars + passwordSecret: "" # REQUIRED: min 8 chars + clickhouse: + username: "default" + password: "" # REQUIRED: must match credentials-clickhouse.yaml + database: "countly_drill" + kafka: + securityProtocol: "PLAINTEXT" + mongodb: + password: "" # REQUIRED: must match credentials-mongodb.yaml users.app.password +EOF + + cat > "${env_dir}/credentials-kafka.yaml" <<'EOF' +# Kafka secrets — FILL IN before first deploy +secrets: + mode: values + +kafkaConnect: + clickhouse: + password: "" # REQUIRED: must match ClickHouse default user password +EOF + + cat > "${env_dir}/credentials-clickhouse.yaml" <<'EOF' +# ClickHouse secrets — FILL IN before first deploy +secrets: + mode: values + +auth: + defaultUserPassword: + password: "" # REQUIRED: must match credentials-countly.yaml secrets.clickhouse.password +EOF + + cat > "${env_dir}/credentials-mongodb.yaml" <<'EOF' +# MongoDB secrets — FILL IN before first deploy +secrets: + mode: values + +users: + admin: + enabled: true + password: "" # REQUIRED: MongoDB super admin/root-style user + app: + password: "" # REQUIRED: must match credentials-countly.yaml secrets.mongodb.password + metrics: + enabled: true + password: "" # REQUIRED: metrics exporter password +EOF + + cat > "${env_dir}/credentials-migration.yaml" <<'EOF' +# Migration secrets — FILL IN when migration is enabled +secrets: + mode: values + +backingServices: + mongodb: + password: "" # REQUIRED when migration uses bundled MongoDB + clickhouse: + password: "" # REQUIRED when migration uses bundled ClickHouse +EOF +fi + +cat > "${customer_file}" <- convention + 6. Commit and sync countly-bootstrap +EOF