-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement pod primitive #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0b00b78
16081f6
d20af43
c1ba2ca
1249e5b
b4baf8b
63411c5
5322c04
1668b17
fda4c2b
034f6fb
f4d713e
d74a8af
84c379e
564561c
3448a41
345d91c
271dd80
18d0ed8
de86aa5
7e1ce37
4b9e53f
e59f104
4d98770
050ad63
d7e26e5
73a7d0e
97c26b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| # Pod Primitive | ||
|
|
||
| The `pod` primitive is the framework's built-in workload abstraction for managing Kubernetes `Pod` resources directly. | ||
| It integrates fully with the component lifecycle and provides a mutation API for managing containers, pod specs, and | ||
| metadata. | ||
|
|
||
| Pods are rarely managed directly by operators; this primitive is provided for completeness and for operators that manage | ||
| pod objects (e.g. debugging utilities, node-local agents). | ||
|
|
||
| ## Capabilities | ||
|
|
||
| | Capability | Detail | | ||
| | --------------------- | -------------------------------------------------------------------------------------------------- | | ||
| | **Health tracking** | Monitors pod phase and container statuses; reports `Healthy`, `Creating`, `Updating`, or `Failing` | | ||
| | **Graceful rollouts** | Detects degraded or down states via grace status handler | | ||
| | **Suspension** | Deletes the pod (pods cannot be paused); reports `Suspended` | | ||
| | **Mutation pipeline** | Typed editors for metadata, pod spec, and containers | | ||
|
|
||
|
Comment on lines
+10
to
+18
|
||
| ## Building a Pod Primitive | ||
|
|
||
| ```go | ||
| import "github.com/sourcehawk/operator-component-framework/pkg/primitives/pod" | ||
|
|
||
| base := &corev1.Pod{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "debug-pod", | ||
| Namespace: owner.Namespace, | ||
| }, | ||
| Spec: corev1.PodSpec{ | ||
| Containers: []corev1.Container{ | ||
| { | ||
| Name: "debug", | ||
| Image: "busybox:latest", | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| resource, err := pod.NewBuilder(base). | ||
| WithMutation(MyFeatureMutation(owner.Spec.Version)). | ||
| Build() | ||
| ``` | ||
|
|
||
| ## Mutations | ||
|
|
||
| Mutations are the primary mechanism for modifying a `Pod` beyond its baseline. Each mutation is a named function that | ||
| receives a `*Mutator` and records edit intent through typed editors. | ||
|
|
||
| The `Feature` field controls when a mutation applies. Leaving it nil applies the mutation unconditionally. A feature | ||
| with no version constraints and no `When()` conditions is also always enabled: | ||
|
|
||
| ```go | ||
| func MyFeatureMutation(version string) pod.Mutation { | ||
| return pod.Mutation{ | ||
| Name: "my-feature", | ||
| Feature: feature.NewResourceFeature(version, nil), // always enabled | ||
| Mutate: func(m *pod.Mutator) error { | ||
| // record edits here | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Mutations are applied in the order they are registered with the builder. If one mutation depends on a change made by | ||
| another, register the dependency first. | ||
|
|
||
| ### Boolean-gated mutations | ||
|
|
||
| Use `When(bool)` to gate a mutation on a runtime condition: | ||
|
|
||
| ```go | ||
| func DebugMutation(version string, enabled bool) pod.Mutation { | ||
| return pod.Mutation{ | ||
| Name: "debug-mode", | ||
| Feature: feature.NewResourceFeature(version, nil).When(enabled), | ||
| Mutate: func(m *pod.Mutator) error { | ||
| m.EnsureContainerEnvVar(corev1.EnvVar{Name: "DEBUG", Value: "true"}) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Internal Mutation Ordering | ||
|
|
||
| Within a single mutation, edit operations are grouped into categories and applied in a fixed sequence regardless of the | ||
| order they are recorded. This ensures structural consistency across mutations. | ||
|
|
||
| | Step | Category | What it affects | | ||
| | ---- | -------------------------- | ----------------------------------------------------------------------- | | ||
| | 1 | Object metadata edits | Labels and annotations on the `Pod` object | | ||
| | 2 | Pod spec edits | Volumes, tolerations, node selectors, service account, security context | | ||
| | 3 | Regular container presence | Adding or removing containers from `spec.containers` | | ||
| | 4 | Regular container edits | Env vars, args, resources (snapshot taken after step 3) | | ||
| | 5 | Init container presence | Adding or removing containers from `spec.initContainers` | | ||
| | 6 | Init container edits | Env vars, args, resources (snapshot taken after step 5) | | ||
|
|
||
| Container edits (steps 4 and 6) are evaluated against a snapshot taken _after_ presence operations in the same mutation. | ||
| This means a single mutation can add a container and then configure it without selector resolution issues. | ||
|
|
||
| **Kubernetes immutability note:** most fields in `Pod.spec` are immutable after creation, including the overall | ||
| structure of `spec.containers` and `spec.initContainers` and the majority of per-container fields (such as `env`, | ||
| `args`, resources, ports, and probes). Presence operations such as `EnsureContainer` / `RemoveContainer` (and the | ||
| corresponding init container operations) are intended for use when constructing a new Pod or when recreating the Pod, | ||
| not for in-place updates to an existing Pod. If a mutation attempts to add or remove containers on an existing Pod, the | ||
| Kubernetes API server will reject the update. In practice, the set of fields that can be updated in-place on an existing | ||
| Pod is very small (primarily container images, plus a few feature-gated fields such as resources with in-place resize); | ||
| treat Pods as effectively immutable and use delete-and-recreate when you need to change other container attributes. | ||
|
|
||
| ## Editors | ||
|
|
||
| ### PodSpecEditor | ||
|
|
||
| Manages pod-level configuration via `m.EditPodSpec`. | ||
|
|
||
| Available methods: `SetServiceAccountName`, `EnsureVolume`, `RemoveVolume`, `EnsureToleration`, `RemoveTolerations`, | ||
| `EnsureNodeSelector`, `RemoveNodeSelector`, `EnsureImagePullSecret`, `RemoveImagePullSecret`, `SetPriorityClassName`, | ||
| `SetHostNetwork`, `SetHostPID`, `SetHostIPC`, `SetSecurityContext`, `Raw`. `RemoveTolerations` accepts a predicate | ||
| function (`match func(corev1.Toleration) bool`) and removes all tolerations for which `match` returns `true`. | ||
|
|
||
| ```go | ||
| m.EditPodSpec(func(e *editors.PodSpecEditor) error { | ||
| e.SetServiceAccountName("my-service-account") | ||
| e.EnsureVolume(corev1.Volume{ | ||
| Name: "config", | ||
| VolumeSource: corev1.VolumeSource{ | ||
| ConfigMap: &corev1.ConfigMapVolumeSource{ | ||
| LocalObjectReference: corev1.LocalObjectReference{Name: "app-config"}, | ||
| }, | ||
| }, | ||
| }) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ### ContainerEditor | ||
|
|
||
| Modifies individual containers via `m.EditContainers` or `m.EditInitContainers`. Always used in combination with a | ||
| [selector](../primitives.md#container-selectors). | ||
|
|
||
| Available methods: `EnsureEnvVar`, `EnsureEnvVars`, `RemoveEnvVar`, `RemoveEnvVars`, `EnsureArg`, `EnsureArgs`, | ||
| `RemoveArg`, `RemoveArgs`, `SetResourceLimit`, `SetResourceRequest`, `SetResources`, `Raw`. | ||
|
|
||
| ```go | ||
| m.EditContainers(selectors.ContainerNamed("app"), func(e *editors.ContainerEditor) error { | ||
| e.EnsureEnvVar(corev1.EnvVar{Name: "LOG_LEVEL", Value: "info"}) | ||
| e.EnsureArg("--metrics-port=9090") | ||
| e.SetResourceLimit(corev1.ResourceCPU, resource.MustParse("500m")) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| For fields not covered by the typed API (such as volume mounts), use `Raw()`: | ||
|
|
||
| ```go | ||
| m.EditContainers(selectors.ContainerNamed("app"), func(e *editors.ContainerEditor) error { | ||
| e.Raw().VolumeMounts = append(e.Raw().VolumeMounts, corev1.VolumeMount{ | ||
| Name: "config", | ||
| MountPath: "/etc/config", | ||
| }) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ### ObjectMetaEditor | ||
|
|
||
| Modifies labels and annotations via `m.EditObjectMetadata`. | ||
|
|
||
| Available methods: `EnsureLabel`, `RemoveLabel`, `EnsureAnnotation`, `RemoveAnnotation`, `Raw`. | ||
|
|
||
| ```go | ||
| m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error { | ||
| e.EnsureLabel("app.kubernetes.io/version", version) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ### Raw Escape Hatch | ||
|
|
||
| All editors provide a `.Raw()` method for direct access to the underlying Kubernetes struct when the typed API is | ||
| insufficient. The mutation remains scoped to the editor's target — you cannot accidentally modify unrelated parts of the | ||
| spec. | ||
|
|
||
| ```go | ||
| m.EditContainers(selectors.ContainerNamed("app"), func(e *editors.ContainerEditor) error { | ||
| e.Raw().SecurityContext = &corev1.SecurityContext{ | ||
| ReadOnlyRootFilesystem: ptr.To(true), | ||
| } | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ## Convenience Methods | ||
|
|
||
| The `Mutator` also exposes convenience wrappers that target all containers at once: | ||
|
|
||
| | Method | Equivalent to | | ||
| | ----------------------------- | ------------------------------------------------------------- | | ||
| | `EnsureContainerEnvVar(ev)` | `EditContainers(AllContainers(), ...)` → `EnsureEnvVar(ev)` | | ||
| | `RemoveContainerEnvVar(name)` | `EditContainers(AllContainers(), ...)` → `RemoveEnvVar(name)` | | ||
| | `EnsureContainerArg(arg)` | `EditContainers(AllContainers(), ...)` → `EnsureArg(arg)` | | ||
| | `RemoveContainerArg(arg)` | `EditContainers(AllContainers(), ...)` → `RemoveArg(arg)` | | ||
|
|
||
|
sourcehawk marked this conversation as resolved.
Comment on lines
+198
to
+204
|
||
| ## Suspension | ||
|
|
||
| Pods cannot be paused. The default behavior deletes the pod when the component is suspended. | ||
|
|
||
| - `DefaultDeleteOnSuspendHandler`: returns `true` — pod is deleted on suspend. | ||
| - `DefaultSuspendMutationHandler`: no-op (deletion is handled by the framework). | ||
| - `DefaultSuspensionStatusHandler`: always returns `{Suspended, "Pod deleted on suspend"}`. | ||
|
|
||
| ## Guidance | ||
|
|
||
| **`Feature: nil` applies unconditionally.** Omit `Feature` (leave it nil) for mutations that should always run. Use | ||
| `feature.NewResourceFeature(version, constraints)` when version-based gating is needed, and chain `.When(bool)` for | ||
| boolean conditions. | ||
|
|
||
| **Register mutations in dependency order.** If mutation B relies on a container added by mutation A, register A first. | ||
| The internal ordering within each mutation handles intra-mutation dependencies automatically. | ||
|
|
||
| **Prefer `EnsureContainer` over direct slice manipulation.** The mutator tracks presence operations so that selectors in | ||
| the same mutation resolve correctly and reconciliation remains idempotent. | ||
|
|
||
| **Use selectors for precision.** Targeting `AllContainers()` when you only mean to modify the primary container can | ||
| cause unexpected behavior if sidecar containers are present. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Pod Primitive Example | ||
|
|
||
| This example demonstrates the usage of the `pod` primitive within the operator component framework. It shows how to | ||
| manage a Kubernetes Pod as a component of a larger application, utilizing features like: | ||
|
|
||
| - **Base Construction**: Initializing a Pod with basic metadata and spec. | ||
| - **Feature Mutations**: Applying version-gated or conditional metadata changes (labels) using the `Mutator`. | ||
| - **Suspension**: Deleting the pod when the component is suspended (pods cannot be paused). | ||
| - **Data Extraction**: Harvesting information from the reconciled resource. | ||
|
|
||
| ## Directory Structure | ||
|
Comment on lines
+7
to
+11
|
||
|
|
||
| - `app/`: Defines the mock `ExampleApp` CRD and the controller that uses the component framework. | ||
| - `features/`: Contains modular feature definitions: | ||
| - `mutations.go`: label mutations applied to the Pod using the `Mutator`. | ||
| - `resources/`: Contains the central `NewPodResource` factory that assembles all features using the `pod.Builder`. | ||
| - `main.go`: A standalone entry point that demonstrates a single reconciliation loop using a fake client. | ||
|
|
||
| ## Running the Example | ||
|
Comment on lines
+15
to
+19
|
||
|
|
||
| You can run this example directly using `go run`: | ||
|
|
||
| ```bash | ||
| go run examples/pod-primitive/main.go | ||
| ``` | ||
|
|
||
| This will: | ||
|
|
||
| 1. Initialize a fake Kubernetes client. | ||
| 2. Create an `ExampleApp` owner object. | ||
| 3. Reconcile the `ExampleApp` components through multiple spec changes. | ||
| 4. Print the resulting status conditions. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // Package app provides a sample controller using the pod primitive. | ||
| package app | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/sourcehawk/operator-component-framework/pkg/component" | ||
| "k8s.io/apimachinery/pkg/runtime" | ||
| "k8s.io/client-go/tools/record" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| ) | ||
|
|
||
| // ExampleController reconciles an ExampleApp object using the component framework. | ||
| type ExampleController struct { | ||
| client.Client | ||
| Scheme *runtime.Scheme | ||
| Recorder record.EventRecorder | ||
| Metrics component.Recorder | ||
|
|
||
| // NewPodResource is a factory function to create the pod resource. | ||
| // This allows us to inject the resource construction logic. | ||
| NewPodResource func(*ExampleApp) (component.Resource, error) | ||
| } | ||
|
|
||
| // Reconcile performs the reconciliation for a single ExampleApp. | ||
| func (r *ExampleController) Reconcile(ctx context.Context, owner *ExampleApp) error { | ||
| // 1. Build the pod resource for this owner. | ||
| podResource, err := r.NewPodResource(owner) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // 2. Build the component that manages the pod. | ||
| comp, err := component.NewComponentBuilder(). | ||
| WithName("example-app"). | ||
| WithConditionType("AppReady"). | ||
| WithResource(podResource, component.ResourceOptions{}). | ||
| Suspend(owner.Spec.Suspended). | ||
| Build() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // 3. Execute the component reconciliation. | ||
| resCtx := component.ReconcileContext{ | ||
| Client: r.Client, | ||
| Scheme: r.Scheme, | ||
| Recorder: r.Recorder, | ||
| Metrics: r.Metrics, | ||
| Owner: owner, | ||
| } | ||
|
|
||
| return comp.Reconcile(ctx, resCtx) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package app | ||
|
|
||
| import ( | ||
| sharedapp "github.com/sourcehawk/operator-component-framework/examples/shared/app" | ||
| ) | ||
|
|
||
| // ExampleApp re-exports the shared CRD type so callers in this package need no import alias. | ||
| type ExampleApp = sharedapp.ExampleApp | ||
|
|
||
| // ExampleAppSpec re-exports the shared spec type. | ||
| type ExampleAppSpec = sharedapp.ExampleAppSpec | ||
|
|
||
| // ExampleAppStatus re-exports the shared status type. | ||
| type ExampleAppStatus = sharedapp.ExampleAppStatus | ||
|
|
||
| // ExampleAppList re-exports the shared list type. | ||
| type ExampleAppList = sharedapp.ExampleAppList | ||
|
|
||
| // AddToScheme registers the ExampleApp types with the given scheme. | ||
| var AddToScheme = sharedapp.AddToScheme |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // Package features provides sample mutations for the pod primitive example. | ||
| // | ||
| // These examples only mutate object metadata because most Pod spec fields | ||
| // are immutable after creation. Kubernetes does allow a small set of in-place | ||
| // updates (notably container images), but other fields such as env, args, and | ||
| // resources require the Pod to be deleted and recreated. | ||
| package features | ||
|
|
||
| import ( | ||
| "github.com/sourcehawk/operator-component-framework/pkg/feature" | ||
| "github.com/sourcehawk/operator-component-framework/pkg/mutation/editors" | ||
| "github.com/sourcehawk/operator-component-framework/pkg/primitives/pod" | ||
| ) | ||
|
|
||
| // TracingFeature marks the pod as tracing-enabled via metadata. | ||
| // Controllers or sidecar injectors can watch the label and handle | ||
| // any required Pod replacement or injection. | ||
| func TracingFeature(enabled bool) pod.Mutation { | ||
| return pod.Mutation{ | ||
| Name: "Tracing", | ||
| Feature: feature.NewResourceFeature("any", nil).When(enabled), | ||
| Mutate: func(m *pod.Mutator) error { | ||
| m.EditObjectMetadata(func(meta *editors.ObjectMetaEditor) error { | ||
| meta.EnsureLabel("sidecar.jaegertracing.io/inject", "true") | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // VersionFeature records the desired version on the pod as a label. | ||
| // It avoids mutating container images directly; while Kubernetes allows | ||
| // in-place image updates, this example keeps mutations in metadata only. | ||
| func VersionFeature(version string) pod.Mutation { | ||
|
sourcehawk marked this conversation as resolved.
|
||
| return pod.Mutation{ | ||
| Name: "Version", | ||
| Feature: feature.NewResourceFeature(version, nil), | ||
| Mutate: func(m *pod.Mutator) error { | ||
| m.EditObjectMetadata(func(meta *editors.ObjectMetaEditor) error { | ||
| meta.EnsureLabel("app.kubernetes.io/version", version) | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.