Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,133 changes: 57 additions & 1,076 deletions README.md

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions docs/component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Component System

The `component` package provides a structured way to manage logical features in a Kubernetes operator by grouping related resources into **Components**.

A Component acts as a behavioral unit responsible for reconciling multiple resources, managing their shared lifecycle, and reporting their aggregate health through a single condition on the owner CRD.

## Purpose

In complex operators, reconciliation logic often becomes fragmented across large controller loops. This leads to:
* **Controller Logic Fragmentation**: Reconcilers coordinating dozens of unrelated resources in a single function.
* **Inconsistent Lifecycle Handling**: Manual implementation of rollouts, suspension, and degradation for every feature.
* **Scattered Status Reporting**: Inconsistent ways of determining if a feature is truly "Ready" or "Degraded".

Components solve these problems by providing:
* **Structured Reconciliation**: A clear, repeatable pattern for resource synchronization.
* **Lifecycle Orchestration**: Built-in support for progression, grace periods, and suspension.
* **Consistent Status Aggregation**: Automated calculation of a single, meaningful status condition from multiple underlying resources.

## Component Responsibilities

A Component is responsible for:
* **Resource Reconciliation**: Ensuring all registered resources (Deployments, Services, ConfigMaps, etc.) match their desired state.
* **Health Aggregation**: Monitoring the status of each resource and determining the overall health of the logical feature.
* **Lifecycle Semantics**: Applying high-level behaviors like "waiting for readiness" (grace periods) or "scaling down" (suspension).
* **Status Exposure**: Maintaining exactly one `Condition` on the owner object's status that represents the component's state.

## Resource Registration

Resources are registered to a component using the `Builder`. The registration defines how the component interacts with each resource during reconciliation.

```go
builder := component.NewComponentBuilder(false).
WithName("web-interface").
WithConditionType("WebInterfaceReady").
WithResource(deployment, false, false). // Managed (Create/Update)
WithResource(configMap, false, true). // Read-only
WithResource(oldService, true, false) // Delete-only
```

### Resource Flags

* **Managed (Default)**: The component ensures the resource exists and matches the desired state. Its health contributes to the aggregate status.
* **Read-only**: The component only reads the resource's state (e.g., to extract data or check health) but never modifies it in the cluster.
* **Delete-only**: The component ensures the resource is removed from the cluster.

These flags dictate the reconciliation phase: managed resources are updated, read-only resources are only fetched, and delete-only resources are removed.

## Reconciliation Lifecycle

The `Reconcile` method follows a conceptual four-phase process:

1. **Resource Synchronization**: All registered resources are processed. Managed resources are created or updated, delete-only resources are removed, and read-only resources are fetched.
2. **Lifecycle Evaluation**: The component determines the current lifecycle mode (Normal or Suspended) and evaluates the progress of resources (e.g., checking if a Deployment is still rolling out).
3. **Status Aggregation**: The individual states of all resources are collected and compared.
4. **Condition Update**: A single aggregate `Condition` is calculated and applied to the owner CRD's status.

## Status Model

The framework categorizes component states into three functional groups:

### Converging States
These states occur during normal operation as the component moves toward a steady state.
* **Creating**: Resources are being provisioned for the first time.
* **Updating**: Existing resources are being modified.
* **Scaling**: Resources (like Deployments) are changing their replica counts.
* **Ready**: All resources are healthy and match the desired state.

### Grace States
These states are triggered when a component fails to reach "Ready" within its configured grace period.
* **Ready**: All resources are healthy.
* **Degraded**: The component is functional but some non-critical resources are unhealthy or it's taking longer than expected to converge.
* **Down**: Critical resources are failing or the component is completely non-functional.

### Suspension States
These states manage the intentional deactivation of a component.
* **PendingSuspension**: The suspension request is acknowledged, but work hasn't started.
* **Suspending**: Resources are actively being scaled down or cleaned up.
* **Suspended**: All resources have reached their suspended state (e.g., scaled to 0).

