ClusterRole Primitive¶
The clusterrole primitive wraps a Kubernetes ClusterRole and manages RBAC policy rules, aggregation rules, and
object metadata within the component lifecycle.
Ownership limitation for namespaced owners
When a namespaced owner manages a cluster-scoped resource such as a ClusterRole, the framework cannot set a
controller owner reference (the scopes are incompatible). The owner reference is skipped and the skip is logged.
The ClusterRole is not garbage-collected when the owner is deleted. Manage its lifecycle explicitly (for
example with a finalizer on the owner) or use a cluster-scoped owner if automatic cleanup is required. See
Cluster-Scoped Resources for the full behavior.
Capabilities¶
| Capability | Interfaces / detail |
|---|---|
| Static lifecycle | component.Resource. No health tracking, grace periods, or suspension |
| Mutation | PolicyRulesEditor for .rules; SetAggregationRule for .aggregationRule; ObjectMetaEditor for labels and annotations |
| Cluster-scoped | MarkClusterScoped() called during construction; Build() rejects a non-empty namespace |
| Guard | concepts.Guardable: blocks reconciliation when a precondition is not met (Blocked) |
| Data extraction | concepts.DataExtractable: reads values back after each sync cycle |
See Lifecycle Interfaces for the full interface-to-status mapping. For cluster-scoped builder behavior, see Cluster-Scoped Primitives.
Building a ClusterRole Primitive¶
import "github.com/sourcehawk/operator-component-framework/pkg/primitives/clusterrole"
base := &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: "my-operator-role",
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"pods"},
Verbs: []string{"get", "list", "watch"},
},
},
}
resource, err := clusterrole.NewBuilder(base).
WithMutation(CRDAccessMutation(owner.Spec.Version, owner.Spec.ManageCRDs)).
Build()
Build() returns an error if Name is empty or if Namespace is non-empty. The constructor calls
MarkClusterScoped() internally, so you do not need to call it manually.
Identity format: rbac.authorization.k8s.io/v1/ClusterRole/<name>.
Mutations¶
Each mutation is a named clusterrole.Mutation that receives a *Mutator and records edit intent through typed
editors. See The Mutation System for the full model.
func CRDAccessMutation(version string, manageCRDs bool) clusterrole.Mutation {
return clusterrole.Mutation{
Name: "crd-access",
Feature: feature.NewVersionGate(version, nil).When(manageCRDs),
Mutate: func(m *clusterrole.Mutator) error {
m.AddRule(rbacv1.PolicyRule{
APIGroups: []string{"apiextensions.k8s.io"},
Resources: []string{"customresourcedefinitions"},
Verbs: []string{"get", "list", "watch"},
})
return nil
},
}
}
For boolean conditions, chain .When() on the gate. See
Boolean-Gated Mutations. For version constraints, see
Version-Gated Mutations.
Internal Mutation Ordering¶
Within a single mutation, edits are applied in this fixed category order regardless of the call order:
| Step | Category | What it affects |
|---|---|---|
| 1 | Metadata edits | Labels and annotations on the ClusterRole |
| 2 | Rules edits | .rules: EditRules, AddRule |
| 3 | Aggregation rule | .aggregationRule: SetAggregationRule (last call wins within each feature) |
Within each category, edits apply in registration order. Later features observe the object as modified by all earlier ones.
Relevant Editors¶
PolicyRulesEditor¶
The primary API for modifying .rules. Use m.EditRules for full control. See
Mutation Editors for the general editor model.
AddRule¶
AddRule appends a PolicyRule to the rules slice:
m.EditRules(func(e *editors.PolicyRulesEditor) error {
e.AddRule(rbacv1.PolicyRule{
APIGroups: []string{"apps"},
Resources: []string{"deployments"},
Verbs: []string{"get", "list", "watch"},
})
return nil
})
RemoveRuleByIndex¶
RemoveRuleByIndex removes the rule at the given index. No-op if the index is out of bounds:
m.EditRules(func(e *editors.PolicyRulesEditor) error {
e.RemoveRuleByIndex(0) // remove the first rule
return nil
})
Clear¶
Clear removes all rules:
Raw Escape Hatch¶
Raw() returns a pointer to the underlying []rbacv1.PolicyRule for free-form editing:
m.EditRules(func(e *editors.PolicyRulesEditor) error {
raw := e.Raw()
*raw = append(*raw, customRules...)
return nil
})
ObjectMetaEditor¶
Modifies labels and annotations via m.EditObjectMetadata. Available methods: EnsureLabel, RemoveLabel,
EnsureAnnotation, RemoveAnnotation, Raw.
m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error {
e.EnsureLabel("app.kubernetes.io/version", version)
e.EnsureAnnotation("managed-by", "my-operator")
return nil
})
Convenience Methods¶
The *Mutator exposes a direct convenience method for the most common .rules operation:
| Method | Equivalent to |
|---|---|
AddRule(rule) |
EditRules → e.AddRule(rule) |
Use AddRule for simple, single-rule mutations. Use EditRules when you need multiple operations or raw access in a
single edit block.
SetAggregationRule¶
SetAggregationRule sets the ClusterRole's .aggregationRule field. An aggregation rule causes the API server to
combine rules from ClusterRoles whose labels match the provided selectors, instead of using .rules directly:
m.SetAggregationRule(&rbacv1.AggregationRule{
ClusterRoleSelectors: []metav1.LabelSelector{
{MatchLabels: map[string]string{"rbac.example.com/aggregate-to-admin": "true"}},
},
})
Pass nil to clear the aggregation rule. Within a single feature, the last SetAggregationRule call wins.
Note
The Kubernetes API ignores .rules when .aggregationRule is set. The two approaches are mutually exclusive.
Data Extraction¶
WithDataExtractor runs a callback after successful reconciliation with a value copy of the reconciled ClusterRole:
resource, err := clusterrole.NewBuilder(base).
WithDataExtractor(func(cr rbacv1.ClusterRole) error {
sharedState.ClusterRoleName = cr.Name
return nil
}).
Build()
Full Example¶
func CoreRulesMutation() clusterrole.Mutation {
return clusterrole.Mutation{
Name: "core-rules",
Mutate: func(m *clusterrole.Mutator) error {
m.AddRule(rbacv1.PolicyRule{
APIGroups: []string{""},
Resources: []string{"pods", "services", "configmaps"},
Verbs: []string{"get", "list", "watch"},
})
return nil
},
}
}
func CRDAccessMutation(version string, manageCRDs bool) clusterrole.Mutation {
return clusterrole.Mutation{
Name: "crd-access",
Feature: feature.NewVersionGate(version, nil).When(manageCRDs),
Mutate: func(m *clusterrole.Mutator) error {
m.AddRule(rbacv1.PolicyRule{
APIGroups: []string{"apiextensions.k8s.io"},
Resources: []string{"customresourcedefinitions"},
Verbs: []string{"get", "list", "watch"},
})
return nil
},
}
}
resource, err := clusterrole.NewBuilder(base).
WithMutation(CoreRulesMutation()).
WithMutation(CRDAccessMutation(owner.Spec.Version, owner.Spec.ManageCRDs)).
Build()
When ManageCRDs is true, the final rules include both core and CRD access rules. When false, only the core rules are
written. Neither mutation needs to know about the other.
Guidance¶
Feature: nil applies unconditionally. Omit Feature for mutations that always run. Use
feature.NewVersionGate(version, constraints) when version gating is needed, and chain .When(bool) for boolean
conditions.
Use AddRule for composable permissions. AddRule lets each feature contribute rules without knowing about others.
Using SetRules (via Raw) in multiple features means the last write wins; use that only when full replacement is the
intended semantics.
Use SetAggregationRule for composite roles. When you want the API server to aggregate rules from multiple
ClusterRoles via label selectors, call SetAggregationRule instead of managing .rules directly. Do not mix both
approaches on the same role.
Cluster-scoped resources are not garbage-collected by namespaced owners. A namespaced custom resource cannot own a
cluster-scoped ClusterRole. Handle deletion explicitly, for example by adding a finalizer on the owner that deletes
the ClusterRole before the owner is removed.