diff --git a/helm/flowfuse/README.md b/helm/flowfuse/README.md index aa6a1c65..95d54f1a 100644 --- a/helm/flowfuse/README.md +++ b/helm/flowfuse/README.md @@ -113,10 +113,10 @@ To use STMP to send email - `forge.broker.url` URL to access the broker from inside the cluster (default `mqtt://flowforge-broker.[namespace]:1883`) - `forge.broker.public_url` URL to access the broker from outside the cluster (default `ws://mqtt.[forge.domain]`, uses `wss://` if `forge.https` is `true`) - `forge.broker.hostname` the custom Fully Qualified Domain Name (FQDN) where the broker will be hosted (default `mqtt.[forge.domain]`) - - `forge.broker.teamBroker.enabled` Enables Team Broker feature (default `false`) + - `forge.broker.teamBroker.enabled` Enables Team Broker feature (default `false`). Requires `forge.broker.enabled=true` - `forge.broker.teamBroker.api.url` URL for the Team Broker API (default `http://emqx-dashboard.:18083`) - - `forge.broker.teamBroker.api.key` API key for the Team Broker API (default not set) - - `forge.broker.teamBroker.api.secret` API secret for the Team Broker API (default not set) + - `forge.broker.teamBroker.api.key` API key name for the Team Broker API (optional; must be set together with `api.secret`) + - `forge.broker.teamBroker.api.secret` API secret for the Team Broker API (optional; must be set together with `api.key`) - `forge.broker.createMetricsUser` defines if a dedicated MQTT user with broker metrics collection permissions should be created (default `true`) - `forge.broker.affinity` allows to configure [affinity or anti-affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) for the broker pod - `forge.broker.resources` allows to configure [resources](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) for the broker container diff --git a/helm/flowfuse/templates/_helpers.tpl b/helm/flowfuse/templates/_helpers.tpl index b57ce1af..189b1524 100644 --- a/helm/flowfuse/templates/_helpers.tpl +++ b/helm/flowfuse/templates/_helpers.tpl @@ -126,7 +126,7 @@ Note: The value for key .Values.postgresql.auth.existingSecret is inherited from (not (and .Values.forge.email ((and .Values.forge.email.smtp (not .Values.forge.email.smtp.existingSecret))))) (not ((.Values.forge.assistant).enabled)) (not ((.Values.forge.expert).enabled)) - (not ((.Values.forge.broker.teamBroker).enabled))) -}} + (not (include "forge.teamBrokerApiUsesCustomKey" .))) -}} true {{- else -}} false @@ -350,11 +350,53 @@ Get the name from the release name. {{- end -}} {{/* -Get the secret object name with Team Broker secret. +Determine whether the Team Broker API uses a dedicated, user-supplied credential +(forge.broker.teamBroker.api.key + api.secret) instead of the shared EMQX bootstrap key. +Returns "true" only when both api.key and api.secret are provided. */}} -{{- define "forge.teamBrokerSecretName" -}} -{{- if (.Values.forge.broker.teamBroker).enabled -}} +{{- define "forge.teamBrokerApiUsesCustomKey" -}} +{{- if and ((.Values.forge.broker.teamBroker).api).key ((.Values.forge.broker.teamBroker).api).secret -}} +true +{{- end -}} +{{- end -}} + +{{/* +Resolve the Team Broker API key name. Defaults to the shared EMQX bootstrap API key name +("flowfuse"); when a dedicated credential is supplied, uses forge.broker.teamBroker.api.key. +*/}} +{{- define "forge.teamBrokerApiKey" -}} +{{- if ((.Values.forge.broker.teamBroker).api).key -}} + {{- .Values.forge.broker.teamBroker.api.key -}} +{{- else -}} + {{- printf "flowfuse" -}} +{{- end -}} +{{- end -}} + +{{/* +Name of the secret the Team Broker API secret is read from. +- Dedicated credential: the chart-managed flowfuse-secrets. +- Shared bootstrap key: broker.existingSecret when provided, else emqx-config-secrets. +*/}} +{{- define "forge.teamBrokerApiSecretName" -}} +{{- if include "forge.teamBrokerApiUsesCustomKey" . -}} {{- printf "flowfuse-secrets" -}} +{{- else if .Values.broker.existingSecret -}} + {{- .Values.broker.existingSecret -}} +{{- else -}} + {{- printf "emqx-config-secrets" -}} +{{- end -}} +{{- end -}} + +{{/* +Data key within the Team Broker API secret holding the secret value. +- Dedicated credential: teamBrokerApiSecret (in flowfuse-secrets). +- Shared bootstrap key: api_key_secret (in the EMQX secret). +*/}} +{{- define "forge.teamBrokerApiSecretKey" -}} +{{- if include "forge.teamBrokerApiUsesCustomKey" . -}} + {{- printf "teamBrokerApiSecret" -}} +{{- else -}} + {{- printf "api_key_secret" -}} {{- end -}} {{- end -}} @@ -365,17 +407,20 @@ Resolve Team Broker API URL: user-provided value, or default to the in-cluster E {{- if ((.Values.forge.broker.teamBroker).api).url -}} {{- .Values.forge.broker.teamBroker.api.url -}} {{- else -}} - {{- printf "http://emqx-dashboard.%s:18083" .Release.Namespace -}} + {{- printf "http://emqx-dashboard.%s:18083/api/v5" .Release.Namespace -}} {{- end -}} {{- end -}} {{/* -Create Team Broker API secret +Render the custom Team Broker API secret into flowfuse-secrets. +Created when a custom credential is supplied; api.key and api.secret are required together. */}} {{- define "forge.teamBrokerApiSecret" -}} -{{- if and (.Values.forge.broker.teamBroker).enabled (.Values.forge.broker.teamBroker).api -}} -{{- $_ := required "A valid .Values.forge.broker.teamBroker.api.key is required!" .Values.forge.broker.teamBroker.api.key -}} -{{- $token := required "A valid .Values.forge.broker.teamBroker.api.secret is required!" .Values.forge.broker.teamBroker.api.secret -}} +{{- if (.Values.forge.broker.teamBroker).enabled -}} +{{- if or ((.Values.forge.broker.teamBroker).api).key ((.Values.forge.broker.teamBroker).api).secret -}} +{{- $_ := required "A valid .Values.forge.broker.teamBroker.api.key is required when api.secret is set!" ((.Values.forge.broker.teamBroker).api).key -}} +{{- $token := required "A valid .Values.forge.broker.teamBroker.api.secret is required when api.key is set!" ((.Values.forge.broker.teamBroker).api).secret -}} teamBrokerApiSecret: {{ $token | b64enc | quote }} {{- end -}} -{{- end -}} \ No newline at end of file +{{- end -}} +{{- end -}} diff --git a/helm/flowfuse/templates/configmap.yaml b/helm/flowfuse/templates/configmap.yaml index 311d18ff..f32b2ebd 100644 --- a/helm/flowfuse/templates/configmap.yaml +++ b/helm/flowfuse/templates/configmap.yaml @@ -224,10 +224,10 @@ data: teamBroker: enabled: true host: {{ include "forge.teamBrokerHost" . }} - {{- if .Values.forge.broker.teamBroker.api }} + {{- if .Values.forge.broker.teamBroker.enabled }} api: url: {{ include "forge.teamBrokerApiUrl" . }} - key: {{ .Values.forge.broker.teamBroker.api.key }} + key: {{ include "forge.teamBrokerApiKey" . }} secret: <%= ENV['TEAM_BROKER_API_SECRET'] %> {{- end }} {{ end -}} diff --git a/helm/flowfuse/templates/deployment.yaml b/helm/flowfuse/templates/deployment.yaml index 4ce9d990..c0f42613 100644 --- a/helm/flowfuse/templates/deployment.yaml +++ b/helm/flowfuse/templates/deployment.yaml @@ -90,12 +90,12 @@ spec: key: expertToken optional: true {{- end }} - {{- if and (.Values.forge.broker.teamBroker).enabled (.Values.forge.broker.teamBroker).api }} + {{- if and .Values.forge.broker.enabled (.Values.forge.broker.teamBroker).enabled }} - name: TEAM_BROKER_API_SECRET valueFrom: secretKeyRef: - name: {{ include "forge.teamBrokerSecretName" . }} - key: teamBrokerApiSecret + name: {{ include "forge.teamBrokerApiSecretName" . }} + key: {{ include "forge.teamBrokerApiSecretKey" . }} optional: true {{- end }} {{- if .Values.forge.localPostgresql }} diff --git a/helm/flowfuse/templates/emqx.yaml b/helm/flowfuse/templates/emqx.yaml index 726b0dcd..ab2890a0 100644 --- a/helm/flowfuse/templates/emqx.yaml +++ b/helm/flowfuse/templates/emqx.yaml @@ -83,7 +83,7 @@ spec: type: ClusterIP {{- end }} --- -{{- if not .Values.broker.exisitingSecret }} +{{- if not .Values.broker.existingSecret }} apiVersion: v1 kind: Secret metadata: diff --git a/helm/flowfuse/tests/team_broker_api_test.yaml b/helm/flowfuse/tests/team_broker_api_test.yaml index 70954ead..cec53a65 100644 --- a/helm/flowfuse/tests/team_broker_api_test.yaml +++ b/helm/flowfuse/tests/team_broker_api_test.yaml @@ -9,6 +9,8 @@ set: tests: - it: should not include teamBroker api block when teamBroker is disabled by default template: configmap.yaml + set: + forge.broker.enabled: true asserts: - notMatchRegex: path: data["flowforge.yml"] @@ -17,34 +19,19 @@ tests: path: data["flowforge.yml"] pattern: "TEAM_BROKER_API_SECRET" - - it: should render teamBroker.api block with url, key and secret when enabled + - it: should not render teamBroker block when broker is disabled template: configmap.yaml set: - forge.broker.enabled: true + forge.broker.enabled: false forge.broker.teamBroker: enabled: true - api: - url: "https://team-broker.example.com" - key: "team-broker-key" - secret: "team-broker-secret" asserts: - - matchRegex: - path: data["flowforge.yml"] - pattern: "teamBroker:" - - matchRegex: - path: data["flowforge.yml"] - pattern: "url: https://team-broker\\.example\\.com" - - matchRegex: - path: data["flowforge.yml"] - pattern: "key: team-broker-key" - - matchRegex: - path: data["flowforge.yml"] - pattern: "secret: <%= ENV\\['TEAM_BROKER_API_SECRET'\\] %>" - notMatchRegex: path: data["flowforge.yml"] - pattern: "secret: team-broker-secret" + pattern: "teamBroker:" - - it: should render teamBroker block without api sub-block when teamBroker is enabled but api is missing + # Shared bootstrap key (default, no api.key/api.secret) + - it: should render teamBroker.api block with the shared EMQX bootstrap key when enabled template: configmap.yaml set: forge.broker.enabled: true @@ -54,27 +41,40 @@ tests: - matchRegex: path: data["flowforge.yml"] pattern: "teamBroker:" - - notMatchRegex: + - matchRegex: path: data["flowforge.yml"] - pattern: "api:" - - notMatchRegex: + pattern: "key: flowfuse" + - matchRegex: path: data["flowforge.yml"] - pattern: "TEAM_BROKER_API_SECRET" + pattern: "secret: <%= ENV\\['TEAM_BROKER_API_SECRET'\\] %>" - - it: should render teamBroker block without api sub-block when teamBroker is enabled but api is empty + # Dedicated credential (api.key + api.secret) + - it: should render teamBroker.api block with the dedicated key when api.key and api.secret are set template: configmap.yaml set: forge.broker.enabled: true forge.broker.teamBroker: enabled: true - api: {} + api: + url: "https://team-broker.example.com" + key: "team-broker-key" + secret: "team-broker-secret" asserts: - matchRegex: path: data["flowforge.yml"] - pattern: "teamBroker:" + pattern: "url: https://team-broker\\.example\\.com" + - matchRegex: + path: data["flowforge.yml"] + pattern: "key: team-broker-key" + - matchRegex: + path: data["flowforge.yml"] + pattern: "secret: <%= ENV\\['TEAM_BROKER_API_SECRET'\\] %>" - notMatchRegex: path: data["flowforge.yml"] - pattern: "api:" + pattern: "key: flowfuse" + - notMatchRegex: + path: data["flowforge.yml"] + pattern: "secret: team-broker-secret" # Deployment tests - it: should not include TEAM_BROKER_API_SECRET env var by default @@ -86,21 +86,29 @@ tests: - it: should not include TEAM_BROKER_API_SECRET env var when teamBroker is disabled template: deployment.yaml set: + forge.broker.enabled: true forge.broker.teamBroker: enabled: false asserts: - notExists: path: spec.template.spec.initContainers[0].env[?(@.name == "TEAM_BROKER_API_SECRET")] - - it: should include TEAM_BROKER_API_SECRET env var when teamBroker is enabled + - it: should not include TEAM_BROKER_API_SECRET env var when broker is disabled template: deployment.yaml set: + forge.broker.enabled: false + forge.broker.teamBroker: + enabled: true + asserts: + - notExists: + path: spec.template.spec.initContainers[0].env[?(@.name == "TEAM_BROKER_API_SECRET")] + + - it: should source TEAM_BROKER_API_SECRET from the shared EMQX secret when no dedicated credential is set + template: deployment.yaml + set: + forge.broker.enabled: true forge.broker.teamBroker: enabled: true - api: - url: "https://team-broker.example.com" - key: "team-broker-key" - secret: "team-broker-secret" asserts: - contains: path: spec.template.spec.initContainers[0].env @@ -108,28 +116,47 @@ tests: name: TEAM_BROKER_API_SECRET valueFrom: secretKeyRef: - name: flowfuse-secrets - key: teamBrokerApiSecret + name: emqx-config-secrets + key: api_key_secret optional: true - - it: should not include TEAM_BROKER_API_SECRET env var when teamBroker is enabled but api is missing + - it: should source TEAM_BROKER_API_SECRET from broker.existingSecret when set and no dedicated credential template: deployment.yaml set: + forge.broker.enabled: true forge.broker.teamBroker: enabled: true + broker.existingSecret: "my-emqx-secret" asserts: - - notExists: - path: spec.template.spec.initContainers[0].env[?(@.name == "TEAM_BROKER_API_SECRET")] + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: TEAM_BROKER_API_SECRET + valueFrom: + secretKeyRef: + name: my-emqx-secret + key: api_key_secret + optional: true - - it: should not include TEAM_BROKER_API_SECRET env var when teamBroker is enabled but api is empty + - it: should source TEAM_BROKER_API_SECRET from flowfuse-secrets when a dedicated credential is set template: deployment.yaml set: + forge.broker.enabled: true forge.broker.teamBroker: enabled: true - api: {} + api: + key: "team-broker-key" + secret: "team-broker-secret" asserts: - - notExists: - path: spec.template.spec.initContainers[0].env[?(@.name == "TEAM_BROKER_API_SECRET")] + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: TEAM_BROKER_API_SECRET + valueFrom: + secretKeyRef: + name: flowfuse-secrets + key: teamBrokerApiSecret + optional: true # Secrets tests - it: should not include teamBrokerApiSecret in secrets by default @@ -138,64 +165,64 @@ tests: - notExists: path: data.teamBrokerApiSecret - - it: should not include teamBrokerApiSecret in secrets when teamBroker is disabled + - it: should not include teamBrokerApiSecret when using the shared bootstrap key template: secrets.yaml set: + forge.broker.enabled: true forge.broker.teamBroker: - enabled: false + enabled: true asserts: - notExists: path: data.teamBrokerApiSecret - - it: should include teamBrokerApiSecret base64 encoded when teamBroker is enabled + - it: should include teamBrokerApiSecret base64 encoded when a dedicated credential is set template: secrets.yaml set: + forge.broker.enabled: true forge.broker.teamBroker: enabled: true api: - url: "https://team-broker.example.com" key: "team-broker-key" secret: "team-broker-secret" asserts: - isKind: of: Secret - - isNotNullOrEmpty: - path: data.teamBrokerApiSecret - equal: path: data.teamBrokerApiSecret value: dGVhbS1icm9rZXItc2VjcmV0 # base64("team-broker-secret") - - it: should fail when teamBroker is enabled but api.secret is missing + - it: should fail when api.key is set but api.secret is missing template: secrets.yaml set: + forge.broker.enabled: true forge.broker.teamBroker: enabled: true api: - url: "https://team-broker.example.com" key: "team-broker-key" asserts: - failedTemplate: - errorMessage: "A valid .Values.forge.broker.teamBroker.api.secret is required!" + errorMessage: "A valid .Values.forge.broker.teamBroker.api.secret is required when api.key is set!" - - it: should not include teamBrokerApiSecret when teamBroker is enabled but api block is missing + - it: should fail when api.secret is set but api.key is missing template: secrets.yaml set: + forge.broker.enabled: true forge.broker.teamBroker: enabled: true + api: + secret: "team-broker-secret" asserts: - - isKind: - of: Secret - - notExists: - path: data.teamBrokerApiSecret + - failedTemplate: + errorMessage: "A valid .Values.forge.broker.teamBroker.api.key is required when api.secret is set!" # Coexistence with other secrets - - it: should include teamBrokerApiSecret alongside other tokens when multiple features are enabled + - it: should include the dedicated teamBrokerApiSecret alongside other tokens when multiple features are enabled template: secrets.yaml set: + forge.broker.enabled: true forge.broker.teamBroker: enabled: true api: - url: "https://team-broker.example.com" key: "team-broker-key" secret: "team-broker-secret" forge.expert: @@ -218,15 +245,12 @@ tests: - isNotNullOrEmpty: path: data.token - - it: should include both EXPERT_TOKEN and TEAM_BROKER_API_SECRET env vars when both features are enabled + - it: should include both EXPERT_TOKEN and the shared TEAM_BROKER_API_SECRET env when both features are enabled template: deployment.yaml set: + forge.broker.enabled: true forge.broker.teamBroker: enabled: true - api: - url: "https://team-broker.example.com" - key: "team-broker-key" - secret: "team-broker-secret" forge.expert: enabled: true service: @@ -239,8 +263,8 @@ tests: name: TEAM_BROKER_API_SECRET valueFrom: secretKeyRef: - name: flowfuse-secrets - key: teamBrokerApiSecret + name: emqx-config-secrets + key: api_key_secret optional: true - contains: path: spec.template.spec.initContainers[0].env diff --git a/helm/flowfuse/tests/team_broker_helpers_test.yaml b/helm/flowfuse/tests/team_broker_helpers_test.yaml index 9c09cd64..34a9d69a 100644 --- a/helm/flowfuse/tests/team_broker_helpers_test.yaml +++ b/helm/flowfuse/tests/team_broker_helpers_test.yaml @@ -6,14 +6,13 @@ templates: set: forge.domain: "chart-unit-tests.com" tests: - # forge.teamBrokerSecretName helper (verified indirectly through secret name on the rendered Secret) - - it: should render flowfuse-secrets when teamBroker is enabled + # forge.teamBrokerApiSecretName / forge.teamBrokerApiSecretKey (verified via the rendered secret) + - it: should render flowfuse-secrets when a dedicated credential is provided template: secrets.yaml set: forge.broker.teamBroker: enabled: true api: - url: "https://team-broker.example.com" key: "team-broker-key" secret: "team-broker-secret" asserts: @@ -30,14 +29,23 @@ tests: - notExists: path: data.teamBrokerApiSecret + - it: should not produce teamBrokerApiSecret data when using the shared bootstrap key + template: secrets.yaml + set: + forge.broker.enabled: true + forge.broker.teamBroker: + enabled: true + asserts: + - notExists: + path: data.teamBrokerApiSecret + # forge.teamBrokerApiSecret helper - encoding - - it: should base64 encode team broker api secret correctly + - it: should base64 encode the dedicated team broker api secret correctly template: secrets.yaml set: forge.broker.teamBroker: enabled: true api: - url: "https://team-broker.example.com" key: "team-broker-key" secret: "my-team-broker-secret-123" asserts: @@ -46,14 +54,13 @@ tests: value: bXktdGVhbS1icm9rZXItc2VjcmV0LTEyMw== # base64("my-team-broker-secret-123") # Integration with external secrets - - it: should work alongside external postgresql secret + - it: should work alongside external postgresql secret when a dedicated credential is provided template: secrets.yaml set: postgresql.auth.existingSecret: "external-db-secret" forge.broker.teamBroker: enabled: true api: - url: "https://team-broker.example.com" key: "team-broker-key" secret: "team-broker-secret" asserts: @@ -66,7 +73,7 @@ tests: - notExists: path: data.postgres-password - - it: should not create the secret when teamBroker is disabled and external secrets are used + - it: should not create the secret when teamBroker uses the shared key and external secrets are used template: secrets.yaml set: postgresql.auth.existingSecret: "external-db-secret" @@ -74,48 +81,35 @@ tests: smtp: host: smtp.example.com existingSecret: "external-smtp-secret" + forge.broker.enabled: true forge.broker.teamBroker: - enabled: false + enabled: true asserts: - hasDocuments: count: 0 - # Helper error handling: api.key and api.secret are required together; api.url has a default - - it: should not require api.url when teamBroker is enabled and api.url is missing + # Helper error handling: api.key and api.secret are required together + - it: should require api.secret when api.key is set template: secrets.yaml set: forge.broker.teamBroker: enabled: true api: key: "team-broker-key" - secret: "team-broker-secret" - asserts: - - isNotNullOrEmpty: - path: data.teamBrokerApiSecret - - - it: should require api.key when teamBroker is enabled and api.key is missing - template: secrets.yaml - set: - forge.broker.teamBroker: - enabled: true - api: - url: "https://team-broker.example.com" - secret: "team-broker-secret" asserts: - failedTemplate: - errorMessage: "A valid .Values.forge.broker.teamBroker.api.key is required!" + errorMessage: "A valid .Values.forge.broker.teamBroker.api.secret is required when api.key is set!" - - it: should require api.secret when teamBroker is enabled and api.secret is missing + - it: should require api.key when api.secret is set template: secrets.yaml set: forge.broker.teamBroker: enabled: true api: - url: "https://team-broker.example.com" - key: "team-broker-key" + secret: "team-broker-secret" asserts: - failedTemplate: - errorMessage: "A valid .Values.forge.broker.teamBroker.api.secret is required!" + errorMessage: "A valid .Values.forge.broker.teamBroker.api.key is required when api.secret is set!" - it: should skip teamBrokerApiSecret rendering when teamBroker is enabled but api is empty template: secrets.yaml @@ -150,13 +144,10 @@ tests: forge.broker.enabled: true forge.broker.teamBroker: enabled: true - api: - key: "team-broker-key" - secret: "team-broker-secret" asserts: - matchRegex: path: data["flowforge.yml"] - pattern: "url: http://emqx-dashboard\\.my-namespace:18083" + pattern: "url: http://emqx-dashboard\\.my-namespace:18083/api/v5" - it: should use user-provided api.url when set template: configmap.yaml @@ -177,3 +168,15 @@ tests: - notMatchRegex: path: data["flowforge.yml"] pattern: "url: http://emqx-dashboard" + + # forge.teamBrokerApiKey helper - verified indirectly through the rendered configmap + - it: should default api key name to flowfuse when not provided + template: configmap.yaml + set: + forge.broker.enabled: true + forge.broker.teamBroker: + enabled: true + asserts: + - matchRegex: + path: data["flowforge.yml"] + pattern: "key: flowfuse"