Repository navigation
fix(commitrequest): keep a save's writes under its message, and answer it sooner #465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c14b8d9
2474d39
cb6fd4f
986bb71
b1ec6c7
05ba2cf
c0603a0
a2538c4
df38f00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1553,8 +1553,10 @@ out, the retained buffer reaches `--branch-buffer-max-size` (default `8Mi`), an | |
| or an apply comes due for an applier on the branch. The timers | ||
| are the GitTarget's `spec.commit.window` until a `CommitRequest` attaches and replaces them. Successful local commits | ||
| are retained until a fixed push cooldown (`5s`) allows a push, which prevents remote push storms during | ||
| bursts. The one exception is an apply's flush: the writes decided before the apply came due are | ||
| pushed at once, cooldown or not, because the apply waits for them. | ||
| bursts. Two things push at once, cooldown or not, because something waits for the push: an apply's | ||
| flush, for the writes decided before the apply came due, and a `CommitRequest`'s commit, for the | ||
| person who saved. Saves cannot burst the way plain writes can, since an author has one save in | ||
| progress at a time, so the cooldown still paces every stream of plain writes. | ||
|
Comment on lines
+1558
to
+1559
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarify the cooldown claim for mixed pending writes. The scheduler checks whether any pending write carries a Change this to describe plain-only streams, or update the scheduler if plain writes must never bypass the cooldown.
|
||
|
|
||
| ### Local clones and conflict retry | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -430,15 +430,16 @@ applier on the branch, whether or not the push touched its folder, and a `CatchU | |
| create in the folder and when a `GitTarget` first names the applier claim. | ||
|
|
||
| **The apply window.** Until 10 seconds after the sync's operation completes, however long that | ||
| takes, Argo CD's own write to an object nobody edited is the sync landing, and is not mirrored. An | ||
| API edit to a field the `Application` sets, one made after the flush included, can be reverted on | ||
| the cluster by the sync. GitOps Reverser recognizes that write by Argo CD's field manager | ||
| (`argocd-controller`, for a client-side and a server-side apply alike, or | ||
| `argocd-application-controller` for a `Replace=true` sync), withholds it from Git, and asks for a | ||
| `Repair` sync pinned to the tip that holds the edit, but only when Argo CD's entry is as new as the | ||
| edit or newer: undoing an edit can leave an older entry of Argo CD's as the newest, and that undo | ||
| is mirrored. When Argo CD and another manager wrote in the same second, the write is mirrored and | ||
| counted as `ambiguous`. | ||
| takes, and until the watch has delivered every write the cluster held then, for at most two minutes | ||
| ([the life of an apply](bi-directional.md#the-life-of-an-apply)), Argo CD's own write to an object | ||
|
Comment on lines
+433
to
+434
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Qualify the watch-barrier guarantee. The barrier is not unconditional. If the source cannot be listed or its resource versions are invalid, the implementation drops the barrier and keeps only the 10-second grace. The paragraph currently says the window waits for every held write in all cases. (raw.githubusercontent.com) State that the two-minute barrier applies only when it can be taken, and document the grace-only fallback.
|
||
| nobody edited is the sync landing, and is not mirrored. An API edit to a field the `Application` | ||
| sets, one made after the flush included, can be reverted on the cluster by the sync. GitOps Reverser | ||
| recognizes that write by Argo CD's field manager (`argocd-controller`, for a client-side and a | ||
| server-side apply alike, or `argocd-application-controller` for a `Replace=true` sync), withholds it | ||
| from Git, and asks for a `Repair` sync pinned to the tip that holds the edit, but only when Argo | ||
| CD's entry is as new as the edit or newer: undoing an edit can leave an older entry of Argo CD's as | ||
| the newest, and that undo is mirrored. When Argo CD and another manager wrote in the same second, | ||
| the write is mirrored and counted as `ambiguous`. | ||
|
|
||
| A request that fails, and a sync that ends `Failed` or `Error`, is decided again from scratch, after | ||
| that `Application`'s own backoff. A sync still running after 5 minutes is `Unresolved`: GitOps | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -461,11 +461,13 @@ as failed, and records an `OperationUnfollowed` Event. A resumed `Kustomization` | |
| nothing watching it. | ||
|
|
||
| **The apply window.** From the moment an operation is asked for until 10 seconds after it completes, | ||
| a write by `kustomize-controller` alone to an object nobody edited is the apply landing, and is not | ||
| mirrored. A write by `kustomize-controller` over a newer API edit, one made after the flush included, | ||
| is withheld from Git, and GitOps Reverser asks for a `Repair`; the applier claim shows `repairPending` | ||
| until that apply succeeds. Only a `kustomize-controller` entry as new as the edit or newer counts: | ||
| undoing an edit can leave an older one as the newest entry, and that undo is mirrored. When | ||
| and until the watch has delivered every write the cluster held then, for at most two minutes ([the | ||
| life of an apply](bi-directional.md#the-life-of-an-apply)), a write by `kustomize-controller` alone | ||
|
Comment on lines
+464
to
+465
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Qualify the watch-barrier guarantee. The barrier is not unconditional. If the source cannot be listed or its resource versions are invalid, the implementation drops the barrier and keeps only the 10-second grace. The paragraph currently says the window waits for every held write in all cases. (raw.githubusercontent.com) State that the two-minute barrier applies only when it can be taken, and document the grace-only fallback.
|
||
| to an object nobody edited is the apply landing, and is not mirrored. A write by | ||
| `kustomize-controller` over a newer API edit, one made after the flush included, is withheld from | ||
| Git, and GitOps Reverser asks for a `Repair`; the applier claim shows `repairPending` until that | ||
| apply succeeds. Only a `kustomize-controller` entry as new as the edit or newer counts: undoing an | ||
| edit can leave an older one as the newest entry, and that undo is mirrored. When | ||
| `kustomize-controller` and another manager wrote in the same second, the write is mirrored and | ||
| counted as `ambiguous`. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3023,6 +3023,14 @@ The `GitTarget` a request names decides where it belongs, through the `KubeProvi | |
| other one's. Create it on the other cluster instead. When that `KubeProvider` does serve them, the | ||
| request is left alone, since the kubeconfig may point back at this cluster and then its own watch | ||
| serves the request. If it points elsewhere, nothing serves the request and it gets no status. | ||
| - **A request on a source cluster, for a `GitTarget` that mirrors another cluster, is refused** with | ||
| reason `NotAllowed`, and the message names both clusters, when nothing can serve it from there: | ||
| the `GitTarget` mirrors the cluster GitOps Reverser runs in, or its `KubeProvider` serves no | ||
| `CommitRequest`s. Create it on the cluster the `GitTarget` mirrors. A refused request finishes, | ||
| so a `KubeProvider` with `ReconcileAndDeleteFinished` deletes it after the TTL. A kubeconfig that | ||
| points back at the cluster GitOps Reverser runs in sees that cluster's own requests, and leaves | ||
| them to it. A request whose `GitTarget`'s `KubeProvider` does serve `CommitRequest`s is left | ||
| alone, since that `KubeProvider` may connect to the same cluster under another identity. | ||
|
|
||
| Only the path the `GitTarget`'s `KubeProvider` picks writes a request's progress, so several | ||
| `KubeProvider`s that watch one cluster never fight over a request. Any of them may report | ||
|
|
@@ -3085,12 +3093,13 @@ A request is registered with the branch worker only once its own audit event has | |
| and its edit is attributed the same way, so a request created just before its edit does not always | ||
| reach the worker first. On a `GitTarget` whose windows close on the first silence (`idleTimeout: | ||
| "0s"`), such an edit can commit alone before the request is there to attach to it. Make the edit | ||
| once the request is claimed (`Ready=Unknown`), or give the `GitTarget` an `idleTimeout` that covers | ||
| the gap. | ||
| once the request is registered, when its `Reconciling` reason is `WaitingForWindow` (see | ||
| [its status](#its-status)), or give the `GitTarget` an `idleTimeout` that covers the gap. | ||
|
Comment on lines
+3096
to
+3097
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allow both registered phases. When a matching window is already open, the worker can move from
|
||
| `Ready=Unknown` alone is too early: it starts at reason `Saving`, before the registration. | ||
|
|
||
| `maxDuration`, and `idleTimeout` when set, cover the span of the save's own writes after the | ||
| attach. The worst case from registration to the finalize is `attachTimeout` plus `maxDuration`; the | ||
| push follows, with its cooldown and any retries. | ||
| push follows at once, without the push cooldown, with any retries a remote that refuses it needs. | ||
|
|
||
| The two directions are not symmetric. Overshooting costs a few seconds of latency on the commit; | ||
| undershooting `attachTimeout` resolves the request `Ready=True` with reason `NoWindow` while the | ||
|
|
@@ -3104,6 +3113,59 @@ request never resolves `NoWindow` while its commit waits out the push cooldown. | |
| and a request together gives no ordering guarantee between controllers and watch streams. See the | ||
| [request contract](spec/commitrequest-design.md). | ||
|
|
||
| #### Saving a folder with `kubectl apply` | ||
|
|
||
| To save the objects of a folder in one commit with your message, put a `CommitRequest` in the | ||
| folder, next to them, and give it a new name for every apply: | ||
|
|
||
| ```yaml | ||
| # save-2026-10-28-checkout-limits.yaml | ||
| apiVersion: configbutler.ai/v1alpha3 | ||
| kind: CommitRequest | ||
| metadata: | ||
| name: save-2026-10-28-checkout-limits | ||
| spec: | ||
| gitTargetRef: | ||
| name: example-target | ||
| message: "feat: raise the checkout limits" | ||
| ``` | ||
|
|
||
| `kubectl apply -f folder/` then commits the folder's changes, authored by you, with the request's | ||
| message. kubectl applies the files in name order, so the request can land before or after the | ||
| objects: with `attach: CurrentOrNext` the request attaches to the window the objects already opened, | ||
| or waits for the one they open. Do not use `attach: Next` here: it would close the objects' window | ||
| under the generated message, then wait for a write that never comes. Another unclaimed edit of yours | ||
| on the same `GitTarget`, still in its window, shares the commit. | ||
|
|
||
| It is one commit only when the window holds every write, and a window is not a transaction. Two | ||
| things split it: | ||
|
|
||
| - **Time.** With the defaults the window closes 2 seconds after the attach. Writes reach the branch | ||
| worker once their audit facts have named their author, so on a large folder, a loaded API server or | ||
| a source cluster with slow audit delivery, the last writes can arrive later than kubectl sent them. | ||
| A write after the close commits on its own, under the generated message. For such a folder, give | ||
| the request a longer window, `spec.window.maxDuration: "10s"` for example, or rolling collection | ||
| (see [the request window](#the-request-window-specwindow)); the commit lands that much later. | ||
| - **Other writes on the branch.** A branch worker holds one window at a time, so a write by another | ||
| author, or for another `GitTarget` on the same branch, closes yours early, with the request's | ||
| message, and your later writes commit under the generated message. | ||
|
|
||
| The request then ends `Committed` with the writes its window held. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct the sentence grammar. Replace “with the writes its window held” with “with the writes held in its window.”
|
||
|
|
||
| The name is what makes the next apply a save. Applying the same folder again does this: | ||
|
|
||
| - **Nothing changed:** kubectl sends no write for an unchanged object, so there is nothing to save, | ||
| and the finished request keeps its outcome. If the request was already deleted (see | ||
| [finished requests](#finished-requests-are-deleted-after-48-hours)), the apply creates it again, | ||
| and it ends `NoWindow`. | ||
| - **Objects changed, same request:** the request is unchanged and finished, so it saves nothing. | ||
| The edits commit on the `GitTarget`'s own timers, under the generated message. | ||
| - **Message edited, same name:** the API server refuses the request, because its spec is | ||
| immutable, and kubectl exits with an error after applying the other files. Their edits commit | ||
| under the generated message. | ||
|
|
||
| The request itself is never mirrored, even when a `WatchRule` covers its folder or its type. | ||
|
|
||
| ### Who may save: `spec.allowedCommitRequests` | ||
|
|
||
| A `GitTarget` says which `CommitRequest`s may save its windows in `spec.allowedCommitRequests`, a | ||
|
|
@@ -3237,8 +3299,15 @@ request produced a pushed commit: | |
| message, so the two would race for it and one message would be lost without a word; the later | ||
| request is refused at once instead. Save again with a new request once the earlier one is `Ready`. | ||
| A request for the `Next` window is not refused by one that is collecting: it closes that window | ||
| under the earlier request's message first. Saves by different authors never race, since each | ||
| author has windows of its own. | ||
| under the earlier request's message first. Saves by different authors do not race for a | ||
| message: a write by another author closes the open window, and its message, before it opens one | ||
| of its own. | ||
|
|
||
| `SaveInProgress` is decided when the branch worker registers the request, which is after its | ||
| author was found, not when it is created. A client that relies on it to keep two of its saves | ||
| apart waits until the `Reconciling` reason is `WaitingForWindow` or `CollectingWindow` before it | ||
| makes the save's edits, not only for `Ready=Unknown`: a request still `Saving` is not registered | ||
| yet, and its edits could reach the window of the save before it. | ||
|
|
||
| `AlreadyPresent` means **the remote confirmed there was nothing to add**, so it is reported after | ||
| the push rather than when the local plan found no difference. That costs a few seconds and buys a | ||
|
|
@@ -3248,7 +3317,8 @@ request produced a pushed commit: | |
| - **Reconciling** / **Stalled**: the kstatus progress/blocked pair. `Reconciling=True` while the | ||
| request is in progress, with the phase the branch worker reports as its reason: `Saving` | ||
| (claimed, not yet registered), `WaitingForWorker` (the GitTarget has no branch worker yet), | ||
| `WaitingForWindow`, `CollectingWindow`, or `WaitingForPush`. | ||
| `WaitingForWindow`, `CollectingWindow`, or `WaitingForPush`. Each phase, and the outcome, is | ||
| written as soon as the branch worker reports it. | ||
| `Stalled=True` when the request failed and needs attention (kstatus reports the object Failed). | ||
| - **Pushed**: `True` once the commit, empty or not, is in the remote repository. | ||
| - **Applied**: only with [`catchUp: AfterPush`](#applying-a-save-at-once), once `Ready=True`: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update the remaining attach-timeout examples.
The new default is
5s, but the diagram at Line 299 and the explanation at Line 327 still call it2s. Update both passages so readers do not mistake the default wait for a shorter one.