Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions api/v1alpha3/gitbranch_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ type GitBranchSpec struct {

// AllowedGitTargets says which GitTargets may write through this GitBranch. Omitted, it defaults
// to one grant for this GitBranch's own namespace (consumerNamespaces: {from: Same}). A list you
// write is the whole policy: nothing is added to it, so a list without a from: Same grant shuts
// this namespace out, and [] admits no GitTarget. Grants combine by union. Mutable: claiming a
// branch includes the right to share it.
// write is the whole policy: nothing is added to it, so this namespace is admitted only if the
// list grants it access, and [] admits no GitTarget. Grants combine by union. Mutable: claiming
// a branch includes the right to share it.
// +optional
// +listType=atomic
// +kubebuilder:validation:MaxItems=64
Expand Down
6 changes: 3 additions & 3 deletions config/crd/bases/configbutler.ai_gitbranches.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ spec:
description: |-
AllowedGitTargets says which GitTargets may write through this GitBranch. Omitted, it defaults
to one grant for this GitBranch's own namespace (consumerNamespaces: {from: Same}). A list you
write is the whole policy: nothing is added to it, so a list without a from: Same grant shuts
this namespace out, and [] admits no GitTarget. Grants combine by union. Mutable: claiming a
branch includes the right to share it.
write is the whole policy: nothing is added to it, so this namespace is admitted only if the
list grants it access, and [] admits no GitTarget. Grants combine by union. Mutable: claiming
a branch includes the right to share it.
items:
description: |-
GitBranchGitTargetGrant admits GitTargets in some control-cluster namespaces to write through a
Expand Down
86 changes: 33 additions & 53 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,41 @@
The open work, in one list. Each item is one or two lines with a code pointer or the design page
that argues it. Ranked within each group, most urgent first. **Before the next release** marks what
should land before the next tag is cut (the last release is v0.53.0). Checked against `main` at
`1c4fade4` on 2026-10-24.
`05799999` plus #443 on 2026-10-24.

## Before the next release

- [ ] **R1: an applier that comes due during a flush records nothing between the mark and its
joining.** Its `flushEdits` starts when it joins, not at the mark, so an edit decided in between
is neither published before the apply nor recorded; if the apply overwrites it, Git keeps the edit
and the cluster runs the old value with no `Repair` pending. Start `flushEdits` for every applier
when the flush begins, or take a new mark when the due set grows. `flushed()` in
`internal/git/applier_claim.go`.
- [ ] **R4: a replaced branch worker reads the old worker's settled-snapshot record.**
`forgetSettledSnapshots` runs only on `GitTarget` teardown (`internal/watch/materialization.go`),
so after "Branch worker replaced" (`gittarget_controller.go`, the `RequestRecheckForGitTarget`
path) the new worker's startup converge passes the barrier at once and can apply before the folder
is rebuilt. Argo CD can then be asked for a path that does not exist.
- [ ] **Override #428's changelog entry.** Its `BREAKING CHANGE` footer describes
`GitProvider.spec.accessFrom` and "every GitTarget writing one repository branch must name the same
GitProvider", both replaced before release by `GitBranch` and grants (#429, #441). Add a
`BEGIN_COMMIT_OVERRIDE` block to #428's PR body, as #419 did, before the release PR is cut.
- [ ] **Remove the "loud rejection" fields.** `RemovedReceiver` (`gitprovider_types.go`),
`RemovedClusterProviderRef` (`gittarget_types.go`, `fluxclaim_types.go`, `argocdclaim_types.go`),
`RemovedGitProviderRef`, `RemovedBranch` and `RemovedParentBranch` (`gittarget_types.go`) exist
only to refuse an old spelling at admission. With no users yet, an API break is a hard cut; drop
them and their CEL rules, and the matching sentences in [UPGRADING.md](UPGRADING.md).
- [ ] **F-i: CRD text beyond the contract.** `RepairPending` says "an API edit made after its
flush", and `kubectl explain` never defines "flush" (`fluxclaim_types.go`, `argocdclaim_types.go`).
Check `applierClaimRef`'s description for rationale too (AGENTS.md, API changes).
- [ ] **Override the changelog entries of #441 and #419.** Like #428's (overridden), #441's squash
footer narrates moves between shapes that never shipped (`from`/`names`/`selector` into
`consumerNamespaces`, `branches` renamed `branchPatterns`), and #419's override still names
`FluxTrigger`, `ArgoCDTrigger` and `spec.applyTriggerRef`, which #438 renamed. Give each a
`BEGIN_COMMIT_OVERRIDE` block that describes what ships, consistent with
[UPGRADING.md](UPGRADING.md), before the release PR is cut.

## Applier claims and bi-directional editing

- [ ] **Decide Fix 2 for live edits dropped while render fidelity is pending**: hold the events
(2a, the user stays the author) or re-derive them with a resync (2b, the operator becomes the
author), or keep the drop and alert on `git_writes_dropped_total`. It is also the cause behind the
flaky audit-route override e2e spec.
[live-edit-dropped-while-render-fidelity-pending.md](design/live-edit-dropped-while-render-fidelity-pending.md).
- [ ] **Reopen the render-fidelity gate after a Git repair.** A target stuck on
`RenderMatchesLive=False` clears only on a re-declaration or a restart. `Invalidate` and
`RequestRecheckForGitTarget` exist; their only caller is branch-worker replacement
(`gittarget_controller.go`). Specified in
[render-fidelity.md](design/support-boundary/render-fidelity.md) §6b; no decision needed.
- [ ] **Reopen the render-fidelity gate after a Git repair, automatically.** Today a target on
`RenderMatchesLive=False` clears only on an explicit recheck (the
`reconcile.configbutler.ai/requestedAt` annotation, which restarts every scope; the render
fidelity e2e spec proves it recovers a repaired folder), a re-declaration or a restart. Doing it on
an incoming Git revision is more than calling `Invalidate` and `RequestRecheckForGitTarget` from a
new place: it first needs the retained-intent ordering barrier, so the source refresh cannot
discard or race an open commit window or pending live edits
([render-fidelity.md](design/support-boundary/render-fidelity.md) §6b).
- [ ] **Watch the held applier**, so a switch-on or a `flux resume` is undone at once instead of
within the one-minute `heldClaimSteadyInterval`. Also makes ask #31 below exact.
[applier-claim-held-applier-watch.md](design/applier-claim-held-applier-watch.md).
- [ ] **F-a: a held barrier is invisible.** A `GitTarget` with no planned collection holds its
applier, and with it every reason of every `GitTarget` on that claim, `ForeignCommit` included,
with no log, condition or metric. The hold is approved; the silence is the defect.
(`internal/git/applier_claim.go`, `internal/watch/apply_state.go`)
- [ ] **F-e: the apply window compares two clocks.** `sentAt` is the operator's clock,
`LastFieldManagedAt` the source API server's, at second granularity; a skew of 1s flips the echo
decision. Add a skewed-clock test, and probably a one-second grace. (`internal/git/apply_window.go`)
- [ ] **F-b, F-c, F-d: edge cases of the snapshot hold.** Live events dropped at the gate while a
discarded snapshot is held (F-b); an adopt lost while render fidelity is closed, because
`mirrors()` returns before `noteCreated` (F-c); a `GitTarget` that names a claim after its
snapshots settled empty never gets its folder (F-d, `resync_flush.go`).
- [ ] **F3: a Flux fetch is made before the hold is checked**, so a `Kustomization` resumed by hand
applies the fetched artifact at once (`internal/applier/flux.go`, `Request`).
- [ ] **The apply window's remaining clock limits** (`internal/git/apply_window.go`, file comment).
A server clock ahead by more than the time between an earlier apply and the send dates an undo of
an edit published before the send after it, so the undo is not mirrored and the next apply reverts
it. With an edit carried from the window before, the check against the send still mixes the two
clocks, and a server clock behind can withhold this attempt's own apply and ask for an unneeded
`Repair`. Only comparing the event's content with Git separates them.
- [ ] **F-h: small leaks and races.** The 5-minute hold can restart; the `Initial` flag races
settlement; `snapshotHeldSince`, `applierState.named` and dropped collections in
`settledSnapshots` leak.
- [ ] **F2: accept a Flux fetch whose token was stamped over** when the artifact holds the commit.
Today `Flux.fetch` waits only for its own token, so a commit another stamp fetched fails after 30s
(`internal/applier/flux.go`). Also check F3: the hold is checked after the fetch, so a
`Kustomization` resumed by hand applies the fetch at once.
- [ ] **A2: judge a gone Argo CD token by `status.history`** (our commit,
`initiatedBy: gitops-reverser`, `deployStartedAt` at or after our request means `Succeeded`).
Today a manual sync that wipes our token after a success reads as failed and syncs again, so hooks
run twice (`internal/applier/argocd.go`).
- [ ] **Check Argo CD sync windows in `Configured`.** The guide says a sync window must not block
GitOps Reverser's syncs; nothing checks it.
- [ ] **Refuse or flag overrides that are not in Git** (Argo `spec.source.kustomize`; Flux
Expand Down Expand Up @@ -220,6 +190,16 @@ Their argument for each is in [gitops-api-open-asks.md](future/gitops-api-open-a

## Docs and vocabulary

- [ ] **Cleanup: drop the "loud rejection" fields.** `RemovedReceiver` (`gitprovider_types.go`),
`RemovedClusterProviderRef` (`gittarget_types.go`, `fluxclaim_types.go`, `argocdclaim_types.go`),
`RemovedGitProviderRef`, `RemovedBranch` and `RemovedParentBranch` (`gittarget_types.go`) refuse an
old spelling at admission with a message naming its replacement. Keeping that message is not a
release defect; with no users yet they can go, with their CEL rules and the matching sentences in
[UPGRADING.md](UPGRADING.md), whenever convenient.
- [ ] **Cleanup, F-i: CRD text beyond the contract.** `RepairPending` says "an API edit made after
its flush", and `kubectl explain` never defines "flush" (`fluxclaim_types.go`,
`argocdclaim_types.go`). Check `applierClaimRef`'s description for rationale too (AGENTS.md, API
changes).
- [ ] **Stale design-doc headers**, one docs PR: [open-asks-priority.md](design/open-asks-priority.md)
(#339 closed, #23 fixed), [gittarget-api-wave.md](design/gittarget-api-wave.md) (riders shipped),
[git-write-preflight.md](design/git-write-preflight.md) (superseded by the red-status plan),
Expand Down
34 changes: 22 additions & 12 deletions docs/UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ We are pre-1.0, so breaking changes bump the **minor** version (release-please i
## Upgrading from 0.53.0: recreate the connection objects in one pass

**Breaking, with no in-place conversion.** The objects that connect a folder to a cluster, a
repository and an applier are reshaped, and the next five entries say how. `ClusterProvider` is a
repository and an applier are reshaped, and the entries below say how. `ClusterProvider` is a
namespaced `KubeProvider`, a repository branch is a `GitBranch`, the push receiver is a
`GitReceiver`, `FluxTrigger` and `ArgoCDTrigger` are `FluxClaim` and `ArgoCDClaim`, and grants say
who may use each provider and branch. Every `GitTarget` and applier claim is recreated, because
Expand All @@ -31,10 +31,14 @@ the new object takes it over on its first snapshot.
`Application` keeps automated sync off, as you set it for 0.53.0, so Argo CD syncs it only when
someone starts a sync until step 6. Then delete their CRDs:
`kubectl delete crd fluxtriggers.configbutler.ai argocdtriggers.configbutler.ai`.
3. Upgrade the CRDs, the operator and its permissions. The chart's `clusterProvider.*` values are
`kubeProvider.*`, and the provider it renders is a `KubeProvider` named `local` in the release
namespace, which admits only the release namespace's own objects until you set its grants (see
the grants entry).
3. Upgrade the CRDs, the operator and its permissions. Remove `clusterProvider` from your Helm
values first: the chart's schema rejects it. `clusterProvider.createDefault` is
`kubeProvider.create`, `clusterProvider.default.kubeConfig.secretRef` is `kubeProvider.secretRef`,
and `accessFrom` and `allowCrossNamespace` are `kubeProvider.allowedGitTargets` and
`kubeProvider.allowedApplierClaims` (see the grants entry). The provider the chart renders is a
`KubeProvider` named `local` (the default of `kubeProvider.name`; use yours where you set it) in
the release namespace, where 0.53.0's was the `ClusterProvider`
`default`, and it admits only the release namespace's own objects until you set its grants.
4. Create the new connection objects:
- a `KubeProvider` for each other `ClusterProvider`, in the namespace that should own the
credential, with its kubeconfig Secret copied into that namespace;
Expand All @@ -44,10 +48,15 @@ the new object takes it over on its first snapshot.
- a `GitBranch` for each repository branch your `GitTarget`s wrote, from their `gitProviderRef`,
`branch` and `parentBranch`.
5. Delete each `GitTarget` and create it again with `spec.gitBranchRef` and `spec.kubeProviderRef`,
and `spec.applierClaimRef` where it had `spec.applyTriggerRef`.
6. Create a `FluxClaim` or `ArgoCDClaim` for each trigger you deleted: the same `spec`, with
`spec.kubeProviderRef` for `spec.clusterProviderRef` and without `spec.quietPeriod`. It takes
hold of its applier again once a `GitTarget` names it.
and `spec.applierClaimRef` where it had `spec.applyTriggerRef`. `spec.kubeProviderRef` is
required: a `GitTarget` whose `spec.clusterProviderRef` named `default`, which 0.53.0 filled in
when it was omitted, now names the chart's provider,
`{name: local, namespace: <release namespace>}` (your `kubeProvider.name` in place of `local` where
you set one), which admits a `GitTarget` in another
namespace only through a grant.
6. Create a `FluxClaim` or `ArgoCDClaim` for each trigger you deleted: the same `spec`, with the
required `spec.kubeProviderRef` in place of `spec.clusterProviderRef`, named as in step 5, and
without `spec.quietPeriod`. It takes hold of its applier again once a `GitTarget` names it.
7. Point each API server's audit webhook at its provider's new route, or set
`spec.attribution.auditRoute` to the route it posts under today.
8. Delete the old CRD, which Helm leaves installed: `kubectl delete crd clusterproviders.configbutler.ai`.
Expand Down Expand Up @@ -211,7 +220,7 @@ What 0.53.0 allowed, written as grants: a `GitTarget` wrote any `allowedBranches
```yaml
kind: GitProvider
spec:
allowedGitBranches: # was allowedBranches: [main]; required, no default
allowedGitBranches: # was allowedBranches: [main]; no default: omitted admits nobody
- consumerNamespaces: {from: Same}
branchPatterns: [main]
---
Expand Down Expand Up @@ -254,8 +263,9 @@ spec:
reports `Validated=False` (`NamespaceNotAuthorized`).
- **The chart's `local` provider admits only the release namespace** unless
`kubeProvider.allowedGitTargets` and `kubeProvider.allowedApplierClaims` say otherwise; `[]` there
admits nobody. `quickstart.enabled=true` replaces the default with a grant for exactly the
quickstart namespace. To keep 0.53.0's open default, every namespace mirroring its own, install
admits nobody. `quickstart.enabled=true` adds a grant for exactly the quickstart namespace; with
the lists unset, that grant replaces the default rather than joining it. To keep 0.53.0's open
default, every namespace mirroring its own, install
with the chart's `values-playground.yaml`, which sets the `KubeProvider` grants above.
- **The default trusts the object's own namespace.** Whoever may create a `GitTarget` or an applier
claim beside a `KubeProvider` may use its credential within the default scope. Keep a provider in
Expand Down
2 changes: 1 addition & 1 deletion docs/api-first-publication.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ them is retried automatically, so it is worth knowing which one you are looking
| --- | --- | --- |
| Sensitive-resource encryption | Secrets and configured sensitive types must never reach Git in plaintext | The write fails rather than falling back to plaintext. There is no opt-out |
| Acceptance of the Git path | The target's folder must be content the writer can edit safely | The plan is refused and reported as `GitPathAccepted=False` on the `GitTarget`. The periodic refresh raises the same condition off a READ, so folder content somebody else pushed is reported before a write goes looking for it. The target re-checks roughly every ten seconds; recovery requires an accepted resync after correcting the folder or live state, or — for a refusal a read raised — a later read that finds the folder clean |
| Render fidelity | A target whose render-vs-live epoch is pending or divergent must not take live writes | Live events and atomic writes are dropped while the gate is closed, counted by `gitopsreverser_git_writes_dropped_total`, and the first drop under each gate status is logged at Info. Resync stays allowed so it can measure and repair Git. The gate is per `GitTarget`: while any of its collections is replaying, edits to all of them are dropped. A live edit queued behind a replay is not: the worker records the replay's result before it takes the next item |
| Render fidelity | A target whose render-vs-live epoch is pending or divergent must not take live writes | While the gate is pending (a collection of the `GitTarget` is replaying, which closes all of them), live events are held and committed in order, with their authors, once it opens; a replay of their collection supersedes the ones held before it, except a deletion of an object it does not hold. The hold gives up after 10 minutes, or past the branch's retained-byte budget, by dropping what it held and asking for a fresh snapshot. A divergence drops live events, and atomic writes are dropped while the gate is closed. Every drop is counted by `gitopsreverser_git_writes_dropped_total`, and the first under each gate status is logged at Info. Resync stays allowed so it can measure and repair Git |

A refusal is not a transient error and is not retried into success. It is the common reason a live
edit never appears in Git while the worker looks healthy, so check the `GitTarget` conditions before
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ object that lends something, and is named `allowed` plus the kind that refers to
| You write | The list is |
|---|---|
| nothing | `GitBranch`: `[{consumerNamespaces: {from: Same}}]`. `KubeProvider`: `allowedGitTargets: [{consumerNamespaces: {from: Same}, sourceNamespaces: ["{consumer.namespace}"]}]` and `allowedApplierClaims: [{consumerNamespaces: {from: Same}, applierNamespaces: ["{consumer.namespace}"]}]` |
| a list | exactly that list; the default is not added, so a list without a `from: Same` grant shuts the own namespace out |
| a list | exactly that list; the default is not added, so the local namespace is admitted only if the list grants it access |
| `[]` | nobody, the own namespace included |

The default grants no cluster-scoped resource, and it reaches only the namespace of the same
Expand Down
Loading
Loading