## Grace Period

A **Grace Period** defines how long a component is allowed to remain in "progressing" states (Creating, Updating, Scaling) before it is considered unhealthy.

* During the grace period, the component reports its actual converging state (e.g., `Updating`).
* After the grace period expires, if the component is still not `Ready`, the framework transitions the condition to **Degraded** or **Down** based on the resource health.

This prevents premature "False" readiness reports during normal operations like rolling updates.

## Suspension Lifecycle

Suspension allows an operator to intentionally "turn off" a component without deleting its configuration.

When a component is marked as suspended:
1. It calls `Suspend()` on all `Suspendable` resources.
2. Resources may scale down (e.g., Deployments to 0 replicas) or perform cleanup.
3. The component tracks the `SuspensionStatus` of each resource.
4. Once all resources report `Suspended`, the component condition transitions to `Suspended`.

## Condition Priority

When aggregating multiple resources, the framework uses a priority system to ensure the most critical information is reported. Failure states take precedence over progressing states, which take precedence over "Ready".

Conceptual priority (highest to lowest):
1. **Error / Down / Degraded**: Something is wrong.
2. **Suspension States**: The component is intentionally inactive.
3. **Converging States**: The component is working toward readiness.
4. **Ready**: Everything is healthy.

## ReconcileContext

The `ReconcileContext` is passed to the `Reconcile` method and provides all dependencies required for reconciliation:
* **Kubernetes Client**: For interacting with the API server.
* **Scheme**: For resource GVK lookups.
* **Event Recorder**: For emitting Kubernetes Events.
* **Metrics**: For recording component-level health metrics.
* **Owner Object**: The CRD that owns the component.

Dependencies are passed explicitly to ensure the component remains testable and decoupled from global state or specific controller-runtime implementation details.

## Best Practices

* **Keep Controllers Thin**: The controller should only be responsible for fetching the owner CRD and invoking component reconciliation.
* **Model Logical Features**: Create one component per user-visible feature (e.g., "API", "UI", "Database").
* **Group by Lifecycle**: Put resources that must live and die together into the same component.
* **Split for Granularity**: If two features should report separate "Ready" conditions in the CRD status, they should be separate components.
205 changes: 205 additions & 0 deletions docs/primitives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# Resource Primitives

The `primitives` system provides a resource-centric abstraction layer for Kubernetes objects. It acts as the bridge
between high-level **Components** and raw Kubernetes resources, handling the complexities of state synchronization,
mutation, and lifecycle management.

## 1. What primitives are

Primitives are reusable, type-safe resource wrappers for Kubernetes objects. They encapsulate the logic required to
reconcile a specific kind of resource (like a `Deployment` or `ConfigMap`) within the framework's behavioral model.

Each primitive encapsulates:

- **Desired state baseline**: A template or builder for the resource's "ideal" configuration.
- **Lifecycle integration**: Built-in support for readiness detection, grace periods, and suspension.
- **Mutation surfaces**: Controlled APIs for modifying resources based on active features or versions.
- **Field application behavior**: Precise rules for how fields are merged or preserved during reconciliation.

By using primitives, operator authors can avoid writing repetitive "create-or-update" boilerplate and instead focus on
defining how their resources should behave.

---

## 2. Primitive categories

The framework distinguishes between three primary categories of primitives based on their operational characteristics.

### Static Primitives
Examples: `ConfigMap`, `Secret`, `ServiceAccount`, RBAC objects (`Role`, `RoleBinding`).

- **Characteristics**: These resources have a mostly static desired state. They are typically created once or updated
based on configuration changes but do not have complex runtime convergence or scaling behaviors.
- **Lifecycle**: Usually considered "Ready" as soon as they are successfully applied to the API server.

### Workload Primitives
Examples: `Deployment`, `StatefulSet`, `DaemonSet`.

