Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
286 changes: 122 additions & 164 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Taskfile-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ tasks:
--set labels.managedBy=kubectl \
--set createNamespace=true \
--set queue.redis.addr={{.DEFAULT_AUDIT_REDIS_ADDR}} \
--set queue.redis.auth.existingSecret=valkey-auth \
--include-crds \
> dist/install.yaml

Expand Down
34 changes: 19 additions & 15 deletions charts/gitops-reverser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ For the chart's optional starter `quickstart` block, see [`docs/configuration.md

- Kubernetes 1.36 (probably works for older versions, but not tested)
- cert-manager (for TLS certificates)
- **Valkey or Redis** reachable from the controller, with auth enabled (`queue.redis.auth.existingSecret`).
This is **required**: it holds each GitTarget's watch resume state so work is re-picked up after a
restart or reconnect.
- **Optional — attributed mode only:** kube-apiserver audit delivery, which adds commit-author
attribution. Without it the operator still mirrors state, authored by the configured committer.
- **Valkey or Redis: optional but advised.** The default install runs without it in
`configured-author` mode (watches cold-replay on restart). Add it to unlock warm-restart resume
cursors, CommitRequest author capture (the admission webhook), and attributed-author mode; enabling
any of those requires a non-empty `queue.redis.addr`.
- **Optional, attributed-author mode only:** kube-apiserver audit delivery, which adds
mirrored-resource commit-author attribution. Without it the operator still mirrors state, authored by
the configured committer.

## Installation

