Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c7230c3
docs(design): add gitops-api's ask to wake the reconciler only on for…
sunib Oct 16, 2026
b10c9cd
docs(design): answer #29: GitOps Reverser takes over the reconciliati…
sunib Oct 16, 2026
6e700ec
docs: describe GitOps Reverser owning the apply trigger, ahead of the…
sunib Oct 16, 2026
75eac7a
feat!: GitOps Reverser decides when Flux applies a folder
sunib Oct 16, 2026
ee94924
test(e2e): count the refusal spec's empty commits from a baseline, an…
sunib Oct 16, 2026
11ec79c
fix(git): restart the apply trigger's quiet period on every decided w…
sunib Oct 16, 2026
83a5e60
docs: describe onRefusal: Reapply in the API-first publication guide
sunib Oct 16, 2026
2e5f801
docs(design): record the apply trigger's follow-ups
sunib Oct 16, 2026
65621bd
docs: match the apply trigger docs to what the code does
sunib Oct 16, 2026
eb9dc03
docs(design): plan the review fixes before the apply trigger merges
sunib Oct 16, 2026
994d92b
fix(applier): check every apply request again at the patch
sunib Oct 16, 2026
3e130d5
docs: narrow the refusal promise and record the review fixes
sunib Oct 16, 2026
cf1485c
fix(applier): keep status.applier per GitTarget incarnation
sunib Oct 16, 2026
afd7e83
test(e2e): prove the Flux applier's source check, hand-back and receiver
sunib Oct 16, 2026
e3c8df4
test(e2e): prove the refresher alone gets a Git-side push applied
sunib Oct 16, 2026
ffe9503
docs: add the Flux user guide, with its example held to the code
sunib Oct 16, 2026
0847f82
refactor(applier): the branch worker owns every apply obligation
sunib Oct 16, 2026
cdb0d21
test: drop the empty-commit trick and repoint design citations
sunib Oct 16, 2026
483adc5
docs: a fresh apply trigger design, and the aim restored
sunib Oct 16, 2026
84a76c9
fix(applier): never stamp an older commit over a newer push
sunib Oct 16, 2026
27dd081
fix(git): a late notice for our own push no longer cuts a save short
sunib Oct 16, 2026
af4bb65
fix(receiver): accept GitHub's form-encoded webhooks
sunib Oct 16, 2026
537a90f
docs: the stamp rule, staleness, and where the apply trigger goes next
sunib Oct 16, 2026
c326ae5
test(e2e): share one Flux applier helper between the Flux specs
sunib Oct 16, 2026
235ef07
fix(receiver): derive the route from the token, and answer every miss…
sunib Oct 17, 2026
c6e1da7
feat(applier): find the applier through its own ClusterProvider
sunib Oct 17, 2026
e24d242
feat(applier): the API wins an edit made while Flux applies
sunib Oct 17, 2026
8fbc019
docs(design): plan the Argo CD applier, with automated sync off as th…
sunib Oct 17, 2026
3de4908
docs(design): record the Argo CD applier decisions
sunib Oct 17, 2026
c5b1f0c
feat(api): name an Argo CD Application as a GitTarget's applier
sunib Oct 17, 2026
e2cb566
feat(applier): ask an Argo CD Application to sync a commit the worker…
sunib Oct 17, 2026
16bc098
feat(controller): check an Argo CD applier, grant its RBAC, and docum…
sunib Oct 17, 2026
cc49a4d
test(e2e): drive the Argo CD corner under the standard, automated syn…
sunib Oct 17, 2026
b295a97
docs: the architecture overview and index name the Argo CD applier
sunib Oct 17, 2026
bedeb2e
test(e2e): ask the GitTarget to reconcile before reading status.applier
sunib Oct 17, 2026
6efbf4d
docs(argocd): list the requirements, and what not to do with Argo CD
sunib Oct 17, 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 @@
79.9
80.0
2 changes: 2 additions & 0 deletions .docs-lint-scope
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
README.md
docs/architecture.md
docs/configuration.md
docs/flux.md
docs/argocd.md
docs/spec/attribution.md
docs/components.md
docs/quickstart.md
Expand Down
72 changes: 72 additions & 0 deletions api/v1alpha3/gitprovider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
package v1alpha3

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"

