Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cd854e2
Add JobSpecEditor and CronJobSpecEditor mutation editors
sourcehawk Mar 22, 2026
cd2544b
Add CronJob primitive package with Integration lifecycle
sourcehawk Mar 22, 2026
c6c4288
Add CronJob primitive documentation
sourcehawk Mar 22, 2026
cbae2f0
Add CronJob primitive example
sourcehawk Mar 22, 2026
009f807
update golangci-lint to be valid v2
sourcehawk Mar 22, 2026
e244f20
address linter warnings
sourcehawk Mar 22, 2026
465fe84
Address Copilot review comments on CronJob primitive
sourcehawk Mar 22, 2026
1c75a6c
Address Copilot review: fix doc status name and add resource tests
sourcehawk Mar 22, 2026
7c8e5ac
preserve server-managed metadata in default field applicator
sourcehawk Mar 22, 2026
ad7dbb0
Add timeout setting for golangci-lint
sourcehawk Mar 23, 2026
5058194
Update pkg/primitives/cronjob/mutator.go
sourcehawk Mar 23, 2026
c65f548
Merge remote-tracking branch 'origin/main' into feature/cronjob-primi…
sourcehawk Mar 23, 2026
ac1294f
Export BeginFeature to fix cross-package FeatureMutator interface
sourcehawk Mar 23, 2026
8f1e3a4
fix linter
sourcehawk Mar 23, 2026
2e8cb12
revert beginFeature exposed change
sourcehawk Mar 23, 2026
e083c10
Merge branch 'main' into feature/cronjob-primitive
sourcehawk Mar 23, 2026
31e5ff9
Merge remote-tracking branch 'origin/main' into feature/cronjob-primi…
sourcehawk Mar 23, 2026
e445b38
fix: remove beginFeature call from cronjob mutator constructor
sourcehawk Mar 23, 2026
b8419fd
fix: preserve CronJob status in DefaultFieldApplicator and add tests
sourcehawk Mar 23, 2026
ac412f2
Fix/architectural issues (#43)
sourcehawk Mar 23, 2026
668d484
Export BeginFeature to fix cross-package FeatureMutator interface
sourcehawk Mar 23, 2026
3f2221d
fix: preserve CronJob status in DefaultFieldApplicator and add tests
sourcehawk Mar 23, 2026
4fcf126
improve ai instructions (#44)
sourcehawk Mar 23, 2026
a870a04
add markdown formatter, add helper for alive observedgeneration handl…
sourcehawk Mar 24, 2026
7e82369
style: format markdown files with prettier
sourcehawk Mar 24, 2026
0e16941
Merge remote-tracking branch 'origin/main' into feature/cronjob-primi…
sourcehawk Mar 24, 2026
52aa7a2
fix: address PR review comments for cronjob primitive
sourcehawk Mar 24, 2026
498d264
docs: clarify spec.suspend note in cronjob documentation
sourcehawk Mar 24, 2026
7b7bbd2
style: format cronjob docs with prettier
sourcehawk Mar 24, 2026
d6afd3f
fix: check Build() errors in flavors tests and document BeginFeature …
sourcehawk Mar 24, 2026
5dec6ad
Merge remote-tracking branch 'origin/main' into feature/cronjob-primi…
sourcehawk Mar 24, 2026
7ecd3a9
fix: do not initialize empty plan on cronjob mutator construction
sourcehawk Mar 24, 2026
9fffcc4
Merge remote-tracking branch 'origin/main' into feature/cronjob-primi…
sourcehawk Mar 25, 2026
11378e5
refactor: remove field applicators and flavors from cronjob primitive
sourcehawk Mar 25, 2026
16498f4
fix: update test and example to match refactored ApplyMutations signa…
sourcehawk Mar 25, 2026
71434fc
fix linter issues
sourcehawk Mar 25, 2026
4b384ed
docs: remove references to non-existent field applicator and flavor APIs
sourcehawk Mar 25, 2026
f962224
fix: panic with clear message when BeginFeature not called before mut…
sourcehawk Mar 25, 2026
c0962c5
Merge remote-tracking branch 'origin/main' into feature/cronjob-primi…
sourcehawk Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 50 additions & 44 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,21 @@ run:
timeout: 5m

linters:
disable-all: true
default: none
enable:
Comment on lines +7 to 8
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.

This golangci-lint config restructuring is very likely invalid for golangci-lint YAML schema: keys like linters.default, linters.settings, and linters.exclusions (and formatters.exclusions) are not recognized in commonly supported versions, which can cause golangci-lint to error and skip linting entirely. Recommend reverting to the known-good schema (linters: disable-all/enable, linters-settings:, issues: exclude-rules: / issues: exclusions: depending on version) and validating with golangci-lint config verify (or running golangci-lint in CI) before merging.

Copilot uses AI. Check for mistakes.
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- asciicheck
- bidichk
- bodyclose
- dupl
- errcheck
- errname
- errorlint
Comment on lines 6 to 15
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

PR description checklist says this PR "Does not modify shared files", but it changes shared repo configuration (e.g., .golangci.yml). Either update the PR description/checklist to match reality or revert the shared-file changes into a separate PR.

Copilot uses AI. Check for mistakes.
- goconst
- gocritic
- gocyclo
- godot
- revive
- govet
- ineffassign
- misspell
- nakedret
- nilerr
Expand All @@ -30,47 +27,56 @@ linters:
- prealloc
- predeclared
- reassign
- revive
- staticcheck
- unconvert
- unparam
- unused
- whitespace

settings:
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.

This golangci-lint config restructuring is very likely invalid for golangci-lint YAML schema: keys like linters.default, linters.settings, and linters.exclusions (and formatters.exclusions) are not recognized in commonly supported versions, which can cause golangci-lint to error and skip linting entirely. Recommend reverting to the known-good schema (linters: disable-all/enable, linters-settings:, issues: exclude-rules: / issues: exclusions: depending on version) and validating with golangci-lint config verify (or running golangci-lint in CI) before merging.

Copilot uses AI. Check for mistakes.
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
exclusions:
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.

This golangci-lint config restructuring is very likely invalid for golangci-lint YAML schema: keys like linters.default, linters.settings, and linters.exclusions (and formatters.exclusions) are not recognized in commonly supported versions, which can cause golangci-lint to error and skip linting entirely. Recommend reverting to the known-good schema (linters: disable-all/enable, linters-settings:, issues: exclude-rules: / issues: exclusions: depending on version) and validating with golangci-lint config verify (or running golangci-lint in CI) before merging.

Copilot uses AI. Check for mistakes.
generated: lax
rules:
- linters:
- dupl
- goconst
- revive
path: _test\.go
- linters:
- revive
text: dot-imports
paths:
- third_party$
- builtin$
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
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.

This golangci-lint config restructuring is very likely invalid for golangci-lint YAML schema: keys like linters.default, linters.settings, and linters.exclusions (and formatters.exclusions) are not recognized in commonly supported versions, which can cause golangci-lint to error and skip linting entirely. Recommend reverting to the known-good schema (linters: disable-all/enable, linters-settings:, issues: exclude-rules: / issues: exclusions: depending on version) and validating with golangci-lint config verify (or running golangci-lint in CI) before merging.

Copilot uses AI. Check for mistakes.
enable:
- gofmt
- goimports

issues:
exclude-rules:
- path: _test\.go
linters:
- dupl
- goconst
- revive
- text: "dot-imports"
linters:
- revive
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0

linters-settings:
revive:
rules:
- name: blank-imports
- name: context-as-first-argument
- name: context-keys-type
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
exclusions:
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.

This golangci-lint config restructuring is very likely invalid for golangci-lint YAML schema: keys like linters.default, linters.settings, and linters.exclusions (and formatters.exclusions) are not recognized in commonly supported versions, which can cause golangci-lint to error and skip linting entirely. Recommend reverting to the known-good schema (linters: disable-all/enable, linters-settings:, issues: exclude-rules: / issues: exclusions: depending on version) and validating with golangci-lint config verify (or running golangci-lint in CI) before merging.

Copilot uses AI. Check for mistakes.
generated: lax
paths:
- third_party$
- builtin$
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
golang 1.26.1
golang 1.25.6
golangci-lint 2.11.2
nodejs 25.1.0
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: all
all: fmt fmt-md lint test build-examples
all: fmt lint test build-examples

##@ General

Expand Down
1 change: 1 addition & 0 deletions docs/primitives.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ have been applied. This means a single mutation can safely add a container and t
| Primitive | Category | Documentation |
| ----------------------------------- | ----------- | --------------------------------------------------------- |
| `pkg/primitives/deployment` | Workload | [deployment.md](primitives/deployment.md) |
| `pkg/primitives/cronjob` | Integration | [cronjob.md](primitives/cronjob.md) |
| `pkg/primitives/configmap` | Static | [configmap.md](primitives/configmap.md) |
| `pkg/primitives/clusterrole` | Static | [clusterrole.md](primitives/clusterrole.md) |
| `pkg/primitives/clusterrolebinding` | Static | [clusterrolebinding.md](primitives/clusterrolebinding.md) |
Expand Down
245 changes: 245 additions & 0 deletions docs/primitives/cronjob.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# CronJob Primitive

The `cronjob` primitive is the framework's built-in integration abstraction for managing Kubernetes `CronJob` resources.
It integrates with the component lifecycle through the Operational and Suspendable concepts, and provides a rich
mutation API for managing the CronJob schedule, job template, pod spec, and containers.

## Capabilities

| Capability | Detail |
| ------------------------ | ------------------------------------------------------------------------------------------- |
| **Operational tracking** | Reports `OperationPending` (never scheduled) or `Operational` (has scheduled at least once) |
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.

The docs reference OperationPending, but the framework concepts in code use OperationalStatusPending (and the tests assert concepts.OperationalStatusPending). Please align the documentation to the actual status names returned by the primitive to avoid confusing API consumers.

Suggested change
| **Operational tracking** | Reports `OperationPending` (never scheduled) or `Operational` (has scheduled at least once) |
| **Operational tracking** | Reports `OperationalStatusPending` (never scheduled) or `Operational` (has scheduled at least once) |

Copilot uses AI. Check for mistakes.
| **Suspension** | Sets `spec.suspend = true`; reports `Suspending` (active jobs running) / `Suspended` |
| **Mutation pipeline** | Typed editors for metadata, CronJob spec, Job spec, pod spec, and containers |

## Server-Side Apply

The CronJob primitive reconciles resources using **Server-Side Apply** (SSA). Only fields declared by the operator are
sent; server-managed defaults, fields set by other controllers, and values written by webhooks are left untouched. Field
ownership is tracked automatically by the Kubernetes API server.

## Building a CronJob Primitive

```go
import "github.com/sourcehawk/operator-component-framework/pkg/primitives/cronjob"

base := &batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "data-cleanup",
Namespace: owner.Namespace,
},
Spec: batchv1.CronJobSpec{
Schedule: "0 2 * * *",
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "cleanup", Image: "cleanup:latest"},
},
RestartPolicy: corev1.RestartPolicyOnFailure,
},
},
},
},
},
}

resource, err := cronjob.NewBuilder(base).
WithMutation(MyScheduleMutation(owner.Spec.Version)).
Build()
```

## Mutations

Mutations are the primary mechanism for modifying a `CronJob` beyond its baseline. Each mutation is a named function
that receives a `*Mutator` and records edit intent through typed editors.

```go
func MyScheduleMutation(version string) cronjob.Mutation {
return cronjob.Mutation{
Name: "my-schedule",
Feature: feature.NewResourceFeature(version, nil),
Mutate: func(m *cronjob.Mutator) error {
m.EditCronJobSpec(func(e *editors.CronJobSpecEditor) error {
e.SetSchedule("0 */6 * * *")
e.SetConcurrencyPolicy(batchv1.ForbidConcurrent)
return nil
})
return nil
},
}
}
```

### Boolean-gated mutations

Use `When(bool)` to gate a mutation on a runtime condition:

```go
func TimeZoneMutation(version string, enabled bool) cronjob.Mutation {
return cronjob.Mutation{
Name: "timezone",
Feature: feature.NewResourceFeature(version, nil).When(enabled),
Mutate: func(m *cronjob.Mutator) error {
m.EditCronJobSpec(func(e *editors.CronJobSpecEditor) error {
e.SetTimeZone("America/New_York")
return nil
})
return nil
},
}
}
```

## Internal Mutation Ordering

Within a single mutation, edit operations are grouped into categories and applied in a fixed sequence regardless of the
order they are recorded.

| Step | Category | What it affects |
| ---- | --------------------------- | --------------------------------------------------------------------------------------- |
| 1 | CronJob metadata edits | Labels and annotations on the `CronJob` object |
| 2 | CronJobSpec edits | Schedule, concurrency policy, time zone, history limits |
| 3 | JobSpec edits | Completions, parallelism, backoff limit, TTL |
| 4 | Pod template metadata edits | Labels and annotations on the pod template |
| 5 | Pod spec edits | Volumes, tolerations, node selectors, service account, security context |
| 6 | Regular container presence | Adding or removing containers from `spec.jobTemplate.spec.template.spec.containers` |
| 7 | Regular container edits | Env vars, args, resources (snapshot taken after step 6) |
| 8 | Init container presence | Adding or removing containers from `spec.jobTemplate.spec.template.spec.initContainers` |
| 9 | Init container edits | Env vars, args, resources (snapshot taken after step 8) |

Container edits (steps 7 and 9) are evaluated against a snapshot taken _after_ presence operations in the same mutation.
This means a single mutation can add a container and then configure it without selector resolution issues.

## Editors

### CronJobSpecEditor

Controls CronJob-level settings via `m.EditCronJobSpec`.

Available methods: `SetSchedule`, `SetConcurrencyPolicy`, `SetStartingDeadlineSeconds`, `SetSuccessfulJobsHistoryLimit`,
`SetFailedJobsHistoryLimit`, `SetTimeZone`, `Raw`.

```go
m.EditCronJobSpec(func(e *editors.CronJobSpecEditor) error {
e.SetSchedule("0 2 * * *")
e.SetConcurrencyPolicy(batchv1.ForbidConcurrent)
e.SetFailedJobsHistoryLimit(1)
return nil
})
```

Note: no typed helper is provided for `spec.suspend`; it can be set via `Raw()` if needed, but suspension should
typically be handled via the framework's suspend mechanism.

### JobSpecEditor

Controls the embedded job template spec via `m.EditJobSpec`.

Available methods: `SetCompletions`, `SetParallelism`, `SetBackoffLimit`, `SetActiveDeadlineSeconds`,
`SetTTLSecondsAfterFinished`, `SetCompletionMode`, `Raw`.

```go
m.EditJobSpec(func(e *editors.JobSpecEditor) error {
e.SetBackoffLimit(3)
e.SetTTLSecondsAfterFinished(3600)
return nil
})
```

### PodSpecEditor

Manages pod-level configuration via `m.EditPodSpec`.

Available methods: `SetServiceAccountName`, `EnsureVolume`, `RemoveVolume`, `EnsureToleration`, `RemoveTolerations`,
`EnsureNodeSelector`, `RemoveNodeSelector`, `EnsureImagePullSecret`, `RemoveImagePullSecret`, `SetPriorityClassName`,
`SetHostNetwork`, `SetHostPID`, `SetHostIPC`, `SetSecurityContext`, `Raw`.

```go
m.EditPodSpec(func(e *editors.PodSpecEditor) error {
e.SetServiceAccountName("cleanup-sa")
e.Raw().RestartPolicy = corev1.RestartPolicyOnFailure
return nil
})
```

### ContainerEditor

Modifies individual containers via `m.EditContainers` or `m.EditInitContainers`. Always used in combination with a
[selector](../primitives.md#container-selectors).

Available methods: `EnsureEnvVar`, `EnsureEnvVars`, `RemoveEnvVar`, `RemoveEnvVars`, `EnsureArg`, `EnsureArgs`,
`RemoveArg`, `RemoveArgs`, `SetResourceLimit`, `SetResourceRequest`, `SetResources`, `Raw`.

```go
m.EditContainers(selectors.ContainerNamed("cleanup"), func(e *editors.ContainerEditor) error {
e.EnsureEnvVar(corev1.EnvVar{Name: "DRY_RUN", Value: "false"})
e.SetResourceLimit(corev1.ResourceMemory, resource.MustParse("256Mi"))
return nil
})
```

### ObjectMetaEditor

Modifies labels and annotations. Use `m.EditObjectMetadata` to target the `CronJob` object itself, or
`m.EditPodTemplateMetadata` to target the pod template.

Available methods: `EnsureLabel`, `RemoveLabel`, `EnsureAnnotation`, `RemoveAnnotation`, `Raw`.

```go
m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error {
e.EnsureLabel("app.kubernetes.io/version", version)
return nil
})
```

## Convenience Methods

The `Mutator` also exposes convenience wrappers that target all containers at once:

| Method | Equivalent to |
| ----------------------------- | ------------------------------------------------------------- |
| `EnsureContainerEnvVar(ev)` | `EditContainers(AllContainers(), ...)` → `EnsureEnvVar(ev)` |
| `RemoveContainerEnvVar(name)` | `EditContainers(AllContainers(), ...)` → `RemoveEnvVar(name)` |
| `EnsureContainerArg(arg)` | `EditContainers(AllContainers(), ...)` → `EnsureArg(arg)` |
| `RemoveContainerArg(arg)` | `EditContainers(AllContainers(), ...)` → `RemoveArg(arg)` |

## Operational Status

The CronJob primitive reports operational status based on the CronJob's scheduling history:

| Status | Condition |
| ------------------ | -------------------------------- |
| `OperationPending` | `Status.LastScheduleTime == nil` |
| `Operational` | `Status.LastScheduleTime != nil` |
Comment on lines +212 to +215
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.

The docs reference OperationPending, but the framework concepts in code use OperationalStatusPending (and the tests assert concepts.OperationalStatusPending). Please align the documentation to the actual status names returned by the primitive to avoid confusing API consumers.

Copilot uses AI. Check for mistakes.

Failures are reported on the spawned Job resources, not on the CronJob itself.

## Suspension

When the component is suspended, the CronJob primitive sets `spec.suspend = true`. This prevents the CronJob controller
from creating new Job objects. Existing active jobs continue to run.

| Status | Condition |
| ------------ | ---------------------------------------------------- |
| `Suspended` | `spec.suspend == true` and no active jobs |
| `Suspending` | `spec.suspend == true` but active jobs still running |
| `Suspending` | Waiting for suspend flag to be applied |

On unsuspend, the desired state (without `spec.suspend = true`) is applied via SSA, allowing the CronJob to resume
scheduling.

The CronJob is never deleted on suspend (`DeleteOnSuspend = false`).

## Guidance

**`Feature: nil` applies unconditionally.** Omit `Feature` (leave it nil) for mutations that should always run.

**Register mutations in dependency order.** If mutation B relies on a container added by mutation A, register A first.

**Prefer `EnsureContainer` over direct slice manipulation.** The mutator tracks presence operations so that selectors in
the same mutation resolve correctly.

**Use selectors for precision.** Targeting `AllContainers()` when you only mean to modify the primary container can
cause unexpected behavior if sidecar containers are present.
Loading
Loading