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
1 change: 1 addition & 0 deletions charts/gitops-reverser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ nodeSelector:
| `servers.audit.timeouts.idle` | Audit-server idle timeout | `60s` |
| `servers.audit.tls.secretNameOverride` | Override Secret name for audit TLS cert/key | `<release>-audit-server-cert` |
| `controllerManager.additionalSensitiveResources` | Extra Secret-shaped resource types encrypted as `resource` or `group/resource` | `[]` |
| `controllerManager.allowInsecureGitHTTP` | Permit credentials with `http://` GitProvider URLs for trusted in-cluster development Git servers | `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
3 changes: 3 additions & 0 deletions charts/gitops-reverser/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ spec:
{{- with .Values.controllerManager.additionalSensitiveResources }}
- {{ printf "--additional-sensitive-resources=%s" (join "," .) | quote }}
{{- end }}
{{- if .Values.controllerManager.allowInsecureGitHTTP }}
- --allow-insecure-git-http
{{- end }}
{{- if .Values.logging.level }}
- --zap-log-level={{ .Values.logging.level }}
{{- end }}
Expand Down
4 changes: 4 additions & 0 deletions charts/gitops-reverser/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@
"type": "array",
"items": { "type": "string" },
"description": "Resource or group/resource, e.g. core.cozystack.io/tenantsecrets."
},
"allowInsecureGitHTTP": {
"type": "boolean",
"description": "Permit credentials with http:// GitProvider URLs for trusted in-cluster development Git servers."
}
}
},
Expand Down
3 changes: 3 additions & 0 deletions charts/gitops-reverser/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ controllerManager:
# Extra Secret-shaped resources that must use the encrypted Git write path.
# Entries are resource or group/resource, for example core.cozystack.io/tenantsecrets.
additionalSensitiveResources: []
# Permit credentials with http:// GitProvider URLs. This is only for trusted
# in-cluster development Git servers; production remotes should use HTTPS or SSH.
allowInsecureGitHTTP: false

# cert-manager issuer shared by every certificate the chart mints. One self-signed CA
# backs them all, so the issuer is genuinely cross-cutting and lives here; each server
Expand Down
13 changes: 9 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ func main() {
cfg.sensitiveResources,
)
workerManager.SetSSHHostKeyConfig(cfg.sshHostKeys)
workerManager.SetCredentialTransportPolicy(cfg.credentialPolicy)
fatalIfErr(mgr.Add(workerManager), "unable to add worker manager to manager")

// Watch ingestion manager (placeholder, will get EventRouter set later)
Expand Down Expand Up @@ -315,10 +316,11 @@ func main() {
fatalIfErr(mgr.Add(watchMgr), "unable to add watch ingestion manager")

if err := (&controller.GitProviderReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
SSHHostKeys: cfg.sshHostKeys,
Recorder: mgr.GetEventRecorderFor("gitprovider"),
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
SSHHostKeys: cfg.sshHostKeys,
CredentialPolicy: cfg.credentialPolicy,
Recorder: mgr.GetEventRecorderFor("gitprovider"),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "GitProvider")
os.Exit(1)
Expand Down Expand Up @@ -453,6 +455,7 @@ type appConfig struct {
branchBufferMaxBytes int64
sensitiveResources types.SensitiveResourcePolicy
sshHostKeys git.SSHHostKeyConfig
credentialPolicy git.CredentialTransportPolicy
// sourceClusterQPS / sourceClusterBurst bound the rate at which the operator talks to a
// source cluster reached through a GitTarget.spec.kubeConfig. A remote is reached over a
// network the in-cluster config is not, so it carries client-side throttling by default.
Expand Down Expand Up @@ -635,6 +638,8 @@ func parseFlagsWithArgs(fs *flag.FlagSet, args []string) (appConfig, error) {
fs.BoolVar(&cfg.sshHostKeys.AllowMissingKnownHosts, "insecure-allow-missing-known-hosts", false,
"INSECURE, dev/throwaway clusters only: permit SSH when no host-key source produced any "+
"known_hosts at all. A present-but-unparseable known_hosts is always a hard error.")
fs.BoolVar(&cfg.credentialPolicy.AllowInsecureGitHTTP, "allow-insecure-git-http", false,
"INSECURE, dev/throwaway clusters only: permit credentials with http:// GitProvider URLs.")
cfg.zapOpts = zap.Options{
// Production mode defaults to JSON encoding, which is easier for log processors to parse.
Development: false,
Expand Down
10 changes: 10 additions & 0 deletions cmd/main_audit_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func TestParseFlagsWithArgs_Defaults(t *testing.T) {
assert.Equal(t, 3*time.Second, cfg.attributionGrace)
assert.False(t, cfg.zapOpts.Development)
assert.Equal(t, []string{"secrets"}, cfg.sensitiveResources.Entries())
assert.False(t, cfg.credentialPolicy.AllowInsecureGitHTTP)
}

func TestParseFlagsWithArgs_AdmissionWebhookValues(t *testing.T) {
Expand Down Expand Up @@ -85,6 +86,15 @@ func TestParseFlagsWithArgs_AuditUnsecure(t *testing.T) {
assert.True(t, cfg.auditInsecure)
}

func TestParseFlagsWithArgs_InsecureCredentialedGitHTTP(t *testing.T) {
fs := flag.NewFlagSet("test-insecure-git-http", flag.ContinueOnError)

cfg, err := parseFlagsWithArgs(fs, []string{"--allow-insecure-git-http"})

require.NoError(t, err)
assert.True(t, cfg.credentialPolicy.AllowInsecureGitHTTP)
}

func TestParseFlagsWithArgs_CustomAuditValues(t *testing.T) {
fs := flag.NewFlagSet("test-custom", flag.ContinueOnError)
args := []string{
Expand Down
8 changes: 8 additions & 0 deletions cmd/main_chart_args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func TestChartRendersArgsTheBinaryAccepts(t *testing.T) {
wantAttribution bool
wantAuditTLSCert string
wantHTTP2 bool
wantGitHTTP bool
}{
"chart defaults": {
wantKeyPrefix: "gitops-reverser",
Expand Down Expand Up @@ -118,6 +119,11 @@ func TestChartRendersArgsTheBinaryAccepts(t *testing.T) {
wantRedisAddr: "redis.example.com:6379",
wantAttribution: true,
},
"credentialed git http opt-in": {
setValues: []string{"controllerManager.allowInsecureGitHTTP=true"},
wantKeyPrefix: "gitops-reverser",
wantGitHTTP: true,
},
}

for name, tc := range tests {
Expand All @@ -142,6 +148,8 @@ func TestChartRendersArgsTheBinaryAccepts(t *testing.T) {
}
require.Equal(t, tc.wantHTTP2, cfg.enableHTTP2,
"servers.enableHTTP2 must reach the binary, and default to off")
require.Equal(t, tc.wantGitHTTP, cfg.credentialPolicy.AllowInsecureGitHTTP,
"controllerManager.allowInsecureGitHTTP must reach the binary")
})
}
}
2 changes: 2 additions & 0 deletions config/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ spec:
# Dev/e2e only: the in-cluster Gitea SSH host key is not pinned, so permit SSH
# when no known_hosts source exists. Never set this in production.
- --insecure-allow-missing-known-hosts
# Dev/e2e only: the in-cluster Gitea serves Git over plain HTTP. Never set this in production.
- --allow-insecure-git-http
command:
- /manager
env:
Expand Down
3 changes: 2 additions & 1 deletion docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,11 @@ decided), **design, decided** (decision made, not built), **partly built**, **bu
**deferred** (parked, kept as a decision record). The label is the first thing in the page,
so you never have to read a proposal to find out it already shipped.

### Open — 15 pages
### Open — 16 pages

| Doc | Open question |
|---|---|
| [`gittarget-red-status-plan.md`](design/gittarget-red-status-plan.md) | **design, partly built.** The smaller alternative to Git write preflight: make a `GitTarget` turn red and explain why no commit was made, without moving Git checks into the admission webhook and without adding a second state source. Keeps the existing kstatus trio and domain conditions, ranks the most likely operator-facing failures, and has now shipped the first status-transition slice: refusal messages republish when details change, scoped refusals cannot be cleared by unrelated scope success, `BlockMessage()` carries stable issue kind/path/actor detail, and the unsupported-folder e2e covers Git-side break and recovery. |
| [`open-asks-priority.md`](design/open-asks-priority.md) | **the work queue.** Merges three overlapping backlogs — the downstream consumer asks, the API-surface block left unbuilt by the status and configuration-model review, and the config-surface proposal (B1–B6) — into one ordered queue under four stated tests, and says where we deliberately do **not** do what was asked. The standing caveat narrowed once the layout model reversed: a Tier 2 entry belongs to postponed [#294](https://github.com/ConfigButler/gitops-reverser/issues/294) only if it breaks a `GitTarget` field, and everything else is independently schedulable. Makes one design call against what was asked: **delete Option C sibling inference** rather than ship an off-switch for it, because it let a human's edit to the repository change operator behavior with nothing in status recording the move. That deletion has shipped, and "what the deletion taught" records what building it found. **F9 is Tier 1**: the only item whose answer is unknown rather than whose work is unscheduled, and it gates planning the enum work |
| [`proving-the-metric-surface.md`](design/proving-the-metric-surface.md) | **design, partly built.** What identity an operator needs to locate a failure, and what an e2e check has to do to be worth trusting. Written after a metric value that was declared, switched on, surfaced as a condition reason and counted turned out to have no producer at all, and the check added to catch that class then skipped whenever the counter read zero — so deleting every recording call left the suite green. Two rules follow: an assertion must not take its trigger from the metric under test, and no enum value may be reachable only through an injected fake. Carries a cardinality policy that separates bounded configuration identity from per-request identity, a table of which instrument families should own `source_cluster` directly rather than by join, and the PromQL reasoning for `increase()` versus `max_over_time` (neither applies mechanically: one cannot see an event before the first sample, the other cannot reconstruct totals across a reset). Steps 1–4 are built; step 5, diagnostics export, the `e2e_run` scrape label and a pinned-seed regression are not |
| [`placement-visibility-and-declared-defaults.md`](design/placement-visibility-and-declared-defaults.md) | **design, half built.** The three questions the inference deletion left, decided and then built in two goes. PR #291 shipped the deletion and none of the eight items queued behind it; the residue was filed as [#295](https://github.com/ConfigButler/gitops-reverser/issues/295) — **which shipped in 0.42.1 via [#319](https://github.com/ConfigButler/gitops-reverser/pull/319) and is what reversed the layout model** — and [#296](https://github.com/ConfigButler/gitops-reverser/issues/296), whose largest half then shipped in [#326](https://github.com/ConfigButler/gitops-reverser/pull/326): `status.layout` became `status.placement`, and the ambiguous render root came with it as a refusal rather than a third counter outcome. The `declared` metric value has since split into `by_type`/`default` too, which was the one row with an external consumer because it changes a label a dashboard may select on. What still stands and is still unbuilt is the rest of the legibility set: the canonical path as a template constant, and `{kindLower}`, tracked on [#339](https://github.com/ConfigButler/gitops-reverser/issues/339) now that #296 is closed; the Event on the GitTarget was dropped. Its Question 2, a CRD default for `placement.default`, is **re-opened rather than superseded** and now lives as an open question in [`layout/model.md`](layout/model.md); the argument against it is legibility, since #319 removed the correctness objection |
Expand Down
8 changes: 8 additions & 0 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ This file is meant to track the smaller current backlog, not historical notes.

## Current backlog

- [ ] Finish making `GitTarget` turn red quickly and explain why no commit was made.
The plan is in [gittarget-red-status-plan.md](design/gittarget-red-status-plan.md). This is the
replacement direction for Git write preflight: no admission webhook Git work, no extra state
source, and no promise before etcd. The first slice has shipped: stale refusal messages now
republish, scoped refusal recovery is not over-broad, bounded refusal messages include stable
issue detail, and the unsupported-folder e2e covers Git-side break and recovery. Remaining work is
the broader reason/status contract table and any later durable restart behavior.

- [ ] **High availability (near-term priority).** Support running more than one controller replica;
`replicaCount > 1` is currently hard-rejected by the chart. Redis/Valkey is already the shared store
for watch resume cursors, command-author facts, and — since the fact-stream switchover — the
Expand Down
38 changes: 38 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ re-authoring it. The keys read for each auth method:
Auth precedence is SSH key → HTTP basic → bearer token. Client certificates (mTLS), custom CA
certificates, and GitHub App credentials are **not supported**.

Credentials are refused with `http://` GitProvider URLs by default. Use HTTPS or SSH for real
repositories. For trusted in-cluster development Git servers that intentionally serve plain HTTP,
start the manager with `--allow-insecure-git-http` or set
`controllerManager.allowInsecureGitHTTP=true` in the Helm chart.

> **A reused Secret needs write access.** Flux and Argo CD only *clone*, so their Git credentials are
> often read-only (a read-only deploy key, a read-scoped token). GitOps Reverser **pushes** commits,
> so a reused Secret's key or token must have **write** access on the repository; otherwise the
Expand Down Expand Up @@ -489,6 +494,39 @@ The most useful status fields are:

Use conditions for automation.

### Why a `GitTarget` is not committing

Start with the target, then check the rules that feed it:

```bash
kubectl get gittarget -A
kubectl describe gittarget -n <namespace> <name>
kubectl get watchrule,clusterwatchrule -A
```

A red `GitTarget` means the target itself cannot validate, run, or publish. `Ready=False` gives the
summary reason, while the domain conditions name the axis to fix:

| Condition | What to check |
|---|---|
| `Validated=False` | The referenced `GitProvider`, branch policy, `ClusterProvider`, and namespace authorization. |
| `EncryptionConfigured=False` | The age/SOPS Secret and whether key generation is allowed. |
| `GitProviderReady=False` | Repository URL, credential Secret, host keys, and push permissions. |
| `ClusterProviderReady=False` or `SourceClusterReachable=False` | Source-cluster credentials and access. |
| `GitPathAccepted=False` | Repository files under `spec.path`; the message includes the issue kind, path, and actor when known. |
| `RenderMatchesLive=False` | Fields that no longer match the rendered source, often transformer-owned values. |

A green `GitTarget` with a red `WatchRule` or `ClusterWatchRule` means the destination may be fine,
but the selected source objects are not being watched or authorized. Inspect the rule's `Ready`,
`GitTargetReady`, `SourceNamespaceAuthorized`, and `StreamsRunning` conditions.

After fixing a Git-side folder problem, request a fresh read of the path:

```bash
kubectl annotate gittarget -n <namespace> <name> \
reconcile.configbutler.ai/requestedAt="$(date -Iseconds)" --overwrite
```

The kstatus trio is also on Prometheus, so the same answer is alertable without a kubeconfig:
`gitopsreverser_resource_condition{kind="GitTarget", type="Ready", status="False"} == 1` names every
target that has not been accepted, and `reason` says which gate refused. The metric covers `Ready`,
Expand Down
Loading