Skip to content

feat: implement serviceaccount primitive#33

Open
sourcehawk wants to merge 24 commits intomainfrom
feature/serviceaccount-primitive
Open

feat: implement serviceaccount primitive#33
sourcehawk wants to merge 24 commits intomainfrom
feature/serviceaccount-primitive

Conversation

@sourcehawk
Copy link
Owner

@sourcehawk sourcehawk commented Mar 22, 2026

Implements the serviceaccount Kubernetes resource primitive following the pattern established by the existing ConfigMap and Deployment primitives.

Summary

  • Adds serviceaccount primitive package under pkg/primitives/serviceaccount/
  • Implements required lifecycle interfaces
  • Includes editors, mutator, flavors, and builder
  • Adds serviceaccount entry to the built-in primitives table in docs/primitives.md

Checklist

  • Compiles cleanly
  • Tests pass
  • Follows naming conventions in CONTEXT.md
  • Shared file modifications limited to documentation index (docs/primitives.md)

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new serviceaccount primitive to the Operator Component Framework, mirroring the existing configmap/deployment patterns for managing Kubernetes ServiceAccount resources via a static desired-state resource, builder configuration, mutation planning, and field-application flavors.

Changes:

  • Introduces pkg/primitives/serviceaccount/ with Resource, Builder, Mutator, and field-application flavors for ServiceAccount.
  • Adds unit tests for the new builder, mutator behavior (including ordering), and flavors.
  • Adds a runnable example (examples/serviceaccount-primitive/) plus dedicated primitive documentation (docs/primitives/serviceaccount.md).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/primitives/serviceaccount/resource.go Implements the static ServiceAccount primitive Resource wrapper over internal/generic.StaticResource.
pkg/primitives/serviceaccount/builder.go Provides fluent builder API for configuring mutations, flavors, and data extractors for ServiceAccount.
pkg/primitives/serviceaccount/mutator.go Adds plan-and-apply Mutator with ordered edit categories for metadata, image pull secrets, and automount token.
pkg/primitives/serviceaccount/flavors.go Adds typed flavors to preserve current labels/annotations via shared pkg/flavors helpers.
pkg/primitives/serviceaccount/builder_test.go Tests builder validation and builder configuration methods.
pkg/primitives/serviceaccount/mutator_test.go Tests mutator operations, idempotency, ordering, and feature-boundary behavior.
pkg/primitives/serviceaccount/flavors_test.go Tests label/annotation preservation and flavor ordering/error propagation through Mutate.
examples/serviceaccount-primitive/app/controller.go Example controller wiring a component with the serviceaccount resource.
examples/serviceaccount-primitive/features/mutations.go Example feature-gated mutations composing SA behavior.
examples/serviceaccount-primitive/resources/serviceaccount.go Example resource factory building the SA primitive with mutations/flavors/extraction.
examples/serviceaccount-primitive/main.go Runnable fake-client demo for multiple reconciliation cycles.
examples/serviceaccount-primitive/README.md Documentation for running/understanding the new example.
docs/primitives/serviceaccount.md New primitive documentation and usage guidance.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

@sourcehawk
Copy link
Owner Author

approved

@sourcehawk sourcehawk force-pushed the feature/serviceaccount-primitive branch from 2f416f3 to 7078efd Compare March 23, 2026 00:50
Copilot AI review requested due to automatic review settings March 23, 2026 00:50
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

@sourcehawk
Copy link
Owner Author

Claude Review Cycle 1 Complete

Addressed:
None — all three comments were evaluated and determined to not require changes.

Intentionally ignored:

  • mutator_test.go:183 (tests use unexported beginFeature() directly): This is the established codebase convention. Both configmap/mutator_test.go and deployment/mutator_test.go use beginFeature() directly in the same way. Changing only serviceaccount tests would introduce inconsistency.
  • docs/primitives/serviceaccount.md:12 (double pipes in table): The table uses standard single-pipe markdown format, identical to docs/primitives/configmap.md. No double pipes are present.
  • mutator.go:48 (unexported beginFeature prevents FeatureMutator satisfaction): Technically correct — Go interfaces with unexported methods can only be satisfied by types in the same package. However, this equally affects configmap.Mutator and deployment.Mutator, which follow the exact same pattern. This is a framework-level design concern, not a serviceaccount-specific issue. Simplifying only the serviceaccount mutator would make it inconsistent with the other primitives. The proper fix (exporting BeginFeature in the framework interface) is out of scope for this PR.

<!-- claude-review-cycle -->

Copilot AI review requested due to automatic review settings March 23, 2026 16:08
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Copilot AI review requested due to automatic review settings March 23, 2026 17:00
@sourcehawk
Copy link
Owner Author

