Skip to content

Conversation

@camilamacedo86
Copy link
Member

@camilamacedo86 camilamacedo86 commented Jan 6, 2026

What This Fixes

This PR fixes how the Helm plugin (v2-alpha) handles namespaces when converting your Kustomize project to a Helm chart.

Problems Before This Fix

❌ Missing RBAC Resources
If you used namespace-scoped Roles (like for leader election or cross-namespace permissions), they were ignored. Only ClusterRole was converted. Your Helm-deployed operator would fail with permission errors.

❌ Broken Resource Names
If your namespace was user and you had a resource called users, it would break:

  • Input: users
  • Broken output: {{ .Release.Namespace }}s 😱

❌ Incomplete DNS Replacement
Only .svc DNS names worked. Other patterns like .pod, .svc.cluster.local, or .endpoints were left with hardcoded namespaces.

❌ File Name Collisions
Multiple Roles with the same name in different namespaces would overwrite each other.

❌ Hardcoded Annotations
Things like cert-manager.io/inject-ca-from: my-namespace/cert stayed hardcoded.


What This Fixes

βœ… All RBAC Resources Converted
Both ClusterRole AND namespace-scoped Roles/RoleBindings are now converted to Helm templates.

βœ… Safe Namespace Replacement
Resource names like users, deployments, pods are never touched. Only actual namespace fields are replaced.

βœ… All Kubernetes DNS Patterns
Every DNS format works: .svc, .svc.cluster.local, .pod, .endpoints, with ports, with paths, everything.

βœ… Smart Cross-Namespace Handling

  • Your manager namespace (e.g., myapp-system) β†’ {{ .Release.Namespace }}
  • Other namespaces (e.g., infrastructure, production) β†’ kept as-is

βœ… No More File Collisions
Files get unique names: leader-election-role.yaml vs manager-role-infrastructure.yaml

βœ… Complete Templating
Annotations, DNS names, resource references - everything that should be templated is templated.


How It Works (Simple Explanation)

When you run kubebuilder edit --plugins=helm/v2-alpha, the plugin reads your Kustomize output and converts it to Helm templates.

The Smart Rules

  1. If it's YOUR namespace β†’ Replace with {{ .Release.Namespace }}
  2. If it's SOMEONE ELSE'S namespace β†’ Keep it exactly as-is
  3. If it's a RESOURCE NAME β†’ Never touch it!

Example: Your Namespace

# Kustomize (input)
namespace: myapp-system

# Helm Chart (output)
namespace: {{ .Release.Namespace }}

When someone installs your chart:

helm install my-release ./chart --namespace production

It becomes: namespace: production βœ…

Example: External Namespace

# Kustomize (input)
namespace: infrastructure

# Helm Chart (output)
namespace: infrastructure  # Kept as-is!

This is for cross-namespace permissions that must use a specific namespace.


Real-World Examples

Example 1: Leader Election Role

What you write (RBAC marker):

//+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch;delete

What Kustomize generates:

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

What the Helm plugin generates:

  • File: dist/chart/templates/rbac/leader-election-role.yaml
  • Content:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: {{ include "myapp.resourceName" (dict "suffix" "leader-election-role" "context" $) }}
  namespace: {{ .Release.Namespace }}  # ← Templated! Will use whatever namespace you install to
rules:
- apiGroups: ["coordination.k8s.io"]
  resources: ["leases"]               # ← NOT templated! Still "leases", not "lease{{ .Release.Namespace }}s"
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

Why this is important: Your operator can now be installed in ANY namespace:

helm install prod ./chart --namespace production        # Works!
helm install staging ./chart --namespace staging        # Works!
helm install dev ./chart --namespace development        # Works!

Example 2: Cross-Namespace Permissions

What you write (RBAC marker with explicit namespace):

//+kubebuilder:rbac:groups=apps,namespace=infrastructure,resources=deployments,verbs=get;list;watch

What Kustomize generates:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: myapp-manager-role
  namespace: infrastructure  # Explicit namespace!
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch"]

What the Helm plugin generates:

  • File: dist/chart/templates/rbac/manager-role-infrastructure.yaml (note the -infrastructure suffix!)
  • Content:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: {{ include "myapp.resourceName" (dict "suffix" "manager-role" "context" $) }}
  namespace: infrastructure  # ← NOT templated! Kept as "infrastructure" because it's not your manager namespace
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]  # ← NOT templated! Still "deployments"
  verbs: ["get", "list", "watch"]

Why this is important: You can have permissions in specific namespaces:

# Your operator installs to "myapp-prod" but needs to watch deployments in "infrastructure"
helm install myapp ./chart --namespace myapp-prod

# Result:
# - Role created in "infrastructure" namespace (as specified)
# - Your operator runs in "myapp-prod" namespace (from --namespace)

Example 3: ConfigMap with Service URLs

What Kustomize generates:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: myapp-system
data:
  # Internal service (same namespace)
  database-url: "postgresql://db.myapp-system.svc:5432/mydb"
  
  # Internal service with full DNS
  redis-url: "redis://cache.myapp-system.svc.cluster.local:6379"
  
  # External service (different namespace)
  monitoring-url: "http://prometheus.monitoring-system.svc:9090"
  
  # Certificate reference
  tls-cert: "myapp-system/tls-cert"

What the Helm plugin generates:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "myapp.resourceName" (dict "suffix" "app-config" "context" $) }}
  namespace: {{ .Release.Namespace }}  # ← Templated
data:
  # Internal services - namespace is templated
  database-url: "postgresql://db.{{ .Release.Namespace }}.svc:5432/mydb"
  redis-url: "redis://cache.{{ .Release.Namespace }}.svc.cluster.local:6379"
  
  # External service - namespace is PRESERVED
  monitoring-url: "http://prometheus.monitoring-system.svc:9090"
  
  # Reference - templated
  tls-cert: "{{ .Release.Namespace }}/tls-cert"

Why this is important: Your service URLs automatically work in any namespace:

helm install myapp ./chart --namespace staging

# Results in:
# database-url: "postgresql://db.staging.svc:5432/mydb"
# redis-url: "redis://cache.staging.svc.cluster.local:6379"
# monitoring-url: "http://prometheus.monitoring-system.svc:9090"  # Still points to monitoring-system!

Example 4: The Substring Bug Fix

Scenario: Your namespace is user and you have a resource type called users.

Before this fix (BROKEN ❌):

# Input
namespace: user
rules:
- resources:
  - users
  - users/finalizers

# Broken output (blind string replacement)
namespace: {{ .Release.Namespace }}
rules:
- resources:
  - {{ .Release.Namespace }}s        # BROKEN! 😱
  - {{ .Release.Namespace }}s/finalizers  # BROKEN! 😱

After this fix (CORRECT βœ…):

# Input
namespace: user
rules:
- resources:
  - users
  - users/finalizers

# Correct output (smart replacement)
namespace: {{ .Release.Namespace }}
rules:
- resources:
  - users              # βœ… Preserved!
  - users/finalizers   # βœ… Preserved!

How it works: The code uses smart patterns that only match actual namespaces:

  • namespace: user β†’ Has namespace: field, replace it
  • users β†’ Just a word, no namespace field, keep it

What Gets Templated vs Preserved

βœ… Gets Templated (Manager Namespace β†’ {{ .Release.Namespace }})

Context Example Input Example Output
Namespace field namespace: myapp-system namespace: {{ .Release.Namespace }}
Service DNS api.myapp-system.svc api.{{ .Release.Namespace }}.svc
Full DNS api.myapp-system.svc.cluster.local api.{{ .Release.Namespace }}.svc.cluster.local
DNS with port api.myapp-system.svc:8080 api.{{ .Release.Namespace }}.svc:8080
Pod DNS pod.myapp-system.pod.cluster.local pod.{{ .Release.Namespace }}.pod.cluster.local
Annotation refs myapp-system/my-cert {{ .Release.Namespace }}/my-cert
ConfigMap refs configMapRef: myapp-system/config configMapRef: {{ .Release.Namespace }}/config

βœ… Gets Preserved (External Namespaces)

Context Example Why
Cross-namespace Role namespace: infrastructure Explicit namespace for cross-namespace permissions
External service .monitoring-system.svc Service in another namespace
External reference external-namespace/resource Reference to resource in another namespace

βœ… Never Touched (Resource Names)

Context Example Why
Resource types users, deployments, pods These are Kubernetes resource types, not namespaces
Labels app: frontend Labels are not namespaces
Arbitrary strings my-application Just regular strings

