Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
005c961
docs(design): plan suspended appliers as the new direction after revi…
sunib Oct 17, 2026
ad8236e
fix(git): remember a first-write tie as an edit in the apply window
sunib Oct 17, 2026
a362934
feat(applier): follow each apply as an operation, and repair a withhe…
sunib Oct 17, 2026
3cf7330
docs(design): record the suspended-appliers decisions and name the tr…
sunib Oct 17, 2026
47d86e4
feat(api): add FluxTrigger and ArgoCDTrigger, named by GitTarget.spec…
sunib Oct 17, 2026
69ce1f8
docs: name the apply triggers wherever spec.applier was documented
sunib Oct 17, 2026
a75d99b
feat(api): default a trigger's clusterProviderRef as a GitTarget's is
sunib Oct 17, 2026
456fa1a
feat(applier): hold a FluxTrigger's Kustomization suspended, resuming…
sunib Oct 17, 2026
0fb3a99
feat(git): follow an apply recorded on its trigger after a restart
sunib Oct 17, 2026
2a88c71
feat(applier): list a source's other consumers, one operation per sou…
sunib Oct 17, 2026
1875ab7
docs: rewrite the Flux guide around the suspended Kustomization
sunib Oct 17, 2026
96c9fd5
docs: match the trigger reference to the suspended Flux applier
sunib Oct 18, 2026
9b0aaeb
fix(git): let the appliers of a shared source take turns
sunib Oct 18, 2026
fd271ec
fix(applier): count HelmReleases that apply a source through a HelmChart
sunib Oct 18, 2026
3b38795
fix(applier): write an apply's record only through Record, and withdr…
sunib Oct 18, 2026
7affe8d
fix(controller): complete an external resume from the operation read …
sunib Oct 18, 2026
b51c411
fix(controller): prefetch only while every consumer is held, checked …
sunib Oct 18, 2026
efa0499
fix(controller): never let a FluxTrigger's hand-back block its deletion
sunib Oct 18, 2026
0cc3ef4
fix(applier): release a spec.suspend shared with a flux suspend
sunib Oct 18, 2026
49abb0f
test(applier): drop the HelmRelease fixture's always-constant source …
sunib Oct 18, 2026
ded4844
fix(applier): judge a Flux apply on what ran resumed, and follow an u…
sunib Oct 18, 2026
a527f3a
fix(applier): never leave a resumed Kustomization followed by nobody
sunib Oct 18, 2026
c01ca97
docs: describe the record, fence, prefetch and hand-back fixes
sunib Oct 18, 2026
4b57557
refactor(watch): drop SourceDynamicClient, which only tests called
sunib Oct 18, 2026
5fdf784
refactor(api): drop ApplyTrigger.CatchesUp, which nothing called
sunib Oct 18, 2026
806998c
fix(controller): let one trigger hold an applier, and the others wait…
sunib Oct 18, 2026
25cb7ea
fix(git): count the work item in hand when deciding an apply is current
sunib Oct 18, 2026
b333ab3
fix(git): keep the previous apply window's grace when a new attempt o…
sunib Oct 18, 2026
423b886
fix(git): serialize a shared source's operations across sibling workers
sunib Oct 18, 2026
d8e97d6
refactor(git): move ApplyRequest.TargetRefs into the tests that use it
sunib Oct 18, 2026
aea3aa1
refactor(git): ask the trigger for its shared source instead of asser…
sunib Oct 18, 2026
15c3dc3
fix(applier): resume only the Kustomization the request checked
sunib Oct 18, 2026
f6e21d0
fix(applier): follow an Argo CD sync whose update went unanswered
sunib Oct 18, 2026
898822b
fix(applier): ask the decision again after recording the operation
sunib Oct 18, 2026
a95afbe
refactor(applier): name what a request starts Started, not Stamped
sunib Oct 18, 2026
420de6b
refactor(git): give a prefetch its own request type
sunib Oct 18, 2026
0869c2c
docs: describe one trigger per applier, the guarded resume and the ca…
sunib Oct 18, 2026
6403cc5
test: raise the coverage baseline to 80.2%
sunib Oct 18, 2026
76c740b
test(controller): pin the API server's answer to a guarded resume
sunib Oct 18, 2026
79a34da
fix(applier): settle an unanswered or recovered Argo CD sync with a g…
sunib Oct 18, 2026
f6a5b2f
fix(controller): change a Kustomization's holder only between applies…
sunib Oct 18, 2026
c9bbfdc
fix(applier): judge an external resume a worker recovered as its reco…
sunib Oct 18, 2026
99cbdec
docs: a Kustomization changes holders only between applies, and is ne…
sunib Oct 18, 2026
0153342
refactor: remove what the review rounds left unused, and collapse the…
sunib Oct 18, 2026
b689966
docs: split the bi-directional guides by tool, and say when none of i…
sunib Oct 18, 2026
36ebff4
fix(applier): prefetch only the GitRepository whose consumers were ch…
sunib Oct 18, 2026
bd8ee47
refactor(applier): name the FluxTrigger's status from the trigger's side
sunib Oct 19, 2026
a69e656
feat(applier): a trigger acts only while an eligible GitTarget refere…
sunib Oct 19, 2026
37ab6fc
feat(clusterprovider)!: grant cross-namespace sources and appliers in…
sunib Oct 19, 2026
9f02e3f
fix(applier): hand the Kustomization back when its ClusterProvider is…
sunib Oct 19, 2026
26b997f
fix(applier): suspend a resumed Kustomization when the dispatcher sto…
sunib Oct 19, 2026
3835046
fix(applier): owe a newly named applier its first apply
sunib Oct 19, 2026
3569e71
fix(applier): mirror an undo inside the apply window instead of withh…
sunib Oct 19, 2026
d1e2706
fix(applier): end an Argo CD sync whose .operation was removed while …
sunib Oct 19, 2026
422e159
docs: correct the Reapply fetch, the uninstall order, and what kstatu…
sunib Oct 19, 2026
3b60e89
docs: say what kstatus reads without an em dash
sunib Oct 19, 2026
7e5e20d
test(e2e): expect a graceful stop to suspend the Kustomization mid-apply
sunib Oct 19, 2026
d9b5320
docs: describe the rule, the cross-namespace grant and the apply's st…
sunib Oct 19, 2026
d2e7b67
refactor: say what is pending instead of what is owed
sunib Oct 19, 2026
92c762a
test(applier): hold the Kustomization the dispatcher lookup test resumes
sunib Oct 19, 2026
5b3661b
test(queue): wait for the whole rewrite batch before sweeping
sunib Oct 19, 2026
62b01e4
fix(e2e): pull Argo CD's redis from mirror.gcr.io, pinned by digest
sunib Oct 19, 2026
6b4a8b6
fix(e2e): wait out a just-created CRD's null conditions
sunib Oct 19, 2026
da7b8fb
test(e2e): stream prepare-e2e's output so a hang shows where it stopped
sunib Oct 19, 2026
d7ce1bd
fix(git): record a replay's render fidelity before taking the next item
sunib Oct 19, 2026
b987bdd
docs: bring every page in line with the apply trigger as it ships
sunib Oct 19, 2026
cb5a1c9
refactor(chart): configure the push receiver under servers.receiver, …
sunib Oct 19, 2026
2a6c1ab
test(fluxsim): make a status write and a patch each one step, so a re…
sunib Oct 19, 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
2 changes: 1 addition & 1 deletion .coverage-baseline
Original file line number Diff line number Diff line change
@@ -1 +1 @@
80.0
80.2
5 changes: 3 additions & 2 deletions .docs-lint-scope
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@
README.md
docs/architecture.md
docs/configuration.md
docs/flux.md
docs/argocd.md
docs/bi-directional-flux.md
docs/bi-directional-argocd.md
docs/spec/attribution.md
docs/components.md
docs/quickstart.md
docs/demo/commit-window.md
docs/facts/kubernetes-watch-options.md
docs/spec/watch-event-ordering-and-attribution-grace.md
docs/bi-directional.md
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ several source clusters, repositories, branches, and folders.