Expand Down Expand Up @@ -173,19 +175,21 @@ nodeSelector:
| `auditService.nodePort` | Fixed NodePort for the audit Service when `auditService.type=NodePort` | `30444` |
| `auditService.clusterIP` | Optional fixed ClusterIP for the dedicated audit Service | `""` |
| `quickstart.enabled` | Create starter `GitProvider`, `GitTarget`, and `WatchRule` resources | `false` |
| `quickstart.namespace` | Namespace for the starter quickstart resources | `default` |
| `quickstart.namespace` | Namespace for the starter quickstart resources (create it, and the git-creds Secret, before install) | `gitops-reverser-quickstart-demo` |
| `quickstart.createNamespace` | Let the chart create `quickstart.namespace` (Helm then owns it) | `false` |
| `quickstart.gitProvider.url` | Repository URL used by the starter `GitProvider` | `""` |
| `quickstart.gitProvider.secretRef.name` | Existing Secret name used by the starter `GitProvider` | `git-creds` |
| `quickstart.gitTarget.path` | Repository path used by the starter `GitTarget`; set `.` only to deliberately target the repo root | `live-cluster` |
| `quickstart.watchRule.rules` | Rules used by the starter `WatchRule` | `configmaps create/update/delete` |
| `queue.redis.addr` | Redis/Valkey endpoint (`host:port`). **Required** — holds each GitTarget's watch resume cursors (state continuity) and, when audit is configured, the attribution facts | `valkey:6379` |
| `queue.redis.auth.existingSecret` | Name of a pre-created Secret holding the Redis password | `valkey-auth` |
| `queue.redis.addr` | Redis/Valkey endpoint (`host:port`). Optional but advised: empty runs `configured-author` with cold-replay on restart. Set it for warm-restart cursors and for the admission webhook to actually record CommitRequest authors (admission runs as a no-op without it); **required** only when `attribution.enabled=true` | `""` |
| `queue.redis.auth.existingSecret` | Name of a pre-created Secret holding the Redis password (only used when `queue.redis.addr` is set) | `""` |
| `queue.redis.auth.existingSecretKey` | Key within the Secret that holds the password | `password` |
| `queue.redis.auth.username` | Optional Redis ACL username | `""` |
| `queue.redis.tls.enabled` | Enable TLS for Redis connection | `false` |
| `attribution.enabled` | Run audit ingress and name mirrored-resource commit authors from matching kube-apiserver audit facts | `false` |
| `attribution.ttl` | How long an attribution fact is retained waiting for the matching watch event to join it | `10m` |
| `attribution.grace` | Bounded per-event wait for a matching audit fact before a watch event ships as the committer | `3s` |
| `servers.admission.enabled` | Install the validate-operator-types admission webhook that captures CommitRequest authors (a form of author attribution). Enabled by default; a no-op until `queue.redis.addr` is set | `true` |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
| `servers.metrics.bindAddress` | Metrics listener bind address | `:8080` |
| `servers.metrics.tls.enabled` | Serve metrics with TLS | `false` |
| `servers.metrics.tls.certPath` | Metrics TLS certificate mount path | `/tmp/k8s-metrics-server/metrics-server-certs` |
Expand All @@ -194,7 +198,6 @@ nodeSelector:
| `service.ports.audit` | Service port for audit ingress when `attribution.enabled=true` | `9444` |
| `service.ports.metrics` | Service port for metrics | `8080` |
| `servers.healthProbe.bindAddress` | Liveness/readiness probe bind address (`--health-probe-bind-address`) | `:8081` |
| `servers.admission.enabled` | Run the validating admission server hosting the validate-operator-types webhook (captures the CommitRequest submitter as the commit author) | `true` |
| `servers.admission.port` | Admission webhook container/Service port | `9443` |
| `servers.admission.timeoutSeconds` | Admission webhook timeout (failurePolicy is Ignore) | `2` |
| `servers.admission.tls.certManager` | Mint the admission serving cert via cert-manager (false = BYO via `secretNameOverride`) | `true` |
Expand Down Expand Up @@ -222,14 +225,15 @@ See [`values.yaml`](values.yaml) for complete configuration options.
When `attribution.enabled=true`, `https://<service>:9444/audit-webhook` receives audit events from
kube-apiserver. The operator extracts a minimal attribution fact from each (auditID, user, verb,
resourceVersion, GVR, namespace, name, UID, status, timestamps) into the Redis attribution index
(populated only when audit attribution is enabled). The same Redis endpoint also stores each GitTarget's
watch resume cursors — that part is always needed — so reconnects resume a normal watch from the last
processed resourceVersion when the apiserver can still serve that history. Object state itself comes
from Kubernetes **watch**, not from audit; audit only names the commit author.
(populated only when audit attribution is enabled). When a Redis endpoint is configured it also stores
each GitTarget's watch resume cursors, so reconnects resume a normal watch from the last processed
resourceVersion when the apiserver can still serve that history. Object state itself comes from
Kubernetes **watch**, not from audit; audit only names the commit author.

When `attribution.enabled=false`, the audit webhook is not rendered or served and the product runs
committer-only: mirrored-resource commits are authored by the configured committer. `queue.redis.addr`
is still required because Redis/Valkey stores watch resume cursors.
configured-author: mirrored-resource commits are authored by the configured committer. `queue.redis.addr`
is optional here: set it for warm-restart resume cursors, or leave it empty and watches cold-replay
from scratch on restart.

Cluster ID path segments are rejected.

Expand Down
11 changes: 10 additions & 1 deletion charts/gitops-reverser/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,26 @@ What the chart did for you:
- configured the audit listener without TLS (`servers.audit.tls.enabled=false`)
{{- end }}
{{- else }}
- started in committer-only mode (`attribution.enabled=false`)
- started in configured-author mode (`attribution.enabled=false`)
{{- end }}
{{- if .Values.quickstart.enabled }}
{{- if .Values.quickstart.createNamespace }}
- created the demo namespace `{{ .Values.quickstart.namespace }}`
{{- end }}
- created quickstart resources in namespace `{{ .Values.quickstart.namespace }}`
- GitProvider: `{{ .Values.quickstart.gitProvider.name }}`
- GitTarget: `{{ .Values.quickstart.gitTarget.name }}`
- WatchRule: `{{ .Values.quickstart.watchRule.name }}`
{{- end }}

