Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions helm/flowfuse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,77 @@ readinessProbe:
failureThreshold: 3
```

### Ingress Migration Tool

To help with migration from the Ingress-Nginx controller to the Traefik Ingress controller, this chart can run an Ingress migration tool as a Kubernetes Job.

When enabled, the chart creates a Helm hook Job (plus RBAC) that operates on Ingress resources created by FlowFuse in the following namespaces:

- Project namespace (`forge.projectNamespace`)
- Release namespace (the namespace where you install/upgrade the chart) - only in `copy` mode

The job behavior is controlled by values under the `ingressMigration` block:

- `ingressMigration.enabled` Enable/disable the migration job (default `false`).
- `ingressMigration.mode` Mode to run the tool in: `copy` or `cleanup` (default `copy`).
- `ingressMigration.newIngressClassName` IngressClass name to set on migrated Ingress objects (default `traefik`).
- `ingressMigration.oldIngressClassName` Old IngressClass name to target in `cleanup` mode (required when `mode: cleanup`).
- `ingressMigration.nameSuffix` Suffix appended to migrated Ingress names (default `-tfk`).
- `ingressMigration.dryRun` If `true`, the job runs in dry-run mode (default `true`).

Helm hook controls:

- `ingressMigration.helmHook` Helm hook phase to run the job in (default `post-upgrade`).
- `ingressMigration.hookWeight` Hook weight used to order hook execution (default `0`).
- `ingressMigration.hookDeletePolicy` Hook delete policy applied when `dryRun: false` (default `before-hook-creation,hook-succeeded`).

Job controls:

- `ingressMigration.backoffLimit` Job backoff limit (default `3`).
- `ingressMigration.ttlSecondsAfterFinished` How long to keep the Job after completion when `dryRun: false` (default `300`).

Image controls:

- `ingressMigration.image.repository` Tool image repository (default `docker.io/devopswizard/ingress-migration-tool`).
- `ingressMigration.image.tag` Tool image tag (default `latest`).
- `ingressMigration.image.pullPolicy` Image pull policy (default `IfNotPresent`).

Pod scheduling and security:

- `ingressMigration.resources` Pod resources (requests/limits).
- `ingressMigration.securityContext` Container securityContext.
- `ingressMigration.nodeSelector` Node selector (default `role: management`).
- `ingressMigration.tolerations` Tolerations list (default `[]`).

Example: run in `copy` mode first (dry-run), then re-run to apply changes:

```yaml
ingressMigration:
enabled: true
mode: copy
newIngressClassName: traefik
nameSuffix: "-tfk"
dryRun: true
```

Then set `dryRun: false` and run `helm upgrade` again.

Example: cleanup old Ingress objects after switching traffic:

```yaml
ingressMigration:
enabled: true
mode: cleanup
oldIngressClassName: nginx
newIngressClassName: traefik
nameSuffix: "-tfk"
dryRun: true
```

Then set `dryRun: false` and run `helm upgrade` again.



## Upgrading Chart

### Generic upgrade instructions
Expand Down
7 changes: 7 additions & 0 deletions helm/flowfuse/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,10 @@ Get the valkey port number.
{{- .Values.valkey.port }}
{{- end -}}
{{- end -}}

{{/*
Get the name from the release name.
*/}}
{{- define "forge.name" -}}
{{- .Release.Name -}}
{{- end -}}
141 changes: 141 additions & 0 deletions helm/flowfuse/templates/ingress-migration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
{{- if .Values.ingressMigration.enabled }}
{{- $targetNamespaces := list }}
{{- if eq (.Values.ingressMigration.mode | default "copy") "cleanup" }}
{{- $targetNamespaces = list (dict "name" "project" "namespace" .Values.forge.projectNamespace "weight" "0") }}
{{- else }}
{{- $targetNamespaces = list (dict "name" "project" "namespace" .Values.forge.projectNamespace "weight" "0") (dict "name" "release" "namespace" .Release.Namespace "weight" "1") }}
{{- end }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "forge.name" . }}-ingress-migration
namespace: {{ .Release.Namespace }}
annotations:
"helm.sh/hook": {{ .Values.ingressMigration.helmHook | default "post-upgrade" }}
"helm.sh/hook-weight": {{ .Values.ingressMigration.hookWeight | default "-5" | quote }}
{{- if .Values.ingressMigration.dryRun }}
"helm.sh/hook-delete-policy": "before-hook-creation"
{{- else }}
"helm.sh/hook-delete-policy": {{ .Values.ingressMigration.hookDeletePolicy | default "before-hook-creation,hook-succeeded" }}
{{- end }}
labels:
{{- include "forge.labels" . | nindent 4 }}
app.kubernetes.io/component: ingress-migration-job

{{- range $targetNamespaces }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "forge.name" $ }}-ingress-migration-{{ .name }}
namespace: {{ .namespace }}
annotations:
"helm.sh/hook": {{ $.Values.ingressMigration.helmHook | default "post-upgrade" }}
"helm.sh/hook-weight": {{ $.Values.ingressMigration.hookWeight | default "-5" | quote }}
{{- if $.Values.ingressMigration.dryRun }}
"helm.sh/hook-delete-policy": "before-hook-creation"
{{- else }}
"helm.sh/hook-delete-policy": {{ $.Values.ingressMigration.hookDeletePolicy | default "before-hook-creation,hook-succeeded" }}
{{- end }}
labels:
{{- include "forge.labels" $ | nindent 4 }}
app.kubernetes.io/component: ingress-migration-job
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "create", "patch", "delete"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "forge.name" $ }}-ingress-migration-{{ .name }}
namespace: {{ .namespace }}
annotations:
"helm.sh/hook": {{ $.Values.ingressMigration.helmHook | default "post-upgrade" }}
"helm.sh/hook-weight": {{ $.Values.ingressMigration.hookWeight | default "-5" | quote }}
{{- if $.Values.ingressMigration.dryRun }}
"helm.sh/hook-delete-policy": "before-hook-creation"
{{- else }}
"helm.sh/hook-delete-policy": {{ $.Values.ingressMigration.hookDeletePolicy | default "before-hook-creation,hook-succeeded" }}
{{- end }}
labels:
{{- include "forge.labels" $ | nindent 4 }}
app.kubernetes.io/component: ingress-migration-job
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "forge.name" $ }}-ingress-migration-{{ .name }}
subjects:
- kind: ServiceAccount
name: {{ include "forge.name" $ }}-ingress-migration
namespace: {{ $.Release.Namespace }}

---
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "forge.name" $ }}-ingress-migration-{{ .name }}
namespace: {{ $.Release.Namespace }}
annotations:
"helm.sh/hook": {{ $.Values.ingressMigration.helmHook | default "post-upgrade" }}
"helm.sh/hook-weight": {{ .weight | quote }}
{{- if $.Values.ingressMigration.dryRun }}
"helm.sh/hook-delete-policy": "before-hook-creation"
{{- else }}
"helm.sh/hook-delete-policy": {{ $.Values.ingressMigration.hookDeletePolicy | default "before-hook-creation,hook-succeeded" }}
{{- end }}
labels:
{{- include "forge.labels" $ | nindent 4 }}
app.kubernetes.io/component: ingress-migration-job
spec:
backoffLimit: {{ $.Values.ingressMigration.backoffLimit | default 3 }}
{{- if not $.Values.ingressMigration.dryRun }}
ttlSecondsAfterFinished: {{ $.Values.ingressMigration.ttlSecondsAfterFinished | default 300 }}
{{- end }}
template:
metadata:
labels:
{{- include "forge.commonSelectorLabels" $ | nindent 8 }}
app.kubernetes.io/component: ingress-migration-job
spec:
serviceAccountName: {{ include "forge.name" $ }}-ingress-migration
restartPolicy: OnFailure
{{- with $.Values.ingressMigration.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $.Values.ingressMigration.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: ingress-copy
image: {{ $.Values.ingressMigration.image.repository }}:{{ $.Values.ingressMigration.image.tag | default "latest" }}
imagePullPolicy: {{ $.Values.ingressMigration.image.pullPolicy | default "IfNotPresent" }}
env:
- name: MODE
value: {{ $.Values.ingressMigration.mode | default "copy" | quote }}
- name: TARGET_NAMESPACE
value: {{ .namespace | quote }}
{{- if eq ($.Values.ingressMigration.mode | default "copy") "cleanup" }}
- name: OLD_INGRESS_CLASS
value: {{ $.Values.ingressMigration.oldIngressClassName | quote }}
{{- end }}
- name: NEW_INGRESS_CLASS
value: {{ $.Values.ingressMigration.newIngressClassName | quote }}
- name: NAME_SUFFIX
value: {{ $.Values.ingressMigration.nameSuffix | default "-copy" | quote }}
- name: DRY_RUN
value: {{ $.Values.ingressMigration.dryRun | default false | quote }}
{{- with $.Values.ingressMigration.resources }}
resources:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with $.Values.ingressMigration.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- end }}
{{- end }}
159 changes: 159 additions & 0 deletions helm/flowfuse/tests/ingress_migration_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/helm-unittest/helm-unittest/main/schema/helm-testsuite.json
suite: test ingress migration tool
templates:
- ingress-migration.yaml
release:
name: flowfuse
namespace: default
set:
forge.domain: "chart-unit-tests.com"

tests:
- it: should not render anything when disabled
asserts:
- hasDocuments:
count: 0

- it: should render serviceaccount, rbac and two jobs in copy mode (dry-run)
set:
ingressMigration:
enabled: true
mode: copy
dryRun: true
newIngressClassName: "traefik"
nameSuffix: "-tfk"
asserts:
- hasDocuments:
count: 7
- containsDocument:
apiVersion: v1
kind: ServiceAccount
name: flowfuse-ingress-migration
any: true
- containsDocument:
apiVersion: batch/v1
kind: Job
name: flowfuse-ingress-migration-project
any: true
- containsDocument:
apiVersion: batch/v1
kind: Job
name: flowfuse-ingress-migration-release
any: true

- it: should set correct env vars for project job in copy mode and omit cleanup-only env/ttl
set:
ingressMigration:
enabled: true
mode: copy
dryRun: true
newIngressClassName: "traefik"
nameSuffix: "-tfk"
documentSelector:
path: spec.template.spec.containers[0].env[?(@.name == "TARGET_NAMESPACE")].value
value: "flowforge"
asserts:
- isKind:
of: Job
- contains:
path: spec.template.spec.containers[0].env
content:
name: MODE
value: "copy"
- contains:
path: spec.template.spec.containers[0].env
content:
name: TARGET_NAMESPACE
value: "flowforge"
- contains:
path: spec.template.spec.containers[0].env
content:
name: NEW_INGRESS_CLASS
value: "traefik"
- contains:
path: spec.template.spec.containers[0].env
content:
name: NAME_SUFFIX
value: "-tfk"
- contains:
path: spec.template.spec.containers[0].env
content:
name: DRY_RUN
value: "true"
- notExists:
path: spec.template.spec.containers[0].env[?(@.name == "OLD_INGRESS_CLASS")]
- notExists:
path: spec.ttlSecondsAfterFinished

- it: should render single job in cleanup mode and include OLD_INGRESS_CLASS
set:
ingressMigration:
enabled: true
mode: cleanup
dryRun: true
oldIngressClassName: "nginx"
newIngressClassName: "traefik"
nameSuffix: "-tfk"
asserts:
- hasDocuments:
count: 4
- containsDocument:
apiVersion: batch/v1
kind: Job
name: flowfuse-ingress-migration-project
any: true

- it: should set correct env vars for project job in cleanup mode
set:
ingressMigration:
enabled: true
mode: cleanup
dryRun: true
oldIngressClassName: "nginx"
newIngressClassName: "traefik"
nameSuffix: "-tfk"
documentSelector:
path: spec.template.spec.containers[0].env[?(@.name == "TARGET_NAMESPACE")].value
value: "flowforge"
asserts:
- isKind:
of: Job
- contains:
path: spec.template.spec.containers[0].env
content:
name: MODE
value: "cleanup"
- contains:
path: spec.template.spec.containers[0].env
content:
name: OLD_INGRESS_CLASS
value: "nginx"
- contains:
path: spec.template.spec.containers[0].env
content:
name: NEW_INGRESS_CLASS
value: "traefik"
- contains:
path: spec.template.spec.containers[0].env
content:
name: TARGET_NAMESPACE
value: "flowforge"

- it: should set ttlSecondsAfterFinished and use hookDeletePolicy when not dry-run
set:
ingressMigration:
enabled: true
mode: copy
dryRun: false
hookDeletePolicy: "hook-succeeded"
ttlSecondsAfterFinished: 123
documentSelector:
path: spec.template.spec.containers[0].env[?(@.name == "TARGET_NAMESPACE")].value
value: "flowforge"
asserts:
- equal:
path: metadata.annotations["helm.sh/hook-delete-policy"]
value: "hook-succeeded"
- equal:
path: spec.ttlSecondsAfterFinished
value: 123
Loading
Loading