Claude Review Cycle 1 Complete

Addressed:

  • Set Feature: nil for always-enabled mutations (VersionLabelMutation, ImagePullSecretMutation) in examples, matching documented semantics and avoiding unnecessary version parsing (comments 1 & 3)
  • Exported BeginFeature() on the FeatureMutator interface and all primitive mutators (serviceaccount, configmap, deployment) so external packages can satisfy the interface and participate in per-feature ordering semantics (comment 2). This was a broader fix than suggested — the unexported beginFeature() method meant no external primitive could satisfy the interface, so the type assertion in ApplyMutations would always fail.

Intentionally ignored:
None

<!-- claude-review-cycle -->

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.

sourcehawk and others added 9 commits March 23, 2026 20:16
Implement the ServiceAccount static primitive with builder, resource,
mutator, and flavors. The mutator exposes direct methods for
imagePullSecrets and automountServiceAccountToken since ServiceAccount
has no spec field. Apply order: metadata, image pull secrets, automount.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Demonstrates feature-gated image pull secret management, automount
token control, version labelling, label preservation flavors, and
data extraction across multiple reconciliation cycles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add clarifying comment in resources/serviceaccount.go explaining that
  EnableMetrics is reused to drive DisableAutomountMutation for demo
  purposes, and extract to a named variable for readability.
- Fix misleading printf labels in main.go: PrivateRegistry/DisableAutomount
  renamed to EnableTracing/EnableMetrics to match actual spec field names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ctor

Align with the fix applied to deployment and configmap mutators — initialize
the first feature plan inline instead of calling beginFeature(), which would
create an empty extra plan when the mutator helper also calls beginFeature().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address PR review feedback:
- Set Feature to nil for always-enabled mutations in examples instead of
  using feature.NewResourceFeature(version, nil), matching documented
  semantics and avoiding unnecessary version parsing.
- Export BeginFeature() on the FeatureMutator interface and all primitive
  mutators (serviceaccount, configmap, deployment) so that external
  packages can satisfy the interface and participate in per-feature
  ordering semantics via the generic builder's type assertion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename unused version parameter to _ in ImagePullSecretMutation to fix
revive lint error. Update docs to reflect server-managed metadata
preservation in DefaultFieldApplicator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sourcehawk sourcehawk force-pushed the feature/serviceaccount-primitive branch from 443df47 to 9328bb2 Compare March 23, 2026 21:37
@sourcehawk sourcehawk requested a review from Copilot March 23, 2026 21:51
…e to run-examples

- Remove "Field Flavors" / PreserveCurrentLabels mention from README
  (the API was removed in cd27004 but the docs were not fully updated)
- Add serviceaccount-primitive to the Makefile run-examples target

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 25, 2026 16:03
@sourcehawk
Copy link
Owner Author

Claude Review Cycle 1 Complete

Addressed:

  • README.md line 9: Removed stale "Field Flavors" / PreserveCurrentLabels bullet from the feature list, since neither the API nor the builder method exists.
  • Makefile run-examples target: Added go run ./examples/serviceaccount-primitive/. so the new example is exercised alongside the existing ones.