What the chart did NOT do for you:
{{- if .Values.quickstart.enabled }}
- it did NOT create your Git credentials Secret. If `{{ .Values.quickstart.gitProvider.secretRef.name }}` is not
already in namespace `{{ .Values.quickstart.namespace }}`, create it now; the starter GitProvider is re-checked
only about every 5 minutes, so add it promptly to avoid a delayed first commit.
{{- else }}
- it did NOT create your Git credentials Secret
{{- end }}
- it did NOT install Valkey/Redis
{{- if .Values.attribution.enabled }}
- it did NOT write `/etc/kubernetes/...` files on your control-plane node(s)
Expand Down
2 changes: 1 addition & 1 deletion charts/gitops-reverser/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
{{- if .Values.queue.redis.auth.existingSecret }}
{{- if and .Values.queue.redis.addr .Values.queue.redis.auth.existingSecret }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
Expand Down
10 changes: 10 additions & 0 deletions charts/gitops-reverser/templates/quickstart.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
{{- if .Values.quickstart.enabled }}
{{- if .Values.quickstart.createNamespace }}
---
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.quickstart.namespace }}
labels:
{{- include "gitops-reverser.labels" . | nindent 4 }}
app.kubernetes.io/component: quickstart
{{- end }}
---
apiVersion: configbutler.ai/v1alpha3
kind: GitProvider
Expand Down
9 changes: 9 additions & 0 deletions charts/gitops-reverser/templates/validate-redis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{- /*
Attribution (attributed-author mode) reads and writes audit facts in Redis, so it cannot run
without an endpoint — the controller would fail at startup. Fail the render early with an
actionable message instead. The admission webhook is deliberately NOT gated here: it stays
enabled without Redis and simply no-ops command-author capture.
*/ -}}
{{- if and .Values.attribution.enabled (eq (trim .Values.queue.redis.addr) "") -}}
{{- fail "attribution.enabled=true requires queue.redis.addr: attributed-author mode stores audit facts in Redis. Set queue.redis.addr, or leave attribution.enabled=false." -}}
{{- end -}}
38 changes: 25 additions & 13 deletions charts/gitops-reverser/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ servers:
# *command* authorship (from admission). Off → CommitRequests commit as the configured
# committer and report AuthorAttributed=False. See
# docs/design/commitrequest-admission-authorship.md.
#
# Command-author capture is Redis-backed: the captured author is written to Redis at admission and
# read back at reconcile. Enabled by default so the webhook is always installed, but it is a no-op
# without Redis: the pod runs healthy and CommitRequests commit as the configured committer
# (AuthorAttributed=False) until queue.redis.addr is set.
admission:
enabled: true
bindAddress: ":9443"
Expand Down Expand Up @@ -161,17 +166,19 @@ servers:
secretNameOverride: ""

# Redis/Valkey holds each GitTarget's watch resume cursors (warm restart) and, when attribution is
# enabled, the audit attribution facts. Optional in committer-only mode: leave addr empty to run without
# Redis (watches cold-replay on restart). Required when attribution.enabled is true.
# enabled, the audit attribution facts. Empty by default: the chart runs in configured-author mode with
# no Redis (watches cold-replay on restart). Set addr to enable warm restarts; required when
# attribution.enabled is true.
queue:
redis:
# Redis/Valkey endpoint (host:port). Leave empty to run without Redis in committer-only mode;
# required when attribution.enabled is true.
addr: "valkey:6379"
# Redis/Valkey endpoint (host:port). Empty by default (no Redis, configured-author mode). Set to a
# host:port for warm restarts; required when attribution.enabled is true.
addr: ""
auth:
# Pre-existing Secret (same namespace) holding the Redis password. Create it before install;
# leave empty only for dev clusters where Valkey runs without authentication.
existingSecret: "valkey-auth"
# Pre-existing Secret (same namespace) holding the Redis password. Empty by default (no Redis).
# Set it when queue.redis.addr points at an authenticated Valkey; leave empty for a Valkey with
# no auth.
existingSecret: ""
# Key within the Secret that holds the password value.
existingSecretKey: "password"
# Optional Redis username for ACL-based auth (Redis 6+). Not sensitive; stays as a plain value.
Expand All @@ -182,11 +189,11 @@ queue:

# Commit-author attribution from audit facts. Off by default so first-time installs can prove the
# Kubernetes-to-Git workflow without kube-apiserver audit webhook configuration. With it off, the
# operator runs committer-only (Redis optional — leave queue.redis.addr empty to run without it), and every mirrored-resource commit uses the
# operator runs configured-author (Redis optional — leave queue.redis.addr empty to run without it), and every mirrored-resource commit uses the
# configured committer.
attribution:
# When true, run the audit webhook ingress and name commit authors from matching audit facts; requires
# a non-empty queue.redis.addr. When false, run committer-only (no audit ingress, commits authored by
# a non-empty queue.redis.addr. When false, run configured-author (no audit ingress, commits authored by
# the committer); Redis is optional (leave queue.redis.addr empty to run without it).
enabled: false
# How long an attribution fact is retained waiting for the matching watch event to join it.
Expand Down Expand Up @@ -275,9 +282,14 @@ auditKubeconfig:
# you explicitly opt in.
quickstart:
enabled: false
# Namespace for the starter GitProvider, GitTarget, and WatchRule.
# This namespace must already exist.
namespace: default
# Namespace for the starter GitProvider, GitTarget, and WatchRule. Create it (and the git-creds
# Secret) before installing, so the starter GitProvider is Ready on the first reconcile instead of
# waiting up to ~5 minutes (RequeueSteadyInterval) for the Secret to appear.
namespace: gitops-reverser-quickstart-demo
# Let the chart create quickstart.namespace (Helm then owns it). Left false by default because the
# recommended flow pre-creates the namespace and git-creds Secret before install; set true only if
# the credentials Secret will be supplied some other way.
createNamespace: false
gitProvider:
name: example-provider
# Required when quickstart.enabled=true.
Expand Down
44 changes: 28 additions & 16 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func main() {

// Optional author attribution. When enabled, the attribution index is built on the Redis connection,
// the audit webhook records minimal facts, and live watch events are author-attributed when a fact
// matches. When disabled (committer-only), no attribution index exists and commits use the configured
// matches. When disabled (configured-author), no attribution index exists and commits use the configured
// committer identity; the cursor store above is unaffected.
var (
auditRunnable *auditServerRunnable
Expand Down Expand Up @@ -228,10 +228,11 @@ func main() {
setupLog.Info("author attribution enabled: matched audit facts name the commit author",
"redisAddr", cfg.redisAddr, "grace", cfg.attributionGrace.String())
case cfg.redisAddr != "":
setupLog.Info("committer-only mode: author attribution disabled; commits use the configured "+
setupLog.Info("configured-author mode: author attribution disabled; commits use the configured "+
"committer identity", "redisAddr", cfg.redisAddr)
default:
setupLog.Info("committer-only mode: no Redis configured; attribution disabled, watches cold-replay on restart")
setupLog.Info("configured-author mode: no Redis configured; attribution disabled, " +
"watches cold-replay on restart")
}

// Setup watch manager (must be after controllers are set up)
Expand Down Expand Up @@ -268,10 +269,16 @@ func main() {
var commandAuthorStore *queue.CommandAuthorStore
var commandAuthorLookup controller.CommandAuthorLookup
if cfg.admissionWebhookEnabled {
commandAuthorStore = redisStore.CommandAuthorStore()
commandAuthorLookup = commandAuthorStore
setupLog.Info("validate-operator-types webhook enabled: command submitters are captured at admission " +
"and named as the commit author")
if redisStore != nil {
commandAuthorStore = redisStore.CommandAuthorStore()
commandAuthorLookup = commandAuthorStore
setupLog.Info("validate-operator-types webhook enabled: command submitters are captured at admission " +
"and named as the commit author")
} else {
setupLog.Info("validate-operator-types webhook enabled without Redis: command author capture is a " +
"no-op; CommitRequests commit as the configured committer (AuthorAttributed=False). " +
"Set --redis-addr to capture command authors.")
}
}
if err := (&controller.CommitRequestReconciler{
Client: mgr.GetClient(),
Expand All @@ -288,7 +295,7 @@ func main() {
}
// +kubebuilder:scaffold:builder

// Cert watchers (auditCertWatcher is nil in committer-only mode / --audit-insecure).
// Cert watchers (auditCertWatcher is nil in configured-author mode / --audit-insecure).
addCertWatchersToManager(mgr, metricsCertWatcher, auditCertWatcher)

// Health checks: readiness reflects the audit ingress preconditions when attribution is on,
Expand Down Expand Up @@ -411,7 +418,7 @@ func parseFlagsWithArgs(fs *flag.FlagSet, args []string) (appConfig, error) {
fs.BoolVar(&cfg.authorAttribution, "author-attribution", true,
"Name the real actor (human or service account) who caused each change as the Git commit author, "+
"resolved from matching audit facts; this runs the audit webhook ingress (default true). When "+
"false, every commit is authored by the configured committer identity (committer-only mode).")
"false, every commit is authored by the configured committer identity (configured-author mode).")
fs.StringVar(&cfg.redisUsername, "redis-username", "", "Optional Redis username.")
fs.StringVar(
&cfg.redisPassword,
Expand Down Expand Up @@ -519,11 +526,11 @@ func validateAuditConfig(cfg appConfig) error {
if cfg.authorAttribution {
return errors.New("redis-addr is required when author-attribution is enabled")
}
// Committer-only mode with no Redis: watches cold-replay on restart, attribution is off.
// Configured-author mode with no Redis: watches cold-replay on restart, attribution is off.
return nil
}
if !cfg.authorAttribution {
// Committer-only mode with Redis configured: the audit ingress server is not started, so its
// Configured-author mode with Redis configured: the audit ingress server is not started, so its
// server/TLS settings are irrelevant.
return nil
}
Expand Down Expand Up @@ -552,10 +559,9 @@ func validateAdmissionWebhookConfig(cfg appConfig) error {
if !cfg.admissionWebhookEnabled {
return nil
}
if strings.TrimSpace(cfg.redisAddr) == "" {
return errors.New("redis-addr is required when the admission webhook is enabled: " +
"command authorship requires Redis")
}
// No redis-addr requirement here: the admission webhook stays enabled without Redis and
// simply no-ops command-author capture (commits fall back to the committer). Redis is
// wired only when configured; see the commandAuthorStore setup.
if _, _, err := splitBindAddress(cfg.admissionWebhookBindAddress); err != nil {
return fmt.Errorf("invalid admission-webhook-bind-address %q: %w", cfg.admissionWebhookBindAddress, err)
}
Expand Down Expand Up @@ -890,9 +896,15 @@ func setupAdmissionWebhooks(mgr ctrl.Manager, commandAuthorStore *queue.CommandA
webhookhandler.ValidateAllPath,
&ctrladmission.Webhook{Handler: webhookhandler.AdmissionAllowHandler{}},
)
// Leave Store as a nil interface when there is no Redis-backed store, so the handler
// no-ops rather than dereferencing a typed-nil *CommandAuthorStore.
operatorTypesHandler := &webhookhandler.ValidateOperatorTypesHandler{}
if commandAuthorStore != nil {
operatorTypesHandler.Store = commandAuthorStore
}
mgr.GetWebhookServer().Register(
webhookhandler.ValidateOperatorTypesPath,
&ctrladmission.Webhook{Handler: &webhookhandler.ValidateOperatorTypesHandler{Store: commandAuthorStore}},
&ctrladmission.Webhook{Handler: operatorTypesHandler},
)
}

Expand Down
Loading