The primitives package provides reusable, type-safe wrappers for individual Kubernetes objects. Primitives sit between
the Component layer and raw Kubernetes resources. They handle the complexities of state synchronization,
mutation, and lifecycle management so operator authors don't have to.
A primitive wraps a specific Kubernetes kind (e.g., Deployment, ConfigMap) and encapsulates:
- Desired state baseline: the ideal configuration of the resource.
- Lifecycle integration: built-in readiness detection, grace handling, and suspension.
- Mutation surfaces: typed APIs for modifying the resource based on active features or version constraints.
- Server-Side Apply: desired state is applied via SSA, preserving server defaults and fields managed by external controllers.
Each primitive implements the component.Resource interface, and may additionally implement one or more
lifecycle interfaces to participate in component status aggregation.
The framework categorizes primitives based on their runtime behavior.
Examples: ConfigMap, Secret, ServiceAccount, RBAC objects, PodDisruptionBudget
These resources have a mostly static desired state. They are created or updated based on configuration but have no
complex runtime convergence. They are considered Ready as long as they exist. They may optionally implement Alive or
Operational for more granular tracking.
Examples: Deployment, StatefulSet, DaemonSet
These resources represent long-running processes that require runtime convergence (pods being scheduled and becoming
ready). They implement Alive, Graceful, and Suspendable, supporting health tracking, grace periods, and scaling to
zero.
Examples: Job
These resources represent short-lived operations that run to completion (database migrations, backups, initialization
steps). They implement Completable and Suspendable. When suspended, tasks can be paused (if the underlying resource
supports it) or deleted and recreated when resumed.
Examples: Service, Ingress, Gateway, CronJob
These resources define integration points with external or cluster-level systems (networking, load balancers, DNS,
schedules). Their readiness depends on external controllers and may be delayed or partial. They implement Operational,
Graceful, and/or Suspendable.
Some Kubernetes resources are cluster-scoped: they have no namespace. Examples include ClusterRole,
ClusterRoleBinding, and PersistentVolume.
When implementing a primitive for a cluster-scoped kind, the primitive's builder must explicitly call
MarkClusterScoped() on its internal BaseBuilder during construction. This changes ValidateBase() behavior: instead
of requiring a non-empty namespace, it rejects a non-empty namespace. The primitive's builder is also responsible for
providing an identity function that formats the identity string appropriately, typically omitting the namespace segment
(e.g., rbac.authorization.k8s.io/v1/ClusterRole/my-role rather than including a namespace).
At reconcile time, the component framework automatically detects scope incompatibilities between the owner CRD and managed resources using the cluster's REST mapper. See Cluster-Scoped Resources in the component documentation for details on owner reference behavior and garbage collection.
Primitives implement behavioral interfaces that the component layer uses for status aggregation:
| Interface | Status values reported | Typical use |
|---|---|---|
Alive |
Healthy, Creating, Updating, Scaling, Failing |
Deployments, StatefulSets, DaemonSets |
Graceful |
Healthy, Degraded, Down |
Workloads and integrations with slow convergence |
Suspendable |
PendingSuspension, Suspending, Suspended |
Any resource with a deactivation behavior |
Completable |
Completed, TaskRunning, TaskPending, TaskFailing |
Jobs and task primitives |
Operational |
Operational, OperationPending, OperationFailing |
Services, Ingresses, CronJobs |
DataExtractable |
(no status, side-effecting) | Resources that expose post-sync data |
Custom resource wrappers can implement any subset of these interfaces to opt into the corresponding component behaviors.
The framework reconciles resources using Server-Side Apply (SSA). Each primitive builds the desired state (the
baseline object with all registered mutations applied) and patches it to the cluster using client.Apply. Only fields
the operator declares are sent; server-managed defaults, fields set by other controllers (HPAs, sidecar injectors,
annotation-based tooling), and values written by webhooks are left untouched.
Field ownership is tracked automatically by the Kubernetes API server. The field manager name is derived from the owner
and component: "{Owner.GetKind()}/{componentName}". The framework applies with forced ownership, meaning it will take
control of any conflicting fields from other managers. Fields that the operator does not include in its desired state
are left to their current owners.
This approach removes the perpetual-update problem that arises when an operator strips server defaults every reconcile cycle, and it allows primitives to coexist naturally in clusters where multiple controllers touch the same resources.
Primitives use a plan-and-apply pattern: instead of mutating the Kubernetes object directly, mutations record their intent through typed editors, which are applied in a single controlled pass.
This design:
- Prevents uncontrolled mutation: changes are staged before any object is touched
- Enables composability: independent features contribute edits without knowing about each other
- Guarantees ordering: features apply in registration order; within a feature, categories apply in a fixed sequence
- Avoids error-prone slice manipulation: editors handle presence operations and stable selection internally
Editors provide scoped, typed APIs for modifying specific parts of a resource. Every editor exposes a .Raw() method
for cases where the typed API is insufficient, giving direct access to the underlying Kubernetes struct while keeping
the mutation scoped to that editor's target.
Each primitive documents its available editors in its own Relevant Editors section.
Selectors determine which containers an editor targets. This is important for multi-container pods:
selectors.AllContainers() // every container in the pod
selectors.ContainerNamed("app") // a single container by name
selectors.ContainersNamed("web", "api") // multiple containers by name
selectors.ContainerAtIndex(0) // container at a specific indexSelectors are evaluated against the container list after any presence operations (add/remove) within the same mutation have been applied. This means a single mutation can safely add a container and then configure it.
| Primitive | Category | Documentation |
|---|---|---|
pkg/primitives/deployment |
Workload | deployment.md |
pkg/primitives/statefulset |
Workload | statefulset.md |
pkg/primitives/replicaset |
Workload | replicaset.md |
pkg/primitives/daemonset |
Workload | daemonset.md |
pkg/primitives/pod |
Workload | pod.md |
pkg/primitives/job |
Task | job.md |
pkg/primitives/cronjob |
Integration | cronjob.md |
pkg/primitives/configmap |
Static | configmap.md |
pkg/primitives/secret |
Static | secret.md |
pkg/primitives/role |
Static | role.md |
pkg/primitives/rolebinding |
Static | rolebinding.md |
pkg/primitives/pdb |
Static | pdb.md |
pkg/primitives/clusterrole |
Static | clusterrole.md |
pkg/primitives/clusterrolebinding |
Static | clusterrolebinding.md |
pkg/primitives/serviceaccount |
Static | serviceaccount.md |
pkg/primitives/service |
Integration | service.md |
pkg/primitives/pv |
Integration | pv.md |
pkg/primitives/pvc |
Integration | pvc.md |
pkg/primitives/hpa |
Integration | hpa.md |
pkg/primitives/ingress |
Integration | ingress.md |
pkg/primitives/networkpolicy |
Static | networkpolicy.md |
import "github.com/sourcehawk/operator-component-framework/pkg/primitives/deployment"
base := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "web-server",
Namespace: owner.Namespace,
},
// ... spec
}
resource, err := deployment.NewBuilder(base).
Build()import (
corev1 "k8s.io/api/core/v1"
"github.com/sourcehawk/operator-component-framework/pkg/feature"
"github.com/sourcehawk/operator-component-framework/pkg/primitives/deployment"
"github.com/sourcehawk/operator-component-framework/pkg/mutation/editors"
"github.com/sourcehawk/operator-component-framework/pkg/mutation/selectors"
)
resource, err := deployment.NewBuilder(base).
WithMutation(deployment.Mutation{
Name: "add-proxy-sidecar",
Feature: feature.NewResourceFeature(version, nil),
Mutate: func(m *deployment.Mutator) error {
m.EnsureContainer(corev1.Container{
Name: "proxy",
Image: "envoyproxy/envoy:v1.29",
})
m.EditContainers(selectors.ContainerNamed("proxy"), func(e *editors.ContainerEditor) error {
e.EnsureEnvVar(corev1.EnvVar{Name: "PROXY_ADMIN_PORT", Value: "9901"})
return nil
})
return nil
},
}).
Build()m.EditContainers(selectors.ContainersNamed("web", "api"), func(e *editors.ContainerEditor) error {
e.EnsureArg("--log-format=json")
return nil
})| Primitive | Category | Documentation |
|---|---|---|
pkg/primitives/unstructured/static |
Static | unstructured.md |
pkg/primitives/unstructured/workload |
Workload | unstructured.md |
pkg/primitives/unstructured/integration |
Integration | unstructured.md |
pkg/primitives/unstructured/task |
Task | unstructured.md |
The unstructured primitives are an escape hatch for managing arbitrary Kubernetes objects that have no Go type, for example, Crossplane resources, external CRDs, or any object known only at runtime. One variant exists per lifecycle category, each implementing the corresponding interfaces.
Because the framework cannot know the semantics of an unstructured object, it does not infer any semantic or domain-specific defaults. The builders instead configure generic safe defaults: if you omit a grace handler, the primitive treats the resource as Healthy; if you omit suspension handlers, the primitive reports Suspended and the suspend mutation is a no-op. Only the converge/operational status handler is required at build time.
The unstructured primitives share a single Mutator and use an UnstructuredContentEditor for manipulating nested
fields in the object's content map. See unstructured.md for full details.
When the built-in primitives do not cover your use case, you can implement custom resource wrappers for any Kubernetes
object, including custom CRDs. The framework provides generic building blocks in pkg/generic that handle
reconciliation mechanics, mutation sequencing, and suspension, so you only need to provide type-specific logic.
See the Custom Resource Implementation Guide for a complete walkthrough covering mutator design, status handlers, builders, and component registration.