## Principles

- **Creates Git commits.** The only output is Git commits that reflect the state of your cluster.
- **Creates Git commits.** The output is Git commits that reflect the state of your cluster.
Deploying is Flux's and Argo CD's job: run one alongside to bring changes from Git back into the
cluster. See [bi-directional usage](docs/bi-directional.md).
cluster. Only a folder that both GitOps Reverser writes and Flux or Argo CD applies needs more: an
apply trigger, so GitOps Reverser decides when it is applied. See
[bi-directional usage](docs/bi-directional.md).
- **API-first, not API-only.** Most changes are assumed to arrive through the API, so that is what
publishing is tuned for. Your changes end up in your remote Git repo within seconds. Other
authors can push to the same branch, and that is handled without conflicts. If an API edit and a
Expand Down Expand Up @@ -158,14 +160,15 @@ made after the changes or before them. It runs on top of the quickstart above.
## Try it with your existing repo

Start with a [scratch branch](docs/configuration.md#seeing-what-a-target-will-do-before-it-does-it)
containing your existing manifests, with no reconciler deploying that branch. Select a small resource
containing your existing manifests, with nothing deploying that branch. Select a small resource
scope and one destination folder. Inspect the initial commits before making a live edit, then check
which source files changed.

The operator pushes directly to the configured branch. If you later write to a branch that Flux or
Argo CD deploys, read the [bidirectional guide](docs/bi-directional.md) first. Live edits can be
reverted by the reconciler, and [replaying captured state](docs/api-first-publication.md) can overwrite
concurrent Git edits to the same object. Decide per folder which side is authoritative.
The operator pushes directly to the configured branch. Flux or Argo CD deploying other folders is
unaffected. If one of them deploys the folder you write, read the
[bi-directional guide](docs/bi-directional.md) first. Without an apply trigger, live edits can be
reverted by Flux or Argo CD, and [replaying captured state](docs/api-first-publication.md) can
overwrite concurrent Git edits to the same object. Decide per folder which side is authoritative.

## Before you adopt it

Expand Down
211 changes: 211 additions & 0 deletions api/v1alpha3/applytrigger_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// SPDX-License-Identifier: Apache-2.0

package v1alpha3

import (
"time"

meta "github.com/fluxcd/pkg/apis/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
)

// What every apply trigger kind shares: the reference a GitTarget names one by, why an apply is
// asked for, and the operation a trigger's status records. The kinds themselves are in
// fluxtrigger_types.go and argocdtrigger_types.go. docs/definitions.md fixes the words: the
// APPLIER is the Flux Kustomization or Argo CD Application, and the TRIGGER is this operator's
// resource that holds it.

// The apply trigger kinds GitTarget.spec.applyTriggerRef can name.
const (
// ApplyTriggerKindFlux is a FluxTrigger, which holds a Flux Kustomization.
ApplyTriggerKindFlux = "FluxTrigger"
// ApplyTriggerKindArgoCD is an ArgoCDTrigger, which holds an Argo CD Application.
ApplyTriggerKindArgoCD = "ArgoCDTrigger"
)

// The kinds of applier a trigger holds.
const (
// ApplierKindKustomization is a Flux Kustomization (FluxTrigger.spec.kustomizationRef).
ApplierKindKustomization = "Kustomization"
// ApplierKindApplication is an Argo CD Application (ArgoCDTrigger.spec.applicationRef).
ApplierKindApplication = "Application"
)

// Why a kind and a name, and no namespace: the kind keeps each trigger its own schema, the way
// Flux's sourceRef names a kind, and the namespace is the GitTarget's own because the trigger's
// ClusterProvider admits the namespace it is in (spec.accessFrom). A GitTarget that could name a
// trigger elsewhere would drive an applier that its own namespace was never admitted to.

// ApplyTriggerReference names an apply trigger in the GitTarget's own namespace.
type ApplyTriggerReference struct {
// Kind is the trigger's kind: FluxTrigger or ArgoCDTrigger.
// +required
// +kubebuilder:validation:Enum=FluxTrigger;ArgoCDTrigger
Kind string `json:"kind"`

// Name is the trigger's name.
// +required
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`
}

// ApplyReason is why GitOps Reverser asked an applier to apply.
// +kubebuilder:validation:Enum=ForeignCommit;Refusal;Repair;CatchUp
type ApplyReason string

const (
// ApplyReasonForeignCommit is a commit GitOps Reverser did not push.
ApplyReasonForeignCommit ApplyReason = "ForeignCommit"
// ApplyReasonRefusal is a refused edit on a target with spec.onRefusal: Reapply.
ApplyReasonRefusal ApplyReason = "Refusal"
// ApplyReasonRepair puts back an API edit that an earlier apply reverted on the cluster after
// the edit was made. Git kept the edit; the cluster runs the older value until the repair.
ApplyReasonRepair ApplyReason = "Repair"
// ApplyReasonCatchUp keeps the applier current with GitOps Reverser's own pushes.
ApplyReasonCatchUp ApplyReason = "CatchUp"
)

// ApplyOperationPhase is where an apply operation stands.
// +kubebuilder:validation:Enum=Requested;Running;Settling;Unresolved;Succeeded;Failed
type ApplyOperationPhase string

const (
// ApplyOperationRequested is an operation GitOps Reverser has asked the applier for.
ApplyOperationRequested ApplyOperationPhase = "Requested"
// ApplyOperationRunning is an operation the applier is seen working on.
ApplyOperationRunning ApplyOperationPhase = "Running"
// ApplyOperationSettling is a Flux operation that has applied, and whose Kustomization is not
// yet seen suspended again.
ApplyOperationSettling ApplyOperationPhase = "Settling"
// ApplyOperationUnresolved is an operation that has not completed in the time it was given. It
// is still followed, and no timer ends it.
ApplyOperationUnresolved ApplyOperationPhase = "Unresolved"
// ApplyOperationSucceeded is an operation that completed and applied.
ApplyOperationSucceeded ApplyOperationPhase = "Succeeded"
// ApplyOperationFailed is an operation that completed without applying the commit. It may have
// applied part of it.
ApplyOperationFailed ApplyOperationPhase = "Failed"
)

// Completed reports whether the phase ends its operation.
func (p ApplyOperationPhase) Completed() bool {
return p == ApplyOperationSucceeded || p == ApplyOperationFailed
}

// ApplyOperation is an apply GitOps Reverser started on the trigger's applier: the one in flight,
// or the last one, once it completed. The branch worker writes it on every transition.
type ApplyOperation struct {
// Phase is where the operation stands. Requested, Running, Settling and Unresolved are in
// flight: while one is, an edit to an object the applier applies can still be reverted by it,
// and the applier is asked nothing else. Succeeded and Failed have completed.
// +required
Phase ApplyOperationPhase `json:"phase"`

// Reason is why the apply was asked for. With several GitTargets on the trigger, it is the
// strongest of their reasons.
// +optional
Reason ApplyReason `json:"reason,omitempty"`

// Commit is the commit the applier was asked to apply. Empty when the remote did not carry the
// branch.
// +optional
Commit string `json:"commit,omitempty"`

// Token identifies the request in the applier's own status: the reconcile.fluxcd.io/requestedAt
// value on a Flux object, or the "gitops-reverser request" entry in an Argo CD operation's info.
// +optional
Token string `json:"token,omitempty"`

// SuspendToken is the reconcile.fluxcd.io/requestedAt value the suspend after the apply carried.
// Once the Kustomization reports it handled, nothing started before the suspend is still
// running. Flux only.
// +optional
SuspendToken string `json:"suspendToken,omitempty"`

// AppliedRevision is the revision the applier reports it applied once the operation completed.
// It can differ from commit when the branch moved between the check and the apply.
// +optional
AppliedRevision string `json:"appliedRevision,omitempty"`

// RequestedAt is when the applier was asked.
// +required
RequestedAt metav1.Time `json:"requestedAt"`

// FinishedAt is when the operation was seen to complete.
// +optional
FinishedAt *metav1.Time `json:"finishedAt,omitempty"`

// Message says why a Failed operation failed, or why an Unresolved one is overdue.
// +optional
Message string `json:"message,omitempty"`
}

// DefaultApplierQuietPeriod is FluxTrigger.spec.quietPeriod when it is omitted.
const DefaultApplierQuietPeriod = 30 * time.Second

// ApplyTrigger is what every trigger kind answers, so the controllers, the branch workers and the
// dispatcher handle a FluxTrigger and an ArgoCDTrigger alike.
// +kubebuilder:object:generate=false
type ApplyTrigger interface {
metav1.Object
runtime.Object

// TriggerKind is the trigger's kind (ApplyTriggerKind*).
TriggerKind() string
// ClusterProvider is the name of the ClusterProvider whose cluster holds the applier.
ClusterProvider() string
// ApplierKind is the kind of applier the trigger holds (ApplierKind*).
ApplierKind() string
// Applier is the object the trigger holds, its namespace defaulted to the trigger's own.
Applier() types.NamespacedName
// EffectiveQuietPeriod is how long the branch must be quiet before a CatchUp.
EffectiveQuietPeriod() time.Duration
// SourceBinding is the repository and branch the applier applies, as the trigger last read
// them, and false when it has not read them.
SourceBinding() (repositoryURL, branch string, known bool)
// SharedSource is the source object the applier reads, which other appliers may read too, and
// false when the applier has none it shares this way or the trigger has not read it. Appliers
// sharing one have one operation at a time.
SharedSource() (source types.NamespacedName, known bool)
// Prefetches reports whether a push may be fetched into the shared source ahead of the apply:
// every other reader of it is held, so a new artifact applies nothing on its own.
Prefetches() bool
// Operation is status.operation, and SetOperation replaces it and status.repairPending.
Operation() *ApplyOperation
RepairPending() bool
SetOperation(operation *ApplyOperation, repairPending bool)
// StatusConditions returns the condition set, and SetObservedGeneration stamps the generation.
StatusConditions() *[]metav1.Condition
SetObservedGeneration(generation int64)
}

// NewApplyTrigger returns an empty trigger of kind, or nil for a kind this release does not know.
func NewApplyTrigger(kind string) ApplyTrigger {
switch kind {
case ApplyTriggerKindFlux:
return &FluxTrigger{}
case ApplyTriggerKindArgoCD:
return &ArgoCDTrigger{}
default:
return nil
}
}

// defaultedNamespace is namespace, or fallback when it is empty.
func defaultedNamespace(namespace, fallback string) string {
if namespace == "" {
return fallback
}
return namespace
}

// clusterProviderName is ref's name, or DefaultClusterProviderName when ref names none, the way
// GitTarget.SourceCluster resolves an omitted clusterProviderRef.
func clusterProviderName(ref *meta.LocalObjectReference) string {
if ref == nil || ref.Name == "" {
return DefaultClusterProviderName
}
return ref.Name
}
Loading
Loading