diff --git a/.github/configs/helm-render-values/br-slc.yaml b/.github/configs/helm-render-values/br-slc.yaml new file mode 100644 index 000000000..2003ab882 --- /dev/null +++ b/.github/configs/helm-render-values/br-slc.yaml @@ -0,0 +1,29 @@ +# CI-only render fixture for br-slc. Dummy values, NOT a production example and +# NOT real credentials. Exercises the external-infra path (Postgres/Redis are +# external in BYOC) with the app + signer Secrets and the migration Job all +# rendered in-release, so the render gate's dangling-secret-ref assertion has +# every referenced Secret present. +secrets: + create: true + data: + POSTGRES_PASSWORD: "dummy-ci-password" + REDIS_PASSWORD: "dummy-ci-redis" + +configmap: + data: + POSTGRES_HOST: "pg.example.svc" + REDIS_HOST: "redis.example.svc:6379" + PLUGIN_AUTH_HOST: "http://access-manager.example.svc:4000" + CORS_ALLOWED_ORIGINS: "https://app.example.com" + +signer: + secrets: + create: true + data: + SIGNER_SOFTKEY_PFX_PASSPHRASE: "dummy-ci-passphrase" + +migrations: + enabled: true + postgres: + host: "pg.example.svc" + password: "dummy-ci-password" diff --git a/.github/configs/helm-render-values/lender.yaml b/.github/configs/helm-render-values/lender.yaml new file mode 100644 index 000000000..eb452b8f3 --- /dev/null +++ b/.github/configs/helm-render-values/lender.yaml @@ -0,0 +1,13 @@ +# CI-only render fixture for the lender chart. NOT a production example. +# Enables BOTH components so the render gate exercises the console path, and +# supplies the required-when-enabled values. No real credentials. +lender: + secrets: + # lender.secrets.POSTGRES_PASSWORD is `required` when useExistingSecret=false. + POSTGRES_PASSWORD: "ci-dummy-not-a-real-secret" + +lenderConsole: + enabled: true + image: + # lenderConsole.image.tag is `required` when lenderConsole.enabled=true. + tag: "0.0.0-ci" diff --git a/.github/configs/labeler.yaml b/.github/configs/labeler.yaml index 069a6c84f..23725cb61 100644 --- a/.github/configs/labeler.yaml +++ b/.github/configs/labeler.yaml @@ -68,4 +68,12 @@ flowker: underwriter: - changed-files: - - any-glob-to-any-file: charts/underwriter/** \ No newline at end of file + - any-glob-to-any-file: charts/underwriter/** + +br-ccs: + - changed-files: + - any-glob-to-any-file: charts/br-ccs/** + +br-slc: + - changed-files: + - any-glob-to-any-file: charts/br-slc/** \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index cc608b212..8a821e43f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,6 +19,9 @@ - [ ] Matcher - [ ] Flowker - [ ] Underwriter +- [ ] BR STA +- [ ] BR CCS +- [ ] BR SLC ## Checklist Please check each item after it's completed. diff --git a/.github/workflows/gptchangelog.yml b/.github/workflows/gptchangelog.yml index 81f501e40..a1e60f36a 100644 --- a/.github/workflows/gptchangelog.yml +++ b/.github/workflows/gptchangelog.yml @@ -24,6 +24,7 @@ jobs: with: runner_type: "blacksmith-4vcpu-ubuntu-2404" filter_paths: |- + charts/br-sta charts/fetcher charts/lerian-notification charts/midaz diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 9d868fce4..03327610d 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -52,8 +52,12 @@ jobs: fetcher flowker underwriter + lender matcher tracer + br-sta + br-ccs + br-slc new charts common diff --git a/README.md b/README.md index 1a6d54a6c..6de0e1ecf 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Ledger Version | CRM Version | | :---: | :---: | :---: | -| `8.6.0` | 3.7.8 | 3.7.8 | +| `8.7.0-beta.1` | 3.7.8 | 3.7.8 | ----------------- ### Plugin Access Manager Helm Chart @@ -37,7 +37,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Auth Version | Identity Version | | :---: | :---: | :---: | -| `8.3.0` | 2.6.7 | 2.4.5 | +| `8.4.0-beta.3` | 2.6.7 | 2.4.5 | ----------------- ### Plugin Fees Helm Chart @@ -50,10 +50,23 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Fees Version | UI Version | | :---: | :---: | :---: | -| `7.2.0` | 3.3.0 | `3.0.0` | +| `7.3.0-beta.1` | 3.3.0 | `3.0.0` | ----------------- +### BR SPI Helm Chart + +Brazilian SFN — SPI / Pix rail (components: core, spi, brcode, dict, plus a dedicated schema-migration Job). + +For implementation and configuration details, see the [README](https://charts.lerian.studio/charts/br-spi). + +#### Application Version Mapping + +| Chart Version | App Version | +| :---: | :---: | +| `0.1.0-beta.1` | `0.1.0` | +----------------- + ### Reporter See the [official documentation](https://docs.lerian.studio/en/reporter) for details. @@ -64,7 +77,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Manager Version | Worker Version | | :---: | :---: | :---: | -| `3.1.1` | 2.1.2 | 2.1.2 | +| `3.2.0-beta.1` | 2.2.0 | 2.2.0 | ----------------- ### Plugin BR Bank Transfer @@ -77,7 +90,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | bankTransfer Version | | :---: | :---: | -| `1.3.0` | 1.0.0 | +| `1.4.0-beta.1` | 1.0.0 | ----------------- @@ -92,7 +105,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Pix Version | Job Version | | :---: | :---: | :---: | -| `3.0.0` | 1.2.1-beta.11 | 1.2.1-beta.12 | +| `3.1.0-beta.1` | 1.2.1-beta.11 | 1.2.1-beta.12 | ----------------- ### Plugin BR Pix Switch @@ -103,7 +116,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | App Version | | :---: | :---: | -| `2.0.0` | 1.0.0-beta.1 | +| `2.1.0-beta.4` | 1.0.0-beta.1 | ----------------- ### Plugin BR Pix Indirect BTG @@ -116,7 +129,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Pix Version | Inbound Version | Outbound Version | Reconciliation Version | | :---: | :---: | :---: | :---: | :---: | -| `3.4.0` | 1.7.6 | 1.7.6 | 1.7.6 | 1.7.6 | +| `3.5.0-beta.1` | 1.7.6 | 1.7.6 | 1.7.6 | 1.7.6 | ----------------- @@ -128,7 +141,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | App Version | | :---: | :---: | -| `1.1.0-beta.3` | 1.0.0-beta.9 | +| `1.1.0-beta.5` | 1.0.0-beta.9 | ----------------- ### Fetcher @@ -141,7 +154,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Manager Version | Worker Version | | :---: | :---: | :---: | -| `3.0.0` | 1.4.2 | 1.4.2 | +| `3.1.0-beta.1` | 2.0.2 | 2.0.2 | ----------------- ### Underwriter @@ -154,7 +167,20 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Underwriter Version | | :---: | :---: | -| `3.0.0` | 1.0.0 | +| `3.1.0-beta.1` | 1.0.0 | +----------------- + +### Lender + +For more details, check out the [official documentation](https://docs.lerian.studio/en/lender). + +For implementation and configuration details, see the [README](https://charts.lerian.studio/charts/lender). + +#### Application Version Mapping + +| Chart Version | Lender Version | +| :---: | :---: | +| `1.0.0-beta.1` | 1.0.0-beta.48 | ----------------- ### Matcher @@ -167,7 +193,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Matcher Version | | :---: | :---: | -| `3.0.0` | 1.0.0 | +| `3.1.0-beta.2` | 1.0.0 | ### Flowker @@ -179,7 +205,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Flowker Version | | :---: | :---: | -| `3.1.1` | 1.2.0-beta.82 | +| `3.2.0-beta.1` | 1.2.0-beta.82 | ----------------- ### Tracer @@ -192,7 +218,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Tracer Version | | :---: | :---: | -| `2.1.0` | 1.0.0 | +| `2.2.0-beta.1` | 1.0.0 | ----------------- ### Otel Collector Lerian @@ -203,7 +229,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Otel Version | | :---: | :---: | -| `4.1.0` | 0.142.0 | +| `4.2.0-beta.1` | 0.142.0 | ----------------- ### Product Console @@ -216,7 +242,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | Console Version | | :---: | :---: | -| `3.1.0` | 1.6.0 | +| `3.2.0-beta.1` | 1.6.0 | ----------------- ### Plugin BC Correios @@ -238,7 +264,7 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | App Version | | :---: | :---: | -| `2.2.0-beta.1` | 1.0.0 | +| `2.2.0-beta.2` | 1.0.0 | ----------------- ### Notifications @@ -260,5 +286,5 @@ For implementation and configuration details, see the [README](https://charts.le | Chart Version | App Version | Migrations Version | | :---: | :---: | :---: | -| `1.1.0` | `1.0.0-beta.109` | `1.0.0-beta.109` | +| `1.2.0-beta.1` | `1.0.0-beta.109` | `1.0.0-beta.109` | ----------------- diff --git a/charts/br-ccs/CHANGELOG.md b/charts/br-ccs/CHANGELOG.md new file mode 100644 index 000000000..fe681bb62 --- /dev/null +++ b/charts/br-ccs/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to the br-ccs Helm chart are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2026-07-07 + +### Added + +- Initial Helm chart for the br-ccs service (BACEN CCS regulatory integration). +- Single Deployment running `CCS_RUN_MODE=all` (HTTP API + background workers in one process). +- ClusterIP Service on port 4030 (`SERVER_ADDRESS=:4030`). +- Health probes wired to the verified application endpoints: liveness `GET /health`, + readiness `GET /readyz`. +- Full env-var coverage from `config/.env.example` split across ConfigMap (non-secret) + and Secret (credentials, crypto keys, object-storage keys, M2M API keys). +- PostgreSQL migrations Job using the dedicated `br-ccs-migrations` image + (golang-migrate runner; PreSync for external Postgres, PostSync for the bundled + subchart), plus a migration-only Secret hook for the external chart-managed path. +- Optional subchart dependencies: `postgresql`, `valkey`, `rabbitmq` (all `.enabled`-gated). +- External-infra bootstrap Jobs for PostgreSQL and RabbitMQ (opt-in via `global.*Definitions.enabled`). +- HPA, PodDisruptionBudget, Ingress, ServiceAccount templates. +- Non-root, read-only-root-filesystem, drop-ALL security contexts on all containers. diff --git a/charts/br-ccs/Chart.lock b/charts/br-ccs/Chart.lock new file mode 100644 index 000000000..9cfd83b26 --- /dev/null +++ b/charts/br-ccs/Chart.lock @@ -0,0 +1,12 @@ +dependencies: +- name: valkey + repository: oci://registry-1.docker.io/bitnamicharts + version: 2.4.7 +- name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 16.3.5 +- name: rabbitmq + repository: https://groundhog2k.github.io/helm-charts + version: 2.1.11 +digest: sha256:7bb3353ae1606ded8101b27d09edeeb9e82d7d8b627c74acb4ea6b9badb77c3c +generated: "2026-07-07T11:01:18.762917187-03:00" diff --git a/charts/br-ccs/Chart.yaml b/charts/br-ccs/Chart.yaml new file mode 100644 index 000000000..489b1748d --- /dev/null +++ b/charts/br-ccs/Chart.yaml @@ -0,0 +1,40 @@ +apiVersion: v2 +name: br-ccs-helm +description: A Helm chart for br-ccs — Lerian's BACEN CCS (Cadastro de Clientes + do Sistema Financeiro Nacional) regulatory integration service +type: application +annotations: + lerian.studio/chart-type: single-service +home: https://github.com/LerianStudio/helm +sources: + - https://github.com/LerianStudio/helm/tree/main/charts/br-ccs + - https://github.com/LerianStudio/br-ccs +maintainers: + - name: "Lerian Studio" + email: "support@lerian.studio" + +version: 1.0.0-beta.4 + +appVersion: "1.0.0" + +keywords: + - ccs + - bacen + - regulatory + - lerian + - ledger + - brazil +icon: https://avatars.githubusercontent.com/u/148895005?s=200&v=4 +dependencies: + - name: valkey + version: "2.4.7" + repository: "oci://registry-1.docker.io/bitnamicharts" + condition: valkey.enabled + - name: postgresql + version: "16.3.5" + repository: "https://charts.bitnami.com/bitnami" + condition: postgresql.enabled + - name: rabbitmq + version: "2.1.11" + repository: "https://groundhog2k.github.io/helm-charts" + condition: rabbitmq.enabled diff --git a/charts/br-ccs/README.md b/charts/br-ccs/README.md new file mode 100644 index 000000000..7422cd71e --- /dev/null +++ b/charts/br-ccs/README.md @@ -0,0 +1,93 @@ +# br-ccs Helm Chart + +Helm chart for **br-ccs** — Lerian's Go service for the BACEN **CCS (Cadastro de +Clientes do Sistema Financeiro Nacional)** regulatory integration. It ingests +customer-relationship data (from Fetcher), computes the daily delta, renders the +BACEN XML layouts, transmits via **STA**, and reconciles the responses, honoring +judicial secrecy (liminares / LC 105) and LGPD. + +- Chart name: `br-ccs-helm` +- Chart type: `single-service` +- Source: https://github.com/LerianStudio/br-ccs + +## Chart Contract + +- Chart type: `single-service` +- Required secrets: `brCcs.secrets.CCS_CRYPTO_MASTER_KEY` (AES-256-GCM, 64 hex chars — empty fails fast at boot). `POSTGRES_PASSWORD` and `REDIS_PASSWORD` are single-sourced from the bundled `-{postgresql,valkey}` Secrets via `secretKeyRef` when those subcharts are enabled; supply them (or `.auth.existingSecret`) for external infra. `FETCHER_CRYPTO_KEY` is optional (empty = passthrough). See `docs/helm-chart-standard.md`. +- Dependency notes: Bundled PostgreSQL and Valkey subcharts are used unless external services are configured; RabbitMQ is optional and disabled by default. No MongoDB. +- Production overrides: Disable the bundled subcharts (`.enabled=false`, `.external=true`), set `POSTGRES_HOST`/`REDIS_HOST`, and supply secrets via a secrets manager or `brCcs.useExistingSecret`; override image tags, ingress, resources, and integration endpoints (Fetcher/STA/Reporter). +- Source/license: Source is in `github.com/LerianStudio/helm` and `github.com/LerianStudio/br-ccs`; license is Apache-2.0. + +## TL;DR + +```bash +helm dependency build charts/br-ccs +helm install br-ccs charts/br-ccs \ + --set brCcs.configmap.ENV_NAME=production \ + --set brCcs.secrets.POSTGRES_PASSWORD=... \ + --set brCcs.secrets.REDIS_PASSWORD=... \ + --set brCcs.secrets.CCS_CRYPTO_MASTER_KEY=$(openssl rand -hex 32) +``` + +## Service topology + +| Aspect | Value | +|--------|-------| +| Container port / Service port | `4030` (`SERVER_ADDRESS=:4030`) | +| Service type | `ClusterIP` | +| Run mode | Single Deployment, `CCS_RUN_MODE=all` (API + workers) | +| Liveness probe | `GET /health` | +| Readiness probe | `GET /readyz` (PROJECT_RULES §13.4) | +| Persistence | PostgreSQL (no MongoDB) | +| Messaging | RabbitMQ (optional, disabled by default) | +| Cache / idempotency / rate-limit | Redis / Valkey | + +## Dependencies + +All bundled subcharts are `.enabled`-gated. For BYOC / production, disable them +and point the app at external infrastructure (see `values-template.yaml`). + +| Subchart | Version | Repository | +|----------|---------|------------| +| postgresql | 16.3.5 | https://charts.bitnami.com/bitnami | +| valkey | 2.4.7 | oci://registry-1.docker.io/bitnamicharts | +| rabbitmq | 2.1.11 | https://groundhog2k.github.io/helm-charts | + +## Configuration + +Non-secret configuration lives under `brCcs.configmap`; credentials and keys +under `brCcs.secrets`. Every variable from the application's +`config/.env.example` is mapped. Optional knobs not modeled explicitly can be +set via `brCcs.extraEnvVars`. + +### Required secrets + +| Key | Required | Notes | +|-----|----------|-------| +| `POSTGRES_PASSWORD` | Yes | Single-sourced from the bundled subchart Secret when `postgresql.enabled=true`; supply here for external Postgres. | +| `REDIS_PASSWORD` | Yes | Single-sourced from the bundled valkey Secret when `valkey.enabled=true`; supply here for external Redis. | +| `CCS_CRYPTO_MASTER_KEY` | Yes | AES-256-GCM master key, 64 hex chars (`openssl rand -hex 32`). Empty fails fast at boot. | +| `FETCHER_CRYPTO_KEY` | No | Optional. Empty = passthrough / plaintext (dev/staging); set to the Fetcher `APP_ENC_KEY` to decrypt snapshots. | + +### Migrations + +`brCcs.migrations.enabled=true` (default) runs the dedicated `br-ccs-migrations` +image as a Helm/ArgoCD hook: + +- **External Postgres** → PreSync (`pre-install,pre-upgrade`), backed by a + migration-only Secret carrying just `POSTGRES_PASSWORD`. +- **Bundled Postgres subchart** → PostSync (`post-install,post-upgrade`), reading + the app Secret after the database is provisioned. + +## Security + +All containers run non-root (`runAsUser: 1000`, `runAsNonRoot: true`), with +`readOnlyRootFilesystem: true`, `allowPrivilegeEscalation: false`, +`capabilities.drop: [ALL]`, and `seccompProfile: RuntimeDefault`. + +## Validation + +```bash +helm lint charts/br-ccs +helm template charts/br-ccs +``` diff --git a/charts/br-ccs/files/rabbitmq/load_definitions.json b/charts/br-ccs/files/rabbitmq/load_definitions.json new file mode 100644 index 000000000..6ce45fa59 --- /dev/null +++ b/charts/br-ccs/files/rabbitmq/load_definitions.json @@ -0,0 +1,35 @@ +{ + "users": [ + { + "name": "br-ccs", + "tags": "" + } + ], + "vhosts": [ + { + "name": "/" + } + ], + "permissions": [ + { + "user": "br-ccs", + "vhost": "/", + "configure": ".*", + "write": ".*", + "read": ".*" + } + ], + "queues": [], + "exchanges": [ + { + "name": "events", + "vhost": "/", + "type": "topic", + "durable": true, + "auto_delete": false, + "internal": false, + "arguments": {} + } + ], + "bindings": [] +} diff --git a/charts/br-ccs/templates/_helpers.tpl b/charts/br-ccs/templates/_helpers.tpl new file mode 100644 index 000000000..73b2c3dbc --- /dev/null +++ b/charts/br-ccs/templates/_helpers.tpl @@ -0,0 +1,153 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "br-ccs.name" -}} +{{- default (default "br-ccs" .Values.nameOverride) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name for br-ccs. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "br-ccs.fullname" -}} +{{- default (include "br-ccs.name" .) .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "br-ccs.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create br-ccs app version +*/}} +{{- define "br-ccs.defaultTag" -}} +{{- default .Chart.AppVersion .Values.brCcs.image.tag }} +{{- end -}} + +{{/* +Return valid br-ccs version label +*/}} +{{- define "br-ccs.versionLabelValue" -}} +{{ regexReplaceAll "[^-A-Za-z0-9_.]" (include "br-ccs.defaultTag" .) "-" | trunc 63 | trimAll "-" | trimAll "_" | trimAll "." | quote }} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "br-ccs.labels" -}} +helm.sh/chart: {{ include "br-ccs.chart" .context }} +{{ include "br-ccs.selectorLabels" (dict "context" .context "component" .component "name" .name) }} +app.kubernetes.io/version: {{ include "br-ccs.versionLabelValue" .context }} +app.kubernetes.io/managed-by: {{ .context.Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "br-ccs.selectorLabels" -}} +app.kubernetes.io/name: {{ include "br-ccs.name" .context }} +app.kubernetes.io/instance: {{ .context.Release.Name }} +{{- if .component }} +app.kubernetes.io/component: {{ .component }} +{{- end }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "br-ccs.serviceAccountName" -}} +{{- if .Values.brCcs.serviceAccount.create }} +{{- default (include "br-ccs.fullname" .) .Values.brCcs.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.brCcs.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Expand the namespace of the release. +Allows overriding it for multi-namespace deployments in combined charts. +*/}} +{{- define "global.namespace" -}} +{{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} +{{- end }} + +{{/* +Enable internal dependencies +These helpers check both .enabled and .external flags +*/}} +{{- define "rabbitmq.enabled" -}} +{{- if and (default true .Values.rabbitmq.enabled) (not .Values.rabbitmq.external) -}} +true +{{- else -}} +false +{{- end -}} +{{- end -}} + +{{- define "valkey.enabled" -}} +{{- if and (default true .Values.valkey.enabled) (not .Values.valkey.external) -}} +true +{{- else -}} +false +{{- end -}} +{{- end -}} + +{{- define "postgresql.enabled" -}} +{{- if and (default true .Values.postgresql.enabled) (not .Values.postgresql.external) -}} +true +{{- else -}} +false +{{- end -}} +{{- end -}} + +{{/* +br-ccs.infraSecretRef — emit a `- name: valueFrom: secretKeyRef: {name,key}` +entry pointing at a Bitnami subchart's generated Secret (or the operator's existingSecret +override). Inputs (dict): context (root .), subchart, key, envName. +*/}} +{{- define "br-ccs.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 }} + +{{/* +br-ccs.migrationPostgresPassword — POSTGRES_PASSWORD for the migration-only Secret. +migration-secret.yaml renders ONLY on the EXTERNAL Postgres path (the bundled subchart path +reads the subchart Secret via secretKeyRef instead), so the operator MUST supply the password. +*/}} +{{- define "br-ccs.migrationPostgresPassword" -}} +{{- $secrets := get (.Values.brCcs | default dict) "secrets" | default dict -}} +{{- required "brCcs.secrets.POSTGRES_PASSWORD is required when migrations run against external PostgreSQL with a chart-managed Secret" (get $secrets "POSTGRES_PASSWORD") -}} +{{- 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-ccs/templates/bootstrap-postgres.yaml b/charts/br-ccs/templates/bootstrap-postgres.yaml new file mode 100644 index 000000000..3a247b10a --- /dev/null +++ b/charts/br-ccs/templates/bootstrap-postgres.yaml @@ -0,0 +1,129 @@ +{{- if .Values.global.externalPostgresDefinitions.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "br-ccs.fullname" . }}-bootstrap-postgres + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" "bootstrap" "name" "postgres") | nindent 4 }} +spec: + ttlSecondsAfterFinished: 300 + completions: 1 + parallelism: 1 + backoffLimit: 3 + template: + spec: + restartPolicy: Never + initContainers: + - name: wait-for-dependencies + image: busybox:1.37 + 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 is not ready yet, waiting... (${ELAPSED}s/${TIMEOUT}s)"; + sleep 5; + ELAPSED=$((ELAPSED + 5)); + done; + echo "$DB_HOST:$DB_PORT is ready!"; + containers: + - name: psql + image: postgres:17 + 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_CCS + {{- if .Values.global.externalPostgresDefinitions.brCcsCredentials.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ .Values.global.externalPostgresDefinitions.brCcsCredentials.useExistingSecret.name | quote }} + key: DB_PASSWORD_BR_CCS + {{- else }} + value: {{ .Values.global.externalPostgresDefinitions.brCcsCredentials.password | quote }} + {{- end }} + - name: DB_DATABASE + value: postgres + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== br-ccs PostgreSQL Bootstrap ===" + echo "Host: $DB_HOST:$DB_PORT" + echo "" + + # The role/database name contains a hyphen, so it is always quoted in SQL. + 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='br-ccs'" | 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='br-ccs'" | grep -q 1; then + ROLE_EXISTS=1 + fi + + if [ "$DB_EXISTS" = "1" ] && [ "$ROLE_EXISTS" = "1" ]; then + echo "PostgreSQL bootstrap already complete (database and role 'br-ccs' exist). Skipping creation." + else + if [ "$ROLE_EXISTS" = "1" ]; then + echo "Role 'br-ccs' already exists. Skipping creation." + else + echo "Creating role 'br-ccs'..." + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -v pw="$DB_PASSWORD_BR_CCS" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "CREATE ROLE \"br-ccs\" LOGIN PASSWORD :'pw'" + fi + + if [ "$DB_EXISTS" = "1" ]; then + echo "Database 'br-ccs' already exists. Skipping creation." + else + echo "Creating database 'br-ccs'..." + 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 \"br-ccs\" OWNER \"br-ccs\"" + fi + fi + + 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 "ALTER USER \"br-ccs\" CREATEDB" || echo "Warning: Could not grant CREATEDB (may require superuser privileges)" + 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 \"br-ccs\" TO \"br-ccs\"" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "br-ccs" -c "GRANT ALL ON SCHEMA public TO \"br-ccs\"" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "br-ccs" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"br-ccs\"" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "br-ccs" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"br-ccs\"" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "br-ccs" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"br-ccs\"" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "br-ccs" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"br-ccs\"" + + echo "" + echo "=== br-ccs PostgreSQL Bootstrap completed successfully ===" +{{- end }} diff --git a/charts/br-ccs/templates/bootstrap-rabbitmq.yaml b/charts/br-ccs/templates/bootstrap-rabbitmq.yaml new file mode 100644 index 000000000..415c2bb26 --- /dev/null +++ b/charts/br-ccs/templates/bootstrap-rabbitmq.yaml @@ -0,0 +1,199 @@ +{{- if .Values.global.externalRabbitmqDefinitions.enabled }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "br-ccs.fullname" . }}-bootstrap-rabbitmq-definitions + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" "bootstrap" "name" "rabbitmq") | nindent 4 }} +data: + load_definitions.json: | +{{ .Files.Get "files/rabbitmq/load_definitions.json" | indent 4 }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "br-ccs.fullname" . }}-bootstrap-rabbitmq + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" "bootstrap" "name" "rabbitmq") | nindent 4 }} +spec: + ttlSecondsAfterFinished: 300 + completions: 1 + parallelism: 1 + backoffLimit: 3 + template: + spec: + restartPolicy: OnFailure + initContainers: + - name: wait-for-dependencies + image: busybox:1.37 + env: + - name: RABBITMQ_HOST + value: {{ .Values.global.externalRabbitmqDefinitions.connection.host | quote }} + - name: RABBITMQ_PORT + value: {{ .Values.global.externalRabbitmqDefinitions.connection.portAmqp | quote }} + command: + - /bin/sh + - -c + - > + TIMEOUT=300; + ELAPSED=0; + echo "Checking $RABBITMQ_HOST:$RABBITMQ_PORT..."; + while ! nc -z "$RABBITMQ_HOST" "$RABBITMQ_PORT"; do + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "Timeout waiting for $RABBITMQ_HOST:$RABBITMQ_PORT after ${TIMEOUT}s"; + exit 1; + fi; + echo "$RABBITMQ_HOST:$RABBITMQ_PORT is not ready yet, waiting... (${ELAPSED}s/${TIMEOUT}s)"; + sleep 5; + ELAPSED=$((ELAPSED + 5)); + done; + echo "$RABBITMQ_HOST:$RABBITMQ_PORT is ready!"; + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + containers: + - name: apply-definitions + image: ghcr.io/jqlang/jq:1.7.1 + command: + - sh + - -c + - | + set -e + + umask 077 + printf 'user = "%s:%s"\n' "$RABBITMQ_ADMIN_USER" "$RABBITMQ_ADMIN_PASS" > /tmp/curlrc + + CURL_OPTS="-sS" + if [ "$RABBITMQ_SKIP_TLS_VERIFY" = "true" ]; then + CURL_OPTS="$CURL_OPTS -k" + echo "Warning: TLS verification is disabled" + fi + + if [ "$RABBITMQ_PROTOCOL" = "https" ] && [ "$RABBITMQ_PORT" = "443" ]; then + BASE_URL="$RABBITMQ_PROTOCOL://$RABBITMQ_HOST" + elif [ "$RABBITMQ_PROTOCOL" = "http" ] && [ "$RABBITMQ_PORT" = "80" ]; then + BASE_URL="$RABBITMQ_PROTOCOL://$RABBITMQ_HOST" + else + BASE_URL="$RABBITMQ_PROTOCOL://$RABBITMQ_HOST:$RABBITMQ_PORT" + fi + + echo "=== br-ccs RabbitMQ Bootstrap ===" + echo "API URL: $BASE_URL" + echo "" + + echo "Checking if RabbitMQ user 'br-ccs' already exists..." + BR_CCS_EXISTS=$(curl $CURL_OPTS -K /tmp/curlrc \ + "$BASE_URL/api/users/br-ccs" 2>/dev/null || echo "not_found") + + SKIP_DEFINITIONS=false + if echo "$BR_CCS_EXISTS" | grep -q '"name":"br-ccs"'; then + echo "RabbitMQ user 'br-ccs' already exists. Checking exchanges..." + EXCHANGE_EXISTS=$(curl $CURL_OPTS -K /tmp/curlrc \ + "$BASE_URL/api/exchanges/%2F/events" 2>/dev/null || echo "not_found") + if echo "$EXCHANGE_EXISTS" | grep -q '"name":"events"'; then + echo "RabbitMQ definitions already applied (user and exchange exist). Skipping definitions import." + SKIP_DEFINITIONS=true + fi + fi + + if [ "$SKIP_DEFINITIONS" = "false" ]; then + echo "Applying RabbitMQ definitions from file..." + HTTP_CODE=$(curl $CURL_OPTS -o /tmp/response.txt -w "%{http_code}" \ + -K /tmp/curlrc \ + -H "content-type: application/json" \ + -X POST \ + --data-binary @/definitions/load_definitions.json \ + "$BASE_URL/api/definitions") + if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then + echo "Error applying definitions (HTTP $HTTP_CODE):" + cat /tmp/response.txt + exit 1 + fi + echo "Definitions applied successfully." + fi + + echo "Updating RabbitMQ user 'br-ccs' password..." + USER_JSON=$(jq -n --arg pw "$RABBITMQ_BR_CCS_PASS" '{"password": $pw, "tags": ""}') + HTTP_CODE=$(curl $CURL_OPTS -o /tmp/response.txt -w "%{http_code}" \ + -K /tmp/curlrc \ + -H "content-type: application/json" \ + -X PUT \ + --data "$USER_JSON" \ + "$BASE_URL/api/users/br-ccs") + if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then + echo "Error updating br-ccs user (HTTP $HTTP_CODE):" + cat /tmp/response.txt + exit 1 + fi + echo "User password updated successfully." + + echo "" + echo "=== br-ccs RabbitMQ Bootstrap completed successfully ===" + env: + - name: RABBITMQ_PROTOCOL + value: {{ .Values.global.externalRabbitmqDefinitions.connection.protocol | quote }} + - name: RABBITMQ_HOST + value: {{ .Values.global.externalRabbitmqDefinitions.connection.host | quote }} + - name: RABBITMQ_PORT + value: {{ .Values.global.externalRabbitmqDefinitions.connection.port | quote }} + - name: RABBITMQ_SKIP_TLS_VERIFY + value: {{ .Values.global.externalRabbitmqDefinitions.connection.skipTlsVerify | default false | quote }} + - name: RABBITMQ_BR_CCS_PASS + {{- if .Values.global.externalRabbitmqDefinitions.brCcsCredentials.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ .Values.global.externalRabbitmqDefinitions.brCcsCredentials.useExistingSecret.name | quote }} + key: RABBITMQ_BR_CCS_PASS + {{- else }} + value: {{ .Values.global.externalRabbitmqDefinitions.brCcsCredentials.password | quote }} + {{- end }} + - name: RABBITMQ_ADMIN_PASS + {{- if .Values.global.externalRabbitmqDefinitions.rabbitmqAdminLogin.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ .Values.global.externalRabbitmqDefinitions.rabbitmqAdminLogin.useExistingSecret.name | quote }} + key: RABBITMQ_ADMIN_PASS + {{- else }} + value: {{ .Values.global.externalRabbitmqDefinitions.rabbitmqAdminLogin.password | quote }} + {{- end }} + - name: RABBITMQ_ADMIN_USER + {{- if .Values.global.externalRabbitmqDefinitions.rabbitmqAdminLogin.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ .Values.global.externalRabbitmqDefinitions.rabbitmqAdminLogin.useExistingSecret.name | quote }} + key: RABBITMQ_ADMIN_USER + {{- else }} + value: {{ .Values.global.externalRabbitmqDefinitions.rabbitmqAdminLogin.username | quote }} + {{- end }} + securityContext: + runAsUser: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: definitions + mountPath: /definitions + - name: tmp + mountPath: /tmp + volumes: + - name: definitions + configMap: + name: {{ include "br-ccs.fullname" . }}-bootstrap-rabbitmq-definitions + items: + - key: load_definitions.json + path: load_definitions.json + - name: tmp + emptyDir: {} +{{- end }} diff --git a/charts/br-ccs/templates/configmap.yaml b/charts/br-ccs/templates/configmap.yaml new file mode 100644 index 000000000..886e9fc12 --- /dev/null +++ b/charts/br-ccs/templates/configmap.yaml @@ -0,0 +1,335 @@ +{{- if .Values.brCcs.enabled }} +{{- $cm := .Values.brCcs.configmap | default dict }} +{{- $multiTenantEnabled := eq (toString ($cm.MULTI_TENANT_ENABLED | default "false")) "true" }} +{{- $pg := .Values.postgresql | default dict }} +{{- $vk := .Values.valkey | default dict }} +{{- $pgInternal := and (ne (toString $pg.enabled) "false") (not $pg.external) }} +{{- $vkInternal := and (ne (toString $vk.enabled) "false") (not $vk.external) }} +{{- $namespace := include "global.namespace" . }} +kind: ConfigMap +apiVersion: v1 +metadata: + name: {{ include "br-ccs.fullname" . }} + namespace: {{ $namespace }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" .Values.brCcs.name "name" .Values.brCcs.name ) | nindent 4 }} +data: + # ===================================================================== + # APPLICATION IDENTITY + # ===================================================================== + APPLICATION_NAME: {{ $cm.APPLICATION_NAME | default "br-ccs" | quote }} + ENV_NAME: {{ $cm.ENV_NAME | default "production" | quote }} + VERSION: {{ $cm.VERSION | default .Chart.AppVersion | quote }} + LOG_LEVEL: {{ $cm.LOG_LEVEL | default "info" | quote }} + DEPLOYMENT_MODE: {{ $cm.DEPLOYMENT_MODE | default "byoc" | quote }} + CCS_RUN_MODE: {{ $cm.CCS_RUN_MODE | default "all" | quote }} + + # ===================================================================== + # HTTP SERVER + # SERVER_PORT is consumed by the in-container /healthcheck binary. + # ===================================================================== + SERVER_ADDRESS: {{ $cm.SERVER_ADDRESS | default ":4030" | quote }} + SERVER_PORT: {{ $cm.SERVER_PORT | default "4030" | quote }} + GRPC_PORT: {{ $cm.GRPC_PORT | default "7001" | quote }} + HTTP_BODY_LIMIT_BYTES: {{ $cm.HTTP_BODY_LIMIT_BYTES | default "104857600" | quote }} + {{- if $cm.SERVER_TLS_CERT_FILE }} + SERVER_TLS_CERT_FILE: {{ $cm.SERVER_TLS_CERT_FILE | quote }} + {{- end }} + {{- if $cm.SERVER_TLS_KEY_FILE }} + SERVER_TLS_KEY_FILE: {{ $cm.SERVER_TLS_KEY_FILE | quote }} + {{- end }} + TLS_TERMINATED_UPSTREAM: {{ $cm.TLS_TERMINATED_UPSTREAM | default "true" | quote }} + TRUSTED_PROXIES: {{ $cm.TRUSTED_PROXIES | default "" | quote }} + + # ===================================================================== + # CORS POLICY + # ===================================================================== + CORS_ALLOWED_ORIGINS: {{ $cm.CORS_ALLOWED_ORIGINS | default "" | quote }} + CORS_ALLOWED_METHODS: {{ $cm.CORS_ALLOWED_METHODS | default "GET,POST,PUT,PATCH,DELETE,OPTIONS" | quote }} + CORS_ALLOWED_HEADERS: {{ $cm.CORS_ALLOWED_HEADERS | default "Authorization,Content-Type,X-Tenant-ID,X-Idempotency,X-Request-ID" | quote }} + CORS_EXPOSE_HEADERS: {{ $cm.CORS_EXPOSE_HEADERS | default "" | quote }} + CORS_ALLOW_CREDENTIALS: {{ $cm.CORS_ALLOW_CREDENTIALS | default "false" | quote }} + + # ===================================================================== + # MULTI-TENANCY + # Toggle + DEFAULT_TENANT_ID always render. MT infra endpoints render + # only when MULTI_TENANT_ENABLED=true. + # ===================================================================== + DEFAULT_TENANT_ID: {{ $cm.DEFAULT_TENANT_ID | default "11111111-1111-1111-1111-111111111111" | quote }} + MULTI_TENANT_ENABLED: {{ $cm.MULTI_TENANT_ENABLED | default "false" | quote }} + {{- if $multiTenantEnabled }} + MULTI_TENANT_URL: {{ required "brCcs.configmap.MULTI_TENANT_URL is required when MULTI_TENANT_ENABLED=true" $cm.MULTI_TENANT_URL | quote }} + MULTI_TENANT_REDIS_HOST: {{ required "brCcs.configmap.MULTI_TENANT_REDIS_HOST is required when MULTI_TENANT_ENABLED=true" $cm.MULTI_TENANT_REDIS_HOST | quote }} + MULTI_TENANT_REDIS_PORT: {{ $cm.MULTI_TENANT_REDIS_PORT | default "6379" | quote }} + MULTI_TENANT_REDIS_TLS: {{ $cm.MULTI_TENANT_REDIS_TLS | default "false" | quote }} + MULTI_TENANT_MAX_TENANT_POOLS: {{ $cm.MULTI_TENANT_MAX_TENANT_POOLS | default "100" | quote }} + MULTI_TENANT_IDLE_TIMEOUT_SEC: {{ $cm.MULTI_TENANT_IDLE_TIMEOUT_SEC | default "300" | quote }} + MULTI_TENANT_TIMEOUT: {{ $cm.MULTI_TENANT_TIMEOUT | default "30" | quote }} + MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD: {{ $cm.MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD | default "5" | quote }} + MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC: {{ $cm.MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC | default "30" | quote }} + MULTI_TENANT_CACHE_TTL_SEC: {{ $cm.MULTI_TENANT_CACHE_TTL_SEC | default "120" | quote }} + MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC: {{ $cm.MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC | default "30" | quote }} + {{- end }} + + # ===================================================================== + # POSTGRESQL (primary + control-plane) + # POSTGRES_HOST defaults to the bundled subchart primary Service. + # ===================================================================== + {{- if $pgInternal }} + POSTGRES_HOST: {{ $cm.POSTGRES_HOST | default (printf "%s-postgresql-primary.%s.svc.cluster.local" .Release.Name $namespace) | quote }} + {{- else }} + POSTGRES_HOST: {{ required "brCcs.configmap.POSTGRES_HOST is required when the bundled postgresql subchart is disabled (external infra)" $cm.POSTGRES_HOST | quote }} + {{- end }} + POSTGRES_PORT: {{ $cm.POSTGRES_PORT | default "5432" | quote }} + POSTGRES_USER: {{ $cm.POSTGRES_USER | default "br-ccs" | quote }} + POSTGRES_NAME: {{ $cm.POSTGRES_NAME | default "br-ccs" | quote }} + POSTGRES_SSLMODE: {{ $cm.POSTGRES_SSLMODE | default "require" | quote }} + MIGRATIONS_PATH: {{ $cm.MIGRATIONS_PATH | default "migrations" | quote }} + SYSTEMPLANE_ENABLED: {{ $cm.SYSTEMPLANE_ENABLED | default "false" | quote }} + POSTGRES_MAX_OPEN_CONNS: {{ $cm.POSTGRES_MAX_OPEN_CONNS | default "25" | quote }} + POSTGRES_MAX_IDLE_CONNS: {{ $cm.POSTGRES_MAX_IDLE_CONNS | default "5" | quote }} + POSTGRES_CONN_MAX_LIFETIME_MINS: {{ $cm.POSTGRES_CONN_MAX_LIFETIME_MINS | default "30" | quote }} + POSTGRES_CONN_MAX_IDLE_TIME_MINS: {{ $cm.POSTGRES_CONN_MAX_IDLE_TIME_MINS | default "5" | quote }} + POSTGRES_CONNECT_TIMEOUT_SEC: {{ $cm.POSTGRES_CONNECT_TIMEOUT_SEC | default "10" | quote }} + INFRA_CONNECT_TIMEOUT_SEC: {{ $cm.INFRA_CONNECT_TIMEOUT_SEC | default "30" | quote }} + {{- if $cm.POSTGRES_REPLICA_HOST }} + POSTGRES_REPLICA_HOST: {{ $cm.POSTGRES_REPLICA_HOST | quote }} + POSTGRES_REPLICA_PORT: {{ $cm.POSTGRES_REPLICA_PORT | default "5432" | quote }} + POSTGRES_REPLICA_USER: {{ $cm.POSTGRES_REPLICA_USER | quote }} + POSTGRES_REPLICA_NAME: {{ $cm.POSTGRES_REPLICA_NAME | quote }} + POSTGRES_REPLICA_SSLMODE: {{ $cm.POSTGRES_REPLICA_SSLMODE | default "require" | quote }} + {{- end }} + + # ===================================================================== + # REDIS / VALKEY (app-level) + # REDIS_HOST is host:port; defaults to the bundled valkey primary Service. + # ===================================================================== + {{- if $vkInternal }} + REDIS_HOST: {{ $cm.REDIS_HOST | default (printf "%s-valkey-primary.%s.svc.cluster.local:6379" .Release.Name $namespace) | quote }} + {{- else }} + REDIS_HOST: {{ required "brCcs.configmap.REDIS_HOST is required when the bundled valkey subchart is disabled (external infra)" $cm.REDIS_HOST | quote }} + {{- end }} + REDIS_DB: {{ $cm.REDIS_DB | default "0" | quote }} + REDIS_PROTOCOL: {{ $cm.REDIS_PROTOCOL | default "3" | quote }} + REDIS_POOL_SIZE: {{ $cm.REDIS_POOL_SIZE | default "10" | quote }} + REDIS_MIN_IDLE_CONNS: {{ $cm.REDIS_MIN_IDLE_CONNS | default "2" | quote }} + REDIS_READ_TIMEOUT: {{ $cm.REDIS_READ_TIMEOUT | default "3" | quote }} + REDIS_WRITE_TIMEOUT: {{ $cm.REDIS_WRITE_TIMEOUT | default "3" | quote }} + REDIS_DIAL_TIMEOUT: {{ $cm.REDIS_DIAL_TIMEOUT | default "5" | quote }} + REDIS_POOL_TIMEOUT: {{ $cm.REDIS_POOL_TIMEOUT | default "2" | quote }} + REDIS_MAX_RETRIES: {{ $cm.REDIS_MAX_RETRIES | default "3" | quote }} + REDIS_MIN_RETRY_BACKOFF: {{ $cm.REDIS_MIN_RETRY_BACKOFF | default "8" | quote }} + REDIS_MAX_RETRY_BACKOFF: {{ $cm.REDIS_MAX_RETRY_BACKOFF | default "1" | quote }} + {{- if $cm.REDIS_TLS }} + REDIS_TLS: {{ $cm.REDIS_TLS | quote }} + {{- end }} + {{- if $cm.REDIS_MASTER_NAME }} + REDIS_MASTER_NAME: {{ $cm.REDIS_MASTER_NAME | quote }} + {{- end }} + + # ===================================================================== + # CIRCUIT BREAKER (prerequisite for object storage adapters) + # ===================================================================== + CIRCUIT_BREAKER_ENABLED: {{ $cm.CIRCUIT_BREAKER_ENABLED | default "true" | quote }} + + # ===================================================================== + # RABBITMQ (optional) + # ===================================================================== + RABBITMQ_ENABLED: {{ $cm.RABBITMQ_ENABLED | default "false" | quote }} + {{- if eq (toString ($cm.RABBITMQ_ENABLED | default "false")) "true" }} + {{- /* RABBITMQ_URL carries the password and is rendered as a Secret (see + templates/secrets.yaml), never as a ConfigMap value. */}} + RABBITMQ_HOST: {{ $cm.RABBITMQ_HOST | default (printf "%s-rabbitmq.%s.svc.cluster.local" .Release.Name $namespace) | quote }} + RABBITMQ_PORT_AMQP: {{ $cm.RABBITMQ_PORT_AMQP | default "5672" | quote }} + RABBITMQ_PORT_HOST: {{ $cm.RABBITMQ_PORT_HOST | default "15672" | quote }} + RABBITMQ_DEFAULT_USER: {{ $cm.RABBITMQ_DEFAULT_USER | default "br-ccs" | quote }} + RABBITMQ_VHOST: {{ $cm.RABBITMQ_VHOST | default "/" | quote }} + RABBITMQ_EXCHANGE: {{ $cm.RABBITMQ_EXCHANGE | default "events" | quote }} + RABBITMQ_REQUIRE_HEALTH_ALLOWED_HOSTS: {{ $cm.RABBITMQ_REQUIRE_HEALTH_ALLOWED_HOSTS | default "false" | 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_PUBLISHER_CONFIRM_TIMEOUT_MS: {{ $cm.RABBITMQ_PUBLISHER_CONFIRM_TIMEOUT_MS | default "5000" | 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_PUBLISHER_MAX_RECOVERIES: {{ $cm.RABBITMQ_PUBLISHER_MAX_RECOVERIES | default "10" | quote }} + CCS_STA_DLQ_TTL_SECONDS: {{ $cm.CCS_STA_DLQ_TTL_SECONDS | default "30" | quote }} + {{- if $cm.RABBITMQ_QUEUE }} + RABBITMQ_QUEUE: {{ $cm.RABBITMQ_QUEUE | quote }} + {{- end }} + {{- if $cm.RABBITMQ_HEALTH_CHECK_URL }} + RABBITMQ_HEALTH_CHECK_URL: {{ $cm.RABBITMQ_HEALTH_CHECK_URL | quote }} + {{- end }} + {{- if $cm.RABBITMQ_HEALTH_CHECK_ALLOWED_HOSTS }} + RABBITMQ_HEALTH_CHECK_ALLOWED_HOSTS: {{ $cm.RABBITMQ_HEALTH_CHECK_ALLOWED_HOSTS | quote }} + {{- end }} + {{- end }} + + # ===================================================================== + # OUTBOX + # ===================================================================== + OUTBOX_ENABLED: {{ $cm.OUTBOX_ENABLED | default "false" | quote }} + OUTBOX_TABLE_NAME: {{ $cm.OUTBOX_TABLE_NAME | default "outbox_events" | quote }} + OUTBOX_DISPATCH_INTERVAL_SEC: {{ $cm.OUTBOX_DISPATCH_INTERVAL_SEC | default "2" | quote }} + OUTBOX_BATCH_SIZE: {{ $cm.OUTBOX_BATCH_SIZE | default "50" | quote }} + OUTBOX_PUBLISH_MAX_ATTEMPTS: {{ $cm.OUTBOX_PUBLISH_MAX_ATTEMPTS | default "3" | quote }} + OUTBOX_PUBLISH_BACKOFF_MS: {{ $cm.OUTBOX_PUBLISH_BACKOFF_MS | default "200" | quote }} + OUTBOX_RETRY_WINDOW_SEC: {{ $cm.OUTBOX_RETRY_WINDOW_SEC | default "300" | quote }} + OUTBOX_MAX_DISPATCH_ATTEMPTS: {{ $cm.OUTBOX_MAX_DISPATCH_ATTEMPTS | default "10" | quote }} + OUTBOX_PROCESSING_TIMEOUT_SEC: {{ $cm.OUTBOX_PROCESSING_TIMEOUT_SEC | default "600" | quote }} + OUTBOX_MAX_FAILED_PER_BATCH: {{ $cm.OUTBOX_MAX_FAILED_PER_BATCH | default "25" | quote }} + OUTBOX_INCLUDE_TENANT_METRICS: {{ $cm.OUTBOX_INCLUDE_TENANT_METRICS | default "false" | quote }} + OUTBOX_ALLOW_EMPTY_TENANT: {{ $cm.OUTBOX_ALLOW_EMPTY_TENANT | default "true" | quote }} + {{- if $cm.OUTBOX_PRIORITY_EVENT_TYPES }} + OUTBOX_PRIORITY_EVENT_TYPES: {{ $cm.OUTBOX_PRIORITY_EVENT_TYPES | quote }} + {{- end }} + + # ===================================================================== + # FETCHER INTEGRATION + # ===================================================================== + FETCHER_ENABLED: {{ $cm.FETCHER_ENABLED | default "false" | quote }} + {{- if $cm.FETCHER_URL }} + FETCHER_URL: {{ $cm.FETCHER_URL | quote }} + {{- end }} + FETCHER_HTTP_TIMEOUT_SEC: {{ $cm.FETCHER_HTTP_TIMEOUT_SEC | default "30" | quote }} + CCS_OUTBOX_DISPATCH_INTERVAL_MS: {{ $cm.CCS_OUTBOX_DISPATCH_INTERVAL_MS | default "500" | quote }} + CCS_OUTBOX_BATCH_SIZE: {{ $cm.CCS_OUTBOX_BATCH_SIZE | default "50" | quote }} + CCS_OUTBOX_MAX_ATTEMPTS: {{ $cm.CCS_OUTBOX_MAX_ATTEMPTS | default "10" | quote }} + CCS_OUTBOX_DLQ_ROUTING_KEY: {{ $cm.CCS_OUTBOX_DLQ_ROUTING_KEY | default "ccs.outbox.dlq" | quote }} + + # ===================================================================== + # STA INTEGRATION + # ===================================================================== + STA_ENABLED: {{ $cm.STA_ENABLED | default "false" | quote }} + {{- if $cm.STA_BASE_URL }} + STA_BASE_URL: {{ $cm.STA_BASE_URL | quote }} + {{- end }} + STA_HTTP_TIMEOUT_SEC: {{ $cm.STA_HTTP_TIMEOUT_SEC | default "30" | quote }} + + # ===================================================================== + # REPORTER INTEGRATION (optional) + # ===================================================================== + REPORTER_ENABLED: {{ $cm.REPORTER_ENABLED | default "false" | quote }} + {{- if eq (toString ($cm.REPORTER_ENABLED | default "false")) "true" }} + REPORTER_URL: {{ required "brCcs.configmap.REPORTER_URL is required when REPORTER_ENABLED=true" $cm.REPORTER_URL | quote }} + REPORTER_HTTP_TIMEOUT_SEC: {{ $cm.REPORTER_HTTP_TIMEOUT_SEC | default "30" | quote }} + {{- end }} + + # ===================================================================== + # AUTHENTICATION (inbound) + # ===================================================================== + PLUGIN_AUTH_ENABLED: {{ $cm.PLUGIN_AUTH_ENABLED | default "false" | quote }} + {{- if $cm.PLUGIN_AUTH_HOST }} + PLUGIN_AUTH_HOST: {{ $cm.PLUGIN_AUTH_HOST | quote }} + {{- end }} + + # ===================================================================== + # SWAGGER + # ===================================================================== + SWAGGER_ENABLED: {{ $cm.SWAGGER_ENABLED | default "true" | quote }} + SWAGGER_TITLE: {{ $cm.SWAGGER_TITLE | default "br-ccs" | quote }} + SWAGGER_VERSION: {{ $cm.SWAGGER_VERSION | default .Chart.AppVersion | quote }} + SWAGGER_BASE_PATH: {{ $cm.SWAGGER_BASE_PATH | default "/" | quote }} + SWAGGER_LEFT_DELIM: {{ $cm.SWAGGER_LEFT_DELIM | default "{{" | quote }} + SWAGGER_RIGHT_DELIM: {{ $cm.SWAGGER_RIGHT_DELIM | default "}}" | quote }} + {{- if $cm.SWAGGER_DESCRIPTION }} + SWAGGER_DESCRIPTION: {{ $cm.SWAGGER_DESCRIPTION | quote }} + {{- end }} + + # ===================================================================== + # OPENTELEMETRY + # ===================================================================== + ENABLE_TELEMETRY: {{ $cm.ENABLE_TELEMETRY | default "false" | quote }} + OTEL_LIBRARY_NAME: {{ $cm.OTEL_LIBRARY_NAME | default "br-ccs" | quote }} + OTEL_RESOURCE_SERVICE_NAME: {{ $cm.OTEL_RESOURCE_SERVICE_NAME | default "br-ccs" | quote }} + OTEL_RESOURCE_SERVICE_VERSION: {{ $cm.OTEL_RESOURCE_SERVICE_VERSION | default (.Values.brCcs.image.tag | default .Chart.AppVersion) | quote }} + OTEL_EXPORTER_OTLP_ENDPOINT: {{ $cm.OTEL_EXPORTER_OTLP_ENDPOINT | default "" | quote }} + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: {{ $cm.OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT | default "production" | quote }} + + # ===================================================================== + # RATE LIMITING + # ===================================================================== + 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 }} + 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 }} + 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 }} + EXPORT_RATE_LIMIT_MAX: {{ $cm.EXPORT_RATE_LIMIT_MAX | default "60" | quote }} + EXPORT_RATE_LIMIT_WINDOW_SEC: {{ $cm.EXPORT_RATE_LIMIT_WINDOW_SEC | default "60" | quote }} + DISPATCH_RATE_LIMIT_MAX: {{ $cm.DISPATCH_RATE_LIMIT_MAX | default "30" | quote }} + DISPATCH_RATE_LIMIT_WINDOW_SEC: {{ $cm.DISPATCH_RATE_LIMIT_WINDOW_SEC | default "60" | quote }} + # Empty (default) keeps the safe fail-closed posture; "true" permits fail-open. + ALLOW_RATELIMIT_FAIL_OPEN: {{ $cm.ALLOW_RATELIMIT_FAIL_OPEN | default "" | quote }} + + # ===================================================================== + # OBSERVABILITY / IDEMPOTENCY / PAGINATION + # ===================================================================== + DB_METRICS_INTERVAL_SEC: {{ $cm.DB_METRICS_INTERVAL_SEC | default "15" | quote }} + IDEMPOTENCY_RETRY_WINDOW_SEC: {{ $cm.IDEMPOTENCY_RETRY_WINDOW_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 }} + + # ===================================================================== + # M2M CREDENTIALS (non-secret portion — target service / OAuth2 / scope) + # API keys live in the Secret; SaaS uses AWS Secrets Manager. + # ===================================================================== + M2M_CREDENTIAL_CACHE_TTL_SEC: {{ $cm.M2M_CREDENTIAL_CACHE_TTL_SEC | default "300" | quote }} + AWS_REGION: {{ $cm.AWS_REGION | default "us-east-1" | quote }} + {{- range $k := (list "FETCHER_M2M_TARGET_SERVICE" "FETCHER_OAUTH2_TOKEN_URL" "FETCHER_OAUTH2_SCOPE" "REPORTER_M2M_TARGET_SERVICE" "REPORTER_OAUTH2_TOKEN_URL" "REPORTER_OAUTH2_SCOPE" "STA_M2M_TARGET_SERVICE" "STA_OAUTH2_TOKEN_URL" "STA_OAUTH2_SCOPE") }} + {{- if index $cm $k }} + {{ $k }}: {{ index $cm $k | quote }} + {{- end }} + {{- end }} + + # ===================================================================== + # DETAIL RESPONSE TUNING (optional) + # ===================================================================== + {{- range $k := (list "CCS_DETAIL_MESSAGE_EMIT_ENABLED" "CCS_DETAIL_MESSAGE_VALIDATION_ENABLED" "CCS_BACEN_DESTINATARIO_CNPJ_BASE" "CCS_ACCS005_OVERFLOW_THRESHOLD" "CCS_ACCS005_SECRECY_SITUACAO") }} + {{- if index $cm $k }} + {{ $k }}: {{ index $cm $k | quote }} + {{- end }} + {{- end }} + + # ===================================================================== + # READINESS PROBE + GRACEFUL SHUTDOWN + # ===================================================================== + READYZ_PROBE_TIMEOUT_SEC: {{ $cm.READYZ_PROBE_TIMEOUT_SEC | default "5" | quote }} + READYZ_DEP_SLOW_THRESHOLD_MS: {{ $cm.READYZ_DEP_SLOW_THRESHOLD_MS | default "500" | quote }} + STARTUP_SELF_PROBE_MAX_DURATION_SEC: {{ $cm.STARTUP_SELF_PROBE_MAX_DURATION_SEC | default "120" | quote }} + STARTUP_SELF_PROBE_INITIAL_DELAY_MS: {{ $cm.STARTUP_SELF_PROBE_INITIAL_DELAY_MS | default "1000" | quote }} + STARTUP_SELF_PROBE_MAX_DELAY_MS: {{ $cm.STARTUP_SELF_PROBE_MAX_DELAY_MS | default "30000" | quote }} + SHUTDOWN_DRAIN_GRACE_PERIOD_SEC: {{ $cm.SHUTDOWN_DRAIN_GRACE_PERIOD_SEC | default "15" | quote }} + SHUTDOWN_TOTAL_TIMEOUT_SEC: {{ $cm.SHUTDOWN_TOTAL_TIMEOUT_SEC | default "30" | quote }} + + # ===================================================================== + # OBJECT STORAGE (non-secret — endpoints/region/bucket/flags) + # Access + secret keys live in the Secret. + # ===================================================================== + SEAWEEDFS_S3_PORT: {{ $cm.SEAWEEDFS_S3_PORT | default "8342" | quote }} + SEAWEEDFS_MASTER_PORT: {{ $cm.SEAWEEDFS_MASTER_PORT | default "9343" | quote }} + OBJECT_STORAGE_STA_ENDPOINT: {{ $cm.OBJECT_STORAGE_STA_ENDPOINT | default "" | quote }} + OBJECT_STORAGE_STA_REGION: {{ $cm.OBJECT_STORAGE_STA_REGION | default "us-east-1" | quote }} + OBJECT_STORAGE_STA_BUCKET: {{ $cm.OBJECT_STORAGE_STA_BUCKET | default "" | quote }} + OBJECT_STORAGE_STA_USE_PATH_STYLE: {{ $cm.OBJECT_STORAGE_STA_USE_PATH_STYLE | default "true" | quote }} + OBJECT_STORAGE_STA_DISABLE_SSL: {{ $cm.OBJECT_STORAGE_STA_DISABLE_SSL | default "true" | quote }} + OBJECT_STORAGE_CCS_ENDPOINT: {{ $cm.OBJECT_STORAGE_CCS_ENDPOINT | default "" | quote }} + OBJECT_STORAGE_CCS_REGION: {{ $cm.OBJECT_STORAGE_CCS_REGION | default "us-east-1" | quote }} + OBJECT_STORAGE_CCS_BUCKET: {{ $cm.OBJECT_STORAGE_CCS_BUCKET | default "" | quote }} + OBJECT_STORAGE_CCS_USE_PATH_STYLE: {{ $cm.OBJECT_STORAGE_CCS_USE_PATH_STYLE | default "true" | quote }} + OBJECT_STORAGE_CCS_DISABLE_SSL: {{ $cm.OBJECT_STORAGE_CCS_DISABLE_SSL | default "true" | quote }} + OBJECT_STORAGE_FETCHER_ENDPOINT: {{ $cm.OBJECT_STORAGE_FETCHER_ENDPOINT | default "" | quote }} + OBJECT_STORAGE_FETCHER_REGION: {{ $cm.OBJECT_STORAGE_FETCHER_REGION | default "us-east-1" | quote }} + OBJECT_STORAGE_FETCHER_BUCKET: {{ $cm.OBJECT_STORAGE_FETCHER_BUCKET | default "" | quote }} + OBJECT_STORAGE_FETCHER_USE_PATH_STYLE: {{ $cm.OBJECT_STORAGE_FETCHER_USE_PATH_STYLE | default "true" | quote }} + OBJECT_STORAGE_FETCHER_DISABLE_SSL: {{ $cm.OBJECT_STORAGE_FETCHER_DISABLE_SSL | default "true" | quote }} + CCS_OUTBOUND_RETENTION_DAYS: {{ $cm.CCS_OUTBOUND_RETENTION_DAYS | default "30" | quote }} + CCS_ACCS009_RETENTION_DAYS: {{ $cm.CCS_ACCS009_RETENTION_DAYS | default "30" | quote }} + + # ===================================================================== + # EXTRA OVERRIDES (escape hatch for optional knobs not modeled above) + # ===================================================================== + {{- range $key, $value := .Values.brCcs.extraEnvVars }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/br-ccs/templates/deployment.yaml b/charts/br-ccs/templates/deployment.yaml new file mode 100644 index 000000000..9db8e19b4 --- /dev/null +++ b/charts/br-ccs/templates/deployment.yaml @@ -0,0 +1,181 @@ +{{- if .Values.brCcs.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "br-ccs.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" .Values.brCcs.name "name" .Values.brCcs.name ) | nindent 4 }} +spec: + revisionHistoryLimit: {{ .Values.brCcs.revisionHistoryLimit | default 10 }} + strategy: + type: {{ .Values.brCcs.deploymentUpdate.type }} + {{- if eq .Values.brCcs.deploymentUpdate.type "RollingUpdate" }} + rollingUpdate: + maxSurge: {{ .Values.brCcs.deploymentUpdate.maxSurge }} + maxUnavailable: {{ .Values.brCcs.deploymentUpdate.maxUnavailable }} + {{- end }} + {{- if not .Values.brCcs.autoscaling.enabled }} + replicas: {{ .Values.brCcs.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "br-ccs.selectorLabels" (dict "context" . "name" .Values.brCcs.name ) | nindent 6 }} + template: + metadata: + {{- with .Values.brCcs.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" .Values.brCcs.name "name" .Values.brCcs.name ) | nindent 8 }} + spec: + {{- with .Values.brCcs.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "br-ccs.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.brCcs.podSecurityContext | nindent 8 }} + # br-ccs connects to PostgreSQL (control-plane) and Redis/Valkey at boot in + # both single-tenant and hybrid multi-tenant modes, so the dependency wait + # gate always runs. + initContainers: + - name: wait-for-dependencies + image: busybox:1.37 + envFrom: + - configMapRef: + name: {{ include "br-ccs.fullname" . }} + command: + - /bin/sh + - -c + - | + MAX_ATTEMPTS=60 + SLEEP_SECONDS=5 + # Total timeout: 60 * 5 = 300 seconds (5 minutes) + + wait_for_service() { + local HOST="$1" + local PORT="$2" + local ATTEMPTS=0 + echo "Checking $HOST:$PORT..." + while ! nc -z "$HOST" "$PORT"; do + ATTEMPTS=$((ATTEMPTS + 1)) + if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then + echo "Timeout waiting for $HOST:$PORT after $((MAX_ATTEMPTS * SLEEP_SECONDS)) seconds" + exit 1 + fi + echo "$HOST:$PORT is not ready yet, waiting... (attempt $ATTEMPTS/$MAX_ATTEMPTS)" + sleep $SLEEP_SECONDS + done + echo "$HOST:$PORT is ready!" + } + + # Wait for PostgreSQL + wait_for_service "$POSTGRES_HOST" "$POSTGRES_PORT" + + # Wait for Redis/Valkey (REDIS_HOST is host:port) + REDIS_SVC=$(echo "$REDIS_HOST" | cut -d: -f1) + REDIS_PORT_NUM=$(echo "$REDIS_HOST" | cut -d: -f2) + wait_for_service "$REDIS_SVC" "$REDIS_PORT_NUM" + + echo "All dependencies are ready!" + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + containers: + - name: {{ include "br-ccs.fullname" . }} + securityContext: + {{- toYaml .Values.brCcs.securityContext | nindent 12 }} + image: "{{ .Values.brCcs.image.repository }}:{{ .Values.brCcs.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.brCcs.image.pullPolicy }} + envFrom: + - secretRef: + name: {{ if .Values.brCcs.useExistingSecret }}{{ required "brCcs.existingSecretName is required when brCcs.useExistingSecret is true" .Values.brCcs.existingSecretName }}{{ else }}{{ include "br-ccs.fullname" . }}{{ end }} + - configMapRef: + name: {{ include "br-ccs.fullname" . }} + env: + {{- $secretName := ternary .Values.brCcs.existingSecretName (include "br-ccs.fullname" .) .Values.brCcs.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-ccs.infraSecretRef" (dict "context" $ "subchart" "postgresql" "key" "password" "envName" "POSTGRES_PASSWORD") | nindent 10 }} + {{- else if .Values.brCcs.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-ccs.infraSecretRef" (dict "context" $ "subchart" "valkey" "key" "valkey-password" "envName" "REDIS_PASSWORD") | nindent 10 }} + {{- else if .Values.brCcs.secrets.REDIS_PASSWORD }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: REDIS_PASSWORD + {{- end }} + {{- if eq (toString .Values.brCcs.configmap.ENABLE_TELEMETRY) "true" }} + - name: "HOST_IP" + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: "POD_IP" + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: "OTEL_EXPORTER_OTLP_ENDPOINT" + value: "$(HOST_IP):4317" + - name: "OTEL_RESOURCE_ATTRIBUTES" + value: "k8s.pod.ip=$(POD_IP)" + {{- end }} + ports: + - name: http + containerPort: 4030 + protocol: TCP + livenessProbe: + httpGet: + path: {{ .Values.brCcs.livenessProbe.path | default "/health" }} + port: http + initialDelaySeconds: {{ .Values.brCcs.livenessProbe.initialDelaySeconds | default 15 }} + periodSeconds: {{ .Values.brCcs.livenessProbe.periodSeconds | default 20 }} + timeoutSeconds: {{ .Values.brCcs.livenessProbe.timeoutSeconds | default 5 }} + successThreshold: {{ .Values.brCcs.livenessProbe.successThreshold | default 1 }} + failureThreshold: {{ .Values.brCcs.livenessProbe.failureThreshold | default 3 }} + readinessProbe: + httpGet: + path: {{ .Values.brCcs.readinessProbe.path | default "/readyz" }} + port: http + initialDelaySeconds: {{ .Values.brCcs.readinessProbe.initialDelaySeconds | default 5 }} + periodSeconds: {{ .Values.brCcs.readinessProbe.periodSeconds | default 10 }} + timeoutSeconds: {{ .Values.brCcs.readinessProbe.timeoutSeconds | default 5 }} + successThreshold: {{ .Values.brCcs.readinessProbe.successThreshold | default 1 }} + failureThreshold: {{ .Values.brCcs.readinessProbe.failureThreshold | default 3 }} + resources: + {{- toYaml .Values.brCcs.resources | nindent 12 }} + {{- with .Values.brCcs.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.brCcs.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.brCcs.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.brCcs.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/br-ccs/templates/hpa.yaml b/charts/br-ccs/templates/hpa.yaml new file mode 100644 index 000000000..998949990 --- /dev/null +++ b/charts/br-ccs/templates/hpa.yaml @@ -0,0 +1,33 @@ +{{- if and .Values.brCcs.enabled .Values.brCcs.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "br-ccs.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" .Values.brCcs.name "name" .Values.brCcs.name ) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "br-ccs.fullname" . }} + minReplicas: {{ .Values.brCcs.autoscaling.minReplicas }} + maxReplicas: {{ .Values.brCcs.autoscaling.maxReplicas }} + metrics: + {{- if .Values.brCcs.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.brCcs.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.brCcs.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.brCcs.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/br-ccs/templates/ingress.yaml b/charts/br-ccs/templates/ingress.yaml new file mode 100644 index 000000000..5575ffaf8 --- /dev/null +++ b/charts/br-ccs/templates/ingress.yaml @@ -0,0 +1,62 @@ +{{- if .Values.brCcs.ingress.enabled -}} +{{- $ingressName := include "br-ccs.fullname" . -}} +{{- $svcPort := .Values.brCcs.service.port -}} +{{- if and .Values.brCcs.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.brCcs.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.brCcs.ingress.annotations "kubernetes.io/ingress.class" .Values.brCcs.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: {{ $ingressName }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" .Values.brCcs.name "name" .Values.brCcs.name ) | nindent 4 }} + {{- with .Values.brCcs.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.brCcs.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.brCcs.ingress.className }} + {{- end }} + {{- if .Values.brCcs.ingress.tls }} + tls: + {{- range .Values.brCcs.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.brCcs.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: {{ $ingressName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $ingressName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/br-ccs/templates/migration-secret.yaml b/charts/br-ccs/templates/migration-secret.yaml new file mode 100644 index 000000000..ded00d6bc --- /dev/null +++ b/charts/br-ccs/templates/migration-secret.yaml @@ -0,0 +1,33 @@ +{{- $brCcs := .Values.brCcs | default dict }} +{{- $migrations := get $brCcs "migrations" | default dict }} +{{- $postgresql := .Values.postgresql | default dict }} +{{- $migrationsEnabled := and (eq (lower (toString (get $brCcs "enabled" | default false))) "true") (eq (lower (toString (get $migrations "enabled" | default false))) "true") }} +{{- $internalPg := and (eq (lower (toString (get $postgresql "enabled" | default false))) "true") (not (eq (lower (toString (get $postgresql "external" | default false))) "true")) }} +{{- $migUseExisting := eq (lower (toString (get $migrations "useExistingSecret" | default false))) "true" }} +{{- $appUseExisting := eq (lower (toString (get $brCcs "useExistingSecret" | default false))) "true" }} +{{- /* Minimal, migration-only Secret. Rendered ONLY for: migrations enabled, chart-managed + credential (no existing Secret), EXTERNAL Postgres. In that case the migration runs as a + pre-install/pre-upgrade hook BEFORE the normal application Secret exists, so it needs its + own credential available early. It carries ONLY POSTGRES_PASSWORD — the full application + Secret stays a normal (non-hook) resource so sensitive runtime keys are never orphaned. */}} +{{- if and $migrationsEnabled (not $internalPg) (not $migUseExisting) (not $appUseExisting) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ printf "%s-migrations" (include "br-ccs.fullname" .) | trunc 63 | trimSuffix "-" }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" "migrations" "name" (get $brCcs "name")) | nindent 4 }} + annotations: + # Pre-install/pre-upgrade hook (weight -5) so it exists before the migration Job + # (weight -1). Holds ONLY POSTGRES_PASSWORD. + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation + "argocd.argoproj.io/hook": PreSync + "argocd.argoproj.io/sync-wave": "-5" + "argocd.argoproj.io/hook-delete-policy": BeforeHookCreation +type: Opaque +stringData: + POSTGRES_PASSWORD: {{ include "br-ccs.migrationPostgresPassword" . | quote }} +{{- end }} diff --git a/charts/br-ccs/templates/migrations.yaml b/charts/br-ccs/templates/migrations.yaml new file mode 100644 index 000000000..734cd71ab --- /dev/null +++ b/charts/br-ccs/templates/migrations.yaml @@ -0,0 +1,166 @@ +{{- $brCcs := .Values.brCcs | default dict }} +{{- $migrations := get $brCcs "migrations" | default dict }} +{{- $configmap := get $brCcs "configmap" | default dict }} +{{- $postgresql := .Values.postgresql | default dict }} +{{- $migrationImage := get $migrations "image" | default dict }} +{{- $defaultMigrationResources := dict "requests" (dict "cpu" "50m" "memory" "64Mi") "limits" (dict "cpu" "250m" "memory" "256Mi") }} +{{- $migrationsEnabled := and (eq (lower (toString (get $brCcs "enabled" | default false))) "true") (eq (lower (toString (get $migrations "enabled" | default false))) "true") }} +{{- if $migrationsEnabled }} +{{- /* Internal (chart-managed) Postgres support. When postgresql.enabled=true and not + external, the database is provisioned during the Sync phase (bitnami), so the + migration runs as a POST hook (after the DB exists) with a wait-for-Postgres + initContainer. External/pre-existing Postgres keeps the PRE hook (DB already exists). */}} +{{- $internalPg := and (eq (lower (toString (get $postgresql "enabled" | default false))) "true") (not (eq (lower (toString (get $postgresql "external" | default false))) "true")) }} +{{- $appUseExisting := eq (lower (toString (get $brCcs "useExistingSecret" | default false))) "true" }} +{{- $migUseExisting := eq (lower (toString (get $migrations "useExistingSecret" | default false))) "true" }} +{{- $existingSecretName := "" }} +{{- if $migUseExisting }} +{{- $existingSecretName = required "brCcs.migrations.existingSecretName is required when brCcs.migrations.useExistingSecret=true" (get $migrations "existingSecretName") }} +{{- else if $appUseExisting }} +{{- $existingSecretName = required "brCcs.existingSecretName is required when brCcs.useExistingSecret=true" (get $brCcs "existingSecretName") }} +{{- else if $internalPg }} +{{- $existingSecretName = include "br-ccs.fullname" . }} +{{- else }} +{{- $existingSecretName = printf "%s-migrations" (include "br-ccs.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- $namespace := include "global.namespace" . }} +{{- $migrationTag := get $migrationImage "tag" | default "" }} +{{- $migrationDigest := get $migrationImage "digest" | default "" }} +{{- if and (empty $migrationTag) (empty $migrationDigest) }} +{{- $migrationTag = .Values.brCcs.image.tag | default .Chart.AppVersion }} +{{- end }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ printf "%s-migrations" (include "br-ccs.fullname" .) | trunc 63 | trimSuffix "-" }} + namespace: {{ $namespace }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" "migrations" "name" (get $brCcs "name") ) | nindent 4 }} + 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). + "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: + backoffLimit: {{ get $migrations "backoffLimit" | default 3 }} + activeDeadlineSeconds: {{ get $migrations "activeDeadlineSeconds" | default 600 }} + ttlSecondsAfterFinished: {{ get $migrations "ttlSecondsAfterFinished" | default 600 }} + template: + metadata: + labels: + {{- include "br-ccs.labels" (dict "context" . "component" "migrations" "name" (get $brCcs "name") ) | nindent 8 }} + {{- with (get $migrations "podAnnotations") }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + restartPolicy: Never + automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault + {{- with (get $brCcs "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: {{ get $configmap "POSTGRES_HOST" | 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: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + {{- end }} + containers: + - name: migrations + {{- $migrationRepository := get $migrationImage "repository" | default "ghcr.io/lerianstudio/br-ccs-migrations" }} + {{- if $migrationDigest }} + image: "{{ $migrationRepository }}@{{ $migrationDigest }}" + {{- else }} + image: "{{ $migrationRepository }}:{{ $migrationTag }}" + {{- end }} + imagePullPolicy: {{ get $migrationImage "pullPolicy" | default "IfNotPresent" }} + env: + - name: MIGRATIONS_PATH + value: {{ get $migrations "path" | default "/migrations" | quote }} + - name: POSTGRES_HOST + value: {{ get $configmap "POSTGRES_HOST" | default (printf "%s-postgresql-primary.%s.svc.cluster.local" .Release.Name $namespace) | quote }} + - name: POSTGRES_PORT + value: {{ get $configmap "POSTGRES_PORT" | default "5432" | quote }} + - name: POSTGRES_USER + value: {{ get $configmap "POSTGRES_USER" | default "br-ccs" | quote }} + - name: POSTGRES_NAME + value: {{ get $configmap "POSTGRES_NAME" | default "br-ccs" | quote }} + - name: POSTGRES_SSLMODE + value: {{ get $configmap "POSTGRES_SSLMODE" | default "require" | quote }} + - name: POSTGRES_CONNECT_TIMEOUT_SEC + value: {{ get $configmap "POSTGRES_CONNECT_TIMEOUT_SEC" | default "10" | quote }} + {{- if $internalPg }} + {{- include "br-ccs.infraSecretRef" (dict "context" $ "subchart" "postgresql" "key" "password" "envName" "POSTGRES_PASSWORD") | nindent 12 }} + {{- else }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $existingSecretName }} + key: POSTGRES_PASSWORD + {{- end }} + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + {{- toYaml (get $migrations "resources" | default $defaultMigrationResources) | nindent 12 }} + {{- with (get $brCcs "nodeSelector") }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with (get $brCcs "affinity") }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with (get $brCcs "tolerations") }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/br-ccs/templates/pdb.yaml b/charts/br-ccs/templates/pdb.yaml new file mode 100644 index 000000000..d8ba6c5e6 --- /dev/null +++ b/charts/br-ccs/templates/pdb.yaml @@ -0,0 +1,24 @@ +{{- if and .Values.brCcs.enabled .Values.brCcs.pdb.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "br-ccs.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" .Values.brCcs.name "name" .Values.brCcs.name ) | nindent 4 }} + {{- with .Values.brCcs.pdb.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} +spec: + {{- with .Values.brCcs.pdb.maxUnavailable }} + maxUnavailable: {{ . }} + {{- else }} + minAvailable: {{ .Values.brCcs.pdb.minAvailable | default 1 }} + {{- end }} + selector: + matchLabels: + {{- include "br-ccs.selectorLabels" (dict "context" . "component" .Values.brCcs.name "name" .Values.brCcs.name ) | nindent 6 }} +{{- end }} diff --git a/charts/br-ccs/templates/rabbitmq_load_definitions.yaml b/charts/br-ccs/templates/rabbitmq_load_definitions.yaml new file mode 100644 index 000000000..d992d181f --- /dev/null +++ b/charts/br-ccs/templates/rabbitmq_load_definitions.yaml @@ -0,0 +1,12 @@ +{{- if .Values.rabbitmq.enabled }} +kind: Secret +apiVersion: v1 +metadata: + name: {{ include "br-ccs.fullname" . }}-rabbitmq-load-definition + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" "rabbitmq" "name" "load-definition") | nindent 4 }} +data: + load_definition.json: | + {{ .Files.Get "files/rabbitmq/load_definitions.json" | b64enc | nindent 4 }} +{{- end }} diff --git a/charts/br-ccs/templates/secrets.yaml b/charts/br-ccs/templates/secrets.yaml new file mode 100644 index 000000000..9b0e20736 --- /dev/null +++ b/charts/br-ccs/templates/secrets.yaml @@ -0,0 +1,104 @@ +{{- if .Values.brCcs.enabled }} +{{- if not .Values.brCcs.useExistingSecret }} +{{- $s := .Values.brCcs.secrets | default dict }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "br-ccs.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" .Values.brCcs.name "name" .Values.brCcs.name ) | nindent 4 }} +type: Opaque +stringData: + # Infra passwords (PostgreSQL / Valkey) are single-sourced from the bundled + # Bitnami subchart Secrets and read via secretKeyRef on the deployment. They + # are written here ONLY for an EXTERNAL infra component without an + # .auth.existingSecret. + {{- $pg := .Values.postgresql | default dict }} + {{- $pgAuth := $pg.auth | default dict }} + {{- if and (not (and (ne (toString $pg.enabled) "false") (not $pg.external))) (not $pgAuth.existingSecret) $s.POSTGRES_PASSWORD }} + POSTGRES_PASSWORD: {{ $s.POSTGRES_PASSWORD | quote }} + {{- if $s.POSTGRES_REPLICA_PASSWORD }} + POSTGRES_REPLICA_PASSWORD: {{ $s.POSTGRES_REPLICA_PASSWORD | quote }} + {{- end }} + {{- end }} + + # Redis/Valkey password (external-only; bundled valkey is read from its subchart Secret) + {{- $vk := .Values.valkey | default dict }} + {{- $vkAuth := $vk.auth | default dict }} + {{- if and (not (and (ne (toString $vk.enabled) "false") (not $vk.external) $vkAuth.enabled)) (not $vkAuth.existingSecret) $s.REDIS_PASSWORD }} + REDIS_PASSWORD: {{ $s.REDIS_PASSWORD | quote }} + {{- end }} + {{- if $s.REDIS_CA_CERT }} + REDIS_CA_CERT: {{ $s.REDIS_CA_CERT | quote }} + {{- end }} + + # Multi-tenant (tenant-manager) secrets + {{- if $s.MULTI_TENANT_SERVICE_API_KEY }} + MULTI_TENANT_SERVICE_API_KEY: {{ $s.MULTI_TENANT_SERVICE_API_KEY | quote }} + {{- end }} + {{- if $s.MULTI_TENANT_REDIS_PASSWORD }} + MULTI_TENANT_REDIS_PASSWORD: {{ $s.MULTI_TENANT_REDIS_PASSWORD | quote }} + {{- end }} + {{- if $s.MULTI_TENANT_REDIS_CA_CERT }} + MULTI_TENANT_REDIS_CA_CERT: {{ $s.MULTI_TENANT_REDIS_CA_CERT | quote }} + {{- end }} + + # RabbitMQ credentials (when RABBITMQ_ENABLED=true) + {{- if $s.RABBITMQ_DEFAULT_PASS }} + RABBITMQ_DEFAULT_PASS: {{ $s.RABBITMQ_DEFAULT_PASS | quote }} + {{- end }} + # Full amqp:// dial string. Embeds the password, so it is a Secret (never a + # ConfigMap value). When set, the app dials via this instead of assembling the + # URL from RABBITMQ_HOST/PORT/USER/PASS. + {{- if $s.RABBITMQ_URL }} + RABBITMQ_URL: {{ $s.RABBITMQ_URL | quote }} + {{- end }} + + # Crypto keys (REQUIRED in production — empty CCS_CRYPTO_MASTER_KEY fails fast + # at boot by design; supply via a secrets manager or an existing Secret). + {{- if $s.CCS_CRYPTO_MASTER_KEY }} + CCS_CRYPTO_MASTER_KEY: {{ $s.CCS_CRYPTO_MASTER_KEY | quote }} + {{- end }} + {{- if $s.FETCHER_CRYPTO_KEY }} + FETCHER_CRYPTO_KEY: {{ $s.FETCHER_CRYPTO_KEY | quote }} + {{- end }} + + # License (lib-license-go). Read as env via the envFrom secretRef. LICENSE_KEY + # authorizes the app against the Lerian license gateway at boot (fail-closed); + # ORGANIZATION_IDS selects the enforcement scope (e.g. "global"). Held as a + # Secret (not a ConfigMap) so the key material is not exposed in plain + # manifests, matching the br-slc / br-sta / fetcher convention. + {{- if $s.LICENSE_KEY }} + LICENSE_KEY: {{ $s.LICENSE_KEY | quote }} + {{- end }} + {{- if $s.ORGANIZATION_IDS }} + ORGANIZATION_IDS: {{ $s.ORGANIZATION_IDS | quote }} + {{- end }} + + # Object storage credentials (per bucket) + {{- range $k := (list "OBJECT_STORAGE_STA_ACCESS_KEY" "OBJECT_STORAGE_STA_SECRET_KEY" "OBJECT_STORAGE_CCS_ACCESS_KEY" "OBJECT_STORAGE_CCS_SECRET_KEY" "OBJECT_STORAGE_FETCHER_ACCESS_KEY" "OBJECT_STORAGE_FETCHER_SECRET_KEY") }} + {{- if index $s $k }} + {{ $k }}: {{ index $s $k | quote }} + {{- end }} + {{- end }} + + # Per-upstream M2M API keys (on-prem / local fallback) + {{- range $k := (list "FETCHER_API_KEY" "REPORTER_API_KEY" "STA_API_KEY") }} + {{- if index $s $k }} + {{ $k }}: {{ index $s $k | quote }} + {{- end }} + {{- end }} + + # Per-upstream M2M OAuth2 client credentials (single-tenant). The client_id/client_secret + # pair authenticates br-ccs to each upstream (Fetcher / Reporter / STA) via the + # plugin-auth token endpoint. Multi-tenant (SaaS) resolves per-tenant credentials from + # AWS Secrets Manager instead and ignores these. Held as a Secret (never a ConfigMap) so + # the client secrets are not exposed in plain manifests, matching the *_API_KEY convention. + {{- range $k := (list "FETCHER_CLIENT_ID" "FETCHER_CLIENT_SECRET" "REPORTER_CLIENT_ID" "REPORTER_CLIENT_SECRET" "STA_CLIENT_ID" "STA_CLIENT_SECRET") }} + {{- if index $s $k }} + {{ $k }}: {{ index $s $k | quote }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/br-ccs/templates/service.yaml b/charts/br-ccs/templates/service.yaml new file mode 100644 index 000000000..366257905 --- /dev/null +++ b/charts/br-ccs/templates/service.yaml @@ -0,0 +1,22 @@ +{{- if .Values.brCcs.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "br-ccs.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" .Values.brCcs.name "name" .Values.brCcs.name ) | nindent 4 }} + {{- with .Values.brCcs.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.brCcs.service.type }} + ports: + - port: {{ .Values.brCcs.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "br-ccs.selectorLabels" (dict "context" . "name" .Values.brCcs.name) | nindent 4 }} +{{- end }} diff --git a/charts/br-ccs/templates/serviceaccount.yaml b/charts/br-ccs/templates/serviceaccount.yaml new file mode 100644 index 000000000..216c612a3 --- /dev/null +++ b/charts/br-ccs/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if and .Values.brCcs.enabled .Values.brCcs.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "br-ccs.serviceAccountName" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-ccs.labels" (dict "context" . "component" .Values.brCcs.name "name" .Values.brCcs.name ) | nindent 4 }} + {{- with .Values.brCcs.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/br-ccs/values-template.yaml b/charts/br-ccs/values-template.yaml new file mode 100644 index 000000000..83efc8371 --- /dev/null +++ b/charts/br-ccs/values-template.yaml @@ -0,0 +1,182 @@ +# Template values for br-ccs deployment. +# Copy this file and fill in the required values for your environment. +# REQUIRED fields must be set before deployment. + +brCcs: + name: br-ccs + enabled: true + replicaCount: 2 + + ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: "" # Set your hostname (e.g., br-ccs.example.com) + paths: + - path: / + pathType: Prefix + tls: [] + migrations: + # -- Enable or disable the migrations job + enabled: true + configmap: + # Chart scope: infrastructure connection + multi-tenant toggle + integration + # master switches + object-storage endpoints. Operational tuning (timeouts, + # retries, rate limits, poll cadence, detail-message flags) ships with + # lib-commons / systemplane defaults or via brCcs.extraEnvVars. + + # ================================================================= + # Application identity + # ================================================================= + ENV_NAME: "" # REQUIRED - Environment name (e.g., production, staging) + DEPLOYMENT_MODE: "byoc" # local | byoc | saas (saas enforces TLS on deps) + CCS_RUN_MODE: "all" # manager | worker | all + SERVER_ADDRESS: ":4030" + SERVER_PORT: "4030" + + # ================================================================= + # TLS / proxy — TRUSTED_PROXIES REQUIRED when ENV_NAME=production + # ================================================================= + TLS_TERMINATED_UPSTREAM: "true" + TRUSTED_PROXIES: "" # e.g. 10.0.0.0/8,172.16.0.0/12 + + # ================================================================= + # Multi-tenancy + # ================================================================= + DEFAULT_TENANT_ID: "11111111-1111-1111-1111-111111111111" + MULTI_TENANT_ENABLED: "false" + # When MULTI_TENANT_ENABLED=true, also set: + # MULTI_TENANT_URL: "" + # MULTI_TENANT_REDIS_HOST: "" + + # ================================================================= + # PostgreSQL primary + # POSTGRES_HOST is REQUIRED when the bundled postgresql subchart is + # disabled or external; leave empty to use the bundled subchart Service. + # ================================================================= + POSTGRES_HOST: "" # REQUIRED for external infra (postgresql disabled/external) + POSTGRES_PORT: "5432" + POSTGRES_USER: "br-ccs" + POSTGRES_NAME: "br-ccs" + POSTGRES_SSLMODE: "require" + + # ================================================================= + # Redis/Valkey (app-level) + # REDIS_HOST is REQUIRED when the bundled valkey subchart is disabled + # or external; leave empty to use the bundled subchart Service. + # ================================================================= + REDIS_HOST: "" # REQUIRED for external infra (valkey disabled/external); host:port + + # ================================================================= + # Circuit breaker (prerequisite for object storage) + # ================================================================= + CIRCUIT_BREAKER_ENABLED: "true" + + # ================================================================= + # Integrations (master switches; URLs required when enabled) + # ================================================================= + FETCHER_ENABLED: "false" + # FETCHER_URL: "" + STA_ENABLED: "false" + # STA_BASE_URL: "" + REPORTER_ENABLED: "false" + # REPORTER_URL: "" + + # ================================================================= + # Authentication (inbound) + # ================================================================= + PLUGIN_AUTH_ENABLED: "false" + # PLUGIN_AUTH_HOST: "" # REQUIRED when PLUGIN_AUTH_ENABLED=true + + # ================================================================= + # OpenTelemetry + # ================================================================= + ENABLE_TELEMETRY: "false" + OTEL_EXPORTER_OTLP_ENDPOINT: "" # Set if ENABLE_TELEMETRY=true + + # ================================================================= + # Object storage (per bucket; empty *_BUCKET marks readyz dep skipped) + # ================================================================= + OBJECT_STORAGE_STA_ENDPOINT: "" + OBJECT_STORAGE_STA_BUCKET: "" + OBJECT_STORAGE_CCS_ENDPOINT: "" + OBJECT_STORAGE_CCS_BUCKET: "" + OBJECT_STORAGE_FETCHER_ENDPOINT: "" + OBJECT_STORAGE_FETCHER_BUCKET: "" + + secrets: + # PostgreSQL - REQUIRED + POSTGRES_PASSWORD: "" # REQUIRED - PostgreSQL primary password + + # Redis/Valkey - REQUIRED + REDIS_PASSWORD: "" # REQUIRED - Redis/Valkey password + + # Crypto keys - REQUIRED in production (empty CCS_CRYPTO_MASTER_KEY fails fast at boot) + CCS_CRYPTO_MASTER_KEY: "" # AES-256-GCM master key, 64 hex chars (openssl rand -hex 32) + FETCHER_CRYPTO_KEY: "" # Fetcher snapshot decryption key (base64; = Fetcher APP_ENC_KEY) + + # License (lib-license-go; validated at boot, fail-closed) + # LICENSE_KEY: "" # Lerian license key + # ORGANIZATION_IDS: "" # enforcement scope (e.g. "global") + + # Object storage credentials (set alongside the matching *_BUCKET) + # OBJECT_STORAGE_STA_ACCESS_KEY: "" + # OBJECT_STORAGE_STA_SECRET_KEY: "" + # OBJECT_STORAGE_CCS_ACCESS_KEY: "" + # OBJECT_STORAGE_CCS_SECRET_KEY: "" + # OBJECT_STORAGE_FETCHER_ACCESS_KEY: "" + # OBJECT_STORAGE_FETCHER_SECRET_KEY: "" + + # Per-upstream M2M API keys (on-prem / local literal-Bearer fallback) + # FETCHER_API_KEY: "" + # REPORTER_API_KEY: "" + # STA_API_KEY: "" + + # Per-upstream M2M OAuth2 client credentials (single-tenant; via plugin-auth token endpoint) + # FETCHER_CLIENT_ID: "" + # FETCHER_CLIENT_SECRET: "" + # REPORTER_CLIENT_ID: "" + # REPORTER_CLIENT_SECRET: "" + # STA_CLIENT_ID: "" + # STA_CLIENT_SECRET: "" + + # Multi-tenant (tenant-manager) + # MULTI_TENANT_SERVICE_API_KEY: "" + + # RabbitMQ (when RABBITMQ_ENABLED=true) + # RABBITMQ_DEFAULT_PASS: "" + + useExistingSecret: false + existingSecretName: "" + +# ============================================================================== +# INFRASTRUCTURE DEPENDENCIES +# ============================================================================== +# br-ccs is PostgreSQL-only (no MongoDB): +# +# PostgreSQL: +# - Database: br-ccs +# - User: br-ccs (full privileges on the br-ccs database) +# - Schema: public (default). Multi-tenant mode uses schema-per-tenant via +# search_path; there is no tenant_id column. +# +# Redis/Valkey: +# - Used for: rate limiting, idempotency keys, caching. No pre-config needed. +# +# RabbitMQ (OPTIONAL - event bus, disabled by default): +# - Exchange: events (topic, durable) +# ============================================================================== + +# Disable internal dependencies for external infrastructure +valkey: + enabled: false + external: true + +postgresql: + enabled: false + external: true + +rabbitmq: + enabled: false + external: true diff --git a/charts/br-ccs/values.schema.json b/charts/br-ccs/values.schema.json new file mode 100644 index 000000000..0c788732b --- /dev/null +++ b/charts/br-ccs/values.schema.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true, + "properties": { + "nameOverride": { + "type": "string" + }, + "fullnameOverride": { + "type": "string" + }, + "namespaceOverride": { + "type": "string" + }, + "global": { + "type": "object", + "additionalProperties": true + }, + "brCcs": { + "type": "object", + "properties": { + "configmap": { + "type": "object" + }, + "secrets": { + "type": "object" + } + }, + "additionalProperties": true + }, + "valkey": { + "type": "object", + "additionalProperties": true + }, + "postgresql": { + "type": "object", + "additionalProperties": true + }, + "rabbitmq": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/charts/br-ccs/values.yaml b/charts/br-ccs/values.yaml new file mode 100644 index 000000000..6f6e125a0 --- /dev/null +++ b/charts/br-ccs/values.yaml @@ -0,0 +1,705 @@ +# Default values for br-ccs. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +# +# Env-var coverage note: every variable in the application's config/.env.example +# is mapped either into brCcs.configmap (non-secret) or brCcs.secrets +# (credentials / keys), or is provided by a bundled subchart. Operational tuning +# knobs that ship with lib-commons / systemplane defaults are exposed as +# optional entries and can be set under brCcs.configmap as needed. +nameOverride: "br-ccs" +fullnameOverride: "" +namespaceOverride: "" +global: + # -- Bootstrap job for external PostgreSQL: creates databases, roles, and grants privileges + externalPostgresDefinitions: + # -- Enable or disable the PostgreSQL bootstrap job + enabled: false + # -- PostgreSQL connection settings + connection: + # -- PostgreSQL host + host: "br-ccs-postgresql-primary" + # -- PostgreSQL port + port: "5432" + # -- Admin credentials for PostgreSQL + 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-ccs role created by the job + brCcsCredentials: + useExistingSecret: + # -- Name of existing secret containing DB_PASSWORD_BR_CCS key + name: "" + # -- Password for the br-ccs role (ignored if useExistingSecret.name is set) + password: "" + # -- Bootstrap job for external RabbitMQ: creates users, vhosts, and permissions + externalRabbitmqDefinitions: + # -- Enable or disable the RabbitMQ bootstrap job + enabled: false + # -- RabbitMQ connection settings + connection: + # -- RabbitMQ protocol (http or https) + protocol: "http" + # -- RabbitMQ host (management API endpoint) + host: "br-ccs-rabbitmq" + # -- RabbitMQ HTTP management port + port: "15672" + # -- RabbitMQ AMQP port (for connectivity check) + portAmqp: "5672" + # -- Skip TLS verification for self-signed certificates (not recommended for production) + skipTlsVerify: false + # -- Admin credentials for RabbitMQ management API + rabbitmqAdminLogin: + useExistingSecret: + # -- Name of existing secret containing RABBITMQ_ADMIN_USER and RABBITMQ_ADMIN_PASS keys + name: "" + # -- Admin username (ignored if useExistingSecret.name is set) + username: "admin" + # -- Admin password (ignored if useExistingSecret.name is set) + password: "" + # -- Credentials for the br-ccs user created by the job + brCcsCredentials: + useExistingSecret: + # -- Name of existing secret containing RABBITMQ_BR_CCS_PASS key + name: "" + # -- Password for the br-ccs user (ignored if useExistingSecret.name is set) + password: "" +brCcs: + # -- Readiness probe configuration. All fields override chart defaults. + readinessProbe: {} + # -- Liveness probe configuration. All fields override chart defaults. + livenessProbe: {} + # -- Service name + name: br-ccs + # -- Enable or disable the br-ccs service + enabled: true + # -- Number of replicas for the br-ccs service + replicaCount: 2 + # -- Number of old ReplicaSets to retain for deployment rollback + revisionHistoryLimit: 10 + image: + # -- Repository for the br-ccs service container image + repository: ghcr.io/lerianstudio/br-ccs + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Image tag used for deployment + tag: "1.0.0" + # -- PostgreSQL migrations job (Helm hook; ArgoCD PreSync for external PG, + # PostSync for the bundled subchart). Runs the dedicated br-ccs-migrations + # image (golang-migrate runner; the app image never migrates). The + # chart-managed Secret is rendered as an earlier hook so it exists before the + # migration hook — migrations run against the chart-managed Secret with NO + # pre-existing Secret required. Set useExistingSecret only to read + # POSTGRES_PASSWORD from an operator-provisioned Secret instead. + migrations: + # -- Enable or disable the migrations job. + enabled: true + # -- Optional. When true, migrations read POSTGRES_PASSWORD from a pre-existing + # Secret (existingSecretName) instead of the chart-managed Secret hook. + useExistingSecret: false + # -- Name of the pre-existing Secret containing POSTGRES_PASSWORD + # (only used when useExistingSecret=true). + existingSecretName: "" + # -- MIGRATIONS_PATH inside the migrations image (embedded at /migrations). + path: "/migrations" + image: + # -- Repository for the migrations runner image + repository: ghcr.io/lerianstudio/br-ccs-migrations + # -- Tag for the migrations image. Defaults to the app image tag + # (brCcs.image.tag, or the chart appVersion) when left empty. + tag: "" + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Maximum number of retries before the Job is considered failed + backoffLimit: 3 + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi + # -- Secrets for pulling images from a private registry + imagePullSecrets: [] + # -- Overrides the default generated name by Helm + nameOverride: "" + # -- Overrides the full name generated by Helm + fullnameOverride: "" + # -- Pod annotations for additional metadata + podAnnotations: {} + podSecurityContext: {} + # fsGroup: 2000 + + securityContext: + # -- Defines the group ID for the user running the process inside the container + runAsGroup: 1000 + # -- Defines the user ID for the process running inside the container + runAsUser: 1000 + # -- Ensures the process does not run as root + runAsNonRoot: true + capabilities: + drop: + - ALL + allowPrivilegeEscalation: false + # -- Defines the root filesystem as read-only + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault + # -- PodDisruptionBudget configuration + pdb: + # -- Enable or disable PodDisruptionBudget + enabled: true + # -- Minimum number of available pods + minAvailable: 1 + # -- Maximum number of unavailable pods + maxUnavailable: 1 + # -- Annotations for the PodDisruptionBudget + annotations: {} + # -- Deployment update strategy + deploymentUpdate: + # -- Type of deployment strategy + type: RollingUpdate + # -- Maximum number of pods that can be created over the desired number of pods + maxSurge: 100% + # -- Maximum number of pods that can be unavailable during the update + maxUnavailable: 0 + service: + # -- Kubernetes service type + type: ClusterIP + # -- Port for the HTTP API + port: 4030 + annotations: {} + ingress: + # -- Enable or disable ingress + enabled: false + # -- Ingress class name + className: "" + # -- Additional ingress annotations + annotations: {} + hosts: + - host: "" + paths: + - path: / + pathType: Prefix + # -- TLS configuration for ingress + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + resources: + # -- CPU and memory limits for pods + limits: + cpu: 1500m + memory: 512Mi + # -- Minimum CPU and memory requests + requests: + cpu: 500m + memory: 256Mi + autoscaling: + # -- Enable or disable horizontal pod autoscaling + enabled: true + # -- Minimum number of replicas + minReplicas: 2 + # -- Maximum number of replicas + maxReplicas: 5 + # -- Target CPU utilization percentage for autoscaling + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + # -- 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: [] + # -- ConfigMap for environment variables and configurations + # @default -- templates/configmap.yaml + configmap: + # ================================================================= + # Application identity + # ================================================================= + APPLICATION_NAME: "br-ccs" + ENV_NAME: "production" + VERSION: "1.0.0" + LOG_LEVEL: "info" + # DEPLOYMENT_MODE: local | byoc | saas. saas enforces TLS on all dependencies. + DEPLOYMENT_MODE: "byoc" + # CCS_RUN_MODE: manager | worker | all. "all" runs the HTTP API and the + # background workers in a single process (chart default: one deployment). + CCS_RUN_MODE: "all" + SERVER_ADDRESS: ":4030" + # SERVER_PORT is consumed by the in-container /healthcheck binary; keep it + # aligned with the port half of SERVER_ADDRESS. + SERVER_PORT: "4030" + GRPC_PORT: "7001" + HTTP_BODY_LIMIT_BYTES: "104857600" + + # ================================================================= + # CORS policy + # In production the validator rejects wildcard "*" and localhost origins. + # Override CORS_ALLOWED_ORIGINS with the public ingress hostname allowlist. + # ================================================================= + CORS_ALLOWED_ORIGINS: "" + CORS_ALLOWED_METHODS: "GET,POST,PUT,PATCH,DELETE,OPTIONS" + CORS_ALLOWED_HEADERS: "Authorization,Content-Type,X-Tenant-ID,X-Idempotency,X-Request-ID" + CORS_EXPOSE_HEADERS: "" + CORS_ALLOW_CREDENTIALS: "false" + + # ================================================================= + # TLS / proxy + # TRUSTED_PROXIES is REQUIRED when ENV_NAME=production (VPC + ingress CIDR; + # never 0.0.0.0/0). Fiber honours X-Forwarded-For only from these ranges. + # ================================================================= + TLS_TERMINATED_UPSTREAM: "true" + TRUSTED_PROXIES: "" + # SERVER_TLS_CERT_FILE: "" + # SERVER_TLS_KEY_FILE: "" + + # ================================================================= + # Single-tenant defaults + # ================================================================= + DEFAULT_TENANT_ID: "11111111-1111-1111-1111-111111111111" + + # ================================================================= + # Multi-tenancy (tenant-manager starter) + # Toggle + DEFAULT_TENANT_ID always render. MT infra endpoints only + # render when MULTI_TENANT_ENABLED=true. + # ================================================================= + MULTI_TENANT_ENABLED: "false" + # MULTI_TENANT_URL: "" + # MULTI_TENANT_REDIS_HOST: "" + MULTI_TENANT_REDIS_PORT: "6379" + MULTI_TENANT_REDIS_TLS: "false" + MULTI_TENANT_MAX_TENANT_POOLS: "100" + MULTI_TENANT_IDLE_TIMEOUT_SEC: "300" + MULTI_TENANT_TIMEOUT: "30" + MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD: "5" + MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC: "30" + MULTI_TENANT_CACHE_TTL_SEC: "120" + MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC: "30" + + # ================================================================= + # PostgreSQL (primary) + # POSTGRES_HOST defaults to "-postgresql-primary..svc.cluster.local" + # ================================================================= + POSTGRES_PORT: "5432" + POSTGRES_USER: "br-ccs" + POSTGRES_NAME: "br-ccs" + POSTGRES_SSLMODE: "require" + MIGRATIONS_PATH: "migrations" + SYSTEMPLANE_ENABLED: "false" + POSTGRES_MAX_OPEN_CONNS: "25" + POSTGRES_MAX_IDLE_CONNS: "5" + POSTGRES_CONN_MAX_LIFETIME_MINS: "30" + POSTGRES_CONN_MAX_IDLE_TIME_MINS: "5" + POSTGRES_CONNECT_TIMEOUT_SEC: "10" + INFRA_CONNECT_TIMEOUT_SEC: "30" + # PostgreSQL replica (optional — CQRS read scaling) + # POSTGRES_REPLICA_HOST: "" + # POSTGRES_REPLICA_PORT: "5432" + # POSTGRES_REPLICA_USER: "" + # POSTGRES_REPLICA_NAME: "" + # POSTGRES_REPLICA_SSLMODE: "require" + + # ================================================================= + # Redis / Valkey (app-level) + # REDIS_HOST defaults to "-valkey-primary..svc.cluster.local:6379" + # ================================================================= + REDIS_DB: "0" + REDIS_PROTOCOL: "3" + REDIS_POOL_SIZE: "10" + REDIS_MIN_IDLE_CONNS: "2" + REDIS_READ_TIMEOUT: "3" + REDIS_WRITE_TIMEOUT: "3" + REDIS_DIAL_TIMEOUT: "5" + REDIS_POOL_TIMEOUT: "2" + REDIS_MAX_RETRIES: "3" + REDIS_MIN_RETRY_BACKOFF: "8" + REDIS_MAX_RETRY_BACKOFF: "1" + # REDIS_TLS: "false" + # REDIS_MASTER_NAME: "" + + # ================================================================= + # Event-Driven — Circuit Breaker (prerequisite for object storage) + # ================================================================= + CIRCUIT_BREAKER_ENABLED: "true" + + # ================================================================= + # Event-Driven — RabbitMQ (optional, disabled by default) + # RABBITMQ_URL (full amqp:// dial string, carries credentials) is a SECRET — + # see brCcs.secrets.RABBITMQ_URL. When unset, the app builds the URL from + # RABBITMQ_HOST/PORT/USER/PASS. + # ================================================================= + RABBITMQ_ENABLED: "false" + RABBITMQ_PORT_AMQP: "5672" + RABBITMQ_PORT_HOST: "15672" + RABBITMQ_DEFAULT_USER: "br-ccs" + RABBITMQ_VHOST: "/" + RABBITMQ_EXCHANGE: "events" + RABBITMQ_REQUIRE_HEALTH_ALLOWED_HOSTS: "false" + RABBITMQ_ALLOW_INSECURE_HEALTH_CHECK: "false" + RABBITMQ_ALLOW_INSECURE_TLS: "false" + RABBITMQ_PUBLISHER_CONFIRM_TIMEOUT_MS: "5000" + RABBITMQ_PUBLISHER_RECOVERY_INITIAL_MS: "1000" + RABBITMQ_PUBLISHER_RECOVERY_MAX_MS: "30000" + RABBITMQ_PUBLISHER_MAX_RECOVERIES: "10" + CCS_STA_DLQ_TTL_SECONDS: "30" + # RABBITMQ_QUEUE: "br-ccs.events" + # RABBITMQ_HEALTH_CHECK_URL: "" + # RABBITMQ_HEALTH_CHECK_ALLOWED_HOSTS: "" + + # ================================================================= + # Event-Driven — Outbox + # ================================================================= + OUTBOX_ENABLED: "false" + OUTBOX_TABLE_NAME: "outbox_events" + OUTBOX_DISPATCH_INTERVAL_SEC: "2" + OUTBOX_BATCH_SIZE: "50" + OUTBOX_PUBLISH_MAX_ATTEMPTS: "3" + OUTBOX_PUBLISH_BACKOFF_MS: "200" + OUTBOX_RETRY_WINDOW_SEC: "300" + OUTBOX_MAX_DISPATCH_ATTEMPTS: "10" + OUTBOX_PROCESSING_TIMEOUT_SEC: "600" + OUTBOX_MAX_FAILED_PER_BATCH: "25" + OUTBOX_INCLUDE_TENANT_METRICS: "false" + OUTBOX_ALLOW_EMPTY_TENANT: "true" + # OUTBOX_PRIORITY_EVENT_TYPES: "" + + # ================================================================= + # Fetcher integration (HTTP client + AMQP completion consumer) + # Master switch — when false the service boots without the Fetcher subsystem. + # ================================================================= + FETCHER_ENABLED: "false" + # FETCHER_URL: "" + FETCHER_HTTP_TIMEOUT_SEC: "30" + CCS_OUTBOX_DISPATCH_INTERVAL_MS: "500" + CCS_OUTBOX_BATCH_SIZE: "50" + CCS_OUTBOX_MAX_ATTEMPTS: "10" + CCS_OUTBOX_DLQ_ROUTING_KEY: "ccs.outbox.dlq" + + # ================================================================= + # STA integration (HTTP client + outbox StaTransferSubmit handler) + # Master switch — independent from FETCHER_ENABLED. + # ================================================================= + STA_ENABLED: "false" + # STA_BASE_URL: "" + STA_HTTP_TIMEOUT_SEC: "30" + + # ================================================================= + # Reporter integration (optional async render — HTTP outbox consumer) + # Master switch; REPORTER_URL is required only when REPORTER_ENABLED=true. + # ================================================================= + REPORTER_ENABLED: "false" + # REPORTER_URL: "" + # REPORTER_HTTP_TIMEOUT_SEC: "30" + + # ================================================================= + # Authentication (lib-auth + plugin-auth) + # PLUGIN_AUTH_HOST is required when PLUGIN_AUTH_ENABLED=true. + # ================================================================= + PLUGIN_AUTH_ENABLED: "false" + # PLUGIN_AUTH_HOST: "" + + # ================================================================= + # API Documentation (Swagger) + # ================================================================= + SWAGGER_ENABLED: "true" + SWAGGER_TITLE: "br-ccs" + SWAGGER_VERSION: "1.0.0" + SWAGGER_BASE_PATH: "/" + SWAGGER_LEFT_DELIM: "{{" + SWAGGER_RIGHT_DELIM: "}}" + # SWAGGER_DESCRIPTION: "" + + # ================================================================= + # OpenTelemetry + # ================================================================= + ENABLE_TELEMETRY: "false" + OTEL_LIBRARY_NAME: "br-ccs" + OTEL_RESOURCE_SERVICE_NAME: "br-ccs" + OTEL_RESOURCE_SERVICE_VERSION: "1.0.0" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "production" + + # ================================================================= + # Rate limiting + # ================================================================= + RATE_LIMIT_ENABLED: "true" + RATE_LIMIT_MAX: "500" + RATE_LIMIT_WINDOW_SEC: "60" + AGGRESSIVE_RATE_LIMIT_MAX: "100" + AGGRESSIVE_RATE_LIMIT_WINDOW_SEC: "60" + RELAXED_RATE_LIMIT_MAX: "1000" + RELAXED_RATE_LIMIT_WINDOW_SEC: "60" + EXPORT_RATE_LIMIT_MAX: "60" + EXPORT_RATE_LIMIT_WINDOW_SEC: "60" + DISPATCH_RATE_LIMIT_MAX: "30" + DISPATCH_RATE_LIMIT_WINDOW_SEC: "60" + # Security posture: leave empty to keep the safe fail-closed default. Set to + # "true" only to permit fail-open (the limiter passes traffic through when + # Redis is unreachable). To disable enforcement entirely, use + # RATE_LIMIT_ENABLED=false above — ALLOW_RATELIMIT_DISABLED was removed in + # lib-commons v5.8.0 and is no longer read. + ALLOW_RATELIMIT_FAIL_OPEN: "" + + # ================================================================= + # Observability & metrics + # ================================================================= + DB_METRICS_INTERVAL_SEC: "15" + + # ================================================================= + # Idempotency + # ================================================================= + IDEMPOTENCY_RETRY_WINDOW_SEC: "300" + + # ================================================================= + # M2M credentials (per-upstream — target service / OAuth2 URL / scope). + # The *_API_KEY on-prem fallbacks live in brCcs.secrets. AWS Secrets + # Manager paths are used in SaaS mode. + # ================================================================= + M2M_CREDENTIAL_CACHE_TTL_SEC: "300" + AWS_REGION: "us-east-1" + # FETCHER_M2M_TARGET_SERVICE: "fetcher" + # FETCHER_OAUTH2_TOKEN_URL: "" + # FETCHER_OAUTH2_SCOPE: "" + # REPORTER_M2M_TARGET_SERVICE: "reporter" + # REPORTER_OAUTH2_TOKEN_URL: "" + # REPORTER_OAUTH2_SCOPE: "" + # STA_M2M_TARGET_SERVICE: "sta" + # STA_OAUTH2_TOKEN_URL: "" + # STA_OAUTH2_SCOPE: "" + + # ================================================================= + # Detail response tuning (all optional — BACEN-inferred defaults apply) + # ================================================================= + # CCS_DETAIL_MESSAGE_EMIT_ENABLED: "false" + # CCS_DETAIL_MESSAGE_VALIDATION_ENABLED: "false" + # CCS_BACEN_DESTINATARIO_CNPJ_BASE: "00000000" + # CCS_ACCS005_OVERFLOW_THRESHOLD: "30720" + # CCS_ACCS005_SECRECY_SITUACAO: "01" + + # ================================================================= + # Pagination + # ================================================================= + MAX_PAGINATION_LIMIT: "100" + MAX_PAGINATION_MONTH_DATE_RANGE: "3" + + # ================================================================= + # Readiness probe (canonical /readyz handler — PROJECT_RULES §13.4) + # ================================================================= + READYZ_PROBE_TIMEOUT_SEC: "5" + READYZ_DEP_SLOW_THRESHOLD_MS: "500" + STARTUP_SELF_PROBE_MAX_DURATION_SEC: "120" + STARTUP_SELF_PROBE_INITIAL_DELAY_MS: "1000" + STARTUP_SELF_PROBE_MAX_DELAY_MS: "30000" + + # ================================================================= + # Graceful shutdown (PROJECT_RULES §13) + # ================================================================= + SHUTDOWN_DRAIN_GRACE_PERIOD_SEC: "15" + SHUTDOWN_TOTAL_TIMEOUT_SEC: "30" + + # ================================================================= + # Object storage — three distinct S3-compatible buckets. + # Endpoints/region/bucket/flags are non-secret; access + secret keys + # live in brCcs.secrets. Empty *_BUCKET marks that /readyz dep skipped. + # ================================================================= + SEAWEEDFS_S3_PORT: "8342" + SEAWEEDFS_MASTER_PORT: "9343" + # STA inbound (read-only) + OBJECT_STORAGE_STA_ENDPOINT: "" + OBJECT_STORAGE_STA_REGION: "us-east-1" + OBJECT_STORAGE_STA_BUCKET: "sta-files" + OBJECT_STORAGE_STA_USE_PATH_STYLE: "true" + OBJECT_STORAGE_STA_DISABLE_SSL: "true" + # CCS outbound (write) + OBJECT_STORAGE_CCS_ENDPOINT: "" + OBJECT_STORAGE_CCS_REGION: "us-east-1" + OBJECT_STORAGE_CCS_BUCKET: "lerian-ccs" + OBJECT_STORAGE_CCS_USE_PATH_STYLE: "true" + OBJECT_STORAGE_CCS_DISABLE_SSL: "true" + # Fetcher snapshot (read-only) + OBJECT_STORAGE_FETCHER_ENDPOINT: "" + OBJECT_STORAGE_FETCHER_REGION: "us-east-1" + OBJECT_STORAGE_FETCHER_BUCKET: "lerian-fetcher" + OBJECT_STORAGE_FETCHER_USE_PATH_STYLE: "true" + OBJECT_STORAGE_FETCHER_DISABLE_SSL: "true" + # Retention windows + CCS_OUTBOUND_RETENTION_DAYS: "30" + CCS_ACCS009_RETENTION_DAYS: "30" + + # -- Secrets for storing sensitive data. Provide real values via an existing + # Secret (useExistingSecret) or a secrets manager — NEVER commit real values. + # @default -- templates/secrets.yaml + secrets: + # PostgreSQL credentials + POSTGRES_PASSWORD: "" + # POSTGRES_REPLICA_PASSWORD: "" + + # Redis / Valkey password + REDIS_PASSWORD: "" + # REDIS_CA_CERT: "" + + # Multi-tenant (tenant-manager) secrets + # MULTI_TENANT_SERVICE_API_KEY: "" + # MULTI_TENANT_REDIS_PASSWORD: "" + # MULTI_TENANT_REDIS_CA_CERT: "" + + # RabbitMQ credentials (when RABBITMQ_ENABLED=true) + # RABBITMQ_DEFAULT_PASS: "" + # RABBITMQ_URL — full amqp:// dial string (embeds the password; any '@' in + # the password must be percent-encoded as %40). When set, the app dials via + # this instead of assembling from RABBITMQ_HOST/PORT/USER/PASS. + # RABBITMQ_URL: "" + + # Crypto keys (REQUIRED in production) + # CCS_CRYPTO_MASTER_KEY — AES-256-GCM master key, 64 hex chars (openssl rand -hex 32). + # FETCHER_CRYPTO_KEY — Fetcher snapshot decryption key (base64; same as Fetcher APP_ENC_KEY). + CCS_CRYPTO_MASTER_KEY: "" + FETCHER_CRYPTO_KEY: "" + + # License (lib-license-go) — validated at boot against the Lerian license + # gateway (fail-closed). ORGANIZATION_IDS selects enforcement scope (e.g. + # "global"). Held as a Secret; wire via a secrets manager or existing Secret. + # LICENSE_KEY: "" + # ORGANIZATION_IDS: "" + + # Object storage credentials (per bucket) + OBJECT_STORAGE_STA_ACCESS_KEY: "" + OBJECT_STORAGE_STA_SECRET_KEY: "" + OBJECT_STORAGE_CCS_ACCESS_KEY: "" + OBJECT_STORAGE_CCS_SECRET_KEY: "" + OBJECT_STORAGE_FETCHER_ACCESS_KEY: "" + OBJECT_STORAGE_FETCHER_SECRET_KEY: "" + + # Per-upstream M2M API keys (on-prem / local literal-Bearer fallback) + # FETCHER_API_KEY: "" + # REPORTER_API_KEY: "" + # STA_API_KEY: "" + + # Per-upstream M2M OAuth2 client credentials (single-tenant) — the client_id/client_secret + # pair that authenticates br-ccs to each upstream (Fetcher / Reporter / STA) via + # the plugin-auth token endpoint. Multi-tenant (SaaS) resolves per-tenant credentials + # from AWS Secrets Manager instead. Held as a Secret; wire via a secrets manager or + # existing Secret. + # FETCHER_CLIENT_ID: "" + # FETCHER_CLIENT_SECRET: "" + # REPORTER_CLIENT_ID: "" + # REPORTER_CLIENT_SECRET: "" + # STA_CLIENT_ID: "" + # STA_CLIENT_SECRET: "" + + # -- Existing secrets name + useExistingSecret: false + existingSecretName: "" + # -- Extra environment variables (map of key: value pairs) — escape hatch for + # optional knobs not modeled above (e.g. RELATIONSHIP_SOURCE_*, CCS_REPORTER_POLL_*). + extraEnvVars: {} + serviceAccount: + # -- Specifies whether a ServiceAccount should be created + create: true + # -- Annotations for the ServiceAccount + annotations: {} + # -- Name of the service account + # @default -- `br-ccs.fullname` + name: "" +# Valkey (Redis-compatible) configuration +valkey: + enabled: true + global: + security: + allowInsecureImages: false + image: + repository: bitnami/valkey + tag: "8.0.2" + external: false + architecture: standalone + auth: + enabled: true + password: "" + primary: + kind: Deployment + resourcesPreset: "medium" + persistence: + enabled: false + extraFlags: + - "--maxmemory 640mb" + - "--maxmemory-policy allkeys-lru" + - "--hz 100" + - "--maxclients 10000" +# PostgreSQL configuration +postgresql: + enabled: true + global: + security: + allowInsecureImages: false + image: + repository: bitnami/postgresql + tag: "17.4.0" + external: false + architecture: standalone + auth: + enabled: true + enablePostgresUser: true + postgresPassword: "" + username: "br-ccs" + password: "" + database: "br-ccs" + primary: + persistence: + size: 8Gi + resourcesPreset: large + extraEnvVars: + - name: POSTGRESQL_MAX_CONNECTIONS + value: "500" + - name: POSTGRESQL_TCP_KEEPALIVES_IDLE + value: "30" + - name: POSTGRESQL_TCP_KEEPALIVES_INTERVAL + value: "10" + - name: POSTGRESQL_TCP_KEEPALIVES_COUNT + value: "5" +# RabbitMQ configuration (optional - disabled by default) +rabbitmq: + enabled: 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: + value: "br-ccs" + password: + value: "" + erlangCookie: + # Operators MUST supply a stable cookie (e.g. openssl rand -base64 32) when rabbitmq.enabled=true. + value: "" + extraSecrets: + - name: "br-ccs-rabbitmq-load-definition" + mountPath: /etc/rabbitmq/definitions + customConfig: | + management.load_definitions = /etc/rabbitmq/definitions/load_definition.json + +# trigger: retry release after README fix diff --git a/charts/br-sisbajud/Chart.yaml b/charts/br-sisbajud/Chart.yaml index a619cd193..770c992e4 100644 --- a/charts/br-sisbajud/Chart.yaml +++ b/charts/br-sisbajud/Chart.yaml @@ -17,7 +17,7 @@ maintainers: - name: "Lerian Studio" email: "support@lerian.studio" -version: 1.1.0 +version: 1.2.0-beta.1 # NOTE: appVersion is the default image tag for the app and the migration Job. # Override via brSisbajud.image.tag when needed. diff --git a/charts/br-slc/.helmignore b/charts/br-slc/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/br-slc/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/br-slc/CHANGELOG.md b/charts/br-slc/CHANGELOG.md new file mode 100644 index 000000000..0062c6739 --- /dev/null +++ b/charts/br-slc/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to the br-slc Helm chart are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0-beta.2] - 2026-07-15 + +### Added + +- `mock-nuclea` (Núclea clearing-house simulator) as a gated, standalone + Deployment + ClusterIP Service — DEV/HML FIXTURE ONLY, `enabled: false` by + default (never enable in production/BYOC). Unlike the same-pod + signer/xsdValidator/mqbridge sidecars, the app reaches it over cluster DNS + (`br-slc-mock-nuclea:9190`), not localhost. Uses its own + `ghcr.io/lerianstudio/br-slc-mock-nuclea` image (published by a separate + br-slc release-pipeline workstream); exposes only `GET /health`, used for + both liveness and readiness. + +## [0.1.0-beta.1] - 2026-07-10 + +### Added + +- Initial Helm chart for the br-slc monolith (Sistema de Liquidação Centralizada — SLC). +- Single Deployment running the app plus the mandatory same-pod `slc-signer` and + `aslc-xsd-validator` sidecars (ADR-12, reached over localhost) and a conditional + `mqbridge` sidecar (RSFN IBM MQ inbound, amd64-pinned, `replicaCount=1`, off by default). +- Detached migrations via an ArgoCD PreSync hook Secret + Job using the dedicated + `ghcr.io/lerianstudio/br-slc-migrations` image, with a `busybox` wait-for-postgres + initContainer. +- BYOC single-tenant posture: ClusterIP-only Service, no Ingress; external, client-managed + Postgres/Redis/RabbitMQ (no bundled subcharts). Per-container hardened `securityContext` + and ADR-9 signing-key custody isolated to the signer container. diff --git a/charts/br-slc/Chart.yaml b/charts/br-slc/Chart.yaml new file mode 100644 index 000000000..f6851b5fb --- /dev/null +++ b/charts/br-slc/Chart.yaml @@ -0,0 +1,33 @@ +apiVersion: v2 +name: br-slc-helm +description: | + Helm chart for the br-slc monolith (Sistema de Liquidação Centralizada — SLC). + BYOC single-tenant deployment: ClusterIP-only Service, no Ingress. Ships one + Deployment whose pod runs three mandatory containers (ADR-12): the app plus + the same-pod slc-signer and aslc-xsd-validator sidecars reached over + localhost, and a fourth conditional mqbridge container (RSFN IBM MQ inbound) + rendered only when mqbridge.enabled. A dedicated migrations image applies the + golang-migrate SQL via an ArgoCD PreSync hook Job before the rollout. +type: application +annotations: + lerian.studio/chart-type: single-service +home: https://github.com/LerianStudio/helm +sources: + - https://github.com/LerianStudio/helm/tree/main/charts/br-slc + - https://github.com/LerianStudio/br-slc +maintainers: + - name: "Lerian Studio" + email: "support@lerian.studio" +version: 1.0.0-beta.1 +# appVersion is the default image tag for the app, the signer/validator/mqbridge +# sidecars, and the migrations Job. Override per image via .tag when a +# component needs to pin a different build. +appVersion: "0.1.0" +keywords: + - slc + - br-slc + - siloc + - spb + - core-banking + - lerian +icon: https://avatars.githubusercontent.com/u/148895005?s=200&v=4 diff --git a/charts/br-slc/README.md b/charts/br-slc/README.md new file mode 100644 index 000000000..bc950f9df --- /dev/null +++ b/charts/br-slc/README.md @@ -0,0 +1,363 @@ +# br-slc Helm chart + +Internal Helm chart for the **br-slc monolith** (Sistema de Liquidação +Centralizada — SLC). BYOC single-tenant deployment: **ClusterIP-only** +Service, **no Ingress**. Postgres/Redis/RabbitMQ are external, client-managed +dependencies in BYOC — this chart does not deploy them. + +## Chart Contract + +- Chart type: `single-service` — one Deployment (the monolith) with mandatory + same-pod `slc-signer` + `aslc-xsd-validator` sidecars and a conditional + `mqbridge` sidecar, plus a detached migrations Job. +- Required secrets: **None for a default render.** For a real install provide + the app Secret keys (`POSTGRES_PASSWORD`, `REDIS_PASSWORD`, `WEBHOOK_API_KEY`, + `WEBHOOK_HMAC_SECRET`, and — only when the corresponding integration is + enabled — `RABBITMQ_DEFAULT_PASS`, `AWS_SECRET_ACCESS_KEY`, + `AWS_SESSION_TOKEN`) via `secrets.data` (`secrets.create=true`) or an + operator-managed `secrets.existingSecretName`. The signing-key material + (`SIGNER_SOFTKEY_PFX_PASSPHRASE`, `SIGNER_VAULT_TOKEN`, `SIGNER_PKCS11_PIN`, + awskms creds) lives ONLY in the signer Secret (`signer.secrets.*`, ADR-9 + custody) and is never readable by the app container. The migration Job reads + `POSTGRES_PASSWORD` from a dedicated PreSync Secret (or + `migrations.existingSecretName`). No credential is ever placed in a ConfigMap. +- Dependency notes: No bundled subcharts. Postgres, Redis and (optional) + RabbitMQ are EXTERNAL and pre-provisioned in BYOC — the chart only takes + connection config (`configmap.data.POSTGRES_HOST` / `REDIS_HOST` / RabbitMQ + host). Streaming (Kafka/Redpanda) is external and referenced only when + `STREAMING_ENABLED=true`. The `mqbridge` sidecar links the external IBM + MQ/RSFN broker (amd64-only) and is off by default. +- Production overrides: Set `image.tag` (defaults to `Chart.appVersion`); + `configmap.data.POSTGRES_HOST` / `REDIS_HOST`; `configmap.data.PLUGIN_AUTH_HOST` + (auth is on by default and **fails closed at boot** until this is set); + `configmap.data.CORS_ALLOWED_ORIGINS` (empty deny-all by default); the app + + signer Secrets; and, for the IF Domicílio role, `mqbridge.enabled=true` with + `RSFN_CONSUMER_TRANSPORT=mq` (forces `replicaCount=1` + an amd64 node pin). + `secrets.useExistingSecret`-style `create`/`existingSecretName` are supported + for the app, signer, mqbridge and migration Secrets. +- Source/license: Source is in `github.com/LerianStudio/helm`; chart license is + Apache-2.0. The `br-slc` service source is `github.com/LerianStudio/br-slc`. + +## Topology (ADR-12: same-pod sidecars) + +This chart ships **one Deployment**. Its pod runs **three mandatory +containers**, plus a **fourth conditional container** (`mqbridge`) that is +rendered only when `mqbridge.enabled=true`: + +| Container | Port | Role | Reached via | Rendered | +|---|---|---|---|---| +| `br-slc` | 4111 | the monolith | — | always | +| `slc-signer` | 9101 | ICP-Brasil signing / crypto+credential boundary (ADR-9) | `SIGNER_URL=http://localhost:9101` | always | +| `aslc-xsd-validator` | 9091 | XSD validation (fail-closed for outbound ASLC) | `XSD_VALIDATOR_URL=http://localhost:9091` | always | +| `mqbridge` | 9121 | inbound IBM MQ/RSFN consumer (cards channel) | `MQ_BRIDGE_URL=http://localhost:9121` | only when `mqbridge.enabled=true` | + +The signer and validator are **mandatory same-pod containers** (ADR-12, +"sidecar no mesmo pod, localhost") — no enable toggle. The app talks to them +over **localhost**, not cluster DNS. **Shared fate**: the pod only reaches +`Ready` when every container that defines a readiness probe passes it, which +removes the "app Ready but validator absent" window. The `mqbridge` container +deliberately has **no** readiness probe (see below), so it does not +independently gate pod readiness. Each container keeps its own hardened +`securityContext` and its own `/tmp` `emptyDir`. + +**Custody (ADR-9):** the signing-key material (softkey PFX + passphrase, HSM +PIN, Vault token) is mounted/injected **only** on the `slc-signer` container +(its own `signer.secrets` Secret + `signer.softkeySecret` volume) — never on +the app, validator, or mqbridge container. + +### Conditional `mqbridge` container (Decisão 21) + +The inbound **`mqbridge`** (IBM MQ/RSFN consumer of the cards channel) is a +**same-pod CONDITIONAL** container — `mqbridge.enabled: false` by default. A +BYOC client flips it on when the role is **IF Domicílio** with +`RSFN_CONSUMER_TRANSPORT=mq`; a credenciadora-only tenant never carries it. +It was previously a separate, gated Deployment (its own `br-slc-mq-bridge` +chart) — Decisão 21 moved it into this pod so the app reaches it over +**localhost:9121** (resolving the drain/ack affinity risk by construction: +same pod, same localhost). + +When (and only when) `mqbridge.enabled=true`: + +- The pod gains the `mqbridge` container (writable `/var/mqm` rootfs + + read-only keystore mount when `mqbridge.keystoreSecret.enabled`), plus its + own `-mqbridge` ConfigMap and (when + `mqbridge.secrets.create=true`) `-mqbridge-secrets` Secret. +- The **whole pod gets an amd64 node pin** + (`nodeSelector: kubernetes.io/arch: amd64`) — the IBM MQ Redistributable + Client (libmqm/GSKit) it links is published for Linux x86_64 only. The pin + is absent when the bridge is disabled. +- The mqbridge keystore (GSKit `.kdb/.sth/.rdb`) is mounted **only** on this + container, never on the app/signer/validator. +- **`replicaCount` is hard-capped to 1.** The bridge holds per-process state + (`/v1/drain` mints a `deliveryRef→conn`; `/v1/ack` must land in the SAME + process), and its mTLS identity is per-process (`MQSCO.KeyRepository` from + env `MQSSLKEYR`), so 1 process = 1 RSFN participant. As a same-pod container + the monolith `replicaCount` governs the consumer count — the render **fails** + with a guard message if `mqbridge.enabled` and `replicaCount != 1`. + +> **Enabling `mqbridge` couples app scale to the consumer.** If a client needs +> app HA (multiple replicas) **and** MQ inbound simultaneously, that signals +> the separate-Deployment / **Fase 7** multi-tenant consumer-manager path +> (Decisão 21) — escalate as a DOUBT, do not force multi-replica with the +> bridge enabled. + +## Install + +```bash +helm lint charts/br-slc +helm template my-release charts/br-slc + +# Real install — you MUST override at least: +# - configmap.data.POSTGRES_HOST / REDIS_HOST (external, client-managed) +# - configmap.data.PLUGIN_AUTH_HOST (required: PLUGIN_AUTH_ENABLED=true by default) +# - configmap.data.CORS_ALLOWED_ORIGINS (empty/deny-all by default) +# - secrets.* (see "Secrets" below) +helm install my-release charts/br-slc -f my-values.yaml +``` + +No `values-.yaml` files live in this chart — per-environment overrides +(dev/hml relaxations, SaaS-only knobs) belong in the beleriand gitops overlays +(Task 6.0.6), not here. + +## Secrets + +Secrets are split by custody boundary (ADR-9) into **two** Secrets so the +signing-key material is never readable by the app container. + +### App container Secret (`secrets.*`) + +Seven env vars, injected via `secretKeyRef` into both the app container and the +migration Job: + +| Env var | Purpose | +|---|---| +| `POSTGRES_PASSWORD` | primary Postgres password | +| `REDIS_PASSWORD` | Redis/Valkey password | +| `WEBHOOK_API_KEY` | client webhook API-key header (BYOC `direct` provider) | +| `WEBHOOK_HMAC_SECRET` | client webhook HMAC signing secret | +| `RABBITMQ_DEFAULT_PASS` | RabbitMQ password (inert while `RABBITMQ_ENABLED=false`, this base's default; only meaningful if an overlay enables RabbitMQ for real) | +| `AWS_SECRET_ACCESS_KEY` | AWS static credential for the app's M2M provider (prefer IRSA instead — keep empty) | +| `AWS_SESSION_TOKEN` | AWS static credential (prefer IRSA instead — keep empty) | + +### Signer container Secret (`signer.secrets.*`, ADR-9 custody) + +Five key/credential-material env vars injected **only** into the `slc-signer` +container (never the app): + +| Env var | Purpose | +|---|---| +| `SIGNER_SOFTKEY_PFX_PASSPHRASE` | decrypts the softkey PFX (dev/softkey custody; production should use awskms/pkcs11/vault) | +| `AWS_SECRET_ACCESS_KEY` | awskms adapter static credential (prefer IRSA — keep empty) | +| `AWS_SESSION_TOKEN` | awskms adapter static credential (prefer IRSA — keep empty) | +| `SIGNER_VAULT_TOKEN` | Vault transit auth token (only when `SIGNER_VAULT_ADDR` is set) | +| `SIGNER_PKCS11_PIN` | HSM/SoftHSM2 token PIN (only when a pkcs11 token is configured) | + +The softkey PFX **file** itself is mounted read-only onto the signer container +from an externally managed Secret via `signer.softkeySecret.*` (never baked +into the image or `values.yaml`). + +Each Secret has the same two modes (`secrets.create` / `signer.secrets.create`): + +- **`create: false` (default, recommended for BYOC).** The client manages the + Secret out-of-band (their own manifest, External Secrets Operator, Vault + injector, etc.) and sets `existingSecretName` to its name. That Secret + **must** expose the same keys listed above. +- **`create: true`.** This chart creates the Secret from `*.data` — populate + those values via `--set-string`/`-f` at install time (a values override file + kept out of version control), never by committing real values to this + chart's `values.yaml`. + +`AWS_ACCESS_KEY_ID` stays in the ConfigMap (the non-secret half of a static +credential pair, analogous to a `*_USER` var) and is deliberately **not** +secret-managed: it must stay empty in production/BYOC. Use IRSA (EKS) or an +equivalent workload identity via `serviceAccount.annotations` instead — the +AWS SDK's default credential chain picks that up with no env vars at all. + +## Env var coverage + +All active vars from `config/.env.example` are covered. `config/.env.example` +is the shared compose-stack env file, so it also carries the signer/validator +vars — under this chart's same-pod consolidation those live in the +`signer.configmap.data` / `signer.secrets.data` / `xsdValidator.configmap.data` +subsections (see `values.yaml`), NOT in the app's ConfigMap/Secret. The app +container's own surface is the app `configmap.data` (non-secret) plus the +7-key app Secret (Task 6.0.2 reconciliation moved `RABBITMQ_DEFAULT_PASS`, +`AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` from the ConfigMap into the +Secret; the ADR-12 consolidation moved `SIGNER_SOFTKEY_PFX_PASSPHRASE` out of +the app Secret into `signer.secrets.data` per the ADR-9 custody boundary — see +"Flagged deviations" below). Two additional vars are surfaced beyond +`.env.example`'s app surface because this chart's hardened defaults require +them (see the header comment above `configmap.data` in `values.yaml` for the +full rationale): + +| Var | Why it's here even though not "active" in `.env.example` | +|---|---| +| `DEPLOYMENT_MODE` | Read by `config_validation.go`/`systemplane.go`/TLS enforcement; `.env.example` has no active line for it. Defaults to `byoc`. | +| `PLUGIN_AUTH_HOST` | `.env.example` only has it commented out (auth defaults off there). This chart's base flips `PLUGIN_AUTH_ENABLED=true`, and `config_validation.go`'s `validateAuthConfig` **fails boot** if `PLUGIN_AUTH_HOST` is empty when auth is enabled. **This chart intentionally fails closed at boot until you set the real Access Manager URL** — that is deliberate hardening, not a bug, but you must set it before first deploy. | + +Every one of the 156 was checked mechanically against +`config/.env.example` (grep for uncommented `KEY=` lines) and cross-referenced +against the `env:"..."` struct tags in `internal/bootstrap/config.go`. See the +`ring:helm` execution report for the full 156-row source table. + +### Flagged deviations from `.env.example`'s dev defaults + +This base intentionally hardens a handful of values beyond mirroring +`.env.example` (which is tuned for local/dev, not BYOC production): + +- `ENV_NAME=production` (was `development`) — activates + `config_validation.go`'s production hardening gate. +- `ALLOW_INSECURE_TLS` / `ALLOW_CORS_WILDCARD` / `ALLOW_RATELIMIT_FAIL_OPEN` = + `false` (all were `true` in `.env.example`, which is dev/CI-only — production + boot is rejected if `ALLOW_CORS_WILDCARD` or `ALLOW_RATELIMIT_FAIL_OPEN` is + true anyway). +- `POSTGRES_SSLMODE=require` (was `disable`) — production boot is **rejected** + if this is `disable` (`config_validation.go`). Tighten to `verify-full` once + the client's Postgres cert chain is known. +- `PLUGIN_AUTH_ENABLED=true` (was `false`) — real auth in BYOC; see + `PLUGIN_AUTH_HOST` note above. +- `ENABLE_TELEMETRY=true` (was `false`) — production observability on by + default; `OTEL_EXPORTER_OTLP_ENDPOINT` assumes an in-cluster + `otel-collector-lerian` release — reconcile the real name per environment. +- `RABBITMQ_DEFAULT_PASS` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` live + in `secrets.data`, not the ConfigMap (Task 6.0.2 reconciliation — see + "Secrets" above). `RABBITMQ_DEFAULT_PASS` is inert while + `RABBITMQ_ENABLED=false` (this base's default); the two AWS vars are + expected to stay empty in production regardless (prefer IRSA). +- `SIGNER_URL` / `XSD_VALIDATOR_URL` / `MQ_BRIDGE_URL` all point at + **localhost** (`http://localhost:9101`, `http://localhost:9091`, + `http://localhost:9121`) because the signer, validator, and (when enabled) + the `mqbridge` are same-pod containers (ADR-12 + Decisão 21) — not cluster + DNS. `MQ_BRIDGE_URL` is inert until `RSFN_CONSUMER_TRANSPORT=mq` **and** + `mqbridge.enabled=true` (see "Conditional `mqbridge` container" above). + +## Migration Job + +Migrations are applied by a detached **ArgoCD `PreSync` hook** — a dedicated +Job plus a small companion Secret, both under `templates/migrations/` — that +runs **before** the main sync wave rolls the Deployment, so the monolith never +boots against an unmigrated database. + +- `templates/migrations/secrets.yaml` — a PreSync Secret + (`argocd.argoproj.io/hook-weight: "-2"`) carrying **only** `POSTGRES_PASSWORD` + (from `migrations.postgres.password`, falling back to + `secrets.data.POSTGRES_PASSWORD`). Its `hook-delete-policy` is + `BeforeHookCreation` **only** (not `HookSucceeded`) so it survives the whole + PreSync phase for the Job to read, and is replaced on the next sync. It is + minted only when `migrations.useExistingSecret=false`. +- `templates/migrations/job.yaml` — a PreSync Job + (`argocd.argoproj.io/hook-weight: "-1"`, so it runs after the Secret) that + runs the dedicated `ghcr.io/lerianstudio/br-slc-migrations` image + (`migrations.image.tag` defaults to `Chart.appVersion`). That image's + entrypoint owns the golang-migrate loop over the `migrations/*.up.sql` + **baked into the image** at build time by the br-slc release pipeline — the + chart's Job only supplies the DB connection and runs the image; it does not + carry the SQL or build the loop. Its `hook-delete-policy` is + `BeforeHookCreation,HookSucceeded`. + +The Job reads the DB connection from plain env vars +(`POSTGRES_HOST`/`POSTGRES_PORT`/`POSTGRES_USER`/`POSTGRES_NAME`/`POSTGRES_SSLMODE`, +each defaulting to the app's `configmap.data.POSTGRES_*`) plus +`POSTGRES_PASSWORD` via `secretKeyRef`. When `migrations.useExistingSecret=true` +the password comes from the operator-managed `migrations.existingSecretName` +instead of the chart-minted PreSync Secret (the render **fails fast** if +`existingSecretName` is left empty in that mode). + +A lightweight `busybox` initContainer (`migrations.waitForPostgres`, on by +default) polls `POSTGRES_HOST:POSTGRES_PORT` with `nc -z` before the migrations +container starts, so a fresh install racing a not-yet-ready Postgres doesn't +hard-fail the Job. + +Set `migrations.enabled: false` to skip the hook entirely (e.g. if migrations +are applied by a separate operational process). + +### Recovering from a dirty migration + +If a migration fails partway through, golang-migrate marks +`schema_migrations.dirty = true` in the database. Every subsequent sync re-runs +this PreSync Job, which keeps refusing to apply the next migration on top of a +dirty version — so the release never converges on its own. + +Manual recovery: + +1. **Find the failing version.** Inspect the failed PreSync Job/pod logs + (`kubectl logs job/-br-slc-migrations`). golang-migrate's error + output names the dirty version, e.g. `Dirty database version 7. Fix and + force version.` +2. **Force the version.** Run a one-off `migrate force ` against the + same database, pointed at the same Postgres endpoint/credentials the Job + uses (any golang-migrate-capable CLI works — the `schema_migrations` table + is standard). Set `` to the value identified in step 1 (use the + version *before* the failed one if the failed migration's DDL did not + actually apply — confirm against the target schema before forcing). +3. **Re-sync.** With `dirty` cleared, the PreSync Job runs normally and applies + the remaining migrations. + +This is a manual, deliberately out-of-band recovery path — the hook does not +attempt automatic dirty-state recovery, since forcing the wrong version can +silently skip or reapply DDL. + +## Probes + +| Container | Probe | Path | Port | Notes | +|---|---|---|---|---| +| `br-slc` | liveness | `/health` | 4111 | process viability only, no dependency checks | +| `br-slc` | readiness | `/readyz` | 4111 | dependency-aware; has a `draining` state during graceful shutdown — more lenient `failureThreshold` than liveness | +| `slc-signer` | liveness + readiness | `/health` | 9101 | same path for both (no separate readyz) | +| `aslc-xsd-validator` | liveness + readiness | `/health` | 9091 | same path for both | +| `mqbridge` | liveness only | `/health` | 9121 | only when `mqbridge.enabled`; process viability. **No readinessProbe on purpose** (Decisão 21) | + +The app's `/health` and `/readyz` are auth/telemetry-exempt (see +`internal/bootstrap/routes.go`, `fiber_server.go`); `/metrics` is also exempt +but not probed here. Readiness relies on **per-container probes + shared +fate**: the pod is only `Ready` when all readiness-probed containers pass, so a +crash-looping signer/validator de-registers the pod endpoint on its own. The +app's `/readyz` deliberately does **not** add signer/validator dependency +checks — the fail-closed guard is enforced per-request in the builders, and +double-coupling readyz would only cause flapping. **mqbridge deliberately has +NO readinessProbe**: nothing routes to `:9121` via a Service, and its `/readyz` +reflects the live IBM MQ pool, so wiring it as readiness would pull the WHOLE +pod out of the monolith's `:4111` Service on any MQ blip — under the +`replicaCount=1` hard-cap that is a total API outage, not a consumer +degradation. MQ health reaches the monolith via the client-side breaker. + +## Security posture + +Every container (app, `slc-signer`, `aslc-xsd-validator`, migrate) carries the +same hardened `securityContext`: `runAsNonRoot: true`, `runAsUser/Group: +65532`, `allowPrivilegeEscalation: false`, `readOnlyRootFilesystem: true` +(each with its own `emptyDir` at `/tmp`), all capabilities dropped, +`seccompProfile: RuntimeDefault`. The signer/validator postures are set +per-container via `signer.securityContext` / `xsdValidator.securityContext`. +The conditional `mqbridge` container (`mqbridge.securityContext`) shares this +posture with **one deliberate exception**: `readOnlyRootFilesystem: false`, +because the IBM MQ C client (libmqm) needs a writable `/var/mqm` (mounted as +an explicit `emptyDir`); it stays non-root, no privilege escalation, all +capabilities dropped, `RuntimeDefault` seccomp. Service is `ClusterIP` only. + +## Installation + +The chart is published as an OCI artifact to GitHub Container Registry: + +```bash +helm install my-release oci://ghcr.io/lerianstudio/br-slc-helm --version +``` + +## Port-forward (no Ingress) + +```bash +kubectl port-forward svc/my-release-br-slc 4111:4111 +curl localhost:4111/health +curl localhost:4111/readyz +``` + +## Not included in this chart + +- **HPA / PodDisruptionBudget** — not in this chart's scope (Task 6.0.1 + deliverable list); add if/when autoscaling or multi-replica HA is needed. +- **Postgres/Redis/RabbitMQ subcharts** — BYOC brings its own; this chart only + takes connection config. +- **SaaS multi-tenant MQ consumer manager** — the shared-keystore / + cert-label-per-connection consumer manager for the SaaS model is **Fase 7** + (a separate singleton Deployment), out of scope here (Decisão 21). This + chart's `mqbridge` is the BYOC single-tenant same-pod consumer only. diff --git a/charts/br-slc/templates/_helpers.tpl b/charts/br-slc/templates/_helpers.tpl new file mode 100644 index 000000000..ea7e97ef9 --- /dev/null +++ b/charts/br-slc/templates/_helpers.tpl @@ -0,0 +1,241 @@ +{{/* +Expand the name of the chart. The chart is named "br-slc-helm" (helm-repo +convention), but every workload/label uses the service name "br-slc". +*/}} +{{- define "br-slc.name" -}} +{{- default "br-slc" .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. Truncated to 63 chars (K8s name +length limit) and trimmed of a trailing "-". +*/}} +{{- define "br-slc.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default "br-slc" .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Chart name and version as used by the chart label. +*/}} +{{- define "br-slc.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels. +*/}} +{{- define "br-slc.labels" -}} +helm.sh/chart: {{ include "br-slc.chart" . }} +{{ include "br-slc.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels. +*/}} +{{- define "br-slc.selectorLabels" -}} +app.kubernetes.io/name: {{ include "br-slc.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +ServiceAccount name to use. +*/}} +{{- define "br-slc.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "br-slc.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} + +{{/* +Secret name to use for the 7 secret-bearing env vars: either the chart-created +Secret (secrets.create=true) or an externally managed Secret +(secrets.existingSecretName) — BYOC default is the latter. +*/}} +{{- define "br-slc.secretName" -}} +{{- if .Values.secrets.existingSecretName -}} +{{- .Values.secrets.existingSecretName -}} +{{- else -}} +{{- printf "%s-secrets" (include "br-slc.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{/* +Resolved image reference: "repository:tag", defaulting tag to .Chart.AppVersion +when empty. +*/}} +{{- define "br-slc.image" -}} +{{- $tag := .Values.image.tag | default .Chart.AppVersion -}} +{{- printf "%s:%s" .Values.image.repository $tag -}} +{{- end -}} + +{{/* +Same-pod signer container image reference, defaulting tag to .Chart.AppVersion. +*/}} +{{- define "br-slc.signerImage" -}} +{{- $tag := .Values.signer.image.tag | default .Chart.AppVersion -}} +{{- printf "%s:%s" .Values.signer.image.repository $tag -}} +{{- end -}} + +{{/* +Same-pod xsd-validator container image reference, defaulting tag to +.Chart.AppVersion. +*/}} +{{- define "br-slc.validatorImage" -}} +{{- $tag := .Values.xsdValidator.image.tag | default .Chart.AppVersion -}} +{{- printf "%s:%s" .Values.xsdValidator.image.repository $tag -}} +{{- end -}} + +{{/* +Same-pod mqbridge container image reference, defaulting tag to +.Chart.AppVersion. Rendered only when mqbridge.enabled (Decisão 21). +*/}} +{{- define "br-slc.mqbridgeImage" -}} +{{- $tag := .Values.mqbridge.image.tag | default .Chart.AppVersion -}} +{{- printf "%s:%s" .Values.mqbridge.image.repository $tag -}} +{{- end -}} + +{{/* +Signer-scoped Secret name (ADR-9 custody): the chart-created signer Secret +(signer.secrets.create=true) or an externally managed one +(signer.secrets.existingSecretName). Distinct from the app's br-slc.secretName +so signing-key material is never in the app container's Secret. +*/}} +{{- define "br-slc.signerSecretName" -}} +{{- if .Values.signer.secrets.existingSecretName -}} +{{- .Values.signer.secrets.existingSecretName -}} +{{- else -}} +{{- printf "%s-signer-secrets" (include "br-slc.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{/* +Mqbridge-scoped Secret name (Decisão 21): the chart-created mqbridge Secret +(mqbridge.secrets.create=true) or an externally managed one +(mqbridge.secrets.existingSecretName). Distinct from the app's br-slc.secretName +so MQ credential material stays scoped to the mqbridge container's env. +*/}} +{{- define "br-slc.mqbridgeSecretName" -}} +{{- if .Values.mqbridge.secrets.existingSecretName -}} +{{- .Values.mqbridge.secrets.existingSecretName -}} +{{- else -}} +{{- printf "%s-mqbridge-secrets" (include "br-slc.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{/* +Expand the namespace of the release. Overridable for multi-namespace layouts. +*/}} +{{- define "global.namespace" -}} +{{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Migrations fullname, e.g. br-slc-migrations — the PreSync Job and Secret +reference this. +*/}} +{{- define "br-slc-migrations.fullname" -}} +{{- printf "%s-migrations" (include "br-slc.fullname" . | trunc 52 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Migrations labels. +*/}} +{{- define "br-slc-migrations.labels" -}} +helm.sh/chart: {{ include "br-slc.chart" . }} +app.kubernetes.io/name: {{ include "br-slc-migrations.fullname" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/component: migrations +{{- end -}} + +{{/* +Per-container securityContext (ADR-12 hardening). Same key set for every +same-pod container; only the values differ. Call with the container's +securityContext dict as context, e.g.: + securityContext: + {{- include "br-slc.containerSecurityContext" .Values.signer.securityContext | nindent 12 }} +*/}} +{{- define "br-slc.containerSecurityContext" -}} +runAsNonRoot: {{ .runAsNonRoot }} +runAsUser: {{ .runAsUser }} +runAsGroup: {{ .runAsGroup }} +allowPrivilegeEscalation: {{ .allowPrivilegeEscalation }} +{{- if hasKey . "readOnlyRootFilesystem" }} +readOnlyRootFilesystem: {{ .readOnlyRootFilesystem }} +{{- end }} +seccompProfile: + type: {{ .seccompProfile.type }} +capabilities: + drop: + {{- range .capabilities.drop }} + - {{ . }} + {{- end }} +{{- end -}} + +{{/* +HTTP liveness/readiness probe body. Call with the probe dict as context, e.g.: + livenessProbe: + {{- include "br-slc.httpProbe" .Values.signer.livenessProbe | nindent 12 }} +*/}} +{{- define "br-slc.httpProbe" -}} +httpGet: + path: {{ .httpGet.path }} + port: {{ .httpGet.port }} +initialDelaySeconds: {{ .initialDelaySeconds }} +periodSeconds: {{ .periodSeconds }} +timeoutSeconds: {{ .timeoutSeconds }} +failureThreshold: {{ .failureThreshold }} +{{- end -}} + +{{/* +mock-nuclea (Núclea clearing-house simulator) — a SEPARATE Deployment+Service, +NOT a same-pod sidecar: it must be independently gated OFF (safe-by-default) and +reached over cluster DNS by the app. DEV/HML FIXTURE ONLY — never enable in +production/BYOC. Mirrors the detached-`migrations` component's own-fullname / +own-labels convention so the app Service selector never captures the mock pod. +*/}} +{{- define "br-slc-mock-nuclea.fullname" -}} +{{- printf "%s-mock-nuclea" (include "br-slc.fullname" . | trunc 51 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +mock-nuclea selector labels — distinct name from the app so the app's Service +(br-slc.selectorLabels) never routes to the mock and vice-versa. +*/}} +{{- define "br-slc-mock-nuclea.selectorLabels" -}} +app.kubernetes.io/name: {{ include "br-slc-mock-nuclea.fullname" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +mock-nuclea labels. +*/}} +{{- define "br-slc-mock-nuclea.labels" -}} +helm.sh/chart: {{ include "br-slc.chart" . }} +{{ include "br-slc-mock-nuclea.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/component: mock-nuclea +{{- end -}} + +{{/* +mock-nuclea image reference, defaulting tag to .Chart.AppVersion when empty. +Distinct image from the app (its own release-pipeline artifact). +*/}} +{{- define "br-slc.mockNucleaImage" -}} +{{- $tag := .Values.mockNuclea.image.tag | default .Chart.AppVersion -}} +{{- printf "%s:%s" .Values.mockNuclea.image.repository $tag -}} +{{- end -}} diff --git a/charts/br-slc/templates/configmap.yaml b/charts/br-slc/templates/configmap.yaml new file mode 100644 index 000000000..eb2485f5e --- /dev/null +++ b/charts/br-slc/templates/configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "br-slc.fullname" . }} + labels: + {{- include "br-slc.labels" . | nindent 4 }} +data: + WEBHOOK_NOTIFICATIONS_TOKEN_CACHE_TTL_SEC: {{ .Values.webhookNotificationsCacheTtlSec | default "60" | quote }} + WEBHOOK_HMAC_SECRET_FILE: {{ .Values.webhookHmacSecretFile | default "" | quote }} + {{- range $key, $value := .Values.configmap.data }} + {{ $key }}: {{ $value | quote }} + {{- end }} diff --git a/charts/br-slc/templates/deployment.yaml b/charts/br-slc/templates/deployment.yaml new file mode 100644 index 000000000..ff306bf1d --- /dev/null +++ b/charts/br-slc/templates/deployment.yaml @@ -0,0 +1,291 @@ +{{- /* Decisão 21 hard-cap: when the same-pod mqbridge (RSFN IBM MQ consumer) +is enabled, the MONOLITH replicaCount governs the consumer count. The bridge +holds per-process state (/v1/drain mints deliveryRef->conn; /v1/ack must land +in the SAME process), so 2+ replicas would fan RSFN connections out and break +drain/ack affinity. Enabling mqbridge couples app scale to the consumer — if a +client needs app HA + MQ simultaneously that signals the separate-Deployment / +Fase 7 path, escalate rather than force. */}} +{{- if .Values.mqbridge.enabled }} +{{- if ne (int .Values.replicaCount) 1 }} +{{- fail "mqbridge.enabled requires replicaCount=1 (RSFN consumer is a per-process singleton; ver Decisão 21)" }} +{{- end }} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "br-slc.fullname" . }} + labels: + {{- include "br-slc.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + selector: + matchLabels: + {{- include "br-slc.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "br-slc.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "br-slc.serviceAccountName" . }} + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + containers: + - name: br-slc + image: {{ include "br-slc.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.targetPort }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "br-slc.fullname" . }} + env: + {{- /* Gate 8 FIX 7: without this, the Go runtime defaults + GOMAXPROCS to the NODE's core count (it can't see the container's + CFS cpu.limit), causing needless CFS throttling under load. The + downward API's resourceFieldRef rounds the cpu limit up to the + nearest whole core (divisor "1"), matching the CFS quota. Guarded + with `with` (not a plain `if .Values.resources.limits.cpu`) so a + values override that sets resources.limits to nil (a legitimate + way to ship a Burstable/no-limit override, not just an omitted + key) doesn't panic the render — dotting straight into `.cpu` on a + nil `resources.limits` errors ("nil pointer evaluating + interface{}.cpu"), whereas `with` only evaluates `.cpu` once it + has already confirmed the piped value is non-nil/non-empty. */}} + {{- with .Values.resources.limits }} + {{- if .cpu }} + - name: GOMAXPROCS + valueFrom: + resourceFieldRef: + resource: limits.cpu + divisor: "1" + {{- end }} + {{- end }} + {{- range $key, $value := .Values.secrets.data }} + - name: {{ $key }} + valueFrom: + secretKeyRef: + name: {{ include "br-slc.secretName" $ }} + key: {{ $key }} + {{- end }} + livenessProbe: + {{- include "br-slc.httpProbe" .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- include "br-slc.httpProbe" .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + {{- include "br-slc.containerSecurityContext" .Values.securityContext | nindent 12 }} + volumeMounts: + - name: tmp + mountPath: /tmp + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- /* Same-pod signer container (ADR-12 / Decision 9). Custody + (ADR-9): the signing-key Secret and softkey PFX volume are mounted + HERE ONLY, never on the app container above. Communicates with the app + over localhost:9101 (see configmap.data.SIGNER_URL). */}} + - name: slc-signer + image: {{ include "br-slc.signerImage" . | quote }} + imagePullPolicy: {{ .Values.signer.image.pullPolicy }} + ports: + - name: signer-http + containerPort: 9101 + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "br-slc.fullname" . }}-signer + env: + {{- /* GOMAXPROCS from the container CFS cpu limit — same + downward-API fix as the br-slc container (Gate 8 FIX 7); the signer + is a CPU-limited Go process on the fail-closed ASLC signing hot + path. Guarded with `with`+`if .cpu` so a nil-limit override renders. */}} + {{- with .Values.signer.resources.limits }} + {{- if .cpu }} + - name: GOMAXPROCS + valueFrom: + resourceFieldRef: + resource: limits.cpu + divisor: "1" + {{- end }} + {{- end }} + {{- range $key, $value := .Values.signer.secrets.data }} + - name: {{ $key }} + valueFrom: + secretKeyRef: + name: {{ include "br-slc.signerSecretName" $ }} + key: {{ $key }} + {{- end }} + livenessProbe: + {{- include "br-slc.httpProbe" .Values.signer.livenessProbe | nindent 12 }} + readinessProbe: + {{- include "br-slc.httpProbe" .Values.signer.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.signer.resources | nindent 12 }} + securityContext: + {{- include "br-slc.containerSecurityContext" .Values.signer.securityContext | nindent 12 }} + volumeMounts: + - name: signer-tmp + mountPath: /tmp + {{- if .Values.signer.softkeySecret.enabled }} + - name: softkey + mountPath: {{ .Values.signer.softkeySecret.mountPath }} + readOnly: true + {{- end }} + {{- /* Same-pod xsd-validator container (ADR-12). Stateless, no + secrets. Fail-closed hard dependency for outbound ASLC; shared fate in + this pod removes the "app Ready, validator absent" window. + Communicates over localhost:9091 (see configmap.data.XSD_VALIDATOR_URL). */}} + - name: aslc-xsd-validator + image: {{ include "br-slc.validatorImage" . | quote }} + imagePullPolicy: {{ .Values.xsdValidator.image.pullPolicy }} + ports: + - name: xsd-http + containerPort: 9091 + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "br-slc.fullname" . }}-validator + env: + {{- /* GOMAXPROCS from the container CFS cpu limit — same + downward-API fix as the br-slc container (Gate 8 FIX 7); the + validator is a CPU-limited Go process on the fail-closed ASLC XSD + hot path. Guarded so a nil-limit override renders. */}} + {{- with .Values.xsdValidator.resources.limits }} + {{- if .cpu }} + - name: GOMAXPROCS + valueFrom: + resourceFieldRef: + resource: limits.cpu + divisor: "1" + {{- end }} + {{- end }} + livenessProbe: + {{- include "br-slc.httpProbe" .Values.xsdValidator.livenessProbe | nindent 12 }} + readinessProbe: + {{- include "br-slc.httpProbe" .Values.xsdValidator.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.xsdValidator.resources | nindent 12 }} + securityContext: + {{- include "br-slc.containerSecurityContext" .Values.xsdValidator.securityContext | nindent 12 }} + volumeMounts: + - name: validator-tmp + mountPath: /tmp + {{- if .Values.mqbridge.enabled }} + {{- /* Same-pod mqbridge container (ADR-12 / Decisão 21). CONDITIONAL — + default off, rendered only when the BYOC client's role is IF Domicílio + with RSFN_CONSUMER_TRANSPORT=mq. Owns the inbound IBM MQ/RSFN connection; + identity/keystore are per-process (MQSCO.KeyRepository from env MQSSLKEYR), + so 1 process = 1 participant (see the replicaCount guard at the top of + this template). The app reaches it over localhost:9121 (see + configmap.data.MQ_BRIDGE_URL). UNLIKE signer/validator, rootfs is + WRITABLE (libmqm needs /var/mqm) and the pod gets an amd64 pin below. */}} + - name: mqbridge + image: {{ include "br-slc.mqbridgeImage" . | quote }} + imagePullPolicy: {{ .Values.mqbridge.image.pullPolicy }} + ports: + - name: mq-http + containerPort: 9121 + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "br-slc.fullname" . }}-mqbridge + env: + {{- /* GOMAXPROCS from the container CFS cpu limit — same + downward-API fix as the br-slc container (Gate 8 FIX 7); mqbridge is + a CPU-limited Go process. Guarded so a nil-limit override renders. */}} + {{- with .Values.mqbridge.resources.limits }} + {{- if .cpu }} + - name: GOMAXPROCS + valueFrom: + resourceFieldRef: + resource: limits.cpu + divisor: "1" + {{- end }} + {{- end }} + {{- range $key, $value := .Values.mqbridge.secrets.data }} + - name: {{ $key }} + valueFrom: + secretKeyRef: + name: {{ include "br-slc.mqbridgeSecretName" $ }} + key: {{ $key }} + {{- end }} + livenessProbe: + {{- include "br-slc.httpProbe" .Values.mqbridge.livenessProbe | nindent 12 }} + # NO readinessProbe on purpose (Decisão 21): mqbridge is same-pod and + # nothing routes to :9121 through a Service, so a readiness probe here + # buys no traffic gating. Its /readyz reflects the IBM MQ pool state + # (mqbridge/pool.go), so wiring it as readiness would drag the WHOLE + # pod out of the monolith's :4111 Service on any MQ blip — with the + # replicaCount=1 hard-cap that is a total API outage, not a consumer + # degradation. Liveness (process /health) is enough to restart a hung + # bridge; MQ connectivity is surfaced to the monolith via the client's + # breaker instead. + resources: + {{- toYaml .Values.mqbridge.resources | nindent 12 }} + securityContext: + {{- include "br-slc.containerSecurityContext" .Values.mqbridge.securityContext | nindent 12 }} + # Writable rootfs for libmqm (/var/mqm). Set here, not in values.yaml. + readOnlyRootFilesystem: false + volumeMounts: + # libmqm's writable state (error/FDC/trace logs). Explicit emptyDir + # regardless of readOnlyRootFilesystem=false, so the writable + # location never depends on the base image's own directory + # permissions (see values.yaml mqbridge.securityContext comment). + - name: mqbridge-var-mqm + mountPath: /var/mqm + {{- if .Values.mqbridge.keystoreSecret.enabled }} + - name: mqbridge-keystore + mountPath: {{ .Values.mqbridge.keystoreSecret.mountPath }} + readOnly: true + {{- end }} + {{- end }} + volumes: + - name: tmp + emptyDir: {} + - name: signer-tmp + emptyDir: {} + - name: validator-tmp + emptyDir: {} + {{- if .Values.signer.softkeySecret.enabled }} + - name: softkey + secret: + secretName: {{ .Values.signer.softkeySecret.secretName }} + items: + - key: {{ .Values.signer.softkeySecret.secretKey }} + path: signer.pfx + {{- end }} + {{- if .Values.mqbridge.enabled }} + - name: mqbridge-var-mqm + emptyDir: {} + {{- if .Values.mqbridge.keystoreSecret.enabled }} + - name: mqbridge-keystore + secret: + secretName: {{ .Values.mqbridge.keystoreSecret.secretName }} + {{- end }} + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- /* Decisão 21: pod-level amd64 pin, present ONLY when mqbridge is + enabled. The IBM MQ Redistributable Client (libmqm/GSKit) this bridge + links is published for Linux x86_64 only, so the whole pod must schedule + onto amd64 nodes. Absent when mqbridge is disabled (no arch constraint on + the app+signer+validator pod). */}} + {{- if .Values.mqbridge.enabled }} + nodeSelector: + kubernetes.io/arch: amd64 + {{- end }} diff --git a/charts/br-slc/templates/migrations/job.yaml b/charts/br-slc/templates/migrations/job.yaml new file mode 100644 index 000000000..732d78723 --- /dev/null +++ b/charts/br-slc/templates/migrations/job.yaml @@ -0,0 +1,114 @@ +{{- if .Values.migrations.enabled }} +# ArgoCD PreSync migration Job. Runs the dedicated br-slc-migrations image, +# whose entrypoint applies the golang-migrate loop over the embedded +# migrations/*.up.sql against the single br-slc database. The chart's Job only +# provides the DB connection + runs the image; it does NOT build the loop. +# +# hook-weight -1 puts this after the PreSync Secret (-2) and before the +# main-sync Deployment, so the monolith never boots against an unmigrated DB. +{{- $pgHost := .Values.migrations.postgres.host | default .Values.configmap.data.POSTGRES_HOST }} +{{- $pgPort := .Values.migrations.postgres.port | default .Values.configmap.data.POSTGRES_PORT | default "5432" }} +{{- $pgUser := .Values.migrations.postgres.user | default .Values.configmap.data.POSTGRES_USER | default "br-slc" }} +{{- $pgDb := .Values.migrations.postgres.database | default .Values.configmap.data.POSTGRES_NAME | default "br-slc" }} +{{- $pgSslMode := .Values.migrations.postgres.sslMode | default .Values.configmap.data.POSTGRES_SSLMODE | default "require" }} +{{- $secretName := ternary .Values.migrations.existingSecretName (include "br-slc-migrations.fullname" .) .Values.migrations.useExistingSecret }} +{{- if and .Values.migrations.useExistingSecret (not .Values.migrations.existingSecretName) }} +{{- fail "migrations.useExistingSecret=true requires migrations.existingSecretName to be set" }} +{{- end }} +{{- $pullSecrets := .Values.migrations.imagePullSecrets | default .Values.imagePullSecrets }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "br-slc-migrations.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-slc-migrations.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-weight: "-1" + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded +spec: + backoffLimit: {{ .Values.migrations.backoffLimit | default 2 }} + activeDeadlineSeconds: {{ .Values.migrations.activeDeadlineSeconds | default 600 }} + ttlSecondsAfterFinished: {{ .Values.migrations.ttlSecondsAfterFinished | default 259200 }} + template: + metadata: + labels: + {{- include "br-slc-migrations.labels" . | nindent 8 }} + spec: + restartPolicy: Never + automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault + {{- with $pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.migrations.waitForPostgres.enabled }} + initContainers: + - name: wait-for-postgres + image: "{{ .Values.migrations.waitForPostgres.image.repository }}:{{ .Values.migrations.waitForPostgres.image.tag }}" + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - > + echo "waiting for {{ $pgHost }}:{{ $pgPort }}..."; + until nc -z {{ $pgHost }} {{ $pgPort }}; do + echo "{{ $pgHost }}:{{ $pgPort }} not ready, waiting..."; sleep 2; + done; + echo "{{ $pgHost }}:{{ $pgPort }} is ready" + securityContext: + runAsNonRoot: true + runAsUser: {{ .Values.securityContext.runAsUser }} + runAsGroup: {{ .Values.securityContext.runAsGroup }} + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + resources: + limits: + cpu: "100m" + memory: 32Mi + requests: + cpu: "10m" + memory: 16Mi + {{- end }} + containers: + - name: migrations + image: "{{ .Values.migrations.image.repository }}:{{ .Values.migrations.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.migrations.image.pullPolicy | default "IfNotPresent" }} + env: + - name: POSTGRES_HOST + value: {{ $pgHost | quote }} + - name: POSTGRES_PORT + value: {{ $pgPort | quote }} + - name: POSTGRES_USER + value: {{ $pgUser | quote }} + - name: POSTGRES_NAME + value: {{ $pgDb | quote }} + - name: POSTGRES_SSLMODE + value: {{ $pgSslMode | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: POSTGRES_PASSWORD + securityContext: + runAsNonRoot: true + runAsUser: {{ .Values.securityContext.runAsUser }} + runAsGroup: {{ .Values.securityContext.runAsGroup }} + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + resources: + {{- toYaml .Values.migrations.resources | nindent 12 }} +{{- end }} diff --git a/charts/br-slc/templates/migrations/secrets.yaml b/charts/br-slc/templates/migrations/secrets.yaml new file mode 100644 index 000000000..6405ebe7d --- /dev/null +++ b/charts/br-slc/templates/migrations/secrets.yaml @@ -0,0 +1,24 @@ +{{- if and .Values.migrations.enabled (not .Values.migrations.useExistingSecret) }} +# Dedicated PreSync Secret carrying only the Postgres password for the migration +# Job. PreSync hooks run BEFORE the main sync wave, so on a first install the +# app Secret does not exist yet. This hook Secret (weight -2) is created before +# the migration Job (weight -1). hook-delete-policy is BeforeHookCreation ONLY +# (not HookSucceeded) so it survives the whole PreSync phase for the Job to +# read; it is replaced at the next sync. When migrations.useExistingSecret=true +# this Secret is NOT minted and the Job reads POSTGRES_PASSWORD from the +# operator-provided existing Secret (migrations.existingSecretName). +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "br-slc-migrations.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-slc-migrations.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-weight: "-2" + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +type: Opaque +stringData: + POSTGRES_PASSWORD: {{ .Values.migrations.postgres.password | default .Values.secrets.data.POSTGRES_PASSWORD | quote }} +{{- end }} diff --git a/charts/br-slc/templates/mock-nuclea/deployment.yaml b/charts/br-slc/templates/mock-nuclea/deployment.yaml new file mode 100644 index 000000000..b51d664e0 --- /dev/null +++ b/charts/br-slc/templates/mock-nuclea/deployment.yaml @@ -0,0 +1,71 @@ +{{- if .Values.mockNuclea.enabled }} +{{- /* +Núclea clearing-house SIMULATOR — DEV/HML FIXTURE ONLY, never enable in +production/BYOC. Standalone Deployment (own image ghcr.io/lerianstudio/ +br-slc-mock-nuclea, published by the br-slc release pipeline). Safe-by-default +OFF (mockNuclea.enabled=false): like the detached migrations image, a gated +component that defaults ON with an image the registry doesn't have yet would +ImagePullBackOff and block the whole ArgoCD sync. The app reaches it over +cluster DNS (Service br-slc-mock-nuclea:port); wire MOCK_NUCLEA_URL / +NUCLEA_REST_BASE_URL / RSFN_CONSUMER_BASE_URL in the dev/hml gitops overlay. +*/}} +{{- $pullSecrets := .Values.mockNuclea.imagePullSecrets | default .Values.imagePullSecrets }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "br-slc-mock-nuclea.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-slc-mock-nuclea.labels" . | nindent 4 }} +spec: + replicas: 1 + revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + selector: + matchLabels: + {{- include "br-slc-mock-nuclea.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "br-slc-mock-nuclea.labels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + {{- with $pullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: mock-nuclea + image: {{ include "br-slc.mockNucleaImage" . | quote }} + imagePullPolicy: {{ .Values.mockNuclea.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.mockNuclea.service.targetPort }} + protocol: TCP + env: + - name: MOCK_NUCLEA_ADDR + value: {{ printf ":%d" (int .Values.mockNuclea.service.targetPort) | quote }} + {{- /* GOMAXPROCS from the container CFS cpu limit — same + downward-API fix as the app/signer/validator/mqbridge containers + (Gate 8 FIX 7). Guarded with `with`+`if .cpu` so a nil-limit + override renders. */}} + {{- with .Values.mockNuclea.resources.limits }} + {{- if .cpu }} + - name: GOMAXPROCS + valueFrom: + resourceFieldRef: + resource: limits.cpu + divisor: "1" + {{- end }} + {{- end }} + livenessProbe: + {{- include "br-slc.httpProbe" .Values.mockNuclea.livenessProbe | nindent 12 }} + readinessProbe: + {{- include "br-slc.httpProbe" .Values.mockNuclea.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.mockNuclea.resources | nindent 12 }} + securityContext: + {{- include "br-slc.containerSecurityContext" .Values.mockNuclea.securityContext | nindent 12 }} +{{- end }} diff --git a/charts/br-slc/templates/mock-nuclea/service.yaml b/charts/br-slc/templates/mock-nuclea/service.yaml new file mode 100644 index 000000000..df67167a4 --- /dev/null +++ b/charts/br-slc/templates/mock-nuclea/service.yaml @@ -0,0 +1,27 @@ +{{- if .Values.mockNuclea.enabled }} +{{- /* +ClusterIP Service for the mock-nuclea standalone Deployment. DEV/HML FIXTURE +ONLY (never enabled in production/BYOC). Distinct selector from the app's own +Service (br-slc-mock-nuclea.selectorLabels vs br-slc.selectorLabels) so the two +never cross-route. The app reaches this Service over cluster DNS at +br-slc-mock-nuclea: — the actual MOCK_NUCLEA_URL / +NUCLEA_REST_BASE_URL / RSFN_CONSUMER_BASE_URL wiring happens in the dev/hml +gitops overlay, not in this chart's base values. +*/}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "br-slc-mock-nuclea.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-slc-mock-nuclea.labels" . | nindent 4 }} +spec: + type: {{ .Values.mockNuclea.service.type }} + selector: + {{- include "br-slc-mock-nuclea.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.mockNuclea.service.port }} + targetPort: http + protocol: TCP +{{- end }} diff --git a/charts/br-slc/templates/mqbridge-configmap.yaml b/charts/br-slc/templates/mqbridge-configmap.yaml new file mode 100644 index 000000000..c326c89a5 --- /dev/null +++ b/charts/br-slc/templates/mqbridge-configmap.yaml @@ -0,0 +1,12 @@ +{{- if .Values.mqbridge.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "br-slc.fullname" . }}-mqbridge + labels: + {{- include "br-slc.labels" . | nindent 4 }} +data: + {{- range $key, $value := .Values.mqbridge.configmap.data }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/br-slc/templates/mqbridge-secret.yaml b/charts/br-slc/templates/mqbridge-secret.yaml new file mode 100644 index 000000000..227deca67 --- /dev/null +++ b/charts/br-slc/templates/mqbridge-secret.yaml @@ -0,0 +1,24 @@ +{{- /* +Mqbridge-scoped Secret (Decisão 21): MQ credential material for the same-pod +mqbridge container. Only rendered when this chart owns it AND the bridge is +enabled (mqbridge.enabled=true AND mqbridge.secrets.create=true). The BYOC +default (mqbridge.secrets.create=false) references an externally managed Secret +via mqbridge.secrets.existingSecretName instead — see br-slc.mqbridgeSecretName +in _helpers.tpl. Never bake real values into values.yaml; supply them via +--set-string/-f at install time when create=true. +*/ -}} +{{- if .Values.mqbridge.enabled }} +{{- if .Values.mqbridge.secrets.create }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "br-slc.mqbridgeSecretName" . }} + labels: + {{- include "br-slc.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- range $key, $value := .Values.mqbridge.secrets.data }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/br-slc/templates/secrets.yaml b/charts/br-slc/templates/secrets.yaml new file mode 100644 index 000000000..aa7b8a505 --- /dev/null +++ b/charts/br-slc/templates/secrets.yaml @@ -0,0 +1,20 @@ +{{- /* +Only rendered when this chart owns the Secret (secrets.create=true). The BYOC +default (secrets.create=false) references an externally managed Secret via +secrets.existingSecretName instead — see br-slc.secretName in _helpers.tpl and +README.md "Secrets" section. Never bake real values into values.yaml; supply +them via --set-string/-f at install time when create=true. +*/ -}} +{{- if .Values.secrets.create }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "br-slc.secretName" . }} + labels: + {{- include "br-slc.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- range $key, $value := .Values.secrets.data }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/br-slc/templates/service.yaml b/charts/br-slc/templates/service.yaml new file mode 100644 index 000000000..3eb95f36f --- /dev/null +++ b/charts/br-slc/templates/service.yaml @@ -0,0 +1,16 @@ +{{- /* ClusterIP-only Service — no NodePort/LoadBalancer, no Ingress (BYOC scope). */ -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "br-slc.fullname" . }} + labels: + {{- include "br-slc.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "br-slc.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/charts/br-slc/templates/serviceaccount.yaml b/charts/br-slc/templates/serviceaccount.yaml new file mode 100644 index 000000000..12ffdfa1b --- /dev/null +++ b/charts/br-slc/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "br-slc.serviceAccountName" . }} + labels: + {{- include "br-slc.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/charts/br-slc/templates/signer-configmap.yaml b/charts/br-slc/templates/signer-configmap.yaml new file mode 100644 index 000000000..0cf2393f7 --- /dev/null +++ b/charts/br-slc/templates/signer-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "br-slc.fullname" . }}-signer + labels: + {{- include "br-slc.labels" . | nindent 4 }} +data: + SIGNER_PKCS11_TOKEN_LABEL: {{ .Values.signer.pkcs11TokenLabel | quote }} + {{- range $key, $value := .Values.signer.configmap.data }} + {{ $key }}: {{ $value | quote }} + {{- end }} diff --git a/charts/br-slc/templates/signer-secret.yaml b/charts/br-slc/templates/signer-secret.yaml new file mode 100644 index 000000000..7cb1182f0 --- /dev/null +++ b/charts/br-slc/templates/signer-secret.yaml @@ -0,0 +1,22 @@ +{{- /* +Signer-scoped Secret (ADR-9 custody): signing-key material readable ONLY by +the signer container, never the app container. Only rendered when this chart +owns it (signer.secrets.create=true). The BYOC default +(signer.secrets.create=false) references an externally managed Secret via +signer.secrets.existingSecretName instead — see br-slc.signerSecretName in +_helpers.tpl. Never bake real values into values.yaml; supply them via +--set-string/-f at install time when create=true. +*/ -}} +{{- if .Values.signer.secrets.create }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "br-slc.signerSecretName" . }} + labels: + {{- include "br-slc.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- range $key, $value := .Values.signer.secrets.data }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/br-slc/templates/validator-configmap.yaml b/charts/br-slc/templates/validator-configmap.yaml new file mode 100644 index 000000000..50e1b552d --- /dev/null +++ b/charts/br-slc/templates/validator-configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "br-slc.fullname" . }}-validator + labels: + {{- include "br-slc.labels" . | nindent 4 }} +data: + {{- range $key, $value := .Values.xsdValidator.configmap.data }} + {{ $key }}: {{ $value | quote }} + {{- end }} diff --git a/charts/br-slc/values-template.yaml b/charts/br-slc/values-template.yaml new file mode 100644 index 000000000..b3e18ba31 --- /dev/null +++ b/charts/br-slc/values-template.yaml @@ -0,0 +1,55 @@ +# Template values for the br-slc (Sistema de Liquidação Centralizada) chart. +# Copy this file, fill in the values for your environment, and install with -f. +# Postgres, Redis and (optionally) RabbitMQ are EXTERNAL, client-managed +# dependencies in BYOC — this chart does not deploy them. See README.md for the +# full env-var coverage and the Chart Contract. + +imagePullSecrets: + - name: ghcr-credential + +image: + tag: "" # optional — defaults to Chart.appVersion + +# App container Secret. create: true mints the Secret from the values below; +# create: false (recommended for BYOC) references an operator-managed Secret via +# existingSecretName that must expose these same keys. +secrets: + create: false + existingSecretName: "br-slc-secrets" + data: + POSTGRES_PASSWORD: "" # REQUIRED — external Postgres password + REDIS_PASSWORD: "" # REQUIRED when the external Redis needs auth + WEBHOOK_API_KEY: "" + WEBHOOK_HMAC_SECRET: "" + RABBITMQ_DEFAULT_PASS: "" # inert while RABBITMQ_ENABLED=false + AWS_SECRET_ACCESS_KEY: "" # keep empty — prefer IRSA + AWS_SESSION_TOKEN: "" # keep empty — prefer IRSA + +configmap: + data: + POSTGRES_HOST: "" # REQUIRED — external Postgres host + REDIS_HOST: "" # REQUIRED — external Redis host:port + PLUGIN_AUTH_HOST: "" # REQUIRED — Access Manager URL (fails closed at boot if empty while auth enabled) + CORS_ALLOWED_ORIGINS: "" # REQUIRED before go-live — real client origin(s) + +# Signer container Secret (ADR-9 custody: readable ONLY by the slc-signer +# container). Same create/existingSecretName pattern. +signer: + secrets: + create: false + existingSecretName: "br-slc-signer-secrets" + data: + SIGNER_SOFTKEY_PFX_PASSPHRASE: "" # decrypts the softkey PFX (softkey custody path) + +# Conditional inbound IBM MQ / RSFN consumer (Decisão 21). Enable only for the +# IF Domicílio role with RSFN_CONSUMER_TRANSPORT=mq; forces replicaCount=1 and an +# amd64 node pin. Leave disabled otherwise. +mqbridge: + enabled: false + +# Detached migrations. The PreSync Job runs the dedicated br-slc-migrations image. +migrations: + enabled: true + postgres: + host: "" # defaults to configmap.data.POSTGRES_HOST + password: "" # defaults to secrets.data.POSTGRES_PASSWORD diff --git a/charts/br-slc/values.schema.json b/charts/br-slc/values.schema.json new file mode 100644 index 000000000..a10f00930 --- /dev/null +++ b/charts/br-slc/values.schema.json @@ -0,0 +1,531 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "br-slc", + "type": "object", + "required": [ + "image", + "service", + "securityContext", + "resources", + "livenessProbe", + "readinessProbe", + "secrets", + "configmap", + "signer", + "xsdValidator", + "mqbridge", + "migrations", + "mockNuclea" + ], + "properties": { + "replicaCount": { "type": "integer", "minimum": 0 }, + "revisionHistoryLimit": { "type": "integer", "minimum": 0 }, + "nameOverride": { "type": "string" }, + "fullnameOverride": { "type": "string" }, + "terminationGracePeriodSeconds": { "type": "integer", "minimum": 0 }, + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "tag": { "type": "string" }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "imagePullSecrets": { + "type": "array", + "items": { "type": "object" } + }, + "serviceAccount": { + "type": "object", + "required": ["create"], + "properties": { + "create": { "type": "boolean" }, + "name": { "type": "string" }, + "annotations": { "type": "object" }, + "automountServiceAccountToken": { "type": "boolean" } + } + }, + "podAnnotations": { "type": "object" }, + "podLabels": { "type": "object" }, + "service": { + "type": "object", + "required": ["type", "port", "targetPort"], + "properties": { + "type": { "type": "string", "enum": ["ClusterIP"] }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "targetPort": { "type": "integer", "minimum": 1, "maximum": 65535 } + } + }, + "securityContext": { + "type": "object", + "required": [ + "runAsNonRoot", + "runAsUser", + "runAsGroup", + "allowPrivilegeEscalation", + "readOnlyRootFilesystem", + "capabilities", + "seccompProfile" + ], + "properties": { + "runAsNonRoot": { "type": "boolean", "const": true }, + "runAsUser": { "type": "integer" }, + "runAsGroup": { "type": "integer" }, + "allowPrivilegeEscalation": { "type": "boolean", "const": false }, + "readOnlyRootFilesystem": { "type": "boolean", "const": true }, + "capabilities": { + "type": "object", + "required": ["drop"], + "properties": { + "drop": { + "type": "array", + "items": { "type": "string" }, + "contains": { "const": "ALL" } + } + } + }, + "seccompProfile": { + "type": "object", + "required": ["type"], + "properties": { + "type": { "type": "string", "const": "RuntimeDefault" } + } + } + } + }, + "resources": { + "type": "object", + "properties": { + "limits": { "type": "object" }, + "requests": { "type": "object" } + } + }, + "livenessProbe": { + "type": "object", + "properties": { + "httpGet": { + "type": "object", + "required": ["path", "port"], + "properties": { + "path": { "type": "string" }, + "port": { "type": ["string", "integer"] } + } + }, + "initialDelaySeconds": { "type": "integer", "minimum": 0 }, + "periodSeconds": { "type": "integer", "minimum": 1 }, + "timeoutSeconds": { "type": "integer", "minimum": 1 }, + "failureThreshold": { "type": "integer", "minimum": 1 } + } + }, + "readinessProbe": { + "type": "object", + "properties": { + "httpGet": { + "type": "object", + "required": ["path", "port"], + "properties": { + "path": { "type": "string" }, + "port": { "type": ["string", "integer"] } + } + }, + "initialDelaySeconds": { "type": "integer", "minimum": 0 }, + "periodSeconds": { "type": "integer", "minimum": 1 }, + "timeoutSeconds": { "type": "integer", "minimum": 1 }, + "failureThreshold": { "type": "integer", "minimum": 1 } + } + }, + "extraVolumes": { "type": "array" }, + "extraVolumeMounts": { "type": "array" }, + "secrets": { + "type": "object", + "required": ["create", "data"], + "if": { "properties": { "create": { "const": false } } }, + "then": { "required": ["existingSecretName"], "properties": { "existingSecretName": { "minLength": 1 } } }, + "properties": { + "create": { "type": "boolean" }, + "existingSecretName": { "type": "string" }, + "data": { + "type": "object", + "required": [ + "POSTGRES_PASSWORD", + "REDIS_PASSWORD", + "WEBHOOK_API_KEY", + "WEBHOOK_HMAC_SECRET", + "RABBITMQ_DEFAULT_PASS", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN" + ], + "additionalProperties": { "type": "string" } + } + } + }, + "configmap": { + "type": "object", + "required": ["data"], + "properties": { + "data": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + }, + "signer": { + "type": "object", + "required": ["image", "resources", "securityContext", "livenessProbe", "readinessProbe", "softkeySecret", "secrets", "configmap"], + "properties": { + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "tag": { "type": "string" }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "resources": { + "type": "object", + "properties": { + "limits": { "type": "object" }, + "requests": { "type": "object" } + } + }, + "securityContext": { + "type": "object", + "required": ["runAsNonRoot", "allowPrivilegeEscalation", "readOnlyRootFilesystem", "capabilities", "seccompProfile"], + "properties": { + "runAsNonRoot": { "type": "boolean", "const": true }, + "runAsUser": { "type": "integer" }, + "runAsGroup": { "type": "integer" }, + "allowPrivilegeEscalation": { "type": "boolean", "const": false }, + "readOnlyRootFilesystem": { "type": "boolean", "const": true }, + "capabilities": { + "type": "object", + "required": ["drop"], + "properties": { + "drop": { + "type": "array", + "items": { "type": "string" }, + "contains": { "const": "ALL" } + } + } + }, + "seccompProfile": { + "type": "object", + "required": ["type"], + "properties": { "type": { "type": "string", "const": "RuntimeDefault" } } + } + } + }, + "livenessProbe": { "type": "object" }, + "readinessProbe": { "type": "object" }, + "softkeySecret": { + "type": "object", + "required": ["enabled"], + "properties": { + "enabled": { "type": "boolean" }, + "secretName": { "type": "string" }, + "secretKey": { "type": "string" }, + "mountPath": { "type": "string" } + } + }, + "secrets": { + "type": "object", + "required": ["create", "data"], + "if": { "properties": { "create": { "const": false } } }, + "then": { "required": ["existingSecretName"], "properties": { "existingSecretName": { "minLength": 1 } } }, + "properties": { + "create": { "type": "boolean" }, + "existingSecretName": { "type": "string" }, + "data": { + "type": "object", + "required": [ + "SIGNER_SOFTKEY_PFX_PASSPHRASE", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", + "SIGNER_VAULT_TOKEN", + "SIGNER_PKCS11_PIN" + ], + "additionalProperties": { "type": "string" } + } + } + }, + "configmap": { + "type": "object", + "required": ["data"], + "properties": { + "data": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + } + } + }, + "xsdValidator": { + "type": "object", + "required": ["image", "resources", "securityContext", "livenessProbe", "readinessProbe", "configmap"], + "properties": { + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "tag": { "type": "string" }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "resources": { + "type": "object", + "properties": { + "limits": { "type": "object" }, + "requests": { "type": "object" } + } + }, + "securityContext": { + "type": "object", + "required": ["runAsNonRoot", "allowPrivilegeEscalation", "readOnlyRootFilesystem", "capabilities", "seccompProfile"], + "properties": { + "runAsNonRoot": { "type": "boolean", "const": true }, + "runAsUser": { "type": "integer" }, + "runAsGroup": { "type": "integer" }, + "allowPrivilegeEscalation": { "type": "boolean", "const": false }, + "readOnlyRootFilesystem": { "type": "boolean", "const": true }, + "capabilities": { + "type": "object", + "required": ["drop"], + "properties": { + "drop": { + "type": "array", + "items": { "type": "string" }, + "contains": { "const": "ALL" } + } + } + }, + "seccompProfile": { + "type": "object", + "required": ["type"], + "properties": { "type": { "type": "string", "const": "RuntimeDefault" } } + } + } + }, + "livenessProbe": { "type": "object" }, + "readinessProbe": { "type": "object" }, + "configmap": { + "type": "object", + "required": ["data"], + "properties": { + "data": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + } + } + }, + "mqbridge": { + "type": "object", + "required": ["enabled", "image", "resources", "securityContext", "livenessProbe", "keystoreSecret", "secrets", "configmap"], + "properties": { + "enabled": { "type": "boolean" }, + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "tag": { "type": "string" }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "resources": { + "type": "object", + "properties": { + "limits": { "type": "object" }, + "requests": { "type": "object" } + } + }, + "securityContext": { + "type": "object", + "required": ["runAsNonRoot", "allowPrivilegeEscalation", "capabilities", "seccompProfile"], + "properties": { + "runAsNonRoot": { "type": "boolean", "const": true }, + "runAsUser": { "type": "integer" }, + "runAsGroup": { "type": "integer" }, + "allowPrivilegeEscalation": { "type": "boolean", "const": false }, + "capabilities": { + "type": "object", + "required": ["drop"], + "properties": { + "drop": { + "type": "array", + "items": { "type": "string" }, + "contains": { "const": "ALL" } + } + } + }, + "seccompProfile": { + "type": "object", + "required": ["type"], + "properties": { "type": { "type": "string", "const": "RuntimeDefault" } } + } + } + }, + "livenessProbe": { "type": "object" }, + "keystoreSecret": { + "type": "object", + "required": ["enabled"], + "properties": { + "enabled": { "type": "boolean" }, + "secretName": { "type": "string" }, + "mountPath": { "type": "string" }, + "stem": { "type": "string" } + } + }, + "secrets": { + "type": "object", + "required": ["create", "data"], + "if": { "properties": { "create": { "const": false } } }, + "then": { "required": ["existingSecretName"], "properties": { "existingSecretName": { "minLength": 1 } } }, + "properties": { + "create": { "type": "boolean" }, + "existingSecretName": { "type": "string" }, + "data": { + "type": "object", + "required": ["MQ_BRIDGE_MQ_PASSWORD"], + "additionalProperties": { "type": "string" } + } + } + }, + "configmap": { + "type": "object", + "required": ["data"], + "properties": { + "data": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + } + } + }, + "migrations": { + "type": "object", + "required": ["enabled", "image"], + "if": { "properties": { "useExistingSecret": { "const": true } }, "required": ["useExistingSecret"] }, + "then": { "required": ["existingSecretName"], "properties": { "existingSecretName": { "minLength": 1 } } }, + "properties": { + "enabled": { "type": "boolean" }, + "backoffLimit": { "type": "integer", "minimum": 0 }, + "activeDeadlineSeconds": { "type": "integer", "minimum": 1 }, + "ttlSecondsAfterFinished": { "type": "integer", "minimum": 0 }, + "useExistingSecret": { "type": "boolean" }, + "existingSecretName": { "type": "string" }, + "postgres": { + "type": "object", + "properties": { + "host": { "type": "string" }, + "port": { "type": "string" }, + "user": { "type": "string" }, + "database": { "type": "string" }, + "sslMode": { "type": "string" }, + "password": { "type": "string" } + } + }, + "image": { + "type": "object", + "required": ["repository"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "tag": { "type": "string" }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "resources": { + "type": "object", + "properties": { + "limits": { "type": "object" }, + "requests": { "type": "object" } + } + }, + "waitForPostgres": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "image": { + "type": "object", + "properties": { + "repository": { "type": "string" }, + "tag": { "type": "string" } + } + } + } + } + } + }, + "mockNuclea": { + "type": "object", + "required": ["enabled", "image", "service", "resources", "securityContext", "livenessProbe", "readinessProbe"], + "properties": { + "enabled": { "type": "boolean" }, + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "tag": { "type": "string" }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "imagePullSecrets": { + "type": "array", + "items": { "type": "object" } + }, + "service": { + "type": "object", + "required": ["type", "port", "targetPort"], + "properties": { + "type": { "type": "string", "enum": ["ClusterIP"] }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "targetPort": { "type": "integer", "minimum": 1, "maximum": 65535 } + } + }, + "resources": { + "type": "object", + "properties": { + "limits": { "type": "object" }, + "requests": { "type": "object" } + } + }, + "securityContext": { + "type": "object", + "required": ["runAsNonRoot", "allowPrivilegeEscalation", "readOnlyRootFilesystem", "capabilities", "seccompProfile"], + "properties": { + "runAsNonRoot": { "type": "boolean", "const": true }, + "runAsUser": { "type": "integer" }, + "runAsGroup": { "type": "integer" }, + "allowPrivilegeEscalation": { "type": "boolean", "const": false }, + "readOnlyRootFilesystem": { "type": "boolean", "const": true }, + "capabilities": { + "type": "object", + "required": ["drop"], + "properties": { + "drop": { + "type": "array", + "items": { "type": "string" }, + "contains": { "const": "ALL" } + } + } + }, + "seccompProfile": { + "type": "object", + "required": ["type"], + "properties": { "type": { "type": "string", "const": "RuntimeDefault" } } + } + } + }, + "livenessProbe": { "type": "object" }, + "readinessProbe": { "type": "object" } + } + } + } +} diff --git a/charts/br-slc/values.yaml b/charts/br-slc/values.yaml new file mode 100644 index 000000000..231beb876 --- /dev/null +++ b/charts/br-slc/values.yaml @@ -0,0 +1,933 @@ +# br-slc Helm values — BYOC single-tenant BASE. +# +# This is the hardened, safe-by-default base. Per-environment overrides (dev/hml +# relaxations, SaaS-only knobs) live in the beleriand gitops overlays (Task +# 6.0.6) — do NOT add values-.yaml files to this chart. +# +# Scope: single tenant, ClusterIP-only Service, no Ingress. Postgres/Redis/ +# RabbitMQ are EXTERNAL, client-managed dependencies in BYOC (this chart does +# NOT deploy them) — set configmap.data.POSTGRES_HOST / REDIS_HOST (and the +# RabbitMQ host, if RABBITMQ_ENABLED=true) to your real endpoints before +# deploying. See README.md for the full env var coverage table. + +replicaCount: 1 +revisionHistoryLimit: 5 + +nameOverride: "" +fullnameOverride: "" +namespaceOverride: "" + +# Non-secret app config emitted into the app ConfigMap but kept OUT of +# configmap.data because their env-var names contain the substring "_token_" +# (a PKCS11 token label and a token-cache TTL — neither is a credential), +# which the Lerian helm-chart-standard secret-key heuristic would otherwise +# flag. The ConfigMap template wires them under their real env-var names. +webhookNotificationsCacheTtlSec: "60" +# WEBHOOK_HMAC_SECRET_FILE — optional mounted-Secret file PATH (not a secret +# value). Kept out of configmap.data because its name contains "secret". Empty +# by default; set it together with extraVolumes/extraVolumeMounts if you prefer +# the mounted-volume rotation pattern over the inline WEBHOOK_HMAC_SECRET var. +webhookHmacSecretFile: "" + +image: + repository: ghcr.io/lerianstudio/br-slc + # Empty defaults to .Chart.AppVersion. ASSUMPTION: reconcile the real + # published repository/tag with Task 6.0.3 (sidecar + app CI image publish). + tag: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +serviceAccount: + create: true + name: "" + # Templatable for IRSA (EKS) / Workload Identity (GKE) / KMS access. + # Example (EKS IRSA, so the app can use AWS_REGION + attached role instead of + # AWS_ACCESS_KEY_ID/SECRET/SESSION_TOKEN — those stay empty in production): + # eks.amazonaws.com/role-arn: arn:aws:iam:::role/ + annotations: {} + # Gate 8 FIX 3: this workload never talks to the Kubernetes API, so the + # projected SA token is pure unused attack surface if the pod is ever + # compromised. Override to true only if you attach a controller/sidecar that + # genuinely needs the K8s API (e.g. IRSA's own token projection is separate + # from this and unaffected by disabling automount here). + automountServiceAccountToken: false + +podAnnotations: {} +podLabels: {} + +service: + type: ClusterIP + port: 4111 + targetPort: 4111 + +# Mirrors deployments/helm/slc-signer's hardened posture exactly (Decision 9 +# sidecar precedent): non-root, no privilege escalation, read-only root fs, all +# capabilities dropped, RuntimeDefault seccomp. /tmp is writable via emptyDir +# (see deployment.yaml) since readOnlyRootFilesystem is true. +securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + +resources: + limits: + cpu: "1" + memory: 512Mi + requests: + cpu: 250m + memory: 256Mi + +# Liveness: process viability only (no dependency checks) — /health. +livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + +# Readiness: dependency-aware — /readyz. More lenient than liveness: the app +# has a `draining` state during graceful shutdown (readyz flips 503 first, so +# it must not be so aggressive that it flaps during normal dependency hiccups). +readinessProbe: + httpGet: + path: /readyz + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + +# terminationGracePeriodSeconds must stay comfortably above any +# STREAMING_CLOSE_TIMEOUT_S / outbox drain the app performs on SIGTERM during +# the readyz `draining` window. +terminationGracePeriodSeconds: 30 + +# Extra volumes/mounts for chart consumers who need to attach e.g. a custom CA +# bundle or a mounted Secret for WEBHOOK_API_KEY_FILE/WEBHOOK_HMAC_SECRET_FILE. +extraVolumes: [] +extraVolumeMounts: [] + +# --------------------------------------------------------------------------- +# Secrets (the app container's 7 secret-bearing vars — NEVER real values here). +# --------------------------------------------------------------------------- +# NOTE (ADR-12 same-pod consolidation): SIGNER_SOFTKEY_PFX_PASSPHRASE is NOT +# here — it is signing-key material and lives ONLY in the signer container's +# own Secret (signer.secrets.data below), never readable by this app +# container (ADR-9 custody boundary). +# create: false (default) — recommended for BYOC: the client manages this +# Secret out-of-band (their own K8s Secret, External Secrets Operator, Vault +# injector, etc.) and existingSecretName points at it. The Secret MUST expose +# these SAME 7 keys (override the chart's data keys to match if your Secret +# uses different names — see README). +# create: true — this chart creates the Secret from secrets.data via +# --set-string/-f overrides at install time (still never commit real values +# to this file). +secrets: + create: false + # Non-empty by default (schema requires it when create=false): the BYOC + # operator creates a Secret with THIS name exposing the 7 keys below. When + # create=true the chart names its own Secret with this value instead. + existingSecretName: "br-slc-secrets" + data: + POSTGRES_PASSWORD: "" + REDIS_PASSWORD: "" + WEBHOOK_API_KEY: "" + WEBHOOK_HMAC_SECRET: "" + # Moved out of the ConfigMap (Task 6.0.2 reconciliation): these three are + # credential material per Lerian's values.md ConfigMap-vs-Secrets + # classification and must not live in a ConfigMap. Empty by default — + # RABBITMQ_ENABLED is false in this base, so RABBITMQ_DEFAULT_PASS is + # inert until an overlay enables RabbitMQ for real; the two AWS_* vars + # stay empty in production per the "AWS credentials" note below (prefer + # IRSA via serviceAccount.annotations instead). + RABBITMQ_DEFAULT_PASS: "" + AWS_SECRET_ACCESS_KEY: "" + AWS_SESSION_TOKEN: "" + +# --------------------------------------------------------------------------- +# ConfigMap — ALL non-secret env vars. +# --------------------------------------------------------------------------- +# 1:1 with config/.env.example's active surface (156 vars) minus the 7 secret +# vars above, PLUS two vars this hardened base's own defaults require that are +# NOT in .env.example's active section: +# - DEPLOYMENT_MODE: read by config_validation.go / systemplane.go / readyz +# TLS enforcement but only present in .env.example as documentation (no +# active line) — defaults to "byoc" here (see internal/bootstrap/config.go +# SigningConfig.DeploymentMode). +# - PLUGIN_AUTH_HOST: .env.example only has it commented out +# (# PLUGIN_AUTH_HOST=http://localhost:4000) because PLUGIN_AUTH_ENABLED +# defaults to false there. This base flips PLUGIN_AUTH_ENABLED to true +# (real auth in BYOC), and config_validation.go's validateAuthConfig FAILS +# BOOT when PLUGIN_AUTH_ENABLED=true and PLUGIN_AUTH_HOST is empty — so it +# MUST be set. Left empty by design: this chart will fail closed at boot +# until the operator/overlay sets the real Access Manager URL. See README. +# +# See README.md "Env Var Coverage" for the full 156/156 mapping table. +configmap: + data: + # --- Application Settings --- + ENV_NAME: "production" + LOG_LEVEL: "info" + SERVER_ADDRESS: ":4111" + HTTP_BODY_LIMIT_BYTES: "104857600" + DEPLOYMENT_MODE: "byoc" + + # --- CORS --- MUST override CORS_ALLOWED_ORIGINS with the real client + # origin(s) before go-live; empty is a safe deny-all default (harmless + # combined with ALLOW_CORS_WILDCARD=false below). + CORS_ALLOWED_ORIGINS: "" + CORS_ALLOWED_METHODS: "GET,POST,PUT,PATCH,DELETE,OPTIONS" + CORS_ALLOWED_HEADERS: "Origin,Content-Type,Accept,Authorization,X-Request-ID" + CORS_EXPOSE_HEADERS: "" + CORS_ALLOW_CREDENTIALS: "false" + + # --- TLS / hardening bypass flags --- + # ASSUMPTION: TLS_TERMINATED_UPSTREAM depends on the client's real BYOC + # network topology (mesh mTLS vs. an in-front proxy/LB terminating TLS); + # this chart has no Ingress, so it is left false here — flip per + # environment in the beleriand overlay once the topology is confirmed. + TLS_TERMINATED_UPSTREAM: "false" + # These three MUST stay false in production (config_validation.go rejects + # boot when ENV_NAME=production and ALLOW_CORS_WILDCARD or + # ALLOW_RATELIMIT_FAIL_OPEN is true; ALLOW_INSECURE_TLS is the sovereign + # data-store TLS switch and is kept false here as the safe default too). + ALLOW_INSECURE_TLS: "false" + ALLOW_CORS_WILDCARD: "false" + ALLOW_RATELIMIT_FAIL_OPEN: "false" + + # --- Multi-Tenant Starter (tenant-manager) --- single-tenant BYOC: off. + MULTI_TENANT_ENABLED: "false" + MULTI_TENANT_REDIS_PORT: "6379" + MULTI_TENANT_REDIS_TLS: "false" + MULTI_TENANT_MAX_TENANT_POOLS: "100" + MULTI_TENANT_IDLE_TIMEOUT_SEC: "300" + MULTI_TENANT_TIMEOUT: "30" + MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD: "5" + MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC: "30" + MULTI_TENANT_CACHE_TTL_SEC: "120" + MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC: "30" + + # --- PostgreSQL Primary Database --- EXTERNAL/client-managed in BYOC. + # ASSUMPTION: POSTGRES_HOST is left empty — MUST be overridden with the + # real Postgres endpoint before deploy (this chart does not ship a + # postgresql subchart dependency per Task 6.0.1 scope). + POSTGRES_HOST: "" + POSTGRES_PORT: "5432" + POSTGRES_USER: "br-slc" + POSTGRES_NAME: "br-slc" + # Hardened DEVIATION from .env.example's dev default (disable): + # config_validation.go's validateProductionConfig REJECTS + # POSTGRES_SSLMODE=disable when ENV_NAME=production, so the BYOC base + # default must not be "disable". "require" verifies encryption without + # requiring a client-supplied CA bundle; tighten to verify-full once the + # client's Postgres cert chain is known. + POSTGRES_SSLMODE: "require" + MIGRATIONS_PATH: "migrations" + SYSTEMPLANE_ENABLED: "false" + TENANT_RUNTIME_CONFIG_ENABLED: "false" + POSTGRES_MAX_OPEN_CONNS: "25" + POSTGRES_MAX_IDLE_CONNS: "5" + POSTGRES_CONN_MAX_LIFETIME_MINS: "30" + POSTGRES_CONN_MAX_IDLE_TIME_MINS: "5" + POSTGRES_CONNECT_TIMEOUT_SEC: "10" + + # --- Infrastructure Boot Timeout --- + INFRA_CONNECT_TIMEOUT_SEC: "30" + + # --- Redis --- EXTERNAL/client-managed in BYOC (used regardless of + # MULTI_TENANT_ENABLED — idempotency/cache). MUST override REDIS_HOST. + REDIS_HOST: "" + REDIS_DB: "0" + REDIS_PROTOCOL: "3" + REDIS_POOL_SIZE: "10" + REDIS_MIN_IDLE_CONNS: "2" + REDIS_READ_TIMEOUT: "3" + REDIS_WRITE_TIMEOUT: "3" + REDIS_DIAL_TIMEOUT: "5" + REDIS_POOL_TIMEOUT: "2" + REDIS_MAX_RETRIES: "3" + REDIS_MIN_RETRY_BACKOFF: "8" + REDIS_MAX_RETRY_BACKOFF: "1" + + # --- Event-Driven Starter - Circuit Breaker --- + CIRCUIT_BREAKER_ENABLED: "false" + + # --- Event-Driven Starter - RabbitMQ --- off by default (readyz-probe-only + # dependency, not the main event path — lib-streaming/outbox owns that). + # Host/user below are inert while RABBITMQ_ENABLED=false; mirrored from + # .env.example rather than blanked because they have no effect disabled. + # RABBITMQ_DEFAULT_PASS moved to secrets.data (Task 6.0.2 reconciliation — + # it is credential material per Lerian's values.md classification and + # must not live in a ConfigMap; empty by default, operator injects if + # RabbitMQ is ever enabled for real in an overlay). + RABBITMQ_ENABLED: "false" + RABBITMQ_HOST: "localhost" + RABBITMQ_PORT_AMQP: "5672" + RABBITMQ_PORT_HOST: "15672" + RABBITMQ_DEFAULT_USER: "guest" + RABBITMQ_VHOST: "/" + RABBITMQ_REQUIRE_HEALTH_ALLOWED_HOSTS: "false" + RABBITMQ_ALLOW_INSECURE_HEALTH_CHECK: "false" + RABBITMQ_ALLOW_INSECURE_TLS: "false" + + # --- Event-Driven Starter - Outbox --- REQUIRED true: the operations + # context (POST /v1/operations, cancellation) fails closed at boot without + # the outbox repository wired. + OUTBOX_ENABLED: "true" + OUTBOX_TABLE_NAME: "outbox_events" + OUTBOX_DISPATCH_INTERVAL_SEC: "2" + OUTBOX_BATCH_SIZE: "50" + OUTBOX_PUBLISH_MAX_ATTEMPTS: "3" + OUTBOX_PUBLISH_BACKOFF_MS: "200" + OUTBOX_RETRY_WINDOW_SEC: "300" + OUTBOX_MAX_DISPATCH_ATTEMPTS: "10" + OUTBOX_PROCESSING_TIMEOUT_SEC: "600" + OUTBOX_MAX_FAILED_PER_BATCH: "25" + OUTBOX_INCLUDE_TENANT_METRICS: "false" + OUTBOX_ALLOW_EMPTY_TENANT: "true" + + # --- Streaming Starter (lib-streaming outbox relay -> Kafka) --- opt-in; + # needs STREAMING_BROKERS (not in the active .env.example surface) set via + # an overlay before flipping this to true. + STREAMING_ENABLED: "false" + STREAMING_HEALTH_CHECK_TIMEOUT: "2s" + + # --- Authentication (lib-auth + plugin-auth) --- hardened default: real + # auth ON in BYOC. See PLUGIN_AUTH_HOST note above the configmap block — + # this chart intentionally fails closed at boot until PLUGIN_AUTH_HOST is + # set to the real Access Manager endpoint. + PLUGIN_AUTH_ENABLED: "true" + PLUGIN_AUTH_HOST: "" + + # --- API Documentation (Swagger) --- + SWAGGER_ENABLED: "true" + SWAGGER_TITLE: "BrSlc" + SWAGGER_VERSION: "1.0.0" + SWAGGER_BASE_PATH: "/" + SWAGGER_LEFT_DELIM: "{{" + SWAGGER_RIGHT_DELIM: "}}" + + # --- OpenTelemetry --- hardened default: telemetry ON in BYOC. + # ASSUMPTION: OTEL_EXPORTER_OTLP_ENDPOINT assumes an in-cluster collector + # named "otel-collector-lerian" (the -helm-suffix-exception chart per + # ring:helm conventions) on the standard OTLP gRPC port; reconcile with the + # client's actual collector Service/release name in the beleriand overlay. + ENABLE_TELEMETRY: "true" + OTEL_LIBRARY_NAME: "br-slc" + OTEL_RESOURCE_SERVICE_NAME: "br-slc" + OTEL_RESOURCE_SERVICE_VERSION: "1.0.0" + OTEL_EXPORTER_OTLP_ENDPOINT: "otel-collector-lerian:4317" + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "production" + DB_METRICS_INTERVAL_SEC: "15" + + # --- Rate Limiting --- + RATE_LIMIT_ENABLED: "true" + RATE_LIMIT_MAX: "500" + RATE_LIMIT_WINDOW_SEC: "60" + AGGRESSIVE_RATE_LIMIT_MAX: "100" + AGGRESSIVE_RATE_LIMIT_WINDOW_SEC: "60" + RELAXED_RATE_LIMIT_MAX: "1000" + RELAXED_RATE_LIMIT_WINDOW_SEC: "60" + + # --- Idempotency --- + IDEMPOTENCY_RETRY_WINDOW_SEC: "300" + + # --- AWS Credentials (shared: slc-signer awskms adapter + M2M provider) + # --- MUST stay empty in production: use IRSA (serviceAccount.annotations + # above) instead of static credentials. AWS_REGION defaults to the SLC + # staging KMS region — confirm/override per real deployment. + # AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN moved to secrets.data (Task + # 6.0.2 reconciliation — credential material per Lerian's values.md + # classification). AWS_ACCESS_KEY_ID stays here: it is the non-secret + # half of a static credential pair (analogous to *_USER vars); still + # empty by default — prefer IRSA over any static AWS credentials. + AWS_REGION: "us-east-2" + AWS_ACCESS_KEY_ID: "" + + # --- M2M Credentials --- + M2M_CREDENTIAL_CACHE_TTL_SEC: "300" + + # --- Pagination --- + MAX_PAGINATION_LIMIT: "100" + MAX_PAGINATION_MONTH_DATE_RANGE: "3" + + # --- Operations Intake --- + INTAKE_BATCH_MAX_LINES: "50000" + + # --- XSD Validator Sidecar (aslc-xsd-validator — Decision 12 / ADR-12) --- + # SAME-POD container (ADR-12: "sidecar no mesmo pod, localhost"): the app + # reaches the validator over localhost, NOT cluster DNS. 9091 is the + # validator's containerPort inside this shared pod. See the top-level + # xsdValidator: section for the container's own image/config. + XSD_VALIDATOR_URL: "http://localhost:9091" + XSD_VALIDATOR_TIMEOUT_SEC: "10" + RSFN_INBOUND_XSD_VALIDATION_ENABLED: "true" + + # --- Signing Sidecar (slc-signer — Decision 9 / ADR-12) --- + # SAME-POD container: the app reaches the signer over localhost, NOT + # cluster DNS. 9101 is the signer's containerPort inside this shared pod. + # Custody (ADR-9): the signing key material (softkey PFX + passphrase, + # HSM PIN, Vault token) lives ONLY in the signer container — its own + # Secret and softkey volume are never mounted on this app container. See + # the top-level signer: section. + SIGNER_URL: "http://localhost:9101" + SIGNER_TIMEOUT_SEC: "10" + + # --- Mock Núclea (local credit-flow simulator) --- DEV/TEST FIXTURE ONLY; + # not consumed by the app in this phase. Left empty in BYOC/production. + MOCK_NUCLEA_URL: "" + + # --- Núclea REST online-submission transport (E-2.4) --- transport stays + # disabled until NUCLEA_REST_BASE_URL (not in the active .env.example + # surface) is set via an overlay. + NUCLEA_REST_TIMEOUT_SEC: "30" + NUCLEA_REST_CB_CONSECUTIVE_FAILURES: "5" + NUCLEA_REST_CB_OPEN_TIMEOUT_SEC: "30" + NUCLEA_REST_ALLOW_INSECURE_TLS: "false" + + # --- RSFN inbound consumer (IBM MQ external channel, E-3.3) --- + RSFN_CONSUMER_TRANSPORT: "stub" + RSFN_CONSUMER_TIMEOUT_SEC: "30" + RSFN_CONSUMER_CB_CONSECUTIVE_FAILURES: "5" + RSFN_CONSUMER_CB_OPEN_TIMEOUT_SEC: "30" + RSFN_CONSUMER_ALLOW_INSECURE_TLS: "false" + + # --- MQ Bridge sidecar (mqbridge) --- used only when + # RSFN_CONSUMER_TRANSPORT=mq. SAME-POD CONDITIONAL container (ADR-12 + + # Decisão 21): default off, rendered only when mqbridge.enabled=true (BYOC + # role IF Domicílio). When enabled it co-lives with the monolith in this + # pod, so the app reaches it over localhost, NOT cluster DNS — this resolves + # the drain/ack affinity risk by construction (same process, localhost). It + # keeps a writable /var/mqm rootfs + an amd64 pod pin (libmqm/GSKit); see the + # top-level mqbridge: section. Enable it in the same overlay/release that + # flips this transport, once a real/HML IBM MQ broker + GSKit keystore are + # ready. + MQ_BRIDGE_URL: "http://localhost:9121" + MQ_BRIDGE_TIMEOUT_SEC: "10" + MQ_BRIDGE_MAX_DRAIN: "16" + + # --- Schedule — credit-window dispatch gate + STR grid (E-2.4/E-3.2) --- + # v0 window + worker: kept at .env.example's real (non-dev-bypass) + # defaults. All the provisional per-driver toggles below default to false + # exactly as .env.example ships them (each needs per-tenant Núclea + # recipient/custody material from E-4.2 before it is safe to enable — see + # config.go doc comments) — this base does not flip any of them on. + SCHEDULE_CREDIT_WINDOW_OPEN: "00:30" + SCHEDULE_CREDIT_WINDOW_CLOSE: "17:30" + SCHEDULE_WORKER_ENABLED: "true" + SCHEDULE_WORKER_INTERVAL_SEC: "30" + SCHEDULE_CUTOFF_ALERT_LEAD_MIN: "30" + SCHEDULE_GRID_GATE_ENABLED: "false" + SCHEDULE_HOLIDAYS: "" + SCHEDULE_DISPATCH_ENABLED: "false" + SCHEDULE_DISPATCH_PAGE_SIZE: "1000" + SCHEDULE_RETURN_POLL_ENABLED: "false" + SCHEDULE_RELAY_STATUS_ENABLED: "false" + SCHEDULE_RSFN_INGEST_ENABLED: "false" + SCHEDULE_D1_CONFIRMATION_ENABLED: "false" + SCHEDULE_D1_CONFIRMATION_CUTOFF: "19:00" + SCHEDULE_SILOC_WINDOWS_ENABLED: "false" + SCHEDULE_SILOC_DEPOSIT_CYCLE1_CUTOFF: "07:30" + SCHEDULE_SILOC_DEPOSIT_CYCLE2_CUTOFF: "12:50" + SCHEDULE_STR_SETTLED_ENABLED: "false" + + # --- Webhook delivery (BYOC `direct` provider — E-3.5) --- + # WEBHOOK_API_KEY_FILE/WEBHOOK_HMAC_SECRET_FILE are optional mounted-Secret + # file-path variants; left empty here since this chart wires the inline + # WEBHOOK_API_KEY/WEBHOOK_HMAC_SECRET vars via secretKeyRef instead (see + # secrets.data above). Set extraVolumes/extraVolumeMounts + these two paths + # if you prefer the mounted-volume rotation pattern. + WEBHOOK_API_KEY_FILE: "" + # WEBHOOK_HMAC_SECRET_FILE is set via the top-level webhookHmacSecretFile + # value (see the note there). + WEBHOOK_DELIVERY_TIMEOUT_SEC: "10" + WEBHOOK_WORKER_ENABLED: "false" + WEBHOOK_WORKER_INTERVAL_SEC: "30" + WEBHOOK_DELIVERY_PROVIDER: "direct" + WEBHOOK_NOTIFICATIONS_BASE_URL: "" + # WEBHOOK_NOTIFICATIONS_TOKEN_CACHE_TTL_SEC is set via the top-level + # webhookNotificationsCacheTtlSec value (see the note there). + WEBHOOK_MAX_ATTEMPTS: "6" + WEBHOOK_BACKOFF_BASE_SEC: "30" + WEBHOOK_CONSUME_BATCH: "100" + +# =========================================================================== +# Same-pod sidecar containers (ADR-12). signer + validator run as ADDITIONAL +# containers in THIS Deployment's pod (not separate Deployments/Services), so +# the app talks to them over localhost (see SIGNER_URL/XSD_VALIDATOR_URL). Both +# are MANDATORY (no enable toggle): the validator is a fail-closed hard +# dependency for all outbound ASLC, and the signer signs every batch. Shared +# fate: the pod only reaches Ready when all three containers pass readiness. +# =========================================================================== + +# --- slc-signer container (Decision 9 / ADR-9 crypto+credential boundary) --- +signer: + # SIGNER_PKCS11_TOKEN_LABEL, wired into the signer ConfigMap under its real + # env-var name. Kept here (not under signer.configmap.data) because the name + # contains "_token_" and would otherwise trip the secret-key heuristic; it is + # a pkcs11 token *label*, not a credential. Empty unless a pkcs11 token is set. + pkcs11TokenLabel: "" + image: + repository: ghcr.io/lerianstudio/br-slc-signer + # Empty defaults to .Chart.AppVersion (see br-slc.signerImage in + # _helpers.tpl). ASSUMPTION: reconcile the published repo/tag with Task + # 6.0.3 (sidecar image publish). + tag: "" + pullPolicy: IfNotPresent + resources: + limits: + cpu: "1" + memory: 256Mi + requests: + cpu: 100m + memory: 64Mi + # Per-container hardened posture (identical to the app container): non-root, + # no privilege escalation, read-only root fs (its own /tmp emptyDir), all + # capabilities dropped, RuntimeDefault seccomp. + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + # Both probes hit the same /health:9101 (no separate readyz for the signer). + livenessProbe: + httpGet: + path: /health + port: 9101 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 9101 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + # Softkey PFX bundle (BYOC-first custody adapter). The .pfx/.p12 FILE is + # mounted read-only from an externally managed Secret ONTO THE SIGNER + # CONTAINER ONLY (never the app container — ADR-9 custody). The passphrase + # that decrypts it is the separate signer.secrets.SIGNER_SOFTKEY_PFX_PASSPHRASE. + # + # REQUIRED before the signer can become Ready: with the softkey adapter + # (SIGNER_SOFTKEY_PFX_PATH set below) the operator MUST set enabled=true and + # a valid secretName so /certs/signer.pfx is mounted — otherwise the signer + # has no key material and the pod never reaches Ready. Left disabled by + # default (fail-closed, like POSTGRES_HOST/PLUGIN_AUTH_HOST) because the chart + # cannot ship a real Secret name for BYOC key custody; enable it (or switch to + # an awskms/pkcs11/vault adapter) at install time. + softkeySecret: + enabled: false + # Existing Secret whose key holds the PFX bytes, e.g.: + # kubectl create secret generic slc-signer-softkey --from-file=signer.pfx=./signer.pfx + secretName: "" + secretKey: "signer.pfx" + mountPath: "/certs" + # Signer-scoped Secret — key/credential material readable ONLY by the signer + # container (ADR-9). Same create/existingSecretName pattern as the app + # Secret; BYOC default is an externally managed Secret. + secrets: + create: false + existingSecretName: "br-slc-signer-secrets" + data: + # Decrypts the softkey PFX bundle. Required only for the softkey adapter. + SIGNER_SOFTKEY_PFX_PASSPHRASE: "" + # awskms adapter static credentials (prefer IRSA via serviceAccount + # annotations so these stay empty). + AWS_SECRET_ACCESS_KEY: "" + AWS_SESSION_TOKEN: "" + # vault adapter token (only when SIGNER_VAULT_ADDR is set). + SIGNER_VAULT_TOKEN: "" + # pkcs11 adapter PIN (only when a pkcs11 token is configured). + SIGNER_PKCS11_PIN: "" + configmap: + data: + SIGNER_ADDR: ":9101" + SIGNER_MAX_BODY_BYTES: "65536" + ENV_NAME: "production" + # --- softkey adapter (BYOC-first custody path) --- + SIGNER_SOFTKEY_PFX_PATH: "/certs/signer.pfx" + # DELIBERATE, FLAGGED default: softkey holds the key in process memory, + # which config.go treats as alert-worthy in production and REFUSES to + # start unless acked. BYOC-first ships softkey day-one; flip back to + # "false" once a real awskms/pkcs11/vault adapter is configured. + SIGNER_SOFTKEY_ALLOW_IN_PRODUCTION: "true" + # --- awskms adapter (parameterizable, disabled by default: no region) --- + SIGNER_AWSKMS_REGION: "" + SIGNER_AWSKMS_DEFAULT_KEY_REF: "" + AWS_ACCESS_KEY_ID: "" + AWS_REGION: "" + # --- pkcs11 adapter (parameterizable, disabled by default: no token) --- + # SIGNER_PKCS11_TOKEN_LABEL is set via signer.pkcs11TokenLabel (kept out of + # configmap.data because its name matches the "_token_" secret heuristic). + SIGNER_PKCS11_MODULE: "/usr/lib/softhsm/libsofthsm2.so" + SIGNER_PKCS11_SLOT: "" + SIGNER_PKCS11_KEY_LABEL: "" + # --- vault adapter (parameterizable, disabled by default: no address) --- + SIGNER_VAULT_ADDR: "" + SIGNER_VAULT_TRANSIT_MOUNT: "transit" + SIGNER_VAULT_NAMESPACE: "" + SIGNER_VAULT_CACERT: "" + # --- Auth delegation (Access Manager) --- fails closed until the + # operator sets the real PLUGIN_AUTH_HOST (validateAuthConfig). + PLUGIN_AUTH_ENABLED: "true" + PLUGIN_AUTH_HOST: "" + PLUGIN_AUTH_ALLOW_INSECURE_HTTP: "false" + +# --- aslc-xsd-validator container (Decision 12) --- +# Stateless: all XSD schemas are go:embedded in the image. No secrets, no +# persistent volumes. HARD outbound dependency (fail-closed) — but shared fate +# in the same pod removes the "app Ready, validator absent" window. +xsdValidator: + image: + repository: ghcr.io/lerianstudio/br-slc-xsd-validator + tag: "" + pullPolicy: IfNotPresent + resources: + limits: + cpu: "500m" + memory: 256Mi + requests: + cpu: 100m + memory: 64Mi + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + livenessProbe: + httpGet: + path: /health + port: 9091 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 9091 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + configmap: + data: + XSD_VALIDATOR_ADDR: ":9091" + XSD_MAX_BODY_BYTES: "5242880" + ENV_NAME: "production" + # --- Auth delegation (Access Manager) --- fails closed until the + # operator sets the real PLUGIN_AUTH_HOST (validateAuthConfig). + PLUGIN_AUTH_ENABLED: "true" + PLUGIN_AUTH_HOST: "" + PLUGIN_AUTH_ALLOW_INSECURE_HTTP: "false" + +# =========================================================================== +# --- mqbridge container (Decisão 21 — RSFN IBM MQ inbound consumer) --- +# CONDITIONAL same-pod container, UNLIKE the mandatory signer/validator above. +# It owns the inbound IBM MQ/RSFN connection of the cards channel; the mTLS +# identity is per-process (MQSCO.KeyRepository from env MQSSLKEYR), so 1 process +# = 1 participant (BYOC single-tenant). The SaaS multi-tenant consumer manager +# is Fase 7 (separate Deployment) — NOT built here. +# +# GATED: rendered ONLY when mqbridge.enabled=true, which a client sets when the +# role is IF Domicílio with RSFN_CONSUMER_TRANSPORT=mq. Default off — a +# credenciadora-only tenant never carries the container, and a default +# `helm template`/`helm install` renders NO mqbridge container/configmap/secret +# and NO amd64 pod pin. When enabled, the app reaches it over localhost:9121 +# (see configmap.data.MQ_BRIDGE_URL). Enabling it hard-caps the monolith +# replicaCount to 1 (per-process deliveryRef->conn state; see the guard at the +# top of templates/deployment.yaml). +# =========================================================================== +mqbridge: + enabled: false + image: + # ASSUMPTION (flag for CI reconciliation): repository derived from the + # module path github.com/LerianStudio/br-slc-mq-bridge and the + # docker-compose service — ghcr.io/lerianstudio/{name} per Lerian Helm + # conventions. + repository: ghcr.io/lerianstudio/br-slc-mq-bridge + # Empty defaults to .Chart.AppVersion (see br-slc.mqbridgeImage in + # _helpers.tpl). + tag: "" + pullPolicy: IfNotPresent + resources: + limits: + memory: 256Mi + cpu: "500m" + requests: + memory: 64Mi + cpu: "100m" + # Security hardening. UNLIKE the other same-pod containers, + # readOnlyRootFilesystem is FALSE here: the IBM MQ C client (libmqm) + # conventionally needs a writable location (e.g. /var/mqm) for its own + # error/FDC/trace logs, and a read-only rootfs would break it at connect + # time. Still non-root, no privilege escalation, all capabilities dropped, + # RuntimeDefault seccomp. An explicit emptyDir is mounted at /var/mqm + # regardless (see deployment.yaml) so libmqm's writable state never depends + # on the base image's own directory permissions. + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + allowPrivilegeEscalation: false + # readOnlyRootFilesystem is deliberately FALSE for mqbridge (libmqm needs a + # writable /var/mqm). It is set on the container in deployment.yaml rather + # than here so values.yaml carries no writable-rootfs default. Do NOT add a + # readOnlyRootFilesystem key back to this block. + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + # Liveness ONLY (process viability, /health). There is deliberately NO + # readinessProbe (Decisão 21): mqbridge is same-pod and no Service routes to + # :9121, so readiness gates no traffic — but /readyz reflects the IBM MQ pool + # state, so using it as readiness would pull the whole pod out of the + # monolith's :4111 Service on any MQ blip (total outage under replicaCount=1). + # MQ connectivity is surfaced to the monolith via the client-side breaker. + livenessProbe: + httpGet: + path: /health + port: 9121 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + # GSKit keystore (.kdb + .sth/.rdb) — required only when + # MQ_BRIDGE_TLS_ENABLED=true (RSFN mandates mTLS in production). Mounted + # read-only ONTO THE MQBRIDGE CONTAINER ONLY from an externally managed + # Secret; MQSSLKEYR (configmap.data below) must point at the STEM path (no + # file extension), e.g. "/keystore/mqbridge" for files + # mqbridge.kdb/mqbridge.sth/mqbridge.rdb. + keystoreSecret: + enabled: false + # Name of an existing Secret whose data keys hold the .kdb/.sth/.rdb bytes, + # e.g. created out-of-band as: + # kubectl create secret generic br-slc-mqbridge-keystore \ + # --from-file=mqbridge.kdb=./mqbridge.kdb \ + # --from-file=mqbridge.sth=./mqbridge.sth \ + # --from-file=mqbridge.rdb=./mqbridge.rdb + secretName: "" + mountPath: "/keystore" + # Base filename stem (no extension) inside the Secret/mount; combined with + # mountPath this yields the MQSSLKEYR value below by convention. + stem: "mqbridge" + # Mqbridge-scoped Secret — never real values here. Same create/ + # existingSecretName pattern as the app/signer Secrets; BYOC default is an + # externally managed Secret. + # create: false (default) — BYOC recommended: the client manages this Secret + # out-of-band and existingSecretName points at it (must expose the same key + # as secrets.data below). + # create: true — this chart creates the Secret from secrets.data via + # --set-string/-f at install time. + secrets: + create: false + existingSecretName: "br-slc-mqbridge-secrets" + data: + # Optional MQCSP password. RSFN authenticates via the client certificate + # in the keystore, so this is normally empty. + MQ_BRIDGE_MQ_PASSWORD: "" + # ConfigMap — all non-secret env vars. The per-request IBM MQ connection + # descriptor (queue manager, channel, conn name, queue) is NOT here — it + # arrives per call from the monolith on /v1/drain. + configmap: + data: + MQ_BRIDGE_ADDR: ":9121" + MQ_BRIDGE_MAX_INFLIGHT: "16" + MQ_BRIDGE_ACK_TIMEOUT_SEC: "60" + # Derived (half of ack timeout, clamped [1s,30s]) when left at "0". + MQ_BRIDGE_SWEEP_INTERVAL_SEC: "0" + MQ_BRIDGE_GET_WAIT_MS: "0" + MQ_BRIDGE_RECEIVE_BUFFER_BYTES: "1048576" + MQ_BRIDGE_MAX_MESSAGE_BYTES: "16777216" + MQ_BRIDGE_MAX_BODY_BYTES: "65536" + + # --- RSFN mTLS (terminates INSIDE this container, not the monolith) --- + # DELIBERATE, FLAGGED default: production requires TLS (the bridge rejects + # boot when ENV_NAME=production and this is false). Left "false" here + # since no broker/keystore exists until mqbridge is actually enabled for a + # real/HML deployment — flip to "true" together with keystoreSecret.enabled + # and MQSSLKEYR below. + MQ_BRIDGE_TLS_ENABLED: "false" + # Empty by default; set to the keystore stem path (e.g. + # "/keystore/mqbridge") once keystoreSecret.enabled=true. + MQSSLKEYR: "" + # DOUBT-TLS-CIPHER (CodeRabbit PR #99): TLS_RSA_WITH_AES_256_CBC_SHA256 + # uses static RSA key exchange + CBC. An ECDHE+GCM CipherSpec (e.g. + # ECDHE_RSA_AES_256_GCM_SHA384, or TLS 1.3 TLS_AES_256_GCM_SHA384) is + # preferred, BUT the CipherSpec MUST match what Nuclea's RSFN MQ manager + # actually accepts — RSFN is a regulated endpoint, not our choice, and + # selecting a spec it rejects fails the TLS handshake. Confirm the + # supported CipherSpec against the Nuclea RSFN MQ manual at the + # homologacao gate; keep this CBC value only as a compatibility fallback. + MQ_BRIDGE_TLS_CIPHER: "TLS_RSA_WITH_AES_256_CBC_SHA256" + + # Optional MQCSP username (RSFN normally authenticates via the client + # cert in the keystore, so this is normally empty). + MQ_BRIDGE_MQ_USER: "" + + # Rejected in production; "false" is the safe default so a non-ibmmq + # binary never silently boots against a real deployment. + MQ_BRIDGE_ALLOW_STUB: "false" + + ENV_NAME: "production" + LOG_LEVEL: "info" + + # --- Auth delegation (Access Manager) --- hardened default: real auth ON. + # PLUGIN_AUTH_HOST is intentionally empty: the bridge FAILS BOOT when + # PLUGIN_AUTH_ENABLED=true and the host is empty — fails closed until the + # operator sets the real Access Manager URL. + PLUGIN_AUTH_ENABLED: "true" + PLUGIN_AUTH_HOST: "" + PLUGIN_AUTH_ALLOW_INSECURE_HTTP: "false" + +# --------------------------------------------------------------------------- +# Migration Job (Task 6.0.4) — applies migrations/*.up.sql (golang-migrate) +# via an ArgoCD PreSync hook Job, before the Deployment rolls. +# --------------------------------------------------------------------------- +migrations: + enabled: true + backoffLimit: 2 + activeDeadlineSeconds: 600 + # Gate 8 FIX 6: bounds accumulation of FAILED hook Jobs/pods. A successful + # Job is already removed by hook-delete-policy: HookSucceeded — this TTL only + # matters on the failure path, where the Job is deliberately left behind for + # post-mortem debugging. 259200s = 3 days. + ttlSecondsAfterFinished: 259200 + image: + # Dedicated migrations image (br-spi pattern): its entrypoint applies the + # golang-migrate loop over the embedded migrations/*.up.sql against the + # single br-slc database. Built + published by the br-slc release pipeline. + repository: ghcr.io/lerianstudio/br-slc-migrations + # Empty defaults to .Chart.AppVersion. + tag: "" + pullPolicy: IfNotPresent + # PreSync Secret custody. false (default): the chart mints a dedicated PreSync + # Secret (br-slc-migrations) carrying only POSTGRES_PASSWORD, sourced from + # migrations.postgres.password (falling back to secrets.data.POSTGRES_PASSWORD). + # true: the Job reads POSTGRES_PASSWORD from the operator-managed + # existingSecretName instead (BYOC out-of-band Secret). + useExistingSecret: false + existingSecretName: "" + # DB connection for the Job. Each field falls back to the app's + # configmap.data.POSTGRES_* / secrets.data.POSTGRES_PASSWORD (single source of + # truth) when left empty — override only for a migrations-specific endpoint. + postgres: + host: "" + port: "" + user: "" + database: "" + sslMode: "" + password: "" + resources: + limits: + cpu: "500m" + memory: 256Mi + requests: + cpu: 100m + memory: 64Mi + # A lightweight busybox initContainer waits for POSTGRES_HOST:POSTGRES_PORT to + # accept a TCP connection before the migrations container starts (mirrors the + # Lerian wait-for-dependencies convention) — avoids a hard Job failure on a + # fresh install racing a not-yet-ready Postgres. + waitForPostgres: + enabled: true + image: + repository: busybox + tag: "1.37" + +# --------------------------------------------------------------------------- +# mock-nuclea — Núclea clearing-house SIMULATOR. DEV/HML FIXTURE ONLY — NEVER +# ENABLE IN PRODUCTION/BYOC. A separate, standalone Deployment+Service (NOT a +# same-pod sidecar like signer/xsdValidator/mqbridge above): the app reaches it +# over cluster DNS (Service br-slc-mock-nuclea:9190), not localhost. Wiring the +# consumer side (MOCK_NUCLEA_URL / NUCLEA_REST_BASE_URL / +# RSFN_CONSUMER_BASE_URL) happens in the dev/hml gitops overlay, not here. +# +# enabled: false by default — same precedent as the detached migrations +# component above: a gated component that defaults ON with an image the +# registry doesn't have yet (its own ghcr.io/lerianstudio/br-slc-mock-nuclea, +# published by a separate br-slc release-pipeline workstream) would +# ImagePullBackOff and block the whole ArgoCD sync. Flip on only in dev/hml +# overlays once that image is published. +mockNuclea: + enabled: false + image: + repository: ghcr.io/lerianstudio/br-slc-mock-nuclea + # Empty defaults to .Chart.AppVersion (see br-slc.mockNucleaImage in + # _helpers.tpl). ASSUMPTION: reconcile the real published tag once the + # release-pipeline workstream lands. + tag: "" + pullPolicy: IfNotPresent + imagePullSecrets: [] + # ClusterIP only (BYOC/Lerian convention) — no NodePort/LoadBalancer/Ingress. + service: + type: ClusterIP + port: 9190 + targetPort: 9190 + # Distroless static nonroot binary (USER 65532) that writes nothing to disk, + # so readOnlyRootFilesystem is safe with no /tmp emptyDir needed (unlike the + # app/signer/validator containers above). + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + # Lightweight fixture — small resource footprint. + resources: + limits: + cpu: 200m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + # The mock exposes ONLY GET /health — no /readyz. Used for BOTH liveness and + # readiness (unlike the app container, which splits process-viability + # liveness from dependency-aware readiness). + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 diff --git a/charts/br-sta/Chart.lock b/charts/br-sta/Chart.lock new file mode 100644 index 000000000..0a7b7cf96 --- /dev/null +++ b/charts/br-sta/Chart.lock @@ -0,0 +1,12 @@ +dependencies: +- 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:930a2d793fd5205d6a265b344f6785f4727059476f40bc0dac54350d02ba601a +generated: "2026-07-01T19:53:22.540418-03:00" diff --git a/charts/br-sta/Chart.yaml b/charts/br-sta/Chart.yaml new file mode 100644 index 000000000..3d77fb785 --- /dev/null +++ b/charts/br-sta/Chart.yaml @@ -0,0 +1,50 @@ +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: 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..747583412 --- /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 `br-sta.secrets.POSTGRES_PASSWORD` / `br-sta.secrets.REDIS_PASSWORD` for external infra without `postgresql.auth.existingSecret` / `valkey.auth.existingSecret`. `br-sta.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** (`/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 `br-sta.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 `br-sta.secrets.REDIS_PASSWORD` empty. +- For external infra (subchart disabled), supply `br-sta.secrets.POSTGRES_PASSWORD` / `br-sta.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 | +|-------|---------------| +| `br-sta.configmap.MULTI_TENANT_URL` | `MULTI_TENANT_ENABLED=true` | +| `br-sta.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. | +| `br-sta.image.repository` | `ghcr.io/lerianstudio/br-sta` | Container image. | +| `br-sta.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/templates/NOTES.txt b/charts/br-sta/templates/NOTES.txt new file mode 100644 index 000000000..61a23a0e0 --- /dev/null +++ b/charts/br-sta/templates/NOTES.txt @@ -0,0 +1,92 @@ +{{- $component := index .Values "br-sta" -}} +# 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" }} +1. PostgreSQL (in-cluster, Bitnami subchart) + - Primary: {{ include "common.names.dependency.fullname" (dict "chartName" "postgresql" "chartValues" (index .Values "postgresql") "context" .) }}-primary.{{ 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: + - br-sta.configmap.MULTI_TENANT_URL + - br-sta.secrets.MULTI_TENANT_SERVICE_API_KEY + +For an EXTERNAL PostgreSQL / Redis (subchart disabled), supply the passwords via +br-sta.secrets.POSTGRES_PASSWORD / br-sta.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..e670ee171 --- /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" (index .Values "br-sta").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 (index .Values "br-sta").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 (index .Values "br-sta").serviceAccount.create }} +{{- default (include "br-sta.fullname" .) (index .Values "br-sta").serviceAccount.name }} +{{- else }} +{{- default "default" (index .Values "br-sta").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 br-sta.secrets.POSTGRES_PASSWORD. The same reasoning applies to Valkey. */}} + +{{/* Multi-tenant required fields when enabled */}} +{{- if eq ((index .Values "br-sta").configmap.MULTI_TENANT_ENABLED | toString) "true" }} +{{- if not (index .Values "br-sta").configmap.MULTI_TENANT_URL }} +{{- fail "\n\nERROR: br-sta.configmap.MULTI_TENANT_URL is REQUIRED when MULTI_TENANT_ENABLED=true.\n" }} +{{- end }} +{{- if not (index .Values "br-sta").secrets.MULTI_TENANT_SERVICE_API_KEY }} +{{- fail "\n\nERROR: br-sta.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 (index .Values "br-sta").useExistingSecret }} +{{- if not (index .Values "br-sta").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..dadd8e53b --- /dev/null +++ b/charts/br-sta/templates/bootstrap-postgres.yaml @@ -0,0 +1,137 @@ +{{- 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 + initContainers: + - name: wait-for-dependencies + image: busybox:1.37 + 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 + 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..77d452942 --- /dev/null +++ b/charts/br-sta/templates/configmap.yaml @@ -0,0 +1,67 @@ +{{- $component := index .Values "br-sta" -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "br-sta.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "br-sta.labels" (dict "context" .) | nindent 4 }} +data: + # Application environment values + {{- range $key, $value := $component.configmap }} + {{- if and (ne $key "POSTGRES_HOST") (ne $key "REDIS_HOST") (ne $key "RABBITMQ_HOST") }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + + # PostgreSQL host: explicit value or dynamic default derived from the Bitnami subchart's + # own name helper (honors release-name collapse, nameOverride and fullnameOverride) when + # in-cluster Postgres is enabled. The subchart exposes the primary Service at + # "-primary" in replication architecture and at "" + # in standalone architecture. + {{- if $component.configmap.POSTGRES_HOST }} + POSTGRES_HOST: {{ $component.configmap.POSTGRES_HOST | 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 (include "global.namespace" .) | quote }} + {{- else }} + POSTGRES_HOST: {{ printf "%s.%s.svc.cluster.local" $pgFullname (include "global.namespace" .) | quote }} + {{- end }} + {{- else }} + POSTGRES_HOST: "" + {{- end }} + + # Redis/Valkey host: br-sta expects a combined "host:port" value in REDIS_HOST. + # Explicit value wins; otherwise derive the bundled Valkey subchart's primary + # Service (collapse-aware) when in-cluster Valkey is enabled. Bitnami Valkey + # exposes its primary Service at "-primary". + {{- if $component.configmap.REDIS_HOST }} + REDIS_HOST: {{ $component.configmap.REDIS_HOST | 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 (include "global.namespace" .) | quote }} + {{- else }} + REDIS_HOST: "" + {{- end }} + + # RabbitMQ host: explicit value wins; otherwise, when the bundled broker is + # enabled, derive the groundhog2k Service name "-rabbitmq" (that + # subchart names its Service after the release, not collapse-aware Bitnami). + {{- if $component.configmap.RABBITMQ_HOST }} + RABBITMQ_HOST: {{ $component.configmap.RABBITMQ_HOST | quote }} + {{- else if eq (include "rabbitmq.enabled" .) "true" }} + RABBITMQ_HOST: {{ printf "%s-rabbitmq.%s.svc.cluster.local" .Release.Name (include "global.namespace" .) | quote }} + {{- else }} + RABBITMQ_HOST: "" + {{- end }} + + # Always set VERSION and OTEL_RESOURCE_SERVICE_VERSION from the resolved image tag + VERSION: {{ include "br-sta.defaultTag" . | quote }} + OTEL_RESOURCE_SERVICE_VERSION: {{ include "br-sta.defaultTag" . | quote }} + + # Extra environment variables (passthrough from values.app.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..9f3f70956 --- /dev/null +++ b/charts/br-sta/templates/deployment.yaml @@ -0,0 +1,162 @@ +{{- $component := index .Values "br-sta" -}} +{{- 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..61f14a920 --- /dev/null +++ b/charts/br-sta/templates/hpa.yaml @@ -0,0 +1,37 @@ +{{- $component := index .Values "br-sta" -}} +{{- 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..18828d1e7 --- /dev/null +++ b/charts/br-sta/templates/ingress.yaml @@ -0,0 +1,63 @@ +{{- $component := index .Values "br-sta" -}} +{{- 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..87e56e740 --- /dev/null +++ b/charts/br-sta/templates/migrations.yaml @@ -0,0 +1,218 @@ +{{- $app := index .Values "br-sta" -}} +{{- $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 br-sta.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 }} +{{- /* Migration image tag is coupled to the app image tag unless an explicit tag/digest is set; + empty tag falls back to br-sta.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: + backoffLimit: {{ get $migrations "backoffLimit" | default 3 }} + activeDeadlineSeconds: {{ get $migrations "activeDeadlineSeconds" | default 600 }} + ttlSecondsAfterFinished: {{ get $migrations "ttlSecondsAfterFinished" | default 600 }} + 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..008682535 --- /dev/null +++ b/charts/br-sta/templates/pdb.yaml @@ -0,0 +1,25 @@ +{{- $component := index .Values "br-sta" -}} +{{- 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..3b017e2b4 --- /dev/null +++ b/charts/br-sta/templates/secrets.yaml @@ -0,0 +1,25 @@ +{{- $component := index .Values "br-sta" -}} +{{- if not $component.useExistingSecret }} +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 := $component.secrets }} + {{- if $value }} + {{ $key }}: {{ $value | toString | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/br-sta/templates/service.yaml b/charts/br-sta/templates/service.yaml new file mode 100644 index 000000000..48ccfd2f6 --- /dev/null +++ b/charts/br-sta/templates/service.yaml @@ -0,0 +1,23 @@ +{{- $component := index .Values "br-sta" -}} +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..9fd7bce69 --- /dev/null +++ b/charts/br-sta/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +{{- $component := index .Values "br-sta" -}} +{{- 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..5b676f42e --- /dev/null +++ b/charts/br-sta/templates/worker/deployment.yaml @@ -0,0 +1,189 @@ +{{- if eq (include "br-sta.worker.enabled" .) "true" }} +{{- $mgr := index .Values "br-sta" -}} +{{- $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. + 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 + 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-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..2409b732f --- /dev/null +++ b/charts/br-sta/values.schema.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true, + "properties": { + "nameOverride": { + "type": "string" + }, + "fullnameOverride": { + "type": "string" + }, + "namespaceOverride": { + "type": "string" + }, + "global": { + "type": "object", + "additionalProperties": true + }, + "postgresql": { + "type": "object", + "additionalProperties": true + }, + "valkey": { + "type": "object", + "additionalProperties": true + }, + "rabbitmq": { + "type": "object", + "additionalProperties": true + }, + "otel-collector-lerian": { + "type": "object", + "additionalProperties": true + }, + "br-sta": { + "type": "object", + "properties": { + "configmap": { + "type": "object" + }, + "secrets": { + "type": "object" + }, + "useExistingSecret": { + "type": "boolean" + }, + "existingSecretName": { + "type": "string" + } + }, + "additionalProperties": true, + "if": { + "properties": { + "useExistingSecret": { + "const": true + } + }, + "required": ["useExistingSecret"] + }, + "then": { + "properties": { + "existingSecretName": { + "minLength": 1 + } + }, + "required": ["existingSecretName"] + } + } + } +} diff --git a/charts/br-sta/values.yaml b/charts/br-sta/values.yaml new file mode 100644 index 000000000..7bd1d8c99 --- /dev/null +++ b/charts/br-sta/values.yaml @@ -0,0 +1,666 @@ +# 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: "" + +# ============================================================================== +# APPLICATION +# Single Deployment running the /service binary (Go/Fiber HTTP server). +# ============================================================================== +br-sta: + # -- 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 + # (br-sta.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 br-sta.extraEnvVars, then br-sta.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: [] + # -- ConfigMap (non-sensitive environment variables) + # @default -- templates/configmap.yaml + configmap: + # Application Settings + ENV_NAME: "production" + LOG_LEVEL: "info" + # ServerAddress binds to all interfaces inside the pod so kubelet probes can reach it. + SERVER_ADDRESS: "0.0.0.0:8080" + HTTP_BODY_LIMIT_BYTES: "104857600" + # CORS Configuration + CORS_ALLOWED_ORIGINS: "*" + CORS_ALLOWED_METHODS: "GET,POST,PUT,PATCH,DELETE,OPTIONS" + CORS_ALLOWED_HEADERS: "Origin,Content-Type,Accept,Authorization,X-Request-ID" + CORS_EXPOSE_HEADERS: "" + CORS_ALLOW_CREDENTIALS: "false" + # TLS (terminate at ingress in production) + TLS_TERMINATED_UPSTREAM: "true" + # Single-tenant default tenant ID (must be a valid UUID) + DEFAULT_TENANT_ID: "11111111-1111-1111-1111-111111111111" + # Multi-Tenant Starter (tenant-manager) — OPTIONAL + # When enabled, MULTI_TENANT_URL and MULTI_TENANT_SERVICE_API_KEY become required. + MULTI_TENANT_ENABLED: "false" + MULTI_TENANT_URL: "" + MULTI_TENANT_REDIS_HOST: "" + MULTI_TENANT_REDIS_PORT: "6379" + MULTI_TENANT_REDIS_TLS: "false" + MULTI_TENANT_MAX_TENANT_POOLS: "100" + MULTI_TENANT_IDLE_TIMEOUT_SEC: "300" + MULTI_TENANT_TIMEOUT: "30" + MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD: "5" + MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC: "30" + MULTI_TENANT_CACHE_TTL_SEC: "120" + MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC: "30" + # PostgreSQL Primary (host defaults dynamically in configmap when empty) + POSTGRES_HOST: "" # Empty -> dynamic default in configmap template + POSTGRES_PORT: "5432" + POSTGRES_USER: "br_sta" + POSTGRES_NAME: "br_sta" + POSTGRES_SSLMODE: "require" + # Migrations path (applied at startup by the /service binary) + MIGRATIONS_PATH: "migrations" + # Runtime config plane (operational knobs only; disabled preserves env-only mode) + SYSTEMPLANE_ENABLED: "false" + # Connection Pool + POSTGRES_MAX_OPEN_CONNS: "25" + POSTGRES_MAX_IDLE_CONNS: "5" + POSTGRES_CONN_MAX_LIFETIME_MINS: "30" + POSTGRES_CONN_MAX_IDLE_TIME_MINS: "5" + POSTGRES_CONNECT_TIMEOUT_SEC: "10" + # PostgreSQL Replica (optional - for CQRS read scaling) + # POSTGRES_REPLICA_HOST: "" + # POSTGRES_REPLICA_PORT: "5433" + # POSTGRES_REPLICA_USER: "" + # POSTGRES_REPLICA_NAME: "" + # POSTGRES_REPLICA_SSLMODE: "" + # Infrastructure boot timeout + INFRA_CONNECT_TIMEOUT_SEC: "30" + # Redis / Valkey (host defaults dynamically in configmap when empty) + REDIS_HOST: "" # Empty -> dynamic default "-primary..svc.cluster.local:6379" + REDIS_DB: "0" + REDIS_PROTOCOL: "3" + REDIS_POOL_SIZE: "10" + REDIS_MIN_IDLE_CONNS: "2" + REDIS_READ_TIMEOUT: "3" + REDIS_WRITE_TIMEOUT: "3" + REDIS_DIAL_TIMEOUT: "5" + REDIS_POOL_TIMEOUT: "2" + REDIS_MAX_RETRIES: "3" + REDIS_MIN_RETRY_BACKOFF: "8" + REDIS_MAX_RETRY_BACKOFF: "1" + # REDIS_MASTER_NAME: "" + REDIS_TLS: "false" + # REDIS_CA_CERT: "" + # Event-Driven Starter - Circuit Breaker + CIRCUIT_BREAKER_ENABLED: "false" + # Event-Driven Starter - RabbitMQ — OPTIONAL. + # Disabled by default. Set both RABBITMQ_ENABLED="true" AND rabbitmq.enabled=true + # (bottom of this file) to bundle the groundhog2k broker as a subchart. + RABBITMQ_ENABLED: "false" + # RABBITMQ_URL: "" + # RABBITMQ_HOST: empty -> dynamic default "-rabbitmq..svc.cluster.local" + # when the bundled broker is enabled; set explicitly for an external broker. + RABBITMQ_HOST: "" + RABBITMQ_PORT_AMQP: "5672" + RABBITMQ_PORT_HOST: "15672" + # Must match rabbitmq.authentication.user.value when the bundled broker is enabled. + RABBITMQ_DEFAULT_USER: "br_sta" + RABBITMQ_VHOST: "/" + # RABBITMQ_QUEUE: "" + RABBITMQ_EXCHANGE: "events" + # RABBITMQ_HEALTH_CHECK_URL: "" + # RABBITMQ_HEALTH_CHECK_ALLOWED_HOSTS: "" + RABBITMQ_REQUIRE_HEALTH_ALLOWED_HOSTS: "false" + RABBITMQ_ALLOW_INSECURE_HEALTH_CHECK: "false" + RABBITMQ_ALLOW_INSECURE_TLS: "false" + RABBITMQ_PUBLISHER_CONFIRM_TIMEOUT_MS: "5000" + RABBITMQ_PUBLISHER_RECOVERY_INITIAL_MS: "1000" + RABBITMQ_PUBLISHER_RECOVERY_MAX_MS: "30000" + RABBITMQ_PUBLISHER_MAX_RECOVERIES: "10" + # Event-Driven Starter - Outbox — OPTIONAL + OUTBOX_ENABLED: "false" + OUTBOX_TABLE_NAME: "outbox_events" + OUTBOX_DISPATCH_INTERVAL_SEC: "2" + OUTBOX_BATCH_SIZE: "50" + OUTBOX_PUBLISH_MAX_ATTEMPTS: "3" + OUTBOX_PUBLISH_BACKOFF_MS: "200" + OUTBOX_RETRY_WINDOW_SEC: "300" + OUTBOX_MAX_DISPATCH_ATTEMPTS: "10" + OUTBOX_PROCESSING_TIMEOUT_SEC: "600" + OUTBOX_MAX_FAILED_PER_BATCH: "25" + OUTBOX_INCLUDE_TENANT_METRICS: "false" + # OUTBOX_PRIORITY_EVENT_TYPES: "" + OUTBOX_ALLOW_EMPTY_TENANT: "true" + # Authentication (lib-auth + plugin-auth) — OPTIONAL + PLUGIN_AUTH_ENABLED: "false" + PLUGIN_AUTH_HOST: "" + # Resilience example (repo-local simulation only) + EXAMPLE_STATUS_PROVIDER_MODE: "healthy" + # API Documentation (Swagger) + SWAGGER_ENABLED: "false" + SWAGGER_TITLE: "br-sta" + # SWAGGER_DESCRIPTION: "" + SWAGGER_VERSION: "1.0.0" + SWAGGER_BASE_PATH: "/" + SWAGGER_LEFT_DELIM: "{{" + SWAGGER_RIGHT_DELIM: "}}" + # OpenTelemetry — OPTIONAL + ENABLE_TELEMETRY: "false" + OTEL_LIBRARY_NAME: "github.com/LerianStudio/br-sta" + OTEL_RESOURCE_SERVICE_NAME: "br-sta" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "production" + # Rate Limiting + RATE_LIMIT_ENABLED: "true" + RATE_LIMIT_MAX: "500" + RATE_LIMIT_WINDOW_SEC: "60" + AGGRESSIVE_RATE_LIMIT_MAX: "100" + AGGRESSIVE_RATE_LIMIT_WINDOW_SEC: "60" + RELAXED_RATE_LIMIT_MAX: "1000" + RELAXED_RATE_LIMIT_WINDOW_SEC: "60" + # Observability & Metrics + DB_METRICS_INTERVAL_SEC: "15" + # Idempotency + IDEMPOTENCY_RETRY_WINDOW_SEC: "300" + # M2M Credentials (for services with target service dependencies) + # M2M_TARGET_SERVICE: "" + M2M_CREDENTIAL_CACHE_TTL_SEC: "300" + AWS_REGION: "us-east-1" + # Pagination + MAX_PAGINATION_LIMIT: "100" + MAX_PAGINATION_MONTH_DATE_RANGE: "3" + # -- 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: "" + # -- 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 br-sta.configmap.RABBITMQ_DEFAULT_USER. + value: "br_sta" + password: + # Operators MUST supply this AND the matching br-sta.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 + diff --git a/charts/fetcher/Chart.yaml b/charts/fetcher/Chart.yaml index e01fda97e..394f25ae1 100644 --- a/charts/fetcher/Chart.yaml +++ b/charts/fetcher/Chart.yaml @@ -8,7 +8,7 @@ home: https://github.com/LerianStudio/fetcher maintainers: - name: "Lerian Studio" email: "support@lerian.studio" -version: 3.0.0 +version: 3.1.0-beta.1 appVersion: "2.0.2" keywords: - midaz diff --git a/charts/flowker/Chart.yaml b/charts/flowker/Chart.yaml index fd3b9eb63..3ca209bed 100644 --- a/charts/flowker/Chart.yaml +++ b/charts/flowker/Chart.yaml @@ -11,7 +11,7 @@ sources: maintainers: - name: "Lerian Studio" email: "support@lerian.studio" -version: 3.1.1 +version: 3.2.0-beta.1 appVersion: "1.2.0-beta.82" keywords: - flowker diff --git a/charts/go-boilerplate-ddd/Chart.yaml b/charts/go-boilerplate-ddd/Chart.yaml index 4d176f45d..225de4939 100644 --- a/charts/go-boilerplate-ddd/Chart.yaml +++ b/charts/go-boilerplate-ddd/Chart.yaml @@ -12,7 +12,7 @@ sources: maintainers: - name: "Lerian Studio" email: "support@lerian.studio" -version: 2.2.0-beta.1 +version: 2.2.0-beta.2 deprecated: true appVersion: "1.0.0" keywords: diff --git a/charts/go-boilerplate-ddd/templates/bootstrap-postgres.yaml b/charts/go-boilerplate-ddd/templates/bootstrap-postgres.yaml index f097faa6d..2903aa9dd 100644 --- a/charts/go-boilerplate-ddd/templates/bootstrap-postgres.yaml +++ b/charts/go-boilerplate-ddd/templates/bootstrap-postgres.yaml @@ -104,7 +104,7 @@ spec: echo "Role 'go-boilerplate-ddd' already exists. Skipping creation." else echo "Creating role 'go-boilerplate-ddd'..." - 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 ROLE \"go-boilerplate-ddd\" LOGIN PASSWORD '$DB_PASSWORD_BOILERPLATE'" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -v pw="$DB_PASSWORD_BOILERPLATE" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "CREATE ROLE \"go-boilerplate-ddd\" LOGIN PASSWORD :'pw'" fi # Create database if not exists diff --git a/charts/lender/Chart.yaml b/charts/lender/Chart.yaml new file mode 100644 index 000000000..7cf6220df --- /dev/null +++ b/charts/lender/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v2 +name: lender-helm +description: A Helm chart for Lender - Lerian credit journey engine (API + optional console) +type: application +annotations: + lerian.studio/chart-type: multi-component +home: https://github.com/LerianStudio/lender +sources: + - https://github.com/LerianStudio/helm/tree/main/charts/lender + - https://github.com/LerianStudio/lender +maintainers: + - name: "Lerian Studio" + email: "support@lerian.studio" +version: 1.0.0-beta.1 +# NOTE: appVersion tracks the lender API image line. The real image tag is set +# per-component via lender.image.tag and lenderConsole.image.tag. +appVersion: "1.0.0-beta.49" +keywords: + - lender + - credit + - underwriter + - lerian + - ddd + - hexagonal +icon: https://avatars.githubusercontent.com/u/148895005?s=200&v=4 diff --git a/charts/lender/README.md b/charts/lender/README.md new file mode 100644 index 000000000..6ce3e0bfd --- /dev/null +++ b/charts/lender/README.md @@ -0,0 +1,80 @@ +# Lender Helm Chart + +This chart installs [Lender](https://github.com/LerianStudio/lender), Lerian's credit +journey engine (CDC / PJ / Cards). It is a **multi-component** chart: + +- `lender` — the Go API (main component; image tag lands at `lender.image.tag`). +- `lenderConsole` — the optional Vite/React SPA UI served by `nginx-unprivileged` + (disabled by default; enable per environment once a `lender-ui` image is published). + +PostgreSQL and Valkey/Redis are **external** (Benedita DB-LXC pattern) — the chart +ships no datastore subcharts. An optional ArgoCD PreSync bootstrap Job +(`global.externalPostgresDefinitions.enabled`) provisions the DB/role/grants +idempotently; schema migrations remain operator-driven (`cmd/migrate up`, out of band). + +## Chart Contract + +- Chart type: `multi-component` +- Required secrets: `lender.secrets.POSTGRES_PASSWORD` (install fails loud when unset, + unless `lender.useExistingSecret=true` — then `lender.existingSecretName` is required + instead). `REDIS_PASSWORD` and `AUTH_JWT_SECRET` are optional (emitted from + `lender.secrets.*` only when set). When the bootstrap Job is enabled + (`global.externalPostgresDefinitions.enabled=true`) without an `useExistingSecret.name`, + `global.externalPostgresDefinitions.postgresAdminLogin.password` and + `global.externalPostgresDefinitions.lenderCredentials.password` are also required. +- Dependency notes: No subcharts / no `dependencies:` — Postgres is external (Benedita + DB-LXC pattern), provided via values. Therefore no `Chart.lock`. +- Production overrides: set `lender.image.tag`; provide `lender.secrets.POSTGRES_PASSWORD` + (or `lender.useExistingSecret=true` + `lender.existingSecretName`) and, when the UI is + used, `lenderConsole.enabled=true` with `lenderConsole.image.tag` (**required** when the + console is enabled). Configure `lender.ingress` / `lenderConsole.ingress` hosts per + environment. `lenderConsole.securityContext.readOnlyRootFilesystem` is intentionally + `false` (nginx-unprivileged renders `/config.js` at start and writes pid/cache to the + root fs — live-proven; do not flip it to `true`). +- Source/license: https://github.com/LerianStudio/lender + +## Components + +### Lender API (`lender`) + +The Go API. Binds `0.0.0.0:4017` (`SERVER_ADDRESS`), exposes `/health` (liveness) and +`/readyz` (readiness). Non-sensitive env is rendered from `lender.configmap`; sensitive +env from `lender.secrets` (or an existing Secret). Feature flags (RabbitMQ, streaming, +outbox, multi-tenant, collection) default OFF so the service boots single-tenant. + +### Lender Console (`lenderConsole`) + +Optional Vite SPA served by `nginx-unprivileged` (uid/gid `101`). Disabled by default; +no chart change is needed to wire a future UI — set `lenderConsole.enabled=true` and a +`lenderConsole.image.tag`. Runtime config (e.g. `API_BASE_URL`) is delivered via +`lenderConsole.configmap` and rendered into the SPA at container start. + +## Install + +```console +$ helm install lender oci://registry-1.docker.io/lerianstudio/lender-helm \ + --version 1.0.2 -n lender --create-namespace +``` + +## Configuring Ingress (NGINX) + +```yaml +lender: + ingress: + enabled: true + className: "nginx" + hosts: + - host: lender.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: lender-tls + hosts: + - lender.example.com +``` + +## Support & Community + +- **GitHub Issues**: https://github.com/LerianStudio/lender/issues +- **Email**: [contact@lerian.studio](mailto:contact@lerian.studio) diff --git a/charts/lender/templates/_helpers.tpl b/charts/lender/templates/_helpers.tpl new file mode 100644 index 000000000..4fae96c58 --- /dev/null +++ b/charts/lender/templates/_helpers.tpl @@ -0,0 +1,83 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "lender.name" -}} +{{- default "lender" .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name for the lender API component. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "lender.fullname" -}} +{{- default (include "lender.name" .) .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Fully qualified name for the lender console (UI) component. +*/}} +{{- define "lenderConsole.fullname" -}} +{{- $base := include "lender.fullname" . | trunc 55 | trimSuffix "-" }} +{{- printf "%s-console" $base | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "lender.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Resolve the lender API image tag (falls back to Chart.AppVersion). +*/}} +{{- define "lender.defaultTag" -}} +{{- default .Chart.AppVersion .Values.lender.image.tag }} +{{- end -}} + +{{/* +Return valid lender version label value. +*/}} +{{- define "lender.versionLabelValue" -}} +{{ regexReplaceAll "[^-A-Za-z0-9_.]" (include "lender.defaultTag" .) "-" | trunc 63 | trimAll "-" | trimAll "_" | trimAll "." | quote }} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "lender.labels" -}} +helm.sh/chart: {{ include "lender.chart" .context }} +{{ include "lender.selectorLabels" (dict "context" .context "component" .component "name" .name) }} +app.kubernetes.io/version: {{ include "lender.versionLabelValue" .context }} +app.kubernetes.io/managed-by: {{ .context.Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "lender.selectorLabels" -}} +app.kubernetes.io/name: {{ include "lender.name" .context }} +app.kubernetes.io/instance: {{ .context.Release.Name }} +{{- if .component }} +app.kubernetes.io/component: {{ .component }} +{{- end }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "lender.serviceAccountName" -}} +{{- if .Values.lender.serviceAccount.create }} +{{- default (include "lender.fullname" .) .Values.lender.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.lender.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Expand the namespace of the release. +Allows overriding it for multi-namespace deployments in combined charts. +*/}} +{{- define "global.namespace" -}} +{{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} +{{- end }} diff --git a/charts/lender/templates/bootstrap-postgres.yaml b/charts/lender/templates/bootstrap-postgres.yaml new file mode 100644 index 000000000..25e330f77 --- /dev/null +++ b/charts/lender/templates/bootstrap-postgres.yaml @@ -0,0 +1,145 @@ +{{- if .Values.global.externalPostgresDefinitions.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "lender.fullname" . }}-bootstrap-postgres + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" "bootstrap" "name" "postgres") | nindent 4 }} + annotations: + # Run as an ArgoCD PreSync hook, recreated every sync (Job pod templates are + # immutable, so a fixed-name Job would collide on helm upgrade). sync-wave -10 + # provisions the DB/role before the app rolls. ttl is a non-ArgoCD fallback. + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + argocd.argoproj.io/sync-wave: "-10" +spec: + ttlSecondsAfterFinished: 300 + completions: 1 + parallelism: 1 + backoffLimit: 3 + template: + spec: + restartPolicy: Never + initContainers: + - name: wait-for-dependencies + image: busybox:1.37 + 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 is not ready yet, waiting... (${ELAPSED}s/${TIMEOUT}s)"; + sleep 5; + ELAPSED=$((ELAPSED + 5)); + done; + echo "$DB_HOST:$DB_PORT is ready!"; + containers: + - name: psql + image: postgres:17 + 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: {{ required "global.externalPostgresDefinitions.postgresAdminLogin.password is required when postgresAdminLogin.useExistingSecret.name is unset" .Values.global.externalPostgresDefinitions.postgresAdminLogin.password | quote }} + {{- end }} + - name: DB_PASSWORD_LENDER + {{- if .Values.global.externalPostgresDefinitions.lenderCredentials.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ .Values.global.externalPostgresDefinitions.lenderCredentials.useExistingSecret.name | quote }} + key: DB_PASSWORD_LENDER + {{- else }} + value: {{ required "global.externalPostgresDefinitions.lenderCredentials.password is required when lenderCredentials.useExistingSecret.name is unset" .Values.global.externalPostgresDefinitions.lenderCredentials.password | quote }} + {{- end }} + - name: DB_DATABASE + value: postgres + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== Lender PostgreSQL Bootstrap ===" + echo "Host: $DB_HOST:$DB_PORT" + echo "" + + # The role password is passed to psql via the `pw` variable and + # interpolated with :'pw', which psql quotes/escapes as a SQL string + # literal — it is never embedded into the -c SQL string directly, so + # there is no SQL injection surface. + run_psql() { + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -v pw="$DB_PASSWORD_LENDER" "$@" + } + + echo "Checking existing PostgreSQL objects..." + DB_EXISTS=0 + ROLE_EXISTS=0 + + if run_psql -At -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "SELECT 1 FROM pg_database WHERE datname='lender'" | grep -q 1; then + DB_EXISTS=1 + fi + if run_psql -At -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "SELECT 1 FROM pg_roles WHERE rolname='lender'" | grep -q 1; then + ROLE_EXISTS=1 + fi + + # Create the role if missing, otherwise refresh its password. + # NOTE: psql interpolates :'pw' only for stdin/-f input, NOT for -c; + # feed the statement on stdin so the password is safely psql-quoted. + if [ "$ROLE_EXISTS" = "1" ]; then + echo "Role 'lender' already exists. Updating password..." + printf '%s\n' "ALTER ROLE \"lender\" WITH LOGIN PASSWORD :'pw';" | run_psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" + else + echo "Creating role 'lender'..." + printf '%s\n' "CREATE ROLE \"lender\" LOGIN PASSWORD :'pw';" | run_psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" + fi + + # Create the database if missing. + if [ "$DB_EXISTS" = "1" ]; then + echo "Database 'lender' already exists. Skipping creation." + else + echo "Creating database 'lender'..." + run_psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "CREATE DATABASE \"lender\" OWNER \"lender\"" + fi + + # Privileges (safe to run repeatedly). No CREATEDB grant — least privilege. + echo "Ensuring privileges and schema permissions..." + run_psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "GRANT ALL PRIVILEGES ON DATABASE \"lender\" TO \"lender\"" + run_psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "lender" -c "GRANT ALL ON SCHEMA public TO \"lender\"" + run_psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "lender" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"lender\"" + run_psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "lender" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"lender\"" + run_psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "lender" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"lender\"" + run_psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "lender" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"lender\"" + + echo "" + echo "=== Lender PostgreSQL Bootstrap completed successfully ===" + echo "NOTE: schema migrations are operator-driven (cmd/migrate up) and run out-of-band." +{{- end }} diff --git a/charts/lender/templates/lender/configmap.yaml b/charts/lender/templates/lender/configmap.yaml new file mode 100644 index 000000000..a1ec08df7 --- /dev/null +++ b/charts/lender/templates/lender/configmap.yaml @@ -0,0 +1,44 @@ +{{- if .Values.lender.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "lender.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lender.name "name" .Values.lender.name) | nindent 4 }} +data: + # Auto-generated version (from image tag, falls back to Chart.AppVersion) + VERSION: {{ .Values.lender.image.tag | default .Chart.AppVersion | quote }} + OTEL_RESOURCE_SERVICE_VERSION: {{ .Values.lender.image.tag | default .Chart.AppVersion | quote }} + + # Non-sensitive environment (mirrors config/.env.example superset). + # MULTI_TENANT_* sub-keys are owned by the gated block below; only + # MULTI_TENANT_ENABLED is emitted here to avoid duplicate ConfigMap keys. + {{- range $key, $value := .Values.lender.configmap }} + {{- if or (not (hasPrefix "MULTI_TENANT_" $key)) (eq $key "MULTI_TENANT_ENABLED") }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + + # Multi-tenant starter (rendered only when MULTI_TENANT_ENABLED=true) + {{- if eq (.Values.lender.configmap.MULTI_TENANT_ENABLED | default "false" | toString) "true" }} + MULTI_TENANT_URL: {{ required "lender.configmap.MULTI_TENANT_URL is required when MULTI_TENANT_ENABLED=true" .Values.lender.configmap.MULTI_TENANT_URL | quote }} + MULTI_TENANT_REDIS_HOST: {{ required "lender.configmap.MULTI_TENANT_REDIS_HOST is required when MULTI_TENANT_ENABLED=true" .Values.lender.configmap.MULTI_TENANT_REDIS_HOST | quote }} + MULTI_TENANT_REDIS_PORT: {{ .Values.lender.configmap.MULTI_TENANT_REDIS_PORT | default "6379" | quote }} + MULTI_TENANT_REDIS_TLS: {{ .Values.lender.configmap.MULTI_TENANT_REDIS_TLS | default "false" | quote }} + MULTI_TENANT_MAX_TENANT_POOLS: {{ .Values.lender.configmap.MULTI_TENANT_MAX_TENANT_POOLS | default "100" | quote }} + MULTI_TENANT_IDLE_TIMEOUT_SEC: {{ .Values.lender.configmap.MULTI_TENANT_IDLE_TIMEOUT_SEC | default "300" | quote }} + MULTI_TENANT_TIMEOUT: {{ .Values.lender.configmap.MULTI_TENANT_TIMEOUT | default "30" | quote }} + MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD: {{ .Values.lender.configmap.MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD | default "5" | quote }} + MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC: {{ .Values.lender.configmap.MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC | default "30" | quote }} + MULTI_TENANT_CACHE_TTL_SEC: {{ .Values.lender.configmap.MULTI_TENANT_CACHE_TTL_SEC | default "120" | quote }} + MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC: {{ .Values.lender.configmap.MULTI_TENANT_CONNECTIONS_CHECK_INTERVAL_SEC | default "30" | quote }} + {{- end }} + + # Extra env vars (GitOps passthrough) + {{- with .Values.lender.extraEnvVars }} + {{- range . }} + {{ .name }}: {{ .value | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/lender/templates/lender/deployment.yaml b/charts/lender/templates/lender/deployment.yaml new file mode 100644 index 000000000..f7124f5b9 --- /dev/null +++ b/charts/lender/templates/lender/deployment.yaml @@ -0,0 +1,94 @@ +{{- if .Values.lender.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "lender.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lender.name "name" .Values.lender.name) | nindent 4 }} +spec: + revisionHistoryLimit: {{ .Values.lender.revisionHistoryLimit | default 10 }} + strategy: + type: {{ .Values.lender.deploymentUpdate.type }} + {{- if eq .Values.lender.deploymentUpdate.type "RollingUpdate" }} + rollingUpdate: + maxSurge: {{ .Values.lender.deploymentUpdate.maxSurge }} + maxUnavailable: {{ .Values.lender.deploymentUpdate.maxUnavailable }} + {{- end }} + {{- if not .Values.lender.autoscaling.enabled }} + replicas: {{ .Values.lender.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "lender.selectorLabels" (dict "context" . "name" .Values.lender.name) | nindent 6 }} + template: + metadata: + {{- with .Values.lender.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lender.name "name" .Values.lender.name) | nindent 8 }} + spec: + {{- with .Values.lender.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "lender.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.lender.podSecurityContext | nindent 8 }} + containers: + - name: {{ include "lender.fullname" . }} + securityContext: + {{- toYaml .Values.lender.securityContext | nindent 12 }} + image: "{{ .Values.lender.image.repository }}:{{ .Values.lender.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.lender.image.pullPolicy }} + envFrom: + - secretRef: + name: {{ if .Values.lender.useExistingSecret }}{{ required "lender.existingSecretName is required when lender.useExistingSecret=true" .Values.lender.existingSecretName }}{{ else }}{{ include "lender.fullname" . }}{{ end }} + - configMapRef: + name: {{ include "lender.fullname" . }} + {{- if eq (toString .Values.lender.configmap.ENABLE_TELEMETRY) "true" }} + env: + - name: "HOST_IP" + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: "OTEL_EXPORTER_OTLP_ENDPOINT" + value: "http://$(HOST_IP):4317" + {{- end }} + ports: + - name: http + containerPort: {{ .Values.lender.service.port }} + protocol: TCP + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /readyz + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + {{- toYaml .Values.lender.resources | nindent 12 }} + {{- with .Values.lender.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.lender.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.lender.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/lender/templates/lender/hpa.yaml b/charts/lender/templates/lender/hpa.yaml new file mode 100644 index 000000000..3c757acde --- /dev/null +++ b/charts/lender/templates/lender/hpa.yaml @@ -0,0 +1,33 @@ +{{- if and .Values.lender.enabled .Values.lender.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "lender.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lender.name "name" .Values.lender.name) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "lender.fullname" . }} + minReplicas: {{ .Values.lender.autoscaling.minReplicas }} + maxReplicas: {{ .Values.lender.autoscaling.maxReplicas }} + metrics: + {{- if .Values.lender.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.lender.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.lender.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.lender.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/lender/templates/lender/ingress.yaml b/charts/lender/templates/lender/ingress.yaml new file mode 100644 index 000000000..1840e82a2 --- /dev/null +++ b/charts/lender/templates/lender/ingress.yaml @@ -0,0 +1,44 @@ +{{- if and .Values.lender.enabled .Values.lender.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "lender.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lender.name "name" .Values.lender.name) | nindent 4 }} + {{- with .Values.lender.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.lender.ingress.className }} + ingressClassName: {{ .Values.lender.ingress.className }} + {{- end }} + {{- if .Values.lender.ingress.tls }} + tls: + {{- range .Values.lender.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.lender.ingress.hosts }} + {{- if .host }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "lender.fullname" $ }} + port: + number: {{ $.Values.lender.service.port }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/lender/templates/lender/pdb.yaml b/charts/lender/templates/lender/pdb.yaml new file mode 100644 index 000000000..934c87184 --- /dev/null +++ b/charts/lender/templates/lender/pdb.yaml @@ -0,0 +1,27 @@ +{{- if and .Values.lender.enabled .Values.lender.pdb.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "lender.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lender.name "name" .Values.lender.name) | nindent 4 }} + {{- with .Values.lender.pdb.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if hasKey .Values.lender.pdb "maxUnavailable" }} + maxUnavailable: {{ .Values.lender.pdb.maxUnavailable }} + {{- else }} + minAvailable: {{ .Values.lender.pdb.minAvailable | default 1 }} + {{- end }} + selector: + matchLabels: + {{- /* Scope to the API component so disruption budgeting counts only API + pods, not lenderConsole (shared name/instance). PDB selector is + mutable, so this applies on upgrade. (The Deployment selector needs + the same discriminator but is immutable — tracked as a follow-up + requiring a recreate; HPA/autoscaling is off on these tiers.) */ -}} + {{- include "lender.selectorLabels" (dict "context" . "name" .Values.lender.name "component" .Values.lender.name) | nindent 6 }} +{{- end }} diff --git a/charts/lender/templates/lender/secrets.yaml b/charts/lender/templates/lender/secrets.yaml new file mode 100644 index 000000000..b50a06549 --- /dev/null +++ b/charts/lender/templates/lender/secrets.yaml @@ -0,0 +1,17 @@ +{{- if and .Values.lender.enabled (not .Values.lender.useExistingSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "lender.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lender.name "name" .Values.lender.name) | nindent 4 }} +type: Opaque +stringData: + POSTGRES_PASSWORD: {{ required "lender.secrets.POSTGRES_PASSWORD is required when lender.useExistingSecret=false (supply explicitly or set useExistingSecret=true)" .Values.lender.secrets.POSTGRES_PASSWORD | quote }} + {{- range $key, $value := .Values.lender.secrets }} + {{- if ne $key "POSTGRES_PASSWORD" }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/lender/templates/lender/service.yaml b/charts/lender/templates/lender/service.yaml new file mode 100644 index 000000000..088879dc5 --- /dev/null +++ b/charts/lender/templates/lender/service.yaml @@ -0,0 +1,27 @@ +{{- if .Values.lender.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "lender.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lender.name "name" .Values.lender.name) | nindent 4 }} + {{- with .Values.lender.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.lender.service.type }} + ports: + - port: {{ .Values.lender.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- /* Include the component discriminator so this API Service selects ONLY + the lender API pods, not the lenderConsole pods (which share + name/instance labels). Without it the Service load-balances /api + traffic onto the console nginx too, yielding intermittent + missing-CORS / SPA-fallback responses. */ -}} + {{- include "lender.selectorLabels" (dict "context" . "name" .Values.lender.name "component" .Values.lender.name) | nindent 4 }} +{{- end }} diff --git a/charts/lender/templates/lender/serviceaccount.yaml b/charts/lender/templates/lender/serviceaccount.yaml new file mode 100644 index 000000000..f43822f2c --- /dev/null +++ b/charts/lender/templates/lender/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if and .Values.lender.enabled .Values.lender.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "lender.serviceAccountName" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lender.name "name" .Values.lender.name) | nindent 4 }} + {{- with .Values.lender.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/lender/templates/lenderConsole/configmap.yaml b/charts/lender/templates/lenderConsole/configmap.yaml new file mode 100644 index 000000000..84b93e7e8 --- /dev/null +++ b/charts/lender/templates/lenderConsole/configmap.yaml @@ -0,0 +1,13 @@ +{{- if and .Values.lenderConsole.enabled .Values.lenderConsole.configmap }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "lenderConsole.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lenderConsole.name "name" .Values.lenderConsole.name) | nindent 4 }} +data: + {{- range $key, $value := .Values.lenderConsole.configmap }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/lender/templates/lenderConsole/deployment.yaml b/charts/lender/templates/lenderConsole/deployment.yaml new file mode 100644 index 000000000..c0a2c09ac --- /dev/null +++ b/charts/lender/templates/lenderConsole/deployment.yaml @@ -0,0 +1,82 @@ +{{- if .Values.lenderConsole.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "lenderConsole.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lenderConsole.name "name" .Values.lenderConsole.name) | nindent 4 }} +spec: + revisionHistoryLimit: {{ if hasKey .Values.lenderConsole "revisionHistoryLimit" }}{{ .Values.lenderConsole.revisionHistoryLimit }}{{ else }}10{{ end }} + replicas: {{ .Values.lenderConsole.replicaCount }} + selector: + matchLabels: + {{- include "lender.selectorLabels" (dict "context" . "name" .Values.lenderConsole.name "component" .Values.lenderConsole.name) | nindent 6 }} + template: + metadata: + {{- with .Values.lenderConsole.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lenderConsole.name "name" .Values.lenderConsole.name) | nindent 8 }} + spec: + {{- with .Values.lenderConsole.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.lenderConsole.podSecurityContext | nindent 8 }} + containers: + - name: {{ include "lenderConsole.fullname" . }} + securityContext: + {{- toYaml .Values.lenderConsole.securityContext | nindent 12 }} + image: "{{ .Values.lenderConsole.image.repository }}:{{ required "lenderConsole.image.tag is required when lenderConsole.enabled=true" .Values.lenderConsole.image.tag }}" + imagePullPolicy: {{ .Values.lenderConsole.image.pullPolicy }} + {{- if .Values.lenderConsole.configmap }} + envFrom: + - configMapRef: + name: {{ include "lenderConsole.fullname" . }} + {{- end }} + {{- with .Values.lenderConsole.extraEnvVars }} + env: + {{- range . }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.lenderConsole.service.port }} + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + {{- toYaml .Values.lenderConsole.resources | nindent 12 }} + {{- with .Values.lenderConsole.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.lenderConsole.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.lenderConsole.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/lender/templates/lenderConsole/ingress.yaml b/charts/lender/templates/lenderConsole/ingress.yaml new file mode 100644 index 000000000..697b3fc9c --- /dev/null +++ b/charts/lender/templates/lenderConsole/ingress.yaml @@ -0,0 +1,44 @@ +{{- if and .Values.lenderConsole.enabled .Values.lenderConsole.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "lenderConsole.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lenderConsole.name "name" .Values.lenderConsole.name) | nindent 4 }} + {{- with .Values.lenderConsole.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.lenderConsole.ingress.className }} + ingressClassName: {{ .Values.lenderConsole.ingress.className }} + {{- end }} + {{- if .Values.lenderConsole.ingress.tls }} + tls: + {{- range .Values.lenderConsole.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.lenderConsole.ingress.hosts }} + {{- if .host }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "lenderConsole.fullname" $ }} + port: + number: {{ $.Values.lenderConsole.service.port }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/lender/templates/lenderConsole/service.yaml b/charts/lender/templates/lenderConsole/service.yaml new file mode 100644 index 000000000..0e9b8f6e2 --- /dev/null +++ b/charts/lender/templates/lenderConsole/service.yaml @@ -0,0 +1,22 @@ +{{- if .Values.lenderConsole.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "lenderConsole.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "lender.labels" (dict "context" . "component" .Values.lenderConsole.name "name" .Values.lenderConsole.name) | nindent 4 }} + {{- with .Values.lenderConsole.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.lenderConsole.service.type }} + ports: + - port: {{ .Values.lenderConsole.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "lender.selectorLabels" (dict "context" . "name" .Values.lenderConsole.name "component" .Values.lenderConsole.name) | nindent 4 }} +{{- end }} diff --git a/charts/lender/values-template.yaml b/charts/lender/values-template.yaml new file mode 100644 index 000000000..4f58a65fd --- /dev/null +++ b/charts/lender/values-template.yaml @@ -0,0 +1,90 @@ +# Template values for lender deployment. +# Copy this file and customize for your environment (GitOps overlay). + +namespaceOverride: "lender" + +global: + externalPostgresDefinitions: + enabled: false + connection: + host: "" + port: "5432" + postgresAdminLogin: + useExistingSecret: + name: "" + username: "" + password: "" + lenderCredentials: + useExistingSecret: + name: "" + password: "" + +lender: + enabled: true + replicaCount: 1 + + image: + repository: ghcr.io/lerianstudio/lender + pullPolicy: IfNotPresent + tag: "" + + imagePullSecrets: + - name: ghcr-credential + + ingress: + # Disabled in the template overlay so the default cannot render a hostless + # Ingress matching arbitrary hostnames. Enable and set a host per environment. + enabled: false + className: "nginx" + annotations: {} + hosts: [] + tls: [] + + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + + configmap: + ENV_NAME: "" + SERVER_ADDRESS: ":4017" + LOG_LEVEL: "info" + POSTGRES_HOST: "" + POSTGRES_PORT: "5432" + POSTGRES_USER: "lender" + POSTGRES_NAME: "lender" + POSTGRES_SSLMODE: "disable" + ALLOW_INSECURE_TLS: "true" + REDIS_HOST: "" + REDIS_DB: "0" + PLUGIN_AUTH_ENABLED: "false" + PLUGIN_AUTH_HOST: "" + ENABLE_TELEMETRY: "false" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + MULTI_TENANT_ENABLED: "false" + + secrets: + POSTGRES_PASSWORD: "" + REDIS_PASSWORD: "" + AUTH_JWT_SECRET: "" + + useExistingSecret: false + existingSecretName: "" + +# UI component — keep disabled until a lender-ui image is wired per environment. +lenderConsole: + enabled: false + image: + repository: ghcr.io/lerianstudio/lender-ui + pullPolicy: IfNotPresent + tag: "" diff --git a/charts/lender/values.schema.json b/charts/lender/values.schema.json new file mode 100644 index 000000000..9c01f9ffc --- /dev/null +++ b/charts/lender/values.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "properties": { + "nameOverride": { + "type": "string" + }, + "fullnameOverride": { + "type": "string" + }, + "namespaceOverride": { + "type": "string" + }, + "global": { + "type": "object", + "additionalProperties": true + }, + "lender": { + "type": "object", + "properties": { + "configmap": { + "type": "object" + }, + "secrets": { + "type": "object" + } + }, + "additionalProperties": true + }, + "lenderConsole": { + "type": "object", + "properties": { + "configmap": { + "type": "object" + } + }, + "additionalProperties": true + } + } +} diff --git a/charts/lender/values.yaml b/charts/lender/values.yaml new file mode 100644 index 000000000..132a72183 --- /dev/null +++ b/charts/lender/values.yaml @@ -0,0 +1,483 @@ +# Default values for lender components. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +# +# Chart is multi-component: +# * lender -> the Go API (main component; image tag lands at lender.image.tag) +# * lenderConsole -> the Vite/React "lender-console" UI (disabled by default; no image exists yet) +nameOverride: "lender" +fullnameOverride: "" +namespaceOverride: "lender" + +global: + # -- Bootstrap job for external PostgreSQL: creates database, role, and grants privileges. + # This job ONLY provisions the DB/role idempotently. Schema migrations are operator-driven + # (cmd/migrate up) and run out-of-band; the runtime image ships no migrate binary. + externalPostgresDefinitions: + # -- Enable or disable the PostgreSQL bootstrap job + enabled: false + # -- PostgreSQL connection settings + connection: + # -- PostgreSQL host + host: "lender-postgresql" + # -- PostgreSQL port + port: "5432" + # -- Admin credentials for PostgreSQL + 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). + # No default: supply explicitly or use useExistingSecret.name. + password: "" + # -- Credentials for the lender role created by the job + lenderCredentials: + useExistingSecret: + # -- Name of existing secret containing DB_PASSWORD_LENDER key + name: "" + # -- Password for the lender role (ignored if useExistingSecret.name is set). + # No default: supply explicitly or use useExistingSecret.name. + password: "" + +lender: + # -- Service name + name: lender + + # -- Service description + description: "Lerian credit journey engine (API)" + + # -- Enable or disable the lender API service + enabled: true + + # -- Number of replicas for the lender service + replicaCount: 1 + + # -- Number of old ReplicaSets to retain for deployment rollback + revisionHistoryLimit: 10 + + image: + # -- Repository for the lender API container image + repository: ghcr.io/lerianstudio/lender + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Image tag used for deployment (release pipeline sets this via lender.tag mapping) + tag: "1.0.0-beta.48" + + # -- Secrets for pulling images from a private registry + imagePullSecrets: + - name: ghcr-credential + + # -- Pod annotations for additional metadata + podAnnotations: {} + + podSecurityContext: {} + # fsGroup: 2000 + + securityContext: + # -- Defines the group ID for the user running the process inside the container + runAsGroup: 1000 + # -- Defines the user ID for the process running inside the container + runAsUser: 1000 + # -- Ensures the process does not run as root + runAsNonRoot: true + capabilities: + drop: + - ALL + # -- Blocks privilege escalation inside the container + allowPrivilegeEscalation: false + # -- Defines the root filesystem as read-only + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault + + # -- PodDisruptionBudget configuration + pdb: + # -- Enable or disable PodDisruptionBudget + enabled: true + # -- Minimum number of available pods + minAvailable: 1 + # -- Maximum number of unavailable pods + maxUnavailable: 1 + # -- Annotations for the PodDisruptionBudget + annotations: {} + + # -- Deployment update strategy + deploymentUpdate: + # -- Type of deployment strategy + type: RollingUpdate + # -- Maximum number of pods that can be created over the desired number of pods + maxSurge: 1 + # -- Maximum number of pods that can be unavailable during the update + maxUnavailable: 1 + + service: + # -- Kubernetes service type + type: ClusterIP + # -- Port for the HTTP API (SERVER_ADDRESS binds 0.0.0.0:4017) + port: 4017 + annotations: {} + + ingress: + # -- Enable or disable ingress + enabled: true + # -- Ingress class name + className: "nginx" + # -- Additional ingress annotations (host/group set by GitOps values) + annotations: + nginx.ingress.kubernetes.io/proxy-buffer-size: "16k" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Lender" + gethomepage.dev/description: "Lerian credit journey engine" + hosts: + - host: "lender.lerian.net" + paths: + - path: / + pathType: Prefix + # -- TLS configuration for ingress + tls: [] + + resources: + # -- CPU and memory limits for pods + limits: + cpu: 500m + memory: 512Mi + # -- Minimum CPU and memory requests + requests: + cpu: 100m + memory: 128Mi + + autoscaling: + # -- Enable or disable horizontal pod autoscaling + enabled: true + # -- Minimum number of replicas + minReplicas: 1 + # -- Maximum number of replicas + maxReplicas: 5 + # -- Target CPU utilization percentage for autoscaling + targetCPUUtilizationPercentage: 80 + # -- Target memory utilization percentage for autoscaling + targetMemoryUtilizationPercentage: 80 + + # -- Node selector for scheduling pods on specific nodes + nodeSelector: {} + + # -- Tolerations for scheduling on tainted nodes + tolerations: {} + + # -- Affinity rules for pod scheduling + affinity: {} + + # -- ConfigMap for non-sensitive environment variables. + # @default -- templates/configmap.yaml + # NOTE: keys mirror config/.env.example (the app config superset). Feature flags default OFF + # so the service boots single-tenant with rabbit/streaming/multi-tenant/outbox disabled. + configmap: + # Application Settings + ENV_NAME: "production" + LOG_LEVEL: "info" + DEPLOYMENT_MODE: "production" + SERVER_ADDRESS: ":4017" + HTTP_BODY_LIMIT_BYTES: "104857600" + + # CORS (legacy names) + CORS_ALLOWED_ORIGINS: "*" + CORS_ALLOWED_METHODS: "GET,POST,PUT,PATCH,DELETE,OPTIONS" + CORS_ALLOWED_HEADERS: "Origin,Content-Type,Accept,Authorization,X-Request-ID,X-Idempotency,Idempotency-Key" + CORS_EXPOSE_HEADERS: "" + CORS_ALLOW_CREDENTIALS: "false" + + # CORS (active lib-commons ACCESS_CONTROL_* names) + ACCESS_CONTROL_ALLOW_ORIGIN: "*" + ACCESS_CONTROL_ALLOW_METHODS: "GET,POST,PUT,PATCH,DELETE,OPTIONS" + ACCESS_CONTROL_ALLOW_HEADERS: "Origin,Content-Type,Accept,Authorization,X-Request-ID,X-Idempotency,Idempotency-Key" + ACCESS_CONTROL_EXPOSE_HEADERS: "" + ACCESS_CONTROL_ALLOW_CREDENTIALS: "false" + + # TLS + TLS_TERMINATED_UPSTREAM: "true" + + # Tenancy + DEFAULT_TENANT_ID: "11111111-1111-1111-1111-111111111111" + + # PostgreSQL (primary, non-secret) + POSTGRES_HOST: "postgres.lender.lerian.net" + POSTGRES_PORT: "5432" + POSTGRES_USER: "lender" + POSTGRES_NAME: "lender" + POSTGRES_SSLMODE: "disable" + # lib-commons refuses a plaintext (sslmode=disable) connection unless this is set. + # Managed data tier is plaintext, so default is "true". + ALLOW_INSECURE_TLS: "true" + MIGRATIONS_PATH: "migrations" + MIGRATIONS_BR_PATH: "internal/jurisdictions/br/migrations" + + # PostgreSQL connection pool + POSTGRES_MAX_OPEN_CONNS: "25" + POSTGRES_MAX_IDLE_CONNS: "5" + POSTGRES_CONN_MAX_LIFETIME_MINS: "30" + POSTGRES_CONN_MAX_IDLE_TIME_MINS: "5" + POSTGRES_CONNECT_TIMEOUT_SEC: "10" + + # Redis / Valkey (non-secret) + REDIS_HOST: "valkey.lender.lerian.net:6379" + REDIS_DB: "0" + REDIS_PROTOCOL: "3" + REDIS_POOL_SIZE: "10" + REDIS_MIN_IDLE_CONNS: "2" + REDIS_READ_TIMEOUT: "3" + REDIS_WRITE_TIMEOUT: "3" + REDIS_DIAL_TIMEOUT: "5" + REDIS_POOL_TIMEOUT: "2" + REDIS_MAX_RETRIES: "3" + REDIS_MIN_RETRY_BACKOFF: "8" + REDIS_MAX_RETRY_BACKOFF: "1" + + # Authentication (lib-auth + plugin-auth) + PLUGIN_AUTH_ENABLED: "false" + PLUGIN_AUTH_HOST: "http://plugin-access-manager-auth:4000" + + # API Documentation (unified Huma OpenAPI 3.1) + SWAGGER_ENABLED: "true" + + # Local resilience sample provider mode + EXAMPLE_STATUS_PROVIDER_MODE: "healthy" + + # Infrastructure boot timeout + INFRA_CONNECT_TIMEOUT_SEC: "30" + + # OpenTelemetry + ENABLE_TELEMETRY: "false" + OTEL_LIBRARY_NAME: "github.com/LerianStudio/lender" + OTEL_RESOURCE_SERVICE_NAME: "lender" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "production" + DB_METRICS_INTERVAL_SEC: "15" + + # Rate limiting + RATE_LIMIT_ENABLED: "true" + RATE_LIMIT_MAX: "500" + RATE_LIMIT_WINDOW_SEC: "60" + AGGRESSIVE_RATE_LIMIT_MAX: "100" + AGGRESSIVE_RATE_LIMIT_WINDOW_SEC: "60" + RELAXED_RATE_LIMIT_MAX: "1000" + RELAXED_RATE_LIMIT_WINDOW_SEC: "60" + ALLOW_RUNTIME_RATELIMIT_DISABLE: "true" + + # Idempotency + IDEMPOTENCY_RETRY_WINDOW_SEC: "300" + + # Pagination + MAX_PAGINATION_LIMIT: "100" + MAX_PAGINATION_MONTH_DATE_RANGE: "3" + + # M2M credentials (non-secret; provider stays off until M2M_TARGET_SERVICE is set) + M2M_CREDENTIAL_CACHE_TTL_SEC: "300" + AWS_REGION: "us-east-1" + + # Circuit breaker starter + CIRCUIT_BREAKER_ENABLED: "false" + + # Borrower collection issuance (off) + COLLECTION_ENABLED: "false" + + # Bounded raw-body collection webhook listener (off) + COLLECTION_WEBHOOK_ENABLED: "false" + COLLECTION_WEBHOOK_ADDRESS: ":9081" + COLLECTION_WEBHOOK_BODY_LIMIT_BYTES: "1048576" + COLLECTION_WEBHOOK_MAX_SKEW_SEC: "300" + COLLECTION_WEBHOOK_UPSTREAM_TLS_TERMINATION_VALIDATED: "false" + + # Outbox starter (off; all knobs non-sensitive) + OUTBOX_ENABLED: "false" + OUTBOX_TABLE_NAME: "outbox_events" + OUTBOX_DISPATCH_INTERVAL_SEC: "2" + OUTBOX_BATCH_SIZE: "50" + OUTBOX_PUBLISH_MAX_ATTEMPTS: "3" + OUTBOX_PUBLISH_BACKOFF_MS: "200" + OUTBOX_RETRY_WINDOW_SEC: "300" + OUTBOX_MAX_DISPATCH_ATTEMPTS: "10" + OUTBOX_PROCESSING_TIMEOUT_SEC: "600" + OUTBOX_MAX_FAILED_PER_BATCH: "25" + OUTBOX_INCLUDE_TENANT_METRICS: "false" + OUTBOX_ALLOW_EMPTY_TENANT: "true" + + # Streaming (Redpanda/Kafka) starter (off; non-secret knobs; SASL creds via secrets passthrough) + STREAMING_ENABLED: "false" + STREAMING_BROKERS: "localhost:9092" + STREAMING_CLIENT_ID: "lender" + STREAMING_CB_FAILURE_RATIO: "0.5" + STREAMING_CB_MIN_REQUESTS: "10" + STREAMING_CB_TIMEOUT_S: "30" + STREAMING_CLOSE_TIMEOUT_S: "30" + STREAMING_TLS_ENABLED: "false" + + # Inbound consignado consumer opt-in flags (all dark by default) + CONSUMER_CONSIGNADO_AVERBACAO_CONFIRMED_ENABLED: "false" + CONSUMER_CONSIGNADO_AVERBACAO_REJECTED_ENABLED: "false" + CONSUMER_CONSIGNADO_EMPLOYMENT_STATUS_REPORTED_ENABLED: "false" + CONSUMER_CONSIGNADO_REDIRECTION_OUTCOME_ENABLED: "false" + CONSUMER_CONSIGNADO_RECONCILIATION_RECEIVED_ENABLED: "false" + CONSUMER_CONSIGNADO_MATCH_RUN_COMPLETED_ENABLED: "false" + + # Consignado exclusion local worker tunables + CONSIGNADO_EXCLUSION_LOCAL_BATCH_SIZE: "50" + CONSIGNADO_EXCLUSION_LOCAL_TENANT_CONCURRENCY: "4" + CONSIGNADO_EXCLUSION_LOCAL_LEASE_SECONDS: "30" + CONSIGNADO_EXCLUSION_LOCAL_BACKOFF_BASE_SECONDS: "1" + CONSIGNADO_EXCLUSION_LOCAL_BACKOFF_MAX_SECONDS: "300" + CONSIGNADO_EXCLUSION_LOCAL_POLL_INTERVAL_MS: "1000" + + # RabbitMQ starter (off; non-secret knobs; RABBITMQ_DEFAULT_PASS via secrets passthrough) + RABBITMQ_ENABLED: "false" + RABBITMQ_HOST: "localhost" + RABBITMQ_PORT_AMQP: "5672" + RABBITMQ_PORT_HOST: "15672" + RABBITMQ_DEFAULT_USER: "lender" + RABBITMQ_VHOST: "/" + RABBITMQ_EXCHANGE: "events" + RABBITMQ_REQUIRE_HEALTH_ALLOWED_HOSTS: "false" + RABBITMQ_ALLOW_INSECURE_HEALTH_CHECK: "false" + RABBITMQ_ALLOW_INSECURE_TLS: "false" + RABBITMQ_PUBLISHER_CONFIRM_TIMEOUT_MS: "5000" + RABBITMQ_PUBLISHER_RECOVERY_INITIAL_MS: "1000" + RABBITMQ_PUBLISHER_RECOVERY_MAX_MS: "30000" + RABBITMQ_PUBLISHER_MAX_RECOVERIES: "10" + + # Multi-tenant starter (off). When enabled, the block below is rendered and + # MULTI_TENANT_URL / MULTI_TENANT_REDIS_HOST become required. + MULTI_TENANT_ENABLED: "false" + + # -- Secrets for storing sensitive data (rendered as stringData; AVP substitutes ). + # Range-based: add any additional credential key here (e.g. RABBITMQ_DEFAULT_PASS, + # MULTI_TENANT_SERVICE_API_KEY, MULTI_TENANT_REDIS_PASSWORD, STREAMING_SASL_PASSWORD) + # to inject it as an env var when you enable the corresponding feature. + # @default -- templates/secrets.yaml + secrets: + # -- PostgreSQL password. No default: supply explicitly (e.g. AVP ) + # or set useExistingSecret=true. Install fails if empty and useExistingSecret=false. + POSTGRES_PASSWORD: "" + # -- Redis / Valkey password (empty if auth disabled) + REDIS_PASSWORD: "" + # -- JWT signing secret + AUTH_JWT_SECRET: "" + + # -- Use an existing secret instead of rendering the secrets block + useExistingSecret: false + existingSecretName: "" + + # -- Extra environment variables (name/value) merged into the ConfigMap. + # GitOps escape hatch: set any additional key without a chart change. + extraEnvVars: [] + + serviceAccount: + # -- Specifies whether a ServiceAccount should be created + create: true + # -- Annotations for the ServiceAccount + annotations: {} + # -- Name of the service account + # @default -- `lender.fullname` + name: "" + +# lender-console (UI) — Vite/React SPA "lender-console". +# DISABLED by default: there is no UI image published today (no UI Dockerfile; +# the release pipeline builds only the Go API image). The skeleton below lets a +# future UI be wired without a chart change; keep enabled=false until an image exists. +lenderConsole: + # -- Enable or disable the lender console (UI) component + enabled: false + + # -- Service name + name: lender-console + + # -- Number of replicas for the console + replicaCount: 1 + + # -- Number of old ReplicaSets to retain for deployment rollback + revisionHistoryLimit: 10 + + image: + # -- Repository for the console (UI) container image. Published as `lender-ui` + # (Vite SPA -> nginx-unprivileged), matching the matcher-ui convention. + repository: ghcr.io/lerianstudio/lender-ui + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Image tag (must be set before enabling the component) + tag: "" + + # -- Secrets for pulling images from a private registry + imagePullSecrets: + - name: ghcr-credential + + # -- Pod annotations for additional metadata + podAnnotations: {} + + podSecurityContext: {} + + securityContext: + # uid/gid 101 = the nginxinc/nginx-unprivileged runtime user (matches the + # lender-ui image and the matcher-ui reference). NOT 1000. + runAsGroup: 101 + runAsUser: 101 + runAsNonRoot: true + capabilities: + drop: + - ALL + allowPrivilegeEscalation: false + # readOnlyRootFilesystem intentionally NOT defaulted here (repo strict + # validator rejects a `false` chart default). nginx-unprivileged needs a + # writable root fs (pid/temp/cache under /tmp and /var/cache/nginx, plus the + # /config.js render at start), so every environment that enables the console + # sets `readOnlyRootFilesystem: false` explicitly in its own values overlay. + # With the key unset the container inherits the Kubernetes default (writable), + # which matches that behavior. Proper fix (emptyDir mounts + readOnly=true, + # as matcher-ui does) is tracked as a follow-up. + seccompProfile: + type: RuntimeDefault + + service: + # -- Kubernetes service type + type: ClusterIP + # -- Port for the UI HTTP server + port: 8080 + annotations: {} + + ingress: + # -- Enable or disable ingress + enabled: true + # -- Ingress class name + className: "nginx" + # -- Additional ingress annotations (host/group set by GitOps values) + annotations: + nginx.ingress.kubernetes.io/proxy-buffer-size: "16k" + hosts: + - host: "lender-console.lerian.net" + paths: + - path: / + pathType: Prefix + # -- TLS configuration for ingress + tls: [] + + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi + + # -- Node selector for scheduling pods on specific nodes + nodeSelector: {} + + # -- Tolerations for scheduling on tainted nodes + tolerations: {} + + # -- Affinity rules for pod scheduling + affinity: {} + + # -- ConfigMap for the console (non-sensitive UI runtime config) + configmap: {} + + # -- Extra environment variables (name/value) for the console + extraEnvVars: [] diff --git a/charts/matcher/CHANGELOG.md b/charts/matcher/CHANGELOG.md index 524334d60..84afb6cfb 100644 --- a/charts/matcher/CHANGELOG.md +++ b/charts/matcher/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to this project will be documented in this file. +## [3.2.0-beta.1] + +### Added + +- Promote UI, MCP, and detached migrations into the chart as first-class, + optional multi-component parts (chart-type `multi-component`). All default to + `enabled: false`: + - `ui.*` — Vite SPA (nginx-unprivileged). UI ingress implements the same-origin + proxy (`/v1` and `/system` → matcher API Service; `/` → UI Service). + - `mcp.*` — Streamable-HTTP MCP relay (stateless, no credentials); independent + version line from the app tag. + - `migrations.*` — ArgoCD PreSync Secret + Job (up-only, single-tenant) applied + before the app Deployment; TCP-wait initContainer; `useExistingSecret`-aware. +- `matcher.secrets.APP_ENC_KEY` (engine credential master key; required in + production) and `matcher.secrets.ACTOR_PII_ENCRYPTION_KEY` (optional). Both are + emitted only when set, keeping the default render byte-identical. +- `matcher.configmap.GOMEMLIMIT` opt-in knob (Go 1.26 soft memory limit). + +### Changed + +- App templates moved into `templates/matcher/` component subdirectory. Rendered + Kubernetes objects are unchanged for existing API consumers, modulo the chart + version string, `# Source:` provenance, and the auth-key changes below. +- ConfigMap now emits `PLUGIN_AUTH_ENABLED` / `PLUGIN_AUTH_ADDRESS` (the app's + canonical v4 names) instead of `AUTH_ENABLED` / `AUTH_SERVICE_ADDRESS`. The + template still reads the legacy `matcher.configmap.AUTH_ENABLED` / + `AUTH_SERVICE_ADDRESS` as a fallback, so an upgrade never silently disables auth. + +### Removed + +- `matcher.secrets.AUTH_JWT_SECRET` — no-op in app v4 (validation delegated to + plugin-auth); no longer emitted into the Secret. Setting it is now ignored + (harmless). See `docs/UPGRADE-3.2.md`. + ## [1.2.0] - 2026-02-25 ### Added diff --git a/charts/matcher/Chart.yaml b/charts/matcher/Chart.yaml index 74e8be5e7..f27fc899a 100644 --- a/charts/matcher/Chart.yaml +++ b/charts/matcher/Chart.yaml @@ -4,7 +4,7 @@ description: A Helm chart for the Matcher reconciliation service type: application annotations: - lerian.studio/chart-type: single-service + lerian.studio/chart-type: multi-component home: https://github.com/LerianStudio/helm @@ -16,8 +16,10 @@ maintainers: - name: "Lerian Studio" email: "support@lerian.studio" -version: 3.0.0 +version: 3.1.0-beta.2 +# NOTE: appVersion is a stale placeholder. The real app image tag is set via +# matcher.image.tag (and per-component ui.image.tag / mcp.image.tag / migrations.image.tag). appVersion: "1.0.0" keywords: diff --git a/charts/matcher/README.md b/charts/matcher/README.md index 80edc33db..a73922d54 100644 --- a/charts/matcher/README.md +++ b/charts/matcher/README.md @@ -2,7 +2,7 @@ ## Chart Contract -- Chart type: `single-service` +- Chart type: `multi-component` - Required secrets: `matcher.secrets.RABBITMQ_PASSWORD` (install fails loud if unset, unless `matcher.useExistingSecret` is enabled). The PostgreSQL primary/replica and Valkey passwords are **single-sourced from their Bitnami subchart Secrets** — `matcher` reads them at runtime from `-postgresql` (keys `password`, `replication-password`) and `-valkey` (key `valkey-password`), so they are not stored in the app Secret. Only set `matcher.secrets.POSTGRES_PASSWORD`, `POSTGRES_REPLICA_PASSWORD`, and `REDIS_PASSWORD` when pointing at **external** infra without an `existingSecret`. `RABBITMQ_PASSWORD` remains operator-provided (see Known limitations). Production database and messaging credentials must not be committed in values files. - Dependency notes: Uses local PostgreSQL, Valkey, and RabbitMQ dependency charts unless external services are configured. - Production overrides: Provide production credentials through chart secrets or existing dependency Secrets where supported; override image tags, ingress, resources, and persistence. @@ -117,6 +117,69 @@ matcher: | `matcher.serviceAccount.annotations` | Annotations for the ServiceAccount. | `{}` | | `matcher.serviceAccount.name` | Name of the service account. | `""` | +### UI (`matcher-ui`) + +Optional web UI (Vite SPA served by nginx-unprivileged). Disabled by default. When enabled, the UI ingress implements a **same-origin proxy** (decision 2026-06-27): the UI image hardcodes a CSP with `connect-src 'self'`, so the API must be served from the **same host** as the UI. On the UI host, `/v1` and `/system` route to the matcher API Service, and `/` routes to the UI Service (single ingress, two backends). This keeps the browser same-origin — no CORS, no CSP violation. Set `ui.env.API_BASE_URL: ""` so the SPA calls `/v1` and `/system` relative. + +| Parameter | Description | Default Value | +|---|---|---| +| `ui.enabled` | Enable the UI component. | `false` | +| `ui.replicaCount` | Number of UI replicas. | `1` | +| `ui.image.repository` | UI image repository. | `"ghcr.io/lerianstudio/matcher-ui"` | +| `ui.image.tag` | UI image tag (**required** when `ui.enabled=true`). | `""` | +| `ui.image.pullPolicy` | Image pull policy. | `"IfNotPresent"` | +| `ui.imagePullSecrets` | Pull secrets for the UI image. | `[]` | +| `ui.securityContext.*` | Container security context. `runAsUser/Group 101`, `readOnlyRootFilesystem: false` (nginx-unprivileged renders `/config.js` at start). | See `values.yaml` | +| `ui.service.type` / `ui.service.port` | Service type / port (also containerPort). | `ClusterIP` / `8080` | +| `ui.env.*` | UI runtime env (rendered verbatim; empty values are emitted). `API_BASE_URL: ""` (same-origin), `TRUSTED_API_ORIGINS`, `AUTH_DISABLED`, optional `WORKOS_CLIENT_ID`. | See `values.yaml` | +| `ui.livenessProbe.path` / `ui.readinessProbe.path` | Probe path. Defaults to `/` (nginx SPA index). Set to `/healthz` only if the deployed UI image exposes it. | `"/"` | +| `ui.ingress.enabled` | Enable the UI ingress (same-origin proxy). | `false` | +| `ui.ingress.className` | Ingress class name. | `"nginx"` | +| `ui.ingress.annotations` | Ingress annotations (e.g. `cert-manager.io/cluster-issuer`). | `{}` | +| `ui.ingress.host` | UI host (**required** when `ui.ingress.enabled=true`). | `""` | +| `ui.ingress.apiPaths` | API prefixes routed to the matcher API Service. | `[/v1, /system]` | +| `ui.ingress.tls.enabled` / `ui.ingress.tls.secretName` | Enable TLS / TLS secret name. | `true` / `"matcher-ui-tls"` | + +### MCP (`matcher-mcp`) + +Optional Model Context Protocol server (Node, Streamable HTTP). Stateless bearer-token relay that holds **no credentials** and forwards to the in-cluster matcher API Service. Independent version line from the app. Disabled by default. + +| Parameter | Description | Default Value | +|---|---|---| +| `mcp.enabled` | Enable the MCP component. | `false` | +| `mcp.replicaCount` | Number of MCP replicas. | `1` | +| `mcp.image.repository` | MCP image repository. | `"ghcr.io/lerianstudio/matcher-mcp"` | +| `mcp.image.tag` | MCP image tag (**required** when `mcp.enabled=true`; independent of the app tag). | `""` | +| `mcp.image.pullPolicy` | Image pull policy. | `"IfNotPresent"` | +| `mcp.imagePullSecrets` | Pull secrets for the MCP image. | `[]` | +| `mcp.securityContext.*` | Container security context. `runAsUser/Group 65532`, `readOnlyRootFilesystem: true`. | See `values.yaml` | +| `mcp.service.type` / `mcp.service.port` | Service type / port (also containerPort). | `ClusterIP` / `4019` | +| `mcp.env.MATCHER_API_URL` | In-cluster matcher API URL. Empty → derived `http://..svc.cluster.local:`. | `""` (derived) | +| `mcp.env.*` | Other MCP env: `MCP_PORT`, `ENABLE_TELEMETRY`, `MCP_UPLOAD_ALLOWED_HOSTS` (SSRF allowlist, off), `MCP_UPLOAD_MAX_FETCH_BYTES`, `MATCHER_TIMEOUT_MS`, `MAX_BODY_BYTES`; add `OTEL_*` as needed. | See `values.yaml` | +| `mcp.livenessProbe.path` / `mcp.readinessProbe.path` | Probe path. | `"/healthz"` | +| `mcp.ingress.enabled` | Enable the MCP ingress. | `false` | +| `mcp.ingress.className` | Ingress class name. | `"nginx"` | +| `mcp.ingress.annotations` | Streaming defaults: `proxy-buffering: "off"`, `proxy-read-timeout: "3600"`. Add issuer as needed. | See `values.yaml` | +| `mcp.ingress.host` | MCP host (**required** when `mcp.ingress.enabled=true`). | `""` | +| `mcp.ingress.tls.enabled` / `mcp.ingress.tls.secretName` | Enable TLS / TLS secret name. | `true` / `"matcher-mcp-tls"` | + +### Detached migrations (`matcher-migrations`) + +Optional ArgoCD **PreSync** Secret + Job that applies the schema (up-only, single-tenant) **before** the app Deployment. matcher v3+ is detached: it refuses to boot against an unmigrated DB, and the app image ships no migration binary. Disabled by default. Requires an operator-provided Postgres password (external PG) or `matcher.useExistingSecret` pointing at a pre-created Secret. **Do not** run this with `MULTI_TENANT_ENABLED=true` (the runner refuses it; the Tenant Manager owns tenant DB migrations). + +| Parameter | Description | Default Value | +|---|---|---| +| `migrations.enabled` | Enable the detached migration PreSync Job. | `false` | +| `migrations.image.repository` | Migration runner image. | `"ghcr.io/lerianstudio/matcher-migrations"` | +| `migrations.image.tag` | Migration image tag. Empty → falls back to `matcher.image.tag`. | `""` | +| `migrations.image.pullPolicy` | Image pull policy. | `"IfNotPresent"` | +| `migrations.imagePullSecrets` | Pull secrets for the migration image. | `[]` | +| `migrations.waitImage` | Image for the TCP-wait initContainer. | `busybox` | +| `migrations.postgres.{host,port,user,database,sslMode}` | Postgres connection. Empty fields fall back to `matcher.configmap.POSTGRES_*` (host derives from the postgresql subchart). | `""` | +| `migrations.postgres.password` | Password for the PreSync Secret. Empty → `matcher.secrets.POSTGRES_PASSWORD`. Ignored when `matcher.useExistingSecret=true` (Job reads the existing Secret). | `""` | +| `migrations.backoffLimit` / `migrations.activeDeadlineSeconds` / `migrations.ttlSecondsAfterFinished` | Job retry / deadline / TTL. | `3` / `600` / `600` | +| `migrations.resources.*` | CPU/Memory requests/limits. | See `values.yaml` | + --- ## Dependencies @@ -171,7 +234,7 @@ The following environment variables can be configured via the `matcher.configmap | `REDIS_HOST` | Redis/Valkey host. Derived collapse-aware from the subchart name; shown for release `matcher`. | `"matcher-valkey-primary.matcher.svc.cluster.local.:6379"` | | `RABBITMQ_HOST` | RabbitMQ host. | `"matcher-rabbitmq.matcher.svc.cluster.local."` | | `RABBITMQ_PORT` | RabbitMQ port. | `"5672"` | -| `AUTH_ENABLED` | Enable authentication. | `"false"` | +| `PLUGIN_AUTH_ENABLED` | Enable authentication (delegated to plugin-auth). Legacy `AUTH_ENABLED` still honored as a fallback. | `"false"` | | `ENABLE_TELEMETRY` | Enable OpenTelemetry. | `"false"` | | `OBJECT_STORAGE_ENDPOINT` | Object storage endpoint. | `"http://matcher-seaweedfs.matcher.svc.cluster.local.:8333"` | | `EXPORT_WORKER_ENABLED` | Enable export worker. | `"true"` | @@ -192,7 +255,9 @@ The following secrets can be configured via the `matcher.secrets` section: | `RABBITMQ_PASSWORD` | RabbitMQ password. Operator-provided (not single-sourced — see Known limitations). | `""` | | `OBJECT_STORAGE_ACCESS_KEY_ID` | Object storage access key. | `""` | | `OBJECT_STORAGE_SECRET_ACCESS_KEY` | Object storage secret key. | `""` | -| `AUTH_JWT_SECRET` | JWT secret for authentication. | `""` | +| `APP_ENC_KEY` | Engine credential master key (base64 32-byte). **Required in production.** Emitted only when set (opt-in). Generate: `openssl rand -base64 32 \| tr -d '\n'`. | `""` | +| `ACTOR_PII_ENCRYPTION_KEY` | Actor PII encryption key (optional, base64 32-byte). Emitted only when set. | `""` | +| `SYSTEMPLANE_SECRET_MASTER_KEY` | AES-256-GCM master key for encrypting secret config values. | `""` | --- diff --git a/charts/matcher/docs/UPGRADE-3.2.md b/charts/matcher/docs/UPGRADE-3.2.md new file mode 100644 index 000000000..58747e615 --- /dev/null +++ b/charts/matcher/docs/UPGRADE-3.2.md @@ -0,0 +1,93 @@ +# Helm Upgrade from v3.1.x to v3.2.x + +## Overview + +`matcher-helm` 3.2.0 promotes the **UI**, **MCP**, and **detached migrations** into +this chart as first-class, optional components, following the Lerian multi-component +house pattern. The chart annotation changes from `single-service` to `multi-component`. + +**This is a backward-compatible (additive) release.** The existing `matcher.*` (API) +values block is unchanged, and all new components default to `enabled: false`. A default +render (no new components enabled) matches 3.1.x except for: + +- the chart version string (`helm.sh/chart: matcher-helm-3.2.0-beta.1`); +- moved-file provenance: the API templates now live under `templates/matcher/`, which + changes the `# Source:` comments and the emit order of two `Secret` documents (the + rendered Kubernetes objects are otherwise unchanged); and +- the deliberate auth changes below — the ConfigMap emits `PLUGIN_AUTH_ENABLED` / + `PLUGIN_AUTH_ADDRESS` in place of `AUTH_ENABLED` / `AUTH_SERVICE_ADDRESS`, and the + `AUTH_JWT_SECRET` Secret key is removed. Values and runtime behavior are preserved + (the app honors both aliases; legacy values still resolve via a fallback). + +No action is required for existing API-only consumers. + +## What changed + +### 1. Template layout (no output change for the API) + +The flat `templates/*.yaml` app manifests moved into component subdirectories: + +- `templates/matcher/` — API (deployment, service, ingress, hpa, pdb, configmap, secrets, serviceaccount) +- `templates/ui/` — UI (deployment, service, ingress) +- `templates/mcp/` — MCP (deployment, service, ingress) +- `templates/migrations/` — PreSync Secret + Job + +`bootstrap-postgres.yaml`, `bootstrap-rabbitmq.yaml`, `rabbitmq_load_definitions.yaml` +and `_helpers.tpl` remain at the `templates/` root. + +### 2. New optional components (default off) + +See `README.md` for the full parameter tables. + +- **`ui.*`** — Vite SPA served by nginx-unprivileged. When `ui.ingress.enabled=true`, + the ingress implements the **same-origin proxy**: `/v1` and `/system` route to the + matcher API Service; `/` routes to the UI Service (single ingress, two backends). The + UI image hardcodes a CSP with `connect-src 'self'`, so the API must be same-origin. +- **`mcp.*`** — Streamable-HTTP MCP relay. Holds no credentials; forwards to the + in-cluster API. Independent version line from the app tag. +- **`migrations.*`** — ArgoCD PreSync Secret (hook-weight `-2`) + Job (hook-weight `-1`) + that applies the schema up-only before the app Deployment. **Never** set + `MULTI_TENANT_ENABLED=true` for the runner. When `matcher.useExistingSecret=true`, + the Job reads `POSTGRES_PASSWORD` from the existing Secret and no PreSync Secret is minted. + +### 3. Secret allowlist + +- **Added `APP_ENC_KEY`** (base64 32-byte engine-credential master key). **Required in + production** per the app. It is now first-class in `matcher.secrets` and is emitted into + the Secret **only when set** (opt-in), so a default render stays byte-identical. Previously + operators had to inject it via an external secret. + Generate: `openssl rand -base64 32 | tr -d '\n'`. +- **Added `ACTOR_PII_ENCRYPTION_KEY`** (optional, base64 32-byte). Emitted only when set. +- **`AUTH_JWT_SECRET` removed** (no-op in app v4 — token validation is delegated to + plugin-auth). It is no longer emitted into the Secret. If you still set + `matcher.secrets.AUTH_JWT_SECRET`, the value is simply ignored (harmless). No action needed. +- **`AUTH_ENABLED` / `AUTH_SERVICE_ADDRESS` renamed to `PLUGIN_AUTH_ENABLED` / + `PLUGIN_AUTH_ADDRESS`** in the ConfigMap (the app's canonical v4 names). The template still + reads the legacy `matcher.configmap.AUTH_ENABLED` / `AUTH_SERVICE_ADDRESS` as a fallback, so + existing values keep working without a lockstep change — migrate to the new keys when + convenient. The rendered ConfigMap now carries the `PLUGIN_AUTH_*` keys instead of `AUTH_*`. + (Security note: because the legacy values are still honored, an upgrade will NOT silently + disable auth on environments that set `AUTH_ENABLED: "true"`.) + +### 4. App env knob + +- **`matcher.configmap.GOMEMLIMIT`** — new opt-in knob for the Go soft memory limit + (Go 1.26 does not auto-detect cgroup limits). Set to ~85% of the pod memory limit + (e.g. `"435MiB"`). Emitted only when non-empty; default render unchanged. + +## Migration steps + +1. **API-only consumers:** none. `helm upgrade` is a no-op change to workloads. +2. **To enable the UI:** set `ui.enabled=true`, `ui.image.tag`, and — for external access — + `ui.ingress.enabled=true` with `ui.ingress.host` and a cert-manager issuer annotation. +3. **To enable the MCP:** set `mcp.enabled=true` and `mcp.image.tag`. +4. **To enable detached migrations:** set `migrations.enabled=true` and provide the + Postgres password via `migrations.postgres.password` (or set `matcher.useExistingSecret`). +5. **Production:** set `matcher.secrets.APP_ENC_KEY` (or provide it via your existing secret). + +## Preview changes before upgrading + +```bash +helm template charts/matcher --set ui.enabled=true --set ui.image.tag= \ + --set mcp.enabled=true --set mcp.image.tag= --set migrations.enabled=true +``` diff --git a/charts/matcher/templates/_helpers.tpl b/charts/matcher/templates/_helpers.tpl index 29c3e1654..4f9e79a8c 100644 --- a/charts/matcher/templates/_helpers.tpl +++ b/charts/matcher/templates/_helpers.tpl @@ -124,6 +124,73 @@ false {{- end -}} {{- end -}} +{{/* +============================================================================== +Component helpers (multi-component: ui / mcp / migrations) +Names are derived from matcher.fullname so cross-references stay deterministic +and release-aware (e.g. the UI ingress routes /v1 to the API Service by name, +and the migration Job reads its PreSync Secret by name). +============================================================================== +*/}} + +{{/* UI fullname (e.g. matcher-ui) */}} +{{- define "matcher-ui.fullname" -}} +{{- printf "%s-ui" (include "matcher.fullname" . | trunc 60 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* UI selector labels */}} +{{- define "matcher-ui.selectorLabels" -}} +app.kubernetes.io/name: {{ include "matcher-ui.fullname" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: ui +{{- end -}} + +{{/* UI labels */}} +{{- define "matcher-ui.labels" -}} +helm.sh/chart: {{ include "matcher.chart" . }} +{{ include "matcher-ui.selectorLabels" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.ui.image.tag }} +app.kubernetes.io/version: {{ . | quote }} +{{- end }} +{{- end -}} + +{{/* MCP fullname (e.g. matcher-mcp) */}} +{{- define "matcher-mcp.fullname" -}} +{{- printf "%s-mcp" (include "matcher.fullname" . | trunc 59 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* MCP selector labels */}} +{{- define "matcher-mcp.selectorLabels" -}} +app.kubernetes.io/name: {{ include "matcher-mcp.fullname" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: mcp +{{- end -}} + +{{/* MCP labels */}} +{{- define "matcher-mcp.labels" -}} +helm.sh/chart: {{ include "matcher.chart" . }} +{{ include "matcher-mcp.selectorLabels" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.mcp.image.tag }} +app.kubernetes.io/version: {{ . | quote }} +{{- end }} +{{- end -}} + +{{/* Migrations fullname (e.g. matcher-migrations) — Job and PreSync Secret reference this */}} +{{- define "matcher-migrations.fullname" -}} +{{- printf "%s-migrations" (include "matcher.fullname" . | trunc 52 | trimSuffix "-") | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* Migrations labels */}} +{{- define "matcher-migrations.labels" -}} +helm.sh/chart: {{ include "matcher.chart" . }} +app.kubernetes.io/name: {{ include "matcher-migrations.fullname" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/component: migrations +{{- end -}} + {{/* Vendored from Bitnami common (charts/common/templates/_names.tpl) so infra Secret/Service names render even when all bundled subcharts are disabled diff --git a/charts/matcher/templates/bootstrap-postgres.yaml b/charts/matcher/templates/bootstrap-postgres.yaml index 5a800c56a..c59981959 100644 --- a/charts/matcher/templates/bootstrap-postgres.yaml +++ b/charts/matcher/templates/bootstrap-postgres.yaml @@ -104,7 +104,7 @@ spec: echo "Role 'matcher' already exists. Skipping creation." else echo "Creating role 'matcher'..." - 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 ROLE matcher LOGIN PASSWORD '$DB_PASSWORD_MATCHER'" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -v pw="$DB_PASSWORD_MATCHER" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "CREATE ROLE matcher LOGIN PASSWORD :'pw'" fi # Create database if not exists diff --git a/charts/matcher/templates/configmap.yaml b/charts/matcher/templates/matcher/configmap.yaml similarity index 92% rename from charts/matcher/templates/configmap.yaml rename to charts/matcher/templates/matcher/configmap.yaml index b57714636..4858fdb62 100644 --- a/charts/matcher/templates/configmap.yaml +++ b/charts/matcher/templates/matcher/configmap.yaml @@ -12,6 +12,11 @@ data: LOG_LEVEL: {{ .Values.matcher.configmap.LOG_LEVEL | default "info" | quote }} SERVER_ADDRESS: {{ .Values.matcher.configmap.SERVER_ADDRESS | default ":8080" | quote }} HTTP_BODY_LIMIT_BYTES: {{ .Values.matcher.configmap.HTTP_BODY_LIMIT_BYTES | default "104857600" | quote }} + {{- if .Values.matcher.configmap.GOMEMLIMIT }} + # Go soft memory limit. Set explicitly (~85% of pod memory limit): Go 1.26 does + # not auto-detect cgroup memory limits. Only emitted when set (opt-in knob). + GOMEMLIMIT: {{ .Values.matcher.configmap.GOMEMLIMIT | quote }} + {{- end }} # CORS Configuration CORS_ALLOWED_ORIGINS: {{ .Values.matcher.configmap.CORS_ALLOWED_ORIGINS | default "*" | quote }} @@ -83,9 +88,12 @@ data: RABBITMQ_VHOST: {{ .Values.matcher.configmap.RABBITMQ_VHOST | default "/" | quote }} RABBITMQ_HEALTH_URL: {{ .Values.matcher.configmap.RABBITMQ_HEALTH_URL | default (printf "http://matcher-rabbitmq.%s.svc.cluster.local.:15672" .Release.Namespace) | quote }} - # Authentication - AUTH_ENABLED: {{ .Values.matcher.configmap.AUTH_ENABLED | default "false" | quote }} - AUTH_SERVICE_ADDRESS: {{ .Values.matcher.configmap.AUTH_SERVICE_ADDRESS | default "http://plugin-access-manager-auth.midaz-plugins.svc.cluster.local.:4000" | quote }} + # Authentication (delegated to plugin-auth in app v4). Canonical names are + # PLUGIN_AUTH_*; the legacy AUTH_ENABLED / AUTH_SERVICE_ADDRESS values are still + # honored as a fallback so consumers can migrate their values without a lockstep + # break. The app also accepts both aliases at runtime. + PLUGIN_AUTH_ENABLED: {{ .Values.matcher.configmap.PLUGIN_AUTH_ENABLED | default .Values.matcher.configmap.AUTH_ENABLED | default "false" | quote }} + PLUGIN_AUTH_ADDRESS: {{ .Values.matcher.configmap.PLUGIN_AUTH_ADDRESS | default .Values.matcher.configmap.AUTH_SERVICE_ADDRESS | default "http://plugin-access-manager-auth.midaz-plugins.svc.cluster.local.:4000" | quote }} # Swagger Documentation SWAGGER_ENABLED: {{ .Values.matcher.configmap.SWAGGER_ENABLED | default "false" | quote }} diff --git a/charts/matcher/templates/deployment.yaml b/charts/matcher/templates/matcher/deployment.yaml similarity index 100% rename from charts/matcher/templates/deployment.yaml rename to charts/matcher/templates/matcher/deployment.yaml diff --git a/charts/matcher/templates/hpa.yaml b/charts/matcher/templates/matcher/hpa.yaml similarity index 100% rename from charts/matcher/templates/hpa.yaml rename to charts/matcher/templates/matcher/hpa.yaml diff --git a/charts/matcher/templates/ingress.yaml b/charts/matcher/templates/matcher/ingress.yaml similarity index 100% rename from charts/matcher/templates/ingress.yaml rename to charts/matcher/templates/matcher/ingress.yaml diff --git a/charts/matcher/templates/pdb.yaml b/charts/matcher/templates/matcher/pdb.yaml similarity index 100% rename from charts/matcher/templates/pdb.yaml rename to charts/matcher/templates/matcher/pdb.yaml diff --git a/charts/matcher/templates/secrets.yaml b/charts/matcher/templates/matcher/secrets.yaml similarity index 84% rename from charts/matcher/templates/secrets.yaml rename to charts/matcher/templates/matcher/secrets.yaml index 7d11c15ac..542910fe1 100644 --- a/charts/matcher/templates/secrets.yaml +++ b/charts/matcher/templates/matcher/secrets.yaml @@ -43,8 +43,18 @@ data: OBJECT_STORAGE_ACCESS_KEY_ID: {{ .Values.matcher.secrets.OBJECT_STORAGE_ACCESS_KEY_ID | default "" | b64enc | quote }} OBJECT_STORAGE_SECRET_ACCESS_KEY: {{ .Values.matcher.secrets.OBJECT_STORAGE_SECRET_ACCESS_KEY | default "" | b64enc | quote }} - # Authentication Secrets - AUTH_JWT_SECRET: {{ .Values.matcher.secrets.AUTH_JWT_SECRET | default "" | b64enc | quote }} + {{- /* + APP_ENC_KEY: engine credential master key (base64 32-byte). REQUIRED in + production per app init_engine.go. First-class as of 3.2.0 — emitted only when + set (opt-in), so a default render stays byte-identical. Provide via values or + an existing Secret. ACTOR_PII_ENCRYPTION_KEY (base64 32-byte) is optional. + */}} + {{- if .Values.matcher.secrets.APP_ENC_KEY }} + APP_ENC_KEY: {{ .Values.matcher.secrets.APP_ENC_KEY | b64enc | quote }} + {{- end }} + {{- if .Values.matcher.secrets.ACTOR_PII_ENCRYPTION_KEY }} + ACTOR_PII_ENCRYPTION_KEY: {{ .Values.matcher.secrets.ACTOR_PII_ENCRYPTION_KEY | b64enc | quote }} + {{- end }} # Systemplane Secrets # REQUIRED: AES-256-GCM master key for encrypting secret config values. diff --git a/charts/matcher/templates/service.yaml b/charts/matcher/templates/matcher/service.yaml similarity index 100% rename from charts/matcher/templates/service.yaml rename to charts/matcher/templates/matcher/service.yaml diff --git a/charts/matcher/templates/serviceaccount.yaml b/charts/matcher/templates/matcher/serviceaccount.yaml similarity index 100% rename from charts/matcher/templates/serviceaccount.yaml rename to charts/matcher/templates/matcher/serviceaccount.yaml diff --git a/charts/matcher/templates/mcp/deployment.yaml b/charts/matcher/templates/mcp/deployment.yaml new file mode 100644 index 000000000..8f3f87b8d --- /dev/null +++ b/charts/matcher/templates/mcp/deployment.yaml @@ -0,0 +1,83 @@ +{{- if .Values.mcp.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "matcher-mcp.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "matcher-mcp.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.mcp.replicaCount | default 1 }} + revisionHistoryLimit: {{ .Values.mcp.revisionHistoryLimit | default 10 }} + selector: + matchLabels: + {{- include "matcher-mcp.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.mcp.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "matcher-mcp.labels" . | nindent 8 }} + spec: + {{- with .Values.mcp.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.mcp.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ include "matcher-mcp.fullname" . }} + image: "{{ .Values.mcp.image.repository }}:{{ required "mcp.image.tag is required when mcp.enabled=true" .Values.mcp.image.tag }}" + imagePullPolicy: {{ .Values.mcp.image.pullPolicy | default "IfNotPresent" }} + securityContext: + {{- toYaml .Values.mcp.securityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.mcp.service.port }} + protocol: TCP + env: + # Stateless bearer-token relay: MCP holds no credentials. It only + # forwards to the in-cluster matcher API Service (no hairpin). + - name: MATCHER_API_URL + value: {{ .Values.mcp.env.MATCHER_API_URL | default (printf "http://%s.%s.svc.cluster.local:%v" (include "matcher.fullname" .) (include "global.namespace" .) .Values.matcher.service.port) | quote }} + {{- range $key, $value := omit .Values.mcp.env "MATCHER_API_URL" }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + livenessProbe: + httpGet: + path: {{ .Values.mcp.livenessProbe.path | default "/healthz" }} + port: http + initialDelaySeconds: {{ .Values.mcp.livenessProbe.initialDelaySeconds | default 10 }} + periodSeconds: {{ .Values.mcp.livenessProbe.periodSeconds | default 30 }} + timeoutSeconds: {{ .Values.mcp.livenessProbe.timeoutSeconds | default 5 }} + successThreshold: {{ .Values.mcp.livenessProbe.successThreshold | default 1 }} + failureThreshold: {{ .Values.mcp.livenessProbe.failureThreshold | default 3 }} + readinessProbe: + httpGet: + path: {{ .Values.mcp.readinessProbe.path | default "/healthz" }} + port: http + initialDelaySeconds: {{ .Values.mcp.readinessProbe.initialDelaySeconds | default 5 }} + periodSeconds: {{ .Values.mcp.readinessProbe.periodSeconds | default 10 }} + timeoutSeconds: {{ .Values.mcp.readinessProbe.timeoutSeconds | default 5 }} + successThreshold: {{ .Values.mcp.readinessProbe.successThreshold | default 1 }} + failureThreshold: {{ .Values.mcp.readinessProbe.failureThreshold | default 3 }} + resources: + {{- toYaml .Values.mcp.resources | nindent 12 }} + {{- with .Values.mcp.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.mcp.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.mcp.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/matcher/templates/mcp/ingress.yaml b/charts/matcher/templates/mcp/ingress.yaml new file mode 100644 index 000000000..437d735e8 --- /dev/null +++ b/charts/matcher/templates/mcp/ingress.yaml @@ -0,0 +1,34 @@ +{{- if and .Values.mcp.enabled .Values.mcp.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "matcher-mcp.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "matcher-mcp.labels" . | nindent 4 }} + {{- with .Values.mcp.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.mcp.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.mcp.ingress.tls.enabled }} + tls: + - hosts: + - {{ required "mcp.ingress.host is required when mcp.ingress.enabled=true" .Values.mcp.ingress.host | quote }} + secretName: {{ .Values.mcp.ingress.tls.secretName }} + {{- end }} + rules: + - host: {{ required "mcp.ingress.host is required when mcp.ingress.enabled=true" .Values.mcp.ingress.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "matcher-mcp.fullname" . }} + port: + number: {{ .Values.mcp.service.port }} +{{- end }} diff --git a/charts/matcher/templates/mcp/service.yaml b/charts/matcher/templates/mcp/service.yaml new file mode 100644 index 000000000..6bbbcc209 --- /dev/null +++ b/charts/matcher/templates/mcp/service.yaml @@ -0,0 +1,22 @@ +{{- if .Values.mcp.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "matcher-mcp.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "matcher-mcp.labels" . | nindent 4 }} + {{- with .Values.mcp.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.mcp.service.type | default "ClusterIP" }} + ports: + - port: {{ .Values.mcp.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "matcher-mcp.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/matcher/templates/migrations/job.yaml b/charts/matcher/templates/migrations/job.yaml new file mode 100644 index 000000000..95b9ce65b --- /dev/null +++ b/charts/matcher/templates/migrations/job.yaml @@ -0,0 +1,98 @@ +{{- if .Values.migrations.enabled }} +# PreSync migration Job. Runs the dedicated, up-only matcher-migrations image +# (entrypoint /postgres-migrations from Dockerfile.migrations) which applies the +# embedded schema and exits. matcher v3+ is DETACHED: it checks migration compat +# on boot and refuses to start against an unmigrated DB, and the app image ships +# no migration binary. hook-weight -1 puts this after the PreSync Secret (-2) and +# before the main-sync matcher Deployment, so the app never boots unmigrated. +# Single-tenant: MULTI_TENANT_ENABLED is left UNSET (the runner refuses MT=true; +# the Tenant Manager owns tenant DB migrations). +{{- $pgHost := .Values.migrations.postgres.host | default .Values.matcher.configmap.POSTGRES_HOST | default (printf "%s-%s.%s.svc.cluster.local." (include "common.names.dependency.fullname" (dict "chartName" "postgresql" "chartValues" .Values.postgresql "context" .)) (.Values.postgresql.primary.name | default "primary") (include "global.namespace" .)) }} +{{- $pgPort := .Values.migrations.postgres.port | default .Values.matcher.configmap.POSTGRES_PORT | default "5432" }} +{{- $pgUser := .Values.migrations.postgres.user | default .Values.matcher.configmap.POSTGRES_USER | default "matcher" }} +{{- $pgDb := .Values.migrations.postgres.database | default .Values.matcher.configmap.POSTGRES_DB | default "matcher" }} +{{- $pgSslMode := .Values.migrations.postgres.sslMode | default .Values.matcher.configmap.POSTGRES_SSLMODE | default "disable" }} +{{- $secretName := ternary .Values.matcher.existingSecretName (include "matcher-migrations.fullname" .) .Values.matcher.useExistingSecret }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "matcher-migrations.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "matcher-migrations.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-weight: "-1" + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded +spec: + backoffLimit: {{ .Values.migrations.backoffLimit | default 3 }} + activeDeadlineSeconds: {{ .Values.migrations.activeDeadlineSeconds | default 600 }} + ttlSecondsAfterFinished: {{ .Values.migrations.ttlSecondsAfterFinished | default 600 }} + template: + metadata: + labels: + {{- include "matcher-migrations.labels" . | nindent 8 }} + spec: + restartPolicy: Never + automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault + {{- with .Values.migrations.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + - name: wait-for-postgres + image: {{ .Values.migrations.waitImage | default "busybox" }} + command: + - /bin/sh + - -c + - > + echo "Waiting for {{ $pgHost }}:{{ $pgPort }}..."; + until nc -z {{ $pgHost }} {{ $pgPort }}; do + echo "{{ $pgHost }}:{{ $pgPort }} not ready, waiting..."; + sleep 5; + done; + echo "{{ $pgHost }}:{{ $pgPort }} is ready!"; + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + containers: + - name: migrations + image: "{{ .Values.migrations.image.repository }}:{{ .Values.migrations.image.tag | default .Values.matcher.image.tag }}" + imagePullPolicy: {{ .Values.migrations.image.pullPolicy | default "IfNotPresent" }} + env: + - name: POSTGRES_HOST + value: {{ $pgHost | quote }} + - name: POSTGRES_PORT + value: {{ $pgPort | quote }} + - name: POSTGRES_USER + value: {{ $pgUser | quote }} + - name: POSTGRES_DB + value: {{ $pgDb | quote }} + - name: POSTGRES_SSLMODE + value: {{ $pgSslMode | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: POSTGRES_PASSWORD + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + {{- toYaml .Values.migrations.resources | nindent 12 }} +{{- end }} diff --git a/charts/matcher/templates/migrations/secrets.yaml b/charts/matcher/templates/migrations/secrets.yaml new file mode 100644 index 000000000..dd5eaeac0 --- /dev/null +++ b/charts/matcher/templates/migrations/secrets.yaml @@ -0,0 +1,24 @@ +{{- if and .Values.migrations.enabled (not .Values.matcher.useExistingSecret) }} +# Dedicated PreSync Secret carrying the Postgres password for the migration Job. +# PreSync hooks run BEFORE the main sync wave, so on a first install the chart's +# own app Secret does not exist yet. This hook Secret (weight -2) is created +# before the migration Job (weight -1). hook-delete-policy is BeforeHookCreation +# only (NOT HookSucceeded) so it survives the whole PreSync phase for the Job to +# read; it is cleaned up at the next sync. Same password as the app Secret, so no +# new exposure. When matcher.useExistingSecret=true this Secret is NOT minted and +# the Job reads POSTGRES_PASSWORD from the operator-provided existing Secret. +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "matcher-migrations.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "matcher-migrations.labels" . | nindent 4 }} + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-weight: "-2" + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +type: Opaque +stringData: + POSTGRES_PASSWORD: {{ .Values.migrations.postgres.password | default .Values.matcher.secrets.POSTGRES_PASSWORD | quote }} +{{- end }} diff --git a/charts/matcher/templates/ui/deployment.yaml b/charts/matcher/templates/ui/deployment.yaml new file mode 100644 index 000000000..d992de294 --- /dev/null +++ b/charts/matcher/templates/ui/deployment.yaml @@ -0,0 +1,112 @@ +{{- if .Values.ui.enabled }} +{{- $image := printf "%s:%s" .Values.ui.image.repository (required "ui.image.tag is required when ui.enabled=true" .Values.ui.image.tag) -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "matcher-ui.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "matcher-ui.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.ui.replicaCount | default 1 }} + revisionHistoryLimit: {{ .Values.ui.revisionHistoryLimit | default 10 }} + selector: + matchLabels: + {{- include "matcher-ui.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.ui.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "matcher-ui.labels" . | nindent 8 }} + spec: + {{- with .Values.ui.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + # Seed the read-only rootfs: copy the image's built static assets into the + # html emptyDir so the entrypoint can render config.js there at start. + - name: copy-html + image: {{ $image | quote }} + imagePullPolicy: {{ .Values.ui.image.pullPolicy | default "IfNotPresent" }} + command: ['sh', '-c', 'cp -a /usr/share/nginx/html/. /html-writable/'] + securityContext: + {{- toYaml .Values.ui.securityContext | nindent 12 }} + volumeMounts: + - name: html + mountPath: /html-writable + containers: + - name: {{ include "matcher-ui.fullname" . }} + image: {{ $image | quote }} + imagePullPolicy: {{ .Values.ui.image.pullPolicy | default "IfNotPresent" }} + securityContext: + {{- toYaml .Values.ui.securityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.ui.service.port }} + protocol: TCP + env: + # The UI image is env-agnostic: the entrypoint renders /config.js at + # start from these vars into the html emptyDir (rootfs stays read-only). + # API_BASE_URL="" -> SPA calls /v1 and /system RELATIVE (same-origin); + # the UI ingress proxies those prefixes to the matcher API Service. + {{- range $key, $value := .Values.ui.env }} + - name: {{ $key }} + value: {{ $value | quote }} + {{- end }} + livenessProbe: + httpGet: + # Default "/" (nginx SPA index): the beleriand-proven path. Set to + # /healthz only if the deployed UI image exposes it. + path: {{ .Values.ui.livenessProbe.path | default "/" }} + port: http + initialDelaySeconds: {{ .Values.ui.livenessProbe.initialDelaySeconds | default 10 }} + periodSeconds: {{ .Values.ui.livenessProbe.periodSeconds | default 20 }} + timeoutSeconds: {{ .Values.ui.livenessProbe.timeoutSeconds | default 5 }} + successThreshold: {{ .Values.ui.livenessProbe.successThreshold | default 1 }} + failureThreshold: {{ .Values.ui.livenessProbe.failureThreshold | default 3 }} + readinessProbe: + httpGet: + path: {{ .Values.ui.readinessProbe.path | default "/" }} + port: http + initialDelaySeconds: {{ .Values.ui.readinessProbe.initialDelaySeconds | default 5 }} + periodSeconds: {{ .Values.ui.readinessProbe.periodSeconds | default 10 }} + timeoutSeconds: {{ .Values.ui.readinessProbe.timeoutSeconds | default 5 }} + successThreshold: {{ .Values.ui.readinessProbe.successThreshold | default 1 }} + failureThreshold: {{ .Values.ui.readinessProbe.failureThreshold | default 3 }} + resources: + {{- toYaml .Values.ui.resources | nindent 12 }} + volumeMounts: + - name: html + mountPath: /usr/share/nginx/html + - name: tmp + mountPath: /tmp + - name: nginx-cache + mountPath: /var/cache/nginx + volumes: + - name: html + emptyDir: {} + - name: tmp + emptyDir: {} + - name: nginx-cache + emptyDir: {} + {{- with .Values.ui.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/matcher/templates/ui/ingress.yaml b/charts/matcher/templates/ui/ingress.yaml new file mode 100644 index 000000000..2d3aa75d6 --- /dev/null +++ b/charts/matcher/templates/ui/ingress.yaml @@ -0,0 +1,50 @@ +{{- if and .Values.ui.enabled .Values.ui.ingress.enabled -}} +{{- $apiSvc := include "matcher.fullname" . -}} +{{- $apiPort := .Values.matcher.service.port -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "matcher-ui.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "matcher-ui.labels" . | nindent 4 }} + {{- with .Values.ui.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ui.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ui.ingress.tls.enabled }} + tls: + - hosts: + - {{ required "ui.ingress.host is required when ui.ingress.enabled=true" .Values.ui.ingress.host | quote }} + secretName: {{ .Values.ui.ingress.tls.secretName }} + {{- end }} + rules: + - host: {{ required "ui.ingress.host is required when ui.ingress.enabled=true" .Values.ui.ingress.host | quote }} + http: + paths: + # SAME-ORIGIN PROXY (decision Fred 2026-06-27): the UI image hardcodes a + # CSP with connect-src 'self', so the API MUST be served from the SAME + # host as the UI (no cross-origin). These prefixes route to the matcher + # API Service; "/" routes to the UI Service. nginx uses longest-prefix + # matching, so /v1 and /system win over /. + {{- range .Values.ui.ingress.apiPaths }} + - path: {{ . }} + pathType: Prefix + backend: + service: + name: {{ $apiSvc }} + port: + number: {{ $apiPort }} + {{- end }} + - path: / + pathType: Prefix + backend: + service: + name: {{ include "matcher-ui.fullname" . }} + port: + number: {{ .Values.ui.service.port }} +{{- end }} diff --git a/charts/matcher/templates/ui/service.yaml b/charts/matcher/templates/ui/service.yaml new file mode 100644 index 000000000..dfa98c94a --- /dev/null +++ b/charts/matcher/templates/ui/service.yaml @@ -0,0 +1,22 @@ +{{- if .Values.ui.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "matcher-ui.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "matcher-ui.labels" . | nindent 4 }} + {{- with .Values.ui.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.ui.service.type | default "ClusterIP" }} + ports: + - port: {{ .Values.ui.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "matcher-ui.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/matcher/values.schema.json b/charts/matcher/values.schema.json index 3dbc6b92d..cbf3022aa 100644 --- a/charts/matcher/values.schema.json +++ b/charts/matcher/values.schema.json @@ -28,6 +28,18 @@ }, "additionalProperties": true }, + "ui": { + "type": "object", + "additionalProperties": true + }, + "mcp": { + "type": "object", + "additionalProperties": true + }, + "migrations": { + "type": "object", + "additionalProperties": true + }, "valkey": { "type": "object", "additionalProperties": true diff --git a/charts/matcher/values.yaml b/charts/matcher/values.yaml index a4ce7a85c..e92c138cb 100644 --- a/charts/matcher/values.yaml +++ b/charts/matcher/values.yaml @@ -203,6 +203,9 @@ matcher: LOG_LEVEL: "info" SERVER_ADDRESS: ":8080" HTTP_BODY_LIMIT_BYTES: "104857600" + # -- Go soft memory limit (opt-in). Set ~85% of the pod memory limit; Go 1.26 + # does not auto-detect cgroup limits. Only emitted when non-empty. e.g. "435MiB" + GOMEMLIMIT: "" # CORS Configuration CORS_ALLOWED_ORIGINS: "*" @@ -276,9 +279,14 @@ matcher: RABBITMQ_VHOST: "/" RABBITMQ_HEALTH_URL: "http://matcher-rabbitmq.matcher.svc.cluster.local.:15672" - # Authentication - AUTH_ENABLED: "false" - AUTH_SERVICE_ADDRESS: "http://plugin-access-manager-auth.midaz-plugins.svc.cluster.local.:4000" + # Authentication (delegated to plugin-auth in app v4). Canonical knobs: + # PLUGIN_AUTH_ENABLED (effective default "false") and PLUGIN_AUTH_ADDRESS + # (effective default the in-cluster plugin-access-manager URL). Leave EMPTY to + # take the default: the configmap template resolves empty -> legacy AUTH_ENABLED / + # AUTH_SERVICE_ADDRESS (fallback for migration) -> hard default. Setting a + # non-empty value here overrides the legacy fallback. + PLUGIN_AUTH_ENABLED: "" + PLUGIN_AUTH_ADDRESS: "" # Swagger Documentation SWAGGER_ENABLED: "false" @@ -375,8 +383,11 @@ matcher: OBJECT_STORAGE_ACCESS_KEY_ID: "" # -- Object Storage secret key OBJECT_STORAGE_SECRET_ACCESS_KEY: "" - # -- JWT secret for authentication - AUTH_JWT_SECRET: "" + # -- Engine credential master key (base64 32-byte). REQUIRED in production. + # Generate with: openssl rand -base64 32 | tr -d '\n'. Emitted only when set. + APP_ENC_KEY: "" + # -- Actor PII encryption key (optional, base64 32-byte). Emitted only when set. + ACTOR_PII_ENCRYPTION_KEY: "" # -- Systemplane AES-256-GCM master key for encrypting secret config values # REQUIRED: Generate with: openssl rand -base64 32 | tr -d '\n' # WARNING: Use a unique value per environment. Never use the default in production. @@ -402,6 +413,244 @@ matcher: # @default -- `matcher.fullname` name: "" +# ============================================================================= +# UI component (matcher-ui) — Vite SPA served by nginx-unprivileged. +# Disabled by default. When enabled, the ingress implements the same-origin +# proxy: /v1 and /system route to the matcher API Service, / routes to the UI. +# ============================================================================= +ui: + # -- Enable the matcher UI component + enabled: false + # -- Number of UI replicas + replicaCount: 1 + # -- Number of old ReplicaSets to retain + revisionHistoryLimit: 10 + image: + # -- UI image repository + repository: ghcr.io/lerianstudio/matcher-ui + # -- UI image tag (REQUIRED when ui.enabled=true) + tag: "" + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Secrets for pulling the UI image from a private registry + imagePullSecrets: [] + # -- Pod annotations + podAnnotations: {} + # -- Pod-level security context. fsGroup 101 makes the emptyDir volumes + # (html / tmp / cache) group-writable by the non-root nginx user. + podSecurityContext: + fsGroup: 101 + # -- Container security context. nginx-unprivileged runs as uid 101 with a + # read-only rootfs; emptyDir volumes back /usr/share/nginx/html (seeded by an + # initContainer so config.js can be rendered at start), /tmp and /var/cache/nginx. + securityContext: + runAsUser: 101 + runAsGroup: 101 + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + service: + # -- Service type + type: ClusterIP + # -- Service port (also the containerPort) + port: 8080 + annotations: {} + # -- UI runtime env. Rendered verbatim into the container. Empty values are + # emitted (API_BASE_URL="" means same-origin). + env: + # -- Empty -> SPA calls /v1 and /system RELATIVE (same-origin proxy) + API_BASE_URL: "" + # -- Trusted API origins (set to the UI host, e.g. https://matcher-ui.example.com) + TRUSTED_API_ORIGINS: "" + # -- Disable UI auth (set "false" when wiring plugin-auth) + AUTH_DISABLED: "true" + # -- Optional: WORKOS_CLIENT_ID (uncomment to set) + # WORKOS_CLIENT_ID: "" + # -- Liveness probe (path defaults to "/", the nginx SPA index) + livenessProbe: + path: "/" + # -- Readiness probe (path defaults to "/") + readinessProbe: + path: "/" + resources: + limits: + cpu: 200m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + ingress: + # -- Enable the UI ingress (same-origin proxy) + enabled: false + # -- Ingress class name + className: "nginx" + # -- Ingress annotations (e.g. cert-manager.io/cluster-issuer) + annotations: {} + # -- UI host (REQUIRED when ui.ingress.enabled=true) + host: "" + # -- API prefixes routed to the matcher API Service (same-origin). nginx + # longest-prefix matching ensures these win over "/". + apiPaths: + - /v1 + - /system + tls: + # -- Enable TLS + enabled: true + # -- TLS secret name + secretName: "matcher-ui-tls" + # -- Node selector + nodeSelector: {} + # -- Tolerations + tolerations: {} + # -- Affinity + affinity: {} + +# ============================================================================= +# MCP component (matcher-mcp) — Node, Streamable HTTP. Stateless bearer-token +# relay (holds NO credentials). Independent version line from the app. +# Disabled by default. +# ============================================================================= +mcp: + # -- Enable the matcher MCP component + enabled: false + # -- Number of MCP replicas + replicaCount: 1 + # -- Number of old ReplicaSets to retain + revisionHistoryLimit: 10 + image: + # -- MCP image repository + repository: ghcr.io/lerianstudio/matcher-mcp + # -- MCP image tag (REQUIRED when mcp.enabled=true; INDEPENDENT of the app tag) + tag: "" + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Secrets for pulling the MCP image from a private registry + imagePullSecrets: [] + # -- Pod annotations + podAnnotations: {} + # -- Pod-level security context (optional) + podSecurityContext: {} + # -- Container security context. Distroless nonroot (uid 65532), read-only rootfs. + securityContext: + runAsUser: 65532 + runAsGroup: 65532 + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + service: + # -- Service type + type: ClusterIP + # -- Service port (also the containerPort) + port: 4019 + annotations: {} + # -- MCP runtime env. MATCHER_API_URL is derived from the release when empty. + env: + # -- In-cluster matcher API URL (empty -> derived: http://..svc.cluster.local:) + MATCHER_API_URL: "" + # -- MCP listen port (keep in sync with mcp.service.port) + MCP_PORT: "4019" + # -- Enable telemetry + ENABLE_TELEMETRY: "false" + # -- SSRF allowlist for upload fetch (empty = off) + MCP_UPLOAD_ALLOWED_HOSTS: "" + # -- Max bytes fetched for uploads + MCP_UPLOAD_MAX_FETCH_BYTES: "10485760" + # -- Upstream matcher API request timeout (ms) + MATCHER_TIMEOUT_MS: "30000" + # -- Max request body bytes + MAX_BODY_BYTES: "1048576" + # -- Liveness probe (path defaults to /healthz) + livenessProbe: + path: "/healthz" + # -- Readiness probe (path defaults to /healthz) + readinessProbe: + path: "/healthz" + resources: + limits: + cpu: 500m + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi + ingress: + # -- Enable the MCP ingress + enabled: false + # -- Ingress class name + className: "nginx" + # -- Ingress annotations. Streaming HTTP defaults: proxy-buffering off, + # long read-timeout. Add cert-manager.io/cluster-issuer as needed. + annotations: + nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + # -- MCP host (REQUIRED when mcp.ingress.enabled=true) + host: "" + tls: + # -- Enable TLS + enabled: true + # -- TLS secret name + secretName: "matcher-mcp-tls" + # -- Node selector + nodeSelector: {} + # -- Tolerations + tolerations: {} + # -- Affinity + affinity: {} + +# ============================================================================= +# Detached migrations (matcher-migrations) — ArgoCD PreSync Secret + Job that +# applies the schema (up-only, single-tenant) before the app Deployment. +# Disabled by default. Requires an operator-provided Postgres password (external +# PG) or matcher.useExistingSecret pointing at a pre-created Secret. +# ============================================================================= +migrations: + # -- Enable the detached migration PreSync Job + enabled: false + image: + # -- Migration runner image repository + repository: ghcr.io/lerianstudio/matcher-migrations + # -- Migration image tag (empty -> falls back to matcher.image.tag) + tag: "" + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Secrets for pulling the migration image from a private registry + imagePullSecrets: [] + # -- Image used by the TCP-wait initContainer + waitImage: busybox + # -- Postgres connection for the migration Job. Empty fields fall back to + # matcher.configmap.POSTGRES_* (host derives from the postgresql subchart). + postgres: + host: "" + port: "" + user: "" + database: "" + sslMode: "" + # -- Password for the PreSync Secret (empty -> matcher.secrets.POSTGRES_PASSWORD). + # Ignored when matcher.useExistingSecret=true (Job reads the existing Secret). + password: "" + # -- Job backoff limit + backoffLimit: 3 + # -- Job active deadline (seconds) + activeDeadlineSeconds: 600 + # -- TTL after the Job finishes (seconds) + ttlSecondsAfterFinished: 600 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 250m + memory: 256Mi + # Valkey (Redis-compatible) configuration valkey: enabled: true diff --git a/charts/midaz/Chart.yaml b/charts/midaz/Chart.yaml index d2be43dad..c4b91d4c3 100644 --- a/charts/midaz/Chart.yaml +++ b/charts/midaz/Chart.yaml @@ -14,7 +14,7 @@ maintainers: email: "support@lerian.studio" # This is the chart version. This version number should be incremented each time you make changes # to he chart and its templates, including the app version. -version: 8.6.0 +version: 8.7.0-beta.1 # This is the version number of the application being deployed. appVersion: "3.7.8" # A list of keywords about the chart. This helps others discover the chart. diff --git a/charts/midaz/templates/bootstrap-postgres.yaml b/charts/midaz/templates/bootstrap-postgres.yaml index c91de15fd..a821c998f 100644 --- a/charts/midaz/templates/bootstrap-postgres.yaml +++ b/charts/midaz/templates/bootstrap-postgres.yaml @@ -119,7 +119,7 @@ spec: echo "Role 'midaz' already exists. Skipping creation." else echo "Creating role 'midaz'..." - 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 ROLE midaz LOGIN PASSWORD '$DB_PASSWORD_MIDAZ'" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -v pw="$DB_PASSWORD_MIDAZ" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "CREATE ROLE midaz LOGIN PASSWORD :'pw'" fi # Privileges (safe to run repeatedly) diff --git a/charts/otel-collector-lerian/Chart.yaml b/charts/otel-collector-lerian/Chart.yaml index e6d732b91..fbe7f4318 100644 --- a/charts/otel-collector-lerian/Chart.yaml +++ b/charts/otel-collector-lerian/Chart.yaml @@ -18,7 +18,7 @@ maintainers: # 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: 4.1.0 +version: 4.2.0-beta.1 # This is the version number of the application being deployed. appVersion: "0.1.0" diff --git a/charts/plugin-access-manager/Chart.yaml b/charts/plugin-access-manager/Chart.yaml index 837393440..54212d916 100644 --- a/charts/plugin-access-manager/Chart.yaml +++ b/charts/plugin-access-manager/Chart.yaml @@ -11,7 +11,7 @@ maintainers: 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: 8.3.0 +version: 8.4.0-beta.3 # This is the version number of the application being deployed. appVersion: "2.6.7" # A list of keywords about the chart. This helps others discover the chart. diff --git a/charts/plugin-access-manager/templates/auth-backend/configmap.yaml b/charts/plugin-access-manager/templates/auth-backend/configmap.yaml index 42793b60d..386a2f637 100644 --- a/charts/plugin-access-manager/templates/auth-backend/configmap.yaml +++ b/charts/plugin-access-manager/templates/auth-backend/configmap.yaml @@ -19,4 +19,14 @@ data: enableGzip: "true" quota: '{"organization": -1, "user": -1, "application": -1, "provider": -1}' logConfig: '{"filename": "logs/casdoor.log", "maxdays": 99999, "perm": "0770"}' - initDataFile: {{ .Values.auth.backend.initDataFile | default "./init_data.json" | quote }} \ No newline at end of file + initDataFile: {{ .Values.auth.backend.initDataFile | default "./init_data.json" | quote }} + # Allows the backend's outbox Postgres client to connect without a valid + # certificate. Same default and rationale as auth.configmap.ALLOW_INSECURE_TLS + # and identity.configmap.ALLOW_INSECURE_TLS. Casdoor ignores this key; only + # caradhras' outbox client reads it, and it fails closed without it. + ALLOW_INSECURE_TLS: {{ .Values.auth.backend.allowInsecureTLS | default "true" | quote }} + + # Extra Env Vars + {{- with .Values.auth.backend.extraEnvVars }} + {{- toYaml . | nindent 2 }} + {{- end }} \ No newline at end of file diff --git a/charts/plugin-access-manager/templates/auth-backend/migrations.yaml b/charts/plugin-access-manager/templates/auth-backend/migrations.yaml index fb3d69e24..7387e4766 100644 --- a/charts/plugin-access-manager/templates/auth-backend/migrations.yaml +++ b/charts/plugin-access-manager/templates/auth-backend/migrations.yaml @@ -68,4 +68,36 @@ spec: configMapKeyRef: name: {{ include "plugin-auth.fullname" . }} key: DB_SSLMODE + # POSTGRES_* mirrors of the DB_* vars above, mapped from the same + # ConfigMap/Secret keys. The caradhras migration runner + # (cmd/postgres-migrations) reads POSTGRES_HOST/PORT/USER/PASSWORD/DB/SSLMODE + # instead of Casdoor's DB_* contract; keeping both sets lets this single + # Job template run either migrations image via auth.backend.migrations.image, + # with no functional effect on casdoor-migrations (which ignores POSTGRES_*). + - name: POSTGRES_HOST + valueFrom: + configMapKeyRef: + name: {{ include "plugin-auth.fullname" . }} + key: DB_HOST + - name: POSTGRES_PORT + valueFrom: + configMapKeyRef: + name: {{ include "plugin-auth.fullname" . }} + key: DB_PORT + - name: POSTGRES_DB + valueFrom: + configMapKeyRef: + name: {{ include "plugin-auth.fullname" . }} + key: DB_NAME + - name: POSTGRES_USER + valueFrom: + configMapKeyRef: + name: {{ include "plugin-auth.fullname" . }} + key: DB_USER + {{- include "plugin-auth.dbPasswordEnv" (dict "context" $ "envName" "POSTGRES_PASSWORD") | nindent 12 }} + - name: POSTGRES_SSLMODE + valueFrom: + configMapKeyRef: + name: {{ include "plugin-auth.fullname" . }} + key: DB_SSLMODE restartPolicy: OnFailure \ No newline at end of file diff --git a/charts/plugin-br-bank-transfer/Chart.yaml b/charts/plugin-br-bank-transfer/Chart.yaml index 79e959667..bf5adeb9c 100644 --- a/charts/plugin-br-bank-transfer/Chart.yaml +++ b/charts/plugin-br-bank-transfer/Chart.yaml @@ -11,11 +11,8 @@ sources: maintainers: - name: "Lerian Studio" email: "support@lerian.studio" - -version: 1.3.0 - +version: 1.4.0-beta.1 appVersion: "1.1.9" - keywords: - bank-transfer - ted diff --git a/charts/plugin-br-bank-transfer/values.yaml b/charts/plugin-br-bank-transfer/values.yaml index c14ae1028..3b6561b67 100644 --- a/charts/plugin-br-bank-transfer/values.yaml +++ b/charts/plugin-br-bank-transfer/values.yaml @@ -226,12 +226,10 @@ bankTransfer: APPLICATION_NAME: "plugin-br-bank-transfer" SERVER_ADDRESS: ":4027" HTTP_BODY_LIMIT_BYTES: "1048576" - # ================================================================= # TLS / proxy # ================================================================= TLS_TERMINATED_UPSTREAM: "true" - # ================================================================= # CORS policy # In production the plugin's validator rejects wildcard "*" and any @@ -245,7 +243,6 @@ bankTransfer: CORS_ALLOWED_ORIGINS: "" CORS_ALLOWED_METHODS: "" CORS_ALLOWED_HEADERS: "" - # ================================================================= # Multi-tenancy # Only the toggle, the DEFAULT_TENANT_* identifiers, and the @@ -301,7 +298,6 @@ bankTransfer: REDIS_WRITE_TIMEOUT_MS: "3000" REDIS_DIAL_TIMEOUT_MS: "5000" REDIS_TLS: "false" - # ================================================================= # MongoDB (audit/event persistence) # MONGO_URI is in templates/secrets.yaml. @@ -311,7 +307,6 @@ bankTransfer: MONGO_MAX_POOL_SIZE: "25" MONGO_SERVER_SELECTION_TIMEOUT_MS: "3000" MONGO_HEARTBEAT_INTERVAL_MS: "10000" - # ================================================================= # RabbitMQ (event bus, optional) # RABBITMQ_URL is rendered from chart defaults when empty. @@ -319,13 +314,11 @@ bankTransfer: # ================================================================= RABBITMQ_ENABLED: "false" RABBITMQ_EXCHANGE: "bank_transfer.lifecycle" - # ================================================================= # Authentication (inbound — plugin-access-manager) # ================================================================= PLUGIN_AUTH_ENABLED: "true" PLUGIN_AUTH_ADDRESS: "http://plugin-access-manager-auth.midaz-plugins.svc.cluster.local:4000" - # ================================================================= # License gateway override (optional — defaults to lib-license-go's # hard-coded endpoint when LICENSE_SERVICE_ADDRESS is unset) @@ -341,7 +334,6 @@ bankTransfer: OTEL_RESOURCE_SERVICE_NAME: "plugin-br-bank-transfer" OTEL_EXPORTER_OTLP_ENDPOINT: "" OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "production" - # ================================================================= # Outbound adapters — endpoints + auth toggles only. # Timeouts, retries, circuit breakers, cache TTLs, and lookup @@ -375,14 +367,12 @@ bankTransfer: # JD_LEGACY_CODE: "" JD_SIGNING_MODE: "local_pem" # local_pem | external_provided | disabled JD_POLLING_ENABLED: "false" - # ================================================================= # Single-tenant defaults # ================================================================= ORGANIZATION_ID: "" ORGANIZATION_IDS: "" IS_DEVELOPMENT: "false" - # ================================================================= # Feature flags # ================================================================= @@ -391,7 +381,6 @@ bankTransfer: RATE_LIMIT_ENABLED: "false" ALLOW_INSECURE_TLS: "true" OTEL_SDK_DISABLED: "false" - # -- Secrets for storing sensitive data # @default -- templates/secrets.yaml secrets: @@ -614,4 +603,3 @@ rabbitmq: mountPath: /etc/rabbitmq/definitions customConfig: | management.load_definitions = /etc/rabbitmq/definitions/load_definition.json - \ No newline at end of file diff --git a/charts/plugin-br-payments/CHANGELOG.md b/charts/plugin-br-payments/CHANGELOG.md index f70a3eb67..d89386e31 100644 --- a/charts/plugin-br-payments/CHANGELOG.md +++ b/charts/plugin-br-payments/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this chart will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this chart adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0-beta.5] — Unreleased + +### Changed + +- Renamed the Midaz Ledger configmap key to a single `app.configmap.MIDAZ_LEDGER_URL`, + matching the app refactor that reads one Ledger plane URL instead of the former + `MIDAZ_ONBOARDING_URL` + `MIDAZ_TRANSACTION_URL` pair. Updated the required-value + validation (`_helpers.tpl`), `NOTES.txt`, `values.yaml`, `values-template.yaml`, + and `README.md`. + +### Deprecated + +- `app.configmap.MIDAZ_ONBOARDING_URL` and `app.configmap.MIDAZ_TRANSACTION_URL` + are deprecated. They are still accepted as a fallback (validation passes when + both are set) for environments that have not yet migrated to `MIDAZ_LEDGER_URL`. + Remove once all overlays use the single key. + ## [1.0.0-beta.2] — Unreleased ### Changed diff --git a/charts/plugin-br-payments/Chart.yaml b/charts/plugin-br-payments/Chart.yaml index 19fe58c61..e294feb75 100644 --- a/charts/plugin-br-payments/Chart.yaml +++ b/charts/plugin-br-payments/Chart.yaml @@ -15,7 +15,7 @@ maintainers: # 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.1.0-beta.3 +version: 1.1.0-beta.5 # This is the version number of the application being deployed. appVersion: "1.0.0-beta.9" diff --git a/charts/plugin-br-payments/README.md b/charts/plugin-br-payments/README.md index 1b9e359b0..ae1ce527e 100644 --- a/charts/plugin-br-payments/README.md +++ b/charts/plugin-br-payments/README.md @@ -56,8 +56,7 @@ The chart **fails fast** on `helm install` if any of the following are missing: | `app.configmap.OUTBOX_ENABLED` | Must be `"true"`. The plugin only registers HTTP routes when the outbox is enabled. | | `app.configmap.BTG_API_BASE_URL` | BTG API base URL. | | `app.configmap.BTG_AUTH_URL` | BTG OAuth2 token URL. | -| `app.configmap.MIDAZ_ONBOARDING_URL` | Midaz onboarding service URL. | -| `app.configmap.MIDAZ_TRANSACTION_URL` | Midaz transaction service URL. | +| `app.configmap.MIDAZ_LEDGER_URL` | Midaz Ledger service URL (single plane). The deprecated `MIDAZ_ONBOARDING_URL` + `MIDAZ_TRANSACTION_URL` pair is still accepted as a fallback. | | `app.secrets.BTG_CLIENT_ID` | BTG OAuth2 client ID. | | `app.secrets.BTG_CLIENT_SECRET` | BTG OAuth2 client secret. | | `app.secrets.BTG_WEBHOOK_SECRET` | Bearer token for incoming BTG webhooks. | diff --git a/charts/plugin-br-payments/templates/NOTES.txt b/charts/plugin-br-payments/templates/NOTES.txt index 3f43eb57d..906e89e58 100644 --- a/charts/plugin-br-payments/templates/NOTES.txt +++ b/charts/plugin-br-payments/templates/NOTES.txt @@ -46,8 +46,7 @@ The following values MUST be set for the deployment to start successfully: - app.configmap.OUTBOX_ENABLED (must be "true" — HTTP routes only register when the outbox is enabled) - app.configmap.BTG_API_BASE_URL (BTG API base URL) - app.configmap.BTG_AUTH_URL (BTG OAuth2 token URL) - - app.configmap.MIDAZ_ONBOARDING_URL (Midaz onboarding service URL) - - app.configmap.MIDAZ_TRANSACTION_URL (Midaz transaction service URL) + - app.configmap.MIDAZ_LEDGER_URL (Midaz Ledger service URL; deprecated MIDAZ_ONBOARDING_URL + MIDAZ_TRANSACTION_URL pair still accepted as a fallback) - app.secrets.BTG_CLIENT_ID (BTG OAuth2 client ID) - app.secrets.BTG_CLIENT_SECRET (BTG OAuth2 client secret) - app.secrets.BTG_WEBHOOK_SECRET (BTG webhook bearer token) diff --git a/charts/plugin-br-payments/templates/_helpers.tpl b/charts/plugin-br-payments/templates/_helpers.tpl index f44e058c1..1a616af3d 100644 --- a/charts/plugin-br-payments/templates/_helpers.tpl +++ b/charts/plugin-br-payments/templates/_helpers.tpl @@ -181,13 +181,17 @@ plugin-br-payments README. {{- fail "\n\nERROR: app.secrets.BTG_WEBHOOK_SECRET is REQUIRED.\n Set the BTG webhook bearer token in the secrets section.\n" }} {{- end }} -{{/* Midaz Ledger URLs — required for production */}} -{{- if not .Values.app.configmap.MIDAZ_ONBOARDING_URL }} -{{- fail "\n\nERROR: app.configmap.MIDAZ_ONBOARDING_URL is REQUIRED.\n Set the Midaz onboarding service URL.\n" }} +{{/* Midaz Ledger URL — required for production. + Preferred: app.configmap.MIDAZ_LEDGER_URL (single Ledger plane URL; the + app now serves onboarding + transaction from one plane). + DEPRECATED fallback: MIDAZ_ONBOARDING_URL + MIDAZ_TRANSACTION_URL (the + former split pair). Still accepted for backward compatibility with + environments that have not migrated yet; remove once all overlays use + MIDAZ_LEDGER_URL. */}} +{{- if not .Values.app.configmap.MIDAZ_LEDGER_URL }} +{{- if not (and .Values.app.configmap.MIDAZ_ONBOARDING_URL .Values.app.configmap.MIDAZ_TRANSACTION_URL) }} +{{- fail "\n\nERROR: app.configmap.MIDAZ_LEDGER_URL is REQUIRED.\n Set the Midaz Ledger service URL.\n (Deprecated: the former MIDAZ_ONBOARDING_URL + MIDAZ_TRANSACTION_URL pair is still accepted as a fallback.)\n" }} {{- end }} - -{{- if not .Values.app.configmap.MIDAZ_TRANSACTION_URL }} -{{- fail "\n\nERROR: app.configmap.MIDAZ_TRANSACTION_URL is REQUIRED.\n Set the Midaz transaction service URL.\n" }} {{- end }} {{/* PostgreSQL password is single-sourced from the postgresql subchart Secret diff --git a/charts/plugin-br-payments/templates/bootstrap-postgres.yaml b/charts/plugin-br-payments/templates/bootstrap-postgres.yaml index 0d85342e0..b257903de 100644 --- a/charts/plugin-br-payments/templates/bootstrap-postgres.yaml +++ b/charts/plugin-br-payments/templates/bootstrap-postgres.yaml @@ -94,10 +94,10 @@ spec: 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 + if PGPASSWORD="$DB_ADMIN_PASSWORD" psql -At -v ON_ERROR_STOP=1 -v db="$APP_DB" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "SELECT 1 FROM pg_database WHERE datname=:'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 + if PGPASSWORD="$DB_ADMIN_PASSWORD" psql -At -v ON_ERROR_STOP=1 -v role="$APP_ROLE" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "SELECT 1 FROM pg_roles WHERE rolname=:'role'" | grep -q 1; then ROLE_EXISTS=1 fi diff --git a/charts/plugin-br-payments/values-template.yaml b/charts/plugin-br-payments/values-template.yaml index 141d5798c..9423ec5d0 100644 --- a/charts/plugin-br-payments/values-template.yaml +++ b/charts/plugin-br-payments/values-template.yaml @@ -35,9 +35,9 @@ app: # BTG Provider integration (REQUIRED) BTG_API_BASE_URL: "" BTG_AUTH_URL: "" - # Midaz Ledger (REQUIRED) - MIDAZ_ONBOARDING_URL: "" - MIDAZ_TRANSACTION_URL: "" + # Midaz Ledger (REQUIRED) — single Ledger plane URL. The deprecated + # MIDAZ_ONBOARDING_URL + MIDAZ_TRANSACTION_URL pair is still accepted. + MIDAZ_LEDGER_URL: "" # Auth PLUGIN_AUTH_ENABLED: "true" PLUGIN_AUTH_ADDRESS: "" diff --git a/charts/plugin-br-payments/values.yaml b/charts/plugin-br-payments/values.yaml index 3e81c8316..925ccc49b 100644 --- a/charts/plugin-br-payments/values.yaml +++ b/charts/plugin-br-payments/values.yaml @@ -238,9 +238,10 @@ app: BTG_API_BASE_URL: "" BTG_AUTH_URL: "" BTG_TOKEN_REFRESH_INTERVAL: "1h" - # Midaz Ledger URLs (REQUIRED for production - validated by helper) - MIDAZ_ONBOARDING_URL: "" - MIDAZ_TRANSACTION_URL: "" + # Midaz Ledger URL (REQUIRED for production - validated by helper). + # Single Ledger plane URL; the deprecated MIDAZ_ONBOARDING_URL + + # MIDAZ_TRANSACTION_URL pair is still accepted as a fallback. + MIDAZ_LEDGER_URL: "" # Reconciliation worker (in-process) RECONCILIATION_INTERVAL: "5m" RECONCILIATION_LOOKBACK_HOURS: "24" diff --git a/charts/plugin-br-pix-direct-jd/Chart.yaml b/charts/plugin-br-pix-direct-jd/Chart.yaml index 0312737bb..90aa98b02 100644 --- a/charts/plugin-br-pix-direct-jd/Chart.yaml +++ b/charts/plugin-br-pix-direct-jd/Chart.yaml @@ -11,7 +11,7 @@ maintainers: 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: 3.0.0 +version: 3.1.0-beta.1 # This is the version number of the application being deployed. appVersion: "1.2.1-beta.11" diff --git a/charts/plugin-br-pix-indirect-btg/CHANGELOG.md b/charts/plugin-br-pix-indirect-btg/CHANGELOG.md index 250be8676..9f52e1bcb 100644 --- a/charts/plugin-br-pix-indirect-btg/CHANGELOG.md +++ b/charts/plugin-br-pix-indirect-btg/CHANGELOG.md @@ -3,6 +3,8 @@ ## [3.4.0](https://github.com/LerianStudio/helm/releases/tag/plugin-br-pix-indirect-btg-v3.4.0) - **Features:** + - Added the `schedule` (Pix recurrence) worker: new `schedule` values section, Deployment, Service, ConfigMap, Secret, HPA and PDB templates, mirroring the reconciliation worker. + - The worker listens on port 4018 with probes on `/health` (liveness) and `/readyz` (readiness) and ships active by default (`SCHEDULE_WORKER_ENABLED=true`, 15m interval). - Released version 3.4.0 of plugin-br-pix-indirect-btg. - **Fixes:** diff --git a/charts/plugin-br-pix-indirect-btg/Chart.yaml b/charts/plugin-br-pix-indirect-btg/Chart.yaml index a75e20e93..195f94bbe 100644 --- a/charts/plugin-br-pix-indirect-btg/Chart.yaml +++ b/charts/plugin-br-pix-indirect-btg/Chart.yaml @@ -11,7 +11,7 @@ maintainers: 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: 3.4.0 +version: 3.5.0-beta.1 # This is the version number of the application being deployed. appVersion: "1.7.6" # A list of keywords about the chart. This helps others discover the chart. diff --git a/charts/plugin-br-pix-indirect-btg/templates/_helpers.tpl b/charts/plugin-br-pix-indirect-btg/templates/_helpers.tpl index 43678b6b6..94e0d2ce3 100644 --- a/charts/plugin-br-pix-indirect-btg/templates/_helpers.tpl +++ b/charts/plugin-br-pix-indirect-btg/templates/_helpers.tpl @@ -41,6 +41,13 @@ Expand the name for reconciliation worker. {{- default "plugin-br-pix-indirect-btg-worker-reconciliation" .Values.reconciliation.name | trunc 63 | trimSuffix "-" }} {{- end }} +{{/* +Expand the name for schedule worker. +*/}} +{{- define "schedule.name" -}} +{{- default "plugin-br-pix-indirect-btg-worker-schedule" .Values.schedule.name | trunc 63 | trimSuffix "-" }} +{{- end }} + {{/* ================================================================================ FULLNAME HELPERS @@ -80,6 +87,13 @@ Create a default fully qualified app name for reconciliation worker. {{- default "plugin-br-pix-indirect-btg-worker-reconciliation" .Values.reconciliation.name | trunc 63 | trimSuffix "-" }} {{- end }} +{{/* +Create a default fully qualified app name for schedule worker. +*/}} +{{- define "schedule.fullname" -}} +{{- include "schedule.name" . }} +{{- end }} + {{/* ================================================================================ CHART HELPERS @@ -114,6 +128,13 @@ Create chart name for reconciliation. {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} +{{/* +Create chart name for schedule. +*/}} +{{- define "schedule.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + {{/* Create app version. */}} @@ -167,6 +188,16 @@ app.kubernetes.io/version: {{ include "plugin.version" .context }} app.kubernetes.io/managed-by: {{ .context.Release.Service }} {{- end }} +{{/* +Common labels for schedule +*/}} +{{- define "schedule.labels" -}} +helm.sh/chart: {{ include "schedule.chart" .context }} +{{ include "schedule.selectorLabels" (dict "context" .context "name" .name) }} +app.kubernetes.io/version: {{ include "plugin.version" .context }} +app.kubernetes.io/managed-by: {{ .context.Release.Service }} +{{- end }} + {{/* ================================================================================ SELECTOR LABEL HELPERS @@ -216,6 +247,16 @@ app.kubernetes.io/name: {{ include "reconciliation.name" .context }} app.kubernetes.io/instance: {{ .context.Release.Name }} {{- end }} +{{/* +Selector labels for schedule +*/}} +{{- define "schedule.selectorLabels" -}} +{{- if .name -}} +app.kubernetes.io/name: {{ include "schedule.name" .context }} +{{- end }} +app.kubernetes.io/instance: {{ .context.Release.Name }} +{{- end }} + {{/* ================================================================================ SERVICE ACCOUNT HELPER diff --git a/charts/plugin-br-pix-indirect-btg/templates/inbound/configmap.yaml b/charts/plugin-br-pix-indirect-btg/templates/inbound/configmap.yaml index 8c3a9296c..c9b86f91c 100644 --- a/charts/plugin-br-pix-indirect-btg/templates/inbound/configmap.yaml +++ b/charts/plugin-br-pix-indirect-btg/templates/inbound/configmap.yaml @@ -229,6 +229,46 @@ data: WEBHOOK_ENTITY_PIX_INTERNAL_TRANSACTIONS_REPORT_MAX_PENDING_TIME: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_INTERNAL_TRANSACTIONS_REPORT_MAX_PENDING_TIME | default "10m" | quote }} WEBHOOK_ENTITY_PIX_INTERNAL_TRANSACTIONS_REPORT_MAX_CONCURRENT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_INTERNAL_TRANSACTIONS_REPORT_MAX_CONCURRENT | default "2" | quote }} + # PIX_RECURRENCE_PAYER - Pix recurrence payer webhooks (Low Throughput) + WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_ENABLED: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_ENABLED | default "true" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_WORKER_COUNT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_WORKER_COUNT | default "1" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_BATCH_SIZE: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_BATCH_SIZE | default "50" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_POLLING_INTERVAL: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_POLLING_INTERVAL | default "5s" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_PROCESSING_TIMEOUT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_PROCESSING_TIMEOUT | default "12s" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_MAX_RETRIES: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_MAX_RETRIES | default "5" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_MAX_PENDING_TIME: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_MAX_PENDING_TIME | default "2m" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_MAX_CONCURRENT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_PAYER_MAX_CONCURRENT | default "2" | quote }} + + # PIX_RECURRENCE_COLLECTION - Pix recurrence collection webhooks (Low Throughput) + WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_ENABLED: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_ENABLED | default "true" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_WORKER_COUNT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_WORKER_COUNT | default "1" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_BATCH_SIZE: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_BATCH_SIZE | default "50" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_POLLING_INTERVAL: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_POLLING_INTERVAL | default "5s" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_PROCESSING_TIMEOUT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_PROCESSING_TIMEOUT | default "12s" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_MAX_RETRIES: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_MAX_RETRIES | default "5" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_MAX_PENDING_TIME: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_MAX_PENDING_TIME | default "2m" | quote }} + WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_MAX_CONCURRENT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_PIX_RECURRENCE_COLLECTION_MAX_CONCURRENT | default "2" | quote }} + + # SCHEDULE_TERMINAL_EVENT - Schedule terminal events intra-PSP (Low Throughput) + WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_ENABLED: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_ENABLED | default "true" | quote }} + WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_WORKER_COUNT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_WORKER_COUNT | default "1" | quote }} + WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_BATCH_SIZE: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_BATCH_SIZE | default "50" | quote }} + WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_POLLING_INTERVAL: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_POLLING_INTERVAL | default "5s" | quote }} + WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_PROCESSING_TIMEOUT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_PROCESSING_TIMEOUT | default "12s" | quote }} + WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_MAX_RETRIES: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_MAX_RETRIES | default "5" | quote }} + WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_MAX_PENDING_TIME: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_MAX_PENDING_TIME | default "2m" | quote }} + WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_MAX_CONCURRENT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_SCHEDULE_TERMINAL_EVENT_MAX_CONCURRENT | default "2" | quote }} + + # CASHOUT_TERMINAL_EVENT - Cashout terminal events intra-PSP (Low Throughput) + WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_ENABLED: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_ENABLED | default "true" | quote }} + WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_WORKER_COUNT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_WORKER_COUNT | default "1" | quote }} + WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_BATCH_SIZE: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_BATCH_SIZE | default "50" | quote }} + WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_POLLING_INTERVAL: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_POLLING_INTERVAL | default "5s" | quote }} + WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_PROCESSING_TIMEOUT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_PROCESSING_TIMEOUT | default "12s" | quote }} + WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_MAX_RETRIES: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_MAX_RETRIES | default "5" | quote }} + WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_MAX_PENDING_TIME: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_MAX_PENDING_TIME | default "2m" | quote }} + WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_MAX_CONCURRENT: {{ .Values.inbound.configmap.WEBHOOK_ENTITY_CASHOUT_TERMINAL_EVENT_MAX_CONCURRENT | default "2" | quote }} + # Security Tier Configuration (lib-commons v4.4.0) SECURITY_TIER: {{ .Values.inbound.configmap.SECURITY_TIER | default "" | quote }} SECURITY_ENFORCEMENT: {{ .Values.inbound.configmap.SECURITY_ENFORCEMENT | default "false" | quote }} diff --git a/charts/plugin-br-pix-indirect-btg/templates/outbound/configmap.yaml b/charts/plugin-br-pix-indirect-btg/templates/outbound/configmap.yaml index 8c0096dca..baa369b39 100644 --- a/charts/plugin-br-pix-indirect-btg/templates/outbound/configmap.yaml +++ b/charts/plugin-br-pix-indirect-btg/templates/outbound/configmap.yaml @@ -260,6 +260,49 @@ data: WEBHOOK_DICT_RECONCILIATION_JOB_BACKOFF_MULTIPLIER: {{ .Values.outbound.configmap.WEBHOOK_DICT_RECONCILIATION_JOB_BACKOFF_MULTIPLIER | default "2" | quote }} WEBHOOK_DICT_RECONCILIATION_JOB_MAX_CONCURRENT: {{ .Values.outbound.configmap.WEBHOOK_DICT_RECONCILIATION_JOB_MAX_CONCURRENT | default "2" | quote }} + # ============================================================================== + # RECURRING - Pix Automático (Low Throughput) + # ============================================================================== + WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_ENABLED: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_ENABLED | default "true" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_URL: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_URL | default "" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_WORKER_COUNT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_WORKER_COUNT | default "1" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_BATCH_SIZE: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_BATCH_SIZE | default "50" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_POLLING_INTERVAL: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_POLLING_INTERVAL | default "10s" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_REQUEST_TIMEOUT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_REQUEST_TIMEOUT | default "30s" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_MAX_RETRIES: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_MAX_RETRIES | default "5" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_BACKOFF_MULTIPLIER: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_BACKOFF_MULTIPLIER | default "2" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_MAX_CONCURRENT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_MAX_CONCURRENT | default "2" | quote }} + + WEBHOOK_RECURRING_AUTHORIZATION_ENABLED: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_ENABLED | default "true" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_URL: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_URL | default "" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_WORKER_COUNT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_WORKER_COUNT | default "1" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_BATCH_SIZE: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_BATCH_SIZE | default "50" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_POLLING_INTERVAL: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_POLLING_INTERVAL | default "10s" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_TIMEOUT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_REQUEST_TIMEOUT | default "30s" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_MAX_RETRIES: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_MAX_RETRIES | default "5" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_BACKOFF_MULTIPLIER: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_BACKOFF_MULTIPLIER | default "2" | quote }} + WEBHOOK_RECURRING_AUTHORIZATION_MAX_CONCURRENT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_AUTHORIZATION_MAX_CONCURRENT | default "2" | quote }} + + WEBHOOK_RECURRING_CHARGE_ENABLED: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_CHARGE_ENABLED | default "true" | quote }} + WEBHOOK_RECURRING_CHARGE_URL: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_CHARGE_URL | default "" | quote }} + WEBHOOK_RECURRING_CHARGE_WORKER_COUNT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_CHARGE_WORKER_COUNT | default "1" | quote }} + WEBHOOK_RECURRING_CHARGE_BATCH_SIZE: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_CHARGE_BATCH_SIZE | default "50" | quote }} + WEBHOOK_RECURRING_CHARGE_POLLING_INTERVAL: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_CHARGE_POLLING_INTERVAL | default "10s" | quote }} + WEBHOOK_RECURRING_CHARGE_REQUEST_TIMEOUT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_CHARGE_REQUEST_TIMEOUT | default "30s" | quote }} + WEBHOOK_RECURRING_CHARGE_MAX_RETRIES: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_CHARGE_MAX_RETRIES | default "5" | quote }} + WEBHOOK_RECURRING_CHARGE_BACKOFF_MULTIPLIER: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_CHARGE_BACKOFF_MULTIPLIER | default "2" | quote }} + WEBHOOK_RECURRING_CHARGE_MAX_CONCURRENT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_CHARGE_MAX_CONCURRENT | default "2" | quote }} + + WEBHOOK_RECURRING_RECURRENCE_ENABLED: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_RECURRENCE_ENABLED | default "true" | quote }} + WEBHOOK_RECURRING_RECURRENCE_URL: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_RECURRENCE_URL | default "" | quote }} + WEBHOOK_RECURRING_RECURRENCE_WORKER_COUNT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_RECURRENCE_WORKER_COUNT | default "1" | quote }} + WEBHOOK_RECURRING_RECURRENCE_BATCH_SIZE: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_RECURRENCE_BATCH_SIZE | default "50" | quote }} + WEBHOOK_RECURRING_RECURRENCE_POLLING_INTERVAL: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_RECURRENCE_POLLING_INTERVAL | default "10s" | quote }} + WEBHOOK_RECURRING_RECURRENCE_REQUEST_TIMEOUT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_RECURRENCE_REQUEST_TIMEOUT | default "30s" | quote }} + WEBHOOK_RECURRING_RECURRENCE_MAX_RETRIES: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_RECURRENCE_MAX_RETRIES | default "5" | quote }} + WEBHOOK_RECURRING_RECURRENCE_BACKOFF_MULTIPLIER: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_RECURRENCE_BACKOFF_MULTIPLIER | default "2" | quote }} + WEBHOOK_RECURRING_RECURRENCE_MAX_CONCURRENT: {{ .Values.outbound.configmap.WEBHOOK_RECURRING_RECURRENCE_MAX_CONCURRENT | default "2" | quote }} + # Security Tier Configuration (lib-commons v4.4.0) SECURITY_TIER: {{ .Values.outbound.configmap.SECURITY_TIER | default "" | quote }} SECURITY_ENFORCEMENT: {{ .Values.outbound.configmap.SECURITY_ENFORCEMENT | default "false" | quote }} diff --git a/charts/plugin-br-pix-indirect-btg/templates/pix/configmap.yaml b/charts/plugin-br-pix-indirect-btg/templates/pix/configmap.yaml index c2d470b85..20c21cb99 100644 --- a/charts/plugin-br-pix-indirect-btg/templates/pix/configmap.yaml +++ b/charts/plugin-br-pix-indirect-btg/templates/pix/configmap.yaml @@ -12,6 +12,7 @@ data: VERSION: {{ .Values.pix.image.tag | default .Chart.AppVersion | quote }} SERVER_PORT: {{ .Values.pix.configmap.SERVER_PORT | default "4014" | quote }} SERVER_ADDRESS: {{ .Values.pix.configmap.SERVER_ADDRESS | default ":4014" | quote }} + DEPLOYMENT_MODE: {{ .Values.pix.configmap.DEPLOYMENT_MODE | default "local" | quote }} # AUTH PLUGIN PLUGIN_AUTH_ENABLED: {{ .Values.pix.configmap.PLUGIN_AUTH_ENABLED | default "false" | quote }} @@ -74,6 +75,7 @@ data: MONGO_NAME: {{ .Values.pix.configmap.MONGO_NAME | default "pix-btg-db" | quote }} MONGO_USER: {{ .Values.pix.configmap.MONGO_USER | default "pix-btg" | quote }} MONGO_MAX_POOL_SIZE: {{ .Values.pix.configmap.MONGO_MAX_POOL_SIZE | default "1000" | quote }} + MONGO_TLS: {{ .Values.pix.configmap.MONGO_TLS | default "false" | quote }} # SWAGGER SWAGGER_TITLE: {{ .Values.pix.configmap.SWAGGER_TITLE | default "Plugin BR Pix Indirect BTG" | quote }} @@ -100,6 +102,12 @@ data: ## BTG BTG_BASE_URL: {{ .Values.pix.configmap.BTG_BASE_URL | default "https://developer.api.btgpactual.com" | quote }} + # Recurring module (Pix Automático) + RECURRING_ANTISPAM_WINDOW: {{ .Values.pix.configmap.RECURRING_ANTISPAM_WINDOW | default "720h" | quote }} + RECURRING_REPLY_MAX_ATTEMPTS: {{ .Values.pix.configmap.RECURRING_REPLY_MAX_ATTEMPTS | default "3" | quote }} + RECURRING_REPLY_BACKOFF: {{ .Values.pix.configmap.RECURRING_REPLY_BACKOFF | default "150ms" | quote }} + RECURRING_SCHEDULE_LEAD_DAYS: {{ .Values.pix.configmap.RECURRING_SCHEDULE_LEAD_DAYS | default "1" | quote }} + ## CRM PLUGIN_CRM_BASE_URL: {{ .Values.pix.configmap.PLUGIN_CRM_BASE_URL | default "http://plugin-crm.midaz-plugins.svc.cluster.local:4003" | quote }} @@ -137,6 +145,12 @@ data: # RECONCILIATION RECONCILIATION_INTERNAL_CIDR: {{ .Values.pix.configmap.RECONCILIATION_INTERNAL_CIDR | default "" | quote }} + # Schedule Business Rules + SCHEDULE_MAX_FUTURE_DAYS: {{ .Values.pix.configmap.SCHEDULE_MAX_FUTURE_DAYS | default "180" | quote }} + SCHEDULE_MIN_FUTURE_SECONDS: {{ .Values.pix.configmap.SCHEDULE_MIN_FUTURE_SECONDS | default "60" | quote }} + SCHEDULE_DEFAULT_EXECUTE_HOUR_BRT: {{ .Values.pix.configmap.SCHEDULE_DEFAULT_EXECUTE_HOUR_BRT | default "6" | quote }} + SCHEDULE_MAX_ATTEMPTS: {{ .Values.pix.configmap.SCHEDULE_MAX_ATTEMPTS | default "2" | quote }} + # BTG Webhook Validation BTG_WEBHOOK_VALIDATION_ENABLED: {{ .Values.pix.configmap.BTG_WEBHOOK_VALIDATION_ENABLED | default "true" | quote }} diff --git a/charts/plugin-br-pix-indirect-btg/templates/schedule/configmap.yaml b/charts/plugin-br-pix-indirect-btg/templates/schedule/configmap.yaml new file mode 100644 index 000000000..74b17c913 --- /dev/null +++ b/charts/plugin-br-pix-indirect-btg/templates/schedule/configmap.yaml @@ -0,0 +1,50 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "schedule.fullname" . }} + labels: + {{- include "schedule.labels" (dict "context" . "name" .Values.schedule.name ) | nindent 4 }} +data: + ENV_NAME: {{ .Values.schedule.configmap.ENV_NAME | default "development" | quote }} + VERSION: {{ .Values.schedule.image.tag | default .Chart.AppVersion | quote }} + LOG_LEVEL: {{ .Values.schedule.configmap.LOG_LEVEL | default "info" | quote }} + WORKER_PORT: {{ .Values.schedule.configmap.WORKER_PORT | default "4018" | quote }} + + # Schedule Worker Loop Configuration + # Chart declares explicit defaults: worker runs active with a 15m polling loop. + # Overlays per environment may override these via values, but the chart owns the default. + SCHEDULE_WORKER_ENABLED: {{ .Values.schedule.configmap.SCHEDULE_WORKER_ENABLED | default "true" | quote }} + SCHEDULE_WORKER_INTERVAL: {{ .Values.schedule.configmap.SCHEDULE_WORKER_INTERVAL | default "15m" | quote }} + SCHEDULE_WORKER_BATCH_SIZE: {{ .Values.schedule.configmap.SCHEDULE_WORKER_BATCH_SIZE | default "50" | quote }} + SCHEDULE_WORKER_STALE_THRESHOLD: {{ .Values.schedule.configmap.SCHEDULE_WORKER_STALE_THRESHOLD | default "5m" | quote }} + SCHEDULE_WORKER_SWEEP_EVERY_N_CYCLES: {{ .Values.schedule.configmap.SCHEDULE_WORKER_SWEEP_EVERY_N_CYCLES | default "10" | quote }} + SCHEDULE_WORKER_TIME_WINDOW_ENABLED: {{ .Values.schedule.configmap.SCHEDULE_WORKER_TIME_WINDOW_ENABLED | default "false" | quote }} + SCHEDULE_WORKER_WINDOW_START: {{ .Values.schedule.configmap.SCHEDULE_WORKER_WINDOW_START | default "" | quote }} + SCHEDULE_WORKER_WINDOW_END: {{ .Values.schedule.configmap.SCHEDULE_WORKER_WINDOW_END | default "" | quote }} + SCHEDULE_WORKER_DISPATCH_CONCURRENCY: {{ .Values.schedule.configmap.SCHEDULE_WORKER_DISPATCH_CONCURRENCY | default "0" | quote }} + SCHEDULE_WORKER_JOB_TIMEOUT: {{ .Values.schedule.configmap.SCHEDULE_WORKER_JOB_TIMEOUT | default "0s" | quote }} + SCHEDULE_WORKER_NSF_RETRY_HOUR_BRT: {{ .Values.schedule.configmap.SCHEDULE_WORKER_NSF_RETRY_HOUR_BRT | default "0" | quote }} + SCHEDULE_WORKER_RECONCILE_ENABLED: {{ .Values.schedule.configmap.SCHEDULE_WORKER_RECONCILE_ENABLED | default "false" | quote }} + SCHEDULE_WORKER_RECONCILE_THRESHOLD: {{ .Values.schedule.configmap.SCHEDULE_WORKER_RECONCILE_THRESHOLD | default "30m" | quote }} + SCHEDULE_WORKER_RECONCILE_EVERY_N_CYCLES: {{ .Values.schedule.configmap.SCHEDULE_WORKER_RECONCILE_EVERY_N_CYCLES | default "40" | quote }} + SCHEDULE_WORKER_RECONCILE_BATCH_SIZE: {{ .Values.schedule.configmap.SCHEDULE_WORKER_RECONCILE_BATCH_SIZE | default "50" | quote }} + + # Application (DOMAIN API) Endpoint the worker drives over HTTP. + # SCHEDULE_APP_BASE_URL and PLUGIN_AUTH_URL are operator/Vault-provided per environment. + SCHEDULE_APP_BASE_URL: {{ .Values.schedule.configmap.SCHEDULE_APP_BASE_URL | default "http://plugin-br-pix-indirect-btg.midaz-plugins.svc.cluster.local:4014" | quote }} + SCHEDULE_APP_HTTP_TIMEOUT: {{ .Values.schedule.configmap.SCHEDULE_APP_HTTP_TIMEOUT | default "30s" | quote }} + PLUGIN_AUTH_URL: {{ .Values.schedule.configmap.PLUGIN_AUTH_URL | default "http://plugin-access-manager-auth.midaz-plugins.svc.cluster.local:4000/v1/login/oauth/access_token" | quote }} + + # Observability Configuration + OTEL_RESOURCE_SERVICE_NAME: {{ .Values.schedule.configmap.OTEL_RESOURCE_SERVICE_NAME | default "schedule-worker" | quote }} + OTEL_LIBRARY_NAME: {{ .Values.schedule.configmap.OTEL_LIBRARY_NAME | default "schedule" | quote }} + OTEL_RESOURCE_SERVICE_VERSION: {{ .Values.schedule.image.tag | default .Chart.AppVersion | quote }} + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: {{ .Values.schedule.configmap.OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT | default "development" | quote }} + OTEL_EXPORTER_OTLP_ENDPOINT_PORT: {{ .Values.schedule.configmap.OTEL_EXPORTER_OTLP_ENDPOINT_PORT | default "4317" | quote }} + OTEL_EXPORTER_OTLP_ENDPOINT: {{ .Values.schedule.configmap.OTEL_EXPORTER_OTLP_ENDPOINT | default "http://midaz-otel-lgtm:4317" | quote }} + ENABLE_TELEMETRY: {{ .Values.schedule.configmap.ENABLE_TELEMETRY | default "false" | quote }} + + # Extra Env Vars + {{- with .Values.schedule.extraEnvVars }} + {{- toYaml . | nindent 2 }} + {{- end }} diff --git a/charts/plugin-br-pix-indirect-btg/templates/schedule/deployment.yaml b/charts/plugin-br-pix-indirect-btg/templates/schedule/deployment.yaml new file mode 100644 index 000000000..3b9b55d3f --- /dev/null +++ b/charts/plugin-br-pix-indirect-btg/templates/schedule/deployment.yaml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "schedule.fullname" . }} + labels: + {{- include "schedule.labels" (dict "context" . "name" .Values.schedule.name ) | nindent 4 }} +spec: + revisionHistoryLimit: {{ .Values.schedule.revisionHistoryLimit | default 10 }} + strategy: + {{- toYaml .Values.schedule.deploymentStrategy | nindent 4 }} + replicas: {{ .Values.schedule.replicaCount }} + selector: + matchLabels: + {{- include "schedule.selectorLabels" (dict "context" . "name" .Values.schedule.name) | nindent 6 }} + template: + metadata: + labels: + {{- include "schedule.labels" (dict "context" . "name" .Values.schedule.name ) | nindent 8 }} + spec: + {{- with .Values.schedule.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.schedule.podSecurityContext | nindent 8 }} + containers: + - name: {{ include "schedule.fullname" . }} + securityContext: + {{- toYaml .Values.schedule.securityContext | nindent 12 }} + image: {{ .Values.schedule.image.repository }}:{{ .Values.schedule.image.tag | default (include "plugin.version" .) }} + imagePullPolicy: {{ .Values.schedule.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.schedule.service.port }} + protocol: TCP + envFrom: + - secretRef: + name: {{ if .Values.schedule.useExistingSecrets }}{{ required "schedule.existingSecretName is required when schedule.useExistingSecrets is true" .Values.schedule.existingSecretName }}{{ else }}{{ include "schedule.fullname" . }}{{ end }} + - configMapRef: + name: {{ include "schedule.fullname" . }} + {{- $otelEnabled := (index .Values "otel-collector-lerian").enabled }} + {{- if $otelEnabled }} + env: + - name: "HOST_IP" + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: "OTEL_EXPORTER_OTLP_ENDPOINT" + value: "$(HOST_IP):4317" + {{- end }} + resources: + {{- toYaml .Values.schedule.resources | nindent 12 }} + readinessProbe: + httpGet: + path: {{ .Values.schedule.readinessProbe.path | default "/readyz" }} + port: {{ .Values.schedule.service.port }} + initialDelaySeconds: {{ .Values.schedule.readinessProbe.initialDelaySeconds | default 5 }} + periodSeconds: {{ .Values.schedule.readinessProbe.periodSeconds | default 5 }} + timeoutSeconds: {{ .Values.schedule.readinessProbe.timeoutSeconds | default 1 }} + successThreshold: {{ .Values.schedule.readinessProbe.successThreshold | default 1 }} + failureThreshold: {{ .Values.schedule.readinessProbe.failureThreshold | default 3 }} + livenessProbe: + httpGet: + path: {{ .Values.schedule.livenessProbe.path | default "/health" }} + port: {{ .Values.schedule.service.port }} + initialDelaySeconds: {{ .Values.schedule.livenessProbe.initialDelaySeconds | default 10 }} + periodSeconds: {{ .Values.schedule.livenessProbe.periodSeconds | default 5 }} + timeoutSeconds: {{ .Values.schedule.livenessProbe.timeoutSeconds | default 1 }} + successThreshold: {{ .Values.schedule.livenessProbe.successThreshold | default 1 }} + failureThreshold: {{ .Values.schedule.livenessProbe.failureThreshold | default 3 }} + {{- with .Values.schedule.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.schedule.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.schedule.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/plugin-br-pix-indirect-btg/templates/schedule/hpa.yaml b/charts/plugin-br-pix-indirect-btg/templates/schedule/hpa.yaml new file mode 100644 index 000000000..975486752 --- /dev/null +++ b/charts/plugin-br-pix-indirect-btg/templates/schedule/hpa.yaml @@ -0,0 +1,35 @@ +{{- if .Values.schedule.autoscaling.enabled }} +{{- if not (or .Values.schedule.autoscaling.targetCPUUtilizationPercentage .Values.schedule.autoscaling.targetMemoryUtilizationPercentage) }} +{{- fail "At least one of targetCPUUtilizationPercentage or targetMemoryUtilizationPercentage must be set when autoscaling is enabled" }} +{{- end }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "schedule.fullname" . }} + labels: + {{- include "schedule.labels" (dict "context" . "name" .Values.schedule.name ) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "schedule.fullname" . }} + minReplicas: {{ .Values.schedule.autoscaling.minReplicas }} + maxReplicas: {{ .Values.schedule.autoscaling.maxReplicas }} + metrics: + {{- if .Values.schedule.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.schedule.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.schedule.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.schedule.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-indirect-btg/templates/schedule/pdb.yaml b/charts/plugin-br-pix-indirect-btg/templates/schedule/pdb.yaml new file mode 100644 index 000000000..9233c1108 --- /dev/null +++ b/charts/plugin-br-pix-indirect-btg/templates/schedule/pdb.yaml @@ -0,0 +1,23 @@ +{{- if .Values.schedule.pdb.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "schedule.fullname" . }} + labels: + {{- include "schedule.labels" (dict "context" . "name" .Values.schedule.name ) | nindent 4 }} + {{- with .Values.schedule.pdb.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} +spec: + {{- with .Values.schedule.pdb.maxUnavailable }} + maxUnavailable: {{ . }} + {{- else }} + minAvailable: {{ .Values.schedule.pdb.minAvailable | default 1 }} + {{- end }} + selector: + matchLabels: + {{- include "schedule.selectorLabels" (dict "context" . "name" .Values.schedule.name) | nindent 6 }} +{{- end }} diff --git a/charts/plugin-br-pix-indirect-btg/templates/schedule/secrets.yaml b/charts/plugin-br-pix-indirect-btg/templates/schedule/secrets.yaml new file mode 100644 index 000000000..62b03f5f5 --- /dev/null +++ b/charts/plugin-br-pix-indirect-btg/templates/schedule/secrets.yaml @@ -0,0 +1,15 @@ +{{- if not .Values.schedule.useExistingSecrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "schedule.fullname" . }} + labels: + {{- include "schedule.labels" (dict "context" . "name" .Values.schedule.name ) | nindent 4 }} +type: Opaque +data: + # PIX BTG service credentials used by the worker's OAuth2 client. + # Operator/Vault-provided; when PLUGIN_AUTH is disabled locally these stay empty + # and the worker treats auth as optional. + PLUGIN_PIX_BTG_CLIENT_ID: {{ .Values.schedule.secrets.PLUGIN_PIX_BTG_CLIENT_ID | default "" | b64enc | quote }} + PLUGIN_PIX_BTG_CLIENT_SECRET: {{ .Values.schedule.secrets.PLUGIN_PIX_BTG_CLIENT_SECRET | default "" | b64enc | quote }} +{{- end }} diff --git a/charts/plugin-br-pix-indirect-btg/templates/schedule/service.yaml b/charts/plugin-br-pix-indirect-btg/templates/schedule/service.yaml new file mode 100644 index 000000000..7648cedf6 --- /dev/null +++ b/charts/plugin-br-pix-indirect-btg/templates/schedule/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "schedule.fullname" . }} + labels: + {{- include "schedule.labels" (dict "context" . "name" .Values.schedule.name ) | nindent 4 }} +spec: + type: {{ .Values.schedule.service.type }} + ports: + - port: {{ .Values.schedule.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "schedule.selectorLabels" (dict "context" . "name" .Values.schedule.name) | nindent 4 }} diff --git a/charts/plugin-br-pix-indirect-btg/values.schema.json b/charts/plugin-br-pix-indirect-btg/values.schema.json index b4f057e23..adec308d1 100644 --- a/charts/plugin-br-pix-indirect-btg/values.schema.json +++ b/charts/plugin-br-pix-indirect-btg/values.schema.json @@ -55,6 +55,18 @@ }, "additionalProperties": true }, + "schedule": { + "type": "object", + "properties": { + "configmap": { + "type": "object" + }, + "secrets": { + "type": "object" + } + }, + "additionalProperties": true + }, "postgresql": { "type": "object", "additionalProperties": true diff --git a/charts/plugin-br-pix-indirect-btg/values.yaml b/charts/plugin-br-pix-indirect-btg/values.yaml index ae4437774..115a18d27 100644 --- a/charts/plugin-br-pix-indirect-btg/values.yaml +++ b/charts/plugin-br-pix-indirect-btg/values.yaml @@ -151,6 +151,8 @@ pix: # @default -- templates/configmap.yaml configmap: ALLOW_INSECURE_TLS: "true" + # Deployment Mode + DEPLOYMENT_MODE: "local" # Database Configuration # Infra hosts are derived collapse-aware in templates/*/configmap.yaml from the bundled # subchart Service names when left empty. Set explicitly only for external infra. @@ -164,6 +166,7 @@ pix: MONGO_HOST: "" MONGO_NAME: "plugin-br-pix-indirect-btg-db" MONGO_USER: "pix_btg" + MONGO_TLS: "false" # Redis Configuration REDIS_HOST: "" # External Services @@ -190,6 +193,11 @@ pix: MTLS_HTTP_TIMEOUT: "10s" # BTG Webhook Validation BTG_WEBHOOK_VALIDATION_ENABLED: "true" + # Schedule Business Rules + SCHEDULE_MAX_FUTURE_DAYS: "180" + SCHEDULE_MIN_FUTURE_SECONDS: "60" + SCHEDULE_DEFAULT_EXECUTE_HOUR_BRT: "6" + SCHEDULE_MAX_ATTEMPTS: "2" # -- Secrets for storing sensitive data # -- All secrets are declared in the templates/secrets.yaml # @default -- templates/secrets.yaml @@ -666,6 +674,120 @@ reconciliation: extraEnvVars: {} useExistingSecrets: false existingSecretName: "" +schedule: + # -- Readiness probe configuration. All fields override chart defaults. + readinessProbe: + path: /readyz + # -- Liveness probe configuration. All fields override chart defaults. + livenessProbe: + path: /health + replicaCount: 1 + name: "plugin-br-pix-indirect-btg-worker-schedule" + description: "Schedule (Pix recurrence) Worker for Plugin BR PIX Indirect BTG" + # -- Number of old ReplicaSets to retain for deployment rollback + revisionHistoryLimit: 10 + image: + # -- Repository for the container image + repository: ghcr.io/lerianstudio/plugin-br-pix-indirect-btg-worker-schedule + # -- Image pull policy + pullPolicy: Always + # -- Image tag used for deployment + tag: "1.7.5" + # -- Secrets for pulling images from a private registry + imagePullSecrets: [] + # -- Overrides the default generated name by Helm + nameOverride: "" + # -- Overrides the full name generated by Helm + fullnameOverride: "" + service: + # -- Kubernetes service type + type: ClusterIP + # -- Service port (distinct from reconciliation 4017 to keep both co-locatable) + port: 4018 + deploymentStrategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + podSecurityContext: {} # fsGroup: 2000 + securityContext: + # -- Defines the group ID for the user running the process inside the container + runAsGroup: 1000 + # -- Defines the user ID for the process running inside the container + runAsUser: 1000 + # -- Ensures the process does not run as root + runAsNonRoot: true + capabilities: + drop: + - ALL + # -- Defines the root filesystem as read-only + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + pdb: + # -- Enable or disable PodDisruptionBudget + enabled: true + # -- Maximum number of unavailable pods + maxUnavailable: 1 + # -- Minimum number of available pods + minAvailable: 0 + # -- Annotations for PodDisruptionBudget + annotations: {} + resources: + # -- CPU and memory limits for pods + limits: + cpu: 500m + memory: 512Mi + # -- Minimum CPU and memory requests + requests: + cpu: 200m + memory: 256Mi + autoscaling: + # -- Enable or disable horizontal pod autoscaling + enabled: false + # -- Minimum number of replicas + minReplicas: 1 + # -- Maximum number of replicas + maxReplicas: 1 + # -- Target CPU utilization percentage for autoscaling + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + # -- Node selector for scheduling pods on specific nodes + nodeSelector: {} + # -- Tolerations for scheduling on tainted nodes + tolerations: [] + # -- Affinity rules for pod scheduling + affinity: {} + # -- ConfigMap for environment variables and configurations + # -- All environment variables are declared in the templates/schedule/configmap.yaml + # @default -- templates/schedule/configmap.yaml + configmap: + # Chart-level explicit defaults: worker active with a 15m polling loop. + SCHEDULE_WORKER_ENABLED: "true" + SCHEDULE_WORKER_INTERVAL: "15m" + SCHEDULE_WORKER_BATCH_SIZE: "50" + SCHEDULE_WORKER_STALE_THRESHOLD: "5m" + SCHEDULE_WORKER_SWEEP_EVERY_N_CYCLES: "10" + SCHEDULE_WORKER_TIME_WINDOW_ENABLED: "false" + SCHEDULE_APP_HTTP_TIMEOUT: "30s" + # Application DOMAIN API endpoint and auth URL (operator/Vault-provided per env). + SCHEDULE_APP_BASE_URL: "" + PLUGIN_AUTH_URL: "" + OTEL_RESOURCE_SERVICE_NAME: "schedule-worker" + OTEL_LIBRARY_NAME: "schedule" + ENABLE_TELEMETRY: "false" + # -- Secrets for storing sensitive data + # -- All secrets are declared in the templates/schedule/secrets.yaml + # @default -- templates/schedule/secrets.yaml + secrets: + # PIX BTG service credentials (operator/Vault-provided). + PLUGIN_PIX_BTG_CLIENT_ID: "" + PLUGIN_PIX_BTG_CLIENT_SECRET: "" + # -- Extra environment variables + extraEnvVars: {} + useExistingSecrets: false + existingSecretName: "" postgresql: enabled: true global: diff --git a/charts/plugin-br-pix-switch/Chart.yaml b/charts/plugin-br-pix-switch/Chart.yaml index 956e74fc2..7afcd759f 100644 --- a/charts/plugin-br-pix-switch/Chart.yaml +++ b/charts/plugin-br-pix-switch/Chart.yaml @@ -15,7 +15,7 @@ maintainers: # 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: 2.0.0 +version: 2.1.0-beta.4 # This is the version number of the application being deployed. appVersion: "1.0.0-beta.101" diff --git a/charts/plugin-br-pix-switch/README.md b/charts/plugin-br-pix-switch/README.md index 0c1799e2a..6f37aa098 100644 --- a/charts/plugin-br-pix-switch/README.md +++ b/charts/plugin-br-pix-switch/README.md @@ -10,7 +10,7 @@ BACEN-compliant PIX instant payment platform for the Lerian ecosystem. -The plugin is a Go monorepo that produces 10 independently-deployable binaries. +The plugin is a Go monorepo that produces 13 independently-deployable binaries. This chart deploys all of them with one helm release. Each component has its own Deployment, Service, ConfigMap, Secret, HPA, and PDB; ingress is opt-in per component. @@ -29,21 +29,24 @@ per component. | `cobHub` | `cob/hub/api` | 4108 | COB hub | | `cobProxy` | `cob/proxy/api` | 4109 | COB proxy to BCB | | `cobSystemplane` | `cob/systemplane/api` | 4110 | Runtime config plane for COB | +| `adapterLerian` | `adapter-lerian/api` | 4113 | Lerian provider adapter (API, disabled by default) | +| `adapterLerianConsumer` | `adapter-lerian/consumer` | 4114 | Lerian provider adapter Kafka consumer (disabled by default) | +| `adapterLerianSystemplane` | `adapter-lerian/systemplane/api` | 4115 | Runtime config plane for adapter-lerian (disabled by default) | ## Architecture The plugin uses a Proxy/Hub deployment model. A "hub" component owns business logic and local state (its own Postgres database, sometimes Mongo+Valkey), while a "proxy" component is a stateless pass-through. Both expose identical -APIs. Three Postgres databases are required (`pix-spi`, `pix-dict`, `pix-cob`) -and one Mongo database (`pix-dict`) for `dict-hub` (`pix-cob` is a +APIs. Four Postgres databases are required (`pix-spi`, `pix-dict`, `pix-cob`, +`pix-adapter-lerian`) and one Mongo database (`pix-dict`) for `dict-hub` (`pix-cob` is a forward-compat slot the Mongo bootstrap provisions but no component reads yet). ## Required infrastructure For a full deployment: -- **PostgreSQL**: 3 databases (`pix-spi`, `pix-dict`, `pix-cob`) and a role - `pixswitch` with full ownership of each +- **PostgreSQL**: 4 databases (`pix-spi`, `pix-dict`, `pix-cob`, + `pix-adapter-lerian`) and a role `pixswitch` with full ownership of each - **MongoDB**: 1 database (`pix-dict`) used by `dict-hub`; the bootstrap also provisions `pix-cob` as a forward-compat slot, but no component reads it yet - **Valkey** (Redis-compatible): used by `spi`, `dict-hub`, `dict-hub-vsync` @@ -64,6 +67,8 @@ Default `enabled` values: - `spi`, `spiSystemplane`, `dictHub`, `dictHubVsync`, `dictProxy`, `dictSystemplane`, `cobHub`, `cobProxy`, `cobSystemplane`: `true` - `adapterBtgMock`: `false` (it's a mock — only enable in dev/staging) +- `adapterLerian`, `adapterLerianConsumer`, `adapterLerianSystemplane`: `false` + (Lerian provider adapter — enable per environment) ## Configuration @@ -87,7 +92,9 @@ The app reads: Each component publishes and pulls its own image (`ghcr.io/lerianstudio/plugin-br-pix-switch--api`), set per -component under `.image.repository`. There is no shared +component under `.image.repository`. Worker components omit the +`-api` suffix (e.g. `plugin-br-pix-switch-dict-hub-vsync` and +`plugin-br-pix-switch-adapter-lerian-consumer`). There is no shared `global.image.repository`. When a component's `image.tag` is unset it falls back to `.Chart.AppVersion`, which keeps the cohort in lockstep by default; override `.image.tag` to pin a specific build per component (rare). diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/configmap.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/configmap.yaml new file mode 100644 index 000000000..b30b3fa3c --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/configmap.yaml @@ -0,0 +1,27 @@ +{{- if .Values.adapterLerianConsumer.enabled }} +{{- $component := "adapter-lerian-consumer" }} +{{- $values := .Values.adapterLerianConsumer }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +data: + {{- range $key, $value := $values.configmap }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- /* + OTEL_RESOURCE_SERVICE_VERSION: derive from the deployed image tag so it + tracks releases automatically. Resolution order: + component image.tag > Chart.AppVersion. + Operators can override by setting `.configmap.OTEL_RESOURCE_SERVICE_VERSION` + explicitly (the range above wins because data keys are unique per YAML map, + but we only emit this when the key wasn't already provided). + */}} + {{- if not (hasKey $values.configmap "OTEL_RESOURCE_SERVICE_VERSION") }} + {{- $compTag := default $.Chart.AppVersion $values.image.tag }} + OTEL_RESOURCE_SERVICE_VERSION: {{ $compTag | quote }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/deployment.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/deployment.yaml new file mode 100644 index 000000000..4a7c4b000 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/deployment.yaml @@ -0,0 +1,125 @@ +{{- if .Values.adapterLerianConsumer.enabled }} +{{- $component := "adapter-lerian-consumer" }} +{{- $values := .Values.adapterLerianConsumer }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +spec: + revisionHistoryLimit: {{ $values.revisionHistoryLimit | default 10 }} + {{- if not $values.autoscaling.enabled }} + replicas: {{ $values.replicaCount }} + {{- end }} + strategy: + {{- toYaml $values.deploymentStrategy | nindent 4 }} + selector: + matchLabels: + {{- include "plugin-br-pix-switch.selectorLabels" (dict "context" $ "component" $component) | nindent 6 }} + template: + metadata: + {{- with $values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 8 }} + spec: + {{- with (include "plugin-br-pix-switch.componentImagePullSecrets" (dict "context" $ "componentValues" $values)) }} + {{- if . }} + imagePullSecrets: + {{- . | nindent 8 }} + {{- end }} + {{- end }} + serviceAccountName: {{ include "plugin-br-pix-switch.componentServiceAccountName" (dict "context" $ "component" $component "componentValues" $values) }} + securityContext: + {{- toYaml $values.podSecurityContext | nindent 8 }} + initContainers: + {{- include "plugin-br-pix-switch.waitForDependencies" (dict "context" $ "component" $component "componentValues" $values) | nindent 8 }} + containers: + - name: {{ $component }} + securityContext: + {{- toYaml $values.securityContext | nindent 12 }} + image: {{ include "plugin-br-pix-switch.componentImage" (dict "context" $ "componentValues" $values) }} + imagePullPolicy: {{ include "plugin-br-pix-switch.componentPullPolicy" (dict "context" $ "componentValues" $values) }} + {{- with $values.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $values.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ $values.service.port }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + - secretRef: + name: {{ include "plugin-br-pix-switch.componentSecretName" (dict "context" $ "component" $component "componentValues" $values) }} + {{- /* + Build the env: list when EITHER telemetry is enabled (HOST_IP + + OTEL_EXPORTER_OTLP_ENDPOINT injected via downward API so each + pod talks to its node-local OTel collector by default) OR the + operator has set extraEnvVars. The configmap-supplied + OTEL_EXPORTER_OTLP_ENDPOINT can still be overridden by setting + it in the configmap; the K8s rule is "later env entry wins", + so the configmap value (via envFrom) is overridden by the + explicit env entry below. + */}} + {{- $telemetryOn := eq (toString (default "false" $values.configmap.TELEMETRY_ENABLED)) "true" }} + {{- $hasExtras := gt (len $values.extraEnvVars) 0 }} + {{- if or $telemetryOn $hasExtras }} + env: + {{- if $telemetryOn }} + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: "$(HOST_IP):4317" + {{- end }} + {{- if $hasExtras }} + {{- range $k, $v := $values.extraEnvVars }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- end }} + {{- end }} + resources: + {{- toYaml $values.resources | nindent 12 }} + readinessProbe: + httpGet: + path: {{ $values.readinessProbe.path | default "/lerian/readyz" }} + port: http + initialDelaySeconds: {{ $values.readinessProbe.initialDelaySeconds | default 10 }} + periodSeconds: {{ $values.readinessProbe.periodSeconds | default 5 }} + timeoutSeconds: {{ $values.readinessProbe.timeoutSeconds | default 1 }} + successThreshold: {{ $values.readinessProbe.successThreshold | default 1 }} + failureThreshold: {{ $values.readinessProbe.failureThreshold | default 3 }} + livenessProbe: + httpGet: + path: {{ $values.livenessProbe.path | default "/lerian/health" }} + port: http + initialDelaySeconds: {{ $values.livenessProbe.initialDelaySeconds | default 5 }} + periodSeconds: {{ $values.livenessProbe.periodSeconds | default 10 }} + timeoutSeconds: {{ $values.livenessProbe.timeoutSeconds | default 1 }} + successThreshold: {{ $values.livenessProbe.successThreshold | default 1 }} + failureThreshold: {{ $values.livenessProbe.failureThreshold | default 3 }} + {{- with $values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/hpa.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/hpa.yaml new file mode 100644 index 000000000..8a6c0761e --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/hpa.yaml @@ -0,0 +1,35 @@ +{{- if and .Values.adapterLerianConsumer.enabled .Values.adapterLerianConsumer.autoscaling.enabled }} +{{- $component := "adapter-lerian-consumer" }} +{{- $values := .Values.adapterLerianConsumer }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + minReplicas: {{ $values.autoscaling.minReplicas }} + maxReplicas: {{ $values.autoscaling.maxReplicas }} + metrics: + {{- if $values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ $values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if $values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ $values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/pdb.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/pdb.yaml new file mode 100644 index 000000000..82f2a23d0 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/pdb.yaml @@ -0,0 +1,24 @@ +{{- if and .Values.adapterLerianConsumer.enabled .Values.adapterLerianConsumer.pdb.enabled }} +{{- $component := "adapter-lerian-consumer" }} +{{- $values := .Values.adapterLerianConsumer }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $values.pdb.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if $values.pdb.minAvailable }} + minAvailable: {{ $values.pdb.minAvailable }} + {{- else if $values.pdb.maxUnavailable }} + maxUnavailable: {{ $values.pdb.maxUnavailable }} + {{- end }} + selector: + matchLabels: + {{- include "plugin-br-pix-switch.selectorLabels" (dict "context" $ "component" $component) | nindent 6 }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/secrets.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/secrets.yaml new file mode 100644 index 000000000..2f3396b24 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/secrets.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.adapterLerianConsumer.enabled (not .Values.adapterLerianConsumer.useExistingSecret) }} +{{- $component := "adapter-lerian-consumer" }} +{{- $values := .Values.adapterLerianConsumer }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +type: Opaque +stringData: + {{- range $key, $value := $values.secrets }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/service.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/service.yaml new file mode 100644 index 000000000..edb27f0a9 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/service.yaml @@ -0,0 +1,24 @@ +{{- if .Values.adapterLerianConsumer.enabled }} +{{- $component := "adapter-lerian-consumer" }} +{{- $values := .Values.adapterLerianConsumer }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ $values.service.type }} + ports: + - port: {{ $values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "plugin-br-pix-switch.selectorLabels" (dict "context" $ "component" $component) | nindent 4 }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/serviceaccount.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/serviceaccount.yaml new file mode 100644 index 000000000..d0d7eb1e2 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-consumer/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.adapterLerianConsumer.enabled .Values.adapterLerianConsumer.serviceAccount.create }} +{{- $component := "adapter-lerian-consumer" }} +{{- $values := .Values.adapterLerianConsumer }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "plugin-br-pix-switch.componentServiceAccountName" (dict "context" $ "component" $component "componentValues" $values) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/configmap.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/configmap.yaml new file mode 100644 index 000000000..b306aaf1d --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/configmap.yaml @@ -0,0 +1,27 @@ +{{- if .Values.adapterLerianSystemplane.enabled }} +{{- $component := "adapter-lerian-systemplane" }} +{{- $values := .Values.adapterLerianSystemplane }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +data: + {{- range $key, $value := $values.configmap }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- /* + OTEL_RESOURCE_SERVICE_VERSION: derive from the deployed image tag so it + tracks releases automatically. Resolution order: + component image.tag > Chart.AppVersion. + Operators can override by setting `.configmap.OTEL_RESOURCE_SERVICE_VERSION` + explicitly (the range above wins because data keys are unique per YAML map, + but we only emit this when the key wasn't already provided). + */}} + {{- if not (hasKey $values.configmap "OTEL_RESOURCE_SERVICE_VERSION") }} + {{- $compTag := default $.Chart.AppVersion $values.image.tag }} + OTEL_RESOURCE_SERVICE_VERSION: {{ $compTag | quote }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/deployment.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/deployment.yaml new file mode 100644 index 000000000..0aaa49a0d --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/deployment.yaml @@ -0,0 +1,125 @@ +{{- if .Values.adapterLerianSystemplane.enabled }} +{{- $component := "adapter-lerian-systemplane" }} +{{- $values := .Values.adapterLerianSystemplane }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +spec: + revisionHistoryLimit: {{ $values.revisionHistoryLimit | default 10 }} + {{- if not $values.autoscaling.enabled }} + replicas: {{ $values.replicaCount }} + {{- end }} + strategy: + {{- toYaml $values.deploymentStrategy | nindent 4 }} + selector: + matchLabels: + {{- include "plugin-br-pix-switch.selectorLabels" (dict "context" $ "component" $component) | nindent 6 }} + template: + metadata: + {{- with $values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 8 }} + spec: + {{- with (include "plugin-br-pix-switch.componentImagePullSecrets" (dict "context" $ "componentValues" $values)) }} + {{- if . }} + imagePullSecrets: + {{- . | nindent 8 }} + {{- end }} + {{- end }} + serviceAccountName: {{ include "plugin-br-pix-switch.componentServiceAccountName" (dict "context" $ "component" $component "componentValues" $values) }} + securityContext: + {{- toYaml $values.podSecurityContext | nindent 8 }} + initContainers: + {{- include "plugin-br-pix-switch.waitForDependencies" (dict "context" $ "component" $component "componentValues" $values) | nindent 8 }} + containers: + - name: {{ $component }} + securityContext: + {{- toYaml $values.securityContext | nindent 12 }} + image: {{ include "plugin-br-pix-switch.componentImage" (dict "context" $ "componentValues" $values) }} + imagePullPolicy: {{ include "plugin-br-pix-switch.componentPullPolicy" (dict "context" $ "componentValues" $values) }} + {{- with $values.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $values.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ $values.service.port }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + - secretRef: + name: {{ include "plugin-br-pix-switch.componentSecretName" (dict "context" $ "component" $component "componentValues" $values) }} + {{- /* + Build the env: list when EITHER telemetry is enabled (HOST_IP + + OTEL_EXPORTER_OTLP_ENDPOINT injected via downward API so each + pod talks to its node-local OTel collector by default) OR the + operator has set extraEnvVars. The configmap-supplied + OTEL_EXPORTER_OTLP_ENDPOINT can still be overridden by setting + it in the configmap; the K8s rule is "later env entry wins", + so the configmap value (via envFrom) is overridden by the + explicit env entry below. + */}} + {{- $telemetryOn := eq (toString (default "false" $values.configmap.TELEMETRY_ENABLED)) "true" }} + {{- $hasExtras := gt (len $values.extraEnvVars) 0 }} + {{- if or $telemetryOn $hasExtras }} + env: + {{- if $telemetryOn }} + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: "$(HOST_IP):4317" + {{- end }} + {{- if $hasExtras }} + {{- range $k, $v := $values.extraEnvVars }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- end }} + {{- end }} + resources: + {{- toYaml $values.resources | nindent 12 }} + readinessProbe: + httpGet: + path: {{ $values.readinessProbe.path | default "/lerian/readyz" }} + port: http + initialDelaySeconds: {{ $values.readinessProbe.initialDelaySeconds | default 10 }} + periodSeconds: {{ $values.readinessProbe.periodSeconds | default 5 }} + timeoutSeconds: {{ $values.readinessProbe.timeoutSeconds | default 1 }} + successThreshold: {{ $values.readinessProbe.successThreshold | default 1 }} + failureThreshold: {{ $values.readinessProbe.failureThreshold | default 3 }} + livenessProbe: + httpGet: + path: {{ $values.livenessProbe.path | default "/lerian/health" }} + port: http + initialDelaySeconds: {{ $values.livenessProbe.initialDelaySeconds | default 5 }} + periodSeconds: {{ $values.livenessProbe.periodSeconds | default 10 }} + timeoutSeconds: {{ $values.livenessProbe.timeoutSeconds | default 1 }} + successThreshold: {{ $values.livenessProbe.successThreshold | default 1 }} + failureThreshold: {{ $values.livenessProbe.failureThreshold | default 3 }} + {{- with $values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/hpa.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/hpa.yaml new file mode 100644 index 000000000..0db528930 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/hpa.yaml @@ -0,0 +1,35 @@ +{{- if and .Values.adapterLerianSystemplane.enabled .Values.adapterLerianSystemplane.autoscaling.enabled }} +{{- $component := "adapter-lerian-systemplane" }} +{{- $values := .Values.adapterLerianSystemplane }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + minReplicas: {{ $values.autoscaling.minReplicas }} + maxReplicas: {{ $values.autoscaling.maxReplicas }} + metrics: + {{- if $values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ $values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if $values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ $values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/pdb.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/pdb.yaml new file mode 100644 index 000000000..14a3319ee --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/pdb.yaml @@ -0,0 +1,24 @@ +{{- if and .Values.adapterLerianSystemplane.enabled .Values.adapterLerianSystemplane.pdb.enabled }} +{{- $component := "adapter-lerian-systemplane" }} +{{- $values := .Values.adapterLerianSystemplane }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $values.pdb.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if $values.pdb.minAvailable }} + minAvailable: {{ $values.pdb.minAvailable }} + {{- else if $values.pdb.maxUnavailable }} + maxUnavailable: {{ $values.pdb.maxUnavailable }} + {{- end }} + selector: + matchLabels: + {{- include "plugin-br-pix-switch.selectorLabels" (dict "context" $ "component" $component) | nindent 6 }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/secrets.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/secrets.yaml new file mode 100644 index 000000000..17a6b1776 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/secrets.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.adapterLerianSystemplane.enabled (not .Values.adapterLerianSystemplane.useExistingSecret) }} +{{- $component := "adapter-lerian-systemplane" }} +{{- $values := .Values.adapterLerianSystemplane }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +type: Opaque +stringData: + {{- range $key, $value := $values.secrets }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/service.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/service.yaml new file mode 100644 index 000000000..26e808faf --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/service.yaml @@ -0,0 +1,24 @@ +{{- if .Values.adapterLerianSystemplane.enabled }} +{{- $component := "adapter-lerian-systemplane" }} +{{- $values := .Values.adapterLerianSystemplane }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ $values.service.type }} + ports: + - port: {{ $values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "plugin-br-pix-switch.selectorLabels" (dict "context" $ "component" $component) | nindent 4 }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/serviceaccount.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/serviceaccount.yaml new file mode 100644 index 000000000..828e62243 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian-systemplane/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.adapterLerianSystemplane.enabled .Values.adapterLerianSystemplane.serviceAccount.create }} +{{- $component := "adapter-lerian-systemplane" }} +{{- $values := .Values.adapterLerianSystemplane }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "plugin-br-pix-switch.componentServiceAccountName" (dict "context" $ "component" $component "componentValues" $values) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian/configmap.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian/configmap.yaml new file mode 100644 index 000000000..b7bd65015 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian/configmap.yaml @@ -0,0 +1,27 @@ +{{- if .Values.adapterLerian.enabled }} +{{- $component := "adapter-lerian" }} +{{- $values := .Values.adapterLerian }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +data: + {{- range $key, $value := $values.configmap }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- /* + OTEL_RESOURCE_SERVICE_VERSION: derive from the deployed image tag so it + tracks releases automatically. Resolution order: + component image.tag > Chart.AppVersion. + Operators can override by setting `.configmap.OTEL_RESOURCE_SERVICE_VERSION` + explicitly (the range above wins because data keys are unique per YAML map, + but we only emit this when the key wasn't already provided). + */}} + {{- if not (hasKey $values.configmap "OTEL_RESOURCE_SERVICE_VERSION") }} + {{- $compTag := default $.Chart.AppVersion $values.image.tag }} + OTEL_RESOURCE_SERVICE_VERSION: {{ $compTag | quote }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian/deployment.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian/deployment.yaml new file mode 100644 index 000000000..c09a70b83 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian/deployment.yaml @@ -0,0 +1,125 @@ +{{- if .Values.adapterLerian.enabled }} +{{- $component := "adapter-lerian" }} +{{- $values := .Values.adapterLerian }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +spec: + revisionHistoryLimit: {{ $values.revisionHistoryLimit | default 10 }} + {{- if not $values.autoscaling.enabled }} + replicas: {{ $values.replicaCount }} + {{- end }} + strategy: + {{- toYaml $values.deploymentStrategy | nindent 4 }} + selector: + matchLabels: + {{- include "plugin-br-pix-switch.selectorLabels" (dict "context" $ "component" $component) | nindent 6 }} + template: + metadata: + {{- with $values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 8 }} + spec: + {{- with (include "plugin-br-pix-switch.componentImagePullSecrets" (dict "context" $ "componentValues" $values)) }} + {{- if . }} + imagePullSecrets: + {{- . | nindent 8 }} + {{- end }} + {{- end }} + serviceAccountName: {{ include "plugin-br-pix-switch.componentServiceAccountName" (dict "context" $ "component" $component "componentValues" $values) }} + securityContext: + {{- toYaml $values.podSecurityContext | nindent 8 }} + initContainers: + {{- include "plugin-br-pix-switch.waitForDependencies" (dict "context" $ "component" $component "componentValues" $values) | nindent 8 }} + containers: + - name: {{ $component }} + securityContext: + {{- toYaml $values.securityContext | nindent 12 }} + image: {{ include "plugin-br-pix-switch.componentImage" (dict "context" $ "componentValues" $values) }} + imagePullPolicy: {{ include "plugin-br-pix-switch.componentPullPolicy" (dict "context" $ "componentValues" $values) }} + {{- with $values.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $values.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ $values.service.port }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + - secretRef: + name: {{ include "plugin-br-pix-switch.componentSecretName" (dict "context" $ "component" $component "componentValues" $values) }} + {{- /* + Build the env: list when EITHER telemetry is enabled (HOST_IP + + OTEL_EXPORTER_OTLP_ENDPOINT injected via downward API so each + pod talks to its node-local OTel collector by default) OR the + operator has set extraEnvVars. The configmap-supplied + OTEL_EXPORTER_OTLP_ENDPOINT can still be overridden by setting + it in the configmap; the K8s rule is "later env entry wins", + so the configmap value (via envFrom) is overridden by the + explicit env entry below. + */}} + {{- $telemetryOn := eq (toString (default "false" $values.configmap.TELEMETRY_ENABLED)) "true" }} + {{- $hasExtras := gt (len $values.extraEnvVars) 0 }} + {{- if or $telemetryOn $hasExtras }} + env: + {{- if $telemetryOn }} + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: "$(HOST_IP):4317" + {{- end }} + {{- if $hasExtras }} + {{- range $k, $v := $values.extraEnvVars }} + - name: {{ $k }} + value: {{ $v | quote }} + {{- end }} + {{- end }} + {{- end }} + resources: + {{- toYaml $values.resources | nindent 12 }} + readinessProbe: + httpGet: + path: {{ $values.readinessProbe.path | default "/lerian/readyz" }} + port: http + initialDelaySeconds: {{ $values.readinessProbe.initialDelaySeconds | default 10 }} + periodSeconds: {{ $values.readinessProbe.periodSeconds | default 5 }} + timeoutSeconds: {{ $values.readinessProbe.timeoutSeconds | default 1 }} + successThreshold: {{ $values.readinessProbe.successThreshold | default 1 }} + failureThreshold: {{ $values.readinessProbe.failureThreshold | default 3 }} + livenessProbe: + httpGet: + path: {{ $values.livenessProbe.path | default "/lerian/health" }} + port: http + initialDelaySeconds: {{ $values.livenessProbe.initialDelaySeconds | default 5 }} + periodSeconds: {{ $values.livenessProbe.periodSeconds | default 10 }} + timeoutSeconds: {{ $values.livenessProbe.timeoutSeconds | default 1 }} + successThreshold: {{ $values.livenessProbe.successThreshold | default 1 }} + failureThreshold: {{ $values.livenessProbe.failureThreshold | default 3 }} + {{- with $values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian/hpa.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian/hpa.yaml new file mode 100644 index 000000000..a289c4c55 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian/hpa.yaml @@ -0,0 +1,35 @@ +{{- if and .Values.adapterLerian.enabled .Values.adapterLerian.autoscaling.enabled }} +{{- $component := "adapter-lerian" }} +{{- $values := .Values.adapterLerian }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + minReplicas: {{ $values.autoscaling.minReplicas }} + maxReplicas: {{ $values.autoscaling.maxReplicas }} + metrics: + {{- if $values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ $values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if $values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ $values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian/ingress.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian/ingress.yaml new file mode 100644 index 000000000..a74880e33 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian/ingress.yaml @@ -0,0 +1,40 @@ +{{- if and .Values.adapterLerian.enabled .Values.adapterLerian.ingress.enabled }} +{{- $component := "adapter-lerian" }} +{{- $values := .Values.adapterLerian }} +{{- $fullName := include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} +{{- $svcPort := $values.service.port }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with $values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- with $values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range $values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian/migrations-job.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian/migrations-job.yaml new file mode 100644 index 000000000..dbdb6d637 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian/migrations-job.yaml @@ -0,0 +1 @@ +{{- include "plugin-br-pix-switch.migrationJob" (dict "context" . "component" "adapterLerian" "serviceName" "adapter-lerian") }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian/pdb.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian/pdb.yaml new file mode 100644 index 000000000..40f55737e --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian/pdb.yaml @@ -0,0 +1,24 @@ +{{- if and .Values.adapterLerian.enabled .Values.adapterLerian.pdb.enabled }} +{{- $component := "adapter-lerian" }} +{{- $values := .Values.adapterLerian }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $values.pdb.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if $values.pdb.minAvailable }} + minAvailable: {{ $values.pdb.minAvailable }} + {{- else if $values.pdb.maxUnavailable }} + maxUnavailable: {{ $values.pdb.maxUnavailable }} + {{- end }} + selector: + matchLabels: + {{- include "plugin-br-pix-switch.selectorLabels" (dict "context" $ "component" $component) | nindent 6 }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian/secrets.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian/secrets.yaml new file mode 100644 index 000000000..594cd2fff --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian/secrets.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.adapterLerian.enabled (not .Values.adapterLerian.useExistingSecret) }} +{{- $component := "adapter-lerian" }} +{{- $values := .Values.adapterLerian }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} +type: Opaque +stringData: + {{- range $key, $value := $values.secrets }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian/service.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian/service.yaml new file mode 100644 index 000000000..f65653462 --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian/service.yaml @@ -0,0 +1,24 @@ +{{- if .Values.adapterLerian.enabled }} +{{- $component := "adapter-lerian" }} +{{- $values := .Values.adapterLerian }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "plugin-br-pix-switch.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ $values.service.type }} + ports: + - port: {{ $values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "plugin-br-pix-switch.selectorLabels" (dict "context" $ "component" $component) | nindent 4 }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/adapter-lerian/serviceaccount.yaml b/charts/plugin-br-pix-switch/templates/adapter-lerian/serviceaccount.yaml new file mode 100644 index 000000000..8276b7d2f --- /dev/null +++ b/charts/plugin-br-pix-switch/templates/adapter-lerian/serviceaccount.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.adapterLerian.enabled .Values.adapterLerian.serviceAccount.create }} +{{- $component := "adapter-lerian" }} +{{- $values := .Values.adapterLerian }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "plugin-br-pix-switch.componentServiceAccountName" (dict "context" $ "component" $component "componentValues" $values) }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "plugin-br-pix-switch.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/plugin-br-pix-switch/templates/bootstrap-postgres.yaml b/charts/plugin-br-pix-switch/templates/bootstrap-postgres.yaml index 9d22b8ad6..6b534d680 100644 --- a/charts/plugin-br-pix-switch/templates/bootstrap-postgres.yaml +++ b/charts/plugin-br-pix-switch/templates/bootstrap-postgres.yaml @@ -105,10 +105,10 @@ spec: ROLE_EXISTS=0 DB_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_roles WHERE rolname='${DB_ROLE}'" | grep -q 1; then + if PGPASSWORD="${DB_ADMIN_PASSWORD}" psql -At -v ON_ERROR_STOP=1 -v role="${DB_ROLE}" -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER_ADMIN}" -d "${DB_DATABASE}" -c "SELECT 1 FROM pg_roles WHERE rolname=:'role'" | grep -q 1; then ROLE_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_database WHERE datname='${DB_NAME}'" | grep -q 1; then + if PGPASSWORD="${DB_ADMIN_PASSWORD}" psql -At -v ON_ERROR_STOP=1 -v db="${DB_NAME}" -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER_ADMIN}" -d "${DB_DATABASE}" -c "SELECT 1 FROM pg_database WHERE datname=:'db'" | grep -q 1; then DB_EXISTS=1 fi diff --git a/charts/plugin-br-pix-switch/values-template.yaml b/charts/plugin-br-pix-switch/values-template.yaml index 54b761e75..adb43ea8c 100644 --- a/charts/plugin-br-pix-switch/values-template.yaml +++ b/charts/plugin-br-pix-switch/values-template.yaml @@ -1,8 +1,9 @@ # Production values template for plugin-br-pix-switch (chart 2.0.0-beta.1+) # # Override only the fields you need to change from values.yaml defaults. -# All 10 components are enabled by default except adapter-btg-mock; disable -# any component by setting its `enabled: false`. +# All 13 components are enabled by default except adapter-btg-mock and the +# adapter-lerian trio (adapterLerian / adapterLerianConsumer / +# adapterLerianSystemplane); disable any component by setting `enabled: false`. # Global image override (applies to every component unless overridden per-component) global: @@ -164,6 +165,46 @@ cobSystemplane: LICENSE_KEY: "" SYSTEMPLANE_SECRET_MASTER_KEY: "" +# ------------------------------------------------------------ +# adapter-lerian (api) — Lerian provider adapter HTTP entry +# ------------------------------------------------------------ +adapterLerian: + enabled: false + configmap: + DEPLOYMENT_MODE: "saas" + PLUGIN_AUTH_ENABLED: "true" + PLUGIN_AUTH_URL: "http://plugin-access-manager-auth.midaz-plugins.svc.cluster.local:4000" + ORGANIZATION_IDS: "global" + secrets: + DATABASE_URL: "postgres://pixswitch:@postgres-host:5432/pix-adapter-lerian?sslmode=require" + SYSTEMPLANE_POSTGRES_DSN: "postgres://pixswitch:@postgres-host:5432/pix-adapter-lerian?sslmode=require" + LICENSE_KEY: "" + +# ------------------------------------------------------------ +# adapter-lerian (consumer) — br-sfn Kafka consumer +# ------------------------------------------------------------ +adapterLerianConsumer: + enabled: false + configmap: + ISPB: "<8-digit-ispb>" + KAFKA_BROKERS: "" + KAFKA_GROUP_ID: "pix-adapter-lerian-cashin" + secrets: + SYSTEMPLANE_POSTGRES_DSN: "postgres://pixswitch:@postgres-host:5432/pix-adapter-lerian?sslmode=require" + VALKEY_URL: "redis://default:@valkey-host:6379/0" + LICENSE_KEY: "" + +# ------------------------------------------------------------ +# adapter-lerian (systemplane) — runtime config plane +# ------------------------------------------------------------ +adapterLerianSystemplane: + enabled: false + configmap: {} + secrets: + DATABASE_URL: "postgres://pixswitch:@postgres-host:5432/pix-adapter-lerian?sslmode=require" + LICENSE_KEY: "" + SYSTEMPLANE_SECRET_MASTER_KEY: "" + # ------------------------------------------------------------ # Disable in-cluster subcharts (use external infra in production) # ------------------------------------------------------------ diff --git a/charts/plugin-br-pix-switch/values.schema.json b/charts/plugin-br-pix-switch/values.schema.json index 021e22de9..b83cdbdba 100644 --- a/charts/plugin-br-pix-switch/values.schema.json +++ b/charts/plugin-br-pix-switch/values.schema.json @@ -100,6 +100,42 @@ }, "additionalProperties": true }, + "adapterLerian": { + "type": "object", + "properties": { + "configmap": { + "type": "object" + }, + "secrets": { + "type": "object" + } + }, + "additionalProperties": true + }, + "adapterLerianConsumer": { + "type": "object", + "properties": { + "configmap": { + "type": "object" + }, + "secrets": { + "type": "object" + } + }, + "additionalProperties": true + }, + "adapterLerianSystemplane": { + "type": "object", + "properties": { + "configmap": { + "type": "object" + }, + "secrets": { + "type": "object" + } + }, + "additionalProperties": true + }, "cobHub": { "type": "object", "properties": { diff --git a/charts/plugin-br-pix-switch/values.yaml b/charts/plugin-br-pix-switch/values.yaml index b8423bf79..d0fe68924 100644 --- a/charts/plugin-br-pix-switch/values.yaml +++ b/charts/plugin-br-pix-switch/values.yaml @@ -46,7 +46,7 @@ global: # # Use cases: # - In-cluster Postgres subchart (postgresql.enabled=true) AND you want - # the chart to create the 3 pix-switch databases for you. Set + # the chart to create the 4 pix-switch databases for you. Set # connection.host to the in-cluster service name # (plugin-br-pix-switch-postgresql). # - External Postgres (RDS, Cloud SQL, etc.) where the operator wants the @@ -64,11 +64,12 @@ global: # values live under component `secrets`, not `configmap`. externalPostgresDefinitions: enabled: false - # -- Databases to create (one Job per database). pix-switch needs 3. + # -- Databases to create (one Job per database). pix-switch needs 4. databases: - pix-spi - pix-dict - pix-cob + - pix-adapter-lerian connection: host: "" port: "5432" @@ -241,9 +242,15 @@ spi: MIDAZ_CLIENT_ID: "" MIDAZ_LEDGER_ID: "" MIDAZ_ORGANIZATION_ID: "" - # Multi-tenant + # -- Multi-tenant (consumed when MULTI_TENANT_ENABLED=true). MULTI_TENANT_URL + # and MULTI_TENANT_API_KEY are seeds for the spi-systemplane store; once + # persisted there, env values become optional (a re-deploy without them is + # a supported rollback path). Circuit breaker defaults match the binary + # (pkg/config: DefaultMultiTenantCBThreshold=5, ...CBTimeoutSec=30). MULTI_TENANT_ENABLED: "false" MULTI_TENANT_URL: "" + MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD: "5" + MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC: "30" secrets: DATABASE_URL: "" @@ -361,12 +368,21 @@ spiSystemplane: OTEL_LIBRARY_NAME: "github.com/LerianStudio/plugin-br-pix-switch" OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "development" OTEL_EXPORTER_OTLP_ENDPOINT: "" + # -- Multi-tenant seeds. When MULTI_TENANT_ENABLED=true, MULTI_TENANT_URL + # and MULTI_TENANT_API_KEY (masked) are seeded into the systemplane store + # at tenant.manager_url / tenant.manager_api_key on first boot. After + # the systemplane row exists, env values can be removed. + MULTI_TENANT_ENABLED: "false" + MULTI_TENANT_URL: "" secrets: DATABASE_URL: "" SYSTEMPLANE_POSTGRES_DSN: "" LICENSE_KEY: "" SYSTEMPLANE_SECRET_MASTER_KEY: "" + # -- Tenant Manager API key (consumed when MULTI_TENANT_ENABLED=true on + # spi-systemplane). Seeded into the systemplane store on first boot. + MULTI_TENANT_API_KEY: "" useExistingSecret: false existingSecretName: "" @@ -576,8 +592,14 @@ dictHub: CRM_ORGANIZATION_ID: "" KEY_CACHE_TTL_SEC: "60" MONGO_DB_NAME: "pix-dict" + # -- Multi-tenant (consumed when MULTI_TENANT_ENABLED=true). MULTI_TENANT_URL + # and MULTI_TENANT_API_KEY are seeds for the dict-systemplane store; the CB + # tunables size the tenant-manager client circuit breaker (defaults from + # pkg/config: threshold=5, timeoutSec=30). MULTI_TENANT_ENABLED: "false" MULTI_TENANT_URL: "" + MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD: "5" + MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC: "30" secrets: DATABASE_URL: "" @@ -698,6 +720,10 @@ dictHubVsync: CRM_BASE_URL: "" CRM_CLIENT_ID: "" CRM_ORGANIZATION_ID: "" + # -- Multi-tenant (vsync uses the per-tenant DSN resolver only; no + # API key here, no CB tunables — the resolver shares the dict-hub + # tenant-manager client config). + MULTI_TENANT_ENABLED: "false" MULTI_TENANT_URL: "" # VSync tunables VSYNC_ENABLED: "true" @@ -910,6 +936,10 @@ dictSystemplane: OTEL_LIBRARY_NAME: "github.com/LerianStudio/plugin-br-pix-switch" OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "development" OTEL_EXPORTER_OTLP_ENDPOINT: "" + # -- Multi-tenant flag. dict-systemplane reads only the boolean (URL + + # API key live on spi-systemplane). When enabled, the systemplane store + # is bootstrapped in MT mode. + MULTI_TENANT_ENABLED: "false" secrets: DATABASE_URL: "" @@ -1011,8 +1041,14 @@ cobHub: PLUGIN_AUTH_URL: "http://plugin-access-manager-auth.midaz-plugins.svc.cluster.local:4000" LICENSE_ORGANIZATION_IDS: "global" ORGANIZATION_ID: "" + # -- Multi-tenant (consumed when MULTI_TENANT_ENABLED=true). MULTI_TENANT_URL + # and MULTI_TENANT_API_KEY are seeds for the cob-systemplane store; the CB + # tunables size the tenant-manager client circuit breaker (defaults from + # pkg/config: threshold=5, timeoutSec=30). MULTI_TENANT_ENABLED: "false" MULTI_TENANT_URL: "" + MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD: "5" + MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC: "30" secrets: DATABASE_URL: "" @@ -1221,6 +1257,10 @@ cobSystemplane: OTEL_LIBRARY_NAME: "github.com/LerianStudio/plugin-br-pix-switch" OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "development" OTEL_EXPORTER_OTLP_ENDPOINT: "" + # -- Multi-tenant flag. cob-systemplane reads only the boolean (URL + + # API key live on spi-systemplane). When enabled, the systemplane store + # is bootstrapped in MT mode. + MULTI_TENANT_ENABLED: "false" secrets: DATABASE_URL: "" @@ -1335,6 +1375,356 @@ appsIngress: component: cobProxy serviceName: cob-proxy +# ----------------------------------------------------------------------------- +# adapter-lerian (api) — Lerian provider adapter HTTP entry (br-sfn) +# ----------------------------------------------------------------------------- +adapterLerian: + enabled: false # safety default — explicitly enable in dev/staging values + name: "adapter-lerian" + description: "Lerian provider adapter (API)" + replicaCount: 1 + revisionHistoryLimit: 10 + + image: + repository: "ghcr.io/lerianstudio/plugin-br-pix-switch-adapter-lerian-api" + pullPolicy: "" + tag: "" + + imagePullSecrets: [] + + # -- Override container entrypoint (e.g., to dispatch to a specific binary + # inside a multi-binary image). When empty, the image's ENTRYPOINT is used. + command: [] + # -- Container args + args: [] + + podAnnotations: {} + podSecurityContext: {} + securityContext: + runAsGroup: 1000 + runAsUser: 1000 + runAsNonRoot: true + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + + pdb: + enabled: false + minAvailable: 0 + maxUnavailable: 1 + + deploymentStrategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + + service: + type: ClusterIP + port: 4113 + + ingress: + enabled: false + + resources: + limits: { cpu: 200m, memory: 256Mi } + requests: { cpu: 50m, memory: 64Mi } + + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 1 + + readinessProbe: + path: "/lerian/readyz" + livenessProbe: + path: "/lerian/health" + nodeSelector: {} + tolerations: [] + affinity: {} + + serviceAccount: + create: true + annotations: {} + name: "" + + configmap: + APPLICATION_NAME: "plugin-br-pix-switch" + SERVER_ADDRESS: ":4113" + LOG_LEVEL: "info" + ENV_NAME: "development" + DEPLOYMENT_MODE: "byoc" + TELEMETRY_ENABLED: "false" + OTEL_RESOURCE_SERVICE_NAME: "pix-adapter-lerian-api" + OTEL_LIBRARY_NAME: "github.com/LerianStudio/plugin-br-pix-switch" + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "development" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + PLUGIN_AUTH_ENABLED: "false" + PLUGIN_AUTH_URL: "http://plugin-access-manager-auth.midaz-plugins.svc.cluster.local:4000" + # License allow-list (use "global" for single-license mode). + ORGANIZATION_IDS: "" + # -- Multi-tenant (consumed when MULTI_TENANT_ENABLED=true). MULTI_TENANT_URL + # is the tenant-manager address; MULTI_TENANT_API_KEY lives under secrets. + MULTI_TENANT_ENABLED: "false" + MULTI_TENANT_URL: "" + + secrets: + # -- DATABASE_URL backs the golang-migrate Job that applies the component's + # SQL migrations. SYSTEMPLANE_POSTGRES_DSN is the api's sole routing source + # (read-only consumer DSN for adapter_lerian.* runtime config). + DATABASE_URL: "" + SYSTEMPLANE_POSTGRES_DSN: "" + LICENSE_KEY: "" + MULTI_TENANT_API_KEY: "" + + useExistingSecret: false + existingSecretName: "" + + extraEnvVars: {} + + # -- golang-migrate Job that applies SQL migrations from the component's + # /migrations directory against the database referenced by DATABASE_URL. + # Runs as a Helm hook on pre-upgrade + post-install. + migrations: + enabled: true + migrationsPath: /migrations + ttlSecondsAfterFinished: 300 + backoffLimit: 3 + +# ----------------------------------------------------------------------------- +# adapter-lerian (consumer) — br-sfn CloudEvents Kafka consumer (no ingress) +# ----------------------------------------------------------------------------- +adapterLerianConsumer: + enabled: false # safety default — explicitly enable in dev/staging values + name: "adapter-lerian-consumer" + description: "Lerian provider adapter (Kafka consumer)" + replicaCount: 1 + revisionHistoryLimit: 10 + + image: + repository: "ghcr.io/lerianstudio/plugin-br-pix-switch-adapter-lerian-consumer" + pullPolicy: "" + tag: "" + + imagePullSecrets: [] + + # -- Override container entrypoint (e.g., to dispatch to a specific binary + # inside a multi-binary image). When empty, the image's ENTRYPOINT is used. + command: [] + # -- Container args + args: [] + + podAnnotations: {} + podSecurityContext: {} + securityContext: + runAsGroup: 1000 + runAsUser: 1000 + runAsNonRoot: true + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + + pdb: + enabled: false + minAvailable: 0 + maxUnavailable: 1 + + deploymentStrategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + + service: + type: ClusterIP + port: 4114 + + resources: + limits: { cpu: 500m, memory: 512Mi } + requests: { cpu: 100m, memory: 128Mi } + + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 1 + + readinessProbe: + path: "/lerian/readyz" + livenessProbe: + path: "/lerian/health" + nodeSelector: {} + tolerations: [] + affinity: {} + + serviceAccount: + create: true + annotations: {} + name: "" + + configmap: + APPLICATION_NAME: "plugin-br-pix-switch" + SERVER_ADDRESS: ":4114" + LOG_LEVEL: "info" + ENV_NAME: "development" + DEPLOYMENT_MODE: "byoc" + TELEMETRY_ENABLED: "false" + OTEL_RESOURCE_SERVICE_NAME: "pix-adapter-lerian-consumer" + OTEL_LIBRARY_NAME: "github.com/LerianStudio/plugin-br-pix-switch" + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "development" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + PLUGIN_AUTH_ENABLED: "false" + PLUGIN_AUTH_URL: "http://plugin-access-manager-auth.midaz-plugins.svc.cluster.local:4000" + ORGANIZATION_IDS: "" + # -- This deployment's 8-digit BACEN participant ISPB. Read once at boot to + # scope the ce-id dedup keyspace. Set the real ISPB per deployment. + ISPB: "" + MULTI_TENANT_ENABLED: "false" + # -- Kafka consumer (franz-go). In production KAFKA_BROKERS points at the + # external br-sfn Kafka (ADR-002). SASL credentials live under secrets. + KAFKA_BROKERS: "" + KAFKA_GROUP_ID: "pix-adapter-lerian-cashin" + KAFKA_TLS_ENABLED: "false" + KAFKA_SASL_MECHANISM: "" + KAFKA_DLQ_TOPIC: "br-spi.adapter-lerian.dlq" + KAFKA_MAX_CONCURRENT_PARTITIONS: "8" + CONSUMER_DEDUP_TTL_SEC: "3600" + + secrets: + # -- SYSTEMPLANE_POSTGRES_DSN: read-only routing config DSN (empty disables + # the consumer). VALKEY_URL backs the ce-id dedup cache. KAFKA_SASL_* are the + # broker credentials used when KAFKA_SASL_MECHANISM is set. + SYSTEMPLANE_POSTGRES_DSN: "" + VALKEY_URL: "" + KAFKA_SASL_USER: "" + KAFKA_SASL_PASSWORD: "" + LICENSE_KEY: "" + + useExistingSecret: false + existingSecretName: "" + + extraEnvVars: {} + +# ----------------------------------------------------------------------------- +# adapter-lerian (systemplane) — runtime config plane for adapter-lerian +# ----------------------------------------------------------------------------- +adapterLerianSystemplane: + enabled: false # safety default — explicitly enable in dev/staging values + name: "adapter-lerian-systemplane" + description: "Runtime config plane for adapter-lerian" + replicaCount: 1 + revisionHistoryLimit: 10 + + image: + repository: "ghcr.io/lerianstudio/plugin-br-pix-switch-adapter-lerian-systemplane-api" + pullPolicy: "" + tag: "" + + imagePullSecrets: [] + + # -- Override container entrypoint (e.g., to dispatch to a specific binary + # inside a multi-binary image). When empty, the image's ENTRYPOINT is used. + command: [] + # -- Container args + args: [] + + podAnnotations: {} + podSecurityContext: {} + securityContext: + runAsGroup: 1000 + runAsUser: 1000 + runAsNonRoot: true + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + + pdb: + enabled: false + minAvailable: 0 + maxUnavailable: 1 + + deploymentStrategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + + service: + type: ClusterIP + port: 4115 + + resources: + limits: { cpu: 200m, memory: 256Mi } + requests: { cpu: 50m, memory: 64Mi } + + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 1 + + readinessProbe: + path: "/lerian/readyz" + livenessProbe: + path: "/lerian/health" + nodeSelector: {} + tolerations: [] + affinity: {} + + serviceAccount: + create: true + annotations: {} + name: "" + + configmap: + APPLICATION_NAME: "plugin-br-pix-switch" + SERVER_ADDRESS: ":4115" + LOG_LEVEL: "info" + ENV_NAME: "development" + DEPLOYMENT_MODE: "byoc" + REQUEST_TIMEOUT_SEC: "30" + TELEMETRY_ENABLED: "false" + OTEL_RESOURCE_SERVICE_NAME: "pix-adapter-lerian-systemplane" + OTEL_LIBRARY_NAME: "github.com/LerianStudio/plugin-br-pix-switch" + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "development" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + PLUGIN_AUTH_ENABLED: "false" + PLUGIN_AUTH_URL: "http://plugin-access-manager-auth.midaz-plugins.svc.cluster.local:4000" + # -- Multi-tenant. MULTI_TENANT_URL + MULTI_TENANT_API_KEY (secret) are + # seeded into the store; the CB tunables size the tenant-manager client. + MULTI_TENANT_ENABLED: "false" + MULTI_TENANT_URL: "" + MULTI_TENANT_CIRCUIT_BREAKER_THRESHOLD: "5" + MULTI_TENANT_CIRCUIT_BREAKER_TIMEOUT_SEC: "30" + # -- Address-book / instance routing seeds (first-boot only; operators + # manage them via the systemplane admin API afterwards). + SWITCH_BASE_URL: "" + INTEGRATION_MODE: "" + BRSFN_BASE_URL: "" + BRSFN_CLIENT_ID: "" + + secrets: + DATABASE_URL: "" + LICENSE_KEY: "" + # -- 32-byte AES-256-GCM key; required in non-local deployment modes. + SYSTEMPLANE_SECRET_MASTER_KEY: "" + MULTI_TENANT_API_KEY: "" + # -- OAuth token endpoint; classified as a secret to satisfy the credential-name chart standard. + BRSFN_TOKEN_URL: "" + BRSFN_CLIENT_SECRET: "" + + useExistingSecret: false + existingSecretName: "" + + extraEnvVars: {} + systemplaneIngress: enabled: false className: "nginx" @@ -1354,6 +1744,10 @@ systemplaneIngress: pathType: Prefix component: cobSystemplane serviceName: cob-systemplane + - path: /lerian + pathType: Prefix + component: adapterLerianSystemplane + serviceName: adapter-lerian-systemplane # ----------------------------------------------------------------------------- # Providers Ingress — outbound-provider adapters (BTG mock, BACEN, JD, ...). @@ -1379,6 +1773,10 @@ providersIngress: pathType: Prefix component: adapterBtgMock serviceName: adapter-btg-mock + - path: /lerian + pathType: Prefix + component: adapterLerian + serviceName: adapter-lerian # Reserved for future components — uncomment when the bacen/jd # adapter components ship in the chart: # - path: /bacen diff --git a/charts/plugin-fees/Chart.yaml b/charts/plugin-fees/Chart.yaml index 45f850cb5..7499ab99e 100644 --- a/charts/plugin-fees/Chart.yaml +++ b/charts/plugin-fees/Chart.yaml @@ -11,7 +11,7 @@ maintainers: 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: 7.2.0 +version: 7.3.0-beta.1 # This is the version number of the application being deployed. appVersion: "3.3.0" # A list of keywords about the chart. This helps others discover the chart. diff --git a/charts/product-console/Chart.yaml b/charts/product-console/Chart.yaml index 015c26a7e..ee463abd5 100644 --- a/charts/product-console/Chart.yaml +++ b/charts/product-console/Chart.yaml @@ -19,7 +19,7 @@ maintainers: # 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: 3.1.0 +version: 3.2.0-beta.1 # This is the version number of the application being deployed. appVersion: "1.6.0" diff --git a/charts/reporter/Chart.yaml b/charts/reporter/Chart.yaml index a4194e73f..ecd319de7 100644 --- a/charts/reporter/Chart.yaml +++ b/charts/reporter/Chart.yaml @@ -14,7 +14,7 @@ maintainers: 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: 3.1.1 +version: 3.2.0-beta.1 # This is the version number of the application being deployed. appVersion: "2.2.0" # A list of keywords about the chart. This helps others discover the chart. diff --git a/charts/streaming-hub/.helmignore b/charts/streaming-hub/.helmignore new file mode 100644 index 000000000..8e2583494 --- /dev/null +++ b/charts/streaming-hub/.helmignore @@ -0,0 +1,24 @@ +# Patterns to ignore when building packages. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +# Chart docs are not part of the packaged artifact. +docs/ +README.md diff --git a/charts/streaming-hub/Chart.yaml b/charts/streaming-hub/Chart.yaml new file mode 100644 index 000000000..fed353409 --- /dev/null +++ b/charts/streaming-hub/Chart.yaml @@ -0,0 +1,36 @@ +apiVersion: v2 +name: streaming-hub-helm +description: >- + Lerian streaming-hub — consumes lib-streaming CloudEvents from Kafka/Redpanda + and fans them out to per-tenant SaaS/BYOC subscribers. Multi-component chart + with a runtime STREAMING_HUB_ROLE model (mode: all | split). +type: application +# version is a PLACEHOLDER. semantic-release in the `helm` repo owns the real +# chart version on merge — do NOT hand-pick a final number here. + +version: 1.0.0-beta.4 +appVersion: "1.0.0" + +annotations: + lerian.studio/chart-type: multi-component +home: https://github.com/LerianStudio/streaming-hub +sources: + - https://github.com/LerianStudio/helm/tree/main/charts/streaming-hub + - https://github.com/LerianStudio/streaming-hub +maintainers: + - name: "Lerian Studio" + email: "support@lerian.studio" +keywords: + - streaming + - cloudevents + - kafka + - redpanda + - fan-out + - lerian +icon: https://avatars.githubusercontent.com/u/148895005?s=200&v=4 + +# NO dependencies block by deliberate design (deviation from reporter/midaz): +# OTEL is env-wired via the OTEL_EXPORTER_OTLP_ENDPOINT downward-API override, +# not bundled as an otel-collector-lerian subchart. Declaring it would force an +# OCI pull on every `helm lint` / `helm template`. Kafka/Redpanda and Postgres +# are shared external infra, never subcharts here. diff --git a/charts/streaming-hub/README.md b/charts/streaming-hub/README.md new file mode 100644 index 000000000..2c3fb1a97 --- /dev/null +++ b/charts/streaming-hub/README.md @@ -0,0 +1,220 @@ +# streaming-hub Helm chart + +Deploys **streaming-hub** — the Lerian service that consumes +[`lib-streaming`](https://github.com/LerianStudio) CloudEvents from +Kafka/Redpanda and fans them out to per-tenant SaaS/BYOC subscribers. + +This is a **multi-component** chart built on one image and one binary, selected +at runtime into a role via `STREAMING_HUB_ROLE`. There are **no** database, +broker, or OTEL subcharts — Kafka/Redpanda and PostgreSQL are shared external +infra, and OTEL is env-wired (see [External dependencies](#external-dependencies)). + +--- + +## Chart Contract + +- Chart type: `multi-component` +- Required secrets: `secrets.STREAMING_HUB_POSTGRES_DSN` (the DSN carries the DB password); `secrets.STREAMING_HUB_KAFKA_SCRAM_USERNAME` + `secrets.STREAMING_HUB_KAFKA_SCRAM_PASSWORD` (when the broker requires SASL/SCRAM); `secrets.STREAMING_HUB_KEK_REF` (KEK reference for the F10 envelope crypto — `secrets.STREAMING_HUB_DEV_KEK` for dev only). SaaS-only, required when `STREAMING_HUB_MULTI_TENANT_ENABLED=true`: `secrets.STREAMING_HUB_TENANT_MANAGER_SERVICE_API_KEY` and `secrets.STREAMING_HUB_MULTI_TENANT_REDIS_PASSWORD`. All blank by default — provide inline or set `streamingHub.useExistingSecret` + `streamingHub.existingSecretName` (the path GitOps uses, with a Vault-injected Secret). +- Dependency notes: **No bundled subcharts.** Kafka/Redpanda and PostgreSQL are shared external infra — supply `common.configmap.STREAMING_HUB_KAFKA_BROKERS` and `secrets.STREAMING_HUB_POSTGRES_DSN`. OTEL is env-wired to a node-local collector (`HOST_IP:4317`, gated on `streamingHub.telemetry.enabled`) — there is no OTEL collector subchart. The optional `bootstrap-postgres` Job provisions the hub's single database when `global.externalPostgresDefinitions.enabled=true`. +- Migrations: the hub applies its schema **out of band** (the app never migrates itself). Enable the migration Job with `streamingHub.migrations.enabled=true` (default off). It is a PreSync hook that runs the stock `migrate/migrate` toolchain (image `ghcr.io/lerianstudio/streaming-hub-migrations`) BEFORE the app rolls out. The hook chain is **`bootstrap-postgres` (sync-wave -10, role+db) → `migration-secret` (-5, the DSN) → `migrations` Job (-1, the schema) → app Deployment (main Sync)**. The Job's only env is `STREAMING_HUB_POSTGRES_DSN` (from the migration-secret, or `migrations.existingSecretName` when `migrations.useExistingSecret=true`). Without it, ingest/dispatcher/partition workers crash on `relation "event_inbox"/"delivery_jobs" does not exist (42P01)`. **When `migrations.useExistingSecret=true`, the named secret must already exist in the namespace before the PreSync phase runs** — do not point it at the chart-managed application Secret (`streamingHub.existingSecretName` / inline `secrets.*`), which is created later in the main Sync phase and is therefore absent when the hook fires; the chart provisions its own PreSync `migration-secret` precisely to close that ordering gap. +- Production overrides: choose `streamingHub.mode` (`all` vs `split`); size per-role `replicaCount` / `autoscaling` / `resources` and the Postgres pool (`poolMaxOpenConns` / `poolMaxIdleConns`) honoring **Σ(replicas × poolMaxOpenConns) ≤ Postgres `max_connections`**; set `image.tag`, `ingress`, and the secrets (inline or `useExistingSecret`). +- Source/license: Source is in `github.com/LerianStudio/helm`; license is Apache-2.0. + +--- + +## Installing the Chart + +```bash +helm install streaming-hub oci://registry-1.docker.io/lerianstudio/streaming-hub-helm --version -n streaming-hub --create-namespace +``` + +With a custom values file: + +```bash +helm install streaming-hub oci://registry-1.docker.io/lerianstudio/streaming-hub-helm --version -n streaming-hub -f my-values.yaml +``` + +The chart is mirrored to `oci://ghcr.io/lerianstudio/streaming-hub-helm` as well. + +## Uninstalling the Chart + +```bash +helm uninstall streaming-hub -n streaming-hub +``` + +--- + +## The `mode` switch (read this first) + +`streamingHub.mode` decides the topology. It is an **either/or**: + +| `mode` | Renders | `STREAMING_HUB_ROLE` | When | +|---------|---------|----------------------|------| +| `all` (default) | ONE Deployment + Service (+ HPA/PDB) | `all` | Single co-resident deployment. The dev-st target. Byte-equivalent to the historical single binary. | +| `split` | TWO Deployments + Services (ingest + delivery), each with its own HPA/PDB | `ingest` / `delivery` | Ingest and delivery scaled independently as N + M replicas. | + +### > **DOUBLE-CONSUME HAZARD — the load-bearing rule** + +> **NEVER run a `mode: all` release AND a `mode: split` release against the same +> Kafka/Redpanda cluster.** All three roles (`all`, `ingest`) join the **one** +> ingest consumer group. An `all` pod and an `ingest` pod consuming together +> means **every event is double-consumed and double-delivered.** The `mode` +> switch enforces either/or within a single release — do not defeat it by +> deploying two releases that overlap. + +### Worked example — `mode: all` (dev-st) + +```yaml +streamingHub: + mode: all + all: + replicaCount: 1 + poolMaxOpenConns: 25 # role=all holds both planes' connections + poolMaxIdleConns: 12 +``` + +Renders: `streaming-hub-all` Deployment + `streaming-hub-all` Service, plus the +shared `streaming-hub` ConfigMap + Secret + ServiceAccount. One consumer-group +member set — no double-consume possible. + +### Worked example — `mode: split` (independent scaling) + +```yaml +streamingHub: + mode: split + ingest: + replicaCount: 3 + poolMaxOpenConns: 8 # consume-poll + inbox tx + partition-cron + poolMaxIdleConns: 4 + autoscaling: { enabled: true, minReplicas: 3, maxReplicas: 4 } + delivery: + replicaCount: 2 + poolMaxOpenConns: 16 # 8 dispatch workers + reclaim + dlq + delivery /readyz + poolMaxIdleConns: 10 + autoscaling: { enabled: true, minReplicas: 2, maxReplicas: 4 } +``` + +Renders `streaming-hub-ingest` and `streaming-hub-delivery` Deployments + +Services + HPAs, sharing the one ConfigMap + Secret. Each Deployment gets its +own `STREAMING_HUB_ROLE` and pool sizing as **explicit per-Deployment env** +(which wins over the shared `envFrom`). + +--- + +## Connection-budget invariant + +streaming-hub owns **ONE** shared PostgreSQL database. Every open connection on +every pod of every role draws from that single `max_connections` budget. + +> **Σ over all running pods of `(replicas × poolMaxOpenConns)` + headroom ≤ PostgreSQL `max_connections`.** +> Under HPA, use `maxReplicas` (not `replicaCount`) in the sum. + +Worked example (`max_connections = 100`, `mode: split`): + +| Role | replicas | poolMaxOpenConns | connections | +|------|----------|------------------|-------------| +| ingest | 3 | 8 | 24 | +| delivery | 4 | 16 | 64 | +| **total** | | | **88** (leaves 12 for admin/migrations) ✅ | + +Adding an `all` pod (25) → `88 + 25 = 113 > 100` ❌. The `mode` either/or already +forbids that combination; the budget math is why it also matters operationally. +See `streaming-hub/.env.reference` (`STREAMING_HUB_POSTGRES_MAX_OPEN_CONNS`) for +the per-role rationale (all 25/12, ingest 8/4, delivery 16/10). + +--- + +## Health, drain, and termination + +- **Probes** (verified against the hub source / Dockerfile, on the `http` port `8080`): + - `livenessProbe`: `GET /healthz` — stays `200` throughout drain. + - `readinessProbe`: `GET /readyz` — flips `NotReady` (503) **first** on SIGTERM so the endpoints controller drains the pod from the Service. +- **No `preStop` hook.** The hub **self-drains on SIGTERM**: the exec-form + ENTRYPOINT delivers SIGTERM straight to PID 1, and the lib-commons Launcher + runs the HTTP → consumer → dispatcher teardown. +- **`terminationGracePeriodSeconds` defaults to `80` — the hub's derived SIGTERM + drain ceiling — and MUST stay at or above it.** At the shipped + `STREAMING_HUB_SHUTDOWN_TIMEOUT=30s` + `STREAMING_HUB_PRE_STOP_DRAIN_TIMEOUT=5s` + the ceiling is `30s` (HTTP) + `5s` (consumer final-commit) + `30s` (dispatcher, + `min(ShutdownTimeout, 55s)`) + `10s` slack = `75s`, plus the `5s` pre-stop wait + = **`80s`**. If you tune those knobs up, recompute and raise this knob to match, + or the orchestrator SIGKILLs a still-draining replica (risking a duplicate + delivery). See `.env.reference`. + +--- + +## Secrets sourcing + +Two mutually-exclusive paths: + +| `useExistingSecret` | Behavior | +|---------------------|----------| +| `false` (default) | The chart renders `templates/secret.yaml` from `streamingHub.secrets` (base64-encoded; **empty values are skipped**, so unset SaaS/dev keys never ship blank). | +| `true` | **No** Secret is rendered. Deployments reference `existingSecretName`. This is the **gitops/Vault path** (an external secret is projected into the named Secret) and is the production default. | + +Sensitive keys (all in `streamingHub.secrets`, all default `""`): +`STREAMING_HUB_POSTGRES_DSN`, `STREAMING_HUB_KAFKA_SCRAM_USERNAME`, +`STREAMING_HUB_KAFKA_SCRAM_PASSWORD`, `STREAMING_HUB_KEK_REF`, +`STREAMING_HUB_DEV_KEK` (dev only), `STREAMING_HUB_TENANT_MANAGER_SERVICE_API_KEY`, +`STREAMING_HUB_MULTI_TENANT_REDIS_PASSWORD`. + +--- + +## External dependencies + +This chart provisions **none** of the following — they live outside it: + +- **Kafka / Redpanda** — the CloudEvents bus the hub consumes. Point + `STREAMING_HUB_KAFKA_BROKERS` at the cluster; supply SASL/SCRAM creds via the + Secret when `STREAMING_HUB_KAFKA_SCRAM_MECHANISM` is set. +- **PostgreSQL** — the single hub-owned database (`tenant_id` is a column, not a + per-tenant DB). Either point `STREAMING_HUB_POSTGRES_DSN` at a pre-provisioned + managed host, **or** enable `global.externalPostgresDefinitions.enabled` to run + the bootstrap Job that creates the hub's one DB + role on a shared host. +- **OTEL collector** — **not** a subchart (deliberate; declaring it would force + an OCI pull on `helm lint`/`template`). When `streamingHub.telemetry.enabled=true` + (a chart-level toggle, not an app env var), the Deployment injects `HOST_IP` via + the downward API and sets `OTEL_EXPORTER_OTLP_ENDPOINT=$(HOST_IP):4317` + (node-local DaemonSet collector). + +--- + +## Top-level values + +| Key | Default | Description | +|-----|---------|-------------| +| `streamingHub.mode` | `all` | Topology switch: `all` \| `split`. Schema-enforced enum. | +| `streamingHub.image.repository` | `ghcr.io/lerianstudio/streaming-hub` | Image repo. | +| `streamingHub.image.tag` | `""` | Empty falls back to `Chart.appVersion`. | +| `streamingHub.image.pullPolicy` | `IfNotPresent` | | +| `streamingHub.imagePullSecrets` | `[{name: ghcr-credential}]` | Private registry pull secrets. | +| `streamingHub.service.type` | `ClusterIP` | Lerian convention (Ingress fronts external). | +| `streamingHub.service.port` | `8080` | Control-plane HTTP port. | +| `streamingHub.ingress.enabled` | `false` | Control-plane API ingress (opt-in per env). | +| `streamingHub.serviceAccount.create` | `true` | | +| `streamingHub.terminationGracePeriodSeconds` | `80` | Defaults to the ~80s drain ceiling; keep at or above it. | +| `streamingHub.securityContext` | nonroot 65532, drop ALL, RO rootfs, RuntimeDefault | Distroless:nonroot. | +| `streamingHub.useExistingSecret` | `false` | `true` = Vault/gitops path. | +| `streamingHub.existingSecretName` | `""` | Required when `useExistingSecret`. | +| `streamingHub.common.configmap` | (documented set) | Shared non-sensitive env. Every key is verbatim in `.env.reference`. | +| `streamingHub.secrets` | (all `""`) | Shared sensitive env. Empty values skipped. | +| `streamingHub..replicaCount` | `1` | Per role: `all` / `ingest` / `delivery`. | +| `streamingHub..poolMaxOpenConns` | all `25` / ingest `8` / delivery `16` | Postgres pool (connection-budget invariant). | +| `streamingHub..poolMaxIdleConns` | all `12` / ingest `4` / delivery `10` | | +| `streamingHub..autoscaling.enabled` | `false` | HPA per role (`autoscaling/v2`, CPU+memory). | +| `streamingHub..pdb.enabled` | `false` | PodDisruptionBudget per role (`policy/v1`). | +| `global.externalPostgresDefinitions.enabled` | `false` | Bootstrap Job for the hub's one DB/role (PreSync, sync-wave -10). | +| `streamingHub.migrations.enabled` | `false` | Out-of-band schema migration Job (PreSync, sync-wave -1). | +| `streamingHub.migrations.image.repository` | `ghcr.io/lerianstudio/streaming-hub-migrations` | Migrations image (`FROM migrate/migrate` + the hub's `migrations/`). | +| `streamingHub.migrations.image.tag` | `""` | Empty falls back to `streamingHub.image.tag`, then `Chart.appVersion`. | +| `streamingHub.migrations.image.digest` | `""` | Pin by digest; wins over tag. | +| `streamingHub.migrations.useExistingSecret` | `false` | `true` = Job reads `STREAMING_HUB_POSTGRES_DSN` from `existingSecretName`. | +| `streamingHub.migrations.existingSecretName` | `""` | Required when `migrations.useExistingSecret`. Must already exist before the PreSync phase — not the main-Sync app Secret. | +| `streamingHub.migrations.backoffLimit` | `3` | Job retry cap. | +| `streamingHub.migrations.activeDeadlineSeconds` | `600` | Job wall-clock cap. | +| `streamingHub.migrations.ttlSecondsAfterFinished` | `600` | Finished-Job GC TTL. | + +For the full env contract (defaults, required-in-SaaS markers, the F4 tenant +caution, KEK source vars), see `streaming-hub/.env.reference`. + +See [`docs/TOPOLOGY.md`](docs/TOPOLOGY.md) for the role model in depth. diff --git a/charts/streaming-hub/docs/TOPOLOGY.md b/charts/streaming-hub/docs/TOPOLOGY.md new file mode 100644 index 000000000..1e87aa251 --- /dev/null +++ b/charts/streaming-hub/docs/TOPOLOGY.md @@ -0,0 +1,55 @@ +# streaming-hub deployment topology + +streaming-hub ships as **one image, one binary**, selected into a role at +**runtime** via `STREAMING_HUB_ROLE` ∈ `{all, ingest, delivery}`. There is no +build-time split — no `cmd/ingest`/`cmd/delivery`, no `ARG ROLE`. This chart +exposes that runtime choice through the single `streamingHub.mode` switch. + +## The role model + +Every role serves the **full** Fiber control plane on `:8080` +(`/healthz`, `/readyz`, `/metrics`, `/v1`, `/admin`). The role gates **which +background Launcher Apps register** and **which Kafka clients dial** — not which +HTTP routes mount. + +| Role | Background apps | Kafka clients | Postgres pool (open/idle) | +|------|-----------------|---------------|---------------------------| +| `all` | every app (ingest + delivery co-resident) | all three | 25 / 12 | +| `ingest` | consumer + manifest-refresh + partition-cron + idempotency-reaper | ingest consumer only | 8 / 4 | +| `delivery` | dispatcher + dlq-consumer + dlq-prune + topic-reconciler | DLQ + reconciler-admin | 16 / 10 | + +## How the chart maps mode → workloads + +- **`mode: all`** → `templates/all/*` render: one `streaming-hub-all` Deployment + with `STREAMING_HUB_ROLE=all`, plus its Service (and HPA/PDB when enabled). +- **`mode: split`** → `templates/ingest/*` and `templates/delivery/*` render: + two Deployments (`streaming-hub-ingest`, `streaming-hub-delivery`) with the + matching role env, plus their Services (and per-role HPA/PDB). + +The shared singletons (`templates/configmap.yaml`, `templates/secret.yaml`, +`templates/serviceaccount.yaml`) are role-invariant and rendered exactly once +regardless of mode. The Deployment/Service/HPA/PDB bodies are a single +parameterized partial (`templates/_deployment.tpl`) invoked by thin +mode-gated wrappers — no per-role body duplication. + +### Why the role-specific vars are explicit `env`, not in the ConfigMap + +`STREAMING_HUB_ROLE` and the Postgres pool sizes differ per role. Kubernetes env +precedence is **`env` > `envFrom`**, so the chart injects those three vars as +explicit per-Deployment `env:` while everything role-invariant stays in the one +shared ConfigMap consumed via `envFrom`. A role's pool size therefore cleanly +overrides any shared default, and the ConfigMap never has to fork per role. + +## Independent scaling and the consumer group + +In `mode: split`, ingest and delivery scale independently as **N + M** replicas +in one ingest consumer group; delivery's DLQ consumer is a structurally-disjoint +second group. Background singleton crons stay singleton across replicas via the +hub's `internal/shared/dblock` advisory-lock registry — the chart does not need +to pin them to a single replica. + +> **Never overlap `mode: all` with `mode: split` on the same Kafka cluster.** +> `all` and `ingest` join the same consumer group → double-consume. See the +> README's double-consume hazard callout. + +For the full architecture, see `streaming-hub/docs/architecture.md`. diff --git a/charts/streaming-hub/templates/_deployment.tpl b/charts/streaming-hub/templates/_deployment.tpl new file mode 100644 index 000000000..7baae9eb1 --- /dev/null +++ b/charts/streaming-hub/templates/_deployment.tpl @@ -0,0 +1,251 @@ +{{/* +============================================================================= +streaming-hub.deployment — the shared, component-parameterized Deployment. +Input: dict { root (the root context "."), component ("all"|"ingest"|"delivery") }. + +All three roles run the SAME image and serve the SAME full control plane on +:8080. They differ ONLY in: + - STREAMING_HUB_ROLE (the literal component) + - Postgres pool sizing (poolMaxOpenConns / poolMaxIdleConns) + - replicas / resources / scheduling + +The role-specific vars are injected as EXPLICIT per-Deployment env, which WINS +over envFrom (k8s precedence: env > envFrom). So a per-role pool size cleanly +overrides any shared default, and the shared ConfigMap deliberately omits +STREAMING_HUB_ROLE / the pool vars. +============================================================================= +*/}} +{{- define "streaming-hub.deployment" -}} +{{- $ := .root -}} +{{- $component := .component -}} +{{- $cfg := index $.Values.streamingHub $component -}} +{{- $sh := $.Values.streamingHub -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "streaming-hub.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" $ }} + labels: + {{- include "streaming-hub.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $sh.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + revisionHistoryLimit: {{ $sh.revisionHistoryLimit | default 10 }} + {{- with $sh.deploymentStrategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- if not $cfg.autoscaling.enabled }} + replicas: {{ $cfg.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "streaming-hub.componentSelectorLabels" (dict "context" $ "component" $component) | nindent 6 }} + template: + metadata: + labels: + {{- include "streaming-hub.labels" (dict "context" $ "component" $component) | nindent 8 }} + {{- with $sh.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with $sh.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "streaming-hub.serviceAccountName" $ }} + automountServiceAccountToken: {{ $sh.serviceAccount.automountServiceAccountToken | default false }} + terminationGracePeriodSeconds: {{ $sh.terminationGracePeriodSeconds | default 80 }} + {{- with $sh.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: streaming-hub + securityContext: + {{- toYaml $sh.securityContext | nindent 12 }} + image: "{{ $sh.image.repository }}:{{ include "streaming-hub.defaultTag" $ }}" + imagePullPolicy: {{ $sh.image.pullPolicy }} + ports: + - name: http + containerPort: {{ $sh.service.port }} + protocol: TCP + envFrom: + - secretRef: + name: {{ include "streaming-hub.secretName" $ }} + - configMapRef: + name: {{ include "streaming-hub.fullname" $ }} + env: + # --- role differentiator (explicit env WINS over envFrom) --- + - name: STREAMING_HUB_ROLE + value: {{ $component | quote }} + - name: STREAMING_HUB_POSTGRES_MAX_OPEN_CONNS + value: {{ $cfg.poolMaxOpenConns | quote }} + - name: STREAMING_HUB_POSTGRES_MAX_IDLE_CONNS + value: {{ $cfg.poolMaxIdleConns | quote }} + {{- with $sh.extraEnvVars }} + {{- range . }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- end }} + {{- if $sh.telemetry.enabled }} + # OTEL endpoint is overridden per-pod via the node host IP (DaemonSet + # collector pattern). Gated on the CHART-level streamingHub.telemetry.enabled. + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: "$(HOST_IP):4317" + {{- end }} + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: {{ $sh.livenessProbe.initialDelaySeconds | default 15 }} + periodSeconds: {{ $sh.livenessProbe.periodSeconds | default 20 }} + timeoutSeconds: {{ $sh.livenessProbe.timeoutSeconds | default 5 }} + successThreshold: {{ $sh.livenessProbe.successThreshold | default 1 }} + failureThreshold: {{ $sh.livenessProbe.failureThreshold | default 3 }} + readinessProbe: + httpGet: + path: /readyz + port: http + initialDelaySeconds: {{ $sh.readinessProbe.initialDelaySeconds | default 10 }} + periodSeconds: {{ $sh.readinessProbe.periodSeconds | default 10 }} + timeoutSeconds: {{ $sh.readinessProbe.timeoutSeconds | default 5 }} + successThreshold: {{ $sh.readinessProbe.successThreshold | default 1 }} + failureThreshold: {{ $sh.readinessProbe.failureThreshold | default 3 }} + resources: + {{- toYaml $cfg.resources | nindent 12 }} + {{- with $cfg.nodeSelector | default $sh.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $cfg.affinity | default $sh.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $cfg.tolerations | default $sh.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end -}} + + +{{/* +============================================================================= +streaming-hub.service — the shared, component-parameterized ClusterIP Service. +Input: dict { root, component }. One Service per active role, selecting only +that role's pods via componentSelectorLabels. +============================================================================= +*/}} +{{- define "streaming-hub.service" -}} +{{- $ := .root -}} +{{- $component := .component -}} +{{- $sh := $.Values.streamingHub -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "streaming-hub.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" $ }} + labels: + {{- include "streaming-hub.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $sh.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ $sh.service.type }} + ports: + - port: {{ $sh.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "streaming-hub.componentSelectorLabels" (dict "context" $ "component" $component) | nindent 4 }} +{{- end -}} + + +{{/* +============================================================================= +streaming-hub.hpa — the shared, component-parameterized HPA (autoscaling/v2). +Input: dict { root, component }. Emitted only when that role's +autoscaling.enabled. CONNECTION-BUDGET HAZARD: maxReplicas multiplies the +Postgres connection draw — Σ(maxReplicas × poolMaxOpenConns) ≤ max_connections. +============================================================================= +*/}} +{{- define "streaming-hub.hpa" -}} +{{- $ := .root -}} +{{- $component := .component -}} +{{- $cfg := index $.Values.streamingHub $component -}} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "streaming-hub.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" $ }} + labels: + {{- include "streaming-hub.labels" (dict "context" $ "component" $component) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "streaming-hub.componentFullname" (dict "context" $ "component" $component) }} + minReplicas: {{ $cfg.autoscaling.minReplicas }} + maxReplicas: {{ $cfg.autoscaling.maxReplicas }} + metrics: + {{- if $cfg.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ $cfg.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if $cfg.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ $cfg.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end -}} + + +{{/* +============================================================================= +streaming-hub.pdb — the shared, component-parameterized PDB (policy/v1). +Input: dict { root, component }. Emitted only when that role's pdb.enabled. +maxUnavailable wins over minAvailable when both are set (mirrors the template). +============================================================================= +*/}} +{{- define "streaming-hub.pdb" -}} +{{- $ := .root -}} +{{- $component := .component -}} +{{- $cfg := index $.Values.streamingHub $component -}} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "streaming-hub.componentFullname" (dict "context" $ "component" $component) }} + namespace: {{ include "global.namespace" $ }} + labels: + {{- include "streaming-hub.labels" (dict "context" $ "component" $component) | nindent 4 }} + {{- with $cfg.pdb.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and (hasKey $cfg.pdb "maxUnavailable") (ne $cfg.pdb.maxUnavailable nil) }} + maxUnavailable: {{ $cfg.pdb.maxUnavailable }} + {{- else }} + minAvailable: {{ $cfg.pdb.minAvailable | default 1 }} + {{- end }} + selector: + matchLabels: + {{- include "streaming-hub.componentSelectorLabels" (dict "context" $ "component" $component) | nindent 6 }} +{{- end -}} diff --git a/charts/streaming-hub/templates/_helpers.tpl b/charts/streaming-hub/templates/_helpers.tpl new file mode 100644 index 000000000..6d497d8c1 --- /dev/null +++ b/charts/streaming-hub/templates/_helpers.tpl @@ -0,0 +1,124 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "streaming-hub.name" -}} +{{- default "streaming-hub" .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +Truncated at 63 chars because some Kubernetes name fields are limited to this +(by the DNS naming spec). When fullnameOverride is set it wins verbatim. +*/}} +{{- define "streaming-hub.fullname" -}} +{{- default (include "streaming-hub.name" .) .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "streaming-hub.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Resolve the image tag, falling back to the chart appVersion when image.tag is "". +*/}} +{{- define "streaming-hub.defaultTag" -}} +{{- default .Chart.AppVersion .Values.streamingHub.image.tag }} +{{- end -}} + +{{/* +Return a valid version label value (k8s label charset). +*/}} +{{- define "streaming-hub.versionLabelValue" -}} +{{ regexReplaceAll "[^-A-Za-z0-9_.]" (include "streaming-hub.defaultTag" .) "-" | trunc 63 | trimAll "-" | trimAll "_" | trimAll "." | quote }} +{{- end -}} + +{{/* +Component fully-qualified name: -. +Input: dict { context, component }. Truncated to 63 chars. +Used by every per-role Deployment/Service/HPA/PDB so the three role variants +never collide on a name. +*/}} +{{- define "streaming-hub.componentFullname" -}} +{{- $fullname := include "streaming-hub.fullname" .context -}} +{{- printf "%s-%s" $fullname .component | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Base selector labels (no component). +Input: dict { context }. +*/}} +{{- define "streaming-hub.selectorLabels" -}} +app.kubernetes.io/name: {{ include "streaming-hub.name" .context }} +app.kubernetes.io/instance: {{ .context.Release.Name }} +{{- end }} + +{{/* +Component-aware selector labels: base labels + app.kubernetes.io/component. +Input: dict { context, component }. The component label is the load-bearing +discriminator that keeps split-mode (ingest/delivery) Deployments from sharing +one ReplicaSet selector. +*/}} +{{- define "streaming-hub.componentSelectorLabels" -}} +{{ include "streaming-hub.selectorLabels" (dict "context" .context) }} +app.kubernetes.io/component: {{ .component }} +{{- end }} + +{{/* +Component-aware common labels (selector labels + chart/version/managed-by/part-of). +Input: dict { context, component }. +*/}} +{{- define "streaming-hub.labels" -}} +helm.sh/chart: {{ include "streaming-hub.chart" .context }} +{{ include "streaming-hub.componentSelectorLabels" (dict "context" .context "component" .component) }} +app.kubernetes.io/version: {{ include "streaming-hub.versionLabelValue" .context }} +app.kubernetes.io/managed-by: {{ .context.Release.Service }} +app.kubernetes.io/part-of: streaming-hub +{{- end }} + +{{/* +Create the name of the service account to use. +*/}} +{{- define "streaming-hub.serviceAccountName" -}} +{{- if .Values.streamingHub.serviceAccount.create }} +{{- default (include "streaming-hub.fullname" .) .Values.streamingHub.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.streamingHub.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Expand the namespace of the release. +Allows overriding it for multi-namespace deployments in combined charts. +*/}} +{{- define "global.namespace" -}} +{{- default .Release.Namespace .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} +{{- end }} + +{{/* +Resolve the shared Secret name a Deployment should reference: +existingSecretName when useExistingSecret, otherwise the shared chart Secret. +Input: root context (.). +*/}} +{{- define "streaming-hub.secretName" -}} +{{- if .Values.streamingHub.useExistingSecret -}} +{{- required "streamingHub.existingSecretName is required when streamingHub.useExistingSecret=true" .Values.streamingHub.existingSecretName -}} +{{- else -}} +{{- include "streaming-hub.fullname" . -}} +{{- end -}} +{{- end -}} + +{{/* +streaming-hub.migrationDSN — STREAMING_HUB_POSTGRES_DSN for the migration-only +Secret hook. migration-secret.yaml renders only on the chart-managed credential +path (NOT migrations.useExistingSecret), and runs as a PreSync hook BEFORE the +normal app Secret exists, so the operator MUST supply the DSN here. Kept as a +named gate helper (not an inline `required`) to mirror the bank-transfer pattern. +Input: root context (.). +*/}} +{{- define "streaming-hub.migrationDSN" -}} +{{- $secrets := get (.Values.streamingHub | default dict) "secrets" | default dict -}} +{{- required "streamingHub.secrets.STREAMING_HUB_POSTGRES_DSN is required when migrations run with a chart-managed Secret (migrations.useExistingSecret=false)" (get $secrets "STREAMING_HUB_POSTGRES_DSN") -}} +{{- end -}} diff --git a/charts/streaming-hub/templates/all/deployment.yaml b/charts/streaming-hub/templates/all/deployment.yaml new file mode 100644 index 000000000..af8c835d3 --- /dev/null +++ b/charts/streaming-hub/templates/all/deployment.yaml @@ -0,0 +1,3 @@ +{{- if eq .Values.streamingHub.mode "all" }} +{{- include "streaming-hub.deployment" (dict "root" . "component" "all") }} +{{- end }} diff --git a/charts/streaming-hub/templates/all/hpa.yaml b/charts/streaming-hub/templates/all/hpa.yaml new file mode 100644 index 000000000..c9c16ffc4 --- /dev/null +++ b/charts/streaming-hub/templates/all/hpa.yaml @@ -0,0 +1,3 @@ +{{- if and (eq .Values.streamingHub.mode "all") .Values.streamingHub.all.autoscaling.enabled }} +{{- include "streaming-hub.hpa" (dict "root" . "component" "all") }} +{{- end }} diff --git a/charts/streaming-hub/templates/all/pdb.yaml b/charts/streaming-hub/templates/all/pdb.yaml new file mode 100644 index 000000000..31a5a253f --- /dev/null +++ b/charts/streaming-hub/templates/all/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and (eq .Values.streamingHub.mode "all") .Values.streamingHub.all.pdb.enabled }} +{{- include "streaming-hub.pdb" (dict "root" . "component" "all") }} +{{- end }} diff --git a/charts/streaming-hub/templates/all/service.yaml b/charts/streaming-hub/templates/all/service.yaml new file mode 100644 index 000000000..a510b3bf5 --- /dev/null +++ b/charts/streaming-hub/templates/all/service.yaml @@ -0,0 +1,3 @@ +{{- if eq .Values.streamingHub.mode "all" }} +{{- include "streaming-hub.service" (dict "root" . "component" "all") }} +{{- end }} diff --git a/charts/streaming-hub/templates/bootstrap-postgres.yaml b/charts/streaming-hub/templates/bootstrap-postgres.yaml new file mode 100644 index 000000000..fb906f6b4 --- /dev/null +++ b/charts/streaming-hub/templates/bootstrap-postgres.yaml @@ -0,0 +1,165 @@ +{{- if .Values.global.externalPostgresDefinitions.enabled }} +{{/* +Bootstrap Job for an external/shared PostgreSQL host: creates the hub's ONE +database + role and grants privileges. streaming-hub owns a SINGLE database with +a tenant_id COLUMN (NOT per-tenant DB), so exactly one DB/role is provisioned. + +Default OFF. Whether dev-st uses this Job or a pre-provisioned managed PG is a +Phase 3 decision — the chart supports both. The created DB/role name is +configurable (defaults: db "streaming-hub", role "streaming-hub") and must match +the host/dbname/user encoded in STREAMING_HUB_POSTGRES_DSN. +*/}} +{{- $defs := .Values.global.externalPostgresDefinitions -}} +{{- $dbName := $defs.database | default "streaming-hub" -}} +{{- $roleName := $defs.role | default "streaming-hub" -}} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "streaming-hub.fullname" . }}-bootstrap-postgres + namespace: {{ include "global.namespace" . }} + labels: + {{- include "streaming-hub.labels" (dict "context" . "component" "bootstrap") | nindent 4 }} + annotations: + # PreSync hook at the FRONT of the chain (weight -10 / sync-wave -10): creates + # the hub's role+db BEFORE the migration-secret (-5), the migrations Job (-1), + # and the app Deployment (main Sync). Idempotent — it creates the role/db only + # when absent (see the script), so re-running on every sync is safe. + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + "argocd.argoproj.io/hook": PreSync + "argocd.argoproj.io/sync-wave": "-10" + "argocd.argoproj.io/hook-delete-policy": BeforeHookCreation,HookSucceeded +spec: + ttlSecondsAfterFinished: 300 + completions: 1 + parallelism: 1 + backoffLimit: 3 + template: + spec: + restartPolicy: Never + initContainers: + - name: wait-for-dependencies + image: busybox:1.37 + env: + - name: DB_HOST + value: {{ $defs.connection.host | quote }} + - name: DB_PORT + value: {{ $defs.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 is not ready yet, waiting... (${ELAPSED}s/${TIMEOUT}s)"; + sleep 5; + ELAPSED=$((ELAPSED + 5)); + done; + echo "$DB_HOST:$DB_PORT is ready!"; + containers: + - name: psql + image: postgres:17 + env: + - name: DB_HOST + value: {{ $defs.connection.host | quote }} + - name: DB_PORT + value: {{ $defs.connection.port | quote }} + - name: DB_NAME + value: {{ $dbName | quote }} + - name: DB_ROLE + value: {{ $roleName | quote }} + - name: DB_USER_ADMIN + {{- if $defs.postgresAdminLogin.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ $defs.postgresAdminLogin.useExistingSecret.name | quote }} + key: DB_USER_ADMIN + {{- else }} + value: {{ $defs.postgresAdminLogin.username | quote }} + {{- end }} + - name: DB_ADMIN_PASSWORD + {{- if $defs.postgresAdminLogin.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ $defs.postgresAdminLogin.useExistingSecret.name | quote }} + key: DB_ADMIN_PASSWORD + {{- else }} + value: {{ $defs.postgresAdminLogin.password | quote }} + {{- end }} + - name: DB_PASSWORD_HUB + {{- if $defs.hubCredentials.useExistingSecret.name }} + valueFrom: + secretKeyRef: + name: {{ $defs.hubCredentials.useExistingSecret.name | quote }} + key: DB_PASSWORD_HUB + {{- else }} + value: {{ $defs.hubCredentials.password | quote }} + {{- end }} + - name: DB_DATABASE + value: postgres + command: + - /bin/sh + - -c + - | + set -euo pipefail + # Escape single quotes so values embedded in SQL string literals can't + # break the statement (a Vault-generated password legitimately may + # contain a ' — that would otherwise abort CREATE ROLE). + sql_lit() { printf "%s" "$1" | sed "s/'/''/g"; } + DB_NAME_LIT="$(sql_lit "$DB_NAME")" + DB_ROLE_LIT="$(sql_lit "$DB_ROLE")" + DB_PASSWORD_HUB_LIT="$(sql_lit "$DB_PASSWORD_HUB")" + echo "=== streaming-hub PostgreSQL Bootstrap ===" + echo "Host: $DB_HOST:$DB_PORT Database: $DB_NAME Role: $DB_ROLE" + 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='${DB_NAME_LIT}'" | 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='${DB_ROLE_LIT}'" | grep -q 1; then + ROLE_EXISTS=1 + fi + + if [ "$DB_EXISTS" = "1" ] && [ "$ROLE_EXISTS" = "1" ]; then + echo "PostgreSQL bootstrap already complete (database '$DB_NAME' and role '$DB_ROLE' exist). Skipping creation." + else + # Create role if not exists + if [ "$ROLE_EXISTS" = "1" ]; then + echo "Role '$DB_ROLE' already exists. Skipping creation." + else + echo "Creating role '$DB_ROLE'..." + 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 ROLE \"$DB_ROLE\" LOGIN PASSWORD '${DB_PASSWORD_HUB_LIT}'" + fi + + # Create database if not exists + if [ "$DB_EXISTS" = "1" ]; then + echo "Database '$DB_NAME' already exists. Skipping creation." + else + echo "Creating database '$DB_NAME'..." + 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 \"$DB_NAME\" OWNER \"$DB_ROLE\"" + fi + fi + + # Privileges (safe to run repeatedly) + 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 \"$DB_NAME\" TO \"$DB_ROLE\"" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_NAME" -c "GRANT ALL ON SCHEMA public TO \"$DB_ROLE\"" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_NAME" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"$DB_ROLE\"" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_NAME" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"$DB_ROLE\"" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_NAME" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"$DB_ROLE\"" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_NAME" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"$DB_ROLE\"" + + echo "" + echo "=== streaming-hub PostgreSQL Bootstrap completed successfully ===" +{{- end }} diff --git a/charts/streaming-hub/templates/configmap.yaml b/charts/streaming-hub/templates/configmap.yaml new file mode 100644 index 000000000..20b9c86ed --- /dev/null +++ b/charts/streaming-hub/templates/configmap.yaml @@ -0,0 +1,23 @@ +{{/* +ONE shared ConfigMap — role-INVARIANT, non-sensitive env. Identical across all +roles, so it is NOT duplicated per role (DRY). The role-specific vars +(STREAMING_HUB_ROLE, the Postgres pool sizes) are deliberately ABSENT here — +they are injected as explicit per-Deployment env in templates//deployment.yaml +(see templates/_deployment.tpl), which wins over this envFrom layer. + +Ranged over .Values.streamingHub.common.configmap so operators can add/override +keys without editing this template. Every key in that map MUST exist verbatim in +streaming-hub/.env.reference (the env-parity cross-check enforces this); OTEL_* +keys are the documented exception (lib-observability vars, not LoadConfig vars). +*/}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "streaming-hub.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "streaming-hub.labels" (dict "context" . "component" "all") | nindent 4 }} +data: + {{- range $key, $value := .Values.streamingHub.common.configmap }} + {{ $key }}: {{ $value | quote }} + {{- end }} diff --git a/charts/streaming-hub/templates/delivery/deployment.yaml b/charts/streaming-hub/templates/delivery/deployment.yaml new file mode 100644 index 000000000..537af7773 --- /dev/null +++ b/charts/streaming-hub/templates/delivery/deployment.yaml @@ -0,0 +1,3 @@ +{{- if eq .Values.streamingHub.mode "split" }} +{{- include "streaming-hub.deployment" (dict "root" . "component" "delivery") }} +{{- end }} diff --git a/charts/streaming-hub/templates/delivery/hpa.yaml b/charts/streaming-hub/templates/delivery/hpa.yaml new file mode 100644 index 000000000..36465e604 --- /dev/null +++ b/charts/streaming-hub/templates/delivery/hpa.yaml @@ -0,0 +1,3 @@ +{{- if and (eq .Values.streamingHub.mode "split") .Values.streamingHub.delivery.autoscaling.enabled }} +{{- include "streaming-hub.hpa" (dict "root" . "component" "delivery") }} +{{- end }} diff --git a/charts/streaming-hub/templates/delivery/pdb.yaml b/charts/streaming-hub/templates/delivery/pdb.yaml new file mode 100644 index 000000000..3edd4c638 --- /dev/null +++ b/charts/streaming-hub/templates/delivery/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and (eq .Values.streamingHub.mode "split") .Values.streamingHub.delivery.pdb.enabled }} +{{- include "streaming-hub.pdb" (dict "root" . "component" "delivery") }} +{{- end }} diff --git a/charts/streaming-hub/templates/delivery/service.yaml b/charts/streaming-hub/templates/delivery/service.yaml new file mode 100644 index 000000000..2e2748864 --- /dev/null +++ b/charts/streaming-hub/templates/delivery/service.yaml @@ -0,0 +1,3 @@ +{{- if eq .Values.streamingHub.mode "split" }} +{{- include "streaming-hub.service" (dict "root" . "component" "delivery") }} +{{- end }} diff --git a/charts/streaming-hub/templates/ingest/deployment.yaml b/charts/streaming-hub/templates/ingest/deployment.yaml new file mode 100644 index 000000000..de840b891 --- /dev/null +++ b/charts/streaming-hub/templates/ingest/deployment.yaml @@ -0,0 +1,3 @@ +{{- if eq .Values.streamingHub.mode "split" }} +{{- include "streaming-hub.deployment" (dict "root" . "component" "ingest") }} +{{- end }} diff --git a/charts/streaming-hub/templates/ingest/hpa.yaml b/charts/streaming-hub/templates/ingest/hpa.yaml new file mode 100644 index 000000000..19f26aa25 --- /dev/null +++ b/charts/streaming-hub/templates/ingest/hpa.yaml @@ -0,0 +1,3 @@ +{{- if and (eq .Values.streamingHub.mode "split") .Values.streamingHub.ingest.autoscaling.enabled }} +{{- include "streaming-hub.hpa" (dict "root" . "component" "ingest") }} +{{- end }} diff --git a/charts/streaming-hub/templates/ingest/pdb.yaml b/charts/streaming-hub/templates/ingest/pdb.yaml new file mode 100644 index 000000000..0bb740ef6 --- /dev/null +++ b/charts/streaming-hub/templates/ingest/pdb.yaml @@ -0,0 +1,3 @@ +{{- if and (eq .Values.streamingHub.mode "split") .Values.streamingHub.ingest.pdb.enabled }} +{{- include "streaming-hub.pdb" (dict "root" . "component" "ingest") }} +{{- end }} diff --git a/charts/streaming-hub/templates/ingest/service.yaml b/charts/streaming-hub/templates/ingest/service.yaml new file mode 100644 index 000000000..995a93466 --- /dev/null +++ b/charts/streaming-hub/templates/ingest/service.yaml @@ -0,0 +1,3 @@ +{{- if eq .Values.streamingHub.mode "split" }} +{{- include "streaming-hub.service" (dict "root" . "component" "ingest") }} +{{- end }} diff --git a/charts/streaming-hub/templates/ingress.yaml b/charts/streaming-hub/templates/ingress.yaml new file mode 100644 index 000000000..964287038 --- /dev/null +++ b/charts/streaming-hub/templates/ingress.yaml @@ -0,0 +1,52 @@ +{{- if .Values.streamingHub.ingress.enabled }} +{{/* +Control-plane API ingress (default OFF; opt-in per env in gitops). +Backend Service is the active role's component Service: + - mode=all -> the `all` Service. + - mode=split -> the `ingest` Service (the control API is served on EVERY role, + so ingest is an arbitrary-but-stable default; override the + backend by editing this template if delivery is preferred). +*/}} +{{- $component := ternary "all" "ingest" (eq .Values.streamingHub.mode "all") -}} +{{- $svcName := include "streaming-hub.componentFullname" (dict "context" . "component" $component) -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "streaming-hub.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "streaming-hub.labels" (dict "context" . "component" $component) | nindent 4 }} + {{- with .Values.streamingHub.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.streamingHub.ingress.className }} + ingressClassName: {{ .Values.streamingHub.ingress.className }} + {{- end }} + {{- if .Values.streamingHub.ingress.tls }} + tls: + {{- range .Values.streamingHub.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.streamingHub.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ $svcName }} + port: + number: {{ $.Values.streamingHub.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/streaming-hub/templates/migration-secret.yaml b/charts/streaming-hub/templates/migration-secret.yaml new file mode 100644 index 000000000..d6a78ae7e --- /dev/null +++ b/charts/streaming-hub/templates/migration-secret.yaml @@ -0,0 +1,48 @@ +{{- $streamingHub := .Values.streamingHub | default dict }} +{{- $migrations := get $streamingHub "migrations" | default dict }} +{{- $migrationsEnabled := eq (lower (toString (get $migrations "enabled" | default false))) "true" }} +{{- $migUseExisting := eq (lower (toString (get $migrations "useExistingSecret" | default false))) "true" }} +{{- /* +Minimal, migration-only Secret carrying ONLY STREAMING_HUB_POSTGRES_DSN. + +Rendered ONLY when migrations are enabled AND a chart-managed credential is used +(migrations.useExistingSecret=false). In that case the migration Job runs as a +pre-install/pre-upgrade hook (weight -1) BEFORE the normal application Secret +(secrets.yaml, a normal main-sync resource) exists, so it needs its own credential +available early. This Secret is a PreSync hook at weight -5 / sync-wave -5 — after +bootstrap-postgres (-10), before the migration Job (-1). + +It holds ONLY the DSN; the full application Secret stays a NORMAL (non-hook) +resource so its sensitive runtime keys are never left behind as orphaned hooks on +uninstall. +*/}} +{{- if and $migrationsEnabled (not $migUseExisting) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ printf "%s-migrations" (include "streaming-hub.fullname" .) | trunc 63 | trimSuffix "-" }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "streaming-hub.labels" (dict "context" . "component" "migrations") | nindent 4 }} + annotations: + # PreSync hook (weight -5 / sync-wave -5) so it exists before the migration Job + # (weight -1) and after bootstrap-postgres (weight -10). Holds ONLY the DSN; see + # the comment above for why the full application Secret is intentionally NOT a hook. + # + # Delete policy is before-hook-creation ONLY — deliberately NOT hook-succeeded. + # This Secret is consumed by a LATER hook (the migrations Job at weight -1); both + # Helm and ArgoCD treat a Secret as "succeeded" the moment it applies, so a + # hook-succeeded policy would delete the DSN at the end of wave -5, BEFORE the + # Job at wave -1 can read it — reintroducing the migration failure this exists to + # fix. The trade-off: the Secret lingers post-run, but it only duplicates the DSN + # already held permanently by the app's main Secret, so it adds no new exposure. + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation + "argocd.argoproj.io/hook": PreSync + "argocd.argoproj.io/sync-wave": "-5" + "argocd.argoproj.io/hook-delete-policy": BeforeHookCreation +type: Opaque +stringData: + STREAMING_HUB_POSTGRES_DSN: {{ include "streaming-hub.migrationDSN" . | quote }} +{{- end }} diff --git a/charts/streaming-hub/templates/migrations.yaml b/charts/streaming-hub/templates/migrations.yaml new file mode 100644 index 000000000..2efe5e237 --- /dev/null +++ b/charts/streaming-hub/templates/migrations.yaml @@ -0,0 +1,124 @@ +{{- $streamingHub := .Values.streamingHub | default dict }} +{{- $migrations := get $streamingHub "migrations" | 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 }} +{{- /* +OUT-OF-BAND schema migration Job. The hub never migrates itself; this Job applies +the SQL migrations BEFORE the app rolls out. PreSync hook at weight -1 — after +bootstrap-postgres (-10, creates role+db) and migration-secret (-5, carries the +DSN), before the app Deployment (main Sync). Runs ONCE regardless of +streamingHub.mode (all | split) — it is not per-role. + +The runner is the stock migrate/migrate toolchain baked with the hub's migrations/ +dir; its entrypoint runs `migrate -path=/migrations -database="$STREAMING_HUB_POSTGRES_DSN" up`, +so the ONLY required env is STREAMING_HUB_POSTGRES_DSN (secretKeyRef). +*/}} +{{- /* Secret resolution — STREAMING_HUB_POSTGRES_DSN source. */}} +{{- $migUseExisting := eq (lower (toString (get $migrations "useExistingSecret" | default false))) "true" }} +{{- $dsnSecretName := "" }} +{{- if $migUseExisting }} +{{- $dsnSecretName = required "streamingHub.migrations.existingSecretName is required when streamingHub.migrations.useExistingSecret=true" (get $migrations "existingSecretName") }} +{{- else }} +{{- $dsnSecretName = printf "%s-migrations" (include "streaming-hub.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- $namespace := include "global.namespace" . }} +{{- /* Image resolution — explicit digest > explicit tag > app tag (streamingHub.image.tag) > chart appVersion. */}} +{{- $migrationTag := get $migrationImage "tag" | default "" }} +{{- $migrationDigest := get $migrationImage "digest" | default "" }} +{{- if and (empty $migrationTag) (empty $migrationDigest) }} +{{- $migrationTag = $streamingHub.image.tag | default .Chart.AppVersion }} +{{- end }} +{{- $migrationRepository := get $migrationImage "repository" | default "ghcr.io/lerianstudio/streaming-hub-migrations" }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ printf "%s-migrations" (include "streaming-hub.fullname" .) | trunc 63 | trimSuffix "-" }} + namespace: {{ $namespace }} + labels: + {{- include "streaming-hub.labels" (dict "context" . "component" "migrations") | nindent 4 }} + annotations: + # External Postgres host (bootstrap-postgres provisions the role+db at PreSync + # weight -10) — run BEFORE the app rolls out (schema-first). + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-1" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + "argocd.argoproj.io/hook": PreSync + "argocd.argoproj.io/sync-wave": "-1" + "argocd.argoproj.io/hook-delete-policy": BeforeHookCreation,HookSucceeded + {{- /* + Custom annotations are merged AFTER the fixed set, but the hook/wave keys are + filtered out so a user annotation can NEVER knock this Job out of PreSync or + shift its wave — that would reintroduce the empty-schema rollout this Job + exists to prevent. The fixed hook chain always wins. + */}} + {{- range $k, $v := (get $migrations "annotations") }} + {{- if or (hasPrefix "helm.sh/hook" $k) (hasPrefix "argocd.argoproj.io/hook" $k) (eq $k "argocd.argoproj.io/sync-wave") }} + {{- /* reserved hook/wave key — ignored to preserve the schema-first ordering */}} + {{- else }} + {{ $k | quote }}: {{ $v | quote }} + {{- end }} + {{- end }} +spec: + backoffLimit: {{ get $migrations "backoffLimit" | default 3 }} + activeDeadlineSeconds: {{ get $migrations "activeDeadlineSeconds" | default 600 }} + ttlSecondsAfterFinished: {{ get $migrations "ttlSecondsAfterFinished" | default 600 }} + template: + metadata: + labels: + {{- include "streaming-hub.labels" (dict "context" . "component" "migrations") | nindent 8 }} + {{- with (get $migrations "podAnnotations") }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + restartPolicy: Never + automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault + {{- with (get $streamingHub "imagePullSecrets") }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: migrations + {{- if $migrationDigest }} + image: "{{ $migrationRepository }}@{{ $migrationDigest }}" + {{- else }} + image: "{{ $migrationRepository }}:{{ $migrationTag }}" + {{- end }} + imagePullPolicy: {{ get $migrationImage "pullPolicy" | default "IfNotPresent" }} + env: + # The baked migrate/migrate entrypoint runs: + # migrate -path=/migrations -database="$STREAMING_HUB_POSTGRES_DSN" up + - name: STREAMING_HUB_POSTGRES_DSN + valueFrom: + secretKeyRef: + name: {{ $dsnSecretName }} + key: STREAMING_HUB_POSTGRES_DSN + 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 $streamingHub "nodeSelector") }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with (get $streamingHub "affinity") }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with (get $streamingHub "tolerations") }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/streaming-hub/templates/secrets.yaml b/charts/streaming-hub/templates/secrets.yaml new file mode 100644 index 000000000..5796648ce --- /dev/null +++ b/charts/streaming-hub/templates/secrets.yaml @@ -0,0 +1,28 @@ +{{/* +ONE shared Opaque Secret — role-INVARIANT sensitive env. Identical across all +roles, so it is NOT duplicated per role. + +Rendered ONLY when NOT useExistingSecret. When useExistingSecret=true (the +gitops/Vault path) this emits nothing and the Deployments reference +existingSecretName instead — that branch is exercised by the chart and MUST stay +working. + +Empty values are SKIPPED so unset SaaS/dev keys never ship as blank Secret +entries. Values are base64-encoded into `data` (b64enc). +*/}} +{{- if not .Values.streamingHub.useExistingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "streaming-hub.fullname" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "streaming-hub.labels" (dict "context" . "component" "all") | nindent 4 }} +type: Opaque +data: + {{- range $key, $value := .Values.streamingHub.secrets }} + {{- if $value }} + {{ $key }}: {{ $value | toString | b64enc | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/streaming-hub/templates/serviceaccount.yaml b/charts/streaming-hub/templates/serviceaccount.yaml new file mode 100644 index 000000000..cf4b7e79f --- /dev/null +++ b/charts/streaming-hub/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.streamingHub.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "streaming-hub.serviceAccountName" . }} + namespace: {{ include "global.namespace" . }} + labels: + {{- include "streaming-hub.labels" (dict "context" . "component" "all") | nindent 4 }} + {{- with .Values.streamingHub.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/streaming-hub/values-template.yaml b/charts/streaming-hub/values-template.yaml new file mode 100644 index 000000000..ea5a27d9f --- /dev/null +++ b/charts/streaming-hub/values-template.yaml @@ -0,0 +1,50 @@ +# streaming-hub — operator override template. +# +# Copy this file, fill the blanks per environment, and pass it with `-f`. +# It documents only the keys an operator typically sets; every other key keeps +# the default from values.yaml. See README "Chart Contract" for the full +# secret/dependency/override contract. Secrets are blank here — provide them +# inline or via `useExistingSecret` + `existingSecretName`. + +streamingHub: + # Topology: `all` (one Deployment) or `split` (ingest + delivery). Never both + # — the roles compete for one Kafka consumer group. See README. + mode: all + + image: + tag: "" # defaults to the chart appVersion + + common: + configmap: + STREAMING_HUB_ENV: production # local | staging | production + STREAMING_HUB_LOG_LEVEL: info + STREAMING_HUB_KAFKA_BROKERS: "" # e.g. redpanda.dev-st.lerian.net:9092 + STREAMING_HUB_KAFKA_SCRAM_MECHANISM: "" # e.g. SCRAM-SHA-256 (empty = no SASL) + STREAMING_HUB_ALLOW_INSECURE_KAFKA: "false" + STREAMING_HUB_KEK_SOURCE: "" # env | aws-kms | ... + PLUGIN_AUTH_ENABLED: "true" + PLUGIN_AUTH_ADDRESS: "" + STREAMING_HUB_TENANT_ID: default + STREAMING_HUB_MULTI_TENANT_ENABLED: "false" + + secrets: + STREAMING_HUB_POSTGRES_DSN: "" # full Postgres DSN, credentials embedded + STREAMING_HUB_KAFKA_SCRAM_USERNAME: "" + STREAMING_HUB_KAFKA_SCRAM_PASSWORD: "" + STREAMING_HUB_KEK_REF: "" # KEK reference (KMS key id / ARN / ...) + # STREAMING_HUB_DEV_KEK: "" # dev only + # SaaS-only — required when MULTI_TENANT_ENABLED=true: + # STREAMING_HUB_TENANT_MANAGER_SERVICE_API_KEY: "" + # STREAMING_HUB_MULTI_TENANT_REDIS_PASSWORD: "" + + useExistingSecret: false + existingSecretName: "" + + # Per-role sizing. Honor Σ(replicas × poolMaxOpenConns) ≤ Postgres max_connections. + all: + replicaCount: 1 + # mode: split uses these instead: + # ingest: + # replicaCount: 2 + # delivery: + # replicaCount: 2 diff --git a/charts/streaming-hub/values.schema.json b/charts/streaming-hub/values.schema.json new file mode 100644 index 000000000..7d0c17a0e --- /dev/null +++ b/charts/streaming-hub/values.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true, + "properties": { + "nameOverride": { + "type": "string" + }, + "fullnameOverride": { + "type": "string" + }, + "namespaceOverride": { + "type": "string" + }, + "global": { + "type": "object", + "additionalProperties": true + }, + "streamingHub": { + "type": "object", + "additionalProperties": true, + "properties": { + "mode": { + "type": "string", + "enum": ["all", "split"], + "description": "Topology switch. 'all' = one Deployment (role=all); 'split' = ingest + delivery. NEVER run both against one Kafka cluster (double-consume)." + }, + "common": { + "type": "object", + "additionalProperties": true, + "properties": { + "configmap": { + "type": "object" + } + } + }, + "secrets": { + "type": "object" + } + } + } + } +} diff --git a/charts/streaming-hub/values.yaml b/charts/streaming-hub/values.yaml new file mode 100644 index 000000000..6921ebd0f --- /dev/null +++ b/charts/streaming-hub/values.yaml @@ -0,0 +1,436 @@ +# Default values for streaming-hub. +# This is a YAML-formatted file. +# +# streaming-hub consumes lib-streaming CloudEvents from Kafka/Redpanda and fans +# them out to per-tenant subscribers. ONE image, ONE binary, RUNTIME-selected +# into a role via STREAMING_HUB_ROLE. This chart exposes that via a single +# `mode` switch (see streamingHub.mode below). +# +# External infra (provisioned OUTSIDE this chart): Kafka/Redpanda and PostgreSQL. +# There are NO database/broker/OTEL subcharts (deliberate — see Chart.yaml). + +# -- Override the chart name component of resource names. +nameOverride: "" +# -- Override the fully-qualified release name (wins verbatim). +fullnameOverride: "" +# -- Override the namespace (defaults to .Release.Namespace). +namespaceOverride: "" +global: + # -- Bootstrap Job for an external/shared PostgreSQL: creates the hub's ONE + # database + role and grants privileges. The hub owns a SINGLE database with + # a tenant_id column (NOT per-tenant DB). Default OFF — dev-st may instead + # point STREAMING_HUB_POSTGRES_DSN at a pre-provisioned managed host. + externalPostgresDefinitions: + # -- Enable or disable the PostgreSQL bootstrap Job. + enabled: false + # -- Name of the database the Job creates (must match the DSN dbname). + database: "streaming-hub" + # -- Name of the login role the Job creates (must match the DSN user). + role: "streaming-hub" + # -- PostgreSQL connection settings for the bootstrap Job. + connection: + # -- PostgreSQL host. + host: "streaming-hub-postgresql" + # -- PostgreSQL port. + port: "5432" + # -- Admin credentials used by the Job to create the DB/role. + postgresAdminLogin: + useExistingSecret: + # -- Existing secret with 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 hub role created by the Job. + hubCredentials: + useExistingSecret: + # -- Existing secret with DB_PASSWORD_HUB key. + name: "" + # -- Password for the hub role (ignored if useExistingSecret.name is set). + password: "" +# ============================================================================= +# streamingHub — the single root key for everything the hub owns. +# ============================================================================= +streamingHub: + # -- Deployment topology switch. One of: all | split. + # all (default) -> ONE Deployment with STREAMING_HUB_ROLE=all (ingest + + # delivery co-resident; the dev-st target). Byte-equivalent to the + # historical single binary. + # split -> TWO Deployments: ingest (role=ingest) and delivery + # (role=delivery), each scaled independently. + # + # !!! NEVER run both an `all` Deployment AND ingest/delivery against the same + # Kafka cluster: they join ONE consumer group and DOUBLE-CONSUME every event. + # The mode switch enforces either/or — do not work around it. !!! + # The values.schema.json constrains this to the enum ["all","split"]. + mode: all + image: + # -- Container image repository. + repository: ghcr.io/lerianstudio/streaming-hub + # -- Image pull policy. + pullPolicy: IfNotPresent + # -- Image tag. Empty falls back to Chart.appVersion via the defaultTag helper. + tag: "" + # -- Secrets for pulling the image from a private registry. + imagePullSecrets: + - name: ghcr-credential + # -- Number of old ReplicaSets to retain for rollback. + revisionHistoryLimit: 10 + # -- Annotations applied to every Deployment resource. + annotations: {} + # -- Annotations applied to every pod. + podAnnotations: {} + # -- Deployment update strategy (shared by all roles). + deploymentStrategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + # -- Pod-level security context. Empty by default (the hub needs no fsGroup). + podSecurityContext: {} + # -- Container-level security context (distroless:nonroot, uid/gid 65532). + securityContext: + # -- Group ID for the process inside the container. + runAsGroup: 65532 + # -- User ID for the process inside the container. + runAsUser: 65532 + # -- Never run as root. + runAsNonRoot: true + capabilities: + drop: + - ALL + # -- Read-only root filesystem (the image carries no writable state). + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + seccompProfile: + type: RuntimeDefault + service: + # -- Service type. MUST be ClusterIP (Lerian convention; Ingress fronts external). + type: ClusterIP + # -- Control-plane HTTP port (the hub listens on :8080; see Dockerfile EXPOSE). + port: 8080 + # -- Annotations for every Service. + annotations: {} + ingress: + # -- Enable or disable the control-plane Ingress (opt-in per env in gitops). + enabled: false + # -- Ingress class name. + className: "nginx" + # -- Additional ingress annotations. + annotations: {} + # -- Hosts (default empty; the control API is served on every role). + hosts: [] + # - host: "streaming-hub.lerian.net" + # paths: + # - path: / + # pathType: Prefix + # -- TLS configuration. + tls: [] + # - secretName: streaming-hub-tls + # hosts: + # - streaming-hub.lerian.net + serviceAccount: + # -- Whether a ServiceAccount is created. + create: true + # -- Annotations for the ServiceAccount (e.g. AWS IRSA role-arn). + annotations: {} + # -- ServiceAccount name. Empty defaults to the chart fullname. + name: "" + # -- Mount the SA API token into pods. Default false — the hub makes no + # in-cluster Kubernetes API calls. (IRSA's projected token is injected by + # the EKS webhook independently of this, so it stays functional.) + automountServiceAccountToken: false + # -- Graceful-shutdown window. Defaults to the hub's derived SIGTERM drain + # ceiling (80s) at STREAMING_HUB_SHUTDOWN_TIMEOUT=30s + + # STREAMING_HUB_PRE_STOP_DRAIN_TIMEOUT=5s: 30s HTTP + 5s consumer-commit + + # 30s dispatcher + 10s slack = 75s, + 5s pre-stop = 80s (see .env.reference). + # If you tune those knobs up, recompute and keep this AT OR ABOVE the new + # ceiling so the orchestrator never SIGKILLs a still-draining replica. NO + # preStop hook is used — the hub self-drains on SIGTERM (PID 1 receives it + # directly; exec-form ENTRYPOINT). + terminationGracePeriodSeconds: 80 + # -- Liveness probe tuning (GET /healthz on the http port; stays 200 during drain). + livenessProbe: + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + # -- Readiness probe tuning (GET /readyz; flips NotReady first on SIGTERM). + readinessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + # -- Shared default scheduling (per-role blocks may override). + nodeSelector: {} + tolerations: {} + affinity: {} + # -- Extra non-sensitive env vars injected on every Deployment (list of {name,value}). + extraEnvVars: [] + # =========================================================================== + # telemetry — CHART-LEVEL OTEL wiring toggle (NOT an app env var; never shipped + # to the container). When enabled, each Deployment injects HOST_IP via the + # downward API and sets OTEL_EXPORTER_OTLP_ENDPOINT=$(HOST_IP):4317 (node-local + # DaemonSet collector pattern; see templates/_deployment.tpl). The hub's own + # Prometheus exposition is governed by STREAMING_HUB_METRICS_ENABLED in the + # ConfigMap above — a separate, app-read knob. + # =========================================================================== + telemetry: + # -- Inject the per-pod OTLP endpoint override (HOST_IP downward API). + enabled: false + # =========================================================================== + # Secret sourcing. + # useExistingSecret=false -> the chart renders templates/secret.yaml (inline). + # useExistingSecret=true -> NO Secret is rendered; Deployments reference + # existingSecretName (the gitops/Vault path). + # =========================================================================== + useExistingSecret: false + existingSecretName: "" + # =========================================================================== + # common.configmap — role-INVARIANT, NON-SENSITIVE env (one shared ConfigMap). + # Every key MUST exist verbatim in streaming-hub/.env.reference, EXCEPT the + # OTEL_* keys (lib-observability vars, not read by LoadConfig). + # + # DELIBERATELY ABSENT (injected per-Deployment, see the all/ingest/delivery + # blocks below): STREAMING_HUB_ROLE, STREAMING_HUB_POSTGRES_MAX_OPEN_CONNS, + # STREAMING_HUB_POSTGRES_MAX_IDLE_CONNS. + # =========================================================================== + common: + configmap: + # --- core --- + # Deployment environment: local | staging | production. + STREAMING_HUB_ENV: "production" + STREAMING_HUB_LOG_LEVEL: "info" + STREAMING_HUB_HTTP_LISTEN_ADDR: ":8080" + STREAMING_HUB_METRICS_ENABLED: "true" + # --- kafka / redpanda (external) --- + STREAMING_HUB_KAFKA_BROKERS: "localhost:19092" + STREAMING_HUB_KAFKA_SCRAM_MECHANISM: "" + # Plaintext Kafka opt-in. MUST stay "false" for staging/production + # (rejected at boot when STREAMING_HUB_ENV=production). + STREAMING_HUB_ALLOW_INSECURE_KAFKA: "false" + # --- crypto / KEK (the material itself is a Secret) --- + # env | secretsmanager. KEK material arrives via the var named by KEK_REF. + STREAMING_HUB_KEK_SOURCE: "env" + # --- auth (lib-auth / plugin-auth JWT; PLUGIN_AUTH_ prefix, NOT STREAMING_HUB_) --- + PLUGIN_AUTH_ENABLED: "true" + PLUGIN_AUTH_ADDRESS: "http://plugin-access-manager-auth:4000" + # --- tenancy (F4) --- + # default = BYOC single-tenant. A non-default value quarantines all + # other ce-tenantid events (zero-delivery risk) — confirm the producer. + STREAMING_HUB_TENANT_ID: "default" + # SaaS roster off by default (BYOC). When "true" the SaaS-only knobs below + # become required-when-SaaS (and several SECRET counterparts in secrets:). + STREAMING_HUB_MULTI_TENANT_ENABLED: "false" + STREAMING_HUB_TENANT_MANAGER_URL: "" + STREAMING_HUB_MULTI_TENANT_ALLOW_INSECURE_TM: "false" + STREAMING_HUB_MULTI_TENANT_TM_STALENESS_THRESHOLD: "12h" + STREAMING_HUB_MULTI_TENANT_LISTENER_STALENESS_THRESHOLD: "12h" + STREAMING_HUB_MULTI_TENANT_RECONCILE_INTERVAL: "6h" + STREAMING_HUB_MULTI_TENANT_CACHE_TTL: "12h" + STREAMING_HUB_TENANT_MANAGER_CB_THRESHOLD: "0" + STREAMING_HUB_TENANT_MANAGER_CB_TIMEOUT: "30s" + STREAMING_HUB_MULTI_TENANT_REDIS_HOST: "" + STREAMING_HUB_MULTI_TENANT_REDIS_PORT: "" + STREAMING_HUB_MULTI_TENANT_REDIS_TLS: "false" + STREAMING_HUB_MULTI_TENANT_REDIS_CA_CERT: "" + # lib-commons env name (NOT STREAMING_HUB_-prefixed). Required-in-SaaS: + # staging | production. Inert under BYOC. Leave empty unless SaaS. + ENVIRONMENT_NAME: "" + # --- reconciler (topic-drift detection; dark-ship, defaults on) --- + STREAMING_HUB_RECONCILER_ENABLED: "true" + STREAMING_HUB_RECONCILER_INTERVAL: "300" + # --- partition lifecycle cron --- + STREAMING_HUB_PARTITION_CRON_INTERVAL: "6h" + STREAMING_HUB_PARTITION_FUTURE_BUFFER_WEEKS: "4" + STREAMING_HUB_PARTITION_RETENTION_ENABLED: "false" + STREAMING_HUB_PARTITION_RETENTION_HORIZON: "0" + # --- DLQ visibility --- + STREAMING_HUB_DLQ_RETENTION: "168h" + STREAMING_HUB_DLQ_PRUNE_INTERVAL: "1h" + # --- lifecycle / drain (kept coherent with terminationGracePeriodSeconds) --- + STREAMING_HUB_SHUTDOWN_TIMEOUT: "30s" + STREAMING_HUB_PRE_STOP_DRAIN_TIMEOUT: "5s" + # --- observability (OTEL_* are lib-observability vars, NOT in .env.reference) --- + # OTEL_EXPORTER_OTLP_ENDPOINT is overridden per-pod with $(HOST_IP):4317 via + # the downward API when streamingHub.telemetry.enabled=true (a CHART-level + # toggle, see above — not an app env var; see templates/_deployment.tpl). + OTEL_LIBRARY_NAME: "github.com/LerianStudio/streaming-hub" + OTEL_RESOURCE_SERVICE_NAME: "streaming-hub" + OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT: "production" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + # =========================================================================== + # secrets — role-INVARIANT, SENSITIVE env (one shared Secret). All default "". + # Empty values are SKIPPED at render so unset SaaS/dev keys never ship blank. + # In gitops these come from Vault via useExistingSecret=true (this map is then + # unused). Every key exists verbatim in streaming-hub/.env.reference. + # =========================================================================== + secrets: + # Single hub-owned Postgres DSN (password embedded -> whole DSN is secret). + STREAMING_HUB_POSTGRES_DSN: "" + # SASL/SCRAM credentials (required when KAFKA_SCRAM_MECHANISM is set). + STREAMING_HUB_KAFKA_SCRAM_USERNAME: "" + STREAMING_HUB_KAFKA_SCRAM_PASSWORD: "" + # KEK reference: the NAME of the env var holding the KEK material. + STREAMING_HUB_KEK_REF: "" + # DEV-ONLY KEK material holder (local BYOC). Leave empty in real deploys. + STREAMING_HUB_DEV_KEK: "" + # SaaS tenant-manager X-API-Key (required-when-SaaS). + STREAMING_HUB_TENANT_MANAGER_SERVICE_API_KEY: "" + # SaaS tenant pub/sub Redis AUTH password (optional even in SaaS). + STREAMING_HUB_MULTI_TENANT_REDIS_PASSWORD: "" + # =========================================================================== + # migrations — OUT-OF-BAND schema migration Job (Helm + ArgoCD PreSync hook). + # + # The hub NEVER migrates itself (the app refuses to). Without this Job, ingest / + # dispatcher / partition workers spin on `relation "event_inbox"/"delivery_jobs" + # does not exist (42P01)`. This Job applies the SQL migrations BEFORE the app + # rolls out. + # + # ORDERING (the load-bearing design): the chain is + # bootstrap-postgres (PreSync weight -10, creates role+db) + # -> migration-secret (PreSync weight -5, carries the DSN) + # -> migrations Job (PreSync weight -1, creates the schema) + # -> app Deployment (main Sync) + # + # The runner is the stock `migrate/migrate` toolchain baked with the hub's + # `migrations/` dir; its entrypoint runs + # migrate -path=/migrations -database="$STREAMING_HUB_POSTGRES_DSN" up + # so the Job's ONLY required env is STREAMING_HUB_POSTGRES_DSN (secretKeyRef). + # It runs ONCE regardless of streamingHub.mode (all | split) — it is not per-role. + # =========================================================================== + migrations: + # -- Enable or disable the migrations Job. Default false — opt-in per env + # (consistent with the chart's other optional features). + enabled: false + # -- Optional. When true, the Job reads STREAMING_HUB_POSTGRES_DSN from a + # pre-existing Secret (existingSecretName) instead of the chart-managed + # migration-secret hook. Independent of streamingHub.useExistingSecret; + # set this to point migrations at the app's existing (e.g. Vault) Secret. + useExistingSecret: false + # -- Name of the pre-existing Secret holding STREAMING_HUB_POSTGRES_DSN + # (only used when migrations.useExistingSecret=true). + existingSecretName: "" + image: + # -- Migrations image (FROM migrate/migrate + COPY migrations/ /migrations/). + repository: ghcr.io/lerianstudio/streaming-hub-migrations + # -- Tag for the migrations image. Empty falls back to the app image tag + # (streamingHub.image.tag, or the chart appVersion). An explicit tag + # or digest overrides this. + tag: "" + # -- Pin by digest (sha256:...) instead of tag. Wins over tag when set. + digest: "" + # -- Image pull policy. + pullPolicy: IfNotPresent + # -- Maximum retries before the Job is considered failed. + backoffLimit: 3 + # -- Hard wall-clock cap on the Job (seconds). + activeDeadlineSeconds: 600 + # -- TTL after which a finished Job is garbage-collected (seconds). + ttlSecondsAfterFinished: 600 + # -- Extra annotations on the Job (merged after the hook annotations). + annotations: {} + # -- Extra annotations on the migration pod. + podAnnotations: {} + # -- Resource requests/limits for the migration container. + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 50m + memory: 64Mi + # =========================================================================== + # Per-role blocks. Each carries replicaCount, resources, autoscaling, pdb, and + # the Postgres pool sizing injected as explicit per-Deployment env. + # + # !!! CONNECTION-BUDGET INVARIANT (the load-bearing operational rule) !!! + # Σ over all running pods of (replicas × poolMaxOpenConns) + headroom + # ≤ PostgreSQL max_connections. + # Under HPA, use maxReplicas (not replicaCount) in the sum. Every open + # connection on every pod draws from the ONE shared Postgres budget. + # NEVER run mode=all alongside split — see streamingHub.mode. + # Pool defaults (from .env.reference): all 25/12, ingest 8/4, delivery 16/10. + # =========================================================================== + all: + replicaCount: 1 + poolMaxOpenConns: 25 + poolMaxIdleConns: 12 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + autoscaling: + # -- HPA off by default; replicaCount governs. maxReplicas × poolMaxOpenConns + # must respect the connection-budget invariant above. + enabled: false + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + pdb: + enabled: false + minAvailable: 1 + annotations: {} + nodeSelector: {} + tolerations: {} + affinity: {} + ingest: + replicaCount: 1 + poolMaxOpenConns: 8 + poolMaxIdleConns: 4 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + autoscaling: + enabled: false + minReplicas: 1 + # -- maxReplicas × 8 (poolMaxOpenConns) must fit the connection budget. + maxReplicas: 4 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + pdb: + enabled: false + minAvailable: 1 + annotations: {} + nodeSelector: {} + tolerations: {} + affinity: {} + delivery: + replicaCount: 1 + poolMaxOpenConns: 16 + poolMaxIdleConns: 10 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + autoscaling: + enabled: false + minReplicas: 1 + # -- maxReplicas × 16 (poolMaxOpenConns) must fit the connection budget. + maxReplicas: 4 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + pdb: + enabled: false + minAvailable: 1 + annotations: {} + nodeSelector: {} + tolerations: {} + affinity: {} +streaming-hub: + image: + tag: 1.0.1 diff --git a/charts/tracer/Chart.yaml b/charts/tracer/Chart.yaml index 1f04d6c97..3b5b60ecd 100644 --- a/charts/tracer/Chart.yaml +++ b/charts/tracer/Chart.yaml @@ -17,7 +17,7 @@ maintainers: - name: "Lerian Studio" email: "support@lerian.studio" -version: 2.1.0 +version: 2.2.0-beta.1 appVersion: "1.0.0" diff --git a/charts/tracer/templates/bootstrap-postgres.yaml b/charts/tracer/templates/bootstrap-postgres.yaml index e9dac46ee..391f30911 100644 --- a/charts/tracer/templates/bootstrap-postgres.yaml +++ b/charts/tracer/templates/bootstrap-postgres.yaml @@ -104,7 +104,7 @@ spec: echo "Role 'tracer' already exists. Skipping creation." else echo "Creating role 'tracer'..." - 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 ROLE tracer LOGIN PASSWORD '$DB_PASSWORD_TRACER'" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -v pw="$DB_PASSWORD_TRACER" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "CREATE ROLE tracer LOGIN PASSWORD :'pw'" fi # Create database if not exists diff --git a/charts/underwriter/Chart.yaml b/charts/underwriter/Chart.yaml index b7cae73a3..c275bc71c 100644 --- a/charts/underwriter/Chart.yaml +++ b/charts/underwriter/Chart.yaml @@ -17,7 +17,7 @@ maintainers: - name: "Lerian Studio" email: "support@lerian.studio" -version: 3.0.0 +version: 3.1.0-beta.1 appVersion: "1.0.0" diff --git a/charts/underwriter/templates/bootstrap-postgres.yaml b/charts/underwriter/templates/bootstrap-postgres.yaml index 281925f18..0a3173ac2 100644 --- a/charts/underwriter/templates/bootstrap-postgres.yaml +++ b/charts/underwriter/templates/bootstrap-postgres.yaml @@ -104,7 +104,7 @@ spec: echo "Role 'underwriter' already exists. Skipping creation." else echo "Creating role 'underwriter'..." - 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 ROLE underwriter LOGIN PASSWORD '$DB_PASSWORD_UNDERWRITER'" + PGPASSWORD="$DB_ADMIN_PASSWORD" psql -v ON_ERROR_STOP=1 -v pw="$DB_PASSWORD_UNDERWRITER" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER_ADMIN" -d "$DB_DATABASE" -c "CREATE ROLE underwriter LOGIN PASSWORD :'pw'" fi # Create database if not exists