Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0c5638f
feat(watch): never mirror the kube-root-ca.crt ConfigMap
sunib Oct 26, 2026
a77b82c
feat(applier): converge the applier on the worker's own pushes at spe…
sunib Oct 26, 2026
91020f4
feat(commitrequest): spec.catchUp asks the applier to apply a save at…
sunib Oct 26, 2026
4c8a6f8
feat(commitrequest): report the apply a save asked for in an Applied …
sunib Oct 26, 2026
59a7848
fix(kustomize): keep a release's values file, and find generator entr…
sunib Oct 26, 2026
c838fce
fix(commitrequest): settle Applied by the commit the applier applied;…
sunib Oct 26, 2026
c5afe20
fix(commitrequest): a save that needed no apply is Applied=True, Noth…
sunib Oct 26, 2026
2a2e1c7
test(e2e): Flux applying into a kcp workspace the GitTarget mirrors
sunib Oct 26, 2026
5ccaf54
feat(commitrequest): take over a window whose writes name no author, …
sunib Oct 26, 2026
33498b9
fix(commitrequest): a save whose GitTarget is gone is Applied=True
sunib Oct 26, 2026
ae00b25
feat(commitrequest): claiming a window whose writes name no author is…
sunib Oct 26, 2026
1a45946
fix(applier): an apply window waits until the watch delivered the app…
sunib Oct 26, 2026
df42e46
test(e2e): provoke the workspace collision by editing before its even…
sunib Oct 26, 2026
2e2b0b5
feat(saverequest): save the edits made on a source cluster with a Sav…
sunib Oct 26, 2026
aaed46f
fix(applier): carry a pending apply-window barrier as a wait, and bou…
sunib Oct 26, 2026
f9289bf
fix(applier): an attempt that carries a save is never a lag-only conv…
sunib Oct 26, 2026
abf5570
feat(api)!: CommitRequest is SaveRequest; the source-cluster save is …
sunib Oct 26, 2026
0432742
feat(remotesaverequest): a GitTarget admits RemoteSaveRequests, and a…
sunib Oct 26, 2026
219b6ea
feat(saverequest): name a GitTarget in another namespace, which admit…
sunib Oct 26, 2026
e0a5b8b
feat(remotesaverequest): name the GitTarget, refuse a second save in …
sunib Oct 26, 2026
fed6d58
fix(saverequest): bind a save to the GitTarget that admitted it, and …
sunib Oct 26, 2026
7a45f46
docs(review): one CommitRequest, no admission webhook
sunib Oct 27, 2026
4bf9b3e
refactor(api): CommitRequest is CommitRequest again
sunib Oct 27, 2026
448a108
feat!: remove the admission webhook and window takeover
sunib Oct 27, 2026
a2023a9
feat(commitrequest)!: serve requests through one core, with the autho…
sunib Oct 27, 2026
cc4d221
feat(commitrequest): serve the CommitRequests of a source cluster thr…
sunib Oct 27, 2026
c02b506
docs: one CommitRequest, served here or on a source cluster; cert-man…
sunib Oct 27, 2026
3f71841
fix(commitrequest): report a granted Role at once, and retry a source…
sunib Oct 27, 2026
36bb4b4
docs(review): phase 1 of the one-CommitRequest design is built
sunib Oct 27, 2026
cecd538
test(e2e): make an edit a save was created for once the save is claimed
sunib Oct 27, 2026
dada53a
fix(commitrequest): follow a rotated credential, re-read audit state,…
sunib Oct 27, 2026
eaa9f83
docs: design the mutation-capture lab on its own cluster, and revise …
sunib Oct 27, 2026
c7fc27a
fix(build): drop the removed source CRDs from helm-sync's generates
sunib Oct 27, 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.3
82.5
8 changes: 4 additions & 4 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"features": {
"ghcr.io/devcontainers/features/common-utils:2.7.0": {
"version": "2.7.0",
"resolved": "ghcr.io/devcontainers/features/common-utils@sha256:3f63bbee7418dc940d9e5e7ab93ef3049acfe4dba3085b17ee45fe307fdba4e5",
"integrity": "sha256:3f63bbee7418dc940d9e5e7ab93ef3049acfe4dba3085b17ee45fe307fdba4e5"
"ghcr.io/devcontainers/features/common-utils:2.7.1": {
"version": "2.7.1",
"resolved": "ghcr.io/devcontainers/features/common-utils@sha256:e76a6a8a35d1ad824daad1540725e0ec9f8ed2dc00d2850e990a9830013c7d08",
"integrity": "sha256:e76a6a8a35d1ad824daad1540725e0ec9f8ed2dc00d2850e990a9830013c7d08"
},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1.10.1": {
"version": "1.10.1",
Expand Down
3 changes: 2 additions & 1 deletion .github/RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ Push to main

4. **When Release PR is Merged**:
- GitHub Release created with tag (e.g., `v0.2.0`) — first as a **draft**, so every signed
asset (`crds.yaml`, `install.yaml`, SBOM, `.sigstore.json` signatures, `.intoto.jsonl` attestations)
asset (`crds.yaml`, `commitrequest-crd.yaml`, `install.yaml`, SBOM, `.sigstore.json` signatures,
`.intoto.jsonl` attestations)
can be attached before it goes public; immutable releases reject post-publish uploads, so
`publish-release` flips the draft to published only after every asset is in place.
- The linux/amd64 + linux/arm64 image digests already built and scanned by that
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ jobs:
name: release-bundle
path: |
dist/crds.yaml
dist/commitrequest-crd.yaml
dist/install.yaml
gitops-reverser.tgz
if-no-files-found: error
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,11 @@ jobs:
- name: Sign crds.yaml (cosign keyless)
run: cosign sign-blob --bundle crds.yaml.sigstore.json --yes dist/crds.yaml

# The CommitRequest CRD alone, for the source clusters that serve CommitRequests and need
# none of the other kinds. It is part of crds.yaml too; this copy is signed on its own.
- name: Sign commitrequest-crd.yaml (cosign keyless)
run: cosign sign-blob --bundle commitrequest-crd.yaml.sigstore.json --yes dist/commitrequest-crd.yaml

- name: Attest SLSA provenance for install.yaml
id: attest-install-yaml
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
Expand All @@ -372,6 +377,15 @@ jobs:
- name: Rename crds provenance bundle to the Scorecard-recognized suffix
run: cp "${{ steps.attest-crds-yaml.outputs.bundle-path }}" crds.yaml.intoto.jsonl

- name: Attest SLSA provenance for commitrequest-crd.yaml
id: attest-commitrequest-crd
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: dist/commitrequest-crd.yaml

- name: Rename commitrequest-crd provenance bundle to the Scorecard-recognized suffix
run: cp "${{ steps.attest-commitrequest-crd.outputs.bundle-path }}" commitrequest-crd.yaml.intoto.jsonl

- name: Upload installer bundle as release assets
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
Expand All @@ -384,6 +398,9 @@ jobs:
dist/crds.yaml
crds.yaml.sigstore.json
crds.yaml.intoto.jsonl
dist/commitrequest-crd.yaml
commitrequest-crd.yaml.sigstore.json
commitrequest-crd.yaml.intoto.jsonl
dist/install.yaml
install.yaml.sigstore.json
install.yaml.intoto.jsonl
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ loadtest
/external-sources/
/docs/local-only/*
hack/release-notes/node_modules
docs/review/*
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ See [Documentation checks](./CONTRIBUTING.md#documentation-checks).

### Webhook Code (`internal/webhook/`)

- Implement admission webhook interface correctly
- Add proper validation/mutation logic
- Update webhook configuration in `config/webhook/`
- It is the audit ingress and the push receiver: HTTP handlers for what kube-apiserver and Git
hosts post. GitOps Reverser ships no admission webhook; validation lives in the schema, CEL and
the reconciler (`docs/spec/where-validation-lives.md`)

### API Changes (`api/v1alpha3/`)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ commits. If both touch the same resource, the API wins

Follow the [installation walkthrough](docs/quickstart.md) to capture ConfigMaps from a demo namespace
into a disposable repo. You need a Kubernetes cluster, `kubectl`, Helm 3, and Git write access.
The walkthrough includes cert-manager setup; Redis and audit delivery are optional.
Redis, audit delivery and cert-manager are optional: they come in with named commit authors.

The demo captures a live change:

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Security reports are especially relevant for areas such as:
- queue and Valkey/Redis handling
- Git credential handling
- SOPS and Secret processing
- admission webhook validation
- `CommitRequest` admission and serving, including on source clusters

## Supported versions

Expand Down
3 changes: 3 additions & 0 deletions Taskfile-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,7 @@ tasks:
- exclude: cmd/**/zz_generated.deepcopy.go
generates:
- dist/crds.yaml
- dist/commitrequest-crd.yaml
- dist/install.yaml
cmds:
- |
Expand All @@ -784,6 +785,8 @@ tasks:
# here. Each chart CRD file already begins with its own `---`, so a plain cat is valid
# multi-doc YAML.
cat charts/gitops-reverser/crds/*.yaml > dist/crds.yaml
# A source cluster needs only the CommitRequest CRD, so it ships on its own too.
cp charts/gitops-reverser/crds/configbutler.ai_commitrequests.yaml dist/commitrequest-crd.yaml
{{.HELM}} template {{.INSTALL_NAME}} charts/gitops-reverser \
--namespace {{.NAMESPACE}} \
--set labels.managedBy=kubectl \
Expand Down
87 changes: 86 additions & 1 deletion api/v1alpha3/access_grants.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,72 @@ type KubeProviderApplierClaimGrant struct {
ApplierNamespaces []ScopeNamespace `json:"applierNamespaces"`
}

// The API server defaults an omitted grant list on GitBranch and KubeProvider (the
// GitTargetCommitRequestGrant admits the CommitRequests of some namespaces to
// save a GitTarget's commit windows: on the cluster GitOps Reverser runs in
// (consumerNamespaces), or on the cluster the GitTarget mirrors
// (sourceNamespaces). It sets exactly one of the two.
// +kubebuilder:validation:XValidation:rule="has(self.consumerNamespaces) != has(self.sourceNamespaces)",message="a grant in allowedCommitRequests sets exactly one of consumerNamespaces and sourceNamespaces"
type GitTargetCommitRequestGrant struct {
// ConsumerNamespaces are the namespaces, on the cluster GitOps Reverser
// runs in, whose CommitRequests this grant admits. from: "Same" is this
// GitTarget's namespace. It admits requests only for a GitTarget whose
// KubeProvider sets spec.inCluster.
// +optional
ConsumerNamespaces *ConsumerNamespaces `json:"consumerNamespaces,omitempty"`

// SourceNamespaces are the namespaces, on the cluster spec.kubeProviderRef
// names, whose CommitRequests this grant admits: exact names, or "*" for
// every namespace this GitTarget may mirror. Variables are refused. A
// namespace the KubeProvider does not lend this GitTarget in
// spec.allowedGitTargets is never admitted. It admits requests only for a
// GitTarget whose KubeProvider names a kubeconfig.
// +optional
// +listType=set
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=64
SourceNamespaces []SourceNamespace `json:"sourceNamespaces,omitempty"`
}

// AdmitsCommitRequest reports whether spec.allowedCommitRequests admits a CommitRequest in namespace
// of the control cluster, by the labels that namespace carries there: its consumerNamespaces grants.
func (t *GitTarget) AdmitsCommitRequest(namespace string, nsLabels map[string]string) (bool, error) {
admitting, err := admitAll(consumersOf(t.commitRequestGrants()), t.Namespace, namespace, nsLabels)
return len(admitting) > 0, err
}

// DescribeAllowedCommitRequests names the namespaces spec.allowedCommitRequests admits on the control
// cluster.
func (t *GitTarget) DescribeAllowedCommitRequests() string {
grants := t.commitRequestGrants()
consumers := make([]ConsumerNamespaces, 0, len(grants))
for i := range grants {
if grants[i].ConsumerNamespaces != nil {
consumers = append(consumers, *grants[i].ConsumerNamespaces)
}
}
return describeAdmitted(consumers, t.Namespace)
}

// AdmitsSourceCommitRequest reports whether spec.allowedCommitRequests admits a CommitRequest in
// namespace of the cluster this GitTarget mirrors: its sourceNamespaces grants, "*" for any.
func (t *GitTarget) AdmitsSourceCommitRequest(namespace string) bool {
for _, grant := range t.commitRequestGrants() {
if slices.ContainsFunc(grant.SourceNamespaces, func(ns SourceNamespace) bool {
return ns == "*" || string(ns) == namespace
}) {
return true
}
}
return false
}

// SourceNamespace is a namespace of a source cluster, by exact name, or "*"
// for every namespace.
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:XValidation:rule="self == '*' || self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')",message="a source namespace is a namespace name, or '*' alone; variables are refused, because a source cluster's namespaces are not the control cluster's"
type SourceNamespace string

// The API server defaults an omitted grant list on GitBranch, KubeProvider and GitTarget (the
// +kubebuilder:default markers on their fields), so a list read from it is never nil. These mirror
// those markers for an object that did not come from the API server, such as one built in memory or
// read through a fake client: nil is the default, and only a non-nil empty list admits nobody.
Expand Down Expand Up @@ -249,6 +314,12 @@ func DefaultKubeProviderApplierClaimGrants() []KubeProviderApplierClaimGrant {
}}
}

// DefaultGitTargetCommitRequestGrants is GitTarget.spec.allowedCommitRequests when it is omitted:
// CommitRequests in the GitTarget's own namespace.
func DefaultGitTargetCommitRequestGrants() []GitTargetCommitRequestGrant {
return []GitTargetCommitRequestGrant{{ConsumerNamespaces: &ConsumerNamespaces{From: GrantFromSame}}}
}

// orDefault is grants, or def when grants is nil (omitted rather than empty).
func orDefault[G any](grants []G, def func() []G) []G {
if grants == nil {
Expand All @@ -272,6 +343,20 @@ func (p *KubeProvider) applierClaimGrants() []KubeProviderApplierClaimGrant {
return orDefault(p.Spec.AllowedApplierClaims, DefaultKubeProviderApplierClaimGrants)
}

// commitRequestGrants is spec.allowedCommitRequests as the API server serves it.
func (t *GitTarget) commitRequestGrants() []GitTargetCommitRequestGrant {
return orDefault(t.Spec.AllowedCommitRequests, DefaultGitTargetCommitRequestGrants)
}

// consumers is the admitting half of a GitTargetCommitRequestGrant on the control cluster. A grant of
// source namespaces admits nobody there: its zero ConsumerNamespaces has no from.
func (g *GitTargetCommitRequestGrant) consumers() ConsumerNamespaces {
if g.ConsumerNamespaces == nil {
return ConsumerNamespaces{}
}
return *g.ConsumerNamespaces
}

// consumers is the admitting half of a GitProviderGitBranchGrant.
func (g *GitProviderGitBranchGrant) consumers() ConsumerNamespaces { return g.ConsumerNamespaces }

Expand Down
10 changes: 10 additions & 0 deletions api/v1alpha3/access_grants_defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func TestGrantDefaultsMatchCRDs(t *testing.T) {
assertCRDDefault(t, "configbutler.ai_kubeproviders.yaml", "allowedApplierClaims",
DefaultKubeProviderApplierClaimGrants())
})
t.Run("GitTarget allowedCommitRequests", func(t *testing.T) {
assertCRDDefault(t, "configbutler.ai_gittargets.yaml", "allowedCommitRequests",
DefaultGitTargetCommitRequestGrants())
})
}

// assertCRDDefault asserts that the CRD's default for spec.<field> decodes to want.
Expand Down Expand Up @@ -73,6 +77,12 @@ func TestGrantLists_EmptySurvivesSerialization(t *testing.T) {
}
}

raw, err := json.Marshal(GitTargetSpec{AllowedCommitRequests: []GitTargetCommitRequestGrant{}})
require.NoError(t, err)
var targetFields map[string]json.RawMessage
require.NoError(t, json.Unmarshal(raw, &targetFields))
assert.JSONEq(t, "[]", string(targetFields["allowedCommitRequests"]), "GitTarget spec.allowedCommitRequests")

var decoded GitBranchSpec
require.NoError(t, json.Unmarshal([]byte(`{"allowedGitTargets":[]}`), &decoded))
assert.NotNil(t, decoded.AllowedGitTargets, "a decoded [] stays distinguishable from omitted")
Expand Down
30 changes: 30 additions & 0 deletions api/v1alpha3/access_grants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,33 @@ func TestGitBranch_GitProviderKey(t *testing.T) {
branch.Spec.GitProviderRef.Namespace = "platform"
assert.Equal(t, "platform/shared-repo", branch.GitProviderKey().String())
}

// A GitTarget admits a CommitRequest by the side its grant names: consumerNamespaces admit a
// namespace of the control cluster, sourceNamespaces a namespace of the cluster the GitTarget mirrors.
// A grant for one side admits nothing on the other.
func TestGitTarget_AdmitsCommitRequestBySide(t *testing.T) {
target := func(grants ...GitTargetCommitRequestGrant) *GitTarget {
return &GitTarget{ObjectMeta: metav1.ObjectMeta{Namespace: "team-a"}, Spec: GitTargetSpec{
AllowedCommitRequests: grants,
}}
}
source := func(namespaces ...SourceNamespace) GitTargetCommitRequestGrant {
return GitTargetCommitRequestGrant{SourceNamespaces: namespaces}
}

defaulted := &GitTarget{ObjectMeta: metav1.ObjectMeta{Namespace: "team-a"}}
admitted, err := defaulted.AdmitsCommitRequest("team-a", nil)
require.NoError(t, err)
assert.True(t, admitted, "the default admits the GitTarget's own namespace")
assert.False(t, defaulted.AdmitsSourceCommitRequest("team-a"), "the default admits nothing on a source cluster")

assert.False(t, target().AdmitsSourceCommitRequest("shop"), "an empty list admits nobody")
assert.True(t, target(source("*")).AdmitsSourceCommitRequest("shop"))
assert.True(t, target(source("shop")).AdmitsSourceCommitRequest("shop"))
assert.False(t, target(source("cart")).AdmitsSourceCommitRequest("shop"))

admitted, err = target(source("*")).AdmitsCommitRequest("team-a", nil)
require.NoError(t, err)
assert.False(t, admitted, "a source grant admits nothing on the control cluster")
assert.Equal(t, "admits no namespace", target(source("*")).DescribeAllowedCommitRequests())
}
14 changes: 14 additions & 0 deletions api/v1alpha3/applierclaim_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package v1alpha3

import (
"time"

meta "github.com/fluxcd/pkg/apis/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -190,6 +192,10 @@ type ApplierClaim interface {
// false when the applier has none it shares this way or the applier claim has not read it. Appliers
// sharing one have one operation at a time.
SharedSource() (source types.NamespacedName, known bool)
// CatchUpAfter is spec.catchUpAfter, or the kind's default when it is omitted: how long
// the applier may stay behind the branch after a push of GitOps Reverser's own. Zero never
// converges for that reason.
CatchUpAfter() time.Duration
// Operation is status.operation, and SetOperation replaces it and status.repairPending.
Operation() *ApplyOperation
RepairPending() bool
Expand All @@ -214,6 +220,14 @@ func NewApplierClaim(kind string) ApplierClaim {
}
}

// durationOr is d, or fallback when it is omitted.
func durationOr(d *metav1.Duration, fallback time.Duration) time.Duration {
if d == nil {
return fallback
}
return d.Duration
}

// defaultedNamespace is namespace, or fallback when it is empty.
func defaultedNamespace(namespace, fallback string) string {
if namespace == "" {
Expand Down
34 changes: 33 additions & 1 deletion api/v1alpha3/argocdclaim_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package v1alpha3

import (
"strings"
"time"

meta "github.com/fluxcd/pkg/apis/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -54,6 +55,27 @@ type ArgoCDClaimSpec struct {
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec.applicationRef is immutable; delete and recreate the ArgoCDClaim to hold another Application"
// +kubebuilder:validation:XValidation:rule="self.name != ''",message="spec.applicationRef.name must not be empty"
ApplicationRef meta.NamespacedObjectReference `json:"applicationRef"`

// Why off by default, where a FluxClaim's is on: a sync runs the Application's hooks, which
// cost more than a no-op apply, and Argo CD's own refresh already compares the live state
// against the tip without a sync. Only the last sync's revision lags.

// CatchUpAfter is how long the Application stays behind the branch after
// a push of GitOps Reverser's own before a sync of the tip is asked for,
// as a Go duration ("5m"). Such a push changes nothing on the cluster,
// which already holds the edit, but leaves the Application's last sync at
// the commit before it. The interval starts when the Application is first
// seen behind, so a burst of saves costs one sync, hooks included, and one
// at the tip costs none. The sync can still wait for startup, a retry
// backoff or edits being published. "0s" never asks for this reason, and
// drops a sync it asked for that has not started. Otherwise at least "10s"
// and at most "24h". Defaults to "0s".
// +optional
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|μs|ms|s|m|h))+$"
// +kubebuilder:default="0s"
// +kubebuilder:validation:XValidation:rule="duration(self) == duration('0s') || (duration(self) >= duration('10s') && duration(self) <= duration('24h'))",message="spec.catchUpAfter must be 0s, or a Go duration from 10s to 24h"
CatchUpAfter *metav1.Duration `json:"catchUpAfter,omitempty"`
}

// ArgoCDClaimStatus is what the applier claim knows about its Application, and
Expand Down Expand Up @@ -162,7 +184,9 @@ type ArgoCDClaimSource struct {
// - after a refused edit, on a GitTarget with spec.onRefusal "Reapply";
// - after a sync reverted an edit made through the API;
// - after objects were created through the API in its folders;
// - once at startup, and when a GitTarget first names this ArgoCDClaim.
// - once at startup, and when a GitTarget first names this ArgoCDClaim;
// - once spec.catchUpAfter passed since a push of GitOps Reverser's own;
// - after a CommitRequest with spec.catchUp "AfterPush" pushed a commit.
//
// GitTargets name it with spec.applierClaimRef.
type ArgoCDClaim struct {
Expand Down Expand Up @@ -195,6 +219,9 @@ func init() {
SchemeBuilder.Register(&ArgoCDClaim{}, &ArgoCDClaimList{})
}

// DefaultArgoCDCatchUpAfter is an ArgoCDClaim's spec.catchUpAfter when it is omitted: off.
const DefaultArgoCDCatchUpAfter = 0

var _ ApplierClaim = (*ArgoCDClaim)(nil)

// ClaimKind is ArgoCDClaim.
Expand Down Expand Up @@ -235,6 +262,11 @@ func (t *ArgoCDClaim) SharedSource() (types.NamespacedName, bool) {
return types.NamespacedName{}, false
}

// CatchUpAfter is spec.catchUpAfter, or DefaultArgoCDCatchUpAfter when it is omitted.
func (t *ArgoCDClaim) CatchUpAfter() time.Duration {
return durationOr(t.Spec.CatchUpAfter, DefaultArgoCDCatchUpAfter)
}

// Operation is status.operation.
func (t *ArgoCDClaim) Operation() *ApplyOperation { return t.Status.Operation }

Expand Down
Loading
Loading