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
2 changes: 1 addition & 1 deletion .coverage-baseline
Original file line number Diff line number Diff line change
@@ -1 +1 @@
73.9
74.5
11 changes: 9 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,8 @@ write side is shared with live writes):
* before anything is planned, a **structure-only acceptance gate** runs over the scanned subtree; if it
finds content the operator cannot safely manage — a kustomization using an unsupported feature
(generators / patches / components / helm / replacements / transformers / name(pre|suf)fix / remote
bases), a duplicate manifest identity, an impure managed file, or a standalone non-KRM / invalid YAML —
bases) or malformed `images:`/`replicas:` overrides, a duplicate manifest identity, an impure managed
file, or a standalone non-KRM / invalid YAML —
the whole apply is **refused**: nothing is committed, `GitPathAccepted=False`, `Stalled=True`, and
`Ready=False` with reason `UnsupportedContent` until a human cleans the path;
* desired resources are upserted through the same content derived path as live writes;
Expand Down Expand Up @@ -901,10 +902,16 @@ hydrates only touched files into buffers for the commit, and flushes only change
* **Upserts:** if a managed document for the resource already exists, patch it in place (preserving
siblings in a multi document file); if it is sensitive, encrypt the whole document again at its existing
path; if no document exists, create a new file at the canonical placement path.
* **Kustomize override edit-through:** a live value produced by a well-formed `images:` or `replicas:`
entry in the document's kustomization chain is written back to that entry (comment-preserving, only
fields the entry already declares); the source manifest keeps its bytes. Anything the inversion cannot
express falls back to the plain in-place patch. See
[gitops-api/f1-images-replicas-edit-through.md](design/gitops-api/f1-images-replicas-edit-through.md).
* **Deletes:** use the manifest identity index, so a moved manifest can still be deleted even when it is
not at the canonical path.
* **Field patches** (currently `/scale` → parent `spec.replicas`) are intentionally narrow: they only
patch an existing parent manifest and never fabricate a parent object from partial subresource data.
patch an existing parent manifest and never fabricate a parent object from partial subresource data; a
`spec.replicas` assignment governed by a `replicas:` override is routed to the entry instead.

### File Placement

Expand Down
32 changes: 32 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,38 @@ Entries are `resource` for the core API group or `group/resource` for grouped AP
API version, so a served CRD version change does not change the sensitive classification. The custom
resource still needs a `GitTarget` with `spec.encryption` configured before Git writes can succeed.

### Kustomize support in the target path

A target path may contain `kustomization.yaml` files. The operator retains them as build directives
(it never sweeps them) and understands a deliberately small, round-trippable subset:

- **`namespace:` + `resources:`/`bases`** (local files and directory bases): a namespace-less
resource file inherits its namespace from the kustomization that references it, and
`metadata.namespace` is kept out of the file on write.
- **`images:` and `replicas:` overrides**: a live change *produced by* an override entry — an image
tag, name, or digest pinned by `images:`, or a replica count pinned by `replicas:` (including
`kubectl scale`) — is written back **to that entry**, preserving comments, and the source manifest
keeps its bytes. Only fields the entry already declares are updated; the operator never adds or
removes entries. Note that one entry is a shared knob, exactly as in kustomize itself: updating it
affects every resource in the build whose image matches.

Anything outside that subset **refuses the whole target path before anything is written**:
`patches`/`patchesStrategicMerge`/`patchesJson6902`, generators, `components`, Helm fields,
`replacements`, `transformers`, `namePrefix`/`nameSuffix`, remote bases, and `images:`/`replicas:`
values that do not parse (those would fail `kustomize build` too). A refusal is loud: the target
reports `GitPathAccepted=False`, `Stalled=True`, and `Ready=False` with reason `UnsupportedContent`
until the path is cleaned up.

Two situations fall back to plain in-place editing of the source manifest instead of refusing:
a resource file reachable from more than one render root with differing override chains
(ambiguous — the operator will not guess which chain governs), and a live change an entry cannot
express (for example a removed digest, or two containers demanding different values from one
entry). These fallbacks are recorded as store diagnostics — visible in the analyzer CLI and, for
the running operator, in the logs at debug verbosity (`manifest store diagnostic`).

For design details and the exact boundary, see
[design/gitops-api/f1-images-replicas-edit-through.md](design/gitops-api/f1-images-replicas-edit-through.md).

## `WatchRule`

`WatchRule` is the normal namespaced watcher. It only watches resources in its own namespace and
Expand Down
73 changes: 73 additions & 0 deletions docs/design/gitops-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# GitOps API: editing existing GitOps folders as a product surface

> Status: active workstream
> Captured: 2026-07-06
> Related:
> [../../future/idea-application-editing.md](../../future/idea-application-editing.md),
> [../manifest/contextual-namespace-and-kustomize-folder-editing.md](../manifest/contextual-namespace-and-kustomize-folder-editing.md),
> [../manifest/file-agnostic-placement.md](../manifest/file-agnostic-placement.md),
> [../../finished/current-manifest-support-review.md](../../finished/current-manifest-support-review.md)

## Goal

Enable a product layer on top of GitOps Reverser where an end user points the
operator at a Git folder holding **raw YAML or simple Kustomize**, edits those
resources through the Kubernetes API, and the operator writes the changes to a
branch. The product layer then opens the pull request through the Git host's
API.

The division of responsibility is deliberate and already a recorded decision
([file-agnostic-placement.md](../manifest/file-agnostic-placement.md)):

- **GitOps Reverser**: watch live state, edit the folder the way a careful
human would (in place, comment-preserving, refusing what it cannot own),
push to a named branch, and expose pollable status (`CommitRequest`
`Pushed=True` + `status.sha` + `status.branch`).
- **Product layer**: session/branch lifecycle policy, PR creation and merge,
branch cleanup, and any UI.

The operator never gains Git-host knowledge (no GitHub/GitLab API calls).

## The governing rule

Every feature in this workstream must keep the repository **round-trippable**:
one source document owns one live object, and every edit the operator writes
must be expressible in both directions (live → Git and Git → live via the
GitOps tool's render). Constructs that are lossy or one-way (arbitrary patches,
generators with hash suffixes, name prefixes, Helm inflation) stay refused —
that refusal is the product's defensible support contract, not a gap.

## Feature ladder

Ordered by value-per-risk. Each feature gets its own design doc in this folder
when work starts.

| # | Feature | Design doc | Status |
|---|---------|-----------|--------|
| F1 | Kustomize `images:` / `replicas:` edit-through — a live change produced by an override entry is written back to that entry, never through into the source manifest | [f1-images-replicas-edit-through.md](f1-images-replicas-edit-through.md) | implemented ([#198](https://github.com/ConfigButler/gitops-reverser/pull/198)) |
| F2 | Render-root scoping — a GitTarget declares its render root (e.g. `overlays/env1`); base files reached through `../../base` become read-only context, dissolving overlay fan-out ambiguity | — | not designed |
Comment thread
sunib marked this conversation as resolved.
| F3 | Restricted patch authoring — write/update scalar-field strategic-merge patches in an overlay ("live drift lands in the environment's overlay") | — | not designed |
| F4 | New-file placement rules — sibling inference + `spec.newFilePath` template so new resources land in the folder's convention, not the canonical REST path | designed in [version2/gittarget-new-file-placement-rules.md](../manifest/version2/gittarget-new-file-placement-rules.md) | designed, unbuilt |
| F5 | Branch/session ergonomics — base-branch selection, opt-in remote branch cleanup, a GitTarget-level quiescence condition | — | not designed |

## What already works (baseline, 2026-07-06)

- Raw-YAML folders (explicit namespaces): in-place, comment-preserving edits;
match-first placement; mark-and-sweep resync.
- Single-context Kustomize folders (`namespace:` + `resources:`/`bases`, local
files and child-directory bases): graph-aware namespace inference; inherited
namespaces are kept out of the file bytes on write.
- Branch writing: `GitTarget.spec.branch` (immutable, glob-authorized via
`GitProvider.spec.allowedBranches`); `CommitRequest` as the per-save control
surface with terminal `Pushed=True` + SHA.
- Refusals: unsupported kustomize features, duplicate identities, impure or
foreign content — refuse-first, never mis-edit.

## Known boundary (what stays refused)

Classic base + per-env overlay repositories are refused today on two
independent gates: any overlay feature outside the supported subset
(`patches*`, generators, `components`, `namePrefix`/`nameSuffix`, Helm, remote
bases), and multi-root namespace fan-out over a shared base
(`ambiguous-namespace`). F1 does not change that boundary; F2/F3 are the
features that would move it.
Loading