Skip to content

Role Primitive

The role primitive wraps a Kubernetes Role and manages RBAC policy rules and object metadata within the component lifecycle.

Capabilities

Capability Interfaces / detail
Static lifecycle component.Resource. No health tracking, grace periods, or suspension
Mutation PolicyRulesEditor for .rules; ObjectMetaEditor for labels and annotations
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.

Building a Role Primitive

import "github.com/sourcehawk/operator-component-framework/pkg/primitives/role"

base := &rbacv1.Role{
    ObjectMeta: metav1.ObjectMeta{
        Name:      "app-role",
        Namespace: owner.Namespace,
    },
    Rules: []rbacv1.PolicyRule{
        {
            APIGroups: []string{""},
            Resources: []string{"pods"},
            Verbs:     []string{"get", "list", "watch"},
        },
    },
}

resource, err := role.NewBuilder(base).
    WithMutation(SecretAccessMutation(owner.Spec.Version, owner.Spec.EnableSecretAccess)).
    Build()

Build() returns an error if Name or Namespace is empty.

Identity format: rbac.authorization.k8s.io/v1/Role/<namespace>/<name>.

Mutations

Each mutation is a named role.Mutation that receives a *Mutator and records edit intent through typed editors. See The Mutation System for the full model.

func SecretAccessMutation(version string, enabled bool) role.Mutation {
    return role.Mutation{
        Name:    "secret-access",
        Feature: feature.NewVersionGate(version, nil).When(enabled),
        Mutate: func(m *role.Mutator) error {
            m.EditRules(func(e *editors.PolicyRulesEditor) error {
                e.AddRule(rbacv1.PolicyRule{
                    APIGroups: []string{""},
                    Resources: []string{"secrets"},
                    Verbs:     []string{"get", "list"},
                })
                return nil
            })
            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 Role
2 Rules edits .rules: SetRules, AddRule, Raw

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.

SetRules

SetRules replaces the entire rules slice atomically. Use this when a mutation should define the complete set of rules, discarding any previously accumulated entries.

m.EditRules(func(e *editors.PolicyRulesEditor) error {
    e.SetRules([]rbacv1.PolicyRule{
        {APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"get", "list", "watch"}},
    })
    return nil
})

AddRule

AddRule appends a single rule to the existing rules slice. Use this when a feature contributes additional permissions without needing to know about rules from other features.

m.EditRules(func(e *editors.PolicyRulesEditor) error {
    e.AddRule(rbacv1.PolicyRule{
        APIGroups: []string{""},
        Resources: []string{"configmaps"},
        Verbs:     []string{"get", "watch"},
    })
    return nil
})

Raw Escape Hatch

Raw() returns a pointer to the underlying []rbacv1.PolicyRule for direct manipulation when none of the structured methods are sufficient:

m.EditRules(func(e *editors.PolicyRulesEditor) error {
    raw := e.Raw()
    filtered := (*raw)[:0]
    for _, r := range *raw {
        if !containsVerb(r.Verbs, "create") {
            filtered = append(filtered, r)
        }
    }
    *raw = filtered
    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
})

Data Extraction

WithDataExtractor runs a callback after successful reconciliation with a value copy of the reconciled Role. Use it to surface the applied rules or metadata to other resources:

resource, err := role.NewBuilder(base).
    WithDataExtractor(func(r rbacv1.Role) error {
        sharedState.RoleName = r.Name
        return nil
    }).
    Build()

Full Example

func BaseRuleMutation(version string) role.Mutation {
    return role.Mutation{
        Name:    "base-rules",
        Feature: feature.NewVersionGate(version, nil),
        Mutate: func(m *role.Mutator) error {
            m.EditRules(func(e *editors.PolicyRulesEditor) error {
                e.SetRules([]rbacv1.PolicyRule{
                    {APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"get", "list", "watch"}},
                })
                return nil
            })
            return nil
        },
    }
}

func SecretAccessMutation(version string, enabled bool) role.Mutation {
    return role.Mutation{
        Name:    "secret-access",
        Feature: feature.NewVersionGate(version, nil).When(enabled),
        Mutate: func(m *role.Mutator) error {
            m.EditRules(func(e *editors.PolicyRulesEditor) error {
                e.AddRule(rbacv1.PolicyRule{
                    APIGroups: []string{""},
                    Resources: []string{"secrets"},
                    Verbs:     []string{"get", "list"},
                })
                return nil
            })
            return nil
        },
    }
}

resource, err := role.NewBuilder(base).
    WithMutation(BaseRuleMutation(owner.Spec.Version)).
    WithMutation(SecretAccessMutation(owner.Spec.Version, owner.Spec.EnableSecretAccess)).
    Build()

When EnableSecretAccess is true, the final Role contains both the base pod rules and the secrets rule. When false, only the base rules are applied. 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. When multiple features contribute rules to the same Role, AddRule lets each feature add its permissions independently. SetRules in multiple features means the last registration wins; only use that when full replacement is the intended semantics.

PolicyRule has no unique key. There is no upsert or remove-by-key operation on rules. Use SetRules to replace atomically, AddRule to accumulate, or Raw() for arbitrary manipulation including filtering.

Register mutations in dependency order. If mutation B relies on rules set by mutation A, register A first.