From a270a32bf69945786da4181ee7be9a903977a36a Mon Sep 17 00:00:00 2001 From: Kai Udo <76635578+u-kai@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:18:24 +0900 Subject: [PATCH] fix(helm): resolve RBAC permissions for namespaced gateway sources (#5578) * fix(helm): resolve RBAC permissions for namespaced gateway sources * feat(helm): add support for gateway namespace in RBAC configuration * chore(helm): update docs and fix formatting issues * fix(helm): revert README changes and add gatewayNamespace docs * chore lint fmt --- charts/external-dns/README.md | 1 + charts/external-dns/templates/_helpers.tpl | 9 + .../external-dns/templates/clusterrole.yaml | 33 ++- .../templates/clusterrolebinding.yaml | 35 +++ charts/external-dns/templates/deployment.yaml | 3 + charts/external-dns/tests/rbac_test.yaml | 235 ++++++++++++++++++ charts/external-dns/values.yaml | 3 + 7 files changed, 318 insertions(+), 1 deletion(-) diff --git a/charts/external-dns/README.md b/charts/external-dns/README.md index 24f7ed9b3..2daf4f005 100644 --- a/charts/external-dns/README.md +++ b/charts/external-dns/README.md @@ -109,6 +109,7 @@ If `namespaced` is set to `true`, please ensure that `sources` my only contains | extraVolumeMounts | list | `[]` | Extra [volume mounts](https://kubernetes.io/docs/concepts/storage/volumes/) for the `external-dns` container. | | extraVolumes | list | `[]` | Extra [volumes](https://kubernetes.io/docs/concepts/storage/volumes/) for the `Pod`. | | fullnameOverride | string | `nil` | Override the full name of the chart. | +| gatewayNamespace | string | `nil` | _Gateway API_ gateway namespace to watch. | | global.imagePullSecrets | list | `[]` | Global image pull secrets. | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy for the `external-dns` container. | | image.repository | string | `"registry.k8s.io/external-dns/external-dns"` | Image repository for the `external-dns` container. | diff --git a/charts/external-dns/templates/_helpers.tpl b/charts/external-dns/templates/_helpers.tpl index a7277c8ef..aad09822e 100644 --- a/charts/external-dns/templates/_helpers.tpl +++ b/charts/external-dns/templates/_helpers.tpl @@ -103,3 +103,12 @@ labelSelector: matchLabels: {{ include "external-dns.selectorLabels" . | nindent 4 }} {{- end }} + +{{/* +Check if any Gateway API sources are enabled +*/}} +{{- define "external-dns.hasGatewaySources" -}} +{{- if or (has "gateway-httproute" .Values.sources) (has "gateway-grpcroute" .Values.sources) (has "gateway-tlsroute" .Values.sources) (has "gateway-tcproute" .Values.sources) (has "gateway-udproute" .Values.sources) -}} +true +{{- end -}} +{{- end }} diff --git a/charts/external-dns/templates/clusterrole.yaml b/charts/external-dns/templates/clusterrole.yaml index a3d8cb777..f418416e2 100644 --- a/charts/external-dns/templates/clusterrole.yaml +++ b/charts/external-dns/templates/clusterrole.yaml @@ -58,14 +58,18 @@ rules: resources: ["dnsendpoints/status"] verbs: ["*"] {{- end }} -{{- if or (has "gateway-httproute" .Values.sources) (has "gateway-grpcroute" .Values.sources) (has "gateway-tlsroute" .Values.sources) (has "gateway-tcproute" .Values.sources) (has "gateway-udproute" .Values.sources) }} +{{- if include "external-dns.hasGatewaySources" . }} +{{- if or (not .Values.namespaced) (and .Values.namespaced (not .Values.gatewayNamespace)) }} - apiGroups: ["gateway.networking.k8s.io"] resources: ["gateways"] verbs: ["get","watch","list"] +{{- end }} +{{- if not .Values.namespaced }} - apiGroups: [""] resources: ["namespaces"] verbs: ["get","watch","list"] {{- end }} +{{- end }} {{- if has "gateway-httproute" .Values.sources }} - apiGroups: ["gateway.networking.k8s.io"] resources: ["httproutes"] @@ -127,4 +131,31 @@ rules: {{- with .Values.rbac.additionalPermissions }} {{- toYaml . | nindent 2 }} {{- end }} +{{- if and .Values.rbac.create .Values.namespaced (include "external-dns.hasGatewaySources" .) }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "external-dns.fullname" . }}-namespaces + labels: + {{- include "external-dns.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get","watch","list"] +{{- if .Values.gatewayNamespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "external-dns.fullname" . }}-gateway + namespace: {{ .Values.gatewayNamespace }} + labels: + {{- include "external-dns.labels" . | nindent 4 }} +rules: + - apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] +{{- end }} +{{- end }} {{- end }} diff --git a/charts/external-dns/templates/clusterrolebinding.yaml b/charts/external-dns/templates/clusterrolebinding.yaml index 74a51476f..49400c0be 100644 --- a/charts/external-dns/templates/clusterrolebinding.yaml +++ b/charts/external-dns/templates/clusterrolebinding.yaml @@ -13,4 +13,39 @@ subjects: - kind: ServiceAccount name: {{ template "external-dns.serviceAccountName" . }} namespace: {{ .Release.Namespace }} +{{- if and .Values.rbac.create .Values.namespaced (include "external-dns.hasGatewaySources" .) }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ template "external-dns.fullname" . }}-namespaces + labels: + {{- include "external-dns.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ template "external-dns.fullname" . }}-namespaces +subjects: + - kind: ServiceAccount + name: {{ template "external-dns.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- if .Values.gatewayNamespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "external-dns.fullname" . }}-gateway + namespace: {{ .Values.gatewayNamespace }} + labels: + {{- include "external-dns.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "external-dns.fullname" . }}-gateway +subjects: + - kind: ServiceAccount + name: {{ template "external-dns.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} +{{- end }} {{- end }} diff --git a/charts/external-dns/templates/deployment.yaml b/charts/external-dns/templates/deployment.yaml index f0c967a33..7db118370 100644 --- a/charts/external-dns/templates/deployment.yaml +++ b/charts/external-dns/templates/deployment.yaml @@ -111,6 +111,9 @@ spec: {{- if .Values.namespaced }} - --namespace={{ .Release.Namespace }} {{- end }} + {{- if .Values.gatewayNamespace }} + - --gateway-namespace={{ .Values.gatewayNamespace }} + {{- end }} {{- range .Values.domainFilters }} - --domain-filter={{ . }} {{- end }} diff --git a/charts/external-dns/tests/rbac_test.yaml b/charts/external-dns/tests/rbac_test.yaml index 9e061c649..c8fcaea2c 100644 --- a/charts/external-dns/tests/rbac_test.yaml +++ b/charts/external-dns/tests/rbac_test.yaml @@ -156,3 +156,238 @@ tests: - apiGroups: ["gateway.networking.k8s.io"] resources: ["udproutes"] verbs: ["get","watch","list"] + + - it: should create Role instead of ClusterRole when namespaced is true + set: + namespaced: true + sources: + - service + asserts: + - isKind: + of: Role + template: clusterrole.yaml + - equal: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + + - it: should create RoleBinding instead of ClusterRoleBinding when namespaced is true + set: + namespaced: true + sources: + - service + asserts: + - isKind: + of: RoleBinding + template: clusterrolebinding.yaml + - equal: + path: metadata.name + value: rbac-external-dns-viewer + template: clusterrolebinding.yaml + + - it: should create all required resources when namespaced=true and gatewayNamespace is specified + set: + namespaced: true + gatewayNamespace: gateway-ns + sources: + - gateway-httproute + asserts: + # Should have: main Role + ClusterRole for namespaces + Gateway Role + - hasDocuments: + count: 3 + template: clusterrole.yaml + - hasDocuments: + count: 3 + template: clusterrolebinding.yaml + + # Main role should exist and contain route permissions but NOT gateway permissions + - isKind: + of: Role + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["httproutes"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + - notContains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + + # ClusterRole for namespaces should exist + - isKind: + of: ClusterRole + documentSelector: + path: metadata.name + value: rbac-external-dns-namespaces + template: clusterrole.yaml + + # Gateway role should exist and have gateway permissions only + - isKind: + of: Role + documentSelector: + path: metadata.name + value: rbac-external-dns-gateway + template: clusterrole.yaml + - equal: + path: metadata.namespace + value: gateway-ns + documentSelector: + path: metadata.name + value: rbac-external-dns-gateway + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns-gateway + template: clusterrole.yaml + + + - it: should create main Role with gateway permissions and ClusterRole for namespaces when namespaced=true and gatewayNamespace is not set + set: + namespaced: true + sources: + - gateway-httproute + asserts: + # Should have: main Role + ClusterRole for namespaces + - hasDocuments: + count: 2 + template: clusterrole.yaml + - hasDocuments: + count: 2 + template: clusterrolebinding.yaml + # Main Role should exist and contain gateway permissions + - isKind: + of: Role + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["httproutes"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + # ClusterRole for namespaces should exist + - isKind: + of: ClusterRole + documentSelector: + path: metadata.name + value: rbac-external-dns-namespaces + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: [""] + resources: ["namespaces"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns-namespaces + template: clusterrole.yaml + + - it: should create ClusterRole with all permissions when namespaced=false and gateway sources exist + set: + namespaced: false + sources: + - gateway-httproute + asserts: + - hasDocuments: + count: 1 + template: clusterrole.yaml + - hasDocuments: + count: 1 + template: clusterrolebinding.yaml + - isKind: + of: ClusterRole + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: [""] + resources: ["namespaces"] + verbs: ["get","watch","list"] + template: clusterrole.yaml + + - it: should create only ClusterRole when namespaced=false and no gateway sources + set: + namespaced: false + sources: + - service + - ingress + asserts: + - hasDocuments: + count: 1 + template: clusterrole.yaml + - hasDocuments: + count: 1 + template: clusterrolebinding.yaml + - isKind: + of: ClusterRole + template: clusterrole.yaml + - notContains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + template: clusterrole.yaml + + - it: should create only Role when namespaced=true and no gateway sources + set: + namespaced: true + sources: + - service + - ingress + asserts: + - hasDocuments: + count: 1 + template: clusterrole.yaml + - hasDocuments: + count: 1 + template: clusterrolebinding.yaml + - isKind: + of: Role + template: clusterrole.yaml + - isKind: + of: RoleBinding + template: clusterrolebinding.yaml diff --git a/charts/external-dns/values.yaml b/charts/external-dns/values.yaml index a6fa3212b..8bc037974 100644 --- a/charts/external-dns/values.yaml +++ b/charts/external-dns/values.yaml @@ -205,6 +205,9 @@ triggerLoopOnEvent: false # -- if `true`, _ExternalDNS_ will run in a namespaced scope (`Role`` and `Rolebinding`` will be namespaced too). namespaced: false +# -- _Gateway API_ gateway namespace to watch. +gatewayNamespace: # @schema type:[string, null]; default: null + # -- _Kubernetes_ resources to monitor for DNS entries. sources: - service