diff --git a/.github/configs/helm-render-values/br-sta.yaml b/.github/configs/helm-render-values/br-sta.yaml new file mode 100644 index 000000000..5933f48e8 --- /dev/null +++ b/.github/configs/helm-render-values/br-sta.yaml @@ -0,0 +1,36 @@ +# Render-gate fixture for br-sta — exercises the productized (lerian-common) +# paths in their ENABLED state so the render gate catches regressions there: +# - multiTenant.env + fail-fast multiTenant.secret (MT on, API key supplied) +# - otel.env driven by global.observability +# - datastore.value broker mask + RABBITMQ_DEFAULT_PASS fail-fast (rmq on) +# Values are placeholders; the gate only checks that the chart renders. +brSta: + multiTenant: + enabled: true + serviceDiscovery: + enabled: true + streaming: + enabled: true + configmap: + RABBITMQ_ENABLED: "true" + STREAMING_BROKERS: "kafka:9092" + STREAMING_SASL_MECHANISM: "PLAIN" + STREAMING_SASL_USERNAME: "u" + STREAMING_TLS_ENABLED: "true" + secrets: + MULTI_TENANT_SERVICE_API_KEY: "render-fixture-mt-api-key" + STREAMING_SASL_PASSWORD: "p" + SD_TOKEN: "t" + RABBITMQ_DEFAULT_PASS: "render-fixture-rmq-pass" +global: + observability: + enabled: true + otlpEndpoint: "otel-collector:4317" + deploymentEnvironment: "staging" + multiTenant: + url: "http://tenant-manager:8080" + redisHost: "tenant-manager-redis" + serviceDiscovery: + address: "consul:8500" + streaming: + brokers: "kafka:9092" diff --git a/charts/br-sta/Chart.lock b/charts/br-sta/Chart.lock new file mode 100644 index 000000000..87b51ff87 --- /dev/null +++ b/charts/br-sta/Chart.lock @@ -0,0 +1,15 @@ +dependencies: +- name: lerian-common-helm + repository: file://../lerian-common + version: 1.4.0 +- name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 16.3.5 +- name: valkey + repository: oci://registry-1.docker.io/bitnamicharts + version: 2.4.7 +- name: rabbitmq + repository: https://groundhog2k.github.io/helm-charts + version: 2.1.11 +digest: sha256:c083184b66b524fe31164e51f1732a7e0f615814712d9050e58ecd8c3490d5a6 +generated: "2026-08-04T17:38:03.977223-03:00" diff --git a/charts/br-sta/Chart.yaml b/charts/br-sta/Chart.yaml new file mode 100644 index 000000000..299dc1a2c --- /dev/null +++ b/charts/br-sta/Chart.yaml @@ -0,0 +1,53 @@ +apiVersion: v2 +name: br-sta-helm +description: A Helm chart for br-sta, a Lerian Studio Go/Fiber HTTP service + (manager) plus its background worker, backed by PostgreSQL and Redis/Valkey. +type: application +annotations: + lerian.studio/chart-type: multi-component +home: https://github.com/LerianStudio/br-sta +sources: + - https://github.com/LerianStudio/helm/tree/main/charts/br-sta + - https://github.com/LerianStudio/br-sta +maintainers: + - name: "Lerian Studio" + email: "support@lerian.studio" + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 1.0.0-beta.5 + +# This is the version number of the application being deployed. +appVersion: "1.0.0-beta.32" + +# A list of keywords about the chart. This helps others discover the chart. +keywords: + - br-sta + - lerian + - go + - fiber + - service + +# The URL to an icon file for this chart. +icon: https://avatars.githubusercontent.com/u/148895005?s=200&v=4 + +# Dependencies — br-sta requires PostgreSQL (SQL migrations) and Redis/Valkey +# (caching + rate limiting). RabbitMQ is OPTIONAL and bundled only when +# rabbitmq.enabled=true (disabled by default). All subcharts are gated by +# their `.enabled` condition. +dependencies: + - name: lerian-common-helm + version: "1.4.0" + repository: "file://../lerian-common" + - name: postgresql + version: "16.3.5" + repository: "https://charts.bitnami.com/bitnami" + condition: postgresql.enabled + - name: valkey + version: "2.4.7" + repository: "oci://registry-1.docker.io/bitnamicharts" + condition: valkey.enabled + - name: rabbitmq + version: "2.1.11" + repository: "https://groundhog2k.github.io/helm-charts" + condition: rabbitmq.enabled diff --git a/charts/br-sta/README.md b/charts/br-sta/README.md new file mode 100644 index 000000000..91cf46c39 --- /dev/null +++ b/charts/br-sta/README.md @@ -0,0 +1,149 @@ +# br-sta-helm + +## Chart Contract + +- Chart type: `multi-component` +- Required secrets: `None for default render`. With the bundled PostgreSQL and Valkey subcharts the database and Redis passwords are auto-generated and read via `secretKeyRef`. Only supply `brSta.secrets.POSTGRES_PASSWORD` / `brSta.secrets.REDIS_PASSWORD` for external infra without `postgresql.auth.existingSecret` / `valkey.auth.existingSecret`. `brSta.secrets.MULTI_TENANT_SERVICE_API_KEY` is required only when `MULTI_TENANT_ENABLED=true`. +- Dependency notes: Bundles two local subcharts — Bitnami `postgresql` (`postgresql.enabled`, default `true`) and Bitnami `valkey` (`valkey.enabled`, default `true`). RabbitMQ is optional, config-only, and NOT bundled (`RABBITMQ_ENABLED=false`). All can be pointed at external services. +- Production overrides: Disable the bundled subcharts and point `POSTGRES_HOST` / `REDIS_HOST` at managed services; supply credentials via chart secrets or an existing Secret; override image tag, ingress, resources, and persistence. +- Source/license: Source is in `github.com/LerianStudio/br-sta`; chart license is Apache-2.0. + +A Helm chart for [br-sta](https://github.com/LerianStudio/br-sta) — a Lerian Studio Go/Fiber HTTP service. It ships a manager Deployment running the `/service` binary and an optional worker Deployment running background jobs, backed by PostgreSQL (SQL migrations applied at startup) and Redis/Valkey (caching + rate limiting). + +## TL;DR + +```bash +helm repo add lerian https://lerianstudio.github.io/helm +helm install my-br-sta lerian/br-sta-helm \ + --namespace br-sta --create-namespace +``` + +The default render brings up br-sta plus an in-cluster PostgreSQL and Valkey, with no operator-provided secrets required. + +## Prerequisites + +- Kubernetes 1.23+ +- Helm 3.10+ +- Either the bundled PostgreSQL/Valkey subcharts (default) or externally managed PostgreSQL 16+ and Redis/Valkey. + +## Architecture + +The chart deploys a **manager Deployment** (`/service`, a Go/Fiber HTTP server) and an optional **worker Deployment** (`/service`, built from `cmd/worker`, background jobs): + +- HTTP API served on port `8080` (`/health`, `/api/v1/...`). +- PostgreSQL is the primary datastore; SQL migrations are applied at startup from `MIGRATIONS_PATH` (`migrations`). +- Redis/Valkey provides caching and rate limiting (`REDIS_*`). + +### Optional integrations (disabled by default) + +| Toggle | Default | Purpose | +|--------|---------|---------| +| `RABBITMQ_ENABLED` | `"false"` | Event-driven starter (config-only; broker not bundled). | +| `OUTBOX_ENABLED` | `"false"` | Transactional outbox dispatcher. | +| `PLUGIN_AUTH_ENABLED` | `"false"` | lib-auth / plugin-auth integration. | +| `MULTI_TENANT_ENABLED` | `"false"` | Multi-tenant mode via tenant-manager. | +| `ENABLE_TELEMETRY` | `"false"` | OpenTelemetry OTLP export. | + +## Storage + +- **PostgreSQL** — application data + SQL migrations. +- **Redis/Valkey** — cache + rate-limiting counters. + +## Single-source infra credentials + +Following [`docs/helm-chart-standard.md`](../../docs/helm-chart-standard.md): + +- With the bundled **PostgreSQL** subchart (default), the password is auto-generated into the subchart's own Secret and read by the app via `secretKeyRef` (key `password`) — leave `brSta.secrets.POSTGRES_PASSWORD` empty. +- With the bundled **Valkey** subchart (default), the password is auto-generated into the subchart's own Secret and read via `secretKeyRef` (key `valkey-password`) — leave `brSta.secrets.REDIS_PASSWORD` empty. +- For external infra (subchart disabled), supply `brSta.secrets.POSTGRES_PASSWORD` / `brSta.secrets.REDIS_PASSWORD`, or set `postgresql.auth.existingSecret` / `valkey.auth.existingSecret`. + +## Required configuration + +The chart **fails fast** on `helm install` only when an enabled optional integration is missing its inputs: + +| Field | When required | +|-------|---------------| +| `brSta.configmap.MULTI_TENANT_URL` | `MULTI_TENANT_ENABLED=true` | +| `brSta.secrets.MULTI_TENANT_SERVICE_API_KEY` | `MULTI_TENANT_ENABLED=true` | + +## Probes + +| Probe | Path | Notes | +|-------|------|-------| +| Liveness | `/health` | HTTP self-probe. | +| Readiness | `/health` | br-sta exposes a single `/health` endpoint. | + +## Common values + +| Key | Default | Description | +|-----|---------|-------------| +| `br-sta.replicaCount` | `2` | Number of replicas. | +| `brSta.image.repository` | `ghcr.io/lerianstudio/br-sta` | Container image. | +| `brSta.image.tag` | `""` (Chart `appVersion`) | Image tag. | +| `br-sta.service.port` | `8080` | Service port. | +| `br-sta.ingress.enabled` | `false` | Expose via Ingress. | +| `br-sta.autoscaling.enabled` | `true` | Enable HPA. | +| `postgresql.enabled` | `true` | Deploy the in-cluster PostgreSQL subchart. | +| `postgresql.architecture` | `replication` | Primary + read replica. | +| `valkey.enabled` | `true` | Deploy the in-cluster Valkey subchart. | +| `valkey.architecture` | `standalone` | Single Valkey primary. | +| `global.externalPostgresDefinitions.enabled` | `false` | Run a bootstrap Job against an external PostgreSQL. | +| `otel-collector-lerian.enabled` | `false` | Inject host-level OTLP endpoint env vars. | + +See [`values.yaml`](./values.yaml) for the full list, and [`values-template.yaml`](./values-template.yaml) for a production overlay starter. + +## Production layout + +1. **Disable the bundled infra** and point at managed services: + ```yaml + postgresql: + enabled: false + valkey: + enabled: false + br-sta: + configmap: + POSTGRES_HOST: my-rds-instance.example.com + POSTGRES_SSLMODE: require + REDIS_HOST: my-redis.example.com:6379 + secrets: + POSTGRES_PASSWORD: + REDIS_PASSWORD: + ``` + +2. **Use an existing Secret** instead of inline values: + ```yaml + br-sta: + useExistingSecret: true + existingSecretName: br-sta-secrets + ``` + +3. **Optional bootstrap** for a fresh external Postgres (creates DB + role + grants, idempotent): + ```yaml + global: + externalPostgresDefinitions: + enabled: true + connection: + host: my-rds-instance.example.com + port: "5432" + postgresAdminLogin: + username: postgres + password: + appCredentials: + password: + ``` + +## Uninstall + +```bash +helm uninstall my-br-sta -n br-sta +``` + +If the bundled PostgreSQL was used, its PVCs are NOT deleted automatically: + +```bash +kubectl delete pvc -n br-sta -l app.kubernetes.io/instance=my-br-sta +``` + +## License + +[Apache 2.0](../../LICENSE) (chart). diff --git a/charts/br-sta/README.params.md b/charts/br-sta/README.params.md new file mode 100644 index 000000000..aed819134 --- /dev/null +++ b/charts/br-sta/README.params.md @@ -0,0 +1,108 @@ +# Parameters + +## Parameters + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `nameOverride` | string | `"br-sta"` | Override the chart top-level name | +| `fullnameOverride` | string | `""` | Override the fully generated name | +| `namespaceOverride` | string | `""` | Override the namespace used by templates | +| `global.externalPostgresDefinitions` | string | `{}` | Bootstrap job for external PostgreSQL: creates database and role | +| `global.externalPostgresDefinitions.enabled` | bool | `false` | Enable or disable the PostgreSQL bootstrap job | +| `global.externalPostgresDefinitions.connection` | string | `{}` | PostgreSQL connection settings (used by the bootstrap job only) | +| `global.externalPostgresDefinitions.connection.host` | string | `"br-sta-postgresql-primary"` | PostgreSQL host | +| `global.externalPostgresDefinitions.connection.port` | string | `"5432"` | PostgreSQL port | +| `global.externalPostgresDefinitions.postgresAdminLogin` | string | `{}` | Admin (superuser) credentials used to create the application DB and role | +| `global.externalPostgresDefinitions.postgresAdminLogin.useExistingSecret.name` | string | `""` | Name of existing secret containing DB_USER_ADMIN and DB_ADMIN_PASSWORD keys | +| `global.externalPostgresDefinitions.postgresAdminLogin.username` | string | `"postgres"` | Admin username (ignored if useExistingSecret.name is set) | +| `global.externalPostgresDefinitions.postgresAdminLogin.password` | string | `""` | Admin password (ignored if useExistingSecret.name is set) | +| `global.externalPostgresDefinitions.appCredentials` | string | `{}` | Credentials for the br_sta role created by the job | +| `global.externalPostgresDefinitions.appCredentials.useExistingSecret.name` | string | `""` | Name of existing secret containing DB_PASSWORD_BR_STA key | +| `global.externalPostgresDefinitions.appCredentials.password` | string | `""` | Password for br_sta role (ignored if useExistingSecret.name is set) | +| `global.observability` | string | `{}` | Env-wide observability, consumed by lerian-common.otel.env. Declare once at the umbrella level; a component configmap. still overrides per-service. Precedence: brSta.configmap. > global.observability. > chart default. | +| `global.observability.enabled` | bool | `false` | Enable telemetry export (ENABLE_TELEMETRY). Unset → chart default "false". | +| `global.observability.otlpEndpoint` | string | `""` | OTLP collector endpoint (OTEL_EXPORTER_OTLP_ENDPOINT). Unset → chart default "". | +| `global.observability.deploymentEnvironment` | string | `"production"` | Deployment environment tag (OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT). Unset → "production". | +| `global.multiTenant` | object | `{}` | Env-wide multi-tenant infra (tenant-manager URL + its Redis), consumed by lerian-common.multiTenant.env. Only used when MT is enabled (see brSta.multiTenant.enabled). A component configmap. still overrides. | +| `global.datastores` | object | `{}` | Env-wide datastore mask, consumed by lerian-common.datastore.value. Declare a SHARED instance once here; a DEDICATED per-service instance goes under brSta.datastores; a component configmap. still overrides everything. Precedence: brSta.configmap. > brSta.datastores.. > global.datastores.. > chart default. | +| `global.serviceDiscovery` | object | `{}` | Env-wide service discovery (Consul), consumed by lerian-common.serviceDiscovery.env. Only used when SD is enabled (brSta.serviceDiscovery.enabled). Leave {} for defaults. | +| `global.streaming` | object | `{}` | Env-wide streaming (lib-streaming → Kafka), consumed by lerian-common.streaming.env. Only used when streaming is enabled (brSta.streaming.enabled). Leave {} for defaults. | +| `global.auth` | object | `{}` | Env-wide inbound auth (access-manager), consumed by lerian-common.globalValue for PLUGIN_AUTH_ENABLED / PLUGIN_AUTH_HOST. A component configmap. still overrides. Precedence: brSta.configmap.PLUGIN_AUTH_* > global.auth. > chart default. | +| `brSta.multiTenant` | string | `{}` | Multi-tenancy toggle (grouped API for MULTI_TENANT_ENABLED). Tenant-manager URL + Redis infra come from global.multiTenant (or configmap.MULTI_TENANT_*). Precedence: configmap.MULTI_TENANT_ENABLED > brSta.multiTenant.enabled > "false". | +| `brSta.multiTenant.enabled` | bool | `false` | Enable multi-tenancy (MULTI_TENANT_ENABLED) | +| `brSta.datastores` | object | `{}` | Dedicated datastore mask for THIS service (see global.datastores for the shared form + precedence). Same fields as global.datastores; wins over global, loses to configmap.. Leave empty ({}) to keep the bundled-subchart / native defaults. | +| `brSta.name` | string | `"br-sta"` | Service name | +| `brSta.replicaCount` | int | `2` | Number of replicas | +| `brSta.revisionHistoryLimit` | int | `10` | Number of old ReplicaSets to retain for rollback | +| `brSta.annotations` | object | `{}` | Annotations applied to the Deployment resource | +| `brSta.podAnnotations` | object | `{}` | Annotations applied to the pods | +| `brSta.image.repository` | string | `ghcr.io/lerianstudio/br-sta` | Repository for the br-sta image | +| `brSta.image.pullPolicy` | string | `IfNotPresent` | Image pull policy | +| `brSta.image.tag` | string | `""` | Image tag (defaults to Chart.appVersion if empty) | +| `brSta.imagePullSecrets` | list | `[]` | Image pull secrets for private registries | +| `brSta.migrations` | string | `{}` | Database migrations Job (init/postgres-migrations runner image). Applies the SQL migrations once as an Argo hook — PreSync for external Postgres (schema-first, before the app rolls out) or PostSync for the bundled subchart (after the DB is provisioned) — so schema changes are applied by a dedicated process rather than at application runtime. Disabled by default; enable per-environment (e.g. the dev-st gitops values). | +| `brSta.migrations.enabled` | bool | `false` | Enable or disable the migrations Job. | +| `brSta.migrations.useExistingSecret` | bool | `false` | When true, read POSTGRES_PASSWORD from a pre-existing Secret (existingSecretName) instead of the chart-managed app Secret. | +| `brSta.migrations.existingSecretName` | string | `""` | Name of the pre-existing Secret containing POSTGRES_PASSWORD (only used when useExistingSecret=true). | +| `brSta.migrations.image.repository` | string | `ghcr.io/lerianstudio/br-sta-migrations` | Repository for the migrations runner image. | +| `brSta.migrations.image.tag` | string | `""` | Tag for the migrations image. Defaults to the app image tag (brSta.image.tag, or the chart appVersion) when left empty. | +| `brSta.migrations.image.digest` | string | `""` | Optional image digest (overrides tag when set). | +| `brSta.migrations.image.pullPolicy` | string | `IfNotPresent` | Image pull policy. | +| `brSta.migrations.path` | string | `"/migrations"` | Path inside the image where the SQL migrations live. | +| `brSta.migrations.allowInsecureTLS` | string | `""` | Bypass the lib-commons migrator TLS guard for a non-TLS Postgres (POSTGRES_SSLMODE=disable is rejected without it). When empty, inherits ALLOW_INSECURE_TLS from brSta.extraEnvVars, then brSta.configmap. Leave empty for a TLS-enabled Postgres. | +| `brSta.migrations.backoffLimit` | int | `3` | Maximum retries before the Job is considered failed. | +| `brSta.migrations.activeDeadlineSeconds` | int | `600` | Hard wall-clock cap for the Job (seconds). | +| `brSta.migrations.ttlSecondsAfterFinished` | int | `600` | Seconds to retain the finished Job before garbage collection. | +| `brSta.migrations.timeoutSeconds` | string | `""` | Optional MIGRATIONS_TIMEOUT_SEC passed to the runner (per-run deadline). | +| `brSta.migrations.resources` | string | `{}` | Resource requests/limits for the migrations container. | +| `brSta.migrations.annotations` | object | `{}` | Extra annotations on the Job resource. | +| `brSta.migrations.podAnnotations` | object | `{}` | Extra annotations on the migration pod. | +| `brSta.nameOverride` | string | `""` | Override of the resource name | +| `brSta.fullnameOverride` | string | `""` | Override of the fully qualified resource name | +| `brSta.terminationGracePeriodSeconds` | int | `60` | Termination grace period. | +| `brSta.podSecurityContext` | object | `{}` | Pod security context | +| `brSta.securityContext` | string | `{}` | Container security context (Distroless nonroot UID/GID is 65532) | +| `brSta.pdb` | string | `{}` | PodDisruptionBudget configuration | +| `brSta.deploymentStrategy` | string | `{}` | Deployment strategy | +| `brSta.service` | string | `{}` | Service configuration | +| `brSta.ingress` | string | `{}` | Ingress configuration | +| `brSta.resources` | string | `{}` | Resource requests and limits | +| `brSta.autoscaling` | string | `{}` | HorizontalPodAutoscaler configuration | +| `brSta.readinessProbe` | string | `{}` | Readiness probe configuration | +| `brSta.livenessProbe` | string | `{}` | Liveness probe configuration | +| `brSta.nodeSelector` | object | `{}` | Node selector for scheduling pods on specific nodes | +| `brSta.tolerations` | list | `[]` | Tolerations for scheduling on tainted nodes | +| `brSta.affinity` | object | `{}` | Affinity rules for pod scheduling | +| `brSta.hostAliases` | list | `[]` | Host aliases for custom DNS resolution inside the pod | +| `brSta.configmap` | object | `templates/configmap.yaml` | Primary override surface: raw ConfigMap env-var passthrough. Every non-dependency app key defaults in templates/configmap.yaml; set brSta.configmap. here to override a default or pass an opt-in key (e.g. AUDIT_PUBLISHER_INTERVAL_SEC). Dependency connections are the ONLY typed knobs — Postgres/Redis/RabbitMQ HOST/PORT/ USER/SSL via the datastores mask; MULTI_TENANT/OTEL/streaming/SD via the global.* blocks; PLUGIN_AUTH via global.auth. Precedence: configmap. > mask/global > default. | +| `brSta.serviceDiscovery` | string | `{}` | Service discovery toggle (grouped API for SD_ENABLED). SD_TOKEN (ACL) is a Secret. | +| `brSta.streaming` | string | `{}` | Streaming toggle (grouped API for STREAMING_ENABLED). SASL password is a Secret. | +| `brSta.secrets` | string | `templates/secrets.yaml` | Secrets (sensitive environment variables) | +| `brSta.useExistingSecret` | bool | `false` | Use an externally managed Secret instead of generating one | +| `brSta.existingSecretName` | string | `""` | Name of the externally managed Secret | +| `brSta.extraEnvVars` | object | `{}` | Extra environment variables (map of key:value pairs) | +| `brSta.serviceAccount` | string | `{}` | ServiceAccount configuration | +| `worker.enabled` | bool | `false` | Enable the worker Deployment. REQUIRES worker.image to point at a dedicated worker image (built from cmd/worker) — keep this false until that image is confirmed available, then enable per-deployment. | +| `worker.replicaCount` | int | `1` | Replicas. The background jobs are leader-gated but the worker does NOT run its own leader election yet — keep this at 1 (and do not add an HPA) to avoid duplicate processing. | +| `worker.revisionHistoryLimit` | int | `10` | Number of old ReplicaSets to retain for rollback | +| `worker.annotations` | object | `{}` | Annotations applied to the Deployment resource | +| `worker.podAnnotations` | object | `{}` | Annotations applied to the pods | +| `worker.image` | string | `{}` | Worker image. REQUIRED: point at the dedicated worker image (built from cmd/worker). Empty repository/tag fall back to the manager image, which does NOT run the worker's logic — only set for local testing. | +| `worker.imagePullSecrets` | list | `[]` | Image pull secrets (empty inherits the manager's) | +| `worker.command` | string | `{}` | Container command. The worker image's own ENTRYPOINT already runs the correct binary (/service, built from cmd/worker) — this override matches that ENTRYPOINT rather than replacing it with a different binary name. | +| `worker.terminationGracePeriodSeconds` | int | `60` | Termination grace period | +| `worker.podSecurityContext` | object | `{}` | Pod security context | +| `worker.securityContext` | object | `{}` | Container security context (empty inherits the manager's) | +| `worker.serviceAccount` | string | `{}` | ServiceAccount configuration | +| `worker.service` | string | `{}` | Probe server port. WorkerMode binds SERVER_ADDRESS here; no Service or Ingress is created for the worker. | +| `worker.resources` | string | `{}` | Resource requests and limits | +| `worker.deploymentStrategy` | string | `{}` | Deployment strategy. Recreate by default: the worker is leader-gated but runs no leader election, so a RollingUpdate surge would briefly run two worker pods and duplicate background processing (audit drain, scheduler). | +| `worker.readinessProbe` | string | `{}` | Readiness probe configuration | +| `worker.livenessProbe` | string | `{}` | Liveness probe configuration | +| `worker.nodeSelector` | object | `{}` | Node selector for scheduling pods on specific nodes | +| `worker.tolerations` | list | `[]` | Tolerations for scheduling on tainted nodes | +| `worker.affinity` | object | `{}` | Affinity rules for pod scheduling | +| `worker.hostAliases` | list | `[]` | Host aliases for custom DNS resolution inside the pod | +| `worker.configmap` | string | `templates/worker/configmap.yaml` | Worker-only environment (layered on top of the manager ConfigMap/Secret). SetConfigFromEnvVars ignores envDefault, so every knob a background job needs must be set explicitly. SERVER_ADDRESS is derived from service.port. | +| `worker.extraEnvVars` | object | `{}` | Extra environment variables (map of key:value pairs) rendered as inline container env (wins over both ConfigMaps). | + diff --git a/charts/br-sta/templates/NOTES.txt b/charts/br-sta/templates/NOTES.txt new file mode 100644 index 000000000..a48f7622c --- /dev/null +++ b/charts/br-sta/templates/NOTES.txt @@ -0,0 +1,94 @@ +{{- $component := .Values.brSta -}} +# br-sta + +Thank you for installing {{ .Chart.Name }} v{{ .Chart.Version }}! + +## Deployment Status + +{{ if .Release.IsUpgrade }} +The chart has been UPGRADED to version {{ .Chart.Version }} (appVersion {{ .Chart.AppVersion }}). +{{ else }} +The chart has been INSTALLED at version {{ .Chart.Version }} (appVersion {{ .Chart.AppVersion }}). +{{ end }} + +## Component Deployed + +Manager Deployment ({{ $component.replicaCount }} replica(s)) running the /service binary +(Go/Fiber HTTP server). + + Service: {{ include "br-sta.fullname" . }}.{{ include "global.namespace" . }}.svc.cluster.local:{{ $component.service.port }} + Health: /health + API: /api/v1/... + +{{- if eq (include "br-sta.worker.enabled" .) "true" }} + +Worker Deployment ({{ (.Values.worker | default dict).replicaCount | default 1 }} replica(s)) running the /service binary +(background jobs: audit outbox publisher/consumer, leader-gated scheduler, credential-recovery-on-boot). +{{- end }} + +## Dependencies + +{{- if eq (include "postgresql.enabled" .) "true" }} +{{- $pgArch := default "standalone" .Values.postgresql.architecture }} +{{- $pgFullname := include "common.names.dependency.fullname" (dict "chartName" "postgresql" "chartValues" (index .Values "postgresql") "context" .) }} +1. PostgreSQL (in-cluster, Bitnami subchart) + - Primary: {{ $pgFullname }}{{ if eq $pgArch "replication" }}-primary{{ end }}.{{ include "global.namespace" . }}.svc.cluster.local:5432 + - Database: {{ .Values.postgresql.auth.database }} + - User: {{ .Values.postgresql.auth.username }} +{{- else if .Values.global.externalPostgresDefinitions.enabled }} +1. PostgreSQL (external, bootstrapped by Helm) + - Host: {{ .Values.global.externalPostgresDefinitions.connection.host }}:{{ .Values.global.externalPostgresDefinitions.connection.port }} + - Database/Role: br_sta +{{- else }} +1. PostgreSQL (external, expected to be pre-provisioned) + - Configure POSTGRES_HOST/POSTGRES_PORT/POSTGRES_USER/POSTGRES_NAME/POSTGRES_PASSWORD before installing. +{{- end }} + +{{- if eq (include "valkey.enabled" .) "true" }} +2. Redis/Valkey (in-cluster, Bitnami subchart) + - Host: {{ include "common.names.dependency.fullname" (dict "chartName" "valkey" "chartValues" (index .Values "valkey") "context" .) }}-primary.{{ include "global.namespace" . }}.svc.cluster.local:6379 +{{- else }} +2. Redis/Valkey (external, expected to be pre-provisioned) + - Configure REDIS_HOST (host:port) and REDIS_PASSWORD before installing. +{{- end }} + +## Optional Integrations (disabled by default) + + - RabbitMQ (RABBITMQ_ENABLED) — event-driven starter (config-only; not bundled) + - Outbox (OUTBOX_ENABLED) — transactional outbox dispatcher + - plugin-auth (PLUGIN_AUTH_ENABLED) — lib-auth integration + - Multi-tenant (MULTI_TENANT_ENABLED) — tenant-manager (requires MULTI_TENANT_URL + MULTI_TENANT_SERVICE_API_KEY) + - Telemetry (ENABLE_TELEMETRY) — OpenTelemetry OTLP export + +## Required Configuration + +The default render (bundled PostgreSQL + Valkey) requires no operator secrets — +the infra passwords are auto-generated by the subcharts and read via secretKeyRef. + +When MULTI_TENANT_ENABLED=true, the following become required: + - brSta.configmap.MULTI_TENANT_URL + - brSta.secrets.MULTI_TENANT_SERVICE_API_KEY + +For an EXTERNAL PostgreSQL / Redis (subchart disabled), supply the passwords via +brSta.secrets.POSTGRES_PASSWORD / brSta.secrets.REDIS_PASSWORD (or existingSecret). + +## Accessing the API + +{{- if contains "ClusterIP" $component.service.type }} + +Port-forward locally: + + kubectl port-forward -n {{ include "global.namespace" . }} svc/{{ include "br-sta.fullname" . }} {{ $component.service.port }}:{{ $component.service.port }} + +Then access: http://localhost:{{ $component.service.port }}/health +{{- end }} + +## Useful Commands + + kubectl get pods -n {{ include "global.namespace" . }} -l app.kubernetes.io/instance={{ .Release.Name }} + kubectl logs -n {{ include "global.namespace" . }} -l app.kubernetes.io/instance={{ .Release.Name }} --tail=200 + +## Source + + - Service source: https://github.com/LerianStudio/br-sta + - Chart source: https://github.com/LerianStudio/helm/tree/main/charts/br-sta diff --git a/charts/br-sta/templates/_helpers.tpl b/charts/br-sta/templates/_helpers.tpl new file mode 100644 index 000000000..ba1359c73 --- /dev/null +++ b/charts/br-sta/templates/_helpers.tpl @@ -0,0 +1,300 @@ +{{/* +================================================================================ +BR-STA - HELM TEMPLATE HELPERS +================================================================================ +br-sta is a Go/Fiber HTTP service. One Deployment, one pod, one process +(the /service binary). It requires PostgreSQL and Redis/Valkey. +================================================================================ +*/}} + +{{/* +================================================================================ +NAME HELPERS +================================================================================ +*/}} + +{{/* +Top-level chart name. +*/}} +{{- define "br-sta.name" -}} +{{- default "br-sta" .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Application name (single deployment). +*/}} +{{- define "br-sta.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- default "br-sta" (.Values.brSta).name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{/* +================================================================================ +CHART HELPERS +================================================================================ +*/}} + +{{- define "br-sta.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Resolve the application image tag (Chart.appVersion if app.image.tag is empty). +*/}} +{{- define "br-sta.defaultTag" -}} +{{- default .Chart.AppVersion (.Values.brSta).image.tag }} +{{- end -}} + +{{/* +Sanitize tag for use in app.kubernetes.io/version label. +*/}} +{{- define "br-sta.versionLabelValue" -}} +{{ regexReplaceAll "[^-A-Za-z0-9_.]" (include "br-sta.defaultTag" .) "-" | trunc 63 | trimAll "-" | trimAll "_" | trimAll "." | quote }} +{{- end -}} + +{{/* +================================================================================ +LABEL HELPERS +================================================================================ +*/}} + +{{/* +Common labels. +Usage: {{ include "br-sta.labels" (dict "context" .) }} +*/}} +{{- define "br-sta.labels" -}} +helm.sh/chart: {{ include "br-sta.chart" .context }} +{{ include "br-sta.selectorLabels" (dict "context" .context) }} +app.kubernetes.io/version: {{ include "br-sta.versionLabelValue" .context }} +app.kubernetes.io/managed-by: {{ .context.Release.Service }} +app.kubernetes.io/part-of: br-sta +{{- end }} + +{{/* +Selector labels. +*/}} +{{- define "br-sta.selectorLabels" -}} +app.kubernetes.io/name: {{ include "br-sta.name" .context }} +app.kubernetes.io/instance: {{ .context.Release.Name }} +{{- end }} + +{{/* +================================================================================ +SERVICE ACCOUNT HELPER +================================================================================ +*/}} + +{{- define "br-sta.serviceAccountName" -}} +{{- if (.Values.brSta).serviceAccount.create }} +{{- default (include "br-sta.fullname" .) (.Values.brSta).serviceAccount.name }} +{{- else }} +{{- default "default" (.Values.brSta).serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +================================================================================ +WORKER HELPERS +================================================================================ +The worker is a SECOND Deployment running the /service binary (background jobs: +audit publisher/consumer, scheduler, credential-recovery sweep, inbound poll). +It reuses the manager's ConfigMap + Secret (all shared infra config) and layers +worker-only env on top. It gets its OWN app.kubernetes.io/name so its selector +never overlaps the manager's (selectors are immutable across upgrades). +================================================================================ +*/}} + +{{/* Worker resource name: "-worker". */}} +{{- define "br-sta.worker.fullname" -}} +{{- printf "%s-worker" (include "br-sta.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- end }} + +{{/* Worker app name (distinct from the manager so selectors don't overlap). */}} +{{- define "br-sta.worker.name" -}} +{{- printf "%s-worker" (include "br-sta.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end }} + +{{/* Worker selector labels — distinct name keeps the two Deployments apart. */}} +{{- define "br-sta.worker.selectorLabels" -}} +app.kubernetes.io/name: {{ include "br-sta.worker.name" .context }} +app.kubernetes.io/instance: {{ .context.Release.Name }} +{{- end }} + +{{/* Worker common labels. */}} +{{- define "br-sta.worker.labels" -}} +helm.sh/chart: {{ include "br-sta.chart" .context }} +{{ include "br-sta.worker.selectorLabels" (dict "context" .context) }} +app.kubernetes.io/version: {{ include "br-sta.versionLabelValue" .context }} +app.kubernetes.io/managed-by: {{ .context.Release.Service }} +app.kubernetes.io/part-of: br-sta +app.kubernetes.io/component: worker +{{- end }} + +{{/* Worker ServiceAccount name (own SA, or reuse the manager's when not creating). */}} +{{- define "br-sta.worker.serviceAccountName" -}} +{{- $w := .Values.worker | default dict -}} +{{- $sa := $w.serviceAccount | default dict -}} +{{- if $sa.create }} +{{- default (include "br-sta.worker.fullname" .) $sa.name }} +{{- else }} +{{- default (include "br-sta.serviceAccountName" .) $sa.name }} +{{- end }} +{{- end }} + +{{/* Worker enabled — nil-aware: unset/true enables, explicit false disables. */}} +{{- define "br-sta.worker.enabled" -}} +{{- $w := .Values.worker | default dict -}} +{{- if ne (toString $w.enabled) "false" -}} +true +{{- else -}} +false +{{- end -}} +{{- end -}} + +{{/* +================================================================================ +NAMESPACE HELPER +================================================================================ +*/}} + +{{- define "global.namespace" -}} +{{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} +{{- end }} + +{{/* +infraSecretRef — emit a `- name: valueFrom: secretKeyRef: {name,key}` env entry +pointing at a Bitnami subchart's generated Secret (or the operator's existingSecret override). +Inputs (dict): context (root .), subchart ("postgresql"|"valkey"), +key (data key), envName (container env var name). +See docs/helm-chart-standard.md "Single-Source Infra Secrets". +*/}} +{{- define "br-sta.infraSecretRef" -}} +{{- $ctx := .context -}} +{{- $sub := .subchart -}} +{{- $auth := default dict (index $ctx.Values $sub "auth") -}} +{{- $secretName := "" -}} +{{- if $auth.existingSecret -}} +{{- $secretName = $auth.existingSecret -}} +{{- else -}} +{{- $secretName = include "common.names.dependency.fullname" (dict "chartName" $sub "chartValues" (index $ctx.Values $sub) "context" $ctx) -}} +{{- end -}} +- name: {{ .envName }} + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ .key }} +{{- end }} + +{{/* +================================================================================ +DEPENDENCY ENABLED HELPERS +================================================================================ +*/}} + +{{/* Nil-aware: ne (toString .enabled) "false" is true for unset/true, false only for + an explicit false — avoids the `default true` coercion GOTCHA in the standard. */}} +{{- define "postgresql.enabled" -}} +{{- if and (ne (toString .Values.postgresql.enabled) "false") (not .Values.postgresql.external) -}} +true +{{- else -}} +false +{{- end -}} +{{- end -}} + +{{- define "valkey.enabled" -}} +{{- if and (ne (toString .Values.valkey.enabled) "false") (not .Values.valkey.external) -}} +true +{{- else -}} +false +{{- end -}} +{{- end -}} + +{{/* RabbitMQ is OPTIONAL and disabled by default, so an unset value must NOT + coerce to enabled — the nil-aware check treats unset/true as enabled and an + explicit false (the default) as disabled. Honors .external for external mode. */}} +{{- define "rabbitmq.enabled" -}} +{{- $rmq := .Values.rabbitmq | default dict -}} +{{- if and (ne (toString $rmq.enabled) "false") (not $rmq.external) -}} +true +{{- else -}} +false +{{- end -}} +{{- end -}} + +{{/* +================================================================================ +VALIDATION HELPERS +================================================================================ +ERRORS (fail) block deployment for truly required fields. br-sta only requires +operator input for optional integrations that have been toggled on (multi-tenant). +================================================================================ +*/}} + +{{- define "br-sta.validateRequired" -}} + +{{/* PostgreSQL password is single-sourced from the postgresql subchart Secret + via secretKeyRef; see docs/helm-chart-standard.md "Single-Source Infra + Secrets". No gate here: for the bundled subchart the value is generated; + for external Postgres the operator supplies postgresql.auth.existingSecret + or brSta.secrets.POSTGRES_PASSWORD. The same reasoning applies to Valkey. */}} + +{{/* Multi-tenant required fields when enabled */}} +{{- if eq ((.Values.brSta).configmap.MULTI_TENANT_ENABLED | toString) "true" }} +{{- if not (.Values.brSta).configmap.MULTI_TENANT_URL }} +{{- fail "\n\nERROR: brSta.configmap.MULTI_TENANT_URL is REQUIRED when MULTI_TENANT_ENABLED=true.\n" }} +{{- end }} +{{- if not (.Values.brSta).secrets.MULTI_TENANT_SERVICE_API_KEY }} +{{- fail "\n\nERROR: brSta.secrets.MULTI_TENANT_SERVICE_API_KEY is REQUIRED when MULTI_TENANT_ENABLED=true.\n" }} +{{- end }} +{{- end }} + +{{/* existingSecretName is required whenever useExistingSecret is enabled, + otherwise secretRef.name renders empty and the pod fails to start. */}} +{{- if (.Values.brSta).useExistingSecret }} +{{- if not (.Values.brSta).existingSecretName }} +{{- fail "\n\nERROR: br-sta.existingSecretName is REQUIRED when br-sta.useExistingSecret=true.\n" }} +{{- end }} +{{- end }} + +{{- end }} + +{{/* +Generate annotation listing default-value warnings (non-blocking). +*/}} +{{- define "br-sta.secretWarnings" -}} +{{- $warnings := list -}} +{{- if eq (include "postgresql.enabled" .) "true" -}} +{{- if eq (.Values.postgresql.auth.password | toString) "lerian" -}} +{{- $warnings = append $warnings "postgresql.auth.password is using default value 'lerian'" -}} +{{- end -}} +{{- end -}} +{{- if eq (include "valkey.enabled" .) "true" -}} +{{- if eq (.Values.valkey.auth.password | toString) "lerian" -}} +{{- $warnings = append $warnings "valkey.auth.password is using default value 'lerian'" -}} +{{- end -}} +{{- end -}} +{{- if gt (len $warnings) 0 -}} +lerian.studio/security-warnings: {{ $warnings | join "; " | quote }} +{{- end -}} +{{- end -}} + +{{/* +Vendored from Bitnami common (charts/common/templates/_names.tpl) so infra +Secret/Service names render even when all bundled subcharts are disabled +(external-infra path). Self-contained: no other common.* helpers required. +*/}} +{{- define "common.names.dependency.fullname" -}} +{{- if .chartValues.fullnameOverride -}} +{{- .chartValues.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .chartName .chartValues.nameOverride -}} +{{- if contains $name .context.Release.Name -}} +{{- .context.Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .context.Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/charts/br-sta/templates/bootstrap-postgres.yaml b/charts/br-sta/templates/bootstrap-postgres.yaml new file mode 100644 index 000000000..801c4c179 --- /dev/null +++ b/charts/br-sta/templates/bootstrap-postgres.yaml @@ -0,0 +1,150 @@ +{{- if .Values.global.externalPostgresDefinitions.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "br-sta.fullname" . }}-bootstrap-postgres + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} + app.kubernetes.io/component: bootstrap-postgres + annotations: + "helm.sh/hook": "pre-install,pre-upgrade" + "helm.sh/hook-weight": "-4" + "helm.sh/hook-delete-policy": "before-hook-creation" +spec: + ttlSecondsAfterFinished: 300 + completions: 1 + parallelism: 1 + backoffLimit: 3 + template: + spec: + restartPolicy: Never + securityContext: + seccompProfile: + type: RuntimeDefault + initContainers: + - name: wait-for-dependencies + image: busybox:1.37 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + env: + - name: DB_HOST + value: {{ .Values.global.externalPostgresDefinitions.connection.host | quote }} + - name: DB_PORT + value: {{ .Values.global.externalPostgresDefinitions.connection.port | quote }} + command: + - /bin/sh + - -c + - > + TIMEOUT=300; + ELAPSED=0; + echo "Checking $DB_HOST:$DB_PORT..."; + while ! nc -z "$DB_HOST" "$DB_PORT"; do + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "Timeout waiting for $DB_HOST:$DB_PORT after ${TIMEOUT}s"; + exit 1; + fi; + echo "$DB_HOST:$DB_PORT not ready, waiting (${ELAPSED}s/${TIMEOUT}s)"; + sleep 5; + ELAPSED=$((ELAPSED + 5)); + done; + echo "$DB_HOST:$DB_PORT is ready!"; + containers: + - name: psql + image: postgres:17 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + env: + - name: DB_HOST + value: {{ .Values.global.externalPostgresDefinitions.connection.host | quote }} + - name: DB_PORT + value: {{ .Values.global.externalPostgresDefinitions.connection.port | quote }} + - name: DB_USER_ADMIN + {{- if .Values.global.externalPostgresDefinitions.postgresAdminLogin.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ .Values.global.externalPostgresDefinitions.postgresAdminLogin.useExistingSecret.name | quote }} + key: DB_USER_ADMIN + {{- else }} + value: {{ .Values.global.externalPostgresDefinitions.postgresAdminLogin.username | quote }} + {{- end }} + - name: DB_ADMIN_PASSWORD + {{- if .Values.global.externalPostgresDefinitions.postgresAdminLogin.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ .Values.global.externalPostgresDefinitions.postgresAdminLogin.useExistingSecret.name | quote }} + key: DB_ADMIN_PASSWORD + {{- else }} + value: {{ .Values.global.externalPostgresDefinitions.postgresAdminLogin.password | quote }} + {{- end }} + - name: DB_PASSWORD_BR_STA + {{- if .Values.global.externalPostgresDefinitions.appCredentials.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ .Values.global.externalPostgresDefinitions.appCredentials.useExistingSecret.name | quote }} + key: DB_PASSWORD_BR_STA + {{- else }} + value: {{ .Values.global.externalPostgresDefinitions.appCredentials.password | quote }} + {{- end }} + - name: DB_DATABASE + value: postgres + - name: APP_DB + value: br_sta + - name: APP_ROLE + value: br_sta + command: + - /bin/sh + - -c + - | + set -euo pipefail + echo "=== br-sta PostgreSQL Bootstrap ===" + echo "Host: $DB_HOST:$DB_PORT" + echo "" + + echo "Checking existing PostgreSQL objects..." + DB_EXISTS=0 + ROLE_EXISTS=0 + + if PGPASSWORD="$DB_ADMIN_PASSWORD" psql -At -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "SELECT 1 FROM pg_database WHERE datname='$APP_DB'" | grep -q 1; then + DB_EXISTS=1 + fi + if PGPASSWORD="$DB_ADMIN_PASSWORD" psql -At -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "SELECT 1 FROM pg_roles WHERE rolname='$APP_ROLE'" | grep -q 1; then + ROLE_EXISTS=1 + fi + + if [ "$DB_EXISTS" = "1" ] && [ "$ROLE_EXISTS" = "1" ]; then + echo "Bootstrap already complete (database '$APP_DB' and role '$APP_ROLE' exist). Skipping creation." + else + if [ "$ROLE_EXISTS" = "1" ]; then + echo "Role '$APP_ROLE' already exists. Skipping creation." + else + echo "Creating role '$APP_ROLE'..." + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -v pw="$DB_PASSWORD_BR_STA" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "CREATE ROLE ${APP_ROLE} LOGIN PASSWORD :'pw'" + fi + + if [ "$DB_EXISTS" = "1" ]; then + echo "Database '$APP_DB' already exists. Skipping creation." + else + echo "Creating database '$APP_DB'..." + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "CREATE DATABASE ${APP_DB} OWNER ${APP_ROLE}" + fi + fi + + # Privileges (idempotent) + echo "Ensuring privileges and schema permissions..." + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "GRANT ALL PRIVILEGES ON DATABASE ${APP_DB} TO ${APP_ROLE}" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$APP_DB" -c "GRANT ALL ON SCHEMA public TO ${APP_ROLE}" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$APP_DB" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${APP_ROLE}" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$APP_DB" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${APP_ROLE}" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$APP_DB" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${APP_ROLE}" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$APP_DB" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${APP_ROLE}" + + echo "" + echo "=== br-sta PostgreSQL Bootstrap completed successfully ===" +{{- end }} diff --git a/charts/br-sta/templates/configmap.yaml b/charts/br-sta/templates/configmap.yaml new file mode 100644 index 000000000..95b18fea5 --- /dev/null +++ b/charts/br-sta/templates/configmap.yaml @@ -0,0 +1,270 @@ +{{- $component := .Values.brSta -}} +{{- $cm := $component.configmap | default dict -}} +{{- $ds := $component.datastores | default dict -}} +{{- $sd := $component.serviceDiscovery | default dict -}} +{{- $strm := $component.streaming | default dict -}} +{{- $namespace := include "global.namespace" . -}} +{{- /* Dependency-connection knobs (Postgres/Redis/RabbitMQ) resolve via the + lerian-common.datastore.value mask; MULTI_TENANT/OTEL/SD/STREAMING via their + domain helpers; PLUGIN_AUTH via lerian-common.globalValue (global.auth). Every + OTHER app key is an escape-hatch passthrough: configmap. > chart default. + The helper/mask-handled keys are the "$managed" set — excluded from passthrough + so each is single-sourced. */ -}} +{{- $pgName := include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "postgres" "field" "name" "nativeKey" "POSTGRES_NAME" "default" "br_sta") -}} +{{- $multiTenantEnabled := eq (include "lerian-common.cfgValue" (dict "configmap" $cm "nativeKey" "MULTI_TENANT_ENABLED" "params" ($component.multiTenant | default dict) "field" "enabled" "default" "false")) "true" -}} +{{- $sdEnabled := eq (include "lerian-common.cfgValue" (dict "configmap" $cm "nativeKey" "SD_ENABLED" "params" $sd "field" "enabled" "default" "false")) "true" -}} +{{- $streamingEnabled := eq (include "lerian-common.cfgValue" (dict "configmap" $cm "nativeKey" "STREAMING_ENABLED" "params" $strm "field" "enabled" "default" "false")) "true" -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "br-sta.fullname" . }} + namespace: {{ $namespace }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} +data: + # Passthrough config — each key resolves configmap. (escape hatch) + # over the chart default in the template. Set brSta.configmap. to override. + AGGRESSIVE_RATE_LIMIT_MAX: {{ $cm.AGGRESSIVE_RATE_LIMIT_MAX | default "100" | quote }} + AGGRESSIVE_RATE_LIMIT_WINDOW_SEC: {{ $cm.AGGRESSIVE_RATE_LIMIT_WINDOW_SEC | default "60" | quote }} + AWS_REGION: {{ $cm.AWS_REGION | default "us-east-1" | quote }} + CIRCUIT_BREAKER_ENABLED: {{ $cm.CIRCUIT_BREAKER_ENABLED | default "false" | quote }} + CORS_ALLOWED_HEADERS: {{ $cm.CORS_ALLOWED_HEADERS | default "Origin,Content-Type,Accept,Authorization,X-Request-ID" | quote }} + CORS_ALLOWED_METHODS: {{ $cm.CORS_ALLOWED_METHODS | default "GET,POST,PUT,PATCH,DELETE,OPTIONS" | quote }} + CORS_ALLOWED_ORIGINS: {{ $cm.CORS_ALLOWED_ORIGINS | default "*" | quote }} + CORS_ALLOW_CREDENTIALS: {{ $cm.CORS_ALLOW_CREDENTIALS | default "false" | quote }} + CORS_EXPOSE_HEADERS: {{ $cm.CORS_EXPOSE_HEADERS | default "" | quote }} + DB_METRICS_INTERVAL_SEC: {{ $cm.DB_METRICS_INTERVAL_SEC | default "15" | quote }} + DEFAULT_TENANT_ID: {{ $cm.DEFAULT_TENANT_ID | default "11111111-1111-1111-1111-111111111111" | quote }} + ENV_NAME: {{ $cm.ENV_NAME | default "production" | quote }} + EXAMPLE_STATUS_PROVIDER_MODE: {{ $cm.EXAMPLE_STATUS_PROVIDER_MODE | default "healthy" | quote }} + HTTP_BODY_LIMIT_BYTES: {{ $cm.HTTP_BODY_LIMIT_BYTES | default "104857600" | quote }} + IDEMPOTENCY_RETRY_WINDOW_SEC: {{ $cm.IDEMPOTENCY_RETRY_WINDOW_SEC | default "300" | quote }} + INFRA_CONNECT_TIMEOUT_SEC: {{ $cm.INFRA_CONNECT_TIMEOUT_SEC | default "30" | quote }} + LOG_LEVEL: {{ $cm.LOG_LEVEL | default "info" | quote }} + M2M_CREDENTIAL_CACHE_TTL_SEC: {{ $cm.M2M_CREDENTIAL_CACHE_TTL_SEC | default "300" | quote }} + MAX_PAGINATION_LIMIT: {{ $cm.MAX_PAGINATION_LIMIT | default "100" | quote }} + MAX_PAGINATION_MONTH_DATE_RANGE: {{ $cm.MAX_PAGINATION_MONTH_DATE_RANGE | default "3" | quote }} + MIGRATIONS_PATH: {{ $cm.MIGRATIONS_PATH | default "migrations" | quote }} + OUTBOX_ALLOW_EMPTY_TENANT: {{ $cm.OUTBOX_ALLOW_EMPTY_TENANT | default "true" | quote }} + OUTBOX_BATCH_SIZE: {{ $cm.OUTBOX_BATCH_SIZE | default "50" | quote }} + OUTBOX_DISPATCH_INTERVAL_SEC: {{ $cm.OUTBOX_DISPATCH_INTERVAL_SEC | default "2" | quote }} + OUTBOX_ENABLED: {{ $cm.OUTBOX_ENABLED | default "false" | quote }} + OUTBOX_INCLUDE_TENANT_METRICS: {{ $cm.OUTBOX_INCLUDE_TENANT_METRICS | default "false" | quote }} + OUTBOX_MAX_DISPATCH_ATTEMPTS: {{ $cm.OUTBOX_MAX_DISPATCH_ATTEMPTS | default "10" | quote }} + OUTBOX_MAX_FAILED_PER_BATCH: {{ $cm.OUTBOX_MAX_FAILED_PER_BATCH | default "25" | quote }} + OUTBOX_PROCESSING_TIMEOUT_SEC: {{ $cm.OUTBOX_PROCESSING_TIMEOUT_SEC | default "600" | quote }} + OUTBOX_PUBLISH_BACKOFF_MS: {{ $cm.OUTBOX_PUBLISH_BACKOFF_MS | default "200" | quote }} + OUTBOX_PUBLISH_MAX_ATTEMPTS: {{ $cm.OUTBOX_PUBLISH_MAX_ATTEMPTS | default "3" | quote }} + OUTBOX_RETRY_WINDOW_SEC: {{ $cm.OUTBOX_RETRY_WINDOW_SEC | default "300" | quote }} + OUTBOX_TABLE_NAME: {{ $cm.OUTBOX_TABLE_NAME | default "outbox_events" | quote }} + # AUTHENTICATION (inbound): ENABLED + HOST resolve via global.auth + # (globalValue: configmap. > global.auth. > default). + PLUGIN_AUTH_ENABLED: {{ include "lerian-common.globalValue" (dict "context" $ "configmap" $cm "block" "auth" "field" "enabled" "nativeKey" "PLUGIN_AUTH_ENABLED" "default" "false") | quote }} + PLUGIN_AUTH_HOST: {{ include "lerian-common.globalValue" (dict "context" $ "configmap" $cm "block" "auth" "field" "host" "nativeKey" "PLUGIN_AUTH_HOST" "default" "") | quote }} + POSTGRES_CONNECT_TIMEOUT_SEC: {{ $cm.POSTGRES_CONNECT_TIMEOUT_SEC | default "10" | quote }} + POSTGRES_CONN_MAX_IDLE_TIME_MINS: {{ $cm.POSTGRES_CONN_MAX_IDLE_TIME_MINS | default "5" | quote }} + POSTGRES_CONN_MAX_LIFETIME_MINS: {{ $cm.POSTGRES_CONN_MAX_LIFETIME_MINS | default "30" | quote }} + POSTGRES_MAX_IDLE_CONNS: {{ $cm.POSTGRES_MAX_IDLE_CONNS | default "5" | quote }} + POSTGRES_MAX_OPEN_CONNS: {{ $cm.POSTGRES_MAX_OPEN_CONNS | default "25" | quote }} + POSTGRES_NAME: {{ $pgName | quote }} + RABBITMQ_ALLOW_INSECURE_HEALTH_CHECK: {{ $cm.RABBITMQ_ALLOW_INSECURE_HEALTH_CHECK | default "false" | quote }} + RABBITMQ_ALLOW_INSECURE_TLS: {{ $cm.RABBITMQ_ALLOW_INSECURE_TLS | default "false" | quote }} + RABBITMQ_ENABLED: {{ $cm.RABBITMQ_ENABLED | default "false" | quote }} + RABBITMQ_EXCHANGE: {{ $cm.RABBITMQ_EXCHANGE | default "events" | quote }} + RABBITMQ_PORT_HOST: {{ $cm.RABBITMQ_PORT_HOST | default "15672" | quote }} + RABBITMQ_PUBLISHER_CONFIRM_TIMEOUT_MS: {{ $cm.RABBITMQ_PUBLISHER_CONFIRM_TIMEOUT_MS | default "5000" | quote }} + RABBITMQ_PUBLISHER_MAX_RECOVERIES: {{ $cm.RABBITMQ_PUBLISHER_MAX_RECOVERIES | default "10" | quote }} + RABBITMQ_PUBLISHER_RECOVERY_INITIAL_MS: {{ $cm.RABBITMQ_PUBLISHER_RECOVERY_INITIAL_MS | default "1000" | quote }} + RABBITMQ_PUBLISHER_RECOVERY_MAX_MS: {{ $cm.RABBITMQ_PUBLISHER_RECOVERY_MAX_MS | default "30000" | quote }} + RABBITMQ_REQUIRE_HEALTH_ALLOWED_HOSTS: {{ $cm.RABBITMQ_REQUIRE_HEALTH_ALLOWED_HOSTS | default "false" | quote }} + RABBITMQ_VHOST: {{ $cm.RABBITMQ_VHOST | default "/" | quote }} + RATE_LIMIT_ENABLED: {{ $cm.RATE_LIMIT_ENABLED | default "true" | quote }} + RATE_LIMIT_MAX: {{ $cm.RATE_LIMIT_MAX | default "500" | quote }} + RATE_LIMIT_WINDOW_SEC: {{ $cm.RATE_LIMIT_WINDOW_SEC | default "60" | quote }} + REDIS_DB: {{ $cm.REDIS_DB | default "0" | quote }} + REDIS_DIAL_TIMEOUT: {{ $cm.REDIS_DIAL_TIMEOUT | default "5" | quote }} + REDIS_MAX_RETRIES: {{ $cm.REDIS_MAX_RETRIES | default "3" | quote }} + REDIS_MAX_RETRY_BACKOFF: {{ $cm.REDIS_MAX_RETRY_BACKOFF | default "1" | quote }} + REDIS_MIN_IDLE_CONNS: {{ $cm.REDIS_MIN_IDLE_CONNS | default "2" | quote }} + REDIS_MIN_RETRY_BACKOFF: {{ $cm.REDIS_MIN_RETRY_BACKOFF | default "8" | quote }} + REDIS_POOL_SIZE: {{ $cm.REDIS_POOL_SIZE | default "10" | quote }} + REDIS_POOL_TIMEOUT: {{ $cm.REDIS_POOL_TIMEOUT | default "2" | quote }} + REDIS_PROTOCOL: {{ $cm.REDIS_PROTOCOL | default "3" | quote }} + REDIS_READ_TIMEOUT: {{ $cm.REDIS_READ_TIMEOUT | default "3" | quote }} + REDIS_TLS: {{ $cm.REDIS_TLS | default "false" | quote }} + REDIS_WRITE_TIMEOUT: {{ $cm.REDIS_WRITE_TIMEOUT | default "3" | quote }} + RELAXED_RATE_LIMIT_MAX: {{ $cm.RELAXED_RATE_LIMIT_MAX | default "1000" | quote }} + RELAXED_RATE_LIMIT_WINDOW_SEC: {{ $cm.RELAXED_RATE_LIMIT_WINDOW_SEC | default "60" | quote }} + SERVER_ADDRESS: {{ $cm.SERVER_ADDRESS | default "0.0.0.0:8080" | quote }} + SWAGGER_BASE_PATH: {{ $cm.SWAGGER_BASE_PATH | default "/" | quote }} + SWAGGER_ENABLED: {{ $cm.SWAGGER_ENABLED | default "false" | quote }} + SWAGGER_LEFT_DELIM: {{ $cm.SWAGGER_LEFT_DELIM | default "{{" | quote }} + SWAGGER_RIGHT_DELIM: {{ $cm.SWAGGER_RIGHT_DELIM | default "}}" | quote }} + SWAGGER_TITLE: {{ $cm.SWAGGER_TITLE | default "br-sta" | quote }} + SWAGGER_VERSION: {{ $cm.SWAGGER_VERSION | default "1.0.0" | quote }} + SYSTEMPLANE_ENABLED: {{ $cm.SYSTEMPLANE_ENABLED | default "false" | quote }} + TLS_TERMINATED_UPSTREAM: {{ $cm.TLS_TERMINATED_UPSTREAM | default "true" | quote }} + + # Always set VERSION from the resolved image tag. + VERSION: {{ include "br-sta.defaultTag" . | quote }} + + # ===================================================================== + # MULTI-TENANCY (lerian-common.multiTenant.env). Toggle via + # brSta.multiTenant.enabled; URL + Redis infra from global.multiTenant. + # MT infra keys render only when enabled; URL/REDIS_HOST required then. + # ===================================================================== + MULTI_TENANT_ENABLED: {{ $multiTenantEnabled | quote }} + {{- include "lerian-common.multiTenant.env" (dict + "context" $ "configmap" $cm "enabled" $multiTenantEnabled + "emitRedis" true "emitPool" true "emitCache" true + "emitAllowInsecure" true + "requiredUrl" true "requiredRedisHost" true) | nindent 2 }} + {{- if $multiTenantEnabled }} + MULTI_TENANT_POOL_MAX_CONNS: {{ $cm.MULTI_TENANT_POOL_MAX_CONNS | default "20" | quote }} + MULTI_TENANT_POOL_MAX_IDLE_CONNS: {{ $cm.MULTI_TENANT_POOL_MAX_IDLE_CONNS | default "5" | quote }} + {{- end }} + + # ===================================================================== + # OPENTELEMETRY. Shared keys (ENABLE_TELEMETRY / OTLP endpoint / + # deployment-environment) derive from global.observability with a + # configmap. override; identity (name/library/version) per-service. + # ===================================================================== + {{- include "lerian-common.otel.env" (dict + "context" $ "configmap" $cm + "enabledDefault" "false" + "endpointDefault" "" + "deploymentEnvironmentDefault" "production") | nindent 2 }} + {{- include "lerian-common.otel.envFlat" (dict + "configmap" $cm + "keys" (list "OTEL_RESOURCE_SERVICE_NAME" "OTEL_LIBRARY_NAME" "OTEL_RESOURCE_SERVICE_VERSION") + "defaults" (dict + "OTEL_RESOURCE_SERVICE_NAME" "br-sta" + "OTEL_LIBRARY_NAME" "github.com/LerianStudio/br-sta" + "OTEL_RESOURCE_SERVICE_VERSION" (include "br-sta.defaultTag" .))) | nindent 2 }} + + # ===================================================================== + # SERVICE DISCOVERY (serviceDiscovery.env + global.serviceDiscovery). Toggle via + # brSta.serviceDiscovery.enabled; SD_TOKEN (ACL) is a Secret. Full block when enabled. + # ===================================================================== + SD_ENABLED: {{ $sdEnabled | quote }} + {{- include "lerian-common.serviceDiscovery.env" (dict + "context" $ + "enabled" $sdEnabled + "configmap" $cm + "name" (include "br-sta.fullname" .) + "port" ($component.service).port + "namespace" $namespace + "ingressHost" (include "lerian-common.firstIngressHost" (dict "ingress" ($component.ingress | default dict)))) | nindent 2 }} + + # ===================================================================== + # STREAMING (streaming.env + global.streaming). Toggle via brSta.streaming.enabled; + # STREAMING_SASL_PASSWORD is a Secret. Full SASL/broker block when enabled. + # ===================================================================== + STREAMING_ENABLED: {{ $streamingEnabled | quote }} + {{- include "lerian-common.streaming.env" (dict + "context" $ + "enabled" $streamingEnabled + "configmap" $cm + "clientId" (include "br-sta.fullname" .) + "cloudeventsSource" "lerian.br-sta") | nindent 2 }} + + # ===================================================================== + # ADDITIONAL CONFIG (audit / transfer / trust-store / crypto provider / misc). + # Passthrough (configmap. > chart default). Object storage (TRUST_STORE_S3_* / + # TRANSFER_OBJECT_STORAGE_BUCKET) stays escape-hatch until lerian-common ships the + # objectStorage mask. + # TODO 1.5.0: adopt lerian-common.objectStorage.value for the *_S3_* / *_OBJECT_STORAGE_* keys. + # ===================================================================== + AUDIT_EXPORT_RATE_LIMIT_MAX: {{ $cm.AUDIT_EXPORT_RATE_LIMIT_MAX | default "10" | quote }} + AUDIT_EXPORT_RATE_LIMIT_WINDOW_SEC: {{ $cm.AUDIT_EXPORT_RATE_LIMIT_WINDOW_SEC | default "60" | quote }} + AUDIT_PUBLISHER_ALTERNATE_EXCHANGE: {{ $cm.AUDIT_PUBLISHER_ALTERNATE_EXCHANGE | default "sta.audit.dlq" | quote }} + AUDIT_PUBLISHER_CONFIRM_TIMEOUT_SEC: {{ $cm.AUDIT_PUBLISHER_CONFIRM_TIMEOUT_SEC | default "5" | quote }} + BACEN_ENVIRONMENT: {{ $cm.BACEN_ENVIRONMENT | default "homologation" | quote }} + CREDENTIALS_RECOVERY_ON_BOOT: {{ $cm.CREDENTIALS_RECOVERY_ON_BOOT | default "true" | quote }} + MASTER_KEY_PROVIDER: {{ $cm.MASTER_KEY_PROVIDER | default "envvar" | quote }} + MASTER_KEY_VERSION: {{ $cm.MASTER_KEY_VERSION | default "v1" | quote }} + POSTGRES_MAX_CONNECTIONS: {{ $cm.POSTGRES_MAX_CONNECTIONS | default "100" | quote }} + POSTGRES_SHARED_BUFFERS: {{ $cm.POSTGRES_SHARED_BUFFERS | default "128MB" | quote }} + RABBITMQ_SCHEME: {{ $cm.RABBITMQ_SCHEME | default "amqp" | quote }} + SERVER_TRUSTED_PROXIES: {{ $cm.SERVER_TRUSTED_PROXIES | default "" | quote }} + TRANSFER_INBOUND_ENABLED: {{ $cm.TRANSFER_INBOUND_ENABLED | default "false" | quote }} + TRANSFER_INBOUND_MAX_FILE_SIZE_BYTES: {{ $cm.TRANSFER_INBOUND_MAX_FILE_SIZE_BYTES | default "5368709120" | quote }} + TRANSFER_OBJECT_STORAGE_BUCKET: {{ $cm.TRANSFER_OBJECT_STORAGE_BUCKET | default "" | quote }} + TRUST_STORE_DEFAULT_PAGE_SIZE: {{ $cm.TRUST_STORE_DEFAULT_PAGE_SIZE | default "25" | quote }} + TRUST_STORE_EXPIRING_SOON_DAYS: {{ $cm.TRUST_STORE_EXPIRING_SOON_DAYS | default "30" | quote }} + TRUST_STORE_MAX_CERT_SIZE_BYTES: {{ $cm.TRUST_STORE_MAX_CERT_SIZE_BYTES | default "65536" | quote }} + TRUST_STORE_MAX_PAGE_SIZE: {{ $cm.TRUST_STORE_MAX_PAGE_SIZE | default "100" | quote }} + TRUST_STORE_S3_BUCKET: {{ $cm.TRUST_STORE_S3_BUCKET | default "br-sta-truststore" | quote }} + TRUST_STORE_S3_ENDPOINT: {{ $cm.TRUST_STORE_S3_ENDPOINT | default "http://localhost:8333" | quote }} + TRUST_STORE_S3_PATH_STYLE: {{ $cm.TRUST_STORE_S3_PATH_STYLE | default "true" | quote }} + TRUST_STORE_S3_REGION: {{ $cm.TRUST_STORE_S3_REGION | default "us-east-1" | quote }} + # AWS_ACCESS_KEY_ID is the (non-secret) object-storage access key id. coalesce keeps + # the "any" fallback while the trailing `| default ""` is the only default literal the + # secret-default gate sees (empty → not treated as a published credential). + AWS_ACCESS_KEY_ID: {{ coalesce $cm.AWS_ACCESS_KEY_ID "any" | default "" | quote }} + + # ===================================================================== + # POSTGRESQL. HOST/PORT/USER/SSL from the datastore mask (brSta.datastores.postgres + # → global.datastores.postgres) with a configmap. override. When the mask + # yields no HOST and the bundled subchart is enabled, HOST defaults to the + # subchart primary Service DNS; otherwise "". + # ===================================================================== + {{- $pgHost := include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "postgres" "field" "host" "nativeKey" "POSTGRES_HOST") }} + {{- if $pgHost }} + POSTGRES_HOST: {{ $pgHost | quote }} + {{- else if eq (include "postgresql.enabled" .) "true" }} + {{- $pgArch := default "standalone" .Values.postgresql.architecture }} + {{- $pgFullname := include "common.names.dependency.fullname" (dict "chartName" "postgresql" "chartValues" (index .Values "postgresql") "context" .) }} + {{- if eq $pgArch "replication" }} + POSTGRES_HOST: {{ printf "%s-primary.%s.svc.cluster.local" $pgFullname $namespace | quote }} + {{- else }} + POSTGRES_HOST: {{ printf "%s.%s.svc.cluster.local" $pgFullname $namespace | quote }} + {{- end }} + {{- else }} + POSTGRES_HOST: "" + {{- end }} + POSTGRES_PORT: {{ include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "postgres" "field" "port" "nativeKey" "POSTGRES_PORT" "default" "5432") | quote }} + POSTGRES_USER: {{ include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "postgres" "field" "user" "nativeKey" "POSTGRES_USER" "default" "br_sta") | quote }} + POSTGRES_SSLMODE: {{ include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "postgres" "field" "ssl" "nativeKey" "POSTGRES_SSLMODE" "default" "require") | quote }} + {{- $replicaHost := include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "postgres" "field" "replicaHost" "nativeKey" "POSTGRES_REPLICA_HOST") }} + {{- if $replicaHost }} + POSTGRES_REPLICA_HOST: {{ $replicaHost | quote }} + POSTGRES_REPLICA_PORT: {{ include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "postgres" "field" "port" "nativeKey" "POSTGRES_REPLICA_PORT" "default" "5432") | quote }} + POSTGRES_REPLICA_USER: {{ include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "postgres" "field" "user" "nativeKey" "POSTGRES_REPLICA_USER" "default" "br_sta") | quote }} + POSTGRES_REPLICA_NAME: {{ $cm.POSTGRES_REPLICA_NAME | default $pgName | quote }} + POSTGRES_REPLICA_SSLMODE: {{ include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "postgres" "field" "ssl" "nativeKey" "POSTGRES_REPLICA_SSLMODE" "default" "require") | quote }} + {{- end }} + + # ===================================================================== + # REDIS / VALKEY. HOST (host:port) from the datastore mask; when empty and + # the bundled subchart is enabled, defaults to the primary Service DNS. + # ===================================================================== + {{- $redisHost := include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "redis" "field" "host" "nativeKey" "REDIS_HOST") }} + {{- if $redisHost }} + REDIS_HOST: {{ $redisHost | quote }} + {{- else if eq (include "valkey.enabled" .) "true" }} + {{- $vkFullname := include "common.names.dependency.fullname" (dict "chartName" "valkey" "chartValues" (index .Values "valkey") "context" .) }} + REDIS_HOST: {{ printf "%s-primary.%s.svc.cluster.local:6379" $vkFullname $namespace | quote }} + {{- else }} + REDIS_HOST: "" + {{- end }} + + # ===================================================================== + # RABBITMQ. HOST from the datastore mask; when empty and the bundled broker + # is enabled, defaults to the groundhog2k Service DNS. PORT/USER via mask. + # ===================================================================== + {{- $rmqHost := include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "broker" "field" "host" "nativeKey" "RABBITMQ_HOST") }} + {{- if $rmqHost }} + RABBITMQ_HOST: {{ $rmqHost | quote }} + {{- else if eq (include "rabbitmq.enabled" .) "true" }} + RABBITMQ_HOST: {{ printf "%s-rabbitmq.%s.svc.cluster.local" .Release.Name $namespace | quote }} + {{- else }} + RABBITMQ_HOST: "" + {{- end }} + RABBITMQ_PORT_AMQP: {{ include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "broker" "field" "port" "nativeKey" "RABBITMQ_PORT_AMQP" "default" "5672") | quote }} + RABBITMQ_DEFAULT_USER: {{ include "lerian-common.datastore.value" (dict "context" $ "dedicated" $ds "configmap" $cm "type" "broker" "field" "user" "nativeKey" "RABBITMQ_DEFAULT_USER" "default" "br_sta") | quote }} + + # Extra environment variables (passthrough from values.brSta.extraEnvVars) + {{- with $component.extraEnvVars }} + {{- toYaml . | nindent 2 }} + {{- end }} diff --git a/charts/br-sta/templates/deployment.yaml b/charts/br-sta/templates/deployment.yaml new file mode 100644 index 000000000..8b2179a2c --- /dev/null +++ b/charts/br-sta/templates/deployment.yaml @@ -0,0 +1,162 @@ +{{- $component := .Values.brSta -}} +{{- include "br-sta.validateRequired" . }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "br-sta.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} + {{- with $component.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} +spec: + revisionHistoryLimit: {{ $component.revisionHistoryLimit | default 10 }} + {{- with $component.deploymentStrategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if not $component.autoscaling.enabled }} + replicas: {{ $component.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "br-sta.selectorLabels" (dict "context" .) | nindent 6 }} + template: + metadata: + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 8 }} + {{- with $component.podAnnotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + spec: + {{- with $component.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "br-sta.serviceAccountName" . }} + terminationGracePeriodSeconds: {{ $component.terminationGracePeriodSeconds | default 60 }} + {{- with $component.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $component.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if or (eq (include "postgresql.enabled" .) "true") (eq (include "valkey.enabled" .) "true") }} + initContainers: + - name: wait-for-infra + image: busybox:1.37 + envFrom: + - configMapRef: + name: {{ include "br-sta.fullname" . }} + command: + - /bin/sh + - -c + - > + {{- if eq (include "postgresql.enabled" .) "true" }} + echo "Waiting for PostgreSQL $POSTGRES_HOST:$POSTGRES_PORT..."; + until nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do + echo "$POSTGRES_HOST:$POSTGRES_PORT not ready, sleeping 5s"; + sleep 5; + done; + echo "PostgreSQL is ready"; + {{- end }} + {{- if eq (include "valkey.enabled" .) "true" }} + REDIS_SVC=$(echo "$REDIS_HOST" | cut -d: -f1); + REDIS_PORT_NUM=$(echo "$REDIS_HOST" | cut -d: -f2); + [ -z "$REDIS_PORT_NUM" ] && REDIS_PORT_NUM=6379; + echo "Waiting for Redis/Valkey $REDIS_SVC:$REDIS_PORT_NUM..."; + until nc -z "$REDIS_SVC" "$REDIS_PORT_NUM"; do + echo "$REDIS_SVC:$REDIS_PORT_NUM not ready, sleeping 5s"; + sleep 5; + done; + echo "Redis/Valkey is ready"; + {{- end }} + {{- end }} + containers: + - name: {{ include "br-sta.fullname" . }} + securityContext: + {{- toYaml $component.securityContext | nindent 12 }} + image: "{{ $component.image.repository }}:{{ $component.image.tag | default (include "br-sta.defaultTag" .) }}" + imagePullPolicy: {{ $component.image.pullPolicy }} + ports: + - name: http + containerPort: {{ $component.service.port }} + protocol: TCP + {{- $secretName := ternary $component.existingSecretName (include "br-sta.fullname" .) $component.useExistingSecret }} + envFrom: + - configMapRef: + name: {{ include "br-sta.fullname" . }} + - secretRef: + name: {{ $secretName }} + env: + {{- $pg := .Values.postgresql | default dict }} + {{- $pgAuth := $pg.auth | default dict }} + {{- if or (and (ne (toString $pg.enabled) "false") (not $pg.external)) $pgAuth.existingSecret }} + {{- include "br-sta.infraSecretRef" (dict "context" $ "subchart" "postgresql" "key" "password" "envName" "POSTGRES_PASSWORD") | nindent 12 }} + {{- else if $component.secrets.POSTGRES_PASSWORD }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: POSTGRES_PASSWORD + {{- end }} + {{- $vk := .Values.valkey | default dict }} + {{- $vkAuth := $vk.auth | default dict }} + {{- if or (and (ne (toString $vk.enabled) "false") (not $vk.external) $vkAuth.enabled) $vkAuth.existingSecret }} + {{- include "br-sta.infraSecretRef" (dict "context" $ "subchart" "valkey" "key" "valkey-password" "envName" "REDIS_PASSWORD") | nindent 12 }} + {{- else if $component.secrets.REDIS_PASSWORD }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: REDIS_PASSWORD + {{- end }} + {{- if (index .Values "otel-collector-lerian").enabled }} + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: "$(HOST_IP):4317" + {{- end }} + resources: + {{- toYaml $component.resources | nindent 12 }} + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: {{ $component.readinessProbe.initialDelaySeconds | default 5 }} + periodSeconds: {{ $component.readinessProbe.periodSeconds | default 5 }} + timeoutSeconds: {{ $component.readinessProbe.timeoutSeconds | default 3 }} + successThreshold: {{ $component.readinessProbe.successThreshold | default 1 }} + failureThreshold: {{ $component.readinessProbe.failureThreshold | default 2 }} + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: {{ $component.livenessProbe.initialDelaySeconds | default 30 }} + periodSeconds: {{ $component.livenessProbe.periodSeconds | default 10 }} + timeoutSeconds: {{ $component.livenessProbe.timeoutSeconds | default 3 }} + successThreshold: {{ $component.livenessProbe.successThreshold | default 1 }} + failureThreshold: {{ $component.livenessProbe.failureThreshold | default 3 }} + {{- with $component.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $component.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $component.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/br-sta/templates/hpa.yaml b/charts/br-sta/templates/hpa.yaml new file mode 100644 index 000000000..274a33801 --- /dev/null +++ b/charts/br-sta/templates/hpa.yaml @@ -0,0 +1,37 @@ +{{- $component := .Values.brSta -}} +{{- if $component.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "br-sta.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "br-sta.fullname" . }} + minReplicas: {{ $component.autoscaling.minReplicas }} + maxReplicas: {{ $component.autoscaling.maxReplicas }} + metrics: + {{- if $component.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ $component.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if $component.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ $component.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} + behavior: + scaleDown: + stabilizationWindowSeconds: {{ $component.autoscaling.scaleDownStabilizationSeconds | default 300 }} +{{- end }} diff --git a/charts/br-sta/templates/ingress.yaml b/charts/br-sta/templates/ingress.yaml new file mode 100644 index 000000000..dcb6dd1ad --- /dev/null +++ b/charts/br-sta/templates/ingress.yaml @@ -0,0 +1,63 @@ +{{- $component := .Values.brSta -}} +{{- if $component.ingress.enabled -}} +{{- $fullName := include "br-sta.fullname" . -}} +{{- $svcPort := $component.service.port -}} +{{- if and $component.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey $component.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set $component.ingress.annotations "kubernetes.io/ingress.class" $component.ingress.className }} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} + {{- with $component.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and $component.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ $component.ingress.className }} + {{- end }} + {{- if $component.ingress.tls }} + tls: + {{- range $component.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range $component.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/br-sta/templates/migrations.yaml b/charts/br-sta/templates/migrations.yaml new file mode 100644 index 000000000..f7a8093bc --- /dev/null +++ b/charts/br-sta/templates/migrations.yaml @@ -0,0 +1,225 @@ +{{- $app := .Values.brSta -}} +{{- $migrations := get $app "migrations" | default dict -}} +{{- $configmap := get $app "configmap" | default dict -}} +{{- $migrationImage := get $migrations "image" | default dict -}} +{{- $defaultMigrationResources := dict "requests" (dict "cpu" "50m" "memory" "64Mi") "limits" (dict "cpu" "250m" "memory" "256Mi") -}} +{{- $migrationsEnabled := eq (lower (toString (get $migrations "enabled" | default false))) "true" -}} +{{- if $migrationsEnabled }} +{{- /* The standalone runner (init/postgres-migrations) refuses MULTI_TENANT_ENABLED=true: + tenant databases are migrated by the tenant-manager, not this chart. Fail fast so the + misconfiguration surfaces at render time rather than as a crash-looping Job. */}} +{{- if eq (lower (toString (get $configmap "MULTI_TENANT_ENABLED" | default "false"))) "true" }} +{{- fail "br-sta.migrations.enabled cannot be true when brSta.configmap.MULTI_TENANT_ENABLED=true; tenant-manager owns tenant database migrations" }} +{{- end }} +{{- $namespace := include "global.namespace" . -}} +{{- $pg := .Values.postgresql | default dict -}} +{{- $pgAuth := $pg.auth | default dict -}} +{{- /* Internal (chart-managed) Postgres: provisioned during Sync (bitnami subchart), so the + migration must run as a POST hook (after the DB exists) with a wait-for-Postgres init. + External/pre-existing Postgres keeps the PRE hook (schema-first, DB already exists). This + mirrors the "postgresql.enabled" helper (enabled && not external). */}} +{{- $internalPg := eq (include "postgresql.enabled" .) "true" -}} +{{- /* POSTGRES_PASSWORD source mirrors deployment.yaml exactly: + - bundled subchart OR postgresql.auth.existingSecret → single-source the subchart Secret + via secretKeyRef (br-sta.infraSecretRef). + - external, chart-managed → the app Secret (secrets.yaml) is itself a pre-install/ + pre-upgrade hook at weight -5, so it already exists before this Job (weight -1); no + separate migration-only Secret is required. + A migration-specific or app-level existing Secret overrides the chart-managed name. */}} +{{- $useInfraSecret := or (and (ne (toString $pg.enabled) "false") (not $pg.external)) $pgAuth.existingSecret -}} +{{- $migUseExisting := eq (lower (toString (get $migrations "useExistingSecret" | default false))) "true" -}} +{{- $appUseExisting := eq (lower (toString (get $app "useExistingSecret" | default false))) "true" -}} +{{- $pwSecretName := include "br-sta.fullname" . -}} +{{- if $migUseExisting }} +{{- $pwSecretName = required "br-sta.migrations.existingSecretName is required when br-sta.migrations.useExistingSecret=true" (get $migrations "existingSecretName") -}} +{{- else if $appUseExisting }} +{{- $pwSecretName = required "br-sta.existingSecretName is required when br-sta.useExistingSecret=true" (get $app "existingSecretName") -}} +{{- end }} +{{- /* Resolve POSTGRES_HOST the same way configmap.yaml does when left empty: derive the + collapse-aware subchart primary Service for internal Postgres; external installs set it + explicitly. */}} +{{- $pgHost := get $configmap "POSTGRES_HOST" -}} +{{- if and (not $pgHost) $internalPg }} +{{- $pgFullname := include "common.names.dependency.fullname" (dict "chartName" "postgresql" "chartValues" $pg "context" .) -}} +{{- if eq (default "standalone" $pg.architecture) "replication" }} +{{- $pgHost = printf "%s-primary.%s.svc.cluster.local" $pgFullname $namespace -}} +{{- else }} +{{- $pgHost = printf "%s.%s.svc.cluster.local" $pgFullname $namespace -}} +{{- end }} +{{- end }} +{{- /* External Postgres with no resolvable host would render POSTGRES_HOST="" and the Job + would only fail after burning its retry/deadline budget — fail fast at render instead. */}} +{{- if not $pgHost }} +{{- fail "brSta.configmap.POSTGRES_HOST is required when brSta.migrations.enabled=true and PostgreSQL is external" }} +{{- end }} +{{- /* Migration image tag is coupled to the app image tag unless an explicit tag/digest is set; + empty tag falls back to brSta.image.tag, then the chart appVersion. */}} +{{- $migrationTag := get $migrationImage "tag" | default (get (get $app "image" | default dict) "tag" | default .Chart.AppVersion) -}} +{{- $migrationDigest := get $migrationImage "digest" | default "" -}} +{{- $migrationRepository := get $migrationImage "repository" | default "ghcr.io/lerianstudio/br-sta-migrations" -}} +{{- /* lib-commons' migrator refuses to connect to a non-TLS Postgres unless + ALLOW_INSECURE_TLS=true — POSTGRES_SSLMODE=disable alone is not enough. The + app deployment gets this via extraEnvVars/configmap; the migration Job must + mirror it. Resolution order: migrations.allowInsecureTLS override → + app.extraEnvVars → app.configmap. Emitted only when set, so TLS tiers stay + secure by default. */}} +{{- $extraEnv := get $app "extraEnvVars" | default dict -}} +{{- $allowInsecureTLS := "" -}} +{{- if get $migrations "allowInsecureTLS" -}} +{{- $allowInsecureTLS = toString (get $migrations "allowInsecureTLS") -}} +{{- else if get $extraEnv "ALLOW_INSECURE_TLS" -}} +{{- $allowInsecureTLS = toString (get $extraEnv "ALLOW_INSECURE_TLS") -}} +{{- else if get $configmap "ALLOW_INSECURE_TLS" -}} +{{- $allowInsecureTLS = toString (get $configmap "ALLOW_INSECURE_TLS") -}} +{{- end -}} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ printf "%s-migrations" (include "br-sta.fullname" .) | trunc 63 | trimSuffix "-" }} + namespace: {{ $namespace }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} + app.kubernetes.io/component: migrations + annotations: + {{- if $internalPg }} + # Internal Postgres is provisioned during Sync — run AFTER it exists and is reachable. + "helm.sh/hook": post-install,post-upgrade + "argocd.argoproj.io/hook": PostSync + {{- else }} + # External Postgres already exists — run BEFORE the app rolls out (schema-first). Weight -1 + # runs after the app Secret hook (weight -5) so its POSTGRES_PASSWORD is available. + "helm.sh/hook": pre-install,pre-upgrade + "argocd.argoproj.io/hook": PreSync + {{- end }} + "helm.sh/hook-weight": "-1" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + "argocd.argoproj.io/hook-delete-policy": BeforeHookCreation,HookSucceeded + {{- with (get $migrations "annotations") }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- /* hasKey (not `default`) so an explicit 0 renders as 0 — sprig `default` treats 0 as + empty and would silently fall back to 3/600. */}} + backoffLimit: {{ if hasKey $migrations "backoffLimit" }}{{ get $migrations "backoffLimit" }}{{ else }}3{{ end }} + activeDeadlineSeconds: {{ if hasKey $migrations "activeDeadlineSeconds" }}{{ get $migrations "activeDeadlineSeconds" }}{{ else }}600{{ end }} + ttlSecondsAfterFinished: {{ if hasKey $migrations "ttlSecondsAfterFinished" }}{{ get $migrations "ttlSecondsAfterFinished" }}{{ else }}600{{ end }} + template: + metadata: + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 8 }} + app.kubernetes.io/component: migrations + {{- with (get $migrations "podAnnotations") }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + restartPolicy: Never + automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault + {{- with (get $app "imagePullSecrets") }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with (get $migrations "serviceAccountName") }} + serviceAccountName: {{ . | quote }} + {{- end }} + {{- if $internalPg }} + initContainers: + - name: wait-for-postgres + image: busybox:1.37 + command: + - /bin/sh + - -c + - > + echo "Waiting for $POSTGRES_HOST:$POSTGRES_PORT..."; + ELAPSED=0; + while ! nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do + if [ $ELAPSED -ge 300 ]; then echo "Timeout after 300s"; exit 1; fi; + echo "not ready, waiting (${ELAPSED}s/300s)"; sleep 5; ELAPSED=$((ELAPSED + 5)); + done; + echo "Postgres is ready"; + env: + - name: POSTGRES_HOST + value: {{ $pgHost | default (printf "%s-postgresql-primary.%s.svc.cluster.local" .Release.Name $namespace) | quote }} + - name: POSTGRES_PORT + value: {{ get $configmap "POSTGRES_PORT" | default "5432" | quote }} + securityContext: + runAsUser: 65532 + runAsGroup: 65532 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + {{- end }} + containers: + - name: migrations + {{- if $migrationDigest }} + image: "{{ $migrationRepository }}@{{ $migrationDigest }}" + {{- else }} + image: "{{ $migrationRepository }}:{{ $migrationTag }}" + {{- end }} + imagePullPolicy: {{ get $migrationImage "pullPolicy" | default "IfNotPresent" }} + env: + # The runner reads MIGRATIONS_PATH; the image bundles the SQL at /migrations. + - name: MIGRATIONS_PATH + value: {{ get $migrations "path" | default "/migrations" | quote }} + - name: POSTGRES_HOST + value: {{ $pgHost | quote }} + - name: POSTGRES_PORT + value: {{ get $configmap "POSTGRES_PORT" | default "5432" | quote }} + - name: POSTGRES_USER + value: {{ get $configmap "POSTGRES_USER" | default "br_sta" | quote }} + # br-sta's runner reads the database name from POSTGRES_NAME (not POSTGRES_DB). + - name: POSTGRES_NAME + value: {{ get $configmap "POSTGRES_NAME" | default "br_sta" | quote }} + - name: POSTGRES_SSLMODE + value: {{ get $configmap "POSTGRES_SSLMODE" | default "require" | quote }} + {{- if $allowInsecureTLS }} + # lib-commons migrator bypass for non-TLS Postgres (sslmode=disable + # alone is rejected). Mirrors the app's ALLOW_INSECURE_TLS. + - name: ALLOW_INSECURE_TLS + value: {{ $allowInsecureTLS | quote }} + {{- end }} + - name: POSTGRES_CONNECT_TIMEOUT_SEC + value: {{ get $configmap "POSTGRES_CONNECT_TIMEOUT_SEC" | default "10" | quote }} + {{- with (get $migrations "timeoutSeconds") }} + - name: MIGRATIONS_TIMEOUT_SEC + value: {{ . | quote }} + {{- end }} + {{- if $useInfraSecret }} + {{- include "br-sta.infraSecretRef" (dict "context" $ "subchart" "postgresql" "key" "password" "envName" "POSTGRES_PASSWORD") | nindent 12 }} + {{- else }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $pwSecretName }} + key: POSTGRES_PASSWORD + {{- end }} + securityContext: + runAsUser: 65532 + runAsGroup: 65532 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + {{- toYaml (get $migrations "resources" | default $defaultMigrationResources) | nindent 12 }} + {{- with (get $app "nodeSelector") }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with (get $app "affinity") }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with (get $app "tolerations") }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/br-sta/templates/pdb.yaml b/charts/br-sta/templates/pdb.yaml new file mode 100644 index 000000000..e4f2e7634 --- /dev/null +++ b/charts/br-sta/templates/pdb.yaml @@ -0,0 +1,25 @@ +{{- $component := .Values.brSta -}} +{{- if $component.pdb.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "br-sta.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} + {{- with $component.pdb.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} +spec: + {{- if $component.pdb.maxUnavailable }} + maxUnavailable: {{ $component.pdb.maxUnavailable }} + {{- else }} + minAvailable: {{ $component.pdb.minAvailable | default 1 }} + {{- end }} + selector: + matchLabels: + {{- include "br-sta.selectorLabels" (dict "context" .) | nindent 6 }} +{{- end }} diff --git a/charts/br-sta/templates/secrets.yaml b/charts/br-sta/templates/secrets.yaml new file mode 100644 index 000000000..38fad199a --- /dev/null +++ b/charts/br-sta/templates/secrets.yaml @@ -0,0 +1,68 @@ +{{- $component := .Values.brSta -}} +{{- if not $component.useExistingSecret }} +{{- $s := $component.secrets | default dict -}} +{{- $cm := $component.configmap | default dict -}} +{{- $mtEnabled := eq (include "lerian-common.cfgValue" (dict "configmap" $cm "nativeKey" "MULTI_TENANT_ENABLED" "params" ($component.multiTenant | default dict) "field" "enabled" "default" "false")) "true" -}} +{{- $rmqEnabled := eq (include "lerian-common.cfgValue" (dict "configmap" $cm "nativeKey" "RABBITMQ_ENABLED" "params" ($component.broker | default dict) "field" "enabled" "default" "false")) "true" -}} +{{- $streamingEnabled := eq (include "lerian-common.cfgValue" (dict "configmap" $cm "nativeKey" "STREAMING_ENABLED" "params" ($component.streaming | default dict) "field" "enabled" "default" "false")) "true" -}} +{{- $gStream := (.Values.global | default dict).streaming | default dict -}} +{{- $streamMech := $gStream.saslMechanism | default "" }}{{- if hasKey $cm "STREAMING_SASL_MECHANISM" }}{{- $streamMech = index $cm "STREAMING_SASL_MECHANISM" }}{{- end }} +{{- $streamUser := $gStream.saslUsername | default "" }}{{- if hasKey $cm "STREAMING_SASL_USERNAME" }}{{- $streamUser = index $cm "STREAMING_SASL_USERNAME" }}{{- end }} +{{- /* Secret keys emitted with fail-fast semantics below — excluded from the generic + passthrough range so they are single-sourced (and gated by feature enablement). */ -}} +{{- $managed := dict -}} +{{- range $k := (list "MULTI_TENANT_SERVICE_API_KEY" "MULTI_TENANT_REDIS_PASSWORD" "RABBITMQ_DEFAULT_PASS" "STREAMING_SASL_PASSWORD") -}} +{{- $_ := set $managed $k true -}} +{{- end -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "br-sta.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} + annotations: + "helm.sh/hook": "pre-install,pre-upgrade" + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": "before-hook-creation" + {{- $warns := include "br-sta.secretWarnings" . }} + {{- if $warns }} + {{ $warns | nindent 4 }} + {{- end }} +type: Opaque +stringData: + {{- range $key, $value := $s }} + {{- if and $value (not (hasKey $managed $key)) }} + {{ $key }}: {{ $value | toString | quote }} + {{- end }} + {{- end }} + {{- /* Multi-tenant secrets (lerian-common.multiTenant.secret): SERVICE_API_KEY + required + REDIS_PASSWORD optional, emitted only when MT is enabled; fails fast + when enabled but the API key is empty. */ -}} + {{- with (include "lerian-common.multiTenant.secret" (dict + "context" . "secrets" $s + "secretName" (include "br-sta.fullname" .) + "valuesPrefix" "brSta.secrets." "mode" "stringData" + "enabled" $mtEnabled + "useExistingSecret" $component.useExistingSecret)) }} + {{- . | nindent 2 }} + {{- end }} + {{- /* RabbitMQ password: required when the broker is on and no full RABBITMQ_URL + (which embeds the password) is supplied; optional otherwise. */ -}} + {{- if and $rmqEnabled (not $s.RABBITMQ_URL) }} + RABBITMQ_DEFAULT_PASS: {{ required "brSta.secrets.RABBITMQ_DEFAULT_PASS is required when RABBITMQ_ENABLED=true (or provide brSta.secrets.RABBITMQ_URL instead)" $s.RABBITMQ_DEFAULT_PASS | quote }} + {{- else if $s.RABBITMQ_DEFAULT_PASS }} + RABBITMQ_DEFAULT_PASS: {{ $s.RABBITMQ_DEFAULT_PASS | quote }} + {{- end }} + {{- /* Streaming SASL credentials (lerian-common.streaming.secret): PASSWORD required + when streaming is enabled with a SASL mechanism; fails fast otherwise. */ -}} + {{- with (include "lerian-common.streaming.secret" (dict + "context" . "secrets" $s + "secretName" (include "br-sta.fullname" .) + "valuesPrefix" "brSta.secrets." "mode" "stringData" + "enabled" $streamingEnabled + "useExistingSecret" $component.useExistingSecret + "saslMechanism" $streamMech "saslUsername" $streamUser)) }} + {{- . | nindent 2 }} + {{- end }} +{{- end }} diff --git a/charts/br-sta/templates/service.yaml b/charts/br-sta/templates/service.yaml new file mode 100644 index 000000000..442e6f7e1 --- /dev/null +++ b/charts/br-sta/templates/service.yaml @@ -0,0 +1,23 @@ +{{- $component := .Values.brSta -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "br-sta.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} + {{- with $component.service.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} +spec: + type: {{ $component.service.type }} + ports: + - port: {{ $component.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "br-sta.selectorLabels" (dict "context" .) | nindent 4 }} diff --git a/charts/br-sta/templates/serviceaccount.yaml b/charts/br-sta/templates/serviceaccount.yaml new file mode 100644 index 000000000..527ed5da0 --- /dev/null +++ b/charts/br-sta/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- $component := .Values.brSta -}} +{{- if $component.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "br-sta.serviceAccountName" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} + {{- with $component.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/br-sta/templates/worker/configmap.yaml b/charts/br-sta/templates/worker/configmap.yaml new file mode 100644 index 000000000..fc334e32f --- /dev/null +++ b/charts/br-sta/templates/worker/configmap.yaml @@ -0,0 +1,22 @@ +{{- if eq (include "br-sta.worker.enabled" .) "true" }} +{{- $w := .Values.worker | default dict -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "br-sta.worker.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.worker.labels" (dict "context" .) | nindent 4 }} +data: + # Worker-only environment. Layered on top of the manager ConfigMap + Secret + # (envFrom order in the worker Deployment), so keys here win over shared ones. + # The worker binds SERVER_ADDRESS only for its probe server (WorkerMode). + SERVER_ADDRESS: {{ printf "0.0.0.0:%v" ($w.service.port | default 8081) | quote }} + {{- with $w.configmap }} + {{- range $key, $value := . }} + {{- if ne $key "SERVER_ADDRESS" }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/br-sta/templates/worker/deployment.yaml b/charts/br-sta/templates/worker/deployment.yaml new file mode 100644 index 000000000..d040319a6 --- /dev/null +++ b/charts/br-sta/templates/worker/deployment.yaml @@ -0,0 +1,194 @@ +{{- if eq (include "br-sta.worker.enabled" .) "true" }} +{{- $mgr := .Values.brSta -}} +{{- $w := .Values.worker | default dict -}} +{{- $wImage := $w.image | default dict -}} +{{- $repo := $wImage.repository | default $mgr.image.repository -}} +{{- $tag := $wImage.tag | default (include "br-sta.defaultTag" .) -}} +{{- $pullPolicy := $wImage.pullPolicy | default $mgr.image.pullPolicy -}} +{{- $pullSecrets := $w.imagePullSecrets | default $mgr.imagePullSecrets -}} +{{- $secCtx := $w.securityContext | default $mgr.securityContext -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "br-sta.worker.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.worker.labels" (dict "context" .) | nindent 4 }} + {{- with $w.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} +spec: + revisionHistoryLimit: {{ $w.revisionHistoryLimit | default 10 }} + {{- with $w.deploymentStrategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + # The worker is leader-gated but does NOT run its own leader election yet; + # keep this at 1 to avoid duplicate background processing (audit drain, + # scheduler, inbound poll). Do NOT add an HPA to the worker. + {{- if gt (int ($w.replicaCount | default 1)) 1 }} + {{- fail "worker.replicaCount must be 1: the worker has no leader election yet, so >1 duplicates background processing" }} + {{- end }} + replicas: {{ $w.replicaCount | default 1 }} + selector: + matchLabels: + {{- include "br-sta.worker.selectorLabels" (dict "context" .) | nindent 6 }} + template: + metadata: + labels: + {{- include "br-sta.worker.labels" (dict "context" .) | nindent 8 }} + {{- with $w.podAnnotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + spec: + {{- with $pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "br-sta.worker.serviceAccountName" . }} + terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds | default 60 }} + {{- with $w.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $w.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if or (eq (include "postgresql.enabled" .) "true") (eq (include "valkey.enabled" .) "true") }} + initContainers: + - name: wait-for-infra + image: busybox:1.37 + securityContext: + {{- toYaml $secCtx | nindent 12 }} + envFrom: + # POSTGRES_HOST / REDIS_HOST live in the manager ConfigMap. + - configMapRef: + name: {{ include "br-sta.fullname" . }} + command: + - /bin/sh + - -c + - > + {{- if eq (include "postgresql.enabled" .) "true" }} + echo "Waiting for PostgreSQL $POSTGRES_HOST:$POSTGRES_PORT..."; + until nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do + echo "$POSTGRES_HOST:$POSTGRES_PORT not ready, sleeping 5s"; + sleep 5; + done; + echo "PostgreSQL is ready"; + {{- end }} + {{- if eq (include "valkey.enabled" .) "true" }} + REDIS_SVC=$(echo "$REDIS_HOST" | cut -d: -f1); + REDIS_PORT_NUM=$(echo "$REDIS_HOST" | cut -d: -f2); + [ -z "$REDIS_PORT_NUM" ] && REDIS_PORT_NUM=6379; + echo "Waiting for Redis/Valkey $REDIS_SVC:$REDIS_PORT_NUM..."; + until nc -z "$REDIS_SVC" "$REDIS_PORT_NUM"; do + echo "$REDIS_SVC:$REDIS_PORT_NUM not ready, sleeping 5s"; + sleep 5; + done; + echo "Redis/Valkey is ready"; + {{- end }} + {{- end }} + containers: + - name: {{ include "br-sta.worker.fullname" . }} + securityContext: + {{- toYaml $secCtx | nindent 12 }} + image: "{{ $repo }}:{{ $tag }}" + imagePullPolicy: {{ $pullPolicy }} + # Matches the worker image's own ENTRYPOINT (/service, built from + # cmd/worker) — see the worker.command comment in values.yaml. + command: + {{- toYaml ($w.command | default (list "/service")) | nindent 12 }} + ports: + - name: http + containerPort: {{ $w.service.port | default 8081 }} + protocol: TCP + envFrom: + # Shared config + secrets from the manager (single source of truth). + - configMapRef: + name: {{ include "br-sta.fullname" . }} + - secretRef: + name: {{ if $mgr.useExistingSecret }}{{ $mgr.existingSecretName }}{{ else }}{{ include "br-sta.fullname" . }}{{ end }} + # Worker-only overrides (SERVER_ADDRESS, background-job knobs) — last + # so its keys win over the shared manager ConfigMap. + - configMapRef: + name: {{ include "br-sta.worker.fullname" . }} + env: + {{- $secretName := ternary $mgr.existingSecretName (include "br-sta.fullname" .) $mgr.useExistingSecret }} + {{- $pg := .Values.postgresql | default dict }} + {{- $pgAuth := $pg.auth | default dict }} + {{- if or (and (ne (toString $pg.enabled) "false") (not $pg.external)) $pgAuth.existingSecret }} + {{- include "br-sta.infraSecretRef" (dict "context" $ "subchart" "postgresql" "key" "password" "envName" "POSTGRES_PASSWORD") | nindent 12 }} + {{- else if $mgr.secrets.POSTGRES_PASSWORD }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: POSTGRES_PASSWORD + {{- end }} + {{- $vk := .Values.valkey | default dict }} + {{- $vkAuth := $vk.auth | default dict }} + {{- if or (and (ne (toString $vk.enabled) "false") (not $vk.external) $vkAuth.enabled) $vkAuth.existingSecret }} + {{- include "br-sta.infraSecretRef" (dict "context" $ "subchart" "valkey" "key" "valkey-password" "envName" "REDIS_PASSWORD") | nindent 12 }} + {{- else if $mgr.secrets.REDIS_PASSWORD }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: REDIS_PASSWORD + {{- end }} + {{- if (index .Values "otel-collector-lerian").enabled }} + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: "$(HOST_IP):4317" + {{- end }} + {{- with $w.extraEnvVars }} + {{- range $key, $value := . }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + {{- end }} + resources: + {{- toYaml ($w.resources | default $mgr.resources) | nindent 12 }} + # The worker binary runs in WorkerMode: it serves ONLY the probe + # endpoints on SERVER_ADDRESS (no business HTTP API). + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: {{ (($w.readinessProbe).initialDelaySeconds) | default 5 }} + periodSeconds: {{ (($w.readinessProbe).periodSeconds) | default 5 }} + timeoutSeconds: {{ (($w.readinessProbe).timeoutSeconds) | default 3 }} + successThreshold: {{ (($w.readinessProbe).successThreshold) | default 1 }} + failureThreshold: {{ (($w.readinessProbe).failureThreshold) | default 2 }} + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: {{ (($w.livenessProbe).initialDelaySeconds) | default 30 }} + periodSeconds: {{ (($w.livenessProbe).periodSeconds) | default 10 }} + timeoutSeconds: {{ (($w.livenessProbe).timeoutSeconds) | default 3 }} + successThreshold: {{ (($w.livenessProbe).successThreshold) | default 1 }} + failureThreshold: {{ (($w.livenessProbe).failureThreshold) | default 3 }} + {{- with $w.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $w.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $w.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/br-sta/templates/worker/serviceaccount.yaml b/charts/br-sta/templates/worker/serviceaccount.yaml new file mode 100644 index 000000000..2bec051ee --- /dev/null +++ b/charts/br-sta/templates/worker/serviceaccount.yaml @@ -0,0 +1,17 @@ +{{- if eq (include "br-sta.worker.enabled" .) "true" }} +{{- $w := .Values.worker | default dict -}} +{{- $sa := $w.serviceAccount | default dict -}} +{{- if $sa.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "br-sta.worker.serviceAccountName" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.worker.labels" (dict "context" .) | nindent 4 }} + {{- with $sa.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/br-sta/values-quickstart.yaml b/charts/br-sta/values-quickstart.yaml new file mode 100644 index 000000000..c8d098465 --- /dev/null +++ b/charts/br-sta/values-quickstart.yaml @@ -0,0 +1,90 @@ +# ============================================================================= +# br-sta — QUICKSTART values (the layperson layer) +# ----------------------------------------------------------------------------- +# The full values.yaml is the power-user API. This file is the short list of +# DEPENDENCY-CONNECTION knobs a non-expert fills to get br-sta running: where the +# database/cache/broker live, the image tag, the ingress host, and the +# operator-provided secrets. Everything else has a working default in the chart; +# reach it only via brSta.configmap. (the escape hatch) when you must. +# +# helm install br-sta charts/br-sta -f values-quickstart.yaml +# +# Anything with CHANGE_ME MUST be set. The bundled Bitnami subcharts (postgresql, +# valkey) are ON by default and RabbitMQ is opt-in, so the *host* knobs below only +# matter when you point br-sta at EXTERNAL infra (set .enabled=false). +# ============================================================================= + +global: + # -- Set each dependency connection ONCE here; the manager (and the worker via + # envFrom) emit their native env keys from it. Leave a block out to keep the + # in-cluster bundled default. + datastores: {} + # postgres: { host: "pg-primary", port: "5432", user: "br_sta", ssl: "require", name: "br_sta", replicaHost: "pg-replica" } + # redis: { host: "valkey-primary:6379" } + # broker: { host: "rabbitmq", port: "5672", user: "br_sta" } + # -- OTel collector (leave empty to keep telemetry off). + observability: {} + # enabled: "true" + # otlpEndpoint: "otel-collector:4317" + # deploymentEnvironment: "production" + # -- access-manager auth (per-service override via brSta.configmap.PLUGIN_AUTH_*). + auth: {} + # enabled: "true" + # host: "http://plugin-access-manager-auth:4000" + # -- Tenant-manager (only needed when MULTI_TENANT_ENABLED=true). + multiTenant: {} + # url: "http://tenant-manager:8080" + # redisHost: "tenant-manager-redis" + # -- Streaming backend (only needed when STREAMING_ENABLED=true). + streaming: {} + # brokers: "redpanda:9092" + # saslMechanism: "SCRAM-SHA-512" + # saslUsername: "br_sta" + # tlsEnabled: "true" + +brSta: + image: + tag: "" # -- pin the manager image version (defaults to chart appVersion) + ingress: + enabled: false + # hosts: [{ host: "br-sta.example.com", paths: [{ path: "/", pathType: Prefix }] }] + # -- Turn on env-wide features here (they read global.multiTenant / global.streaming / global.serviceDiscovery): + multiTenant: + enabled: false # MULTI_TENANT_ENABLED + serviceDiscovery: + enabled: false # SD_ENABLED + streaming: + enabled: false # STREAMING_ENABLED + # -- Common opt-in overrides go through the escape hatch: + configmap: {} + # RABBITMQ_ENABLED: "true" # then set rabbitmq.enabled=true + secrets.RABBITMQ_DEFAULT_PASS + # TRUST_STORE_S3_BUCKET: "br-sta-truststore" # object storage bucket (S3/SeaweedFS) + # TRANSFER_OBJECT_STORAGE_BUCKET: "br-sta-transfers" + secrets: + # -- Envelope-encryption master key: REQUIRED — the manager ABORTS BOOT without it. + # Comma-separated "version:hexkey" (32-byte / 64-hex-char AES key). openssl rand -hex 32 + MASTER_KEYS: "CHANGE_ME" + # -- Object-storage credentials (SeaweedFS/MinIO/AWS S3). AWS_ACCESS_KEY_ID is config. + # AWS_SECRET_ACCESS_KEY: "CHANGE_ME" + # -- Only for EXTERNAL infra (subchart disabled / .external=true): + # POSTGRES_PASSWORD: "CHANGE_ME" + # REDIS_PASSWORD: "CHANGE_ME" + # -- Only when the matching feature is enabled: + # RABBITMQ_DEFAULT_PASS: "CHANGE_ME" # RABBITMQ_ENABLED=true + # MULTI_TENANT_SERVICE_API_KEY: "CHANGE_ME" # multiTenant.enabled=true + # STREAMING_SASL_PASSWORD: "CHANGE_ME" # streaming.enabled=true with a SASL mechanism + # SD_TOKEN: "CHANGE_ME" # serviceDiscovery.enabled=true (ACL token) + +# --- WORKER (background jobs: audit outbox/consumer, scheduler, inbound poll) --- +# Opt-in. REQUIRES a dedicated worker image. It reuses the manager ConfigMap+Secret. +worker: + enabled: false + # image: + # repository: ghcr.io/lerianstudio/br-sta-worker + # tag: "" + +# --- EXTERNAL INFRA: disable the bundled subcharts to use your own --- +# postgresql: +# enabled: false +# valkey: +# enabled: false diff --git a/charts/br-sta/values-template.yaml b/charts/br-sta/values-template.yaml new file mode 100644 index 000000000..a06b0e7c1 --- /dev/null +++ b/charts/br-sta/values-template.yaml @@ -0,0 +1,74 @@ +# Production values overlay starter for br-sta-helm. +# Copy this file, fill in the placeholders, and pass it to helm install/upgrade. +# +# helm upgrade --install br-sta lerian/br-sta-helm \ +# -n --create-namespace \ +# -f values-prod.yaml + +br-sta: + replicaCount: 2 + image: + tag: "latest" # pin to a published immutable tag + ingress: + enabled: false + className: nginx + annotations: {} + hosts: + - host: br-sta.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: br-sta-tls + hosts: + - br-sta.example.com + configmap: + ENV_NAME: "production" + LOG_LEVEL: "info" + # External PostgreSQL — leave empty only if postgresql.enabled=true. + POSTGRES_HOST: "" + POSTGRES_SSLMODE: "require" + # External Redis/Valkey (host:port) — leave empty only if valkey.enabled=true. + REDIS_HOST: "" + REDIS_TLS: "false" + # Multi-tenancy (set to "true" to enable — then MULTI_TENANT_URL + API key are required) + MULTI_TENANT_ENABLED: "false" + # MULTI_TENANT_URL: "" + # OpenTelemetry (or use otel-collector-lerian.enabled below for host injection) + ENABLE_TELEMETRY: "false" + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "production" + secrets: + # Database — leave empty when postgresql.enabled=true (auto-generated). + POSTGRES_PASSWORD: "" + # Redis/Valkey — leave empty when valkey.enabled=true (auto-generated). + REDIS_PASSWORD: "" + # Multi-tenancy (REQUIRED when MULTI_TENANT_ENABLED=true) + # MULTI_TENANT_SERVICE_API_KEY: "" + # RabbitMQ (only relevant when RABBITMQ_ENABLED=true) + # RABBITMQ_DEFAULT_PASS: "" + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 5 + +# Use managed Postgres + Redis in production. +postgresql: + enabled: false +valkey: + enabled: false + +# When using a fresh external Postgres, optionally run the bootstrap Job once: +# global: +# externalPostgresDefinitions: +# enabled: true +# connection: +# host: my-rds.example.com +# port: "5432" +# postgresAdminLogin: +# username: postgres +# password: +# appCredentials: +# password: + +# otel-collector-lerian: +# enabled: true diff --git a/charts/br-sta/values.schema.json b/charts/br-sta/values.schema.json new file mode 100644 index 000000000..fabe6ad44 --- /dev/null +++ b/charts/br-sta/values.schema.json @@ -0,0 +1,1236 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "Values schema (generated by productize-chart-env/gen-schema.py)", + "type": "object", + "properties": { + "nameOverride": { + "type": "string", + "default": "br-sta", + "description": "Override the chart top-level name" + }, + "fullnameOverride": { + "type": "string", + "default": "", + "description": "Override the fully generated name" + }, + "namespaceOverride": { + "type": "string", + "default": "", + "description": "Override the namespace used by templates" + }, + "global": { + "type": "object", + "additionalProperties": true + }, + "brSta": { + "type": "object", + "properties": { + "multiTenant": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable multi-tenancy (MULTI_TENANT_ENABLED)" + } + }, + "additionalProperties": false, + "description": "Multi-tenancy toggle (grouped API for MULTI_TENANT_ENABLED). Tenant-manager" + }, + "datastores": { + "type": "object", + "additionalProperties": true, + "description": "Dedicated datastore mask for THIS service (see global.datastores for the shared" + }, + "name": { + "default": "br-sta", + "description": "Service name" + }, + "replicaCount": { + "default": 2, + "description": "Number of replicas" + }, + "revisionHistoryLimit": { + "default": 10, + "description": "Number of old ReplicaSets to retain for rollback" + }, + "annotations": { + "type": "object", + "additionalProperties": true, + "description": "Annotations applied to the Deployment resource" + }, + "podAnnotations": { + "type": "object", + "additionalProperties": true, + "description": "Annotations applied to the pods" + }, + "image": { + "type": "object", + "properties": { + "repository": { + "default": "ghcr.io/lerianstudio/br-sta", + "description": "Repository for the br-sta image" + }, + "pullPolicy": { + "default": "IfNotPresent", + "description": "Image pull policy" + }, + "tag": { + "default": "", + "description": "Image tag (defaults to Chart.appVersion if empty)" + } + }, + "additionalProperties": true + }, + "imagePullSecrets": { + "type": "array", + "description": "Image pull secrets for private registries" + }, + "migrations": { + "type": "object", + "description": "Database migrations Job (init/postgres-migrations runner image).", + "properties": { + "enabled": { + "default": false, + "description": "Enable or disable the migrations Job." + }, + "useExistingSecret": { + "default": false, + "description": "When true, read POSTGRES_PASSWORD from a pre-existing Secret" + }, + "existingSecretName": { + "default": "", + "description": "Name of the pre-existing Secret containing POSTGRES_PASSWORD" + }, + "image": { + "type": "object", + "properties": { + "repository": { + "default": "ghcr.io/lerianstudio/br-sta-migrations", + "description": "Repository for the migrations runner image." + }, + "tag": { + "default": "", + "description": "Tag for the migrations image. Defaults to the app image tag" + }, + "digest": { + "default": "", + "description": "Optional image digest (overrides tag when set)." + }, + "pullPolicy": { + "default": "IfNotPresent", + "description": "Image pull policy." + } + }, + "additionalProperties": true + }, + "path": { + "default": "/migrations", + "description": "Path inside the image where the SQL migrations live." + }, + "allowInsecureTLS": { + "default": "", + "description": "Bypass the lib-commons migrator TLS guard for a non-TLS Postgres" + }, + "backoffLimit": { + "default": 3, + "description": "Maximum retries before the Job is considered failed." + }, + "activeDeadlineSeconds": { + "default": 600, + "description": "Hard wall-clock cap for the Job (seconds)." + }, + "ttlSecondsAfterFinished": { + "default": 600, + "description": "Seconds to retain the finished Job before garbage collection." + }, + "timeoutSeconds": { + "default": "", + "description": "Optional MIGRATIONS_TIMEOUT_SEC passed to the runner (per-run deadline)." + }, + "resources": { + "type": "object", + "description": "Resource requests/limits for the migrations container.", + "properties": { + "requests": { + "type": "object", + "properties": { + "cpu": { + "default": "50m" + }, + "memory": { + "default": "64Mi" + } + }, + "additionalProperties": true + }, + "limits": { + "type": "object", + "properties": { + "cpu": { + "default": "250m" + }, + "memory": { + "default": "256Mi" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "annotations": { + "type": "object", + "additionalProperties": true, + "description": "Extra annotations on the Job resource." + }, + "podAnnotations": { + "type": "object", + "additionalProperties": true, + "description": "Extra annotations on the migration pod." + } + }, + "additionalProperties": true + }, + "nameOverride": { + "default": "", + "description": "Override of the resource name" + }, + "fullnameOverride": { + "default": "", + "description": "Override of the fully qualified resource name" + }, + "terminationGracePeriodSeconds": { + "default": 60, + "description": "Termination grace period." + }, + "podSecurityContext": { + "type": "object", + "additionalProperties": true, + "description": "Pod security context" + }, + "securityContext": { + "type": "object", + "description": "Container security context (Distroless nonroot UID/GID is 65532)", + "properties": { + "runAsGroup": { + "default": 65532 + }, + "runAsUser": { + "default": 65532 + }, + "runAsNonRoot": { + "default": true + }, + "allowPrivilegeEscalation": { + "default": false + }, + "capabilities": { + "type": "object", + "properties": { + "drop": { + "type": "array" + } + }, + "additionalProperties": true + }, + "readOnlyRootFilesystem": { + "default": true + }, + "seccompProfile": { + "type": "object", + "properties": { + "type": { + "default": "RuntimeDefault" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "pdb": { + "type": "object", + "description": "PodDisruptionBudget configuration", + "properties": { + "enabled": { + "default": true + }, + "minAvailable": { + "default": 1 + }, + "maxUnavailable": { + "default": "" + }, + "annotations": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "deploymentStrategy": { + "type": "object", + "description": "Deployment strategy", + "properties": { + "type": { + "default": "RollingUpdate" + }, + "rollingUpdate": { + "type": "object", + "properties": { + "maxSurge": { + "default": 1 + }, + "maxUnavailable": { + "default": 0 + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "service": { + "type": "object", + "description": "Service configuration", + "properties": { + "type": { + "default": "ClusterIP" + }, + "port": { + "default": 8080 + }, + "annotations": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "ingress": { + "type": "object", + "description": "Ingress configuration", + "properties": { + "enabled": { + "default": false + }, + "className": { + "default": "" + }, + "annotations": { + "type": "object", + "additionalProperties": true + }, + "hosts": { + "type": "array" + }, + "tls": { + "type": "array" + } + }, + "additionalProperties": true + }, + "resources": { + "type": "object", + "description": "Resource requests and limits", + "properties": { + "limits": { + "type": "object", + "properties": { + "cpu": { + "default": "1000m" + }, + "memory": { + "default": "512Mi" + } + }, + "additionalProperties": true + }, + "requests": { + "type": "object", + "properties": { + "cpu": { + "default": "100m" + }, + "memory": { + "default": "128Mi" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "autoscaling": { + "type": "object", + "description": "HorizontalPodAutoscaler configuration", + "properties": { + "enabled": { + "default": true + }, + "minReplicas": { + "default": 2 + }, + "maxReplicas": { + "default": 5 + }, + "targetCPUUtilizationPercentage": { + "default": 80 + }, + "targetMemoryUtilizationPercentage": { + "default": 80 + }, + "scaleDownStabilizationSeconds": { + "default": 300 + } + }, + "additionalProperties": true + }, + "readinessProbe": { + "type": "object", + "description": "Readiness probe configuration", + "properties": { + "initialDelaySeconds": { + "default": 5 + }, + "periodSeconds": { + "default": 5 + }, + "timeoutSeconds": { + "default": 3 + }, + "successThreshold": { + "default": 1 + }, + "failureThreshold": { + "default": 2 + } + }, + "additionalProperties": true + }, + "livenessProbe": { + "type": "object", + "description": "Liveness probe configuration", + "properties": { + "initialDelaySeconds": { + "default": 30 + }, + "periodSeconds": { + "default": 10 + }, + "timeoutSeconds": { + "default": 3 + }, + "successThreshold": { + "default": 1 + }, + "failureThreshold": { + "default": 3 + } + }, + "additionalProperties": true + }, + "nodeSelector": { + "type": "object", + "additionalProperties": true, + "description": "Node selector for scheduling pods on specific nodes" + }, + "tolerations": { + "type": "array", + "description": "Tolerations for scheduling on tainted nodes" + }, + "affinity": { + "type": "object", + "additionalProperties": true, + "description": "Affinity rules for pod scheduling" + }, + "hostAliases": { + "type": "array", + "description": "Host aliases for custom DNS resolution inside the pod" + }, + "configmap": { + "type": "object", + "additionalProperties": true, + "description": "Primary override surface: raw ConfigMap env-var passthrough. Every non-dependency", + "propertyNames": { + "enum": [ + "AGGRESSIVE_RATE_LIMIT_MAX", + "AGGRESSIVE_RATE_LIMIT_WINDOW_SEC", + "AUDIT_API_DEFAULT_PAGE_SIZE", + "AUDIT_API_MAX_EXPORT_DATE_RANGE_DAYS", + "AUDIT_API_MAX_PAGE_SIZE", + "AUDIT_CLEANUP_ENABLED", + "AUDIT_CLEANUP_INTERVAL_HOURS", + "AUDIT_CLEANUP_RETENTION_DAYS", + "AUDIT_CLEANUP_SERVICE_NAME", + "AUDIT_CONSUMER_ALTERNATE_EXCHANGE", + "AUDIT_CONSUMER_DEDUP_TTL_SEC", + "AUDIT_CONSUMER_DLQ_EXCHANGE", + "AUDIT_CONSUMER_ENABLED", + "AUDIT_CONSUMER_EXCHANGE", + "AUDIT_CONSUMER_LOCK_CLASS_ID", + "AUDIT_CONSUMER_MAX_RETRY_ATTEMPTS", + "AUDIT_CONSUMER_QUEUE", + "AUDIT_CONSUMER_SERVICE_NAME", + "AUDIT_EXPORT_GENERATOR_BATCH_SIZE", + "AUDIT_EXPORT_GENERATOR_ENABLED", + "AUDIT_EXPORT_GENERATOR_INTERVAL_SEC", + "AUDIT_EXPORT_GENERATOR_OBJECT_PREFIX", + "AUDIT_EXPORT_GENERATOR_S3_BUCKET", + "AUDIT_EXPORT_GENERATOR_S3_ENDPOINT", + "AUDIT_EXPORT_GENERATOR_S3_PATH_STYLE", + "AUDIT_EXPORT_GENERATOR_S3_REGION", + "AUDIT_EXPORT_GENERATOR_SERVICE_NAME", + "AUDIT_EXPORT_RATE_LIMIT_MAX", + "AUDIT_EXPORT_RATE_LIMIT_WINDOW_SEC", + "AUDIT_PARTITION_INTERVAL_HOURS", + "AUDIT_PARTITION_LOOKAHEAD_MONTHS", + "AUDIT_PARTITION_MANAGER_ENABLED", + "AUDIT_PARTITION_SERVICE_NAME", + "AUDIT_PUBLISHER_ALTERNATE_EXCHANGE", + "AUDIT_PUBLISHER_BATCH_SIZE", + "AUDIT_PUBLISHER_CONFIRM_TIMEOUT_SEC", + "AUDIT_PUBLISHER_ENABLED", + "AUDIT_PUBLISHER_EXCHANGE", + "AUDIT_PUBLISHER_INTERVAL_SEC", + "AUDIT_PUBLISHER_LOCK_CLASS_ID", + "AUDIT_PUBLISHER_MAX_ATTEMPTS", + "AUDIT_PUBLISHER_SERVICE_NAME", + "AWS_ACCESS_KEY_ID", + "AWS_REGION", + "AWS_SECRET_ACCESS_KEY", + "BACEN_ENVIRONMENT", + "BUSINESS_EVENTS_ALTERNATE_EXCHANGE", + "BUSINESS_EVENTS_BATCH_SIZE", + "BUSINESS_EVENTS_CONFIRM_TIMEOUT_SEC", + "BUSINESS_EVENTS_ENABLED", + "BUSINESS_EVENTS_EXCHANGE", + "BUSINESS_EVENTS_INTERVAL_SEC", + "BUSINESS_EVENTS_LOCK_CLASS_ID", + "BUSINESS_EVENTS_MAX_ATTEMPTS", + "BUSINESS_EVENTS_SERVICE_NAME", + "CIRCUIT_BREAKER_ENABLED", + "CORS_ALLOWED_HEADERS", + "CORS_ALLOWED_METHODS", + "CORS_ALLOWED_ORIGINS", + "CORS_ALLOW_CREDENTIALS", + "CORS_EXPOSE_HEADERS", + "CREDENTIALS_RECOVERY_ON_BOOT", + "DB_CONNECTION_STRING", + "DB_METRICS_INTERVAL_SEC", + "DEFAULT_TENANT_ID", + "ENABLE_TELEMETRY", + "ENV_NAME", + "EXAMPLE_STATUS_PROVIDER_MODE", + "HTTP_BODY_LIMIT_BYTES", + "IDEMPOTENCY_RETRY_WINDOW_SEC", + "INFRA_CONNECT_TIMEOUT_SEC", + "LICENSE_KEY", + "LICENSE_SERVICE_ADDRESS", + "LOG_LEVEL", + "M2M_CREDENTIAL_CACHE_TTL_SEC", + "M2M_TARGET_SERVICE", + "MASTER_ENCRYPTION_KEY", + "MASTER_KEYS", + "MASTER_KEY_KMS_KEY_ID", + "MASTER_KEY_KMS_REGION", + "MASTER_KEY_PROVIDER", + "MASTER_KEY_VERSION", + "MAX_PAGINATION_LIMIT", + "MAX_PAGINATION_MONTH_DATE_RANGE", + "MIGRATIONS_PATH", + "MULTI_TENANT_ALLOW_INSECURE_HTTP", + "MULTI_TENANT_CACHE_TTL_SEC", + "MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD", + "MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC", + "MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC", + "MULTI_TENANT_ENABLED", + "MULTI_TENANT_IDLE_TIMEOUT_SEC", + "MULTI_TENANT_MAX_TENANT_POOLS", + "MULTI_TENANT_POOL_MAX_CONNS", + "MULTI_TENANT_POOL_MAX_IDLE_CONNS", + "MULTI_TENANT_REDIS_HOST", + "MULTI_TENANT_REDIS_PASSWORD", + "MULTI_TENANT_REDIS_PORT", + "MULTI_TENANT_REDIS_TLS", + "MULTI_TENANT_SERVICE_API_KEY", + "MULTI_TENANT_TIMEOUT", + "MULTI_TENANT_URL", + "ORGANIZATION_IDS", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_LIBRARY_NAME", + "OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT", + "OTEL_RESOURCE_SERVICE_NAME", + "OTEL_RESOURCE_SERVICE_VERSION", + "OUTBOX_ALLOW_EMPTY_TENANT", + "OUTBOX_BATCH_SIZE", + "OUTBOX_DISPATCH_INTERVAL_SEC", + "OUTBOX_ENABLED", + "OUTBOX_INCLUDE_TENANT_METRICS", + "OUTBOX_MAX_DISPATCH_ATTEMPTS", + "OUTBOX_MAX_FAILED_PER_BATCH", + "OUTBOX_PRIORITY_EVENT_TYPES", + "OUTBOX_PROCESSING_TIMEOUT_SEC", + "OUTBOX_PUBLISH_BACKOFF_MS", + "OUTBOX_PUBLISH_MAX_ATTEMPTS", + "OUTBOX_RETRY_WINDOW_SEC", + "OUTBOX_TABLE_NAME", + "PLUGIN_AUTH_ENABLED", + "PLUGIN_AUTH_HOST", + "POSTGRES_CONNECT_TIMEOUT_SEC", + "POSTGRES_CONN_MAX_IDLE_TIME_MINS", + "POSTGRES_CONN_MAX_LIFETIME_MINS", + "POSTGRES_HOST", + "POSTGRES_MAX_CONNECTIONS", + "POSTGRES_MAX_IDLE_CONNS", + "POSTGRES_MAX_OPEN_CONNS", + "POSTGRES_NAME", + "POSTGRES_PASSWORD", + "POSTGRES_PORT", + "POSTGRES_REPLICA_HOST", + "POSTGRES_REPLICA_NAME", + "POSTGRES_REPLICA_PASSWORD", + "POSTGRES_REPLICA_PORT", + "POSTGRES_REPLICA_SSLMODE", + "POSTGRES_REPLICA_USER", + "POSTGRES_SHARED_BUFFERS", + "POSTGRES_SSLMODE", + "POSTGRES_USER", + "RABBITMQ_ALLOW_INSECURE_HEALTH_CHECK", + "RABBITMQ_ALLOW_INSECURE_TLS", + "RABBITMQ_DEFAULT_PASS", + "RABBITMQ_DEFAULT_USER", + "RABBITMQ_ENABLED", + "RABBITMQ_EXCHANGE", + "RABBITMQ_HEALTH_CHECK_ALLOWED_HOSTS", + "RABBITMQ_HEALTH_CHECK_URL", + "RABBITMQ_HOST", + "RABBITMQ_PORT_AMQP", + "RABBITMQ_PORT_HOST", + "RABBITMQ_PUBLISHER_CONFIRM_TIMEOUT_MS", + "RABBITMQ_PUBLISHER_MAX_RECOVERIES", + "RABBITMQ_PUBLISHER_RECOVERY_INITIAL_MS", + "RABBITMQ_PUBLISHER_RECOVERY_MAX_MS", + "RABBITMQ_QUEUE", + "RABBITMQ_REQUIRE_HEALTH_ALLOWED_HOSTS", + "RABBITMQ_SCHEME", + "RABBITMQ_URL", + "RABBITMQ_VHOST", + "RATE_LIMIT_ENABLED", + "RATE_LIMIT_MAX", + "RATE_LIMIT_WINDOW_SEC", + "REDIS_CA_CERT", + "REDIS_DB", + "REDIS_DIAL_TIMEOUT", + "REDIS_HOST", + "REDIS_MASTER_NAME", + "REDIS_MAX_RETRIES", + "REDIS_MAX_RETRY_BACKOFF", + "REDIS_MIN_IDLE_CONNS", + "REDIS_MIN_RETRY_BACKOFF", + "REDIS_PASSWORD", + "REDIS_POOL_SIZE", + "REDIS_POOL_TIMEOUT", + "REDIS_PROTOCOL", + "REDIS_READ_TIMEOUT", + "REDIS_TLS", + "REDIS_URL", + "REDIS_WRITE_TIMEOUT", + "RELAXED_RATE_LIMIT_MAX", + "RELAXED_RATE_LIMIT_WINDOW_SEC", + "SCHEDULER_ENABLED", + "SD_ADDRESS", + "SD_ENABLED", + "SD_EXTERNAL_ADDRESS", + "SD_EXTERNAL_PORT", + "SD_INTERNAL_ADDRESS", + "SD_INTERNAL_PORT", + "SD_INTERNAL_SCHEME", + "SD_PREFER_VIEW", + "SD_TLS", + "SD_TLS_SKIP_VERIFY", + "SD_TOKEN", + "SD_WATCH_WAIT_TIME", + "SD_WORKLOAD", + "SERVER_ADDRESS", + "SERVER_TLS_CERT_FILE", + "SERVER_TLS_KEY_FILE", + "SERVER_TRUSTED_PROXIES", + "STREAMING_BATCH_LINGER_MS", + "STREAMING_BROKERS", + "STREAMING_CB_FAILURE_RATIO", + "STREAMING_CB_MIN_REQUESTS", + "STREAMING_CB_TIMEOUT_S", + "STREAMING_CLIENT_ID", + "STREAMING_CLOSE_TIMEOUT_S", + "STREAMING_CLOUDEVENTS_SOURCE", + "STREAMING_COMPRESSION", + "STREAMING_ENABLED", + "STREAMING_IMPORTANT_EMIT_TIMEOUT_MS", + "STREAMING_REQUIRED_ACKS", + "STREAMING_SASL_ALLOW_PLAINTEXT", + "STREAMING_SASL_MECHANISM", + "STREAMING_SASL_PASSWORD", + "STREAMING_SASL_USERNAME", + "STREAMING_TLS_CA_CERT", + "STREAMING_TLS_ENABLED", + "SWAGGER_BASE_PATH", + "SWAGGER_DESCRIPTION", + "SWAGGER_ENABLED", + "SWAGGER_LEFT_DELIM", + "SWAGGER_RIGHT_DELIM", + "SWAGGER_TITLE", + "SWAGGER_VERSION", + "SYSTEMPLANE_ENABLED", + "TLS_TERMINATED_UPSTREAM", + "TRANSFER_INBOUND_ENABLED", + "TRANSFER_INBOUND_MAX_FILE_SIZE_BYTES", + "TRANSFER_OBJECT_STORAGE_BUCKET", + "TRUST_STORE_DEFAULT_PAGE_SIZE", + "TRUST_STORE_EXPIRING_SOON_DAYS", + "TRUST_STORE_MAX_CERT_SIZE_BYTES", + "TRUST_STORE_MAX_PAGE_SIZE", + "TRUST_STORE_S3_BUCKET", + "TRUST_STORE_S3_ENDPOINT", + "TRUST_STORE_S3_PATH_STYLE", + "TRUST_STORE_S3_REGION", + "VERSION" + ] + } + }, + "serviceDiscovery": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false, + "description": "Service discovery toggle (grouped API for SD_ENABLED). SD_TOKEN (ACL) is a Secret." + }, + "streaming": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false, + "description": "Streaming toggle (grouped API for STREAMING_ENABLED). SASL password is a Secret." + }, + "secrets": { + "type": "object", + "additionalProperties": true, + "description": "Secrets (sensitive environment variables)" + }, + "useExistingSecret": { + "default": false, + "description": "Use an externally managed Secret instead of generating one" + }, + "existingSecretName": { + "default": "", + "description": "Name of the externally managed Secret" + }, + "extraEnvVars": { + "type": "object", + "additionalProperties": true, + "description": "Extra environment variables (map of key:value pairs)" + }, + "serviceAccount": { + "type": "object", + "description": "ServiceAccount configuration", + "properties": { + "create": { + "default": true + }, + "annotations": { + "type": "object", + "additionalProperties": true + }, + "name": { + "default": "" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "worker": { + "type": "object", + "properties": { + "enabled": { + "default": false, + "description": "Enable the worker Deployment. REQUIRES worker.image to point at a" + }, + "replicaCount": { + "default": 1, + "description": "Replicas. The background jobs are leader-gated but the worker does NOT" + }, + "revisionHistoryLimit": { + "default": 10, + "description": "Number of old ReplicaSets to retain for rollback" + }, + "annotations": { + "type": "object", + "additionalProperties": true, + "description": "Annotations applied to the Deployment resource" + }, + "podAnnotations": { + "type": "object", + "additionalProperties": true, + "description": "Annotations applied to the pods" + }, + "image": { + "type": "object", + "description": "Worker image. REQUIRED: point at the dedicated worker image (built", + "properties": { + "repository": { + "default": "" + }, + "tag": { + "default": "" + }, + "pullPolicy": { + "default": "IfNotPresent" + } + }, + "additionalProperties": true + }, + "imagePullSecrets": { + "type": "array", + "description": "Image pull secrets (empty inherits the manager's)" + }, + "command": { + "type": "array", + "description": "Container command. The worker image's own ENTRYPOINT already runs the" + }, + "terminationGracePeriodSeconds": { + "default": 60, + "description": "Termination grace period" + }, + "podSecurityContext": { + "type": "object", + "additionalProperties": true, + "description": "Pod security context" + }, + "securityContext": { + "type": "object", + "additionalProperties": true, + "description": "Container security context (empty inherits the manager's)" + }, + "serviceAccount": { + "type": "object", + "description": "ServiceAccount configuration", + "properties": { + "create": { + "default": true + }, + "annotations": { + "type": "object", + "additionalProperties": true + }, + "name": { + "default": "" + } + }, + "additionalProperties": true + }, + "service": { + "type": "object", + "description": "Probe server port. WorkerMode binds SERVER_ADDRESS here; no Service or", + "properties": { + "port": { + "default": 8081 + } + }, + "additionalProperties": true + }, + "resources": { + "type": "object", + "description": "Resource requests and limits", + "properties": { + "limits": { + "type": "object", + "properties": { + "cpu": { + "default": "500m" + }, + "memory": { + "default": "512Mi" + } + }, + "additionalProperties": true + }, + "requests": { + "type": "object", + "properties": { + "cpu": { + "default": "100m" + }, + "memory": { + "default": "128Mi" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "deploymentStrategy": { + "type": "object", + "description": "Deployment strategy. Recreate by default: the worker is leader-gated but", + "properties": { + "type": { + "default": "Recreate" + } + }, + "additionalProperties": true + }, + "readinessProbe": { + "type": "object", + "description": "Readiness probe configuration", + "properties": { + "initialDelaySeconds": { + "default": 5 + }, + "periodSeconds": { + "default": 5 + }, + "timeoutSeconds": { + "default": 3 + }, + "successThreshold": { + "default": 1 + }, + "failureThreshold": { + "default": 2 + } + }, + "additionalProperties": true + }, + "livenessProbe": { + "type": "object", + "description": "Liveness probe configuration", + "properties": { + "initialDelaySeconds": { + "default": 30 + }, + "periodSeconds": { + "default": 10 + }, + "timeoutSeconds": { + "default": 3 + }, + "successThreshold": { + "default": 1 + }, + "failureThreshold": { + "default": 3 + } + }, + "additionalProperties": true + }, + "nodeSelector": { + "type": "object", + "additionalProperties": true, + "description": "Node selector for scheduling pods on specific nodes" + }, + "tolerations": { + "type": "array", + "description": "Tolerations for scheduling on tainted nodes" + }, + "affinity": { + "type": "object", + "additionalProperties": true, + "description": "Affinity rules for pod scheduling" + }, + "hostAliases": { + "type": "array", + "description": "Host aliases for custom DNS resolution inside the pod" + }, + "configmap": { + "type": "object", + "additionalProperties": true, + "description": "Worker-only environment (layered on top of the manager ConfigMap/Secret).", + "propertyNames": { + "enum": [ + "AGGRESSIVE_RATE_LIMIT_MAX", + "AGGRESSIVE_RATE_LIMIT_WINDOW_SEC", + "AUDIT_API_DEFAULT_PAGE_SIZE", + "AUDIT_API_MAX_EXPORT_DATE_RANGE_DAYS", + "AUDIT_API_MAX_PAGE_SIZE", + "AUDIT_CLEANUP_ENABLED", + "AUDIT_CLEANUP_INTERVAL_HOURS", + "AUDIT_CLEANUP_RETENTION_DAYS", + "AUDIT_CLEANUP_SERVICE_NAME", + "AUDIT_CONSUMER_ALTERNATE_EXCHANGE", + "AUDIT_CONSUMER_DEDUP_TTL_SEC", + "AUDIT_CONSUMER_DLQ_EXCHANGE", + "AUDIT_CONSUMER_ENABLED", + "AUDIT_CONSUMER_EXCHANGE", + "AUDIT_CONSUMER_LOCK_CLASS_ID", + "AUDIT_CONSUMER_MAX_RETRY_ATTEMPTS", + "AUDIT_CONSUMER_QUEUE", + "AUDIT_CONSUMER_SERVICE_NAME", + "AUDIT_EXPORT_GENERATOR_BATCH_SIZE", + "AUDIT_EXPORT_GENERATOR_ENABLED", + "AUDIT_EXPORT_GENERATOR_INTERVAL_SEC", + "AUDIT_EXPORT_GENERATOR_OBJECT_PREFIX", + "AUDIT_EXPORT_GENERATOR_S3_BUCKET", + "AUDIT_EXPORT_GENERATOR_S3_ENDPOINT", + "AUDIT_EXPORT_GENERATOR_S3_PATH_STYLE", + "AUDIT_EXPORT_GENERATOR_S3_REGION", + "AUDIT_EXPORT_GENERATOR_SERVICE_NAME", + "AUDIT_EXPORT_RATE_LIMIT_MAX", + "AUDIT_EXPORT_RATE_LIMIT_WINDOW_SEC", + "AUDIT_PARTITION_INTERVAL_HOURS", + "AUDIT_PARTITION_LOOKAHEAD_MONTHS", + "AUDIT_PARTITION_MANAGER_ENABLED", + "AUDIT_PARTITION_SERVICE_NAME", + "AUDIT_PUBLISHER_ALTERNATE_EXCHANGE", + "AUDIT_PUBLISHER_BATCH_SIZE", + "AUDIT_PUBLISHER_CONFIRM_TIMEOUT_SEC", + "AUDIT_PUBLISHER_ENABLED", + "AUDIT_PUBLISHER_EXCHANGE", + "AUDIT_PUBLISHER_INTERVAL_SEC", + "AUDIT_PUBLISHER_LOCK_CLASS_ID", + "AUDIT_PUBLISHER_MAX_ATTEMPTS", + "AUDIT_PUBLISHER_SERVICE_NAME", + "AWS_ACCESS_KEY_ID", + "AWS_REGION", + "AWS_SECRET_ACCESS_KEY", + "BACEN_ENVIRONMENT", + "BUSINESS_EVENTS_ALTERNATE_EXCHANGE", + "BUSINESS_EVENTS_BATCH_SIZE", + "BUSINESS_EVENTS_CONFIRM_TIMEOUT_SEC", + "BUSINESS_EVENTS_ENABLED", + "BUSINESS_EVENTS_EXCHANGE", + "BUSINESS_EVENTS_INTERVAL_SEC", + "BUSINESS_EVENTS_LOCK_CLASS_ID", + "BUSINESS_EVENTS_MAX_ATTEMPTS", + "BUSINESS_EVENTS_SERVICE_NAME", + "CIRCUIT_BREAKER_ENABLED", + "CORS_ALLOWED_HEADERS", + "CORS_ALLOWED_METHODS", + "CORS_ALLOWED_ORIGINS", + "CORS_ALLOW_CREDENTIALS", + "CORS_EXPOSE_HEADERS", + "CREDENTIALS_RECOVERY_ON_BOOT", + "DB_CONNECTION_STRING", + "DB_METRICS_INTERVAL_SEC", + "DEFAULT_TENANT_ID", + "ENABLE_TELEMETRY", + "ENV_NAME", + "EXAMPLE_STATUS_PROVIDER_MODE", + "HTTP_BODY_LIMIT_BYTES", + "IDEMPOTENCY_RETRY_WINDOW_SEC", + "INFRA_CONNECT_TIMEOUT_SEC", + "LICENSE_KEY", + "LICENSE_SERVICE_ADDRESS", + "LOG_LEVEL", + "M2M_CREDENTIAL_CACHE_TTL_SEC", + "M2M_TARGET_SERVICE", + "MASTER_ENCRYPTION_KEY", + "MASTER_KEYS", + "MASTER_KEY_KMS_KEY_ID", + "MASTER_KEY_KMS_REGION", + "MASTER_KEY_PROVIDER", + "MASTER_KEY_VERSION", + "MAX_PAGINATION_LIMIT", + "MAX_PAGINATION_MONTH_DATE_RANGE", + "MIGRATIONS_PATH", + "MULTI_TENANT_ALLOW_INSECURE_HTTP", + "MULTI_TENANT_CACHE_TTL_SEC", + "MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD", + "MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC", + "MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC", + "MULTI_TENANT_ENABLED", + "MULTI_TENANT_IDLE_TIMEOUT_SEC", + "MULTI_TENANT_MAX_TENANT_POOLS", + "MULTI_TENANT_POOL_MAX_CONNS", + "MULTI_TENANT_POOL_MAX_IDLE_CONNS", + "MULTI_TENANT_REDIS_HOST", + "MULTI_TENANT_REDIS_PASSWORD", + "MULTI_TENANT_REDIS_PORT", + "MULTI_TENANT_REDIS_TLS", + "MULTI_TENANT_SERVICE_API_KEY", + "MULTI_TENANT_TIMEOUT", + "MULTI_TENANT_URL", + "ORGANIZATION_IDS", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_LIBRARY_NAME", + "OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT", + "OTEL_RESOURCE_SERVICE_NAME", + "OTEL_RESOURCE_SERVICE_VERSION", + "OUTBOX_ALLOW_EMPTY_TENANT", + "OUTBOX_BATCH_SIZE", + "OUTBOX_DISPATCH_INTERVAL_SEC", + "OUTBOX_ENABLED", + "OUTBOX_INCLUDE_TENANT_METRICS", + "OUTBOX_MAX_DISPATCH_ATTEMPTS", + "OUTBOX_MAX_FAILED_PER_BATCH", + "OUTBOX_PRIORITY_EVENT_TYPES", + "OUTBOX_PROCESSING_TIMEOUT_SEC", + "OUTBOX_PUBLISH_BACKOFF_MS", + "OUTBOX_PUBLISH_MAX_ATTEMPTS", + "OUTBOX_RETRY_WINDOW_SEC", + "OUTBOX_TABLE_NAME", + "PLUGIN_AUTH_ENABLED", + "PLUGIN_AUTH_HOST", + "POSTGRES_CONNECT_TIMEOUT_SEC", + "POSTGRES_CONN_MAX_IDLE_TIME_MINS", + "POSTGRES_CONN_MAX_LIFETIME_MINS", + "POSTGRES_HOST", + "POSTGRES_MAX_CONNECTIONS", + "POSTGRES_MAX_IDLE_CONNS", + "POSTGRES_MAX_OPEN_CONNS", + "POSTGRES_NAME", + "POSTGRES_PASSWORD", + "POSTGRES_PORT", + "POSTGRES_REPLICA_HOST", + "POSTGRES_REPLICA_NAME", + "POSTGRES_REPLICA_PASSWORD", + "POSTGRES_REPLICA_PORT", + "POSTGRES_REPLICA_SSLMODE", + "POSTGRES_REPLICA_USER", + "POSTGRES_SHARED_BUFFERS", + "POSTGRES_SSLMODE", + "POSTGRES_USER", + "RABBITMQ_ALLOW_INSECURE_HEALTH_CHECK", + "RABBITMQ_ALLOW_INSECURE_TLS", + "RABBITMQ_DEFAULT_PASS", + "RABBITMQ_DEFAULT_USER", + "RABBITMQ_ENABLED", + "RABBITMQ_EXCHANGE", + "RABBITMQ_HEALTH_CHECK_ALLOWED_HOSTS", + "RABBITMQ_HEALTH_CHECK_URL", + "RABBITMQ_HOST", + "RABBITMQ_PORT_AMQP", + "RABBITMQ_PORT_HOST", + "RABBITMQ_PUBLISHER_CONFIRM_TIMEOUT_MS", + "RABBITMQ_PUBLISHER_MAX_RECOVERIES", + "RABBITMQ_PUBLISHER_RECOVERY_INITIAL_MS", + "RABBITMQ_PUBLISHER_RECOVERY_MAX_MS", + "RABBITMQ_QUEUE", + "RABBITMQ_REQUIRE_HEALTH_ALLOWED_HOSTS", + "RABBITMQ_SCHEME", + "RABBITMQ_URL", + "RABBITMQ_VHOST", + "RATE_LIMIT_ENABLED", + "RATE_LIMIT_MAX", + "RATE_LIMIT_WINDOW_SEC", + "REDIS_CA_CERT", + "REDIS_DB", + "REDIS_DIAL_TIMEOUT", + "REDIS_HOST", + "REDIS_MASTER_NAME", + "REDIS_MAX_RETRIES", + "REDIS_MAX_RETRY_BACKOFF", + "REDIS_MIN_IDLE_CONNS", + "REDIS_MIN_RETRY_BACKOFF", + "REDIS_PASSWORD", + "REDIS_POOL_SIZE", + "REDIS_POOL_TIMEOUT", + "REDIS_PROTOCOL", + "REDIS_READ_TIMEOUT", + "REDIS_TLS", + "REDIS_URL", + "REDIS_WRITE_TIMEOUT", + "RELAXED_RATE_LIMIT_MAX", + "RELAXED_RATE_LIMIT_WINDOW_SEC", + "SCHEDULER_ENABLED", + "SD_ADDRESS", + "SD_ENABLED", + "SD_EXTERNAL_ADDRESS", + "SD_EXTERNAL_PORT", + "SD_INTERNAL_ADDRESS", + "SD_INTERNAL_PORT", + "SD_INTERNAL_SCHEME", + "SD_PREFER_VIEW", + "SD_TLS", + "SD_TLS_SKIP_VERIFY", + "SD_TOKEN", + "SD_WATCH_WAIT_TIME", + "SD_WORKLOAD", + "SERVER_ADDRESS", + "SERVER_TLS_CERT_FILE", + "SERVER_TLS_KEY_FILE", + "SERVER_TRUSTED_PROXIES", + "STREAMING_BATCH_LINGER_MS", + "STREAMING_BROKERS", + "STREAMING_CB_FAILURE_RATIO", + "STREAMING_CB_MIN_REQUESTS", + "STREAMING_CB_TIMEOUT_S", + "STREAMING_CLIENT_ID", + "STREAMING_CLOSE_TIMEOUT_S", + "STREAMING_CLOUDEVENTS_SOURCE", + "STREAMING_COMPRESSION", + "STREAMING_ENABLED", + "STREAMING_IMPORTANT_EMIT_TIMEOUT_MS", + "STREAMING_REQUIRED_ACKS", + "STREAMING_SASL_ALLOW_PLAINTEXT", + "STREAMING_SASL_MECHANISM", + "STREAMING_SASL_PASSWORD", + "STREAMING_SASL_USERNAME", + "STREAMING_TLS_CA_CERT", + "STREAMING_TLS_ENABLED", + "SWAGGER_BASE_PATH", + "SWAGGER_DESCRIPTION", + "SWAGGER_ENABLED", + "SWAGGER_LEFT_DELIM", + "SWAGGER_RIGHT_DELIM", + "SWAGGER_TITLE", + "SWAGGER_VERSION", + "SYSTEMPLANE_ENABLED", + "TLS_TERMINATED_UPSTREAM", + "TRANSFER_INBOUND_ENABLED", + "TRANSFER_INBOUND_MAX_FILE_SIZE_BYTES", + "TRANSFER_OBJECT_STORAGE_BUCKET", + "TRUST_STORE_DEFAULT_PAGE_SIZE", + "TRUST_STORE_EXPIRING_SOON_DAYS", + "TRUST_STORE_MAX_CERT_SIZE_BYTES", + "TRUST_STORE_MAX_PAGE_SIZE", + "TRUST_STORE_S3_BUCKET", + "TRUST_STORE_S3_ENDPOINT", + "TRUST_STORE_S3_PATH_STYLE", + "TRUST_STORE_S3_REGION", + "VERSION" + ] + } + }, + "extraEnvVars": { + "type": "object", + "additionalProperties": true, + "description": "Extra environment variables (map of key:value pairs) rendered as inline" + } + }, + "additionalProperties": true + }, + "postgresql": { + "type": "object", + "additionalProperties": true + }, + "valkey": { + "type": "object", + "additionalProperties": true + }, + "rabbitmq": { + "type": "object", + "additionalProperties": true + }, + "otel-collector-lerian": { + "type": "object", + "properties": { + "enabled": { + "default": false + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true +} diff --git a/charts/br-sta/values.yaml b/charts/br-sta/values.yaml new file mode 100644 index 000000000..8683bc2cf --- /dev/null +++ b/charts/br-sta/values.yaml @@ -0,0 +1,594 @@ +# Default values for br-sta. +# This is a YAML-formatted file. +# +# Runtime model: ONE Deployment, ONE pod, ONE process (the /service binary). +# br-sta is a Go/Fiber HTTP service. It requires PostgreSQL (SQL migrations at +# startup) and Redis/Valkey (caching + rate limiting). RabbitMQ, plugin-auth, +# multi-tenant, outbox and telemetry are all OPTIONAL and disabled by default. + +# -- Override the chart top-level name +nameOverride: "br-sta" +# -- Override the fully generated name +fullnameOverride: "" +# -- Override the namespace used by templates +namespaceOverride: "" + +global: + # -- Bootstrap job for external PostgreSQL: creates database and role + externalPostgresDefinitions: + # -- Enable or disable the PostgreSQL bootstrap job + enabled: false + # -- PostgreSQL connection settings (used by the bootstrap job only) + connection: + # -- PostgreSQL host + host: "br-sta-postgresql-primary" + # -- PostgreSQL port + port: "5432" + # -- Admin (superuser) credentials used to create the application DB and role + postgresAdminLogin: + useExistingSecret: + # -- Name of existing secret containing DB_USER_ADMIN and DB_ADMIN_PASSWORD keys + name: "" + # -- Admin username (ignored if useExistingSecret.name is set) + username: "postgres" + # -- Admin password (ignored if useExistingSecret.name is set) + password: "" + # -- Credentials for the br_sta role created by the job + appCredentials: + useExistingSecret: + # -- Name of existing secret containing DB_PASSWORD_BR_STA key + name: "" + # -- Password for br_sta role (ignored if useExistingSecret.name is set) + password: "" + # -- Env-wide observability, consumed by lerian-common.otel.env. Declare once at + # the umbrella level; a component configmap. still overrides per-service. + # Precedence: brSta.configmap. > global.observability. > chart default. + observability: + # -- Enable telemetry export (ENABLE_TELEMETRY). Unset → chart default "false". + enabled: false + # -- OTLP collector endpoint (OTEL_EXPORTER_OTLP_ENDPOINT). Unset → chart default "". + otlpEndpoint: "" + # -- Deployment environment tag (OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT). Unset → "production". + deploymentEnvironment: "production" + # -- Env-wide multi-tenant infra (tenant-manager URL + its Redis), consumed by + # lerian-common.multiTenant.env. Only used when MT is enabled (see + # brSta.multiTenant.enabled). A component configmap. still overrides. + multiTenant: {} + # url: "" # MULTI_TENANT_URL (required when MT enabled) + # redisHost: "" # MULTI_TENANT_REDIS_HOST (required when MT enabled) + # redisPort: "6379" # MULTI_TENANT_REDIS_PORT + # redisTls: "false" # MULTI_TENANT_REDIS_TLS + # -- Env-wide datastore mask, consumed by lerian-common.datastore.value. Declare a + # SHARED instance once here; a DEDICATED per-service instance goes under + # brSta.datastores; a component configmap. still overrides everything. + # Precedence: brSta.configmap. > brSta.datastores.. + # > global.datastores.. > chart default. + datastores: {} + # postgres: + # host: "" # POSTGRES_HOST (host only) + # port: "5432" # POSTGRES_PORT + # user: "br_sta" # POSTGRES_USER + # ssl: "require" # POSTGRES_SSLMODE + # replicaHost: "" # POSTGRES_REPLICA_HOST (enables the replica block when set) + # redis: + # host: "" # REDIS_HOST (host:port) + # broker: + # host: "" # RABBITMQ_HOST + # port: "5672" # RABBITMQ_PORT_AMQP + # user: "br_sta" # RABBITMQ_DEFAULT_USER + # -- Env-wide service discovery (Consul), consumed by lerian-common.serviceDiscovery.env. + # Only used when SD is enabled (brSta.serviceDiscovery.enabled). Leave {} for defaults. + serviceDiscovery: {} + # -- Env-wide streaming (lib-streaming → Kafka), consumed by lerian-common.streaming.env. + # Only used when streaming is enabled (brSta.streaming.enabled). Leave {} for defaults. + streaming: {} + # -- Env-wide inbound auth (access-manager), consumed by lerian-common.globalValue + # for PLUGIN_AUTH_ENABLED / PLUGIN_AUTH_HOST. A component configmap. still overrides. + # Precedence: brSta.configmap.PLUGIN_AUTH_* > global.auth. > chart default. + auth: {} + # enabled: "false" # PLUGIN_AUTH_ENABLED + # host: "" # PLUGIN_AUTH_HOST (access-manager base URL) + +# ============================================================================== +# APPLICATION +# Single Deployment running the /service binary (Go/Fiber HTTP server). +# ============================================================================== +brSta: + # -- Multi-tenancy toggle (grouped API for MULTI_TENANT_ENABLED). Tenant-manager + # URL + Redis infra come from global.multiTenant (or configmap.MULTI_TENANT_*). + # Precedence: configmap.MULTI_TENANT_ENABLED > brSta.multiTenant.enabled > "false". + multiTenant: + # -- Enable multi-tenancy (MULTI_TENANT_ENABLED) + enabled: false + # -- Dedicated datastore mask for THIS service (see global.datastores for the shared + # form + precedence). Same fields as global.datastores; wins over global, loses to + # configmap.. Leave empty ({}) to keep the bundled-subchart / native defaults. + datastores: {} + # -- Service name + name: "br-sta" + # -- Number of replicas + replicaCount: 2 + # -- Number of old ReplicaSets to retain for rollback + revisionHistoryLimit: 10 + # -- Annotations applied to the Deployment resource + annotations: {} + # -- Annotations applied to the pods + podAnnotations: {} + image: + # -- Repository for the br-sta image + repository: ghcr.io/lerianstudio/br-sta + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Image tag (defaults to Chart.appVersion if empty) + tag: "" + # -- Image pull secrets for private registries + imagePullSecrets: [] + # -- Database migrations Job (init/postgres-migrations runner image). + # Applies the SQL migrations once as an Argo hook — PreSync for external + # Postgres (schema-first, before the app rolls out) or PostSync for the + # bundled subchart (after the DB is provisioned) — so schema changes are + # applied by a dedicated process rather than at application runtime. + # Disabled by default; enable per-environment (e.g. the dev-st gitops values). + migrations: + # -- Enable or disable the migrations Job. + enabled: false + # -- When true, read POSTGRES_PASSWORD from a pre-existing Secret + # (existingSecretName) instead of the chart-managed app Secret. + useExistingSecret: false + # -- Name of the pre-existing Secret containing POSTGRES_PASSWORD + # (only used when useExistingSecret=true). + existingSecretName: "" + image: + # -- Repository for the migrations runner image. + repository: ghcr.io/lerianstudio/br-sta-migrations + # -- Tag for the migrations image. Defaults to the app image tag + # (brSta.image.tag, or the chart appVersion) when left empty. + tag: "" + # -- Optional image digest (overrides tag when set). + digest: "" + # -- Image pull policy. + pullPolicy: IfNotPresent + # -- Path inside the image where the SQL migrations live. + path: "/migrations" + # -- Bypass the lib-commons migrator TLS guard for a non-TLS Postgres + # (POSTGRES_SSLMODE=disable is rejected without it). When empty, inherits + # ALLOW_INSECURE_TLS from brSta.extraEnvVars, then brSta.configmap. Leave + # empty for a TLS-enabled Postgres. + allowInsecureTLS: "" + # -- Maximum retries before the Job is considered failed. + backoffLimit: 3 + # -- Hard wall-clock cap for the Job (seconds). + activeDeadlineSeconds: 600 + # -- Seconds to retain the finished Job before garbage collection. + ttlSecondsAfterFinished: 600 + # -- Optional MIGRATIONS_TIMEOUT_SEC passed to the runner (per-run deadline). + timeoutSeconds: "" + # -- Resource requests/limits for the migrations container. + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 250m + memory: 256Mi + # -- Extra annotations on the Job resource. + annotations: {} + # -- Extra annotations on the migration pod. + podAnnotations: {} + # -- Override of the resource name + nameOverride: "" + # -- Override of the fully qualified resource name + fullnameOverride: "" + # -- Termination grace period. + terminationGracePeriodSeconds: 60 + # -- Pod security context + podSecurityContext: {} + # -- Container security context (Distroless nonroot UID/GID is 65532) + securityContext: + runAsGroup: 65532 + runAsUser: 65532 + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault + # -- PodDisruptionBudget configuration + pdb: + enabled: true + minAvailable: 1 + maxUnavailable: "" + annotations: {} + # -- Deployment strategy + deploymentStrategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + # -- Service configuration + service: + type: ClusterIP + port: 8080 + annotations: {} + # -- Ingress configuration + ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: "" + paths: + - path: / + pathType: Prefix + tls: [] + # -- Resource requests and limits + resources: + limits: + cpu: 1000m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + # -- HorizontalPodAutoscaler configuration + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + scaleDownStabilizationSeconds: 300 + # -- Readiness probe configuration + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 2 + # -- Liveness probe configuration + livenessProbe: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 3 + # -- Node selector for scheduling pods on specific nodes + nodeSelector: {} + # -- Tolerations for scheduling on tainted nodes + tolerations: [] + # -- Affinity rules for pod scheduling + affinity: {} + # -- Host aliases for custom DNS resolution inside the pod + hostAliases: [] + # -- Primary override surface: raw ConfigMap env-var passthrough. Every non-dependency + # app key defaults in templates/configmap.yaml; set brSta.configmap. here + # to override a default or pass an opt-in key (e.g. AUDIT_PUBLISHER_INTERVAL_SEC). + # Dependency connections are the ONLY typed knobs — Postgres/Redis/RabbitMQ HOST/PORT/ + # USER/SSL via the datastores mask; MULTI_TENANT/OTEL/streaming/SD via the global.* + # blocks; PLUGIN_AUTH via global.auth. Precedence: configmap. > mask/global > default. + # @default -- templates/configmap.yaml + configmap: {} + # -- Service discovery toggle (grouped API for SD_ENABLED). SD_TOKEN (ACL) is a Secret. + serviceDiscovery: + enabled: false + # -- Streaming toggle (grouped API for STREAMING_ENABLED). SASL password is a Secret. + streaming: + enabled: false + # -- Secrets (sensitive environment variables) + # @default -- templates/secrets.yaml + secrets: + # PostgreSQL password is single-sourced from the postgresql subchart Secret + # (name derived collapse-aware via common.names.dependency.fullname; key "password") + # and read via secretKeyRef on the deployment — leave empty with the bundled subchart. + # Only set this for an EXTERNAL Postgres without postgresql.auth.existingSecret. + # See docs/helm-chart-standard.md "Single-Source Infra Secrets". + POSTGRES_PASSWORD: "" + # POSTGRES_REPLICA_PASSWORD: "" + # Redis/Valkey password is single-sourced from the valkey subchart Secret + # (key "valkey-password") and read via secretKeyRef — leave empty with the + # bundled subchart. Only set this for an EXTERNAL Redis without + # valkey.auth.existingSecret. + REDIS_PASSWORD: "" + # RabbitMQ password — kept in the app Secret (only relevant when RABBITMQ_ENABLED=true). + RABBITMQ_DEFAULT_PASS: "" + # Multi-Tenant Manager API key (REQUIRED when MULTI_TENANT_ENABLED=true) + MULTI_TENANT_SERVICE_API_KEY: "" + # MULTI_TENANT_REDIS_PASSWORD: "" + # Crypto master-key catalogue (MASTER_KEY_PROVIDER=envvar) — key material, provide via a secrets manager. + MASTER_KEYS: "" + # Object-storage credential (the non-secret AWS_ACCESS_KEY_ID lives in the ConfigMap / m2m group). + AWS_SECRET_ACCESS_KEY: "" + # Service-discovery ACL token (only when SD is enabled). + SD_TOKEN: "" + # Streaming SASL password (REQUIRED when streaming is enabled with a SASL mechanism). + STREAMING_SASL_PASSWORD: "" + # -- Use an externally managed Secret instead of generating one + useExistingSecret: false + # -- Name of the externally managed Secret + existingSecretName: "" + # -- Extra environment variables (map of key:value pairs) + extraEnvVars: {} + # -- ServiceAccount configuration + serviceAccount: + create: true + annotations: {} + name: "" + +# ============================================================================== +# WORKER +# Second Deployment running the br-sta worker component. Upstream builds the +# manager (cmd/manager) and worker (cmd/worker) as two SEPARATE images, each +# with its own ENTRYPOINT already set to the right compiled binary (both are +# named /service inside their respective images by convention — see +# github.com/LerianStudio/br-sta's per-component Dockerfiles). Set +# worker.image to the dedicated worker image; do not point it at the manager +# image. It runs the background jobs: audit outbox publisher/consumer, the +# leader-gated scheduler (credential expiry/rotation, retention, +# stale-protocol cleanup), the credential-recovery-on-boot sweep, and the +# inbound BACEN poll loop. It serves ONLY the probe endpoints (WorkerMode — +# no business HTTP API), reuses the manager ConfigMap + Secret for all shared +# infra config, and layers the worker-only knobs below on top. +# ============================================================================== +worker: + # -- Enable the worker Deployment. REQUIRES worker.image to point at a + # dedicated worker image (built from cmd/worker) — keep this false until + # that image is confirmed available, then enable per-deployment. + enabled: false + # -- Replicas. The background jobs are leader-gated but the worker does NOT + # run its own leader election yet — keep this at 1 (and do not add an HPA) to + # avoid duplicate processing. + replicaCount: 1 + # -- Number of old ReplicaSets to retain for rollback + revisionHistoryLimit: 10 + # -- Annotations applied to the Deployment resource + annotations: {} + # -- Annotations applied to the pods + podAnnotations: {} + # -- Worker image. REQUIRED: point at the dedicated worker image (built + # from cmd/worker). Empty repository/tag fall back to the manager image, + # which does NOT run the worker's logic — only set for local testing. + image: + repository: "" + tag: "" + pullPolicy: IfNotPresent + # -- Image pull secrets (empty inherits the manager's) + imagePullSecrets: [] + # -- Container command. The worker image's own ENTRYPOINT already runs the + # correct binary (/service, built from cmd/worker) — this override matches + # that ENTRYPOINT rather than replacing it with a different binary name. + command: + - /service + # -- Termination grace period + terminationGracePeriodSeconds: 60 + # -- Pod security context + podSecurityContext: {} + # -- Container security context (empty inherits the manager's) + securityContext: {} + # -- ServiceAccount configuration + serviceAccount: + create: true + annotations: {} + name: "" + # -- Probe server port. WorkerMode binds SERVER_ADDRESS here; no Service or + # Ingress is created for the worker. + service: + port: 8081 + # -- Resource requests and limits + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + # -- Deployment strategy. Recreate by default: the worker is leader-gated but + # runs no leader election, so a RollingUpdate surge would briefly run two + # worker pods and duplicate background processing (audit drain, scheduler). + deploymentStrategy: + type: Recreate + # -- Readiness probe configuration + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 2 + # -- Liveness probe configuration + livenessProbe: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 3 + # -- Node selector for scheduling pods on specific nodes + nodeSelector: {} + # -- Tolerations for scheduling on tainted nodes + tolerations: [] + # -- Affinity rules for pod scheduling + affinity: {} + # -- Host aliases for custom DNS resolution inside the pod + hostAliases: [] + # -- Worker-only environment (layered on top of the manager ConfigMap/Secret). + # SetConfigFromEnvVars ignores envDefault, so every knob a background job + # needs must be set explicitly. SERVER_ADDRESS is derived from service.port. + # @default -- templates/worker/configmap.yaml + configmap: + # OTEL identity for the worker process + OTEL_RESOURCE_SERVICE_NAME: "br-sta-worker" + # Leader-gated scheduler (credential expiry/rotation, retention, cleanup) + SCHEDULER_ENABLED: "true" + # Resolve credentials left mid-rotation by a prior worker death, at boot + CREDENTIALS_RECOVERY_ON_BOOT: "true" + # Audit outbox publisher (drains audit_outbox -> sta.audit.events). + # Requires the shared RABBITMQ_ENABLED="true". + AUDIT_PUBLISHER_ENABLED: "true" + AUDIT_PUBLISHER_INTERVAL_SEC: "2" + AUDIT_PUBLISHER_BATCH_SIZE: "100" + AUDIT_PUBLISHER_MAX_ATTEMPTS: "10" + AUDIT_PUBLISHER_EXCHANGE: "sta.audit.events" + AUDIT_PUBLISHER_LOCK_CLASS_ID: "7700" + AUDIT_PUBLISHER_SERVICE_NAME: "br-sta-worker" + # Audit consumer (writes hash-chained rows into the audit log tables) + AUDIT_CONSUMER_ENABLED: "true" + AUDIT_CONSUMER_QUEUE: "br-sta.audit.consume" + AUDIT_CONSUMER_EXCHANGE: "sta.audit.events" + AUDIT_CONSUMER_DEDUP_TTL_SEC: "5" + AUDIT_CONSUMER_LOCK_CLASS_ID: "7701" + AUDIT_CONSUMER_MAX_RETRY_ATTEMPTS: "10" + AUDIT_CONSUMER_DLQ_EXCHANGE: "sta.audit.dlq" + AUDIT_CONSUMER_SERVICE_NAME: "br-sta-worker" + # -- Extra environment variables (map of key:value pairs) rendered as inline + # container env (wins over both ConfigMaps). + extraEnvVars: {} + +# ============================================================================== +# POSTGRESQL SUB-CHART (Bitnami) +# Default: enabled with replication for in-cluster development/staging. +# Production: set postgresql.enabled=false and configure +# global.externalPostgresDefinitions for an externally managed Postgres. +# ============================================================================== +postgresql: + enabled: true + external: false + global: + security: + # Bitnami moved free images to bitnamisecure / bitnamilegacy in 2025; + # the upstream Bitnami chart treats those repos as "non-standard" and + # blocks them unless this flag is set. Required for fresh installs. + allowInsecureImages: true + image: + repository: bitnamisecure/postgresql + tag: "latest" + architecture: replication + replication: + numSynchronousReplicas: 1 + auth: + enabled: true + enablePostgresUser: true + postgresPassword: "" + username: "br_sta" + password: "" + database: "br_sta" + replicationUsername: "replicator" + replicationPassword: "" + primary: + persistence: + size: 8Gi + resourcesPreset: "medium" + extendedConfiguration: | + shared_buffers = 512MB + max_wal_senders = 20 + wal_keep_size = 512MB + max_replication_slots = 20 + extraEnvVars: + - name: POSTGRESQL_WAL_LEVEL + value: "logical" + - name: POSTGRESQL_HOST_STANDBY + value: "on" + - name: POSTGRESQL_MAX_CONNECTIONS + value: "200" + readReplicas: + name: replication + replicaCount: 1 + persistence: + size: 8Gi + resourcesPreset: "medium" + extendedConfiguration: | + shared_buffers = 512MB + max_wal_senders = 20 + max_replication_slots = 20 + wal_keep_size = 512MB + extraEnvVars: + - name: POSTGRESQL_WAL_LEVEL + value: "logical" + - name: POSTGRESQL_HOST_STANDBY + value: "on" + - name: POSTGRESQL_MAX_CONNECTIONS + value: "200" + +# ============================================================================== +# VALKEY SUB-CHART (Bitnami, Redis-compatible) +# Default: enabled (standalone) for in-cluster development/staging. +# Provides caching + rate limiting for br-sta (REDIS_* env vars). +# Production: set valkey.enabled=false and point REDIS_HOST/REDIS_PASSWORD at +# an externally managed Redis/Valkey. +# ============================================================================== +valkey: + enabled: true + external: false + global: + security: + allowInsecureImages: true + image: + repository: bitnamisecure/valkey + tag: "latest" + architecture: standalone + auth: + enabled: true + password: "" + username: br_sta + primary: + kind: Deployment + resourcesPreset: "medium" + persistence: + enabled: false + extraFlags: + - "--maxmemory 640mb" + - "--maxmemory-policy allkeys-lru" + - "--hz 100" + - "--maxclients 10000" + +# ============================================================================== +# RABBITMQ SUB-CHART (groundhog2k) — OPTIONAL +# Disabled by default. Set rabbitmq.enabled=true to bundle an in-cluster broker +# for the event-driven starter. When enabled, also set app.configmap.RABBITMQ_ENABLED="true". +# The broker Service is "-rabbitmq" (AMQP :5672) — RABBITMQ_HOST is +# derived from it automatically in the ConfigMap unless overridden. +# Production: set rabbitmq.enabled=false and point RABBITMQ_HOST / RABBITMQ_DEFAULT_USER +# / app.secrets.RABBITMQ_DEFAULT_PASS at an externally managed broker. +# ============================================================================== +rabbitmq: + enabled: false + external: false + image: + tag: "3.13.6" + persistence: + size: 8Gi + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: "1" + memory: 1Gi + podSecurityContext: + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + fsGroupChangePolicy: "OnRootMismatch" + seccompProfile: {type: RuntimeDefault} + containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: {drop: ["ALL"]} + authentication: + user: + # Must match brSta.configmap.RABBITMQ_DEFAULT_USER. + value: "br_sta" + password: + # Operators MUST supply this AND the matching brSta.secrets.RABBITMQ_DEFAULT_PASS + # when rabbitmq.enabled=true. Both must be the same value. + value: "" + erlangCookie: + # Operators MUST supply a stable cookie (e.g. openssl rand -base64 32) when + # rabbitmq.enabled=true. It must not change across upgrades (breaks clustering). + value: "" + +# ============================================================================== +# OPTIONAL OTEL COLLECTOR INTEGRATION +# Setting otel-collector-lerian.enabled=true causes the deployment to inject +# HOST_IP/OTEL_EXPORTER_OTLP_ENDPOINT pointing at the host node's collector. +# ============================================================================== +otel-collector-lerian: + enabled: false