Intentionally ignored:

  • builder.go line 66 (Builder missing WithFieldApplicationFlavor): The example code no longer calls this method — it was already removed in commit cd27004/2ee1b8e. The comment is stale; no code change needed.
  • resources/serviceaccount.go line 44 (example won't compile): The example compiles and runs successfully — WithFieldApplicationFlavor is not called in the current code. The comment was based on an outdated view of the file.

<!-- claude-review-cycle -->

@sourcehawk sourcehawk requested review from Copilot and removed request for Copilot March 25, 2026 16:04
@sourcehawk sourcehawk requested review from Copilot and removed request for Copilot March 25, 2026 16:09
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Comment on lines +58 to +63
func (m *Mutator) EditObjectMetadata(edit func(*editors.ObjectMetaEditor) error) {
if edit == nil {
return
}
m.active.metadataEdits = append(m.active.metadataEdits, edit)
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These mutation registration methods dereference m.active without ensuring BeginFeature() has been called. If a caller invokes any of these before BeginFeature(), this will panic with a nil pointer dereference. Since this is a public mutator API, consider making it non-panicking by (mandatory) adding a guard that initializes an active plan when m.active == nil (e.g., lazily calling BeginFeature()), or alternatively returning a structured error (would require API changes) rather than crashing at runtime.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +76
func (m *Mutator) EnsureImagePullSecret(name string) {
m.active.imagePullSecretEdits = append(m.active.imagePullSecretEdits, func(sa *corev1.ServiceAccount) {
for _, ref := range sa.ImagePullSecrets {
if ref.Name == name {
return
}
}
sa.ImagePullSecrets = append(sa.ImagePullSecrets, corev1.LocalObjectReference{Name: name})
})
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These mutation registration methods dereference m.active without ensuring BeginFeature() has been called. If a caller invokes any of these before BeginFeature(), this will panic with a nil pointer dereference. Since this is a public mutator API, consider making it non-panicking by (mandatory) adding a guard that initializes an active plan when m.active == nil (e.g., lazily calling BeginFeature()), or alternatively returning a structured error (would require API changes) rather than crashing at runtime.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +90
func (m *Mutator) RemoveImagePullSecret(name string) {
m.active.imagePullSecretEdits = append(m.active.imagePullSecretEdits, func(sa *corev1.ServiceAccount) {
filtered := sa.ImagePullSecrets[:0]
for _, ref := range sa.ImagePullSecrets {
if ref.Name != name {
filtered = append(filtered, ref)
}
}
sa.ImagePullSecrets = filtered
})
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These mutation registration methods dereference m.active without ensuring BeginFeature() has been called. If a caller invokes any of these before BeginFeature(), this will panic with a nil pointer dereference. Since this is a public mutator API, consider making it non-panicking by (mandatory) adding a guard that initializes an active plan when m.active == nil (e.g., lazily calling BeginFeature()), or alternatively returning a structured error (would require API changes) rather than crashing at runtime.

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +107
func (m *Mutator) SetAutomountServiceAccountToken(v *bool) {
var snapshot *bool
if v != nil {
val := *v
snapshot = &val
}

m.active.automountEdits = append(m.active.automountEdits, func(sa *corev1.ServiceAccount) {
sa.AutomountServiceAccountToken = snapshot
})
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These mutation registration methods dereference m.active without ensuring BeginFeature() has been called. If a caller invokes any of these before BeginFeature(), this will panic with a nil pointer dereference. Since this is a public mutator API, consider making it non-panicking by (mandatory) adding a guard that initializes an active plan when m.active == nil (e.g., lazily calling BeginFeature()), or alternatively returning a structured error (would require API changes) rather than crashing at runtime.

Copilot uses AI. Check for mistakes.
@sourcehawk
Copy link
Owner Author

Claude Review Cycle 1 Complete

Addressed:
None

Intentionally ignored:

  • mutator.go lines 63, 76, 90, 107 — nil guard for m.active before BeginFeature() is called: This is a valid defensive programming concern in isolation, but the same pattern (no nil guard on m.active) is used consistently across all other primitive mutators in the codebase (configmap, deployment). The precondition is already documented on NewMutator: "BeginFeature must be called before registering any mutations." Adding a lazy-init guard only to the serviceaccount mutator would be inconsistent with the established codebase convention. If this is to be addressed, it should be done uniformly across all primitives in a separate change.

<!-- claude-review-cycle -->

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Prevent nil pointer panic when mutator methods are called without
a prior BeginFeature() call. Each public mutation method now calls
ensureActive() which panics with a clear message instead of a
cryptic nil dereference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sourcehawk
Copy link
Owner Author

Claude Review Cycle 1 Complete

Addressed:

  • mutator.go: Added ensureActive() guard to all public mutator methods (EditObjectMetadata, EnsureImagePullSecret, RemoveImagePullSecret, SetAutomountServiceAccountToken). If BeginFeature() has not been called, these methods now panic with a clear error message instead of a nil pointer dereference.

Intentionally ignored:

  • builder_test.go loop variable capture (tt := tt): This project uses Go 1.25.6, which scopes loop variables per iteration (changed in Go 1.22). The tt := tt rebind is unnecessary and no other test in the codebase uses this pattern.

<!-- claude-review-cycle -->

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

…uard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sourcehawk
Copy link
Owner Author

Claude Review Cycle 1 Complete

Addressed:

  • mutator.go:58 — Replaced panic in ensureActive() with a lazy BeginFeature() call so that misconfiguration results in controlled behavior (auto-initializing a default plan) rather than crashing the controller process.

Intentionally ignored:

  • resource_test.go:74 — The suggestion to construct a divergent current ServiceAccount and pass it to Mutate() is based on a misunderstanding of the architecture. Mutate() applies only feature mutations to the current object; it does not overwrite base state fields (labels, ImagePullSecrets, etc.). This is consistent with how all other primitives (configmap, deployment) work and are tested. The existing test correctly uses res.Object() (which returns the desired state) as the starting point for mutation, matching the framework's reconciliation pattern.

<!-- claude-review-cycle -->

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants