Skip to content

Conversation

@camilamacedo86
Copy link
Member

@camilamacedo86 camilamacedo86 commented Jan 6, 2026

Description

Fixes handling of namespace-scoped RBAC resources (Role, RoleBinding) in cross-namespace scenarios for the helm/v2-alpha plugin.

Problem: When multiple namespace-scoped Roles/RoleBindings exist with the same name in different namespaces (e.g., for leader election in production, cross-namespace permissions in infrastructure), they would overwrite each other's files, causing missing RBAC permissions at runtime.

Solution: This PR ensures:

  1. Cross-namespace RBAC resources get unique filenames (with namespace suffix)
  2. Cross-namespace RBAC metadata preserves explicit namespace
  3. Manager-namespace RBAC resources are properly templated to {{ .Release.Namespace }}
  4. All RBAC resources always go to rbac/ directory (never extras/)

Behavior

RBAC Namespace Filename Suffix Namespace Value
== manager No {{ .Release.Namespace }}
!= manager Yes Original value preserved

Examples

Example 1: Role in Manager Namespace

Input (from kustomize):

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-leader-election-role
  namespace: test-project-system
rules:
- apiGroups: ["coordination.k8s.io"]
  resources: ["leases"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

Output (Helm template):

  • File: dist/chart/templates/rbac/leader-election-role.yaml
  • Content:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: {{ include "test-project.resourceName" (dict "suffix" "leader-election-role" "context" $) }}
  namespace: {{ .Release.Namespace }}  # ✅ Templated
  labels:
    {{- include "test-project.labels" . | nindent 4 }}
rules:
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete

Example 2: Role in Cross-Namespace (infrastructure)

Input (from kustomize):

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-manager-role
  namespace: infrastructure
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "patch", "update", "watch"]

Output (Helm template):

  • File: dist/chart/templates/rbac/manager-role-infrastructure.yaml
  • Content:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: {{ include "test-project.resourceName" (dict "suffix" "manager-role" "context" $) }}
  namespace: infrastructure  # ✅ Preserved (NOT templated)
  labels:
    {{- include "test-project.labels" . | nindent 4 }}
rules:
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - get
  - list
  - patch
  - update
  - watch

Example 3: RoleBinding in Cross-Namespace with Manager Subject

Input (from kustomize):

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-manager-rolebinding
  namespace: infrastructure
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: test-project-manager-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system

Output (Helm template):

  • File: dist/chart/templates/rbac/manager-rolebinding-infrastructure.yaml
  • Content:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: {{ include "test-project.resourceName" (dict "suffix" "manager-rolebinding" "context" $) }}
  namespace: infrastructure  # ✅ Preserved (cross-namespace binding)
  labels:
    {{- include "test-project.labels" . | nindent 4 }}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: {{ include "test-project.resourceName" (dict "suffix" "manager-role" "context" $) }}
subjects:
- kind: ServiceAccount
  name: {{ include "test-project.resourceName" (dict "suffix" "controller-manager" "context" $) }}
  namespace: {{ .Release.Namespace }}  # ✅ Templated (references manager namespace)

Why?

  • metadata.namespace: infrastructure → Cross-namespace binding, keep explicit
  • subjects[].namespace: test-project-system → References controller's namespace, template it

Example 4: RoleBinding with External ServiceAccount

Input (from kustomize):

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-cross-binding
  namespace: infrastructure
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
- kind: ServiceAccount
  name: external-sa
  namespace: external-namespace

Output (Helm template):

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: infrastructure  # ✅ Preserved
subjects:
- kind: ServiceAccount
  name: {{ include "test-project.resourceName" (dict "suffix" "controller-manager" "context" $) }}
  namespace: {{ .Release.Namespace }}  # ✅ Templated (was test-project-system)
- kind: ServiceAccount
  name: external-sa
  namespace: external-namespace  # ✅ Preserved (not manager namespace)

Closes: #5354

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: camilamacedo86

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added approved Indicates a PR has been approved by an approver from all required OWNERS files. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels Jan 6, 2026
@camilamacedo86
Copy link
Member Author

@AlirezaPourchali

Could you help us in the review?

@camilamacedo86
Copy link
Member Author

/test pull-kubebuilder-e2e-k8s-1-34-0

namespace := resource.GetNamespace()
if namespace != "" && (kind == "Role" || kind == "RoleBinding") {
fileName = fmt.Sprintf("%s-%s", fileName, namespace)
}
Copy link
Member Author

@camilamacedo86 camilamacedo86 Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/hold
Requires changes

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename collision fix is good.
The logic for cross-namespace RBAC filenames makes sense

@k8s-ci-robot k8s-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jan 7, 2026
@camilamacedo86 camilamacedo86 changed the title 🐛 (helm/v2-alpha): Fix namespace-scoped Role/RoleBinding filename collisions WIP 🐛 (helm/v2-alpha): Fix namespace-scoped Role/RoleBinding filename collisions Jan 7, 2026
@k8s-ci-robot k8s-ci-robot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jan 7, 2026
@camilamacedo86 camilamacedo86 changed the title WIP 🐛 (helm/v2-alpha): Fix namespace-scoped Role/RoleBinding filename collisions (helm/v2-alpha): Fix cross-namespace RBAC file naming and templating Jan 7, 2026
@k8s-ci-robot k8s-ci-robot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jan 7, 2026
Fixes handling of namespace-scoped RBAC resources (Role, RoleBinding) in
cross-namespace scenarios for leader election and cross-namespace permissions.

Changes:
- Extract manager namespace from Deployment resource
- Append namespace suffix only for cross-namespace RBAC files
- Template only manager namespace refs, preserve cross-namespace values

