Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
93219e9
docs: define the project vocabulary, and plan the cleanup that makes …
sunib Sep 25, 2026
4361ed2
docs: settle enum casing against core/v1 rather than Flux, and say wh…
sunib Sep 25, 2026
ce05cb2
docs: show the rename set per kind, and stop restating the conditions…
sunib Sep 25, 2026
d006c0f
docs: drop the dependency-projection columns, and settle on commit fo…
sunib Sep 25, 2026
51bdfc2
docs: close the open calls, and break in place rather than keeping an…
sunib Sep 25, 2026
eaef95d
fix(api): stop naming a kind that no longer exists in two rule reasons
sunib Sep 25, 2026
4726237
refactor(api): stop restating condition types in reasons
sunib Sep 25, 2026
500d5b7
refactor(api): delete the pre-rename condition aliases
sunib Sep 25, 2026
fe2e577
feat(api)!: drop the dependency-projection printer columns
sunib Sep 25, 2026
4cf6a1f
feat(api)!: name every printer column for what it reads
sunib Sep 25, 2026
89402b5
feat(api)!: rename the status fields the naming rules rule out
sunib Sep 25, 2026
5f30611
feat(api)!: group the per-cluster client throttles, and fix one flag'…
sunib Sep 25, 2026
952e382
docs: record the vocabulary cleanup in the upgrade guide
sunib Sep 25, 2026
34d5c52
docs: say where a rename changes behavior, and correct two counts
sunib Sep 25, 2026
b1d47d0
fix(api)!: name the one cause ResourcesResolved=False can report
sunib Sep 25, 2026
8757bf4
refactor(git): call a finalized commit hash Commit
sunib Sep 25, 2026
8a02133
docs: correct the ResourcesResolved cause, the stream unit, and finis…
sunib Sep 25, 2026
0046105
feat(api)!: refuse the old ClusterProvider throttle spellings, and dr…
sunib Sep 28, 2026
f296662
docs(agents): push once lint and unit pass, and hold only a high-risk…
sunib Sep 28, 2026
65233aa
test(watchrule): retry the direct reconcile until the status reflects…
sunib Sep 28, 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
14 changes: 13 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ task test # Must pass all unit tests + the coverage ratchet (see TESTING RE
task test-e2e # Must pass end-to-end tests
```

### When to push

Push once `task lint` and `task test` pass. Do not hold a push for `task test-e2e`: CI runs the
e2e legs on the pushed branch, so waiting locally first only adds the wait. Keep the local e2e run
going (or read the CI legs) and fix forward if it fails. The PR is not ready until e2e is green
somewhere.

Wait for a local e2e pass **before** pushing only for a high-risk, large change: the Git write
path (`internal/git/`), watch/stream plumbing, the release or CI workflows, or a change big
enough that a red branch would cost reviewers real time.

`task lint` also runs `actionlint` on every workflow under `.github/workflows/` (via the
`lint-actions` task) and `hadolint` on the Dockerfiles (via `lint-dockerfiles`), so a
workflow or Dockerfile change is covered by the normal lint gate; you can also run
Expand Down Expand Up @@ -166,7 +177,8 @@ describes behavior you also changed in code/config during the same task.
4. `task vet` - Run go vet
5. `task lint` - Run golangci-lint (**MANDATORY**)
6. `task test` - Run unit tests (**MANDATORY**)
7. `task test-e2e` - Run e2e tests (**MANDATORY**)
7. `task test-e2e` - Run e2e tests (**MANDATORY**, but see [When to push](#when-to-push): it
gates the PR being ready, not the push)

## FAILURE HANDLING

Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Key points it enforces:
- **Mandatory validation before a change is complete:** `task lint`, `task test`,
and `task test-e2e` must all pass. Run the e2e commands **sequentially, not in
parallel**.
- **Push once lint + unit pass;** don't hold the push for local e2e. Only a high-risk, large
change waits for a local e2e pass first — see "When to push" in AGENTS.md.
- **e2e tests need Docker** — verify with `docker info` before running
`task test-e2e`; ask the user to start the Docker daemon if it is not running.
- **Docs-only exception:** a pure markdown/docs change that touches no Go code,
Expand Down
42 changes: 31 additions & 11 deletions api/v1alpha3/clusterprovider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const DefaultClusterProviderName = "default"
// secretRef.name comes from the external meta.KubeConfigReference schema, which marks it required
// but permits the empty string; an empty name can never resolve a Secret, so reject it here.
// +kubebuilder:validation:XValidation:rule="!has(self.kubeConfig) || !has(self.kubeConfig.secretRef) || size(self.kubeConfig.secretRef.name) > 0",message="spec.kubeConfig.secretRef.name must not be empty"
//
// spec.qps and spec.burst moved to spec.client. A pruned field would apply cleanly and drop the
// throttle without a word, so the old spellings stay in the schema only to be refused by name.
// +kubebuilder:validation:XValidation:rule="!has(self.qps) && !has(self.burst)",message="spec.qps and spec.burst moved to spec.client.qps and spec.client.burst"
type ClusterProviderSpec struct {
// KubeConfig names the SOURCE CLUSTER this provider represents and the credentials to reach it
// (Flux's meta.KubeConfigReference, embedded verbatim). OMITTED means the operator's own
Expand Down Expand Up @@ -77,18 +81,19 @@ type ClusterProviderSpec struct {
// +kubebuilder:default=false
AllowAnySourceNamespace bool `json:"allowAnySourceNamespace,omitempty"`

// QPS overrides the operator's outgoing kube-client query-per-second throttle for this
// cluster's watches and discovery. Omitted, the operator-wide --source-cluster-qps applies.
// Ignored when kubeConfig is omitted (the in-cluster client is not per-provider).
// Client overrides the operator's outgoing kube-client throttles for this cluster. Omitted,
// the built-in defaults apply (20 QPS, burst 30). Ignored when kubeConfig is omitted (the
// in-cluster client is not per-provider).
// +optional
// +kubebuilder:validation:Minimum=1
QPS *int32 `json:"qps,omitempty"`
Client *ClusterProviderClient `json:"client,omitempty"`

// Burst overrides the operator's outgoing kube-client burst for this cluster. Omitted, the
// operator-wide --source-cluster-burst applies. Ignored when kubeConfig is omitted.
// RemovedQPS is the old spelling of client.qps. It is never read; setting it is refused.
// +optional
// +kubebuilder:validation:Minimum=1
Burst *int32 `json:"burst,omitempty"`
RemovedQPS *int32 `json:"qps,omitempty"`

// RemovedBurst is the old spelling of client.burst. It is never read; setting it is refused.
// +optional
RemovedBurst *int32 `json:"burst,omitempty"`

// Attribution groups this cluster's author-attribution settings. The block is spelled
// "attribution" rather than "authorAttribution" even though the operator flags are
Expand All @@ -98,6 +103,21 @@ type ClusterProviderSpec struct {
Attribution *ClusterProviderAttribution `json:"attribution,omitempty"`
}

// ClusterProviderClient holds the per-cluster kube-client throttles.
type ClusterProviderClient struct {
// QPS overrides the operator's outgoing kube-client query-per-second throttle for this
// cluster's watches and discovery. Omitted, the built-in default of 20 applies.
// +optional
// +kubebuilder:validation:Minimum=1
QPS *int32 `json:"qps,omitempty"`

// Burst overrides the operator's outgoing kube-client burst for this cluster. Omitted, the
// built-in default of 30 applies.
// +optional
// +kubebuilder:validation:Minimum=1
Burst *int32 `json:"burst,omitempty"`
}

// ClusterProviderAttribution holds the per-cluster author-attribution settings. It exists as a
// block so later per-cluster knobs (grace, mode) have a home beside auditRoute.
type ClusterProviderAttribution struct {
Expand Down Expand Up @@ -168,9 +188,9 @@ type ClusterProviderStatus struct {
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Reason",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
// +kubebuilder:printcolumn:name="Facts",type=string,JSONPath=`.status.conditions[?(@.type=="AuditFactsReceived")].status`
// +kubebuilder:printcolumn:name="FactsReceived",type=string,JSONPath=`.status.conditions[?(@.type=="AuditFactsReceived")].status`
// +kubebuilder:printcolumn:name="Validated",type=string,JSONPath=`.status.conditions[?(@.type=="Validated")].status`,priority=1
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1
// +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

// ClusterProvider is the cluster-scoped, read-side peer of GitProvider: it names a SOURCE cluster a
Expand Down
7 changes: 4 additions & 3 deletions api/v1alpha3/clusterwatchrule_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ type ClusterWatchRuleStatus struct {

// Streams is the bounded stream-readiness roll-up for the types this rule resolves.
// +optional
Streams *WatchRuleStreamsStatus `json:"streams,omitempty"`
Streams *StreamsStatus `json:"streams,omitempty"`
}

// Cluster-scoped objects have no namespace, so no namespace policy bounds them: this is
Expand All @@ -115,12 +115,13 @@ type ClusterWatchRuleStatus struct {
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:printcolumn:name="Target",type=string,JSONPath=`.spec.gitTargetRef.name`
// +kubebuilder:printcolumn:name="GitTarget",type=string,JSONPath=`.spec.gitTargetRef.name`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Reason",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
// +kubebuilder:printcolumn:name="Streams",type=string,JSONPath=`.status.streams.summary`
// +kubebuilder:printcolumn:name="GitTargetReady",type=string,JSONPath=`.status.conditions[?(@.type=="GitTargetReady")].status`,priority=1
// +kubebuilder:printcolumn:name="StreamsRunning",type=string,JSONPath=`.status.conditions[?(@.type=="StreamsRunning")].status`,priority=1
// +kubebuilder:printcolumn:name="ResourcesResolved",type=string,JSONPath=`.status.conditions[?(@.type=="ResourcesResolved")].status`,priority=1
// +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

// ClusterWatchRule selects CLUSTER-SCOPED resources on the source cluster its GitTarget mirrors
Expand Down
9 changes: 5 additions & 4 deletions api/v1alpha3/commitrequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,25 +101,26 @@ type CommitRequestStatus struct {
// +optional
Branch string `json:"branch,omitempty"`

// SHA is the resulting commit SHA. Set when the commit was pushed (Pushed=True).
// Commit is the resulting commit hash. Set when the commit was pushed (Pushed=True).
// +optional
SHA string `json:"sha,omitempty"`
Commit string `json:"commit,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="GitTarget",type=string,JSONPath=`.spec.gitTargetRef.name`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Reason",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
// +kubebuilder:printcolumn:name="SHA",type=string,JSONPath=`.status.sha`
// +kubebuilder:printcolumn:name="Commit",type=string,JSONPath=`.status.commit`
// +kubebuilder:printcolumn:name="AuthorAttributed",type=string,JSONPath=`.status.conditions[?(@.type=="AuthorAttributed")].status`,priority=1
// +kubebuilder:printcolumn:name="Pushed",type=string,JSONPath=`.status.conditions[?(@.type=="Pushed")].status`,priority=1
// +kubebuilder:printcolumn:name="Branch",type=string,JSONPath=`.status.branch`,priority=1
// +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