meta "github.com/fluxcd/pkg/apis/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
Expand Down Expand Up @@ -54,6 +58,68 @@ type GitProviderSpec struct {
// formatting moved to GitTarget.spec.commit.message.
// +optional
Commit *CommitSpec `json:"commit,omitempty"`

// The names are Flux's Receiver's, type values included, so a Flux user already knows them.

// Receiver lets the Git host tell GitOps Reverser that somebody pushed. Point the host's push
// webhook at the receiver listener plus status.webhookPath, with the Secret's token as the
// webhook secret. A delivery is only a hint: it makes GitOps Reverser read where the branch is
// on the remote, and what the remote says decides whether a GitTarget's applier is asked to
// apply. Without a receiver, a foreign push is still found, by the next rejected push or by
// the periodic refresh.
// +optional
Receiver *GitProviderReceiver `json:"receiver,omitempty"`
}

// GitProviderReceiver is the push-notification endpoint of a GitProvider.
type GitProviderReceiver struct {
// Type is the payload and signature the Git host sends. "github" checks X-Hub-Signature-256,
// "gitlab" checks X-Gitlab-Token, "gitea" checks X-Gitea-Signature, and "generic-hmac" checks
// X-Signature: sha256=<hex HMAC of the body> over a {"repository", "branch", "after"} body,
// for a relay of your own.
// +required
// +kubebuilder:validation:Enum=github;gitlab;gitea;generic-hmac
Type ReceiverType `json:"type"`

// SecretRef names a Secret in this namespace whose "token" key is the webhook secret. The
// token also derives status.webhookPath, so rotating it moves the path: register the new path
// with the new secret.
// +required
SecretRef meta.LocalObjectReference `json:"secretRef"`
}

// ReceiverType is the payload format a receiver accepts. The values are Flux's Receiver types.
type ReceiverType string

const (
// ReceiverTypeGitHub is a GitHub push event.
ReceiverTypeGitHub ReceiverType = "github"
// ReceiverTypeGitLab is a GitLab push event.
ReceiverTypeGitLab ReceiverType = "gitlab"
// ReceiverTypeGitea is a Gitea push event.
ReceiverTypeGitea ReceiverType = "gitea"
// ReceiverTypeGenericHMAC is a {"repository", "branch", "after"} body signed as Flux's
// generic-hmac Receiver expects.
ReceiverTypeGenericHMAC ReceiverType = "generic-hmac"
)

// ReceiverTokenKey is the Secret key holding a receiver's token, the key a Flux Receiver Secret uses.
const ReceiverTokenKey = "token"

// ReceiverPathPrefix is the prefix every receiver path starts with.
const ReceiverPathPrefix = "/git-push/"

// A path that names the GitProvider would let anybody who can reach the listener list which
// namespaces and providers exist. Like Flux's Receiver, the path is derived from the token, so
// only whoever holds the token can find it; unlike Flux's sha256(token+name+namespace), it is an
// HMAC over "namespace/name", which no two providers sharing a token can collide on.

// ReceiverPath is the path a GitProvider's receiver is served on: the prefix and the hex
// HMAC-SHA256 of "<namespace>/<name>", keyed by the receiver token.
func ReceiverPath(token []byte, namespace, name string) string {
mac := hmac.New(sha256.New, token)
mac.Write([]byte(namespace + "/" + name))
return ReceiverPathPrefix + hex.EncodeToString(mac.Sum(nil))
}

// KnownHostsReference points at a namespace-local ConfigMap or Secret that holds SSH known_hosts
Expand Down Expand Up @@ -166,6 +232,12 @@ type GitProviderStatus struct {
// that cadence. Providers are few and the check was already running; this adds no request.
// +optional
LastVerifiedAt *metav1.Time `json:"lastVerifiedAt,omitempty"`

// WebhookPath is the path spec.receiver is served on, under the receiver listener:
// /git-push/ and a hash of this provider's name and the receiver token. Empty without
// spec.receiver, and while the token Secret or its "token" key is missing.
// +optional
WebhookPath string `json:"webhookPath,omitempty"`
}

// GitProviderBranchStatus is one branch of this repository that GitTargets are configured to
Expand Down
242 changes: 195 additions & 47 deletions api/v1alpha3/gittarget_types.go

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions api/v1alpha3/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,21 @@ func TestIsLocalSource_IsANameTest(t *testing.T) {
})
}
}

// TestApplierCluster_DefaultsToTheSourceCluster: an omitted spec.applier.clusterProviderRef is the
// GitTarget's own source cluster, never the "default" provider, so a remote-source target keeps its
// meaning when the field is added to the API.
func TestApplierCluster_DefaultsToTheSourceCluster(t *testing.T) {
target := &GitTarget{Spec: GitTargetSpec{
ClusterProviderRef: &meta.LocalObjectReference{Name: "prod-eu-1"},
Applier: &GitTargetApplier{},
}}
assert.Equal(t, "prod-eu-1", target.ApplierCluster())

target.Spec.Applier.ClusterProviderRef = &meta.LocalObjectReference{Name: "hub"}
assert.Equal(t, "hub", target.ApplierCluster())

target.Spec.ClusterProviderRef = nil
target.Spec.Applier.ClusterProviderRef = nil
assert.Equal(t, DefaultClusterProviderName, target.ApplierCluster())
}
85 changes: 85 additions & 0 deletions api/v1alpha3/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions charts/gitops-reverser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ nodeSelector:
| `controllerManager.branchBufferMaxSize` | Maximum in-memory event buffer per branch worker. Bounds the open commit window plus the writes retained for replay until a push succeeds. While the remote cannot be reached, retained writes that reach it close the worker to new writes, saves and resyncs until they are published; the watch delivers a refused event again | `8Mi` |
| `controllerManager.commitRequestTTL` | How long a `CommitRequest` is kept once it finishes. Written onto the request as the `configbutler.ai/delete-after` annotation; remove or edit that to keep one. `"0"` writes none | `48h` |
| `git.refreshInterval` | How often an idle `GitTarget` re-proves where its branch is on the Git remote. `"0"` disables it | `10m` |
| `receiver.bindAddress` | `host:port` the push receiver listens on, over plain HTTP (`--receiver-bind-address`), for Git hosts' push webhooks to `GitProvider.status.webhookPath`. Empty or `"0"` serves no receiver. When set, the chart adds a `<release>-receiver` Service; expose it with an Ingress or Gateway route of your own that terminates TLS. See [push receiver settings](../../docs/configuration.md#push-receiver-settings) | `""` |
| `applierAccess.flux` | Grant the operator `get` and `patch` on Flux `Kustomizations` and `GitRepositories`, and `create` on `SelfSubjectAccessReviews`, so it can ask Flux to apply a commit somebody else pushed | `false` |
| `applierAccess.argocd` | Grant the operator `get` and `update` on Argo CD `Applications`, and `create` on `SelfSubjectAccessReviews`, so it can start the syncs of an `Application` named by `spec.applier.applicationRef` | `false` |
| `auditService.type` | Service type for the dedicated audit Service | `NodePort` |
| `auditService.nodePort` | Fixed NodePort for the audit Service when `auditService.type=NodePort` | `30444` |
| `auditService.clusterIP` | Optional fixed ClusterIP for the dedicated audit Service | `""` |
Expand Down Expand Up @@ -220,6 +223,7 @@ nodeSelector:
| `servers.metrics.tls.secretNameOverride` | Override Secret name for metrics TLS cert/key | `<release>-metrics-server-cert` |
| `service.clusterIP` | Optional fixed ClusterIP for single controller Service | `""` |
| `service.ports.audit` | Service port for audit ingress when `attribution.enabled=true` | `9444` |
| `service.ports.receiver` | Port of the `<release>-receiver` Service when `receiver.bindAddress` is set | `80` |
| `service.ports.metrics` | Service port for metrics | `8080` |
| `servers.healthProbe.bindAddress` | Liveness/readiness probe bind address (`--health-probe-bind-address`) | `:8081` |
| `servers.admission.port` | Admission webhook container/Service port | `9443` |
Expand Down
24 changes: 24 additions & 0 deletions charts/gitops-reverser/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,27 @@ as the Certificate name and in the webhook's cert-manager.io/inject-ca-from anno
{{- define "gitops-reverser.admissionServerCertName" -}}
{{- printf "%s-admission-server-cert" (include "gitops-reverser.fullname" .) -}}
{{- end }}

{{/*
Non-empty when the push receiver listener is on: receiver.bindAddress is set and is not the "0"
the binary reads as off. Used as `if include ...`, so it renders "true" or nothing.
*/}}
{{- define "gitops-reverser.receiverEnabled" -}}
{{- $addr := .Values.receiver.bindAddress | default "" | toString | trim -}}
{{- if and $addr (ne $addr "0") -}}true{{- end -}}
{{- end }}

{{/*
The port in receiver.bindAddress ("host:port"; an empty host or an IPv6 "[::]" host included), so
the containerPort and the Service's targetPort cannot drift from what the binary binds.
*/}}
{{- define "gitops-reverser.receiverPort" -}}
{{- splitList ":" (.Values.receiver.bindAddress | toString | trim) | last | int -}}
{{- end }}

{{/*
Name of the push receiver Service.
*/}}
{{- define "gitops-reverser.receiverServiceName" -}}
{{- printf "%s-receiver" (include "gitops-reverser.fullname" .) | trunc 63 | trimSuffix "-" }}
{{- end }}
93 changes: 93 additions & 0 deletions charts/gitops-reverser/templates/applier-rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{{- if and .Values.rbac.create .Values.applierAccess.flux }}
# What GitOps Reverser needs to ask Flux to apply a commit after somebody else pushed to a branch a
# GitTarget writes: read and patch the Kustomization and its GitRepository (an apply request is a
# reconcile request on them), and create SelfSubjectAccessReviews so the operator can check it holds
# that access before relying on it. Separate from the manager role because it is optional and
# names another project's API: an install without Flux grants nothing on it. See
# docs/design/apply-trigger.md.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "gitops-reverser.fullname" . }}-applier-flux
labels:
{{- include "gitops-reverser.labels" . | nindent 4 }}
rules:
- apiGroups:
- kustomize.toolkit.fluxcd.io
resources:
- kustomizations
verbs:
- get
- patch
- apiGroups:
- source.toolkit.fluxcd.io
resources:
- gitrepositories
verbs:
- get
- patch
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- apiGroups:
- authorization.k8s.io
resources:
- selfsubjectaccessreviews
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "gitops-reverser.fullname" . }}-applier-flux
labels:
{{- include "gitops-reverser.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "gitops-reverser.fullname" . }}-applier-flux
subjects:
- kind: ServiceAccount
name: {{ include "gitops-reverser.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}
{{- if and .Values.rbac.create .Values.applierAccess.argocd }}
---
# What GitOps Reverser needs to ask an Argo CD Application to sync a commit it checked: read the
# Application and update it with a sync operation (how Argo CD's own automated sync starts one), and
# create SelfSubjectAccessReviews so the operator can check it holds that access before relying on
# it. Separate from the manager role because it is optional and names another project's API: an
# install without Argo CD grants nothing on it. See docs/argocd.md.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "gitops-reverser.fullname" . }}-applier-argocd
labels:
{{- include "gitops-reverser.labels" . | nindent 4 }}
rules:
- apiGroups:
- argoproj.io
resources:
- applications
verbs:
- get
- update
- apiGroups:
- authorization.k8s.io
resources:
- selfsubjectaccessreviews
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "gitops-reverser.fullname" . }}-applier-argocd
labels:
{{- include "gitops-reverser.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "gitops-reverser.fullname" . }}-applier-argocd
subjects:
- kind: ServiceAccount
name: {{ include "gitops-reverser.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}
Loading
Loading