Result:
- manager-role.yaml (manager NS, templated)
- manager-role-infrastructure.yaml (cross-NS, preserved)

Assisted-by: Cursor
@k8s-ci-robot k8s-ci-robot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jan 7, 2026
@camilamacedo86
Copy link
Member Author

/hold cancel

@k8s-ci-robot k8s-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jan 7, 2026
@camilamacedo86 camilamacedo86 changed the title (helm/v2-alpha): Fix cross-namespace RBAC file naming and templating 🐛 (helm/v2-alpha): Fix cross-namespace RBAC file naming and templating Jan 7, 2026
Copy link

@AlirezaPourchali AlirezaPourchali left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this fix! I really appreciate you addressing issue #5354.

The filename collision prevention logic in chart_writer.go (lines 165-174) looks great - appending the namespace suffix should properly distinguish cross-namespace roles.

I tested the new changes on my project to generate the helm chart and i found an interesting bug.
if the CRD name contains the namespace name, we will have this in the ClusterRole manifest:

....
      resources:
        - {{ .Release.Namespace }}s/finalizers
      verbs:
        - update
    - apiGroups:
        - identity.me.cloud
      resources:
        - {{ .Release.Namespace }}s/status
      verbs:
        - get
        - patch
        - update

you can see my inline comment for more details.

// Replace only the manager namespace with template variable.
// Cross-namespace values (infrastructure, production, etc.) are naturally preserved
// because they don't match the manager namespace string.
yamlContent = strings.ReplaceAll(yamlContent, managerNamespace, namespaceTemplate)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This string replacement is too aggressive and will break resource names that contain the namespace as a substring.

Example bug:

  • CRD resource name: users
  • Manager namespace: user
  • Result: {{ .Release.Namespace }}s ❌ (should stay users)

Suggested fix:
Replace only in the namespace field context:

yamlContent = strings.ReplaceAll(yamlContent, "namespace: "+managerNamespace, "namespace: "+namespaceTemplate)

or even a better solution:
Parse YAML → Modify specific fields → Re-serialize
This way we know what we are changing.

Copy link
Member Author

@camilamacedo86 camilamacedo86 Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All that is under the manager namespace is the namespace we define when we install Helm: {{ .Release.Namespace }}.

If we follow your suggestion as-is, we’ll end up with a namespace bug: the namespace would be set to <project-name>-system, because that value is coming from kustomize in this case.

So, in the same way we replace all occurrences of <project-name>-system with {{ .Release.Namespace }}, this will continue to work correctly.

We just need a caveat around this, due to the ability to customize the namespace via options in the default config/kustomize configuration.

However, if the ns is not == the manager's one, then we keep it.

Copy link

@AlirezaPourchali AlirezaPourchali Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, but i think you misunderstood me.

The replacement via strings.ReplaceAll works well on everything expect on the rbac yaml files for my example.
My CRD name is users and my manager deployment file which is config/manager/manager.yaml has the namespace field namespace: user. The namespace is a substring of the CRD name and when the helm chart is being created it replaces every user word with {{ .Release.Namespace }}.
This replacement will break the RBAC files including the CRD name inside it (resources field), it will create the following rbac template file:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
    name: {{ include "osiris.resourceName" (dict "suffix" "manager-role" "context" $) }}
rules:
    - apiGroups:
        - ""
      resources:
        - configmaps
      verbs:
        - get
        - list
        - patch
        - update
        - watch
    - apiGroups:
        - apps
      resources:
        - deployments
      verbs:
        - get
        - list
        - watch
    - apiGroups:
        - identity.me.cloud
      resources:
        - {{ .Release.Namespace }}s
      verbs:
        - create
        - delete
        - get
        - list
        - patch
        - update
        - watch
    - apiGroups:
        - identity.me.cloud
      resources:
        - {{ .Release.Namespace }}s/finalizers
      verbs:
        - update
    - apiGroups:
        - identity.me.cloud
      resources:
        - {{ .Release.Namespace }}s/status
      verbs:
        - get
        - patch
        - update

{{ .Release.Namespace }}s is the issue here.
the correct generated file would be:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
    name: {{ include "osiris.resourceName" (dict "suffix" "manager-role" "context" $) }}
rules:
    - apiGroups:
        - ""
      resources:
        - configmaps
      verbs:
        - get
        - list
        - patch
        - update
        - watch
    - apiGroups:
        - apps
      resources:
        - deployments
      verbs:
        - get
        - list
        - watch
    - apiGroups:
        - identity.me.cloud
      resources:
        - users
      verbs:
        - create
        - delete
        - get
        - list
        - patch
        - update
        - watch
    - apiGroups:
        - identity.me.cloud
      resources:
        - users/finalizers
      verbs:
        - update
    - apiGroups:
        - identity.me.cloud
      resources:
        - users/status
      verbs:
        - get
        - patch
        - update

Copy link

@AlirezaPourchali AlirezaPourchali Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think treating the yaml files as a structured object would be our solution here, in that way we wouldn't blindly replace the substring inside the yaml files.

Copy link
Member Author

@camilamacedo86 camilamacedo86 Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Role that has ns == "<project-name>-system" in the install YAML must be applied in the same namespace where the manager is installed.

If you added a namespace via the RBAC markers, it won’t be replaced with "{{ .Release.Namespace }}", because that substitution is tied to "<project-name>-system". So, unless you literally set the marker namespace to "-system" (which wouldn’t make sense), Helm won’t swap it to "{{ .Release.Namespace }}".

Did you test this locally?
If so, can you please share the exact steps to reproduce the issue that you have seen?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. release-blocker size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[helm/v2-alpha] plugin ignores namespace-scoped Roles when generating Helm chart

3 participants