From 39a59d7e55b882d04a32b0f89468ea117c07adbc Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 16 Sep 2026 09:48:37 +0200 Subject: [PATCH 1/4] Add official Helm chart for nuts-admin Adds charts/nuts-admin with Deployment, Service, ConfigMap (config.yaml), optional Ingress, and NUTS_NODE_ADDRESS/NUTS_CONFIGFILE env wiring, following the same publish pattern as nuts-node-chart: versioned independently under charts/, released via chart-releaser to GitHub Pages on push to main. Lets nuts-knooppunt depend on this chart directly instead of maintaining its own copy. Also anchors the "nuts-admin" gitignore entry to the repo root so it only matches the built binary, not the new charts/nuts-admin directory. Assisted by AI --- .github/workflows/helm-chart-release.yaml | 43 +++++++++ .gitignore | 3 +- charts/nuts-admin/.helmignore | 23 +++++ charts/nuts-admin/Chart.yaml | 14 +++ charts/nuts-admin/README.md | 40 +++++++++ charts/nuts-admin/templates/NOTES.txt | 24 +++++ charts/nuts-admin/templates/_helpers.tpl | 62 +++++++++++++ charts/nuts-admin/templates/configmap.yaml | 9 ++ charts/nuts-admin/templates/deployment.yaml | 90 +++++++++++++++++++ charts/nuts-admin/templates/hpa.yaml | 32 +++++++ charts/nuts-admin/templates/ingress.yaml | 43 +++++++++ charts/nuts-admin/templates/service.yaml | 15 ++++ .../nuts-admin/templates/serviceaccount.yaml | 13 +++ charts/nuts-admin/values.yaml | 74 +++++++++++++++ 14 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/helm-chart-release.yaml create mode 100644 charts/nuts-admin/.helmignore create mode 100644 charts/nuts-admin/Chart.yaml create mode 100644 charts/nuts-admin/README.md create mode 100644 charts/nuts-admin/templates/NOTES.txt create mode 100644 charts/nuts-admin/templates/_helpers.tpl create mode 100644 charts/nuts-admin/templates/configmap.yaml create mode 100644 charts/nuts-admin/templates/deployment.yaml create mode 100644 charts/nuts-admin/templates/hpa.yaml create mode 100644 charts/nuts-admin/templates/ingress.yaml create mode 100644 charts/nuts-admin/templates/service.yaml create mode 100644 charts/nuts-admin/templates/serviceaccount.yaml create mode 100644 charts/nuts-admin/values.yaml diff --git a/.github/workflows/helm-chart-release.yaml b/.github/workflows/helm-chart-release.yaml new file mode 100644 index 0000000..b4ea5a8 --- /dev/null +++ b/.github/workflows/helm-chart-release.yaml @@ -0,0 +1,43 @@ +name: Release Charts + +on: + push: + branches: + - 'main' + paths: + - 'charts/**' + +# deny-all default; the job below grants only the scopes it needs +permissions: {} + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v5 + with: + version: v3.8.0 + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.7.0 + with: + mark_as_latest: false + env: + CR_CHARTS_DIR: charts + CR_PAGES_BRANCH: gh-pages + CR_SKIP_EXISTING: true + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.gitignore b/.gitignore index ba2b68a..8dbe883 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,8 @@ web/dist data # Binaries -nuts-admin +/nuts-admin # mac .DS_Store +.scratch/ diff --git a/charts/nuts-admin/.helmignore b/charts/nuts-admin/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/nuts-admin/.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/nuts-admin/Chart.yaml b/charts/nuts-admin/Chart.yaml new file mode 100644 index 0000000..d4ad734 --- /dev/null +++ b/charts/nuts-admin/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: nuts-admin-chart +description: A Helm chart for nuts-admin, the management UI for a Nuts node + +type: application + +# 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. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. +appVersion: "6.5.2" diff --git a/charts/nuts-admin/README.md b/charts/nuts-admin/README.md new file mode 100644 index 0000000..3f16fd0 --- /dev/null +++ b/charts/nuts-admin/README.md @@ -0,0 +1,40 @@ +# Helm Chart for nuts-admin + +This chart deploys [nuts-admin](https://github.com/nuts-foundation/nuts-admin), the management UI for a Nuts node, on a Kubernetes cluster. + +It creates a Deployment, Service, ConfigMap (rendering `config.yaml`) and, optionally, an Ingress. It does not run or manage a Nuts node itself; point it at an existing one through `env.nutsNodeAddress`. + +## Configuration + +All configurable properties can be found in [./values.yaml](./values.yaml). The most relevant ones: + +| Property | Description | Default | +|----------------------|--------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| `env.nutsNodeAddress` | Internal API address of the Nuts node this instance manages, injected as `NUTS_NODE_ADDRESS`. | `http://localhost:8081` | +| `config` | Rendered into `config.yaml`, mounted into the pod and referenced via `NUTS_CONFIGFILE`. See the [nuts-admin README](../../README.md) and [deploy/config.yaml.example](../../deploy/config.yaml.example) for the options it accepts, e.g. `oidc` and `credentialprofiles`. | `{}` | +| `ingress.enabled` | Expose nuts-admin through an Ingress. | `false` | + +## Installing nuts-admin + +### From source + +Execute the following command from the root of the chart folder. Replace `` with the name you wish to give this Helm installation. + +```shell +helm install . +``` + +### From the Nuts Helm repo + +Add the repo: + +```shell +helm repo add nuts-admin https://nuts-foundation.github.io/nuts-admin/ +helm repo update +``` + +Then install it, optionally overriding values with your own `values.yaml`: + +```shell +helm install -f values.yaml nuts-admin/nuts-admin-chart +``` diff --git a/charts/nuts-admin/templates/NOTES.txt b/charts/nuts-admin/templates/NOTES.txt new file mode 100644 index 0000000..57c2b70 --- /dev/null +++ b/charts/nuts-admin/templates/NOTES.txt @@ -0,0 +1,24 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "nuts-admin.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "nuts-admin.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "nuts-admin.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "nuts-admin.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:{{ .Values.service.port }} to use nuts-admin" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME {{ .Values.service.port }}:$CONTAINER_PORT +{{- end }} + +2. Make sure `env.nutsNodeAddress` points at the internal API of the Nuts node this instance should manage. diff --git a/charts/nuts-admin/templates/_helpers.tpl b/charts/nuts-admin/templates/_helpers.tpl new file mode 100644 index 0000000..b66d722 --- /dev/null +++ b/charts/nuts-admin/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nuts-admin.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "nuts-admin.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .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 }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nuts-admin.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nuts-admin.labels" -}} +helm.sh/chart: {{ include "nuts-admin.chart" . }} +{{ include "nuts-admin.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nuts-admin.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nuts-admin.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "nuts-admin.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "nuts-admin.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/nuts-admin/templates/configmap.yaml b/charts/nuts-admin/templates/configmap.yaml new file mode 100644 index 0000000..483d444 --- /dev/null +++ b/charts/nuts-admin/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "nuts-admin.fullname" . }}-config + labels: + {{- include "nuts-admin.labels" . | nindent 4 }} +data: + config.yaml: | + {{- .Values.config | toYaml | nindent 4 }} diff --git a/charts/nuts-admin/templates/deployment.yaml b/charts/nuts-admin/templates/deployment.yaml new file mode 100644 index 0000000..254da6b --- /dev/null +++ b/charts/nuts-admin/templates/deployment.yaml @@ -0,0 +1,90 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nuts-admin.fullname" . }} + labels: + {{- include "nuts-admin.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "nuts-admin.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "nuts-admin.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nuts-admin.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: NUTS_NODE_ADDRESS + value: {{ .Values.env.nutsNodeAddress | quote }} + - name: NUTS_CONFIGFILE + value: /app/config.yaml + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: config + mountPath: /app/config.yaml + subPath: config.yaml + readOnly: true + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: config + configMap: + name: {{ include "nuts-admin.fullname" . }}-config + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/nuts-admin/templates/hpa.yaml b/charts/nuts-admin/templates/hpa.yaml new file mode 100644 index 0000000..4141415 --- /dev/null +++ b/charts/nuts-admin/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "nuts-admin.fullname" . }} + labels: + {{- include "nuts-admin.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "nuts-admin.fullname" . }} + 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/nuts-admin/templates/ingress.yaml b/charts/nuts-admin/templates/ingress.yaml new file mode 100644 index 0000000..e4081e2 --- /dev/null +++ b/charts/nuts-admin/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "nuts-admin.fullname" . }} + labels: + {{- include "nuts-admin.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "nuts-admin.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/nuts-admin/templates/service.yaml b/charts/nuts-admin/templates/service.yaml new file mode 100644 index 0000000..203204a --- /dev/null +++ b/charts/nuts-admin/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nuts-admin.fullname" . }} + labels: + {{- include "nuts-admin.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "nuts-admin.selectorLabels" . | nindent 4 }} diff --git a/charts/nuts-admin/templates/serviceaccount.yaml b/charts/nuts-admin/templates/serviceaccount.yaml new file mode 100644 index 0000000..5ac4075 --- /dev/null +++ b/charts/nuts-admin/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "nuts-admin.serviceAccountName" . }} + labels: + {{- include "nuts-admin.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/nuts-admin/values.yaml b/charts/nuts-admin/values.yaml new file mode 100644 index 0000000..26ac35c --- /dev/null +++ b/charts/nuts-admin/values.yaml @@ -0,0 +1,74 @@ +# Default values for nuts-admin. + +replicaCount: 1 + +image: + repository: nutsfoundation/nuts-admin + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + automount: true + annotations: {} + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: {} +securityContext: {} + +service: + type: ClusterIP + port: 1305 + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + +resources: {} + +livenessProbe: + httpGet: + path: /status + port: http +readinessProbe: + httpGet: + path: /status + port: http + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + +# Additional volumes/volumeMounts on the output Deployment definition. +volumes: [] +volumeMounts: [] + +nodeSelector: {} +tolerations: [] +affinity: {} + +# The Nuts node internal API this nuts-admin instance manages. +env: + nutsNodeAddress: "http://localhost:8081" + +# Rendered into config.yaml (mounted at /app/config.yaml, referenced by +# NUTS_CONFIGFILE). See deploy/config.yaml.example in the nuts-admin repo +# for the full set of options this accepts (e.g. oidc, credentialprofiles). +config: {} From f797973014c7bd50fe844eebfed7d7d0a18b8adb Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 16 Sep 2026 10:07:28 +0200 Subject: [PATCH 2/4] Support existingSecret for OIDC client secret in Helm chart Adds oidc.existingSecret/oidc.existingSecretKey values, injected as NUTS_OIDC_CLIENT_SECRET via secretKeyRef, so the secret doesn't have to be set through config.oidc.client.secret (which lands in the ConfigMap in plaintext). Assisted by AI --- charts/nuts-admin/README.md | 2 ++ charts/nuts-admin/templates/deployment.yaml | 7 +++++++ charts/nuts-admin/values.yaml | 10 ++++++++++ 3 files changed, 19 insertions(+) diff --git a/charts/nuts-admin/README.md b/charts/nuts-admin/README.md index 3f16fd0..933c4f3 100644 --- a/charts/nuts-admin/README.md +++ b/charts/nuts-admin/README.md @@ -12,6 +12,8 @@ All configurable properties can be found in [./values.yaml](./values.yaml). The |----------------------|--------------------------------------------------------------------------------------------------------------------------|-----------------------------| | `env.nutsNodeAddress` | Internal API address of the Nuts node this instance manages, injected as `NUTS_NODE_ADDRESS`. | `http://localhost:8081` | | `config` | Rendered into `config.yaml`, mounted into the pod and referenced via `NUTS_CONFIGFILE`. See the [nuts-admin README](../../README.md) and [deploy/config.yaml.example](../../deploy/config.yaml.example) for the options it accepts, e.g. `oidc` and `credentialprofiles`. | `{}` | +| `oidc.existingSecret` | Name of an existing Secret holding the OIDC client secret, injected as `NUTS_OIDC_CLIENT_SECRET`. Use this instead of setting `config.oidc.client.secret` directly, which would put it in the ConfigMap in plaintext. The rest of the OIDC config (`enabled`, `metadata`, `client.id`, `scope`) still goes through `config.oidc`. | `""` | +| `oidc.existingSecretKey` | Key within `oidc.existingSecret` holding the client secret. | `client-secret` | | `ingress.enabled` | Expose nuts-admin through an Ingress. | `false` | ## Installing nuts-admin diff --git a/charts/nuts-admin/templates/deployment.yaml b/charts/nuts-admin/templates/deployment.yaml index 254da6b..96fa83d 100644 --- a/charts/nuts-admin/templates/deployment.yaml +++ b/charts/nuts-admin/templates/deployment.yaml @@ -45,6 +45,13 @@ spec: value: {{ .Values.env.nutsNodeAddress | quote }} - name: NUTS_CONFIGFILE value: /app/config.yaml + {{- with .Values.oidc.existingSecret }} + - name: NUTS_OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ . }} + key: {{ $.Values.oidc.existingSecretKey }} + {{- end }} ports: - name: http containerPort: {{ .Values.service.port }} diff --git a/charts/nuts-admin/values.yaml b/charts/nuts-admin/values.yaml index 26ac35c..7c05aa9 100644 --- a/charts/nuts-admin/values.yaml +++ b/charts/nuts-admin/values.yaml @@ -72,3 +72,13 @@ env: # NUTS_CONFIGFILE). See deploy/config.yaml.example in the nuts-admin repo # for the full set of options this accepts (e.g. oidc, credentialprofiles). config: {} + +oidc: + # Name of an existing Secret holding the OIDC client secret. When set, it's + # injected as the NUTS_OIDC_CLIENT_SECRET env var, so the secret doesn't + # have to go into `config.oidc.client.secret` (which would land in the + # ConfigMap in plaintext). The rest of the oidc config still goes through + # `config.oidc` (enabled, metadata, client.id, scope). + existingSecret: "" + # Key within existingSecret holding the client secret. + existingSecretKey: client-secret From 2486cdd00f02622b91e73de782066ac22039de8f Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 16 Sep 2026 15:34:57 +0200 Subject: [PATCH 3/4] Address review: publish via ghcr OCI, fix port/annotations/oidc - Switch chart publishing from chart-releaser/GitHub Pages to pushing an OCI artifact to ghcr.io, matching nuts-knooppunt's own chart publishing pattern and avoiding the manual Pages setup step. - Add checksum/config pod annotation so pods restart when config.yaml changes. - Fix containerPort: it was tied to service.port, so changing the Service's exposed port silently broke health checks and routing since the app still listens on its own configured port. Derive it from config.port (default 1305) instead. - Fail fast when autoscaling.enabled and config.oidc.enabled are both set: nuts-admin keeps OIDC sessions in memory, which breaks across replicas. Assisted by AI --- .github/workflows/helm-chart-release.yaml | 43 ++++++++++++--------- charts/nuts-admin/README.md | 15 +++---- charts/nuts-admin/templates/deployment.yaml | 10 +++-- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/.github/workflows/helm-chart-release.yaml b/.github/workflows/helm-chart-release.yaml index b4ea5a8..2581595 100644 --- a/.github/workflows/helm-chart-release.yaml +++ b/.github/workflows/helm-chart-release.yaml @@ -1,4 +1,4 @@ -name: Release Charts +name: Release Chart on: push: @@ -14,30 +14,37 @@ jobs: release: runs-on: ubuntu-latest permissions: - contents: write + packages: write steps: - name: Checkout uses: actions/checkout@v7 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Configure Git - run: | - git config user.name "$GITHUB_ACTOR" - git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - name: Install Helm uses: azure/setup-helm@v5 with: version: v3.8.0 - - name: Run chart-releaser - uses: helm/chart-releaser-action@v1.7.0 + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 with: - mark_as_latest: false - env: - CR_CHARTS_DIR: charts - CR_PAGES_BRANCH: gh-pages - CR_SKIP_EXISTING: true - CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Package chart + run: helm package charts/nuts-admin -d .helm-packages + + - name: Check if this chart version is already published + id: check + run: | + VERSION=$(grep '^version:' charts/nuts-admin/Chart.yaml | awk '{print $2}') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + if helm pull oci://ghcr.io/${{ github.repository_owner }}/nuts-admin-chart --version "$VERSION" -d /tmp >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Push chart + if: steps.check.outputs.exists == 'false' + run: helm push .helm-packages/nuts-admin-chart-${{ steps.check.outputs.version }}.tgz oci://ghcr.io/${{ github.repository_owner }} diff --git a/charts/nuts-admin/README.md b/charts/nuts-admin/README.md index 933c4f3..c3bb9d8 100644 --- a/charts/nuts-admin/README.md +++ b/charts/nuts-admin/README.md @@ -16,6 +16,8 @@ All configurable properties can be found in [./values.yaml](./values.yaml). The | `oidc.existingSecretKey` | Key within `oidc.existingSecret` holding the client secret. | `client-secret` | | `ingress.enabled` | Expose nuts-admin through an Ingress. | `false` | +Note: nuts-admin keeps OIDC sessions in memory, so `autoscaling.enabled: true` together with `config.oidc.enabled: true` isn't supported — the chart refuses to render (`fail`) in that combination. Use a single replica if you need OIDC. + ## Installing nuts-admin ### From source @@ -26,17 +28,10 @@ Execute the following command from the root of the chart folder. Replace ` helm install . ``` -### From the Nuts Helm repo - -Add the repo: - -```shell -helm repo add nuts-admin https://nuts-foundation.github.io/nuts-admin/ -helm repo update -``` +### From GitHub Container Registry -Then install it, optionally overriding values with your own `values.yaml`: +Chart releases are published as an OCI artifact to `ghcr.io` on every change to `charts/` on `main`. Install directly by version, optionally overriding values with your own `values.yaml`: ```shell -helm install -f values.yaml nuts-admin/nuts-admin-chart +helm install -f values.yaml oci://ghcr.io/nuts-foundation/nuts-admin-chart --version ``` diff --git a/charts/nuts-admin/templates/deployment.yaml b/charts/nuts-admin/templates/deployment.yaml index 96fa83d..36cdbc5 100644 --- a/charts/nuts-admin/templates/deployment.yaml +++ b/charts/nuts-admin/templates/deployment.yaml @@ -1,3 +1,6 @@ +{{- if and .Values.autoscaling.enabled (dig "oidc" "enabled" false .Values.config) }} +{{ fail "autoscaling.enabled and config.oidc.enabled=true are incompatible: nuts-admin keeps OIDC sessions in memory, which breaks when requests land on different replicas. Set replicaCount: 1 (and autoscaling.enabled: false), or disable OIDC." }} +{{- end }} apiVersion: apps/v1 kind: Deployment metadata: @@ -13,10 +16,11 @@ spec: {{- include "nuts-admin.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} labels: {{- include "nuts-admin.labels" . | nindent 8 }} {{- with .Values.podLabels }} @@ -54,7 +58,7 @@ spec: {{- end }} ports: - name: http - containerPort: {{ .Values.service.port }} + containerPort: {{ .Values.config.port | default 1305 }} protocol: TCP {{- with .Values.livenessProbe }} livenessProbe: From 2e8577bdd84c1378ceb30ba9067828a70f2b3406 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 16 Sep 2026 16:44:59 +0200 Subject: [PATCH 4/4] Remove HPA/autoscaling support from the chart Drop the HPA template and autoscaling values entirely instead of just checking autoscaling.enabled: as pointed out in review, replicaCount could be bumped directly with autoscaling disabled and OIDC enabled, bypassing the earlier guard. nuts-admin's in-memory sessions don't support running more than one replica at all, so there's no scenario where autoscaling makes sense for this chart. The fail-fast check now guards replicaCount > 1 combined with config.oidc.enabled directly. Assisted by AI --- charts/nuts-admin/README.md | 2 +- charts/nuts-admin/templates/deployment.yaml | 6 ++-- charts/nuts-admin/templates/hpa.yaml | 32 --------------------- charts/nuts-admin/values.yaml | 6 ---- 4 files changed, 3 insertions(+), 43 deletions(-) delete mode 100644 charts/nuts-admin/templates/hpa.yaml diff --git a/charts/nuts-admin/README.md b/charts/nuts-admin/README.md index c3bb9d8..10464d7 100644 --- a/charts/nuts-admin/README.md +++ b/charts/nuts-admin/README.md @@ -16,7 +16,7 @@ All configurable properties can be found in [./values.yaml](./values.yaml). The | `oidc.existingSecretKey` | Key within `oidc.existingSecret` holding the client secret. | `client-secret` | | `ingress.enabled` | Expose nuts-admin through an Ingress. | `false` | -Note: nuts-admin keeps OIDC sessions in memory, so `autoscaling.enabled: true` together with `config.oidc.enabled: true` isn't supported — the chart refuses to render (`fail`) in that combination. Use a single replica if you need OIDC. +Note: nuts-admin keeps OIDC sessions in memory, so `replicaCount` above `1` together with `config.oidc.enabled: true` isn't supported — the chart refuses to render (`fail`) in that combination. There's no autoscaling support in this chart for the same reason. ## Installing nuts-admin diff --git a/charts/nuts-admin/templates/deployment.yaml b/charts/nuts-admin/templates/deployment.yaml index 36cdbc5..f111cd3 100644 --- a/charts/nuts-admin/templates/deployment.yaml +++ b/charts/nuts-admin/templates/deployment.yaml @@ -1,5 +1,5 @@ -{{- if and .Values.autoscaling.enabled (dig "oidc" "enabled" false .Values.config) }} -{{ fail "autoscaling.enabled and config.oidc.enabled=true are incompatible: nuts-admin keeps OIDC sessions in memory, which breaks when requests land on different replicas. Set replicaCount: 1 (and autoscaling.enabled: false), or disable OIDC." }} +{{- if and (gt (.Values.replicaCount | int) 1) (dig "oidc" "enabled" false .Values.config) }} +{{ fail "replicaCount > 1 and config.oidc.enabled=true are incompatible: nuts-admin keeps OIDC sessions in memory, which breaks when requests land on different replicas. Set replicaCount: 1, or disable OIDC." }} {{- end }} apiVersion: apps/v1 kind: Deployment @@ -8,9 +8,7 @@ metadata: labels: {{- include "nuts-admin.labels" . | nindent 4 }} spec: - {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} - {{- end }} selector: matchLabels: {{- include "nuts-admin.selectorLabels" . | nindent 6 }} diff --git a/charts/nuts-admin/templates/hpa.yaml b/charts/nuts-admin/templates/hpa.yaml deleted file mode 100644 index 4141415..0000000 --- a/charts/nuts-admin/templates/hpa.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "nuts-admin.fullname" . }} - labels: - {{- include "nuts-admin.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "nuts-admin.fullname" . }} - 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/nuts-admin/values.yaml b/charts/nuts-admin/values.yaml index 7c05aa9..31637a9 100644 --- a/charts/nuts-admin/values.yaml +++ b/charts/nuts-admin/values.yaml @@ -50,12 +50,6 @@ readinessProbe: path: /status port: http -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # Additional volumes/volumeMounts on the output Deployment definition. volumes: [] volumeMounts: []