// CommitRequest is a one-shot "save" signal: creating one finalizes the open
// commit window for the referenced GitTarget instead of waiting for the
// silence timer. The resulting commit SHA is reported back in status.
// silence timer. The resulting commit hash is reported back in status.
type CommitRequest struct {
metav1.TypeMeta `json:",inline"`

Expand Down
6 changes: 3 additions & 3 deletions api/v1alpha3/gitprovider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,12 @@ type GitProviderBranchStatus struct {
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`

// GitTargets is how many GitTargets are configured to write this branch. More than one means
// GitTargetCount is how many GitTargets are configured to write this branch. More than one means
// they share a branch worker, and each still reports its own folder's health on its own
// conditions.
// +required
// +kubebuilder:validation:Minimum=0
GitTargets int32 `json:"gitTargets"`
GitTargetCount int32 `json:"gitTargetCount"`
}

// CommitSpec configures the commit identity and signing a GitProvider uses. Message formatting
Expand Down Expand Up @@ -288,8 +288,8 @@ type CommitSigningSpec struct {
// +kubebuilder:printcolumn:name="URL",type=string,JSONPath=`.spec.url`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Reason",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1
// +kubebuilder:printcolumn:name="Verified",type=date,JSONPath=`.status.lastVerifiedAt`,priority=1
// +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

// GitProvider is the Schema for the gitproviders API.
Expand Down
57 changes: 16 additions & 41 deletions api/v1alpha3/gittarget_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ type GitTargetStatus struct {
// Streams is the bounded data-plane roll-up over this GitTarget's tracked types.
// Counts, never a per-type list, so it stays bounded however many types are watched.
// +optional
Streams *GitTargetStreamsStatus `json:"streams,omitempty"`
Streams *StreamsStatus `json:"streams,omitempty"`

// An observation, not a condition: a sweep suppressed by spec.prune.mode is the configured
// outcome, and a condition going False for it would train operators to ignore the real ones.
Expand Down Expand Up @@ -410,25 +410,25 @@ type GitTargetStatus struct {

// GitTargetRemoteStatus is the answer to "where is my branch, and when did we last prove it".
//
// It is written whenever the revision changes — including one we pushed ourselves — and otherwise
// only when the published timestamp is older than one refresh interval: the revision has to move
// It is written whenever the commit changes — including one we pushed ourselves — and otherwise
// only when the published timestamp is older than one refresh interval: the commit has to move
// with the fact it dates.
type GitTargetRemoteStatus struct {
// Revision is the commit the branch is at on the remote. EMPTY means the branch is not on
// Commit is the commit hash the branch is at on the remote. EMPTY means the branch is not on
// the remote at all, which is not an error: a branch does not exist without a commit, and a
// target that has never written has nothing there yet.
// +optional
Revision string `json:"revision,omitempty"`
Commit string `json:"commit,omitempty"`

// LastVerifiedAt is when the remote was last observed. It answers "has anything looked",
// which placement.resolvedAtRevision deliberately does not: that one dates the resolution, so
// which placement.resolvedAtCommit deliberately does not: that one dates the resolution, so
// an old value there means the layout has not changed rather than that nothing has looked.
// +optional
LastVerifiedAt *metav1.Time `json:"lastVerifiedAt,omitempty"`

// VerifiedBy is what proved it: `Push` means the server accepted a ref update of ours, so
// this revision is our own work; `Fetch` means we went and looked, and this is what was
// there. A `Fetch` next to a revision no publication of yours produced is how a foreign push
// this commit is our own work; `Fetch` means we went and looked, and this is what was
// there. A `Fetch` next to a commit no publication of yours produced is how a foreign push
// to the branch is read off kubectl.
// +optional
// +kubebuilder:validation:Enum=Push;Fetch
Expand Down Expand Up @@ -479,18 +479,18 @@ type GitTargetPlacementStatus struct {
// +optional
ReadOnlyBases []string `json:"readOnlyBases,omitempty"`

// ResolvedAtRevision is the Git revision this resolution was first observed at. It is not
// ResolvedAtCommit is the commit hash this resolution was first observed at. It is not
// re-stamped on every scan: a resolution that has not changed is not republished, because
// doing so would write status once per commit to the branch, whichever target caused the
// commit. So it dates the RESOLUTION, not the last scan — a revision older than the branch
// commit. So it dates the RESOLUTION, not the last scan — a commit older than the branch
// head means the folder's layout has not changed since, not that nothing has looked.
//
// It is empty when the branch had no commit at the time (a folder nothing has written to yet)
// and is filled in by the first scan that finds one.
// +optional
ResolvedAtRevision string `json:"resolvedAtRevision,omitempty"`
ResolvedAtCommit string `json:"resolvedAtCommit,omitempty"`

// ResolvedAt is when this resolution was computed. Like ResolvedAtRevision it dates the
// ResolvedAt is when this resolution was computed. Like ResolvedAtCommit it dates the
// resolution rather than the last scan, so a timestamp well in the past means the folder's
// shape has been stable, not that scanning stopped.
// +optional
Expand All @@ -511,30 +511,6 @@ const (
PlacementModeKustomizeOverlay PlacementMode = "KustomizeOverlay"
)

// GitTargetStreamsStatus is a bounded roll-up of the stream readiness state for the
// types this GitTarget tracks.
type GitTargetStreamsStatus struct {
// Summary is the display-only ready/total ratio, e.g. "3/4".
//
// It restates Ready and Total, which the API conventions would normally rule out. It exists
// solely to feed the Streams printer column: a column can read one JSONPath, not format two.
// Do not compute anything from it — read ready and total.
// +optional
Summary string `json:"summary,omitempty"`

// Total is how many types this target tracks.
Total int32 `json:"total"`

// Ready is how many tracked types are Streaming.
Ready int32 `json:"ready"`

// Replaying is how many tracked types are still replaying their initial events.
Replaying int32 `json:"replaying"`

// Blocked is how many tracked types cannot currently be watched.
Blocked int32 `json:"blocked"`
}

// Counts, never a per-document list, so the field stays bounded however many documents are
// retained. Pull-based: only as fresh as the last reconcile, which is fine for an observation and
// is another reason this must not become a condition.
Expand All @@ -545,19 +521,20 @@ type GitTargetRetentionStatus struct {
// rather than left to be read from the spec because a GitTarget that predates spec.prune has
// no stored value at all, so the spec alone cannot explain why documents are being kept.
// +optional
// +kubebuilder:validation:Enum=Never;OnEvent;Always
Mode PruneMode `json:"mode,omitempty"`

// RetainedDocuments is how many managed documents the policy kept that a converged mirror
// would not hold. Zero means a resync ran and found nothing to retain — the mirror is
// converged. An ABSENT retention block means something different: no resync has reported yet.
RetainedDocuments int32 `json:"retainedDocuments"`

// LastChangedTime records when the reported retention count or effective prune mode last
// LastChangedAt records when the reported retention count or effective prune mode last
// changed. It does NOT indicate when retention was last evaluated: a resync that re-reports the
// same count leaves it untouched, so an old timestamp is equally consistent with stable
// retention and with nothing having measured it since.
// +optional
LastChangedTime *metav1.Time `json:"lastChangedTime,omitempty"`
LastChangedAt *metav1.Time `json:"lastChangedAt,omitempty"`
}

// +kubebuilder:object:root=true
Expand All @@ -575,13 +552,11 @@ type GitTargetRetentionStatus struct {
// +kubebuilder:printcolumn:name="RenderMatchesLive",type=string,JSONPath=`.status.conditions[?(@.type=="RenderMatchesLive")].status`,priority=1
// +kubebuilder:printcolumn:name="StreamsRunning",type=string,JSONPath=`.status.conditions[?(@.type=="StreamsRunning")].status`,priority=1
// +kubebuilder:printcolumn:name="SourceReachable",type=string,JSONPath=`.status.conditions[?(@.type=="SourceClusterReachable")].reason`,priority=1
// +kubebuilder:printcolumn:name="ProviderReady",type=string,JSONPath=`.status.conditions[?(@.type=="GitProviderReady")].status`,priority=1
// +kubebuilder:printcolumn:name="ClusterProviderReady",type=string,JSONPath=`.status.conditions[?(@.type=="ClusterProviderReady")].status`,priority=1
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1
// +kubebuilder:printcolumn:name="Encryption",type=string,JSONPath=`.spec.encryption.provider`,priority=1
// +kubebuilder:printcolumn:name="Provider",type=string,JSONPath=`.spec.gitProviderRef.name`,priority=1
// +kubebuilder:printcolumn:name="Branch",type=string,JSONPath=`.spec.branch`,priority=1
// +kubebuilder:printcolumn:name="Path",type=string,JSONPath=`.spec.path`,priority=1
// +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].message`,priority=1
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

// GitTarget is the Schema for the gittargets API.
Expand Down
Loading
Loading