-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement pv primitive #27
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
26 commits
Select commit
Hold shift + click to select a range
e3486d6
Add PVSpecEditor for PersistentVolume spec mutations
sourcehawk b46717d
Add PersistentVolume primitive with Integration lifecycle
sourcehawk 650b118
Add PersistentVolume primitive documentation
sourcehawk 700431b
Add PersistentVolume primitive example
sourcehawk 8f995fe
Fix staticcheck lint: remove redundant embedded field selector
sourcehawk c117fa2
Address Copilot review comments on PV primitive PR
sourcehawk 597e7de
fix lint
sourcehawk f06d21c
Add unit tests for DefaultFieldApplicator
sourcehawk 4eb153c
preserve server-managed metadata in default field applicator
sourcehawk 46fb62b
docs: address PR review feedback for PV primitive
sourcehawk 195ad33
refactor: delegate PV builder to generic IntegrationBuilder.Build()
sourcehawk 35bc9b1
fix: remove beginFeature from PV mutator constructor
sourcehawk 29fadc1
fix: address PR review feedback for PV primitive
sourcehawk 96f290d
fix: export BeginFeature() on PV mutator to match FeatureMutator inte…
sourcehawk 1a4ad33
Merge remote-tracking branch 'origin/main' into feature/pv-primitive
sourcehawk 045722c
style: format markdown files with prettier
sourcehawk 15c7aaa
Merge remote-tracking branch 'origin/main' into feature/pv-primitive
sourcehawk b64e1dc
fix: do not initialize empty feature plan in PV mutator constructor
sourcehawk 9707069
Merge remote-tracking branch 'origin/main' into feature/pv-primitive
sourcehawk 34983d4
refactor: remove field applicators and flavors from PV primitive
sourcehawk 8f143cf
fix: remove references to deleted WithFieldApplicationFlavor and Pres…
sourcehawk 32d81f4
fix: format pv.md to pass prettier lint check
sourcehawk db71071
fix: correct misleading comment about flavor-based annotation preserv…
sourcehawk 175fd91
fix: align operational status names in docs and comments with code
sourcehawk 7c6589c
fix docs
sourcehawk e8aa859
Merge branch 'main' into feature/pv-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,259 @@ | ||
| # PersistentVolume Primitive | ||
|
|
||
| The `pv` primitive is the framework's built-in integration abstraction for managing Kubernetes `PersistentVolume` | ||
| resources. It integrates with the component lifecycle and provides a structured mutation API for managing PV spec fields | ||
| and object metadata. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| | Capability | Detail | | ||
| | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | **Integration lifecycle** | Reports `concepts.OperationalStatusOperational`, `concepts.OperationalStatusPending`, or `concepts.OperationalStatusFailing` based on the PV's phase | | ||
| | **Cluster-scoped** | No namespace in the identity or builder — PersistentVolumes are cluster-scoped resources | | ||
| | **Mutation pipeline** | Typed editors for PV spec fields and object metadata, with a raw escape hatch for free-form access | | ||
| | **Data extraction** | Reads generated or updated values back from the reconciled PersistentVolume after each sync cycle | | ||
|
|
||
| ## Building a PersistentVolume Primitive | ||
|
|
||
| ```go | ||
| import "github.com/sourcehawk/operator-component-framework/pkg/primitives/pv" | ||
|
|
||
| base := &corev1.PersistentVolume{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "data-volume", | ||
| }, | ||
| Spec: corev1.PersistentVolumeSpec{ | ||
| Capacity: corev1.ResourceList{ | ||
| corev1.ResourceStorage: resource.MustParse("100Gi"), | ||
| }, | ||
| AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, | ||
| PersistentVolumeSource: corev1.PersistentVolumeSource{ | ||
| CSI: &corev1.CSIPersistentVolumeSource{ | ||
| Driver: "ebs.csi.aws.com", | ||
| VolumeHandle: "vol-abc123", | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| resource, err := pv.NewBuilder(base). | ||
| WithMutation(MyFeatureMutation(owner.Spec.Version)). | ||
| Build() | ||
| ``` | ||
|
|
||
| PersistentVolumes are cluster-scoped. The builder validates that Name is set and that Namespace is empty. Setting a | ||
| namespace on the PV object will cause `Build()` to return an error. | ||
|
|
||
| ## Mutations | ||
|
|
||
| Mutations are the primary mechanism for modifying a `PersistentVolume` 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) pv.Mutation { | ||
| return pv.Mutation{ | ||
| Name: "my-feature", | ||
| Feature: feature.NewResourceFeature(version, nil), // always enabled | ||
| Mutate: func(m *pv.Mutator) error { | ||
| m.SetStorageClassName("fast-ssd") | ||
| 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 RetainPolicyMutation(version string, retainEnabled bool) pv.Mutation { | ||
| return pv.Mutation{ | ||
| Name: "retain-policy", | ||
| Feature: feature.NewResourceFeature(version, nil).When(retainEnabled), | ||
| Mutate: func(m *pv.Mutator) error { | ||
| m.SetReclaimPolicy(corev1.PersistentVolumeReclaimRetain) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Version-gated mutations | ||
|
|
||
| ```go | ||
| var legacyConstraint = mustSemverConstraint("< 2.0.0") | ||
|
|
||
| func LegacyStorageClassMutation(version string) pv.Mutation { | ||
| return pv.Mutation{ | ||
| Name: "legacy-storage-class", | ||
| Feature: feature.NewResourceFeature( | ||
| version, | ||
| []feature.VersionConstraint{legacyConstraint}, | ||
| ), | ||
| Mutate: func(m *pv.Mutator) error { | ||
| m.SetStorageClassName("legacy-hdd") | ||
| 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 `PersistentVolume` | | ||
| | 2 | Spec edits | PV spec fields — storage class, reclaim policy, mount options, etc. | | ||
|
|
||
| Within each category, edits are applied in their registration order. Later features observe the PersistentVolume as | ||
| modified by all previous features. | ||
|
|
||
| ## Editors | ||
|
|
||
| ### PVSpecEditor | ||
|
|
||
| The primary API for modifying PersistentVolume spec fields. Use `m.EditPVSpec` for full control: | ||
|
|
||
| ```go | ||
| m.EditPVSpec(func(e *editors.PVSpecEditor) error { | ||
| e.SetCapacity(resource.MustParse("200Gi")) | ||
| e.SetAccessModes([]corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}) | ||
| e.SetPersistentVolumeReclaimPolicy(corev1.PersistentVolumeReclaimRetain) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### Available methods | ||
|
|
||
| | Method | What it sets | | ||
| | -------------------------------------- | -------------------------------------- | | ||
| | `SetCapacity(resource.Quantity)` | `.spec.capacity[storage]` | | ||
| | `SetAccessModes([]AccessMode)` | `.spec.accessModes` | | ||
| | `SetPersistentVolumeReclaimPolicy` | `.spec.persistentVolumeReclaimPolicy` | | ||
| | `SetStorageClassName(string)` | `.spec.storageClassName` | | ||
| | `SetMountOptions([]string)` | `.spec.mountOptions` | | ||
| | `SetVolumeMode(PersistentVolumeMode)` | `.spec.volumeMode` | | ||
| | `SetNodeAffinity(*VolumeNodeAffinity)` | `.spec.nodeAffinity` | | ||
| | `Raw()` | Returns `*corev1.PersistentVolumeSpec` | | ||
|
|
||
| #### Raw escape hatch | ||
|
|
||
| `Raw()` returns the underlying `*corev1.PersistentVolumeSpec` for free-form editing when none of the structured methods | ||
| are sufficient: | ||
|
|
||
| ```go | ||
| m.EditPVSpec(func(e *editors.PVSpecEditor) error { | ||
| e.Raw().PersistentVolumeReclaimPolicy = corev1.PersistentVolumeReclaimDelete | ||
| 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("storage-tier", "premium") | ||
| e.EnsureAnnotation("provisioned-by", "my-operator") | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ## Convenience Methods | ||
|
|
||
| The `Mutator` exposes convenience wrappers for the most common PV spec operations: | ||
|
|
||
| | Method | Equivalent to | | ||
| | --------------------------- | ------------------------------------------------------ | | ||
| | `SetStorageClassName(name)` | `EditPVSpec` → `e.SetStorageClassName(name)` | | ||
| | `SetReclaimPolicy(policy)` | `EditPVSpec` → `e.SetPersistentVolumeReclaimPolicy(p)` | | ||
| | `SetMountOptions(opts)` | `EditPVSpec` → `e.SetMountOptions(opts)` | | ||
|
|
||
| Use these for simple, single-operation mutations. Use `EditPVSpec` when you need multiple operations or raw access in a | ||
| single edit block. | ||
|
|
||
| ## Operational Status | ||
|
|
||
| The PV primitive uses the Integration lifecycle. The default operational status handler maps PV phases to framework | ||
| status: | ||
|
|
||
| | PV Phase | Operational Status | Meaning | | ||
| | --------- | ---------------------------- | -------------------------------------- | | ||
| | Available | OperationalStatusOperational | PV is ready for binding | | ||
| | Bound | OperationalStatusOperational | PV is bound to a PersistentVolumeClaim | | ||
| | Pending | OperationalStatusPending | PV is waiting to become available | | ||
| | Released | OperationalStatusFailing | PV was released, not yet reclaimed | | ||
| | Failed | OperationalStatusFailing | PV reclamation has failed | | ||
|
|
||
| Override with `WithCustomOperationalStatus` when your PV requires different readiness logic. | ||
|
|
||
| ## Full Example: Storage-Tier PersistentVolume | ||
|
|
||
| ```go | ||
| func StorageClassMutation(version string) pv.Mutation { | ||
| return pv.Mutation{ | ||
| Name: "storage-class", | ||
| Feature: feature.NewResourceFeature(version, nil), | ||
| Mutate: func(m *pv.Mutator) error { | ||
| m.SetStorageClassName("fast-ssd") | ||
| m.SetReclaimPolicy(corev1.PersistentVolumeReclaimRetain) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func TierLabelMutation(version, tier string) pv.Mutation { | ||
| return pv.Mutation{ | ||
| Name: "tier-label", | ||
| Feature: feature.NewResourceFeature(version, nil), | ||
| Mutate: func(m *pv.Mutator) error { | ||
| m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error { | ||
| e.EnsureLabel("storage-tier", tier) | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| resource, err := pv.NewBuilder(base). | ||
| WithMutation(StorageClassMutation(owner.Spec.Version)). | ||
| WithMutation(TierLabelMutation(owner.Spec.Version, "premium")). | ||
| Build() | ||
| ``` | ||
|
|
||
| ## Guidance | ||
|
|
||
| **PersistentVolumes are cluster-scoped.** Do not set a namespace on the PV object. The builder rejects namespaced PVs | ||
| with a clear error. | ||
|
|
||
| **Use the Integration lifecycle for status.** PVs report `OperationalStatusOperational`, `OperationalStatusPending`, or | ||
| `OperationalStatusFailing` based on their phase. Override with `WithCustomOperationalStatus` only when phase-based | ||
| readiness is insufficient. | ||
|
|
||
| **Controller references and garbage collection.** The component reconciliation pipeline attempts to set a controller | ||
| reference on created/updated resources. Because `PersistentVolume` is cluster-scoped, its controller owner must also be | ||
| cluster-scoped. When the owner is namespace-scoped and the PV is cluster-scoped, the framework detects this mismatch and | ||
| **skips setting `ownerReferences`** (logging an informational message) instead of letting the API server reject the | ||
| request. As a result, such PVs will **not** be garbage collected automatically when the owning component is deleted. If | ||
| you need garbage collection for PVs, either: | ||
|
|
||
| - Model the PV as owned by a dedicated **cluster-scoped** controller/component so a valid controller reference can be | ||
| set, or | ||
| - Accept that PVs managed from a **namespace-scoped** component will not have `ownerReferences` and handle their | ||
| lifecycle explicitly (for example, by deleting them in custom logic when appropriate). | ||
|
|
||
| **Register mutations in dependency order.** If mutation B relies on a field 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,30 @@ | ||
| # PersistentVolume Primitive Example | ||
|
|
||
| This example demonstrates the usage of the `pv` primitive within the operator component framework. It shows how to | ||
| manage a Kubernetes PersistentVolume as a component of a larger application, utilising features like: | ||
|
|
||
| - **Base Construction**: Initializing a cluster-scoped PersistentVolume with storage configuration. | ||
| - **Feature Mutations**: Applying feature-gated changes to reclaim policy, mount options, and metadata. | ||
| - **Field Flavors**: Preserving annotations managed by external controllers using `PreserveCurrentAnnotations`. | ||
| - **Result Inspection**: Printing PV configuration 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, boolean-gated retain policy, and mount options mutations. | ||
| - `resources/`: Contains the central `NewPVResource` factory that assembles all features using `pv.Builder`. | ||
| - `main.go`: A standalone entry point that demonstrates in-memory mutation across multiple spec variations. | ||
|
|
||
| ## Running the Example | ||
|
|
||
| ```bash | ||
| go run examples/pv-primitive/main.go | ||
| ``` | ||
|
|
||
| This will: | ||
|
|
||
| 1. Create an `ExampleApp` owner object. | ||
| 2. Apply mutations across four spec variations, printing the resulting PV YAML after each cycle. | ||
| 3. Print operational status examples for each PV phase. |
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,13 @@ | ||
| // Package app provides a sample controller using the pv primitive. | ||
| // | ||
| // PersistentVolumes are cluster-scoped resources. In a real operator, the owner | ||
| // would typically be a cluster-scoped CRD (e.g. a ClusterStorageConfig). When a | ||
| // component create pipeline is used, it will only set controller references where | ||
| // the owner/owned scopes are compatible, and will skip ownerReferences for | ||
| // cluster-scoped resources that would otherwise have namespace-scoped owners. In | ||
| // those skipped cases, the cluster-scoped resource will not be garbage-collected | ||
| // with the namespaced owner. | ||
| // | ||
| // This example demonstrates the PV primitive's builder, mutation, and status APIs | ||
| // without full component reconciliation to keep the focus on the primitive itself. | ||
| package app |
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,61 @@ | ||
| // Package features provides sample mutations for the pv primitive example. | ||
| package features | ||
|
|
||
| import ( | ||
| "github.com/sourcehawk/operator-component-framework/pkg/component/concepts" | ||
| "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/pv" | ||
| corev1 "k8s.io/api/core/v1" | ||
| ) | ||
|
|
||
| // VersionLabelMutation sets the app.kubernetes.io/version label on the PersistentVolume. | ||
| // It is always enabled. | ||
| func VersionLabelMutation(version string) pv.Mutation { | ||
| return pv.Mutation{ | ||
| Name: "version-label", | ||
| Feature: feature.NewResourceFeature(version, nil), | ||
| Mutate: func(m *pv.Mutator) error { | ||
| m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error { | ||
| e.EnsureLabel("app.kubernetes.io/version", version) | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // RetainPolicyMutation sets the reclaim policy to Retain when enabled. | ||
| // This is gated by a boolean condition. | ||
| func RetainPolicyMutation(version string, enabled bool) pv.Mutation { | ||
| return pv.Mutation{ | ||
| Name: "retain-policy", | ||
| Feature: feature.NewResourceFeature(version, nil).When(enabled), | ||
| Mutate: func(m *pv.Mutator) error { | ||
| m.SetReclaimPolicy(corev1.PersistentVolumeReclaimRetain) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // MountOptionsMutation adds NFS mount options when enabled. | ||
| // This is gated by a boolean condition. | ||
| func MountOptionsMutation(version string, enabled bool) pv.Mutation { | ||
| return pv.Mutation{ | ||
| Name: "mount-options", | ||
| Feature: feature.NewResourceFeature(version, nil).When(enabled), | ||
| Mutate: func(m *pv.Mutator) error { | ||
| m.SetMountOptions([]string{"hard", "nfsvers=4.1"}) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // ExampleOperationalStatus demonstrates the default operational status handler | ||
| // by returning the status for a given PV phase. | ||
| func ExampleOperationalStatus(phase corev1.PersistentVolumePhase) (concepts.OperationalStatusWithReason, error) { | ||
| p := &corev1.PersistentVolume{ | ||
| Status: corev1.PersistentVolumeStatus{Phase: phase}, | ||
| } | ||
| return pv.DefaultOperationalStatusHandler(concepts.ConvergingOperationNone, p) | ||
| } |
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.