- **Characteristics**: These resources represent long-running processes that require runtime convergence (e.g.,
pods being scheduled and becoming ready).
- **Behavior**: They support advanced features like suspension (scaling to zero), grace handling for slow rollouts, and
complex feature-based mutations.

### Batch Primitives

TBD

---

## 3. Field application model

Primitives use a structured pipeline to synchronize the desired state with the current state in the cluster. This
process is managed by a **Field Applicator**.

### The Pipeline Order
When a primitive is reconciled, it follows a strict order of operations:

1. **Baseline field application**: The `FieldApplicator` merges the "baseline" desired state onto the current object.
2. **Flavor adjustments**: Post-baseline merge policies (Flavors) are applied to preserve specific fields.
3. **Mutation edits**: Feature-specific or version-specific edits are applied (Workload primitives only).

This ensures that mutations always operate on a predictable, fully-formed baseline.

---

## 4. Field application flavors

**Flavors** are reusable merge policies that run after the baseline application but before mutations. Their primary
purpose is to preserve fields that may be managed by other controllers or external systems (like sidecar injectors
or autoscalers).

### Examples of Flavors:
- **Preserving Labels/Annotations**: Ensuring that metadata added by external tools is not wiped out during
reconciliation.
- **Preserving Pod Template Metadata**: Keeping sidecar-related annotations on a Deployment's pod template.

Flavors allow the framework to be "good citizens" in a cluster where multiple controllers might be touching the same
resources.

---

## 5. Mutation system

Workload primitives employ a **plan-and-apply pattern** for modifications. Instead of mutating the Kubernetes object
directly and repeatedly, the framework records "edit intent" through a series of planned mutations.

### Why this pattern exists:
- **Prevents uncontrolled mutation**: Changes are staged and applied in a single, controlled pass.
- **Improves composability**: Multiple independent features can contribute edits without knowing about each other.
- **Predictable Ordering**: Features are applied in the order they are registered. Later features observe the resource state after earlier features have already applied their changes.
- **Efficiency**: Avoids expensive and error-prone manual slice manipulations (like searching for a container by name
multiple times).

### Internal Ordering within a Feature:
While features apply in registration order, the internal operations within a single feature follow a fixed category-based sequence to ensure consistency:
1. Deployment metadata edits
2. DeploymentSpec edits
3. Pod template metadata edits
4. Pod spec edits
5. Regular container presence operations
6. Regular container edits (using a snapshot taken after presence operations)
7. Init container presence operations
8. Init container edits (using a snapshot taken after presence operations)

---

## 6. Mutation editors

**Editors** provide a scoped, typed API for making changes to specific parts of a resource. They ensure that mutations
are safe and follow Kubernetes best practices.

Common editors include:
- `ContainerEditor`: For modifying environment variables, arguments, and resource limits.
- `PodSpecEditor`: For managing volumes, affinity, or service account names.
- `DeploymentSpecEditor`: For controlling replicas, strategy, and selectors.
- `ObjectMetaEditor`: For manipulating labels and annotations.

Editors act as a protective layer, offering helper methods like `EnsureEnvVar` or `RemoveArg`.

---

## 7. Selectors

**Selectors** determine which parts of a resource an editor should target. This is particularly important for
multi-container pods.

For example, a `ContainerSelector` can be used to:
- Target all containers (`AllContainers()`).
- Target a specific container by name (`ContainerNamed("sidecar")`).
- Target containers at specific indices (`ContainerAtIndex(0)`).

Selectors allow mutations to be precise and reusable across different resource configurations.

---

## 8. Raw mutation escape hatch

While editors provide safe wrappers, there are times when you need to perform advanced customizations that the
framework doesn't explicitly support. For these cases, every editor provides a `Raw()` method.

- **Purpose**: Gives direct access to the underlying Kubernetes struct (e.g., `*corev1.Container`).
- **Safety**: The mutation remains scoped to the editor's target (e.g., you can't accidentally delete the entire PodSpec from a ContainerEditor).
- **Flexibility**: Ensures that the framework never blocks you from using new Kubernetes features or edge-case configurations.

