A Go framework for building highly maintainable Kubernetes operators using a behavioral component model and version-gated feature mutations.
Every Kubernetes operator starts simple: a reconciler, a few resources, some status updates. Then reality sets in. The reconciler grows into a monolith where creation, update, health-checking, suspension, and version-compatibility logic are all interleaved in a single function. Lifecycle behavior gets copy-pasted across resources because there is no shared abstraction for "a deployment that can be suspended" or "a job that runs to completion." Status reporting drifts: one resource sets a condition, another logs a warning, a third does nothing. And when you need to support multiple product versions, compatibility shims get wired directly into orchestration code, making it impossible to reason about what the baseline behavior actually is.
The Operator Component Framework exists because these problems are structural, not incidental. They cannot be solved by writing more careful code in the same flat reconciler model. They require a different organizational unit for operator logic.
The framework introduces three composable layers that separate concerns that operators routinely conflate:
- Components are logical feature units that reconcile multiple resources together and report a single user-facing condition. A component is the answer to "what does this feature need, and is it healthy?"
- Resource Primitives are reusable, type-safe wrappers for individual Kubernetes objects with built-in lifecycle semantics. A primitive knows how to create, update, suspend, and report health for its resource, so your reconciler does not have to.
- Feature Mutations are composable, version-gated modifications that keep baseline resource definitions clean.
Instead of scattering
if version < Xchecks throughout your reconciler, mutations declare their applicability and are applied in a predictable sequence.
Controller
└─ Component
└─ Resource Primitive
└─ Kubernetes Object
| Layer | Responsibility |
|---|---|
| Controller | Determines which components should exist; orchestrates reconciliation at a high level |
| Component | Represents one logical feature; reconciles its resources and reports a single condition |
| Resource Primitive | Encapsulates desired state and lifecycle behavior for a single Kubernetes object |
| Kubernetes Object | The raw client.Object (e.g. Deployment) persisted to the cluster |
- Structured reconciliation with predictable, phased lifecycle management
- Condition aggregation across multiple resources into a single component condition
- Grace period support to avoid premature degraded status during normal operations like rolling updates
- Suspension handling with configurable behavior (scale to zero, delete, or custom logic)
- Version-gated mutations to apply backward-compatibility patches only when needed
- Composable mutation layers that stack without interfering with each other
- Built-in lifecycle interfaces (
Alive,Graceful,Suspendable,Completable,Operational,DataExtractable) covering the full range of Kubernetes workload types - Typed mutation editors for kubernetes resource primitives
- Metrics and event recording integrations out of the box
go get github.com/sourcehawk/operator-component-frameworkRequires Go 1.25.6+ and a project using controller-runtime.
The following example builds a component that manages a single Deployment, with an optional tracing feature applied as
a mutation.
import (
"time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/sourcehawk/operator-component-framework/pkg/component"
"github.com/sourcehawk/operator-component-framework/pkg/primitives/deployment"
)
func buildWebInterfaceComponent(owner *MyOperatorCR) (*component.Component, error) {
// 1. Define the baseline resource
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "web-server",
Namespace: owner.Namespace,
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "web-server"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": "web-server"},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "app", Image: "my-app:latest"},
},
},
},
},
}
// 2. Build a resource primitive, applying optional feature mutations
res, err := deployment.NewBuilder(dep).
WithMutation(TracingFeature(owner.Spec.Version, owner.Spec.TracingEnabled)).
Build()
if err != nil {
return nil, err
}
// 3. Assemble the component
return component.NewComponentBuilder().
WithName("web-interface").
WithConditionType("WebInterfaceReady").
WithResource(res, component.ResourceOptions{}).
WithGracePeriod(5 * time.Minute).
Suspend(owner.Spec.Suspended).
Build()
}
// 4. Reconcile from your controller
func (r *MyReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
owner := &MyOperatorCR{}
if err := r.Get(ctx, req.NamespacedName, owner); err != nil {
return reconcile.Result{}, client.IgnoreNotFound(err)
}
comp, err := buildWebInterfaceComponent(owner)
if err != nil {
return reconcile.Result{}, err
}
recCtx := component.ReconcileContext{
Client: r.Client,
Scheme: r.Scheme,
Recorder: r.Recorder,
Metrics: r.Metrics,
Owner: owner,
}
return reconcile.Result{}, comp.Reconcile(ctx, recCtx)
}Mutations decouple version-specific or feature-gated logic from the baseline resource definition. A mutation declares a condition under which it applies and a function that modifies the resource.
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"
)
func TracingFeature(version string, enabled bool) deployment.Mutation {
return deployment.Mutation{
Name: "enable-tracing",
Feature: feature.NewResourceFeature(version, nil).When(enabled),
Mutate: func(m *deployment.Mutator) error {
m.EditContainers(selectors.ContainerNamed("app"), func(e *editors.ContainerEditor) error {
e.EnsureEnvVar(corev1.EnvVar{Name: "TRACING_ENABLED", Value: "true"})
return nil
})
return nil
},
}
}Mutations are applied in registration order. Each mutation is independent: multiple mutations can target the same resource without interfering with each other, and the framework guarantees a consistent application sequence.
Resource primitives implement behavioral interfaces that the component layer uses for status aggregation:
| Interface | Behavior | Example resources |
|---|---|---|
Alive |
Observable health with rolling-update awareness | Deployments, StatefulSets, DaemonSets |
Graceful |
Time-bounded convergence with degradation | Workloads or integrations with slow rollouts |
Suspendable |
Controlled deactivation (scale to zero or delete) | Workloads, task primitives |
Completable |
Run-to-completion tracking | Jobs |
Operational |
External dependency readiness | Services, Ingresses, Gateways, CronJobs |
DataExtractable |
Post-reconciliation data harvest | Any resource exposing status fields |
You can wrap any Kubernetes object, including custom CRDs, by implementing the Resource interface:
type Resource interface {
// Object returns the desired-state Kubernetes object.
Object() (client.Object, error)
// Mutate receives the current cluster state and applies the desired state to it.
Mutate(current client.Object) error
// Identity returns a stable string that uniquely identifies this resource.
Identity() string
}Optionally implement any of the lifecycle interfaces (Alive, Suspendable, etc.) to participate in condition
aggregation. The framework provides generic building blocks in pkg/generic that handle reconciliation mechanics,
mutation sequencing, and suspension so you can wrap any custom CRD without reimplementing these from scratch.
See the Custom Resource Implementation Guide for a complete walkthrough.
| Document | Description |
|---|---|
| Component Framework | Reconciliation lifecycle, condition model, grace periods, suspension |
| Resource Primitives | Primitive categories, Server-Side Apply, mutation system |
| Custom Resources | Implementing custom resource wrappers using the generic building blocks |
Contributions are welcome. Please open an issue to discuss significant changes before submitting a pull request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes
- Open a pull request against
main
All new code should include tests. The project uses Ginkgo and Gomega for testing.
go test ./...Apache License 2.0. See LICENSE for details.