Skip to content

ServiceAccount Primitive

The serviceaccount primitive wraps a Kubernetes ServiceAccount and manages image pull secrets, the automount token flag, and object metadata within the component lifecycle.

Capabilities

Capability Interfaces / detail
Static lifecycle component.Resource. No health tracking, grace periods, or suspension
Mutation Direct mutator methods for .imagePullSecrets and .automountServiceAccountToken; metadata editor
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 ServiceAccount Primitive

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

base := &corev1.ServiceAccount{
    ObjectMeta: metav1.ObjectMeta{
        Name:      "app-sa",
        Namespace: owner.Namespace,
    },
}

resource, err := serviceaccount.NewBuilder(base).
    WithMutation(BaseTokenMutation(owner.Spec.Version)).
    Build()

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

Identity format: v1/ServiceAccount/<namespace>/<name>.

Mutations

Each mutation is a named serviceaccount.Mutation that receives a *Mutator and records edit intent through direct methods. See The Mutation System for the full model.

func BaseTokenMutation(version string) serviceaccount.Mutation {
    return serviceaccount.Mutation{
        Name:    "base-token",
        Feature: feature.NewVersionGate(version, nil),
        Mutate: func(m *serviceaccount.Mutator) error {
            m.EnsureImagePullSecret("default-registry")
            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 ServiceAccount
2 Image pull secret edits .imagePullSecrets: EnsureImagePullSecret, RemoveImagePullSecret
3 Automount edits .automountServiceAccountToken: SetAutomountServiceAccountToken

Within each category, edits apply in registration order. Later features observe the object as modified by all earlier ones.

Relevant Editors

ObjectMetaEditor

Modifies labels and annotations via m.EditObjectMetadata. Available methods: EnsureLabel, RemoveLabel, EnsureAnnotation, RemoveAnnotation, Raw. See Mutation Editors for the general editor model.

m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error {
    e.EnsureLabel("app.kubernetes.io/version", version)
    e.EnsureAnnotation("managed-by", "my-operator")
    return nil
})

Mutator Methods

The *serviceaccount.Mutator exposes direct methods that bypass a nested editor for the two ServiceAccount-specific fields.

EnsureImagePullSecret

Adds a named image pull secret to .imagePullSecrets if not already present. Idempotent: calling it with an already-present name is a no-op.

m.EnsureImagePullSecret("registry-creds")

RemoveImagePullSecret

Removes a named image pull secret from .imagePullSecrets. No-op if the name is not present.

m.RemoveImagePullSecret("old-registry-creds")

SetAutomountServiceAccountToken

Sets .automountServiceAccountToken. Pass nil to unset the field.

v := false
m.SetAutomountServiceAccountToken(&v)

The pointed-to value is snapshotted at registration time, so later caller-side changes do not affect Apply().

Data Extraction

WithDataExtractor runs a callback after successful reconciliation with a value copy of the reconciled ServiceAccount. Use it to surface generated fields to other resources:

resource, err := serviceaccount.NewBuilder(base).
    WithDataExtractor(func(sa corev1.ServiceAccount) error {
        sharedState.ServiceAccountName = sa.Name
        return nil
    }).
    Build()

Full Example

func PullSecretMutation(version string) serviceaccount.Mutation {
    return serviceaccount.Mutation{
        Name:    "pull-secret",
        Feature: feature.NewVersionGate(version, nil),
        Mutate: func(m *serviceaccount.Mutator) error {
            m.EnsureImagePullSecret("default-registry")
            return nil
        },
    }
}

func DisableAutomountMutation(version string, disable bool) serviceaccount.Mutation {
    return serviceaccount.Mutation{
        Name:    "disable-automount",
        Feature: feature.NewVersionGate(version, nil).When(disable),
        Mutate: func(m *serviceaccount.Mutator) error {
            v := false
            m.SetAutomountServiceAccountToken(&v)
            return nil
        },
    }
}

resource, err := serviceaccount.NewBuilder(base).
    WithMutation(PullSecretMutation(owner.Spec.Version)).
    WithMutation(DisableAutomountMutation(owner.Spec.Version, owner.Spec.DisableAutomount)).
    Build()

When DisableAutomount is true, .automountServiceAccountToken is set to false. When the condition is not met, the field stays at its baseline value.

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 EnsureImagePullSecret for idempotent secret registration. Multiple features can independently ensure their required pull secrets without conflicting.

Register mutations in dependency order. If one mutation depends on a field set by another, register the dependency first.

ServiceAccount is genuinely simple. The *Mutator exposes direct methods rather than a nested editor because the only mutable fields are .imagePullSecrets and .automountServiceAccountToken. For anything beyond those fields, use EditObjectMetadata.