File Organization

RBAC Files Go to rbac/ Directory

All RBAC resources are organized in dist/chart/templates/rbac/:

dist/chart/templates/rbac/
β”œβ”€β”€ controller-manager.yaml           (ServiceAccount)
β”œβ”€β”€ manager-role.yaml                 (ClusterRole)
β”œβ”€β”€ manager-rolebinding.yaml          (ClusterRoleBinding)
β”œβ”€β”€ leader-election-role.yaml         (Role in manager namespace)
β”œβ”€β”€ leader-election-rolebinding.yaml  (RoleBinding in manager namespace)
β”œβ”€β”€ manager-role-infrastructure.yaml  (Role in "infrastructure" namespace)
β”œβ”€β”€ manager-rolebinding-infrastructure.yaml (RoleBinding in "infrastructure")
└── ...

Note: They're never in extras/, even custom ServiceAccounts.

Filename Rules

Resource Type Namespace Example Filename
ClusterRole (none) manager-role.yaml
Role Manager namespace leader-election-role.yaml
Role Cross-namespace manager-role-infrastructure.yaml
RoleBinding Manager namespace leader-election-rolebinding.yaml
RoleBinding Cross-namespace manager-rolebinding-infrastructure.yaml

The -<namespace> suffix prevents file collisions when you have multiple Roles with the same name in different namespaces.


Before vs After Comparison

Before This Fix

$ kubebuilder edit --plugins=helm/v2-alpha
$ ls dist/chart/templates/rbac/
manager-role.yaml              # ← Only ClusterRole
manager-rolebinding.yaml       # ← Only ClusterRoleBinding
# WHERE IS MY LEADER ELECTION ROLE?? 😱

$ helm install myapp dist/chart/ --namespace production
# Result: Deployment fails with "forbidden: User cannot get leases" πŸ’₯

After This Fix

$ kubebuilder edit --plugins=helm/v2-alpha
$ ls dist/chart/templates/rbac/
controller-manager.yaml
manager-role.yaml
manager-rolebinding.yaml
leader-election-role.yaml              # ← NOW INCLUDED! βœ…
leader-election-rolebinding.yaml       # ← NOW INCLUDED! βœ…
manager-role-infrastructure.yaml       # ← Cross-namespace roles too! βœ…
manager-rolebinding-infrastructure.yaml

$ helm install myapp dist/chart/ --namespace production
# Result: Everything works! βœ…

Technical Details (For the Curious)

Why Regex Instead of YAML Parsing?

Short answer: The content has Helm templates mixed with YAML, which breaks YAML parsers.

Long answer: By the time we need to replace namespaces, the content looks like this:

metadata:
  name: {{ include "chart.name" . }}    # ← Helm template (not valid YAML!)
  namespace: myapp-system                # ← We need to replace this
  {{- if .Values.certManager.enable }}  # ← Helm conditional (not valid YAML!)
  annotations:
    cert-manager: enabled
  {{- end }}

If we try to parse this as YAML:

yaml.Unmarshal(yamlContent, &data)
// Error: "could not find expected ':'"

So we use smart regex patterns that only match what we want:

  1. Namespace fields: namespace: myapp-system (only if it's on its own line)
  2. DNS names: .myapp-system.svc (only if there are dots on both sides)
  3. References: myapp-system/cert (only if followed by /)

Safety Guarantees

Each pattern is designed to prevent false matches:

Pattern What It Matches What It Skips
(?m)^(\s*)namespace:\s+<value>\s*$ namespace: myapp-system message: "namespace: myapp-system" (in string)
\b<value>/ myapp-system/cert https://example.com/myapp-system/docs (URL)
\.<value>\. .myapp-system.svc users (no dots), app=myapp-system (no dots on both sides)

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
@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.

@camilamacedo86 camilamacedo86 force-pushed the fix-bug-roles branch 3 times, most recently from 8238663 to efddbe6 Compare January 10, 2026 09:20
@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 namespace handling Jan 10, 2026
@camilamacedo86
Copy link
Member Author

Hi @AlirezaPourchali

Thank you for all your reviews.
I think I understood your scenario.
See that I addressed changes based on your comments.
The description is either updated.

Could you please check it now?

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
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