-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement pvc primitive #34
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
979a02e
Add PVCSpecEditor for PersistentVolumeClaim spec mutations
sourcehawk 8342c75
Add PVC primitive package with Integration lifecycle
sourcehawk 943fcbf
Add PVC primitive documentation
sourcehawk 3a91a5d
Add PVC primitive example
sourcehawk d41665c
Add unit tests for DefaultFieldApplicator
sourcehawk ffb1d90
preserve server-managed metadata in default field applicator
sourcehawk 4e7b2bb
add PVC primitive to built-in primitives documentation table
sourcehawk 25906de
fix(pvc): initialize first feature plan inline instead of calling beg…
sourcehawk 2c25d1f
fix(pvc): address PR review - preserve status, add test, fix docs
sourcehawk dc9f30a
fix(pvc): export BeginFeature to match FeatureMutator interface
sourcehawk aa16978
Merge remote-tracking branch 'origin/main' into feature/pvc-primitive
sourcehawk b218c99
style: format markdown files with prettier
sourcehawk ab9b982
fix(pvc): correct interface and status names in comments and docs
sourcehawk 0a752d3
style: format pvc.md with prettier
sourcehawk a583539
docs(pvc): fix mutation ordering docs to reflect feature boundary gro…
sourcehawk 7438e02
docs(pvc): use correct operational status names in status table
sourcehawk f201f48
test(pvc): add resource-level behavioral tests
sourcehawk 1c649fe
Merge remote-tracking branch 'origin/main' into feature/pvc-primitive
sourcehawk 0a6187a
fix(pvc): do not initialize an empty plan on mutator construction
sourcehawk 85c7299
Merge remote-tracking branch 'origin/main' into feature/pvc-primitive
sourcehawk 4bee9e1
refactor(pvc): remove field applicators and flavors for SSA migration
sourcehawk c6b4d4b
fix(pvc): address Copilot review comments
sourcehawk 73bb703
fix formatting
sourcehawk 9cd3be2
fix(pvc): clarify immutable field comments and add PVCSpecEditor to docs
sourcehawk e61a070
Merge branch 'main' into feature/pvc-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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| # PersistentVolumeClaim Primitive | ||
|
|
||
| The `pvc` primitive is the framework's built-in integration abstraction for managing Kubernetes `PersistentVolumeClaim` | ||
| resources. It integrates with the component lifecycle and provides a structured mutation API for managing storage | ||
| requests and object metadata. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| | Capability | Detail | | ||
| | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | **Operational tracking** | Monitors PVC phase — reports `OperationalStatusOperational` (Bound), `OperationalStatusPending`, or `OperationalStatusFailing` (Lost) | | ||
| | **Suspension** | PVCs are immediately suspended (no runtime state to wind down); data is preserved by default | | ||
| | **Mutation pipeline** | Typed editors for PVC spec and object metadata, with a raw escape hatch for free-form access | | ||
| | **Data extraction** | Reads bound volume name, capacity, or other status fields after each sync cycle | | ||
|
|
||
| ## Building a PVC Primitive | ||
|
|
||
| ```go | ||
| import "github.com/sourcehawk/operator-component-framework/pkg/primitives/pvc" | ||
|
|
||
| base := &corev1.PersistentVolumeClaim{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "app-data", | ||
| Namespace: owner.Namespace, | ||
| }, | ||
| Spec: corev1.PersistentVolumeClaimSpec{ | ||
| AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, | ||
| Resources: corev1.VolumeResourceRequirements{ | ||
| Requests: corev1.ResourceList{ | ||
| corev1.ResourceStorage: resource.MustParse("10Gi"), | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| resource, err := pvc.NewBuilder(base). | ||
| WithMutation(MyStorageMutation(owner.Spec.Version)). | ||
| Build() | ||
| ``` | ||
|
|
||
| ## Mutations | ||
|
|
||
| Mutations are the primary mechanism for modifying a `PersistentVolumeClaim` 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 MyStorageMutation(version string) pvc.Mutation { | ||
| return pvc.Mutation{ | ||
| Name: "storage-expansion", | ||
| Feature: feature.NewResourceFeature(version, nil), // always enabled | ||
| Mutate: func(m *pvc.Mutator) error { | ||
| m.SetStorageRequest(resource.MustParse("20Gi")) | ||
| 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 LargeStorageMutation(version string, needsLargeStorage bool) pvc.Mutation { | ||
| return pvc.Mutation{ | ||
| Name: "large-storage", | ||
| Feature: feature.NewResourceFeature(version, nil).When(needsLargeStorage), | ||
| Mutate: func(m *pvc.Mutator) error { | ||
| m.SetStorageRequest(resource.MustParse("100Gi")) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Version-gated mutations | ||
|
|
||
| ```go | ||
| var v2Constraint = mustSemverConstraint(">= 2.0.0") | ||
|
|
||
| func V2StorageMutation(version string) pvc.Mutation { | ||
| return pvc.Mutation{ | ||
| Name: "v2-storage", | ||
| Feature: feature.NewResourceFeature( | ||
| version, | ||
| []feature.VersionConstraint{v2Constraint}, | ||
| ), | ||
| Mutate: func(m *pvc.Mutator) error { | ||
| m.SetStorageRequest(resource.MustParse("50Gi")) | ||
| 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 `PersistentVolumeClaim` | | ||
| | 2 | Spec edits | PVC spec — storage requests, access modes, etc. | | ||
|
|
||
| Within each category, edits are applied in their registration order. The PVC primitive groups mutations by feature | ||
| boundary: for each applicable feature (after evaluating version constraints and any `When()` conditions), all of its | ||
| planned edits are applied in order, and later features and mutations observe the fully-applied state from earlier ones. | ||
|
|
||
| ## Editors | ||
|
|
||
| ### PVCSpecEditor | ||
|
|
||
| The primary API for modifying PVC spec fields. Use `m.EditPVCSpec` for full control: | ||
|
|
||
| ```go | ||
| m.EditPVCSpec(func(e *editors.PVCSpecEditor) error { | ||
| e.SetStorageRequest(resource.MustParse("20Gi")) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| Available methods: | ||
|
|
||
| | Method | What it does | | ||
| | --------------------- | ------------------------------------------------------- | | ||
| | `SetStorageRequest` | Sets `spec.resources.requests[storage]` | | ||
| | `SetAccessModes` | Sets `spec.accessModes` (immutable after creation) | | ||
| | `SetStorageClassName` | Sets `spec.storageClassName` (immutable after creation) | | ||
| | `SetVolumeMode` | Sets `spec.volumeMode` (immutable after creation) | | ||
| | `SetVolumeName` | Sets `spec.volumeName` (immutable after creation) | | ||
| | `Raw` | Returns `*corev1.PersistentVolumeClaimSpec` | | ||
|
|
||
| #### Raw Escape Hatch | ||
|
|
||
| `Raw()` returns the underlying `*corev1.PersistentVolumeClaimSpec` for free-form editing when none of the structured | ||
| methods are sufficient: | ||
|
|
||
| ```go | ||
| m.EditPVCSpec(func(e *editors.PVCSpecEditor) error { | ||
| raw := e.Raw() | ||
| raw.Selector = &metav1.LabelSelector{ | ||
| MatchLabels: map[string]string{"type": "fast"}, | ||
| } | ||
| 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("storage/class-hint", "fast-ssd") | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ## Convenience Methods | ||
|
|
||
| The `Mutator` exposes a convenience wrapper for the most common PVC operation: | ||
|
|
||
| | Method | Equivalent to | | ||
| | ----------------------------- | ----------------------------------------------- | | ||
| | `SetStorageRequest(quantity)` | `EditPVCSpec` → `e.SetStorageRequest(quantity)` | | ||
|
|
||
| Use this for simple, single-operation mutations. Use `EditPVCSpec` when you need multiple operations or raw access in a | ||
| single edit block. | ||
|
|
||
| ## Status Handlers | ||
|
|
||
| ### Operational Status | ||
|
|
||
| The default handler (`DefaultOperationalStatusHandler`) maps PVC phase to operational status: | ||
|
|
||
| | PVC Phase | Status | Reason | | ||
| | --------- | ------------------------------ | ------------------------------- | | ||
| | `Bound` | `OperationalStatusOperational` | PVC is bound to volume \<name\> | | ||
| | `Pending` | `OperationalStatusPending` | Waiting for PVC to be bound | | ||
| | `Lost` | `OperationalStatusFailing` | PVC has lost its bound volume | | ||
|
|
||
| Override with `WithCustomOperationalStatus` for additional checks (e.g. verifying specific annotations or volume | ||
| attributes). | ||
|
|
||
| ### Suspension | ||
|
|
||
| PVCs have no runtime state to wind down, so: | ||
|
|
||
| - `DefaultSuspendMutationHandler` is a no-op. | ||
| - `DefaultSuspensionStatusHandler` always reports `Suspended`. | ||
| - `DefaultDeleteOnSuspendHandler` returns `false` to preserve data. | ||
|
|
||
| Override these handlers if you need custom suspension behavior, such as adding annotations when suspended or deleting | ||
| PVCs that use ephemeral storage. | ||
|
|
||
| ## Guidance | ||
|
|
||
| **Register mutations for storage expansion carefully.** Kubernetes only allows expanding PVC storage (not shrinking). | ||
| Ensure your mutations respect this constraint. The `SetStorageRequest` method does not enforce this — the API server | ||
| will reject invalid requests. | ||
|
|
||
| **Prefer `WithCustomSuspendDeletionDecision` over deleting PVCs manually.** If you need PVCs to be cleaned up during | ||
| suspension, register a deletion decision handler rather than deleting them in a mutation. |
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 @@ | ||
| # PVC Primitive Example | ||
|
|
||
| This example demonstrates the usage of the `pvc` primitive within the operator component framework. It shows how to | ||
| manage a Kubernetes PersistentVolumeClaim as a component of a larger application, utilising features like: | ||
|
|
||
| - **Base Construction**: Initializing a PVC with access modes, storage request, and metadata. | ||
| - **Feature Mutations**: Applying conditional storage expansion and metadata updates using the `Mutator`. | ||
| - **Suspension**: PVCs are immediately suspended with data preserved by default. | ||
| - **Data Extraction**: Harvesting PVC status after each reconcile 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, storage annotation, and conditional large-storage expansion. | ||
| - `resources/`: Contains the central `NewPVCResource` factory that assembles all features using `pvc.Builder`. | ||
| - `main.go`: A standalone entry point that demonstrates multiple reconciliation cycles with a fake client. | ||
|
|
||
| ## Running the Example | ||
|
|
||
| ```bash | ||
| go run examples/pvc-primitive/main.go | ||
| ``` | ||
|
|
||
| This will: | ||
|
|
||
| 1. Initialize a fake Kubernetes client. | ||
| 2. Create an `ExampleApp` owner object. | ||
| 3. Reconcile through four spec variations, demonstrating version upgrades, storage expansion, and suspension. | ||
| 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 PVC 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 | ||
|
|
||
| // NewPVCResource is a factory function to create the PVC resource. | ||
| // This allows us to inject the resource construction logic. | ||
| NewPVCResource 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 PVC resource for this owner. | ||
| pvcResource, err := r.NewPVCResource(owner) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // 2. Build the component that manages the PVC. | ||
| comp, err := component.NewComponentBuilder(). | ||
| WithName("example-app"). | ||
| WithConditionType("AppReady"). | ||
| WithResource(pvcResource, 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) | ||
| } |
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,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 |
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.
Uh oh!
There was an error while loading. Please reload this page.