feat: implement daemonset primitive#23
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Kubernetes DaemonSet primitive to the operator-component-framework, following the established patterns of existing workload primitives (e.g., deployment) and providing an end-to-end example plus documentation.
Changes:
- Introduces
pkg/primitives/daemonset/with builder, resource wrapper, status handlers, mutator (with selectors/editors), flavors, and tests. - Adds a new shared editor
pkg/mutation/editors/DaemonSetSpecEditorwith unit tests. - Adds an
examples/daemonset-primitive/runnable example anddocs/primitives/daemonset.mddocumentation.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/primitives/daemonset/resource.go | Workload resource wrapper delegating lifecycle hooks to generic workload resource. |
| pkg/primitives/daemonset/builder.go | Fluent builder wiring defaults (applicator, handlers) and mutation/flavor/extractor registration. |
| pkg/primitives/daemonset/handlers.go | Default converge/grace/suspend handlers for DaemonSet lifecycle/health. |
| pkg/primitives/daemonset/flavors.go | Field-application “flavors” to preserve selected live fields (labels/annotations/template metadata). |
| pkg/primitives/daemonset/mutator.go | Planned mutation engine for DaemonSet (metadata/spec/podspec/containers + presence ops + snapshots). |
| pkg/primitives/daemonset/builder_test.go | Builder API/validation/handler wiring tests. |
| pkg/primitives/daemonset/handlers_test.go | Unit tests for default status/suspension handlers. |
| pkg/primitives/daemonset/flavors_test.go | Unit tests for flavors and flavor ordering/error propagation. |
| pkg/primitives/daemonset/mutator_test.go | Extensive mutator behavior tests (ordering, presence ops, snapshots, nil-safety). |
| pkg/mutation/editors/daemonsetspec.go | Adds typed editor for appsv1.DaemonSetSpec. |
| pkg/mutation/editors/daemonsetspec_test.go | Unit tests for the new DaemonSetSpec editor. |
| examples/daemonset-primitive/app/owner.go | Re-exports shared example CRD types for the new example. |
| examples/daemonset-primitive/app/controller.go | Example controller wiring a component with the DaemonSet primitive. |
| examples/daemonset-primitive/features/mutations.go | Example feature-gated mutations using the DaemonSet mutator APIs. |
| examples/daemonset-primitive/features/flavors.go | Example flavors wiring for preserving labels/annotations. |
| examples/daemonset-primitive/resources/daemonset.go | Example DaemonSet resource factory composing builder + features + flavors + extraction. |
| examples/daemonset-primitive/main.go | Runnable fake-client reconciliation sequence demonstrating the primitive. |
| examples/daemonset-primitive/README.md | Example documentation and run instructions. |
| docs/primitives/daemonset.md | Public documentation for the new primitive (API, ordering, editors, suspension, status). |
Claude Review Cycle 1 CompleteAddressed:
Intentionally ignored:
<!-- claude-review-cycle --> |
Claude Review Cycle 1 CompleteAddressed:
Intentionally ignored:
|
Claude Review Cycle 2 CompleteAddressed:
Intentionally ignored:
<!-- claude-review-cycle --> |
c0099c7 to
2638488
Compare
|
approved |
Claude Review Cycle 1 CompleteAddressed:
Intentionally ignored:
<!-- claude-review-cycle --> |
Claude Review Cycle 2 CompleteAddressed:
Intentionally ignored:
|
Claude Review Cycle 1 CompleteAddressed:
Intentionally ignored:
|
Claude Review Cycle 4 CompleteAddressed: Intentionally ignored:
<!-- claude-review-cycle --> |
Claude Review Cycle 5 CompleteAddressed:
Intentionally ignored: <!-- claude-review-cycle --> |
Align daemonset NewMutator with deployment/configmap: do not create a feature plan at construction time. BeginFeature must be called before registering mutations. Update all tests to call BeginFeature and add dedicated tests for constructor state and plan isolation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align with the framework's switch to Server-Side Apply (SSA): - Remove DefaultFieldApplicator, WithCustomFieldApplicator, and WithFieldApplicationFlavor from builder - Delete flavors.go and flavors_test.go - Remove DefaultFieldApplicator parameter from NewWorkloadBuilder call - Update tests to use Object() output instead of empty structs in Mutate - Remove flavor/field applicator references from docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| // 4. Configure flavors (e.g., preserve labels/annotations if they were modified externally). | ||
| builder.WithFieldApplicationFlavor(features.PreserveLabelsFlavor()) | ||
| builder.WithFieldApplicationFlavor(features.PreserveAnnotationsFlavor()) |
There was a problem hiding this comment.
daemonset.Builder does not define WithFieldApplicationFlavor, and the underlying generic builders don’t appear to expose a field-application flavor concept either. This example won’t compile until either the builder API is added or these calls are removed/rewritten.
| - **Feature Mutations**: Applying version-gated or conditional changes (sidecars, env vars, annotations) using the | ||
| `Mutator`. | ||
| - **Field Flavors**: Preserving labels and annotations that might be managed by external tools (e.g., ArgoCD, manual | ||
| edits). | ||
| - **Suspension**: Demonstrating the delete-on-suspend behavior unique to DaemonSets. | ||
| - **Data Extraction**: Harvesting information from the reconciled resource. |
There was a problem hiding this comment.
This README describes “Field Flavors” and implies a FieldApplicationFlavor API exists, but the daemonset primitive/example code currently references non-existent identifiers (e.g. WithFieldApplicationFlavor). Update the README to reflect the actual supported API, or implement the flavor feature it describes so the example and docs stay in sync.
|
|
||
| import ( | ||
| "github.com/sourcehawk/operator-component-framework/pkg/primitives/daemonset" | ||
| ) | ||
|
|
||
| // PreserveLabelsFlavor demonstrates using a flavor to keep external labels. | ||
| func PreserveLabelsFlavor() daemonset.FieldApplicationFlavor { | ||
| return daemonset.PreserveCurrentLabels | ||
| } | ||
|
|
||
| // PreserveAnnotationsFlavor demonstrates using a flavor to keep external annotations. | ||
| func PreserveAnnotationsFlavor() daemonset.FieldApplicationFlavor { | ||
| return daemonset.PreserveCurrentAnnotations | ||
| } |
There was a problem hiding this comment.
daemonset.FieldApplicationFlavor, daemonset.PreserveCurrentLabels, and daemonset.PreserveCurrentAnnotations don’t exist in pkg/primitives/daemonset (or elsewhere in pkg/). This file won’t compile as written. Either add the missing flavor API to the daemonset primitive (and wire it into reconciliation), or remove/replace this example code to match the currently supported API surface.
| import ( | |
| "github.com/sourcehawk/operator-component-framework/pkg/primitives/daemonset" | |
| ) | |
| // PreserveLabelsFlavor demonstrates using a flavor to keep external labels. | |
| func PreserveLabelsFlavor() daemonset.FieldApplicationFlavor { | |
| return daemonset.PreserveCurrentLabels | |
| } | |
| // PreserveAnnotationsFlavor demonstrates using a flavor to keep external annotations. | |
| func PreserveAnnotationsFlavor() daemonset.FieldApplicationFlavor { | |
| return daemonset.PreserveCurrentAnnotations | |
| } |
The file references removed symbols (FieldApplicationFlavor, PreserveCurrentLabels, PreserveCurrentAnnotations) causing typecheck failures in the linter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude Review Cycle 1 CompleteAddressed: Intentionally ignored: <!-- claude-review-cycle --> |
| ## Running the Example | ||
|
|
||
| You can run this example directly using `go run`: | ||
|
|
||
| ```bash | ||
| go run examples/daemonset-primitive/main.go | ||
| ``` |
There was a problem hiding this comment.
This repository’s make run-examples target is documented as running all examples, but the newly added daemonset-primitive example is not included in that target (Makefile currently runs only deployment/configmap/custom-resource examples). Please update run-examples to run this example as well so it’s exercised alongside the others.
The run-examples target was missing the daemonset-primitive example, so it would not be exercised alongside the other examples. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude Review Cycle 2 CompleteAddressed:
Intentionally ignored: <!-- claude-review-cycle --> |
| case concepts.ConvergingOperationCreated: | ||
| status = concepts.AliveConvergingStatusCreating | ||
| case concepts.ConvergingOperationUpdated: | ||
| status = concepts.AliveConvergingStatusUpdating |
There was a problem hiding this comment.
ConvergingOperationNone currently falls into the default branch and is reported as Scaling. This is inconsistent with stale-generation handling (which treats None as Updating) and can misclassify normal converge progress as scaling. Add an explicit case concepts.ConvergingOperationNone: (and/or a dedicated mapping) to return AliveConvergingStatusUpdating for None.
| status = concepts.AliveConvergingStatusUpdating | |
| status = concepts.AliveConvergingStatusUpdating | |
| case concepts.ConvergingOperationNone: | |
| status = concepts.AliveConvergingStatusUpdating |
| ) *Builder { | ||
| if extractor != nil { | ||
| b.base.WithDataExtractor(func(d *appsv1.DaemonSet) error { | ||
| return extractor(*d) |
There was a problem hiding this comment.
This passes a shallow-copied appsv1.DaemonSet value to the extractor. Because DaemonSet contains maps/slices (e.g., labels/annotations), extractor code that mutates those fields would mutate the underlying object held by the framework. To prevent accidental side-effects, pass a deep copy to the extractor (e.g., deep-copy d before dereferencing) so extractors are isolated from the reconciled in-memory object.
| return extractor(*d) | |
| if d == nil { | |
| return extractor(appsv1.DaemonSet{}) | |
| } | |
| return extractor(*d.DeepCopy()) |
| m.active.containerEdits = append(m.active.containerEdits, containerEdit{ | ||
| selector: selector, | ||
| edit: edit, | ||
| }) |
There was a problem hiding this comment.
Most mutator registration methods assume m.active is non-nil, so calling them before BeginFeature() will panic with a nil-pointer dereference. Even if the framework typically calls BeginFeature() internally, Mutator is a public type and this is easy to misuse. Consider making the API safer by lazily creating the first plan when a registration method is called and m.active == nil, or returning an error (or at least a deterministic panic with a clear message) to improve debuggability.
Pass a deep copy of the DaemonSet to data extractors instead of a shallow copy, preventing extractors from accidentally mutating shared maps/slices on the in-memory object held by the framework. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude Review Cycle 1 CompleteAddressed:
Intentionally ignored:
<!-- claude-review-cycle --> |
| func (m *Mutator) EnsureContainer(container corev1.Container) { | ||
| m.active.containerPresence = append(m.active.containerPresence, containerPresenceOp{ | ||
| name: container.Name, | ||
| container: &container, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Mutatormethods assumem.activeis non-nil, butNewMutatorinitializesactiveas nil and several public methods (e.g.EnsureContainer, RemoveContainer, EditObjectMetadata, etc.) dereference m.activeunconditionally. If a caller forgets to callBeginFeature()(or if framework wiring ever changes), this will panic. Consider making the mutator resilient by lazily creating a default feature plan whenactive == nil(non-breaking), or adding a single internal guard (e.g.,ensureActivePlan()`) used by all mutation registration methods; if you prefer to enforce the contract strictly, fail with a deterministic error mechanism (panic with a clear message is better than a nil-pointer panic) and add a unit test for that behavior.
| Namespace: "test-ns", | ||
| }, | ||
| } | ||
| res, _ := NewBuilder(ds).Build() |
There was a problem hiding this comment.
Several tests ignore the Build() error (e.g. res, _ := ...Build()), which can mask failures and cause follow-on panics or misleading assertions if validation changes. Prefer require.NoError(t, err) (and require.NotNil(t, res) where appropriate) in all test setup paths that depend on successful builds.
| op concepts.ConvergingOperation, ds *appsv1.DaemonSet, | ||
| ) (concepts.AliveStatusWithReason, error) { | ||
| if status := concepts.StaleGenerationStatus( | ||
| op, ds.Status.ObservedGeneration, ds.Generation, "daemonset", |
There was a problem hiding this comment.
The user-facing reason strings are inconsistent in capitalization ("daemonset controller" coming from StaleGenerationStatus(..., "daemonset") vs "DaemonSet controller" in DefaultGraceStatusHandler). This makes conditions harder to grep/alert on consistently. Standardize the resource kind casing used to construct these messages (e.g., pass "DaemonSet" into StaleGenerationStatus, or align the grace handler reason to the same convention used elsewhere in the framework).
| op, ds.Status.ObservedGeneration, ds.Generation, "daemonset", | |
| op, ds.Status.ObservedGeneration, ds.Generation, "DaemonSet", |
| return concepts.GraceStatusWithReason{ | ||
| Status: concepts.GraceStatusDegraded, | ||
| Reason: "Waiting for DaemonSet controller to observe latest generation", | ||
| }, nil |
There was a problem hiding this comment.
The user-facing reason strings are inconsistent in capitalization ("daemonset controller" coming from StaleGenerationStatus(..., "daemonset") vs "DaemonSet controller" in DefaultGraceStatusHandler). This makes conditions harder to grep/alert on consistently. Standardize the resource kind casing used to construct these messages (e.g., pass "DaemonSet" into StaleGenerationStatus, or align the grace handler reason to the same convention used elsewhere in the framework).
…tandardize casing - Add requireActivePlan() guard to all Mutator methods that dereference m.active, producing a clear panic message instead of nil-pointer crash if BeginFeature() was not called - Add unit test for the panic-on-missing-BeginFeature behavior - Replace all `res, _ := ...Build()` in resource_test.go with proper `require.NoError(t, err)` checks - Standardize "daemonset" to "DaemonSet" in StaleGenerationStatus call to match casing used elsewhere in handler reason strings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude Review Cycle 1 CompleteAddressed:
Intentionally ignored: <!-- claude-review-cycle --> |
Implements the
daemonsetKubernetes resource primitive following the pattern established by the existingConfigMapandDeploymentprimitives.Summary
daemonsetprimitive package underpkg/primitives/daemonset/DaemonSetSpecEditorto sharedpkg/mutation/editors/packagedocs/primitives.mdwith daemonset primitive documentationChecklist
docs/primitives.md,pkg/mutation/editors/daemonsetspec.go