From 1353abe9e07df414449a6d769fc0c4f655675b45 Mon Sep 17 00:00:00 2001 From: Alexey Masolov Date: Mon, 4 May 2026 16:15:57 +1000 Subject: [PATCH 1/6] feat: add configurable namespace for activation job pods Add activation_job_namespace field to the activation_worker section of the EDA CRD. When set, the operator injects EDA_ACTIVATION_JOB_NAMESPACE into the activation worker ConfigMap and creates a Role + RoleBinding in the target namespace so the EDA ServiceAccount can manage Jobs, Pods, Secrets, and Services there. Changes: - CRD: new optional string field activation_worker.activation_job_namespace - Role defaults: activation_job_namespace defaults to empty string - ConfigMap template: conditionally sets EDA_ACTIVATION_JOB_NAMESPACE - New RBAC template for cross-namespace Role and RoleBinding - Deploy task: applies/removes cross-namespace RBAC conditionally - ClusterRole + ClusterRoleBinding for operator to manage RBAC in the target namespace Closes ansible/eda-server-operator#344 Signed-off-by: Alexey Masolov Co-authored-by: Cursor --- config/crd/bases/eda.ansible.com_edas.yaml | 7 ++ .../rbac/activation_job_namespace_role.yaml | 30 +++++++++ ...activation_job_namespace_role_binding.yaml | 13 ++++ config/rbac/kustomization.yaml | 2 + roles/eda/defaults/main.yml | 1 + roles/eda/tasks/deploy_eda.yml | 38 +++++++++++ .../eda-activation-job-namespace-rbac.yaml.j2 | 65 +++++++++++++++++++ roles/eda/templates/eda.configmap.yaml.j2 | 4 ++ 8 files changed, 160 insertions(+) create mode 100644 config/rbac/activation_job_namespace_role.yaml create mode 100644 config/rbac/activation_job_namespace_role_binding.yaml create mode 100644 roles/eda/templates/eda-activation-job-namespace-rbac.yaml.j2 diff --git a/config/crd/bases/eda.ansible.com_edas.yaml b/config/crd/bases/eda.ansible.com_edas.yaml index d5230b57..7af67851 100644 --- a/config/crd/bases/eda.ansible.com_edas.yaml +++ b/config/crd/bases/eda.ansible.com_edas.yaml @@ -1479,6 +1479,13 @@ spec: activation_worker: description: Defines desired state of eda-activation-worker resources properties: + activation_job_namespace: + description: Kubernetes namespace where activation job pods are + created. When set, jobs run in this namespace instead of the + EDA operator namespace. The operator creates the necessary + RBAC to allow the EDA service account to manage resources + in the target namespace. + type: string node_selector: additionalProperties: type: string diff --git a/config/rbac/activation_job_namespace_role.yaml b/config/rbac/activation_job_namespace_role.yaml new file mode 100644 index 00000000..e3bccf57 --- /dev/null +++ b/config/rbac/activation_job_namespace_role.yaml @@ -0,0 +1,30 @@ +--- +# Cluster-scoped because the target namespace is user-configurable at +# runtime via spec.activation_worker.activation_job_namespace. The +# operator must be able to create Role/RoleBinding resources in whatever +# namespace the user specifies. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: eda-activation-job-namespace-manager +rules: + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + - rolebindings + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/config/rbac/activation_job_namespace_role_binding.yaml b/config/rbac/activation_job_namespace_role_binding.yaml new file mode 100644 index 00000000..4e1ebee7 --- /dev/null +++ b/config/rbac/activation_job_namespace_role_binding.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: eda-activation-job-namespace-manager-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: eda-activation-job-namespace-manager +subjects: + - kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 02139c5c..88a7d201 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -12,3 +12,5 @@ resources: - metrics_auth_role.yaml - metrics_auth_role_binding.yaml - metrics_reader_role.yaml +- activation_job_namespace_role.yaml +- activation_job_namespace_role_binding.yaml diff --git a/roles/eda/defaults/main.yml b/roles/eda/defaults/main.yml index a5a30d11..b62aa207 100644 --- a/roles/eda/defaults/main.yml +++ b/roles/eda/defaults/main.yml @@ -58,6 +58,7 @@ _activation_worker: memory: 150Mi node_selector: {} tolerations: [] + activation_job_namespace: "" # Note: Deprecated "worker: {}" is intentionally excluded here so we know if the user set it _worker: {} diff --git a/roles/eda/tasks/deploy_eda.yml b/roles/eda/tasks/deploy_eda.yml index 144899de..bc25dcb1 100644 --- a/roles/eda/tasks/deploy_eda.yml +++ b/roles/eda/tasks/deploy_eda.yml @@ -38,6 +38,44 @@ wait: yes when: public_base_url is defined +- name: Look up existing ConfigMap for previous activation_job_namespace + kubernetes.core.k8s_info: + api_version: v1 + kind: ConfigMap + namespace: "{{ ansible_operator_meta.namespace }}" + name: "{{ ansible_operator_meta.name }}-{{ deployment_type }}-env-properties" + register: _eda_env_cm + +- name: Record previous activation_job_namespace from ConfigMap + ansible.builtin.set_fact: + _previous_activation_job_namespace: >- + {{ (_eda_env_cm.resources | first).data.EDA_ACTIVATION_JOB_NAMESPACE | default('') }} + when: + - _eda_env_cm.resources | length > 0 + - (_eda_env_cm.resources | first).data is defined + +- name: Apply cross-namespace RBAC for activation job pods + k8s: + apply: yes + definition: "{{ lookup('template', 'eda-activation-job-namespace-rbac.yaml.j2') }}" + wait: yes + when: combined_activation_worker.activation_job_namespace | default('') | length > 0 + +- name: Remove cross-namespace RBAC when activation_job_namespace is unset + k8s: + state: absent + api_version: rbac.authorization.k8s.io/v1 + kind: "{{ item.kind }}" + name: "{{ ansible_operator_meta.name }}-activation-job-manager" + namespace: "{{ _previous_activation_job_namespace }}" + loop: + - { kind: RoleBinding } + - { kind: Role } + when: + - combined_activation_worker.activation_job_namespace | default('') | length == 0 + - _previous_activation_job_namespace | default('') | length > 0 + ignore_errors: yes + - name: Apply Backend deployment resources k8s: apply: yes diff --git a/roles/eda/templates/eda-activation-job-namespace-rbac.yaml.j2 b/roles/eda/templates/eda-activation-job-namespace-rbac.yaml.j2 new file mode 100644 index 00000000..eeb90bfd --- /dev/null +++ b/roles/eda/templates/eda-activation-job-namespace-rbac.yaml.j2 @@ -0,0 +1,65 @@ +# RBAC resources for cross-namespace activation job pods. +# Created when activation_job_namespace is set on the EDA CR. +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: '{{ ansible_operator_meta.name }}-activation-job-manager' + namespace: '{{ combined_activation_worker.activation_job_namespace }}' + labels: + {{ lookup("template", "../common/templates/labels/common.yaml.j2") | indent(width=4) | trim }} +rules: + - apiGroups: + - "" + resources: + - secrets + - pods + - pods/log + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: '{{ ansible_operator_meta.name }}-activation-job-manager' + namespace: '{{ combined_activation_worker.activation_job_namespace }}' + labels: + {{ lookup("template", "../common/templates/labels/common.yaml.j2") | indent(width=4) | trim }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: '{{ ansible_operator_meta.name }}-activation-job-manager' +subjects: + - kind: ServiceAccount + name: '{{ ansible_operator_meta.name }}' + namespace: '{{ ansible_operator_meta.namespace }}' diff --git a/roles/eda/templates/eda.configmap.yaml.j2 b/roles/eda/templates/eda.configmap.yaml.j2 index 55f62f7b..69f5b80d 100644 --- a/roles/eda/templates/eda.configmap.yaml.j2 +++ b/roles/eda/templates/eda.configmap.yaml.j2 @@ -34,6 +34,10 @@ data: EDA_STATIC_URL: /api/eda/static/ +{% if combined_activation_worker.activation_job_namespace | default('') | length > 0 %} + EDA_ACTIVATION_JOB_NAMESPACE: "{{ combined_activation_worker.activation_job_namespace }}" +{% endif %} + # Custom user variables {% for item in extra_settings | default([]) %} {{ item.setting | upper }}: "{{ item.value }}" From 41049e5e15a30792792171804a18b795e8855ae4 Mon Sep 17 00:00:00 2001 From: Alexey Masolov Date: Mon, 18 May 2026 22:05:52 +1000 Subject: [PATCH 2/6] ci: add activation_job_namespace scenario to PR workflow Add a CI test scenario for the new activation_job_namespace parameter. Creates a dedicated CR fixture and pre-creates the target namespace before applying the CR. Signed-off-by: Alexey Masolov Co-authored-by: Cursor --- .ci/eda_v1alpha1_eda.activation_job_namespace.ci.yaml | 11 +++++++++++ .github/workflows/pr.yml | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 .ci/eda_v1alpha1_eda.activation_job_namespace.ci.yaml diff --git a/.ci/eda_v1alpha1_eda.activation_job_namespace.ci.yaml b/.ci/eda_v1alpha1_eda.activation_job_namespace.ci.yaml new file mode 100644 index 00000000..77d1b39d --- /dev/null +++ b/.ci/eda_v1alpha1_eda.activation_job_namespace.ci.yaml @@ -0,0 +1,11 @@ +apiVersion: eda.ansible.com/v1alpha1 +kind: EDA +metadata: + name: eda-demo + annotations: + "ansible.sdk.operatorframework.io/verbosity": "5" +spec: + no_log: false + automation_server_url: http://foo.bar + activation_worker: + activation_job_namespace: "eda-jobs" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6feadce6..820a7fb1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,6 +16,7 @@ jobs: - SCENARIO: externaldb - SCENARIO: ingress - SCENARIO: event_persistence + - SCENARIO: activation_job_namespace steps: - name: Checkout sources uses: actions/checkout@v4 @@ -68,6 +69,10 @@ jobs: run: kubectl apply -f .ci/eda-event-stream-external-database.secret.yaml if: ${{ matrix.SCENARIO == 'externaldb' }} + - name: Create activation job namespace + run: kubectl create namespace eda-jobs + if: ${{ matrix.SCENARIO == 'activation_job_namespace' }} + - name: Create the EDA demo CR run: | kubectl apply -f .ci/eda_v1alpha1_eda.${{ matrix.SCENARIO }}.ci.yaml From 9fb50974ce064c33e1759a609ff71c02a3ee4a09 Mon Sep 17 00:00:00 2001 From: Alexey Masolov Date: Mon, 18 May 2026 22:11:57 +1000 Subject: [PATCH 3/6] fix: capture previous namespace before ConfigMap apply and handle namespace changes Move the lookup of _previous_activation_job_namespace to run before the ConfigMap is applied, so the old value is read before it gets overwritten. Also widen the RBAC cleanup condition to trigger whenever the previous namespace differs from the new one (not just when the new value is empty), so changing from namespace A to B correctly removes orphaned Role and RoleBinding from A. Signed-off-by: Alexey Masolov Co-authored-by: Cursor --- roles/eda/tasks/deploy_eda.yml | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/roles/eda/tasks/deploy_eda.yml b/roles/eda/tasks/deploy_eda.yml index bc25dcb1..d68d7dae 100644 --- a/roles/eda/tasks/deploy_eda.yml +++ b/roles/eda/tasks/deploy_eda.yml @@ -19,6 +19,22 @@ event_stream_mtls_base_url: "{{ public_base_url.rstrip('/') }}/{{ event_stream_mtls_prefix_path }}" when: public_base_url | default('') | length > 0 +- name: Look up existing ConfigMap for previous activation_job_namespace + kubernetes.core.k8s_info: + api_version: v1 + kind: ConfigMap + namespace: "{{ ansible_operator_meta.namespace }}" + name: "{{ ansible_operator_meta.name }}-{{ deployment_type }}-env-properties" + register: _eda_env_cm + +- name: Record previous activation_job_namespace from ConfigMap + ansible.builtin.set_fact: + _previous_activation_job_namespace: >- + {{ (_eda_env_cm.resources | first).data.EDA_ACTIVATION_JOB_NAMESPACE | default('') }} + when: + - _eda_env_cm.resources | length > 0 + - (_eda_env_cm.resources | first).data is defined + - name: Apply ConfigMap resources k8s: apply: yes @@ -38,22 +54,6 @@ wait: yes when: public_base_url is defined -- name: Look up existing ConfigMap for previous activation_job_namespace - kubernetes.core.k8s_info: - api_version: v1 - kind: ConfigMap - namespace: "{{ ansible_operator_meta.namespace }}" - name: "{{ ansible_operator_meta.name }}-{{ deployment_type }}-env-properties" - register: _eda_env_cm - -- name: Record previous activation_job_namespace from ConfigMap - ansible.builtin.set_fact: - _previous_activation_job_namespace: >- - {{ (_eda_env_cm.resources | first).data.EDA_ACTIVATION_JOB_NAMESPACE | default('') }} - when: - - _eda_env_cm.resources | length > 0 - - (_eda_env_cm.resources | first).data is defined - - name: Apply cross-namespace RBAC for activation job pods k8s: apply: yes @@ -61,7 +61,7 @@ wait: yes when: combined_activation_worker.activation_job_namespace | default('') | length > 0 -- name: Remove cross-namespace RBAC when activation_job_namespace is unset +- name: Remove cross-namespace RBAC from previous namespace k8s: state: absent api_version: rbac.authorization.k8s.io/v1 @@ -72,8 +72,8 @@ - { kind: RoleBinding } - { kind: Role } when: - - combined_activation_worker.activation_job_namespace | default('') | length == 0 - _previous_activation_job_namespace | default('') | length > 0 + - combined_activation_worker.activation_job_namespace | default('') != _previous_activation_job_namespace ignore_errors: yes - name: Apply Backend deployment resources From 7f7b9a13abd8b9a7c5117a63f2efe871e90b49b5 Mon Sep 17 00:00:00 2001 From: Alexey Masolov Date: Tue, 19 May 2026 07:59:46 +1000 Subject: [PATCH 4/6] fix: add escalate verb to ClusterRole and fix RBAC task ordering Add the escalate verb on the roles resource in the eda-activation-job-namespace-manager ClusterRole. Kubernetes RBAC escalation prevention blocks creating a Role that grants permissions the creator doesn't hold. The escalate verb is the standard mechanism to permit this without granting the operator broad cluster-wide access to pods, secrets, services, and jobs. Also moves the _previous_activation_job_namespace lookup to run before the ConfigMap is applied (so the old value is captured before overwrite) and widens the RBAC cleanup condition to trigger on any namespace change, not just removal. Signed-off-by: Alexey Masolov Co-authored-by: Cursor --- config/rbac/activation_job_namespace_role.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/config/rbac/activation_job_namespace_role.yaml b/config/rbac/activation_job_namespace_role.yaml index e3bccf57..94be3e0c 100644 --- a/config/rbac/activation_job_namespace_role.yaml +++ b/config/rbac/activation_job_namespace_role.yaml @@ -19,6 +19,18 @@ rules: - rbac.authorization.k8s.io resources: - roles + verbs: + - create + - delete + - escalate + - get + - list + - patch + - update + - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: - rolebindings verbs: - create From ed5c7179c82931def4da54c0d1b266df9a69e62e Mon Sep 17 00:00:00 2001 From: Alexey Masolov Date: Tue, 19 May 2026 08:55:52 +1000 Subject: [PATCH 5/6] fix: add bind verb to ClusterRole for RoleBinding creation The escalate verb fixed Role creation, but creating a RoleBinding that references a Role with elevated permissions also requires the bind verb on the roles resource. Without it, Kubernetes RBAC escalation prevention blocks the RoleBinding creation. Signed-off-by: Alexey Masolov Co-authored-by: Cursor --- config/rbac/activation_job_namespace_role.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/rbac/activation_job_namespace_role.yaml b/config/rbac/activation_job_namespace_role.yaml index 94be3e0c..09054130 100644 --- a/config/rbac/activation_job_namespace_role.yaml +++ b/config/rbac/activation_job_namespace_role.yaml @@ -20,6 +20,7 @@ rules: resources: - roles verbs: + - bind - create - delete - escalate From 82c876466b186d271890b3bc825232899623a6fb Mon Sep 17 00:00:00 2001 From: Alexey Masolov Date: Tue, 2 Jun 2026 08:56:09 +1000 Subject: [PATCH 6/6] fix: remove ignore_errors from RBAC cleanup task The k8s module with state=absent already handles 404 gracefully (treats "not found" as "already absent"), so ignore_errors is unnecessary and could mask legitimate permission or connectivity failures. Signed-off-by: Alexey Masolov --- roles/eda/tasks/deploy_eda.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/roles/eda/tasks/deploy_eda.yml b/roles/eda/tasks/deploy_eda.yml index d68d7dae..729944aa 100644 --- a/roles/eda/tasks/deploy_eda.yml +++ b/roles/eda/tasks/deploy_eda.yml @@ -74,7 +74,6 @@ when: - _previous_activation_job_namespace | default('') | length > 0 - combined_activation_worker.activation_job_namespace | default('') != _previous_activation_job_namespace - ignore_errors: yes - name: Apply Backend deployment resources k8s: