gitops-responder
When a detection produces an EventResponse whose action.type is
GitOpsChange, ugallu-gitops-responder translates it into a real
PR or MR against the GitOps repo. The responder is the only ugallu
component that talks to a Git provider - every other operator stops
at “emit a CR”.
What it watches
Section titled “What it watches”EventResponse CRs cluster-wide where:
spec.action.type == "GitOpsChange"spec.responder.name == "ugallu-gitops-responder"(so two responders can coexist for different routing)
The router matches the ER against routing rules from
GitOpsResponderConfig (loaded once at boot - no live reload yet)
and dispatches to a provider client.
Providers
Section titled “Providers”| Provider | Auth |
|---|---|
| GitHub | GitHub App (preferred - short-lived installation tokens, fine-grained permissions) or fine-grained PAT. |
| GitLab | Project access token via Secret reference. |
noop | Records the would-be PR in the EventResponse status without calling out - used in dev / test environments and during stack bring-up. |
The current build ships the noop provider end-to-end; GitHub /
GitLab clients are wired but their integration is the focus of an
upcoming sprint.
Idempotency
Section titled “Idempotency”A re-emit of the same EventResponse (same UID) is a no-op once
the PR has been created - the PR URL lives on status.providerRef
and the responder short-circuits when it sees one. A failed
attempt retries up to --max-attempts (default honours the CR’s
spec.maxAttempts).
Example
Section titled “Example”apiVersion: security.ugallu.io/v1alpha1kind: GitOpsResponderConfigmetadata: { name: default, namespace: ugallu-system }spec: routes: - match: actionRef: revoke-clusterrolebinding provider: kind: github-app repository: ninsun-labs/ugallu-gitops appCredentialsRef: name: gitops-responder-github-app installationID: 12345 baseBranch: main prTemplate: | ## Auto-generated by ugallu-gitops-responder
Triggered by `{{ .Source.SE.Type }}` on `{{ .Source.SE.Subject.Name }}`.Internals
Section titled “Internals”State machine
Section titled “State machine”EventResponse (consumed, not owned by this operator):
Pending -> Running -> Succeeded | Failed | Cancelled.
Reconcile loop
Section titled “Reconcile loop”on each EventResponse event: er := Get(req) if er.Spec.Action.Type != "GitOpsChange": return if er.Spec.Responder.Name != "ugallu-gitops-responder": return if er.Status.Phase in {Succeeded, Failed, Cancelled}: return if er.Status.ProviderRef != "": # already opened upstream return route := router.match(er) # boot-time routing rules provider := providers[route.Provider] attempt = er.Status.Attempts + 1 resp, err := provider.Apply(ctx, er, route.Repo) patch Status.Attempts = attempt, LastAttemptAt = now if err && attempt < maxAttempts: patch Status.Phase = Running RequeueAfter: backoff(attempt) return patch Status.Phase = (err ? Failed : Succeeded), ProviderRef = resp.URLError recovery
Section titled “Error recovery”Pod restart mid-Apply: new pod re-Gets the ER (still
Pending/Running), retries provider.Apply(). Git providers
are expected to be idempotent (PRs are upserted by branch +
title, not created fresh). If the previous attempt did open a PR
but the responder crashed before writing ProviderRef, the next
attempt detects the existing PR and writes the URL to the status.
Crash recovery scenario
Section titled “Crash recovery scenario”Pod killed after provider.Apply() succeeded but before
Phase=Succeeded was written: the new pod re-Gets the ER, calls
provider.Apply() again. The provider sees the open PR for this
ER’s logical key and returns its URL; the responder writes
ProviderRef and flips Phase=Succeeded.
Edge cases
Section titled “Edge cases”- Boot-time config load.
GitOpsResponderConfigis read once at startup; live reload is on the roadmap. Restart the Deployment to pick up routing changes. - Two responders. Multiple gitops-responders can coexist by
matching different
EventResponse.Spec.Responder.Namevalues. - noop provider. Useful in dev / test environments and during stack bring-up; records the would-be PR in the ER status without calling out.
- Recreate strategy. The Deployment uses
strategy: Recreatebecause the in-memory router snapshot doesn’t share state across replicas.
Full RBAC
Section titled “Full RBAC”# ClusterRolerules: - apiGroups: [security.ugallu.io] resources: [eventresponses] verbs: [get, list, watch, patch] - apiGroups: [security.ugallu.io] resources: [eventresponses/status] verbs: [update, patch] - apiGroups: [security.ugallu.io] resources: [securityevents, gitopsresponderconfigs] verbs: [get, list, watch] - apiGroups: [""] resources: [secrets] verbs: [get, list, watch] # provider credentials - apiGroups: [""] resources: [events] verbs: [create, patch]# Namespaced Role - apiGroups: [coordination.k8s.io] resources: [leases] verbs: [get, list, watch, create, update, patch, delete]- Watched:
EventResponse(does not own). - Owned: none -
GitOpsResponderConfigis read-only at boot.
Key flags
Section titled “Key flags”--cluster-id, --cluster-name, --config-namespace (default
ugallu-system), --max-attempts (0 = honour CR).
Deployment
Section titled “Deployment”Deployment (1 replica, Recreate strategy because the in-memory
router snapshot doesn’t share state) in ugallu-system, leader
election on, priorityClassName=system-cluster-critical. Reads
provider Secrets only from the configured namespace.
Telemetry
Section titled “Telemetry”ugallu_gitops_responder_dispatches_total{provider,outcome},
ugallu_gitops_responder_attempts_total{outcome},
ugallu_gitops_responder_routing_misses_total.