From ab68805e48e9c3e6b7873d04fb79a50988828553 Mon Sep 17 00:00:00 2001 From: Mathieu Grzybek Date: Wed, 6 May 2026 17:22:43 +0200 Subject: [PATCH 1/6] helm(chores): remove deprecated AWX --- .../api/settings/07-disable-kubernetes.yaml | 4 - .../files/api/settings/07-feature-flags.yaml | 9 +++ chart/files/api/settings/08-awx.yaml | 55 ------------- chart/files/api/settings/09-awx-passwords.py | 8 -- chart/files/api/settings/10-disable-caas.yaml | 4 - chart/templates/api/deployment.yaml | 16 ---- chart/templates/api/settings.yaml | 12 +-- chart/values.yaml | 79 +------------------ 8 files changed, 13 insertions(+), 174 deletions(-) delete mode 100644 chart/files/api/settings/07-disable-kubernetes.yaml create mode 100644 chart/files/api/settings/07-feature-flags.yaml delete mode 100644 chart/files/api/settings/08-awx.yaml delete mode 100644 chart/files/api/settings/09-awx-passwords.py delete mode 100644 chart/files/api/settings/10-disable-caas.yaml diff --git a/chart/files/api/settings/07-disable-kubernetes.yaml b/chart/files/api/settings/07-disable-kubernetes.yaml deleted file mode 100644 index 19564192..00000000 --- a/chart/files/api/settings/07-disable-kubernetes.yaml +++ /dev/null @@ -1,4 +0,0 @@ -{{- if not .Values.tags.kubernetes -}} -AZIMUTH: - CLUSTER_API_PROVIDER: null -{{- end }} diff --git a/chart/files/api/settings/07-feature-flags.yaml b/chart/files/api/settings/07-feature-flags.yaml new file mode 100644 index 00000000..c804cbd2 --- /dev/null +++ b/chart/files/api/settings/07-feature-flags.yaml @@ -0,0 +1,9 @@ +{{- if or (not .Values.tags.kubernetes) (not .Values.tags.clusters) -}} +AZIMUTH: + {{- if not .Values.tags.kubernetes }} + CLUSTER_API_PROVIDER: null + {{- end }} + {{- if not .Values.tags.clusters }} + CLUSTER_ENGINE: null + {{- end }} +{{- end }} diff --git a/chart/files/api/settings/08-awx.yaml b/chart/files/api/settings/08-awx.yaml deleted file mode 100644 index 4c80c6b2..00000000 --- a/chart/files/api/settings/08-awx.yaml +++ /dev/null @@ -1,55 +0,0 @@ -{{- if and .Values.tags.clusters (eq .Values.clusterEngine.type "awx") -}} -AZIMUTH: - AWX: - ENABLED: true - URL: {{ required "clusterEngine.awx.url is required" .Values.clusterEngine.awx.url | quote }} - VERIFY_SSL: {{ .Values.clusterEngine.awx.verifySsl }} - USERNAME: {{ .Values.clusterEngine.awx.username }} - CREATE_TEAMS: {{ .Values.clusterEngine.awx.createTeams }} - CREATE_TEAM_ALLOW_ALL_PERMISSION: {{ .Values.clusterEngine.awx.createTeamAllowAllPermission }} - {{- with .Values.clusterEngine.awx.adminUsername }} - ADMIN_USERNAME: {{ . }} - {{- end }} - {{- if .Values.clusterEngine.terraformBackend.enabled }} - {{- if (eq .Values.clusterEngine.terraformBackend.type "consul") }} - EXTRA_CREDENTIALS: - - NAME: CaaS Consul - TYPE: Hashicorp Consul - INPUTS: - address: {{ required "clusterEngine.terraformBackend.consul.address" .Values.clusterEngine.terraformBackend.consul.address | quote }} - {{- else }} - {{- fail (printf "Unsupported terraform backend '%s'" .Values.clusterEngine.terraformBackend.type) }} - {{- end }} - {{- end }} - {{- with .Values.clusterEngine.awx.executionEnvironment }} - EXECUTION_ENVIRONMENT: - IMAGE: {{ .image }} - {{- if .alwaysPull }} - ALWAYS_PULL: true - {{- end }} - {{- with .credentials }} - CREDENTIALS: - HOST: {{ .host }} - USERNAME: {{ quote .username }} - TOKEN: {{ quote .token }} - {{- end }} - {{- end }} - {{- with .Values.clusterEngine.awx.defaultProjects }} - DEFAULT_PROJECTS: - {{- range . }} - - NAME: {{ .name | quote }} - GIT_URL: {{ .gitUrl }} - GIT_VERSION: {{ .gitVersion }} - METADATA_ROOT: {{ .metadataRoot }} - {{- if .alwaysUpdate }} - ALWAYS_UPDATE: true - {{- end }} - {{- with .playbooks }} - PLAYBOOKS: {{ toYaml . | nindent 14 }} - {{- end }} - {{- with .extraVars }} - EXTRA_VARS: {{ toYaml . | nindent 14 }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} diff --git a/chart/files/api/settings/09-awx-passwords.py b/chart/files/api/settings/09-awx-passwords.py deleted file mode 100644 index b2f66d8e..00000000 --- a/chart/files/api/settings/09-awx-passwords.py +++ /dev/null @@ -1,8 +0,0 @@ -{{- if and .Values.tags.clusters (eq .Values.clusterEngine.type "awx") -}} -import os - -# Read the AWX password from an environment variable that is populated from the specified secret -AZIMUTH['AWX']['PASSWORD'] = os.environ['AWX_PASSWORD'] -if 'AWX_ADMIN_PASSWORD' in os.environ: - AZIMUTH['AWX']['ADMIN_PASSWORD'] = os.environ['AWX_ADMIN_PASSWORD'] -{{- end }} diff --git a/chart/files/api/settings/10-disable-caas.yaml b/chart/files/api/settings/10-disable-caas.yaml deleted file mode 100644 index 42e58b2c..00000000 --- a/chart/files/api/settings/10-disable-caas.yaml +++ /dev/null @@ -1,4 +0,0 @@ -{{- if not .Values.tags.clusters -}} -AZIMUTH: - CLUSTER_ENGINE: null -{{- end }} diff --git a/chart/templates/api/deployment.yaml b/chart/templates/api/deployment.yaml index 6e14b1f3..5c8e1b22 100644 --- a/chart/templates/api/deployment.yaml +++ b/chart/templates/api/deployment.yaml @@ -1,6 +1,5 @@ {{- $component := "api" -}} {{- $values := get .Values $component -}} -{{- $clusterEngine := .Values.clusterEngine -}} apiVersion: apps/v1 kind: Deployment metadata: @@ -57,23 +56,8 @@ spec: (eq .Values.authentication.type "oidc") (hasPrefix "http://" .Values.authentication.oidc.issuerUrl) ) - (and .Values.tags.clusters (eq $clusterEngine.type "awx")) }} env: - {{- if and .Values.tags.clusters (eq $clusterEngine.type "awx") }} - - name: AWX_PASSWORD - valueFrom: - secretKeyRef: - name: {{ required "clusterEngine.awx.passwordSecretName is required" $clusterEngine.awx.passwordSecretName | quote }} - key: password - {{- with $clusterEngine.awx.adminPasswordSecretName }} - - name: AWX_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: {{ quote . }} - key: password - {{- end }} - {{- end }} {{- if $values.monitoring.enabled }} - name: GUNICORN_STATSD_HOST value: "localhost:9125" diff --git a/chart/templates/api/settings.yaml b/chart/templates/api/settings.yaml index 2be8b42c..824be99f 100644 --- a/chart/templates/api/settings.yaml +++ b/chart/templates/api/settings.yaml @@ -1,4 +1,4 @@ -{{- if not (has .Values.clusterEngine.type (list "crd" "awx")) -}} +{{- if ne .Values.clusterEngine.type "crd" -}} {{- fail (printf "Unsupported cluster engine '%s'" .Values.clusterEngine.type) }} {{- end }} @@ -23,14 +23,8 @@ data: {{- tpl (.Files.Get "files/api/settings/05-apps.yaml") . | b64enc | nindent 4 }} 06-ssh-key-store.yaml: | {{- tpl (.Files.Get "files/api/settings/06-ssh-key-store.yaml") . | b64enc | nindent 4 }} - 07-disable-kubernetes.yaml: | - {{- tpl (.Files.Get "files/api/settings/07-disable-kubernetes.yaml") . | b64enc | nindent 4 }} - 08-awx.yaml: | - {{- tpl (.Files.Get "files/api/settings/08-awx.yaml") . | b64enc | nindent 4 }} - 09-awx-passwords.py: | - {{- tpl (.Files.Get "files/api/settings/09-awx-passwords.py") . | b64enc | nindent 4 }} - 10-disable-caas.yaml: | - {{- tpl (.Files.Get "files/api/settings/10-disable-caas.yaml") . | b64enc | nindent 4 }} + 07-feature-flags.yaml: | + {{- tpl (.Files.Get "files/api/settings/07-feature-flags.yaml") . | b64enc | nindent 4 }} 11-scheduling.yaml: | {{- tpl (.Files.Get "files/api/settings/11-scheduling.yaml") . | b64enc | nindent 4 }} 12-apps-provider.yaml: | diff --git a/chart/values.yaml b/chart/values.yaml index f1974fa1..408d895b 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -314,85 +314,8 @@ apps: # The cluster engine to use for Cluster-as-a-Service support clusterEngine: - # The type of cluster engine to use - # Currently only CRD is functional, and AWX support is deprecated + # The type of cluster engine to use (only crd is supported) type: crd - # Parameters for the awx cluster engine - # By default, we use values that will work with the managed AWX - awx: - # The AWX or Ansible Tower URL - url: - # Whether to verify SSL with the AWX/Tower API - verifySsl: true - # The username to use for the AWX/Tower API - # This user must be at least an org admin for the CaaS organisation - username: admin - # The name of a secret to get the AWX password from - passwordSecretName: - # Indicates whether teams should be created automatically - createTeams: true - # Indicates whether automatically-created teams should be granted access - # to all cluster types - createTeamAllowAllPermission: true - # Indicates whether CaaS resources should be created in AWX - createResources: true - # Creating resources requires the admin user - # If the user specified above is not a global admin, a separate admin username and - # password secret should be specified - adminUsername: - adminPasswordSecretName: - # Definition of a custom execution environment for CaaS appliances - executionEnvironment: - # # The image to use for the execution environment (required) - # image: registry.example.com/azimuth/caas-ee: - # # Indicates if the image should be pulled for every job (defaults to false) - # # This can be useful in development, but in production the recommended method is - # # to set this to false and use immutable tags - # alwaysPull: false - # # Optional credentials if the image is in a private registry - # credentials: - # # The registry host - # host: registry.example.com - # # The username to use to authenticate - # username: - # # The token/password to use to authenticate - # token: - # Definition of the default projects and job templates. - # By default, the StackHPC demo appliances will be used - defaultProjects: - # - # The name of the project (required) - # name: My Site Appliances - # # The git URL of the project (required) - # gitUrl: https://github.com/myorg/site-appliances.git - # # The branch, tag or commit id to use (required) - # gitVersion: master - # # The base URL for cluster metadata files (required) - # metadataRoot: https://raw.githubusercontent.com/myorg/site-appliances/master/ui-meta - # ## - # # OPTIONAL KEYS - # ## - # # Indicates if the project should be updated to the latest version on each job launch - # # Can be useful during development but recommend to leave as false in production - # # Defaults to false if not given - # alwaysUpdate: true - # # List of playbooks to create job templates for - # # If not given, a job template is created for each playbook in the project - # playbooks: ['cluster1-playbook.yml', 'cluster2-playbook.yml'] - # # Map whose keys are the playbooks and whose values are maps of Ansible extra_vars for those playbooks - # extraVars: - # # This special key can be used to set common extra_vars for all playbooks in a project - # __ALL__: { var1: value1, var2: value2 } - # cluster1-playbook.yml: { var1: "override value1" } - # Configuration for the Terraform backend - terraformBackend: - # Indicates if the Terraform backend should be configured - enabled: true - # The type of the Terraform backend to use (currently only consul is supported) - type: consul - # Configuration for the consul backend - consul: - # The address, including port, of the Consul server to use - address: # The SSH key store to use # The portal asks this store for the SSH key for a user From 8823af47220cd1201be881dd5f587fbb9029ffca Mon Sep 17 00:00:00 2001 From: Mathieu Grzybek Date: Wed, 6 May 2026 17:31:16 +0200 Subject: [PATCH 2/6] chores: remode dead code - conditions never reached --- api/azimuth/views.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/api/azimuth/views.py b/api/azimuth/views.py index 9333f262..db5bbce3 100644 --- a/api/azimuth/views.py +++ b/api/azimuth/views.py @@ -1426,12 +1426,11 @@ def cluster_service(request, tenant, cluster, service): service_fqdn = None service_label = None try: - if cloud_settings.CLUSTER_ENGINE: - with request.auth.scoped_session(tenant) as session: - with cloud_settings.CLUSTER_ENGINE.create_manager( - session - ) as cluster_manager: - cluster = cluster_manager.find_cluster(cluster) + with request.auth.scoped_session(tenant) as session: + with cloud_settings.CLUSTER_ENGINE.create_manager( + session + ) as cluster_manager: + cluster = cluster_manager.find_cluster(cluster) service_obj = next(s for s in cluster.services if s.name == service) service_fqdn = service_obj.fqdn service_label = service_obj.label @@ -1834,9 +1833,8 @@ def kubernetes_cluster_service(request, tenant, cluster, service): service_fqdn = None service_label = None try: - if cloud_settings.CLUSTER_API_PROVIDER: - with cloud_settings.CLUSTER_API_PROVIDER.session(session) as capi_session: - cluster = capi_session.find_cluster(cluster) + with cloud_settings.CLUSTER_API_PROVIDER.session(session) as capi_session: + cluster = capi_session.find_cluster(cluster) service_obj = next(s for s in cluster.services if s.name == service) service_fqdn = service_obj.fqdn service_label = service_obj.label From 288cae8aafcf3972db10c5295fdd5548ff66cdd5 Mon Sep 17 00:00:00 2001 From: Mathieu Grzybek Date: Mon, 22 Jun 2026 15:32:17 +0200 Subject: [PATCH 3/6] fix: scope API secrets access to tenancy namespaces The API ClusterRole granted cluster-wide list/get/create/update/patch/delete on secrets. Combined with create on namespaces, this was an effective full-cluster secret exfiltration path. Secrets are now governed by a namespaced Role + RoleBinding created per tenancy namespace. When ensure_namespace() bootstraps a tenancy namespace it also creates an "azimuth-api-secrets" Role and RoleBinding scoped to that namespace. The API service account identity is supplied via the Kubernetes downward API (AZIMUTH_SA_NAME / AZIMUTH_SA_NAMESPACE env vars added to the Deployment). The ClusterRole no longer contains any secrets rule; instead it gains get/create/patch on roles and rolebindings so the bootstrap can run. --- api/azimuth/utils.py | 63 ++++++++++++++++++++++++++++ chart/templates/api/clusterrole.yaml | 8 ++-- chart/templates/api/deployment.yaml | 17 ++++---- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/api/azimuth/utils.py b/api/azimuth/utils.py index ec241f4e..d7cdf484 100644 --- a/api/azimuth/utils.py +++ b/api/azimuth/utils.py @@ -1,4 +1,5 @@ import logging +import os import re import easykube @@ -111,6 +112,67 @@ def get_namespace(ekclient, tenancy: dto.Tenancy) -> str: raise NamespaceOwnershipError(expected_namespace, tenancy_id, owner_id) +_SECRETS_ROLE_NAME = "azimuth-api-secrets" + + +def ensure_secrets_rbac(ekclient, namespace: str): + """ + Ensures the API service account has the necessary secrets permissions in + the given namespace via a namespaced Role + RoleBinding. + + The service account identity is read from env vars injected by the Helm + chart via the Kubernetes downward API (AZIMUTH_SA_NAME / AZIMUTH_SA_NAMESPACE). + If either variable is absent the function is a no-op so that local test + runs outside a cluster are not broken. + """ + sa_name = os.environ.get("AZIMUTH_SA_NAME") + sa_namespace = os.environ.get("AZIMUTH_SA_NAMESPACE") + if not sa_name or not sa_namespace: + logger.warning( + "AZIMUTH_SA_NAME or AZIMUTH_SA_NAMESPACE not set; " + "skipping per-namespace secrets RBAC in '%s'", + namespace, + ) + return + + rbac = ekclient.api("rbac.authorization.k8s.io/v1") + + rbac.resource("roles").create_or_patch( + _SECRETS_ROLE_NAME, + { + "metadata": {"name": _SECRETS_ROLE_NAME}, + "rules": [ + { + "apiGroups": [""], + "resources": ["secrets"], + "verbs": ["list", "get", "create", "update", "patch", "delete"], + } + ], + }, + namespace=namespace, + ) + + rbac.resource("rolebindings").create_or_patch( + _SECRETS_ROLE_NAME, + { + "metadata": {"name": _SECRETS_ROLE_NAME}, + "subjects": [ + { + "kind": "ServiceAccount", + "name": sa_name, + "namespace": sa_namespace, + } + ], + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": _SECRETS_ROLE_NAME, + }, + }, + namespace=namespace, + ) + + def ensure_namespace(ekclient, namespace: str, tenancy: dto.Tenancy): """ Ensures that the specified namespace exists and is labelled correctly for @@ -130,3 +192,4 @@ def ensure_namespace(ekclient, namespace: str, tenancy: dto.Tenancy): }, }, ) + ensure_secrets_rbac(ekclient, namespace) diff --git a/chart/templates/api/clusterrole.yaml b/chart/templates/api/clusterrole.yaml index 44ce7cf0..2cba2be1 100644 --- a/chart/templates/api/clusterrole.yaml +++ b/chart/templates/api/clusterrole.yaml @@ -74,16 +74,14 @@ rules: - create - patch - apiGroups: - - "" + - rbac.authorization.k8s.io resources: - - secrets + - roles + - rolebindings verbs: - - list - get - create - - update - patch - - delete - apiGroups: - caas.azimuth.stackhpc.com resources: diff --git a/chart/templates/api/deployment.yaml b/chart/templates/api/deployment.yaml index 5c8e1b22..54839a0d 100644 --- a/chart/templates/api/deployment.yaml +++ b/chart/templates/api/deployment.yaml @@ -49,15 +49,15 @@ spec: securityContext: {{ toYaml $values.securityContext | nindent 12 }} image: {{ printf "%s:%s" $values.image.repository (default .Chart.AppVersion $values.image.tag) }} imagePullPolicy: {{ $values.image.pullPolicy }} - {{- - if or - $values.monitoring.enabled - (and - (eq .Values.authentication.type "oidc") - (hasPrefix "http://" .Values.authentication.oidc.issuerUrl) - ) - }} env: + - name: AZIMUTH_SA_NAME + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + - name: AZIMUTH_SA_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace {{- if $values.monitoring.enabled }} - name: GUNICORN_STATSD_HOST value: "localhost:9125" @@ -70,7 +70,6 @@ spec: - name: OAUTHLIB_INSECURE_TRANSPORT value: "1" {{- end }} - {{- end }} ports: - name: http containerPort: 8080 From a72d67c38d1104cabee6590d779cb428da561984 Mon Sep 17 00:00:00 2001 From: Mathieu Grzybek Date: Mon, 22 Jun 2026 16:04:14 +0200 Subject: [PATCH 4/6] test: update Helm snapshot for RBAC changes Signed-off-by: Mathieu Grzybek --- .../__snapshot__/snapshot_test.yaml.snap | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/chart/tests/__snapshot__/snapshot_test.yaml.snap b/chart/tests/__snapshot__/snapshot_test.yaml.snap index 531e1675..4ff85de6 100755 --- a/chart/tests/__snapshot__/snapshot_test.yaml.snap +++ b/chart/tests/__snapshot__/snapshot_test.yaml.snap @@ -82,16 +82,14 @@ templated manifests should match snapshot: - create - patch - apiGroups: - - "" + - rbac.authorization.k8s.io resources: - - secrets + - roles + - rolebindings verbs: - - list - get - create - - update - patch - - delete - apiGroups: - caas.azimuth.stackhpc.com resources: @@ -158,7 +156,7 @@ templated manifests should match snapshot: template: metadata: annotations: - azimuth.stackhpc.com/settings-checksum: ba9764bba470b2cacf65a4646a795dd5a62707a4879baac5749a8884af5dc0cd + azimuth.stackhpc.com/settings-checksum: 65ccb797a35210f0ae53ea1af4483f357627b78cc7c97b321de04fb78bdd6ed8 azimuth.stackhpc.com/theme-checksum: ec0f36322392deee39d80b7f77ecd634df60358857af9dc208077860c4e174ab kubectl.kubernetes.io/default-container: api labels: @@ -167,7 +165,16 @@ templated manifests should match snapshot: app.kubernetes.io/name: azimuth spec: containers: - - image: ghcr.io/azimuth-cloud/azimuth-api:master + - env: + - name: AZIMUTH_SA_NAME + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + - name: AZIMUTH_SA_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: ghcr.io/azimuth-cloud/azimuth-api:master imagePullPolicy: IfNotPresent livenessProbe: httpGet: @@ -261,13 +268,7 @@ templated manifests should match snapshot: Cg== 06-ssh-key-store.yaml: | QVpJTVVUSDoKICBTU0hfS0VZX1NUT1JFOgogICAgRkFDVE9SWTogYXppbXV0aC5rZXlzdG9yZS5wcm92aWRlci5Qcm92aWRlcktleVN0b3JlCg== - 07-disable-kubernetes.yaml: | - Cg== - 08-awx.yaml: | - Cg== - 09-awx-passwords.py: | - Cg== - 10-disable-caas.yaml: | + 07-feature-flags.yaml: | QVpJTVVUSDoKICBDTFVTVEVSX0VOR0lORTogbnVsbAo= 11-scheduling.yaml: | QVpJTVVUSDoKICBTQ0hFRFVMSU5HOgogICAgRU5BQkxFRDogZmFsc2UK From 844728c1a30c8507e82f3d2d9881f23341dba3ba Mon Sep 17 00:00:00 2001 From: Mathieu Grzybek Date: Wed, 24 Jun 2026 16:53:52 +0200 Subject: [PATCH 5/6] fix: add bind/escalate verbs to ClusterRole and improve RBAC error handling The API SA could not create the per-namespace secrets Role because Kubernetes escalation-prevention blocks granting permissions the SA does not already hold. Missing `bind` caused the same rejection for the RoleBinding. Also convert unhandled RBAC ApiErrors in the CRD driver to typed CommunicationError so views return JSON instead of a raw HTML 500, and add explicit 403 messages in ensure_secrets_rbac pointing to the missing ClusterRole verbs. Signed-off-by: Mathieu Grzybek --- .../cluster_engine/drivers/crd/driver.py | 7 +- api/azimuth/utils.py | 84 ++++++++++++------- chart/templates/api/clusterrole.yaml | 2 + .../__snapshot__/snapshot_test.yaml.snap | 2 + 4 files changed, 62 insertions(+), 33 deletions(-) diff --git a/api/azimuth/cluster_engine/drivers/crd/driver.py b/api/azimuth/cluster_engine/drivers/crd/driver.py index 47ec5684..827fd575 100644 --- a/api/azimuth/cluster_engine/drivers/crd/driver.py +++ b/api/azimuth/cluster_engine/drivers/crd/driver.py @@ -27,7 +27,12 @@ def get_k8s_client(ctx: dto.Context, ensure_namespace: bool = False): client = ekconfig.sync_client() client.default_namespace = utils.get_namespace(client, ctx.tenancy) if ensure_namespace: - utils.ensure_namespace(client, client.default_namespace, ctx.tenancy) + try: + utils.ensure_namespace(client, client.default_namespace, ctx.tenancy) + except easykube.ApiError as exc: + raise errors.CommunicationError(str(exc)) from exc + except RuntimeError as exc: + raise errors.CommunicationError(str(exc)) from exc return client diff --git a/api/azimuth/utils.py b/api/azimuth/utils.py index d7cdf484..adeda4b5 100644 --- a/api/azimuth/utils.py +++ b/api/azimuth/utils.py @@ -137,40 +137,60 @@ def ensure_secrets_rbac(ekclient, namespace: str): rbac = ekclient.api("rbac.authorization.k8s.io/v1") - rbac.resource("roles").create_or_patch( - _SECRETS_ROLE_NAME, - { - "metadata": {"name": _SECRETS_ROLE_NAME}, - "rules": [ - { - "apiGroups": [""], - "resources": ["secrets"], - "verbs": ["list", "get", "create", "update", "patch", "delete"], - } - ], - }, - namespace=namespace, - ) + try: + rbac.resource("roles").create_or_patch( + _SECRETS_ROLE_NAME, + { + "metadata": {"name": _SECRETS_ROLE_NAME}, + "rules": [ + { + "apiGroups": [""], + "resources": ["secrets"], + "verbs": ["list", "get", "create", "update", "patch", "delete"], + } + ], + }, + namespace=namespace, + ) + except easykube.ApiError as exc: + if exc.response.status_code == 403: + raise RuntimeError( + f"Permission denied creating secrets Role '{_SECRETS_ROLE_NAME}' " + f"in namespace '{namespace}': the API ClusterRole must include " + f"'escalate' on roles (Kubernetes escalation-prevention). " + f"Kubernetes said: {exc}" + ) from exc + raise - rbac.resource("rolebindings").create_or_patch( - _SECRETS_ROLE_NAME, - { - "metadata": {"name": _SECRETS_ROLE_NAME}, - "subjects": [ - { - "kind": "ServiceAccount", - "name": sa_name, - "namespace": sa_namespace, - } - ], - "roleRef": { - "apiGroup": "rbac.authorization.k8s.io", - "kind": "Role", - "name": _SECRETS_ROLE_NAME, + try: + rbac.resource("rolebindings").create_or_patch( + _SECRETS_ROLE_NAME, + { + "metadata": {"name": _SECRETS_ROLE_NAME}, + "subjects": [ + { + "kind": "ServiceAccount", + "name": sa_name, + "namespace": sa_namespace, + } + ], + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": _SECRETS_ROLE_NAME, + }, }, - }, - namespace=namespace, - ) + namespace=namespace, + ) + except easykube.ApiError as exc: + if exc.response.status_code == 403: + raise RuntimeError( + f"Permission denied creating secrets RoleBinding '{_SECRETS_ROLE_NAME}' " + f"in namespace '{namespace}': the API ClusterRole must include " + f"'bind' on roles (Kubernetes bind-prevention). " + f"Kubernetes said: {exc}" + ) from exc + raise def ensure_namespace(ekclient, namespace: str, tenancy: dto.Tenancy): diff --git a/chart/templates/api/clusterrole.yaml b/chart/templates/api/clusterrole.yaml index 2cba2be1..aa681d5b 100644 --- a/chart/templates/api/clusterrole.yaml +++ b/chart/templates/api/clusterrole.yaml @@ -82,6 +82,8 @@ rules: - get - create - patch + - bind + - escalate - apiGroups: - caas.azimuth.stackhpc.com resources: diff --git a/chart/tests/__snapshot__/snapshot_test.yaml.snap b/chart/tests/__snapshot__/snapshot_test.yaml.snap index 4ff85de6..c30ff71e 100755 --- a/chart/tests/__snapshot__/snapshot_test.yaml.snap +++ b/chart/tests/__snapshot__/snapshot_test.yaml.snap @@ -90,6 +90,8 @@ templated manifests should match snapshot: - get - create - patch + - bind + - escalate - apiGroups: - caas.azimuth.stackhpc.com resources: From e1330748ececa6ccc59bb109da2150a79d34b737 Mon Sep 17 00:00:00 2001 From: Mathieu Grzybek Date: Wed, 24 Jun 2026 17:00:18 +0200 Subject: [PATCH 6/6] syntax: line to long Signed-off-by: Mathieu Grzybek --- api/azimuth/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/azimuth/utils.py b/api/azimuth/utils.py index adeda4b5..1d5ee2f0 100644 --- a/api/azimuth/utils.py +++ b/api/azimuth/utils.py @@ -185,10 +185,10 @@ def ensure_secrets_rbac(ekclient, namespace: str): except easykube.ApiError as exc: if exc.response.status_code == 403: raise RuntimeError( - f"Permission denied creating secrets RoleBinding '{_SECRETS_ROLE_NAME}' " - f"in namespace '{namespace}': the API ClusterRole must include " - f"'bind' on roles (Kubernetes bind-prevention). " - f"Kubernetes said: {exc}" + f"Permission denied creating secrets RoleBinding" + f" '{_SECRETS_ROLE_NAME}' in namespace '{namespace}':" + f" the API ClusterRole must include 'bind' on roles" + f" (Kubernetes bind-prevention). Kubernetes said: {exc}" ) from exc raise