Skip to content

sourcehawk/operator-component-framework

Repository files navigation

Operator Component Framework

Go Reference Go Report Card License

A Go framework for building highly maintainable Kubernetes operators using a behavioral component model and version-gated feature mutations.


Overview

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 < X checks throughout your reconciler, mutations declare their applicability and are applied in a predictable sequence.

Mental Model

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

Features

  • 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

Installation

go get github.com/sourcehawk/operator-component-framework

Requires Go 1.25.6+ and a project using controller-runtime.

Quick Start

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)
}

Feature Mutations

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 Lifecycle Interfaces

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

Implementing a Custom Resource

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.

Documentation

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

Contributing

Contributions are welcome. Please open an issue to discuss significant changes before submitting a pull request.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Commit your changes
  4. Open a pull request against main

All new code should include tests. The project uses Ginkgo and Gomega for testing.

go test ./...

License

Apache License 2.0. See LICENSE for details.

About

Composable framework for building maintainable Kubernetes operators.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors