-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement pdb primitive #24
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
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
04fbe14
Add PodDisruptionBudget primitive and PDB spec editor
sourcehawk 918cf61
Add PDB primitive example
sourcehawk ca8baf0
Add PDB primitive documentation
sourcehawk 6f9a13f
preserve server-managed metadata in default field applicator
sourcehawk 20fc3e8
fix: rename shadowed variable in PDB builder identity func
sourcehawk 0d96de1
fix: pdb mutator constructor should not call beginFeature
sourcehawk fa0bdb9
docs: update pdb DefaultFieldApplicator comment and docs to reflect P…
sourcehawk 4ad9bac
fix: export BeginFeature to satisfy FeatureMutator interface
sourcehawk acde48c
Merge remote-tracking branch 'origin/main' into feature/pdb-primitive
sourcehawk e28f283
style: format markdown files with prettier
sourcehawk 785838f
test: strengthen PDB test coverage for status preservation and operat…
sourcehawk 5e04901
style: fix gofmt alignment in PDB test
sourcehawk 8c55158
build: add pdb-primitive example to run-examples Makefile target
sourcehawk ef48685
Merge remote-tracking branch 'origin/main' into feature/pdb-primitive
sourcehawk aef4f43
fix: do not initialize an empty plan on PDB mutator construction
sourcehawk 74ebb1b
Merge remote-tracking branch 'origin/main' into feature/pdb-primitive
sourcehawk 0b8f836
refactor: remove field applicators and flavors from PDB primitive
sourcehawk c2d4c7d
fix: remove references to deleted field application flavor API in exa…
sourcehawk 13c07f3
docs: remove stale PreserveCurrentLabels reference from PDB example R…
sourcehawk b6a8aa1
fix: add defensive nil-active checks and selector to PDB test helper
sourcehawk bb8c0e2
fix: propagate errors from EditSpec and EditObjectMetadata in tests a…
sourcehawk c5e5149
fix: check error returns and fix import grouping in PDB tests
sourcehawk 7a66032
merge: resolve conflicts from main into feature/pdb-primitive
sourcehawk a012192
docs: clarify nil-edit behavior in PDB mutator type comment
sourcehawk 0bf0e07
Merge branch 'main' into feature/pdb-primitive
sourcehawk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| # PodDisruptionBudget Primitive | ||
|
|
||
| The `pdb` primitive is the framework's built-in static abstraction for managing Kubernetes `PodDisruptionBudget` | ||
| resources. It integrates with the component lifecycle and provides a structured mutation API for managing disruption | ||
| policies and object metadata. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| | Capability | Detail | | ||
| | --------------------- | ---------------------------------------------------------------------------------------------- | | ||
| | **Static lifecycle** | No health tracking, grace periods, or suspension — the resource is reconciled to desired state | | ||
| | **Mutation pipeline** | Typed editors for PDB spec and object metadata, with a raw escape hatch for free-form access | | ||
| | **Data extraction** | Reads generated or updated values back from the reconciled PDB after each sync cycle | | ||
|
|
||
| ## Building a PDB Primitive | ||
|
|
||
| ```go | ||
| import "github.com/sourcehawk/operator-component-framework/pkg/primitives/pdb" | ||
|
|
||
| minAvailable := intstr.FromString("50%") | ||
| base := &policyv1.PodDisruptionBudget{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "web-server-pdb", | ||
| Namespace: owner.Namespace, | ||
| }, | ||
| Spec: policyv1.PodDisruptionBudgetSpec{ | ||
| MinAvailable: &minAvailable, | ||
| Selector: &metav1.LabelSelector{ | ||
| MatchLabels: map[string]string{"app": "web-server"}, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| resource, err := pdb.NewBuilder(base). | ||
| WithMutation(MyFeatureMutation(owner.Spec.Version)). | ||
| Build() | ||
| ``` | ||
|
|
||
| ## Mutations | ||
|
|
||
| Mutations are the primary mechanism for modifying a `PodDisruptionBudget` 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) pdb.Mutation { | ||
| return pdb.Mutation{ | ||
| Name: "my-feature", | ||
| Feature: feature.NewResourceFeature(version, nil), // always enabled | ||
| Mutate: func(m *pdb.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 | ||
|
|
||
| ```go | ||
| func StrictAvailabilityMutation(version string, enabled bool) pdb.Mutation { | ||
| return pdb.Mutation{ | ||
| Name: "strict-availability", | ||
| Feature: feature.NewResourceFeature(version, nil).When(enabled), | ||
| Mutate: func(m *pdb.Mutator) error { | ||
| return m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error { | ||
| e.ClearMinAvailable() | ||
| e.SetMaxUnavailable(intstr.FromInt32(1)) | ||
| return nil | ||
| }) | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Version-gated mutations | ||
|
|
||
| ```go | ||
| var legacyConstraint = mustSemverConstraint("< 2.0.0") | ||
|
|
||
| func LegacyPDBMutation(version string) pdb.Mutation { | ||
| return pdb.Mutation{ | ||
| Name: "legacy-pdb", | ||
| Feature: feature.NewResourceFeature( | ||
| version, | ||
| []feature.VersionConstraint{legacyConstraint}, | ||
| ), | ||
| Mutate: func(m *pdb.Mutator) error { | ||
| return m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error { | ||
| e.SetMinAvailable(intstr.FromInt32(1)) | ||
| return nil | ||
| }) | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| All version constraints and `When()` conditions must be satisfied for a mutation to apply. | ||
|
|
||
| ## Internal Mutation Ordering | ||
|
|
||
| Within a single mutation, edit operations are applied in a fixed category order regardless of the order they are | ||
| recorded: | ||
|
|
||
| | Step | Category | What it affects | | ||
| | ---- | -------------- | ------------------------------------------------------- | | ||
| | 1 | Metadata edits | Labels and annotations on the `PodDisruptionBudget` | | ||
| | 2 | Spec edits | MinAvailable, MaxUnavailable, selector, eviction policy | | ||
|
|
||
| Within each category, edits are applied in their registration order. Later features observe the PodDisruptionBudget as | ||
| modified by all previous features. | ||
|
|
||
| ## Editors | ||
|
|
||
| ### PodDisruptionBudgetSpecEditor | ||
|
|
||
| The primary API for modifying the PDB spec. Use `m.EditSpec` for full control: | ||
|
|
||
| ```go | ||
| m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error { | ||
| e.SetMinAvailable(intstr.FromString("50%")) | ||
| e.SetSelector(&metav1.LabelSelector{ | ||
| MatchLabels: map[string]string{"app": "web"}, | ||
| }) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### SetMinAvailable and SetMaxUnavailable | ||
|
|
||
| `SetMinAvailable` sets the minimum number of pods that must remain available during a disruption. `SetMaxUnavailable` | ||
| sets the maximum number of pods that can be unavailable. Both accept `intstr.IntOrString` — either an integer count or a | ||
| percentage string (e.g. `"50%"`). | ||
|
|
||
| These fields are mutually exclusive in the Kubernetes API. Use `ClearMinAvailable` or `ClearMaxUnavailable` to remove | ||
| the opposing constraint when switching between them: | ||
|
|
||
| ```go | ||
| m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error { | ||
| e.ClearMinAvailable() | ||
| e.SetMaxUnavailable(intstr.FromInt32(1)) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### SetSelector | ||
|
|
||
| `SetSelector` replaces the pod selector used by the PDB: | ||
|
|
||
| ```go | ||
| m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error { | ||
| e.SetSelector(&metav1.LabelSelector{ | ||
| MatchLabels: map[string]string{"app": "web", "tier": "frontend"}, | ||
| }) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### SetUnhealthyPodEvictionPolicy | ||
|
|
||
| `SetUnhealthyPodEvictionPolicy` controls how unhealthy pods are handled during eviction. Valid values are | ||
| `policyv1.IfHealthyBudget` and `policyv1.AlwaysAllow`. Use `ClearUnhealthyPodEvictionPolicy` to revert to the cluster | ||
| default: | ||
|
|
||
| ```go | ||
| m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error { | ||
| e.SetUnhealthyPodEvictionPolicy(policyv1.AlwaysAllow) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### Raw Escape Hatch | ||
|
|
||
| `Raw()` returns the underlying `*policyv1.PodDisruptionBudgetSpec` for direct access when the typed API is insufficient: | ||
|
|
||
| ```go | ||
| m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error { | ||
| e.Raw().MinAvailable = &customValue | ||
| 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) | ||
| e.EnsureAnnotation("pdb.example.io/policy", "strict") | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ## Full Example: Feature-Gated Disruption Policy | ||
|
|
||
| ```go | ||
| func BasePDBMutation(version string) pdb.Mutation { | ||
| return pdb.Mutation{ | ||
| Name: "base-pdb", | ||
| Feature: feature.NewResourceFeature(version, nil), | ||
| Mutate: func(m *pdb.Mutator) error { | ||
| return m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error { | ||
| e.EnsureLabel("app.kubernetes.io/version", version) | ||
| return nil | ||
| }) | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func StrictAvailabilityMutation(version string, enabled bool) pdb.Mutation { | ||
| return pdb.Mutation{ | ||
| Name: "strict-availability", | ||
| Feature: feature.NewResourceFeature(version, nil).When(enabled), | ||
| Mutate: func(m *pdb.Mutator) error { | ||
| return m.EditSpec(func(e *editors.PodDisruptionBudgetSpecEditor) error { | ||
| e.ClearMinAvailable() | ||
| e.SetMaxUnavailable(intstr.FromInt32(1)) | ||
| return nil | ||
| }) | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| resource, err := pdb.NewBuilder(base). | ||
| WithMutation(BasePDBMutation(owner.Spec.Version)). | ||
| WithMutation(StrictAvailabilityMutation(owner.Spec.Version, owner.Spec.StrictMode)). | ||
| Build() | ||
| ``` | ||
|
|
||
| When `StrictMode` is true, the PDB switches from percentage-based `MinAvailable` to an absolute `MaxUnavailable` of 1. | ||
| When false, only the base mutation runs and the original `MinAvailable` from the baseline is preserved. Neither mutation | ||
| needs to know about the other. | ||
|
|
||
| ## 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. | ||
|
|
||
| **`MinAvailable` and `MaxUnavailable` are mutually exclusive.** When switching between them, always clear the opposing | ||
| field first. The typed API makes this explicit with `ClearMinAvailable` and `ClearMaxUnavailable`. | ||
|
|
||
| **Register mutations in dependency order.** If mutation B relies on state set by mutation A, register A first. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # PodDisruptionBudget Primitive Example | ||
|
|
||
| This example demonstrates the usage of the `pdb` primitive within the operator component framework. It shows how to | ||
| manage a Kubernetes PodDisruptionBudget as a component of a larger application, utilising features like: | ||
|
|
||
| - **Base Construction**: Initializing a PDB with a percentage-based `MinAvailable` and a label selector. | ||
| - **Feature Mutations**: Switching between `MinAvailable` and `MaxUnavailable` based on a feature toggle via `EditSpec`. | ||
| - **Metadata Mutations**: Setting version labels on the PDB via `EditObjectMetadata`. | ||
| - **Data Extraction**: Inspecting the reconciled PDB's disruption policy after each sync cycle. | ||
|
|
||
| ## Directory Structure | ||
|
|
||
| - `app/`: Defines the controller that uses the component framework. The `ExampleApp` CRD is shared from | ||
| `examples/shared/app`. | ||
| - `features/`: Contains modular feature definitions: | ||
| - `mutations.go`: version labelling and feature-gated strict availability. | ||
| - `resources/`: Contains the central `NewPDBResource` factory that assembles all features using `pdb.Builder`. | ||
| - `main.go`: A standalone entry point that demonstrates multiple reconciliation cycles with a fake client. | ||
|
|
||
| ## Running the Example | ||
|
|
||
| ```bash | ||
| go run examples/pdb-primitive/main.go | ||
| ``` | ||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| This will: | ||
|
|
||
| 1. Initialize a fake Kubernetes client. | ||
| 2. Create an `ExampleApp` owner object. | ||
| 3. Reconcile through four spec variations, printing the PDB disruption policy after each cycle. | ||
| 4. Print the resulting status conditions. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // Package app provides a sample controller using the PDB primitive. | ||
| package app | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| sharedapp "github.com/sourcehawk/operator-component-framework/examples/shared/app" | ||
| "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 | ||
|
|
||
| // NewPDBResource is a factory function to create the PDB resource. | ||
| // This allows us to inject the resource construction logic. | ||
| NewPDBResource func(*sharedapp.ExampleApp) (component.Resource, error) | ||
| } | ||
|
|
||
| // Reconcile performs the reconciliation for a single ExampleApp. | ||
| func (r *ExampleController) Reconcile(ctx context.Context, owner *sharedapp.ExampleApp) error { | ||
| // 1. Build the PDB resource for this owner. | ||
| pdbResource, err := r.NewPDBResource(owner) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // 2. Build the component that manages the PDB. | ||
| comp, err := component.NewComponentBuilder(). | ||
| WithName("example-app"). | ||
| WithConditionType("AppReady"). | ||
| WithResource(pdbResource, component.ResourceOptions{}). | ||
| 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) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run-examplesnow runs./examples/pdb-primitive/., but the new pdb example currently references non-existent APIs (WithFieldApplicationFlavor/PreserveCurrentLabels) and will fail to build. Please either fix the example so it compiles before adding it torun-examples, or defer adding it to this target until the API is implemented.