multi-componentsecrets.RABBITMQ_DEFAULT_PASS, secrets.RABBITMQ_ERLANG_COOKIE (required when the bundled RabbitMQ subchart is enabled — must be stable across upgrades), and secrets.DATASOURCE_ONBOARDING_PASSWORD. The MongoDB password is not operator-provided with the bundled subchart: it is generated into the <release>-mongodb Secret (key mongodb-root-password), which this chart owns and keeps across uninstall, and read via secretKeyRef.secrets.DATASOURCE_CRED_ENC_KEY — a hex-encoded AES key (16/24/32 bytes; openssl rand -hex 32) that both components use to encrypt registered data-source credentials at rest. It must be identical on the manager and the worker, and it cannot be rotated in that release. The render fails when either component’s image.tag is >= 3.0.0 and the key is empty; app 2.4.x (the chart’s default appVersion) ignores it. See docs/UPGRADE-4.1.md.<release>-mongodb Secret; the subchart reads it through mongodb.auth.existingSecret. RabbitMQ follows Pattern B (the groundhog2k broker is pointed at the application reporter-manager Secret via rabbitmq.authentication.existingSecret, so the broker password lives only in secrets.RABBITMQ_DEFAULT_PASS; this also removes the prior midaz-vs-plugin user mismatch). Valkey: the valkey.io subchart exposes no Secret-based password mechanism (auth is an inline ACL) and ships with auth.enabled: false, so there is no single-source wiring for it here. See docs/helm-chart-standard.md “Single-Source Infra Secrets”.<release>-mongodb / reporter-manager Secret references assume the release is installed as reporter. If you override manager.name/manager.existingSecretName, set rabbitmq.authentication.existingSecret to match.reporter-rabbitmq. The first upgrade to a chart version that renders it restarts the broker once, and messages still queued in the old in-pod directory (emptyDir) are lost; let the queues drain before that upgrade.rabbitmq.enabled the broker’s only user is secrets.RABBITMQ_DEFAULT_USER (tag management, which the app’s broker health check needs) with secrets.RABBITMQ_DEFAULT_PASS. Until the manager and the worker escape the password in their broker URL, only letters, digits and -._~ are safe in it: a space, ", \, a backtick, /, #, ?, % or a non-ASCII letter crash-loops the manager. The render refuses an empty password, the passwords printed in this chart (reporter123 and Lerian@123, defaults of earlier versions, and the CHANGE_ME placeholder), and manager.useExistingSecret or worker.useExistingSecret (the broker never learns an external Secret’s password). The broker takes the password when its pod starts: after changing it, or after upgrading from a version that shipped reporter123, run kubectl -n <namespace> rollout restart statefulset/<release>-rabbitmq. The broker’s volume keeps the users it already has: after renaming secrets.RABBITMQ_DEFAULT_USER, delete the old one with kubectl -n <namespace> exec <release>-rabbitmq-0 -- rabbitmqctl delete_user <old-user>.github.com/LerianStudio/helm; license is Apache-2.0.The Reporter plugin provides a flexible document templating system that enables dynamic report generation based on predefined templates. This plugin consists of two main components:
+----------------+ +----------------+ +----------------+
| | | | | |
| Manager API +----->+ RabbitMQ +----->+ Worker |
| | | | | |
+-------+--------+ +----------------+ +-------+--------+
| |
v v
+-------+--------+ +-------+--------+
| | | |
| MongoDB | | SeaweedFS |
| | | |
+----------------+ +----------------+
helm install reporter oci://registry-1.docker.io/lerianstudio/reporter-helm --version <version> -n reporter --create-namespace
To install with a custom values file:
helm install reporter oci://registry-1.docker.io/lerianstudio/reporter-helm --version <version> -n reporter -f my-values.yaml
helm uninstall reporter -n reporter
Uninstall keeps the data. The bundled MongoDB volume (PVC <release>-mongodb) and its password Secret (<release>-mongodb) stay, as do the SeaweedFS report volumes (StatefulSet claims) and the RabbitMQ volume with its queued messages (PVC reporter-rabbitmq), so a reinstall under the same release name opens the same data with the same password. The RabbitMQ claim keeps that fixed name under any release name, so an install under another release name in the same namespace is refused until the claim is deleted. Deleting the data is a separate, manual step: kubectl delete pvc <release>-mongodb -n reporter, kubectl delete pvc reporter-rabbitmq -n reporter and kubectl delete secret <release>-mongodb -n reporter. Deleting only the Secret resets nothing: a reinstall generates a new password that the kept data never learned. The chart, not the MongoDB subchart, owns that password: set mongodb.auth.rootPassword to choose it, or mongodb.auth.existingSecret to bring your own Secret (the chart then renders none).
The following table lists the configurable parameters and their default values.
global.cloud)Point this chart at a managed-cloud environment (AWS/GCP/Azure) instead of the bundled in-cluster MongoDB/Redis/RabbitMQ/SeaweedFS with one knob:
global:
cloud: "aws" # aws | gcp | azure — leave unset for the bundled dev topology
datastores:
mongo: { host: "my-documentdb.example.com", port: "27017", user: "reporter" }
redis: { host: "my-elasticache.example.com:6379" }
broker: { host: "my-amazonmq.example.com" }
objectStorage:
s3: { endpoint: "https://s3.us-east-1.amazonaws.com", region: "us-east-1", bucket: "my-bucket" }
observability:
enabled: true
auth:
host: "http://plugin-access-manager-auth:4000"
global.cloud sets the connection TOPOLOGY (TLS, AMQP scheme/ports, S3
path-style) for the masks above; only the ENDPOINTS (host/port/user) still
come from global.datastores/global.objectStorage — a cloud preset can’t
know your RDS host. A native common.configmap.<KEY> always overrides any
mask.
Copy values-template.yaml as your starting point — it documents every
global.* mask with a working example. values.yaml is the full
power-user reference; values.schema.json validates it.
This chart version adopts lerian-common typed masks and, alongside the stated
OTEL/TLS fix, ships a few deliberate operational default changes. Each is
render-equivalent for the bundled dev topology (no override needed) unless noted:
| Key | Old default | New default | Rationale |
|---|---|---|---|
common.configmap.ALLOW_INSECURE_TLS |
"false" |
"true" (security-relevant) |
The bundled mongo/redis/rabbitmq/postgres subcharts run without TLS; the app requires this flag to bypass (mongo hard-fails with “TLS required” otherwise), so a zero-override helm install previously crashed. Same convention as plugin-access-manager. Flip to "false" explicitly for any managed-cloud/TLS-terminated topology (global.cloud presets do this for you). |
common.configmap.OTEL_RESOURCE_DEPLOYMENT_ENVIRONMENT |
"production" |
derived from ENV_NAME (default "development") |
Single-sources the OTEL deployment-environment with ENV_NAME instead of two independent hardcoded literals, so a zero-override install no longer contradicts itself (ENV_NAME=development but deployment_environment=production), which previously tripped the app’s own production safety check against the non-TLS bundled OTel endpoint. |
manager.configmap.LOG_LEVEL / worker.configmap.LOG_LEVEL |
debug |
info |
Reduce default log noise; override to debug for local troubleshooting. |
worker.configmap.PDF_POOL_WORKERS |
"5" |
"3" |
Aligns the worker’s default pool size with the manager’s existing default (3), which was already 3 pre-PR. |
worker.configmap.PDF_TIMEOUT_SECONDS |
"30" |
"90" |
Gives PDF generation enough headroom for larger reports before timing out. |
common.configmap.MONGO_PARAMETERS |
"" |
"maxIdleTimeMS=60000" |
Bounds idle MongoDB connections instead of holding them open indefinitely; a sensible production-safe default, not required by the OTEL/TLS fix but bundled here as part of the same “sensible chart defaults” pass. Override with common.configmap.MONGO_PARAMETERS: "" to restore the old (unbounded) behavior. |
seaweedfs.global.serviceAccountName |
(unset — subchart default "seaweedfs") |
"reporter-seaweedfs" |
Upgrade note: paired with seaweedfs.global.createClusterRole: false (also new) to avoid a fixed-name cluster-scoped ClusterRole/ClusterRoleBinding collision when more than one release runs on the same cluster. Because createClusterRole defaults to false, the ClusterRole itself is not created either way — but this does rename the SeaweedFS ServiceAccount object referenced by the master/volume/filer StatefulSets on any existing install that had seaweedfs.enabled: true before this change (old name seaweedfs → new name reporter-seaweedfs). Expect a one-time rolling restart of the SeaweedFS pods on upgrade; if you have IRSA/IAM-role annotations or RoleBindings pinned to the old ServiceAccount name, update them to reporter-seaweedfs before upgrading. |
MONGO_MAX_POOL_SIZE (100) and MONGO_HOST/RABBITMQ_HOST/RABBITMQ_HEALTH_CHECK_URL/REDIS_HOST/OBJECT_STORAGE_ENDPOINT
(FQDN <svc>.reporter.svc.cluster.local form, restored in this PR after an
unintentional short-name drift was caught in review) are unchanged from the
pre-adoption defaults — kept byte-identical for existing installs.
global.scheduling.spread)Every Deployment of this chart (the manager, and the worker when it runs as a
Deployment with keda.enabled=false) renders topologySpreadConstraints from the
lerian-common spread preset. The default is
soft (ScheduleAnyway) across nodes, so replicas are spread across nodes (e.g. spot nodes)
whenever possible, but a pod is never left Pending because of it.
Zone spreading is off by default (zone: ""). When scoring a soft spread, the
scheduler skips every node that lacks the topology.kubernetes.io/zone label, so on
clusters without zone labels (bare-metal, k3s) a zone constraint silently cancels the
node spread. Turn it on (zone: ScheduleAnyway) where every node carries zone labels
(EKS, GKE, AKS).
global:
scheduling:
spread:
enabled: true # master switch
hostname: ScheduleAnyway # kubernetes.io/hostname: ScheduleAnyway | DoNotSchedule | "" (off)
zone: "" # topology.kubernetes.io/zone: same values (off by default)
maxSkew: 1 # integer >= 1
minDomains: 0 # DoNotSchedule constraints only; 0 = off
nodeTaintsPolicy: "" # Honor | Ignore | "" (Kubernetes default Ignore)
manager:
spread: { hostname: DoNotSchedule } # field-level override of the global preset
topologySpreadConstraints: [] # raw list; non-empty replaces the preset entirely
On EKS + Karpenter use a hard node spread:
global:
scheduling:
spread:
hostname: DoNotSchedule
zone: ScheduleAnyway
maxSkew: 1
minDomains: 2
nodeTaintsPolicy: Honor
minDomains: 2 is required with DoNotSchedule. Skew is measured only against nodes
that already exist, so on a single eligible node all replicas would otherwise share
it. With fewer than 2 nodes the extra replica stays Pending until Karpenter launches
another node. nodeTaintsPolicy: Honor stops tainted nodes (for example a node being
drained) from counting as domains.
Precedence: <component>.topologySpreadConstraints (non-empty, wins entirely) >
<component>.spread.<field> > global.scheduling.spread.<field> > off. Each
preset constraint selects only the component’s own pods
(labelSelector = the Deployment’s spec.selector.matchLabels) of the same
ReplicaSet (matchLabelKeys: [pod-template-hash]), so a rolling update is never
blocked by the old ReplicaSet’s pods, even with DoNotSchedule. Requires
Kubernetes >= 1.27 (matchLabelKeys, beta since 1.27 and enabled by default);
on 1.19–1.26 set global.scheduling.spread.enabled: false, leave every
<component>.spread.enabled unset or false (both override the global switch). Raw
<component>.topologySpreadConstraints must also respect the cluster version: omit
matchLabelKeys (needs 1.27+), minDomains (GA in 1.30) and nodeTaintsPolicy /
nodeAffinityPolicy (on by default since 1.26) on older clusters, or the API
rejects or drops them. Use DoNotSchedule only when
the cluster can always provide enough distinct nodes/zones for the replica count
(a hard constraint leaves extra pods Pending otherwise). The KEDA ScaledJob
worker (the default) is a Job and is not spread.
Upgrade note: the preset is on by default, so upgrading to the chart version
that introduces it adds topologySpreadConstraints to the manager (and the
Deployment-mode worker) pod template and triggers one rolling restart of
those Deployments. No values change is required. To keep the previous pod
template exactly, set global.scheduling.spread.enabled: false.
| Parameter | Description | Default |
|---|---|---|
common.configmap |
Common environment variables shared by manager and worker | See values.yaml |
secrets |
Shared secrets for all components (dynamic - any key added here is rendered into the Secret) | See values.yaml |
| Parameter | Description | Default |
|---|---|---|
manager.replicaCount |
Number of manager replicas | 1 |
manager.image.repository |
Manager image repository | ghcr.io/lerianstudio/reporter-manager |
manager.image.tag |
Manager image tag | 1.2.0 |
manager.image.pullPolicy |
Manager image pull policy | IfNotPresent |
manager.service.type |
Kubernetes Service type | ClusterIP |
manager.service.port |
Service HTTP port | 4005 |
manager.resources |
CPU/Memory resource requests/limits | See values.yaml |
manager.useExistingSecret |
Use an existing secret instead of creating a new one | false |
manager.existingSecretName |
Name of the existing secret to use | "" |
manager.clusterRole.create |
Enable or disable ClusterRole and ClusterRoleBinding creation | true |
manager.keda.scaledObject |
KEDA ScaledObject configuration | See values.yaml |
| Parameter | Description | Default |
|---|---|---|
worker.image.repository |
Worker image repository | ghcr.io/lerianstudio/reporter-worker |
worker.image.tag |
Worker image tag | 1.2.0 |
worker.image.pullPolicy |
Worker image pull policy | IfNotPresent |
worker.resources |
CPU/Memory resource requests/limits | See values.yaml |
worker.useExistingSecret |
Use an existing secret instead of creating a new one | false |
worker.existingSecretName |
Name of the existing secret to use | "" |
worker.keda.scaledJob |
KEDA ScaledJob configuration | See values.yaml |
| Parameter | Description | Default |
|---|---|---|
mongodb.enabled |
Enable or disable MongoDB deployment | true |
rabbitmq.enabled |
Enable or disable RabbitMQ deployment | true |
seaweedfs.enabled |
Enable or disable SeaweedFS deployment | true |
valkey.enabled |
Enable or disable Valkey (Redis) deployment | true |
keda.enabled |
Enable or disable KEDA operator deployment | true |
keda.external |
Use an externally installed KEDA operator | false |
The secrets section in values.yaml is fully dynamic. Any key/value pair added under secrets: is automatically rendered into the Kubernetes Secret for both manager and worker:
secrets:
RABBITMQ_DEFAULT_USER: reporter
RABBITMQ_DEFAULT_PASS: <your-rabbitmq-password>
# Stable Erlang cookie for the bundled RabbitMQ (required when rabbitmq.enabled).
# Generate once with: openssl rand -hex 32
RABBITMQ_ERLANG_COOKIE: <stable-cookie>
DATASOURCE_ONBOARDING_PASSWORD: lerian
# MONGO_PASSWORD is single-sourced from the chart's bundled-MongoDB Secret and read
# via secretKeyRef — leave it unset; only set it for an EXTERNAL MongoDB.
# REDIS_PASSWORD is omitted because the bundled valkey runs with auth disabled.
# At-rest encryption of registered data-source credentials. Required by app >= 3.0.0
# (both components); same value on manager and worker; NOT rotatable.
# Generate with: openssl rand -hex 32
DATASOURCE_CRED_ENC_KEY: <64-hex-chars>
# Add any custom datasource password:
DATASOURCE_EXTERNAL_PASSWORD: db_password
DATASOURCE_CRED_ENC_KEYbelongs undersecrets:, never undercommon.configmap:— the configmap escape hatch accepts anyDATASOURCE_*key, so a misplaced one lands in a plaintext ConfigMap without any warning.
For production environments, you can use pre-existing Kubernetes Secrets instead of having the chart create them. When enabled, the chart skips Secret creation and references the provided secret name in the deployments.
manager:
useExistingSecret: true
existingSecretName: "my-manager-secret"
worker:
useExistingSecret: true
existingSecretName: "my-worker-secret"
When using existing secrets, ensure they contain all required keys (including any custom datasource passwords). The deployments inject secrets via envFrom/secretRef, so all keys from the external secret are loaded as environment variables.
The KEDA TriggerAuthentication also respects this setting and will automatically reference the existing secret for RabbitMQ credentials.
The Reporter supports connecting to additional external databases beyond the built-in Midaz onboarding datasource. You can register multiple datasources by following the naming convention DATASOURCE_<NAME>_*.
All variables follow the pattern DATASOURCE_<NAME>_<PROPERTY>, where <NAME> is a unique identifier you choose for the datasource (e.g., EXTERNAL, SALES, ANALYTICS).
DATASOURCE_* is a declared open namespace: the operator names each datasource at
deploy time, so a closed allowlist could only ever list example names. The strict
values.schema.json therefore accepts any well-formed DATASOURCE_<NAME>_<PROPERTY> key
under common.configmap — you can register datasources without ever touching the schema —
while a typo outside the namespace (e.g. REDIS_HOSTX) is still rejected at
helm install. The guard is propertyNames: anyOf: [enum, pattern ^DATASOURCE_...],
generated with gen-schema.py --open-prefixes DATASOURCE_ (see config/schema-keys.txt).
It is a named open family, never a fully-open ConfigMap.
| Variable | Description | Example |
|---|---|---|
DATASOURCE_<NAME>_CONFIG_NAME |
Logical name used to reference this datasource in report templates | external_db |
DATASOURCE_<NAME>_HOST |
Database hostname or IP address | external-postgres.example.com |
DATASOURCE_<NAME>_PORT |
Database port | 5432 |
DATASOURCE_<NAME>_USER |
Database username | db_user |
DATASOURCE_<NAME>_PASSWORD |
Database password (must be defined under secrets) |
db_password |
DATASOURCE_<NAME>_DATABASE |
Database name | external_database |
DATASOURCE_<NAME>_TYPE |
Database type | postgresql |
| Variable | Description | Default |
|---|---|---|
DATASOURCE_<NAME>_SSLMODE |
SSL connection mode | disable |
DATASOURCE_<NAME>_SSLROOTCERT |
Path to SSL root certificate | "" |
DATASOURCE_<NAME>_DB_SCHEMAS |
Comma-separated list of schemas to query | public |
Connection details go under common.configmap and passwords go under secrets:
common:
configmap:
DATASOURCE_EXTERNAL_CONFIG_NAME: external_db
DATASOURCE_EXTERNAL_HOST: external-postgres.example.com
DATASOURCE_EXTERNAL_PORT: "5432"
DATASOURCE_EXTERNAL_USER: db_user
DATASOURCE_EXTERNAL_DATABASE: external_database
DATASOURCE_EXTERNAL_TYPE: postgresql
DATASOURCE_EXTERNAL_SSLMODE: disable
DATASOURCE_EXTERNAL_DB_SCHEMAS: sales,inventory,reporting
secrets:
DATASOURCE_EXTERNAL_PASSWORD: db_password
You can register as many datasources as needed. Each one must have a unique <NAME>:
common:
configmap:
# First datasource
DATASOURCE_SALES_CONFIG_NAME: sales_db
DATASOURCE_SALES_HOST: sales-postgres.example.com
DATASOURCE_SALES_PORT: "5432"
DATASOURCE_SALES_USER: sales_user
DATASOURCE_SALES_DATABASE: sales
DATASOURCE_SALES_TYPE: postgresql
DATASOURCE_SALES_SSLMODE: require
# Second datasource
DATASOURCE_ANALYTICS_CONFIG_NAME: analytics_db
DATASOURCE_ANALYTICS_HOST: analytics-postgres.example.com
DATASOURCE_ANALYTICS_PORT: "5432"
DATASOURCE_ANALYTICS_USER: analytics_user
DATASOURCE_ANALYTICS_DATABASE: analytics
DATASOURCE_ANALYTICS_TYPE: postgresql
DATASOURCE_ANALYTICS_SSLMODE: require
DATASOURCE_ANALYTICS_DB_SCHEMAS: reports,aggregations
secrets:
DATASOURCE_SALES_PASSWORD: sales_password
DATASOURCE_ANALYTICS_PASSWORD: analytics_password
Reference datasources by their CONFIG_NAME value. When querying specific schemas, use the syntax <config_name>:<schema>.<table>:
# Default schema (public)
external_db:orders
# Explicit schema
external_db:sales.orders
external_db:inventory.products
# Different datasources
sales_db:invoices
analytics_db:reports.monthly_summary
When useExistingSecret is enabled for manager/worker, datasource passwords must be included in the external secret alongside all other required keys. The chart does not create any Secret resources in this mode.
CRM is disabled unless at least one DATASOURCE_CRM_* key is supplied. When it
is declared, Reporter requires the complete non-secret block below in
common.configmap; the chart does not invent an organization ID or a connection
default:
common:
configmap:
DATASOURCE_CRM_CONFIG_NAME: "plugin_crm" # reserved name
DATASOURCE_CRM_TYPE: "mongodb"
DATASOURCE_CRM_HOST: "crm.example.invalid"
DATASOURCE_CRM_PORT: "27017"
DATASOURCE_CRM_DATABASE: "crm"
DATASOURCE_CRM_USER: "reporter"
DATASOURCE_CRM_MIDAZ_ORGANIZATION_ID: "<organization-id>"
DATASOURCE_CRM_CONFIG_NAME must remain plugin_crm; it is a reserved
datasource rather than a general-purpose alias. The organization ID must be the
organization entitled to that CRM data. The chart fails its render if CRM is
partially configured, has a different config name or type, or omits this scope.
Non-CRM installations are unaffected.
Keep these values in secrets: when the chart manages its Secrets, never in a
ConfigMap:
secrets:
DATASOURCE_CRM_PASSWORD: "<crm-password>"
CRYPTO_HASH_SECRET_KEY_CRM: "<crm-hash-key>"
CRYPTO_ENCRYPT_SECRET_KEY_CRM: "<crm-encryption-key>"
Do not replace or rotate DATASOURCE_CRED_ENC_KEY as part of this setup. With
manager.useExistingSecret or worker.useExistingSecret, Helm cannot inspect
the external Secret; provide the same CRM keys in each selected external Secret.
The non-secret CRM guard still applies.
In single-tenant mode the Manager owns CRM bootstrap/reconciliation. It may
backfill a missing organization ID but does not overwrite a non-empty persisted
value. In multi-tenant mode that seed is skipped. Do not create, rotate, or edit
plugin_crm through the ordinary datasource API or by manually changing the
registry; investigate an existing scope mismatch through the supported runtime
path.
When using an external RabbitMQ instance (not deployed by this chart), you can enable the bootstrap job to apply the messaging topology the service needs: exchanges, queues and bindings, inside the default vhost /.
The job is topology-only. It does not create the application broker user, its permissions, or any vhost — provision those on the broker before enabling it.
externalRabbitmqDefinitions:
enabled: true
connection:
protocol: "http"
host: "my-rabbitmq.example.com"
port: "15672"
portAmqp: "5672"
rabbitmqAdminLogin:
username: "admin"
password: "REPLACE_ME"
The bootstrap job:
/The application user the service authenticates as (secrets.RABBITMQ_DEFAULT_USER / RABBITMQ_DEFAULT_PASS) must already exist on the broker with permissions on /.
When connection.protocol is https, the job verifies the broker’s TLS certificate by default — it sends the admin credential on every management API call. For a broker with a self-signed certificate, set:
externalRabbitmqDefinitions:
connection:
protocol: "https"
skipTlsVerify: true
externalRabbitmqDefinitions:
enabled: true
connection:
protocol: "https"
host: "my-rabbitmq.example.com"
port: "443"
portAmqp: "5672"
rabbitmqAdminLogin:
useExistingSecret:
name: "my-rabbitmq-admin-secret" # must contain RABBITMQ_ADMIN_USER and RABBITMQ_ADMIN_PASS keys
This chart utilizes KEDA (Kubernetes Event-driven Autoscaling) for scaling components based on metrics and RabbitMQ queue length.
The following KEDA resources are created:
If KEDA is already installed in your cluster, disable the bundled operator and set external: true:
keda:
enabled: false
external: true
The chart will still create ScaledJob and TriggerAuthentication resources but will not install the KEDA operator itself.
The chart creates a ClusterRole and ClusterRoleBinding for the manager to access CRDs and deployments. If these resources already exist in the cluster (e.g., from a previous installation), you can disable their creation:
manager:
clusterRole:
create: false
After deploying the chart, you can access the manager API within the cluster:
http://reporter-manager.reporter.svc.cluster.local:4005
For external access via port-forwarding:
kubectl port-forward svc/reporter-manager 4005:4005 -n reporter
Then access: http://localhost:4005
The API documentation is available at the /swagger/index.html endpoint.
Contributions are welcome! Please feel free to submit a Pull Request.