---

## 9. Default lifecycle behavior

Workload primitives come with "sane defaults" for lifecycle management, integrated directly into the Component status model:

- **Convergence detection**: Automatically determines if a Deployment is "Ready", "Scaling", or "Updating" based on its status fields.
- **Grace handling**: Monitors how long a resource has been non-ready and reports "Degraded" or "Down" if it exceeds a grace period.
- **Suspension behavior**: Provides the logic for scaling resources down to zero and reporting the "Suspended" state.

These defaults can be overridden via the primitive's `Builder` if specialized behavior is required.

---

## 10. When to implement a custom resource

While the provided primitives cover the most common Kubernetes objects, you may need to implement a custom resource
wrapper when:

- You are managing **custom CRDs** that require specific health checks.
- You have **unusual lifecycle semantics** (e.g., a resource that must be deleted and recreated instead of updated).
- You need **highly specialized mutation behavior** not covered by standard editors.

Custom resource wrappers can still leverage the framework's core interfaces (`component.Resource`, `component.Alive`,
`component.Suspendable`). See the `examples/` directory for patterns on implementing custom resource wrappers.

---

## Examples

### Creating a primitive resource
```go
// Define a baseline Deployment
deployment := &appsv1.Deployment{ ... }

// Use the builder to create a primitive
resource, err := deployment.NewBuilder(deployment).
WithFieldApplicationFlavor(deployment.PreserveCurrentLabels).
Build()
```

### Adding mutation edits
```go
// Mutations are typically defined within Feature objects
mutation := deployment.Mutation{
Name: "add-proxy-sidecar",
ApplyIntent: func(m *deployment.Mutator) error {
m.EditContainers(selectors.ContainerNamed("app"), func(e *editors.ContainerEditor) {
e.EnsureEnvVar(corev1.EnvVar{Name: "PROXY_ENABLED", Value: "true"})
})
return nil
},
}
```

### Selecting containers for mutation
```go
// Targeting multiple specific containers
m.EditContainers(selectors.ContainersNamed("web", "api"), func(e *editors.ContainerEditor) {
e.EnsureArg("--verbose")
})
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package features
import (
"github.com/sourcehawk/operator-component-framework/examples/component-architecture-basics/resources"
"github.com/sourcehawk/operator-component-framework/pkg/feature"
corev1 "k8s.io/api/core/v1"
)

var legacyBehaviorFeature = MustRegister("LegacyBehaviorExample", "< 8.0.0")
Expand All @@ -19,7 +20,7 @@ func NewLegacyBehaviorFeature(version string) feature.Mutation[*resources.Deploy
),
Mutate: func(m *resources.DeploymentResourceMutator) error {
// Set a deprecated env var
m.EnsureContainerEnvVar("DEPRECATED_SETTING", "legacy-value")
m.EnsureContainerEnvVar(corev1.EnvVar{Name: "DEPRECATED_SETTING", Value: "legacy-value"})
// Remove the new one as it's not supported in legacy versions
m.RemoveContainerEnvVar("NEW_MANDATORY_SETTING")
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package features
import (
"github.com/sourcehawk/operator-component-framework/examples/component-architecture-basics/resources"
"github.com/sourcehawk/operator-component-framework/pkg/feature"
corev1 "k8s.io/api/core/v1"
)

var tracingFeature = MustRegister("TracingExample", ">= 8.1.0")
Expand All @@ -18,7 +19,7 @@ func NewTracingFeature(version string, enabled bool) feature.Mutation[*resources
version, []feature.VersionConstraint{tracingFeature},
).When(enabled),
Mutate: func(m *resources.DeploymentResourceMutator) error {
m.EnsureContainerEnvVar("ENABLE_TRACING", "true")
m.EnsureContainerEnvVar(corev1.EnvVar{Name: "ENABLE_TRACING", Value: "true"})
return nil
},
}
Expand Down
Loading
Loading