diff --git a/helm/flowfuse/README.md b/helm/flowfuse/README.md index ebc38a08..e9ef3df5 100644 --- a/helm/flowfuse/README.md +++ b/helm/flowfuse/README.md @@ -114,6 +114,9 @@ To use STMP to send email - `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.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.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 @@ -304,7 +307,9 @@ Everything under `forge.rate_limits` is used as input to Fastify Rate Limit plug - `forge.expert.enabled` Enable/disable the FlowFuse Expert feature (default `false`) - `forge.expert.service.url` URL for the FlowFuse Expert service (default not set) - `forge.expert.service.token` Token for the FlowFuse Expert service (default not set) - - `forge.expert.service.requestTimeout` Timeout for the FlowFuse Expert service (default `60000`) + - `forge.expert.service.requestTimeout` Timeout for the FlowFuse Expert service (default `60000`) + - `forge.expert.broker.address` Address of the MQTT broker to use for communication with the Expert service (default not set). Requires `forge.broker.teamBroker.enabled=true`, since the local team broker bridges to this central broker. + - `forge.expert.broker.port` Port of the MQTT broker to use for communication with the Expert service (default `8883`) ### Ingress - `ingress.annotations` ingress annotations (default is `{}`). This value is also applied to Editor instances created by FlowFuse. diff --git a/helm/flowfuse/templates/_helpers.tpl b/helm/flowfuse/templates/_helpers.tpl index de41b3a4..fd38c278 100644 --- a/helm/flowfuse/templates/_helpers.tpl +++ b/helm/flowfuse/templates/_helpers.tpl @@ -122,10 +122,11 @@ Note: The value for key .Values.postgresql.auth.existingSecret is inherited from */}} {{- define "forge.createSecret" -}} -{{- if not (and .Values.postgresql.auth.existingSecret +{{- if not (and .Values.postgresql.auth.existingSecret (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.expert).enabled)) + (not ((.Values.forge.broker.teamBroker).enabled))) -}} true {{- else -}} false @@ -346,4 +347,35 @@ Get the name from the release name. */}} {{- define "forge.name" -}} {{- .Release.Name -}} +{{- end -}} + +{{/* +Get the secret object name with Team Broker secret. +*/}} +{{- define "forge.teamBrokerSecretName" -}} +{{- if (.Values.forge.broker.teamBroker).enabled -}} + {{- printf "flowfuse-secrets" -}} +{{- end -}} +{{- end -}} + +{{/* +Resolve Team Broker API URL: user-provided value, or default to the in-cluster EMQX dashboard service. +*/}} +{{- define "forge.teamBrokerApiUrl" -}} +{{- if ((.Values.forge.broker.teamBroker).api).url -}} + {{- .Values.forge.broker.teamBroker.api.url -}} +{{- else -}} + {{- printf "http://emqx-dashboard.%s:18083" .Release.Namespace -}} +{{- end -}} +{{- end -}} + +{{/* +Create Team Broker API secret +*/}} +{{- define "forge.teamBrokerApiSecret" -}} +{{- if (.Values.forge.broker.teamBroker).enabled -}} +{{- $_ := 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 -}} +teamBrokerApiSecret: {{ $token | b64enc | quote }} +{{- end -}} {{- end -}} \ No newline at end of file diff --git a/helm/flowfuse/templates/configmap.yaml b/helm/flowfuse/templates/configmap.yaml index a14907de..57a30578 100644 --- a/helm/flowfuse/templates/configmap.yaml +++ b/helm/flowfuse/templates/configmap.yaml @@ -224,6 +224,10 @@ data: teamBroker: enabled: true host: {{ include "forge.teamBrokerHost" . }} + api: + url: {{ include "forge.teamBrokerApiUrl" . }} + key: {{ .Values.forge.broker.teamBroker.api.key }} + secret: <%= ENV['TEAM_BROKER_API_SECRET'] %> {{ end -}} {{- end }} logging: @@ -334,6 +338,13 @@ data: token: <%= ENV['EXPERT_TOKEN'] %> url: {{ ((.Values.forge.expert).service).url }} requestTimeout: {{ .Values.forge.expert.requestTimeout | default 60000 }} + {{- if ((.Values.forge.expert).broker).address }} + {{- if not ((.Values.forge.broker.teamBroker).enabled) }} + {{- fail "forge.expert.broker requires the Team Broker to be enabled (forge.broker.teamBroker.enabled=true)" -}} + {{- end }} + centralBroker: + server: {{ printf "%s:%v" .Values.forge.expert.broker.address (.Values.forge.expert.broker.port | default 8883) }} + {{- end }} {{- end }} {{- end }} {{- if and (hasKey .Values.forge "npmRegistry") (hasKey .Values.forge.npmRegistry "enabled") }} diff --git a/helm/flowfuse/templates/deployment.yaml b/helm/flowfuse/templates/deployment.yaml index 2cbaf6df..27c121b7 100644 --- a/helm/flowfuse/templates/deployment.yaml +++ b/helm/flowfuse/templates/deployment.yaml @@ -90,6 +90,14 @@ spec: key: expertToken optional: true {{- end }} + {{- if (.Values.forge.broker.teamBroker).enabled }} + - name: TEAM_BROKER_API_SECRET + valueFrom: + secretKeyRef: + name: {{ include "forge.teamBrokerSecretName" . }} + key: teamBrokerApiSecret + optional: true + {{- end }} {{- if .Values.forge.localPostgresql }} - name: wait-for-local-db {{- $initWaitForLocalDbRegistry := (or .Values.forge.initContainers.waitForLocalDb.image.registry .Values.forge.registry) }} diff --git a/helm/flowfuse/templates/secrets.yaml b/helm/flowfuse/templates/secrets.yaml index 28cdc08e..e8626cfd 100644 --- a/helm/flowfuse/templates/secrets.yaml +++ b/helm/flowfuse/templates/secrets.yaml @@ -20,4 +20,7 @@ data: {{- if (include "forge.expertToken" . | trim) }} {{- include "forge.expertToken" . | nindent 2 -}} {{- end }} + {{- if (include "forge.teamBrokerApiSecret" . | trim) }} + {{- include "forge.teamBrokerApiSecret" . | nindent 2 -}} + {{- end }} {{- end }} \ No newline at end of file diff --git a/helm/flowfuse/tests/expert_central_broker_test.yaml b/helm/flowfuse/tests/expert_central_broker_test.yaml new file mode 100644 index 00000000..9606b374 --- /dev/null +++ b/helm/flowfuse/tests/expert_central_broker_test.yaml @@ -0,0 +1,123 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/helm-unittest/helm-unittest/main/schema/helm-testsuite.json +suite: test expert central broker configuration +templates: + - configmap.yaml +set: + forge.domain: "chart-unit-tests.com" +tests: + - it: should not include central broker block when expert is disabled + template: configmap.yaml + set: + forge.expert: + enabled: false + asserts: + - notMatchRegex: + path: data["flowforge.yml"] + pattern: "centralBroker:" + + - it: should not include central broker block when expert key is absent + template: configmap.yaml + asserts: + - notMatchRegex: + path: data["flowforge.yml"] + pattern: "centralBroker:" + + - it: should not include central broker block when expert is enabled but broker.address is not set + template: configmap.yaml + set: + forge.expert: + enabled: true + service: + url: "https://expert.example.com" + token: "expert-token" + asserts: + - matchRegex: + path: data["flowforge.yml"] + pattern: "expert:" + - notMatchRegex: + path: data["flowforge.yml"] + pattern: "centralBroker:" + + - it: should render central broker server when expert is enabled with broker address and port + template: configmap.yaml + set: + forge.broker.teamBroker.enabled: true + forge.expert: + enabled: true + service: + url: "https://expert.example.com" + token: "expert-token" + broker: + address: "central-broker.example.com" + port: 1883 + asserts: + - matchRegex: + path: data["flowforge.yml"] + pattern: "centralBroker:" + - matchRegex: + path: data["flowforge.yml"] + pattern: "server: central-broker\\.example\\.com:1883" + + - it: should render central broker server with custom port + template: configmap.yaml + set: + forge.broker.teamBroker.enabled: true + forge.expert: + enabled: true + service: + url: "https://expert.example.com" + token: "expert-token" + broker: + address: "10.0.0.5" + port: 1883 + asserts: + - matchRegex: + path: data["flowforge.yml"] + pattern: "server: 10\\.0\\.0\\.5:1883" + + - it: should default the central broker port to 8883 when not provided + template: configmap.yaml + set: + forge.broker.teamBroker.enabled: true + forge.expert: + enabled: true + service: + url: "https://expert.example.com" + token: "expert-token" + broker: + address: "central-broker.example.com" + asserts: + - matchRegex: + path: data["flowforge.yml"] + pattern: "server: central-broker\\.example\\.com:8883" + + - it: should fail when expert broker address is set but team broker is not enabled + template: configmap.yaml + set: + forge.expert: + enabled: true + service: + url: "https://expert.example.com" + token: "expert-token" + broker: + address: "central-broker.example.com" + port: 1883 + asserts: + - failedTemplate: + errorMessage: "forge.expert.broker requires the Team Broker to be enabled (forge.broker.teamBroker.enabled=true)" + + - it: should fail when expert broker address is set and team broker is explicitly disabled + template: configmap.yaml + set: + forge.broker.teamBroker.enabled: false + forge.expert: + enabled: true + service: + url: "https://expert.example.com" + token: "expert-token" + broker: + address: "central-broker.example.com" + port: 1883 + asserts: + - failedTemplate: + errorMessage: "forge.expert.broker requires the Team Broker to be enabled (forge.broker.teamBroker.enabled=true)" diff --git a/helm/flowfuse/tests/team_broker_api_test.yaml b/helm/flowfuse/tests/team_broker_api_test.yaml new file mode 100644 index 00000000..005227f1 --- /dev/null +++ b/helm/flowfuse/tests/team_broker_api_test.yaml @@ -0,0 +1,200 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/helm-unittest/helm-unittest/main/schema/helm-testsuite.json +suite: test team broker API configuration +templates: + - configmap.yaml + - deployment.yaml + - secrets.yaml +set: + forge.domain: "chart-unit-tests.com" +tests: + - it: should not include teamBroker api block when teamBroker is disabled by default + template: configmap.yaml + asserts: + - notMatchRegex: + path: data["flowforge.yml"] + pattern: "teamBroker:" + - notMatchRegex: + path: data["flowforge.yml"] + pattern: "TEAM_BROKER_API_SECRET" + + - it: should render teamBroker.api block with url, key and secret when enabled + template: configmap.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: + - 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" + + # Deployment tests + - it: should not include TEAM_BROKER_API_SECRET env var by default + template: deployment.yaml + asserts: + - notExists: + path: spec.template.spec.initContainers[0].env[?(@.name == "TEAM_BROKER_API_SECRET")] + + - it: should not include TEAM_BROKER_API_SECRET env var when teamBroker is disabled + template: deployment.yaml + set: + 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 + template: deployment.yaml + set: + 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 + 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 + template: secrets.yaml + asserts: + - notExists: + path: data.teamBrokerApiSecret + + - it: should not include teamBrokerApiSecret in secrets when teamBroker is disabled + template: secrets.yaml + set: + forge.broker.teamBroker: + enabled: false + asserts: + - notExists: + path: data.teamBrokerApiSecret + + - it: should include teamBrokerApiSecret base64 encoded when teamBroker is enabled + 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: + - 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 + template: secrets.yaml + set: + 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!" + + - it: should fail when teamBroker is enabled but api block is missing + template: secrets.yaml + set: + forge.broker.teamBroker: + enabled: true + asserts: + - failedTemplate: + errorMessage: "A valid .Values.forge.broker.teamBroker.api.key is required!" + + # Coexistence with other secrets + - it: should include teamBrokerApiSecret alongside other tokens when multiple features are enabled + template: secrets.yaml + set: + 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: + url: "https://expert.example.com" + token: "expert-token" + forge.assistant: + enabled: true + service: + url: "https://assistant.example.com" + token: "assistant-token" + asserts: + - isKind: + of: Secret + - isNotNullOrEmpty: + path: data.teamBrokerApiSecret + - isNotNullOrEmpty: + path: data.expertToken + - isNotNullOrEmpty: + path: data.token + + - it: should include both EXPERT_TOKEN and TEAM_BROKER_API_SECRET env vars when both features are enabled + template: deployment.yaml + set: + 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: + url: "https://expert.example.com" + token: "expert-token" + asserts: + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: TEAM_BROKER_API_SECRET + valueFrom: + secretKeyRef: + name: flowfuse-secrets + key: teamBrokerApiSecret + optional: true + - contains: + path: spec.template.spec.initContainers[0].env + content: + name: EXPERT_TOKEN + valueFrom: + secretKeyRef: + name: flowfuse-secrets + key: expertToken + optional: true diff --git a/helm/flowfuse/tests/team_broker_helpers_test.yaml b/helm/flowfuse/tests/team_broker_helpers_test.yaml new file mode 100644 index 00000000..0ef033d4 --- /dev/null +++ b/helm/flowfuse/tests/team_broker_helpers_test.yaml @@ -0,0 +1,175 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/helm-unittest/helm-unittest/main/schema/helm-testsuite.json +suite: test team broker helper functions +templates: + - secrets.yaml + - configmap.yaml +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 + 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: + - equal: + path: metadata.name + value: flowfuse-secrets + + - it: should not produce teamBrokerApiSecret data when teamBroker is disabled + template: secrets.yaml + set: + forge.broker.teamBroker: + enabled: false + asserts: + - notExists: + path: data.teamBrokerApiSecret + + # forge.teamBrokerApiSecret helper - encoding + - it: should base64 encode 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: + - equal: + path: data.teamBrokerApiSecret + value: bXktdGVhbS1icm9rZXItc2VjcmV0LTEyMw== # base64("my-team-broker-secret-123") + + # Integration with external secrets + - it: should work alongside external postgresql secret + 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: + - isKind: + of: Secret + - isNotNullOrEmpty: + path: data.teamBrokerApiSecret + - notExists: + path: data.password + - notExists: + path: data.postgres-password + + - it: should not create the secret when teamBroker is disabled and external secrets are used + template: secrets.yaml + set: + postgresql.auth.existingSecret: "external-db-secret" + forge.email: + smtp: + host: smtp.example.com + existingSecret: "external-smtp-secret" + forge.broker.teamBroker: + enabled: false + 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 + 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!" + + - it: should require api.secret when teamBroker is enabled and api.secret is missing + template: secrets.yaml + set: + 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!" + + - it: should require api fields when teamBroker is enabled but api is empty + template: secrets.yaml + set: + forge.broker.teamBroker: + enabled: true + api: {} + asserts: + - failedTemplate: + errorMessage: "A valid .Values.forge.broker.teamBroker.api.key is required!" + + - it: should require api fields when teamBroker is enabled but api is missing + template: secrets.yaml + set: + forge.broker.teamBroker: + enabled: true + # api is missing + asserts: + - failedTemplate: + errorMessage: "A valid .Values.forge.broker.teamBroker.api.key is required!" + + # forge.teamBrokerApiUrl helper - verified indirectly through the rendered configmap + - it: should default api.url to in-cluster EMQX dashboard when not provided + template: configmap.yaml + release: + namespace: my-namespace + set: + 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" + + - it: should use user-provided api.url when set + template: configmap.yaml + release: + namespace: my-namespace + 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: + - matchRegex: + path: data["flowforge.yml"] + pattern: "url: https://team-broker\\.example\\.com" + - notMatchRegex: + path: data["flowforge.yml"] + pattern: "url: http://emqx-dashboard" diff --git a/helm/flowfuse/values.schema.json b/helm/flowfuse/values.schema.json index e5f2d5c4..5841984e 100644 --- a/helm/flowfuse/values.schema.json +++ b/helm/flowfuse/values.schema.json @@ -299,6 +299,21 @@ "properties": { "enabled": { "type": "boolean" + }, + "api": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "key": { + "type": "string" + }, + "secret": { + "type": "string" + } + } } } }, @@ -1021,6 +1036,19 @@ "type": "integer" } } + }, + "broker": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + } } } },