diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md
index 195ddacdf..54c86425a 100644
--- a/docs/UPGRADING.md
+++ b/docs/UPGRADING.md
@@ -7,6 +7,37 @@ guidance that the changelog's breaking-change entries link to.
We are pre-1.0, so breaking changes bump the **minor** version (release-please is configured with
`bump-minor-pre-major`) rather than the major. Read the relevant entry before upgrading across it.
+## Unreleased — `kustomization.yaml` is now read by kustomize itself (next minor; behavior change)
+
+The analyzer used to decode `kustomization.yaml` with a hand-written walk over a generic
+YAML map, checked against a hand-maintained list of 17 unsupported keys. It now decodes with
+kustomize's own type (`sigs.k8s.io/kustomize/api/types.Kustomization`) and runs the same
+`Unmarshal` + `FixKustomization` sequence kustomize's builder runs, and it derives the
+unsupported set by reflecting over that type: **anything not explicitly modelled refuses the
+folder.**
+
+Five verdicts change as a result. Each was verified against a real `kustomize build`, and in
+every case the new behaviour is the one that agrees with the renderer:
+
+| Kustomization contains | Before | After |
+|---|---|---|
+| `vars:` | **accepted** — and `$(VAR)` in a source file was silently overwritten with its substituted value | **refused** (`vars`) |
+| `validators:` (plugin code) | **accepted**, unmodelled | **refused** (`validators`) |
+| a `kustomization.yaml` kustomize cannot decode (e.g. `resources:` is a string, or the file is really a Flux `Kustomization` CR) | **accepted**, and written into | **refused** (`unparseable`, quoting kustomize's own error) |
+| `imageTags:` / `bases:` (deprecated spellings) | `imageTags` ignored; `bases` read | both **normalised** into `images`/`resources`, as the builder does |
+| a case-variant key (`newtag:`), or a blank optional component (`newName: ""`) | **refused** | **accepted** — kustomize honours both, so folders that render fine are no longer refused |
+
+**Migration**
+
+- **The first three refuse folders that previously worked.** All three were unsafe: two let
+ the operator write into a folder whose render it had misunderstood, and the third let it
+ write into a folder no GitOps controller can build at all. If a `GitTarget` starts failing
+ with `GitPathAccepted=False` / `UnsupportedContent`, the refusal detail now names the
+ feature — and for `unparseable`, quotes kustomize's decode error verbatim.
+- **`vars` has no supported replacement.** A value derived at render time has no single home
+ in Git; edit the source field instead.
+- Nothing to do for the last two rows: they only ever accept more.
+
## Unreleased — `pkg/manifestanalyzer`: the overlay fan-out refusal code was renamed (next minor; breaking for consumers)
One refusal reason changed its name, in both the Go constant and the machine-readable
diff --git a/docs/design/support-boundary/finished/images-and-replicas-edit-through.md b/docs/design/support-boundary/finished/images-and-replicas-edit-through.md
index f68ddd383..1ad5b12dd 100644
--- a/docs/design/support-boundary/finished/images-and-replicas-edit-through.md
+++ b/docs/design/support-boundary/finished/images-and-replicas-edit-through.md
@@ -189,6 +189,13 @@ not have — batch-wide edit reconciliation is not built.
## What this deliberately does not do
+> **Superseded on 2026-07-14, in part.** "Option D remains future research" was answered:
+> the analyzer now decodes `kustomization.yaml` with kustomize's own type rather than a
+> hand-written key walk, and the re-implemented transformer subset is being removed. The
+> reasoning, and why the original position was wrong, is in
+> [../kustomize-support-boundary.md](../kustomize-support-boundary.md) §7. The rest of
+> this section still describes what shipped here.
+
- No `kustomize build`, no source maps (Option D remains future research).
- No entry creation/deletion, no patch authoring, no `namePrefix`/`nameSuffix`
or generator support — the acceptance boundary for those is unchanged.
diff --git a/docs/design/support-boundary/kustomize-support-boundary.md b/docs/design/support-boundary/kustomize-support-boundary.md
index 022140a5f..0ea2b8bed 100644
--- a/docs/design/support-boundary/kustomize-support-boundary.md
+++ b/docs/design/support-boundary/kustomize-support-boundary.md
@@ -280,12 +280,57 @@ kustomize" means two different things:
- **Understanding** (folder → expected render): ours, necessarily — the
writer must know which file supplied each live value.
-We keep *re-implementing the narrow transformer subset* rather than embedding
-kustomize as the renderer: it is what keeps the refusal boundary honest (we
-refuse exactly what we do not model). The worthwhile upgrade is kustomize's
-Go API (`krusty`) as a **verification oracle**, not a renderer: in the
-acceptance gate or in CI, build each render root in-memory and compare
-against our own projection; mismatch → refuse. That buys kustomize's ground
-truth without ever depending on semantics we have not modeled — the cheap,
-high-confidence version of the "Option D" parked during the `images:`/`replicas:`
-work.
+**Decision (2026-07-14): we embed kustomize (`sigs.k8s.io/kustomize/api`) as the
+renderer. The re-implementation is being removed.** This reverses the earlier
+position — kept here because the reasoning was wrong in a specific, instructive way.
+
+The old position was that re-implementing the narrow transformer subset "keeps the
+refusal boundary honest: we refuse exactly what we do not model," and that `krusty`
+was at best a *verification oracle* comparing against our own projection.
+
+Both halves turned out to be false:
+
+- **The re-implementation does not keep the boundary honest — it makes it dishonest
+ in two places.** `vars` was never added to the deny-list, so `$(VAR)` in a source
+ file is silently overwritten with its substituted value. `commonLabels`/`labels`/
+ `annotations` were classed as benign and leak into source documents as drift.
+ "We refuse exactly what we do not model" was the *intent*; what shipped is "we
+ refuse most of what we do not model, and corrupt source files with the rest."
+- **The dependency was never the cost we thought.** `sigs.k8s.io/kustomize/api` and
+ `kyaml` are **already in this module's requirement graph**. Taking them as a direct
+ dependency at the version Flux ships (v0.21.1) adds **zero new modules** — one line
+ in `go.mod`, and code we already carry starts getting linked instead of re-typed.
+
+Against that: ~1,050 lines across
+[`overrides.go`](../../../internal/manifestanalyzer/overrides.go),
+[`overrides_projection.go`](../../../internal/manifestanalyzer/overrides_projection.go)
+and [`kustomization.go`](../../../internal/git/manifestedit/kustomization.go)
+re-derive image-reference parsing, the image transformer, the replica transformer's
+fieldspec, render-root discovery and the resource DAG walk — and every future feature
+demands more of the same (strategic-merge semantics, name-reference cascades, the
+generator content-hash algorithm). We were re-writing kustomize by instalments.
+
+**Understanding is still ours; deploying is still theirs.** Embedding the library does
+not put us in the deployment business. It replaces our *guess* at what Flux will render
+with the *library Flux actually renders with*.
+
+### The sandbox is part of the contract
+
+```go
+krusty.Options{
+ LoadRestrictions: kustypes.LoadRestrictionsRootOnly,
+ PluginConfig: kustypes.DisabledPluginConfig(), // no exec, no Go plugins
+}
+```
+
+**`LoadRestrictions` does not stop the network, and this was measured, not assumed.**
+Given a remote base, kustomize shells out to `/usr/bin/git fetch` — under
+`LoadRestrictionsRootOnly` *and* under an in-memory filesystem. Both were tried; both
+fetched.
+
+So the remote-base detection we already have
+([`hasRemoteResource`/`isRemoteResource`](../../../internal/manifestanalyzer/store.go))
+is **not** made redundant by the renderer. It is promoted to a **security precondition
+that runs before krusty is ever called**. It is the one piece of the re-implementation
+that must survive, and *"we do not run kustomize on a remote base"* stays literally
+true — now enforced rather than merely implied by not having a renderer at all.
diff --git a/docs/design/support-boundary/orchestrator-knowledge-boundary.md b/docs/design/support-boundary/orchestrator-knowledge-boundary.md
index ba8711c3f..b56aaec64 100644
--- a/docs/design/support-boundary/orchestrator-knowledge-boundary.md
+++ b/docs/design/support-boundary/orchestrator-knowledge-boundary.md
@@ -349,9 +349,13 @@ above:
## Non-goals
-- **No orchestrator emulation.** Interpreters read declarations. They do not run
- kustomize on a remote base, resolve a Helm chart, evaluate an ApplicationSet
- generator, decrypt SOPS, or contact a registry.
+- **No orchestrator emulation.** Interpreters read declarations. They do not resolve a
+ Helm chart, evaluate an ApplicationSet generator, decrypt SOPS, or contact a
+ registry. (The analyzer does decode a `kustomization.yaml` with kustomize's own
+ type, and will render a local render root with kustomize's own library — using the
+ renderer is the opposite of emulating it. It never runs on a remote base: kustomize
+ fetches one by shelling out to `git`, so a remote resource is refused before any
+ build. See [kustomize-support-boundary.md](kustomize-support-boundary.md) §7.)
- **No new operator dependency.** Interpreters serve the analyzer and the repo
scan report. Whether the operator ever consumes a claim is a separate decision; the
ownership *gate* it needs can be fed by Tier 0 (`Encrypted`) alone for the
diff --git a/docs/design/support-boundary/support-contract.md b/docs/design/support-boundary/support-contract.md
index d2d067724..d8f6a2606 100644
--- a/docs/design/support-boundary/support-contract.md
+++ b/docs/design/support-boundary/support-contract.md
@@ -119,12 +119,17 @@ we can store — and is argued in
## What we will not do
-- **No orchestrator emulation.** We do not run kustomize on a remote base, resolve
- a Helm chart, evaluate an ApplicationSet generator or a ResourceSet template,
- decrypt SOPS, or contact a registry.
+- **No orchestrator emulation.** We do not resolve a Helm chart, evaluate an
+ ApplicationSet generator or a ResourceSet template, decrypt SOPS, or contact a
+ registry. **We do run kustomize** — the real `sigs.k8s.io/kustomize/api`, the library
+ Flux itself renders with — locally, with plugins disabled, and **never on a remote
+ base**: a remote resource is refused before the build is invoked. Using the renderer
+ is the opposite of emulating it; see
+ [kustomize-support-boundary.md](kustomize-support-boundary.md) §7.
- **No Argo CD or Flux Go dependency.** Their kinds are matched by group and kind
over `unstructured`; the upstream checkouts under `external-sources/` are
- reference material, never vendored code.
+ reference material, never vendored code. (`sigs.k8s.io/kustomize` is neither — it is
+ a Kubernetes SIG library, already in this module's requirement graph.)
- **We never widen the boundary by guessing.** Ownership and provenance only ever
*refuse more*. Everything refused for renderability stays refused.
diff --git a/go.mod b/go.mod
index 137fdc7c5..21379e5ec 100644
--- a/go.mod
+++ b/go.mod
@@ -30,6 +30,7 @@ require (
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3
sigs.k8s.io/cli-utils v0.37.2
sigs.k8s.io/controller-runtime v0.24.1
+ sigs.k8s.io/kustomize/api v0.21.1
sigs.k8s.io/yaml v1.6.0
)
@@ -53,6 +54,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
+ github.com/go-errors/errors v1.4.2 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
@@ -135,6 +137,7 @@ require (
k8s.io/streaming v0.36.2 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+ sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index e4430a953..d2558a2f2 100644
--- a/go.sum
+++ b/go.sum
@@ -68,6 +68,8 @@ github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
+github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
+github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
@@ -364,6 +366,10 @@ sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9
sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs=
+sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI=
+sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI=
+sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 h1:qmp2e3ZfFi1/jJbDGpD4mt3wyp6PE1NfKHCYLqgNQJo=
diff --git a/internal/git/placement_test.go b/internal/git/placement_test.go
index 706796662..caa7b2d5c 100644
--- a/internal/git/placement_test.go
+++ b/internal/git/placement_test.go
@@ -199,15 +199,14 @@ func TestPlacement_KustomizeEntryIdempotent_OnRepeatedApply(t *testing.T) {
"the resources: entry must appear exactly once")
}
-// A kustomization whose resources: field is malformed (not a sequence) is still
-// accepted by the analyzer (only specific disallowed keys, not resources: shape,
-// disqualify a kustomization) — so the writer must still place the resource's own
-// file, but the resources: entry add is skipped rather than corrupting the
-// kustomization, leaving it exactly as it was for a human to fix.
-func TestPlacement_KustomizeEntryAppendSkipped_MalformedResourcesField(t *testing.T) {
+// A kustomization with no resources: sequence to append to is still accepted — so
+// the writer must place the resource's own file, while the resources: entry add is
+// skipped rather than inventing the key, leaving the kustomization exactly as it
+// was for a human to complete.
+func TestPlacement_KustomizeEntryAppendSkipped_NoResourcesSequence(t *testing.T) {
worktree := newWorktreeForTest(t)
root := worktree.Filesystem.Root()
- kustYAML := "namespace: app\nresources: not-a-list\n"
+ kustYAML := "namespace: app\n"
seedPlacedManifest(t, worktree, "overlays/test/kustomization.yaml", kustYAML)
changed := applyEventsWithPolicy(t, worktree, nil, newConfigMapEvent("cache", "app"))
@@ -222,7 +221,24 @@ func TestPlacement_KustomizeEntryAppendSkipped_MalformedResourcesField(t *testin
// returns the original content untouched, not merely a semantically
// equivalent one.
assert.Equal(t, kustYAML, string(kust), //nolint:testifylint
- "a malformed resources: field must be left untouched, not corrupted")
+ "a kustomization with no resources: sequence must be left untouched, not corrupted")
+}
+
+// A kustomization kustomize itself cannot decode (here resources: is not a
+// sequence, which fails `kustomize build` outright) refuses the folder rather than
+// being tolerated. Before the analyzer used kustomize's own type this file was
+// accepted — only specific disallowed *keys* disqualified a kustomization, never
+// its shape — so the operator would happily write into a folder that no GitOps
+// controller could render.
+func TestPlacement_UndecodableKustomization_RefusesTheFlush(t *testing.T) {
+ worktree := newWorktreeForTest(t)
+ seedPlacedManifest(t, worktree, "overlays/test/kustomization.yaml", "namespace: app\nresources: not-a-list\n")
+
+ w := &BranchWorker{contentWriter: newContentWriter(types.SensitiveResourcePolicy{}), mapper: configMapMapper()}
+ _, err := w.flushEventsToWorktree(
+ context.Background(), worktree, "", []Event{newConfigMapEvent("cache", "app")}, nil,
+ )
+ require.Error(t, err, "a kustomization kustomize cannot build must refuse the folder, not be written into")
}
func newTestWriteBatch(t *testing.T) *writeBatch {
diff --git a/internal/manifestanalyzer/kustomization_parse.go b/internal/manifestanalyzer/kustomization_parse.go
new file mode 100644
index 000000000..65907abca
--- /dev/null
+++ b/internal/manifestanalyzer/kustomization_parse.go
@@ -0,0 +1,281 @@
+// SPDX-License-Identifier: Apache-2.0
+
+package manifestanalyzer
+
+import (
+ "reflect"
+ "sort"
+ "strings"
+
+ kustypes "sigs.k8s.io/kustomize/api/types"
+
+ "github.com/ConfigButler/gitops-reverser/internal/git/manifestedit"
+)
+
+// This file decodes kustomization.yaml with kustomize's own type
+// (sigs.k8s.io/kustomize/api/types.Kustomization) instead of a hand-written walk
+// over map[string]interface{}, and derives the unsupported-feature set by
+// reflecting over that type's fields.
+//
+// The point is not brevity. It is that the check becomes exhaustive by
+// construction: a kustomization field the operator has never heard of lands
+// outside supportedKustomizationFields and refuses the folder, instead of being
+// silently tolerated. The hand-written deny-list could only refuse what someone
+// had remembered to add to it, and it had holes — see supportedKustomizationFields.
+//
+// See docs/design/support-boundary/kustomize-support-boundary.md §7.
+
+// supportedKustomizationFields names the struct fields of kustypes.Kustomization
+// that the operator models. Every other field, present and non-zero, refuses the
+// folder by its own name.
+//
+// Inverting the old hand-written deny-list into an allowlist over kustomize's own
+// type closed two holes it had, each of which let a kustomization render
+// differently from what we believed:
+//
+// - vars — a source document containing $(SOME_VAR) renders to the substituted
+// value. We mirrored that value straight back over the variable in the source
+// file. Silent corruption, in a folder we accepted.
+// - validators — plugin code. Arbitrary code is an unknowable render.
+//
+// Neither was in the deny-list, and neither could have been missed here: they are
+// fields on kustomize's type, and anything not named below is refused. When
+// kustomize grows a field, it lands outside this set and refuses rather than being
+// silently tolerated.
+//
+// Deprecated spellings (bases, imageTags) are absent on purpose: FixKustomization
+// folds them into resources/images before this map is consulted, exactly as the
+// builder does.
+func supportedKustomizationFields() map[string]struct{} {
+ return map[string]struct{}{
+ "TypeMeta": {}, // apiVersion / kind
+ "MetaData": {}, // the kustomization's own metadata; renders nothing
+
+ // The modelled subset: the contextual-namespace walk plus the two
+ // edit-through channels.
+ "Namespace": {},
+ "Resources": {},
+ "Images": {},
+ "Replicas": {},
+
+ // Tolerated exactly as before this change, so it stays a refactor. These
+ // inject metadata into rendered objects and therefore leak into mirrored
+ // source files as drift — a known defect, tracked separately, deliberately
+ // not altered here.
+ "CommonLabels": {},
+ "Labels": {},
+ "CommonAnnotations": {},
+ "BuildMetadata": {},
+
+ "GeneratorOptions": {}, // inert on its own; every generator is refused below
+ "SortOptions": {}, // output ordering only
+ }
+}
+
+// featureUnparseable is reported when kustomize's own decoder rejects the file.
+// Unmarshal disallows unknown fields, so this also covers a key kustomize itself
+// would refuse to build: if we cannot read it, we cannot vouch for what it renders.
+const featureUnparseable = "unparseable"
+
+// featureRemoteBase is reported for a resources/bases entry that is not a local
+// path. It is the one piece of kustomize semantics we must keep owning: kustomize
+// resolves a remote base by shelling out to `git fetch`, and it does so under
+// LoadRestrictionsRootOnly and under an in-memory filesystem alike (both measured).
+// Detecting it ourselves, before any build is invoked, is what keeps "we never
+// fetch a remote base" true.
+const featureRemoteBase = "remote-base"
+
+const (
+ featureMalformedImages = "malformed-images"
+ featureMalformedReplicas = "malformed-replicas"
+)
+
+// parseKustomization decodes one kustomization.yaml and reports every feature the
+// operator does not model, sorted. An empty slice means the file is fully modelled.
+// The doc is returned even when unsupported: callers keep it (so it never acts as a
+// namespace source) rather than dropping it.
+func parseKustomization(content []byte, path string) (*kustomizationDoc, []string) {
+ doc := &kustomizationDoc{path: path}
+
+ // Unmarshal then FixKustomization is exactly what kustomize's own loader does
+ // (internal/target/kusttarget.go: load), so we model the kustomization the
+ // builder will actually see: bases folded into resources, imageTags into
+ // images, the deprecated generator spellings normalised.
+ //
+ // The loader's CheckEmpty/EnforceFields validations are deliberately not run
+ // here: they decide whether a build would succeed, which is a different
+ // question from whether we can model the render, and adding them would refuse
+ // more than this change intends to.
+ var k kustypes.Kustomization
+ if err := k.Unmarshal(content); err != nil {
+ doc.unsupported = true
+ return doc, []string{featureUnparseable}
+ }
+ k.FixKustomization()
+
+ doc.namespace = strings.TrimSpace(k.Namespace)
+ doc.resources = trimmedEntries(k.Resources)
+
+ features := map[string]struct{}{}
+ for _, name := range unmodelledFields(k) {
+ features[name] = struct{}{}
+ }
+ if hasRemoteResource(doc.resources) {
+ features[featureRemoteBase] = struct{}{}
+ }
+
+ var ok bool
+ if doc.images, ok = imageOverrides(k.Images, path); !ok {
+ features[featureMalformedImages] = struct{}{}
+ }
+ if doc.replicas, ok = replicaOverrides(k.Replicas, path); !ok {
+ features[featureMalformedReplicas] = struct{}{}
+ }
+
+ out := make([]string, 0, len(features))
+ for f := range features {
+ out = append(out, f)
+ }
+ sort.Strings(out)
+ doc.unsupported = len(out) > 0
+ return doc, out
+}
+
+// kustomizationDecodeError returns kustomize's own decode error for a file it
+// cannot build, or "" when it decodes. It is what makes an `unparseable` refusal
+// actionable: "your kustomization.yaml is a Flux Kustomization CR" and "resources:
+// is a string, not a list" are both unparseable, and the user needs to know which.
+func kustomizationDecodeError(content []byte) string {
+ var k kustypes.Kustomization
+ if err := k.Unmarshal(content); err != nil {
+ return err.Error()
+ }
+ return ""
+}
+
+// unmodelledFields returns the yaml key of every non-zero Kustomization field that
+// is not in supportedKustomizationFields — the features that refuse the folder,
+// named as the user wrote them.
+func unmodelledFields(k kustypes.Kustomization) []string {
+ var out []string
+ modelledFields := supportedKustomizationFields()
+ v := reflect.ValueOf(k)
+ t := v.Type()
+ for i := range t.NumField() {
+ field := t.Field(i)
+ if _, modelled := modelledFields[field.Name]; modelled {
+ continue
+ }
+ if v.Field(i).IsZero() {
+ continue
+ }
+ out = append(out, kustomizationFieldKey(field))
+ }
+ return out
+}
+
+// kustomizationFieldKey is the yaml key a user writes for a struct field
+// ("configMapGenerator"), so a refusal names the line in their file.
+func kustomizationFieldKey(f reflect.StructField) string {
+ tag := f.Tag.Get("yaml")
+ if i := strings.Index(tag, ","); i >= 0 {
+ tag = tag[:i]
+ }
+ if tag == "" || tag == "-" {
+ return f.Name
+ }
+ return tag
+}
+
+// imageOverrides converts kustomize's parsed images: entries into the writer's
+// model. ok is false when an entry is present but not one we can route an edit
+// through, which refuses the folder rather than misunderstanding it silently.
+//
+// TagSuffix is rejected on purpose. We do not model it, and kustomize's type would
+// happily decode it — accepting it would mean writing a file we believe renders one
+// way while kustomize renders it another. (The old hand-written key check refused it
+// as an unknown key; this preserves that, deliberately.)
+func imageOverrides(entries []kustypes.Image, source string) ([]ImageOverride, bool) {
+ if len(entries) == 0 {
+ return nil, true
+ }
+ out := make([]ImageOverride, 0, len(entries))
+ for i, e := range entries {
+ if strings.TrimSpace(e.Name) == "" || strings.TrimSpace(e.TagSuffix) != "" {
+ return nil, false
+ }
+ out = append(out, ImageOverride{
+ Source: source,
+ Index: i,
+ Name: e.Name,
+ NewName: e.NewName,
+ NewTag: e.NewTag,
+ Digest: e.Digest,
+ HasNewName: e.NewName != "",
+ HasNewTag: e.NewTag != "",
+ HasDigest: e.Digest != "",
+ })
+ }
+ return out, true
+}
+
+// replicaOverrides converts kustomize's parsed replicas: entries into the writer's
+// model. A negative count is not a render kustomize would produce, so it is refused.
+func replicaOverrides(entries []kustypes.Replica, source string) ([]ReplicaOverride, bool) {
+ if len(entries) == 0 {
+ return nil, true
+ }
+ out := make([]ReplicaOverride, 0, len(entries))
+ for i, e := range entries {
+ if strings.TrimSpace(e.Name) == "" || e.Count < 0 {
+ return nil, false
+ }
+ out = append(out, ReplicaOverride{Source: source, Index: i, Name: e.Name, Count: e.Count})
+ }
+ return out, true
+}
+
+// trimmedEntries returns the graph entries the walker follows, dropping blanks.
+// bases: is already folded into resources: by FixKustomization.
+func trimmedEntries(lists ...[]string) []string {
+ var out []string
+ for _, list := range lists {
+ for _, e := range list {
+ if e = strings.TrimSpace(e); e != "" {
+ out = append(out, e)
+ }
+ }
+ }
+ return out
+}
+
+// parseKustomizations reads every kustomization.yaml into a kustomizationDoc keyed
+// by its directory. An unparseable kustomization, or one using an unsupported
+// feature, is kept but marked unsupported so it never acts as a namespace source.
+func parseKustomizations(files []manifestedit.FileContent) map[string]*kustomizationDoc {
+ out := map[string]*kustomizationDoc{}
+ for _, f := range files {
+ if !isKustomizationFile(f.Path) {
+ continue
+ }
+ doc, _ := parseKustomization(f.Content, filepathToSlash(f.Path))
+ out[slashDir(f.Path)] = doc
+ }
+ return out
+}
+
+// kustomizationUsesUnsupportedFeature reports whether a kustomization.yaml uses a
+// feature outside the modelled subset — the predicate the acceptance gate uses to
+// refuse the folder at the retention site.
+func kustomizationUsesUnsupportedFeature(content []byte) bool {
+ _, features := parseKustomization(content, "")
+ return len(features) > 0
+}
+
+// unsupportedKustomizeFeatures names the features a kustomization declares that the
+// operator does not model, for the repo scan's per-candidate refusal detail. It is
+// the same parse the acceptance gate runs, so the two cannot drift.
+func unsupportedKustomizeFeatures(content []byte) []string {
+ _, features := parseKustomization(content, "")
+ return features
+}
diff --git a/internal/manifestanalyzer/overrides.go b/internal/manifestanalyzer/overrides.go
index bf95af557..96fa76e49 100644
--- a/internal/manifestanalyzer/overrides.go
+++ b/internal/manifestanalyzer/overrides.go
@@ -3,7 +3,6 @@
package manifestanalyzer
import (
- "math"
"sort"
"strings"
@@ -69,136 +68,6 @@ type KustomizeOverrides struct {
Replicas []ReplicaOverride
}
-// hasOnlyKeys reports whether the entry map uses only the exact key set
-// kustomize's typed entry accepts. An unknown key means we can no longer vouch
-// for the render (kustomize itself rejects it), so the kustomization is refused
-// as malformed rather than silently misunderstood.
-func hasOnlyKeys(m map[string]interface{}, keys ...string) bool {
- for k := range m {
- known := false
- for _, want := range keys {
- if k == want {
- known = true
- break
- }
- }
- if !known {
- return false
- }
- }
- return true
-}
-
-// parseImageOverrides parses the images: field of a kustomization. ok is false
-// when the field is present but not structurally sound (not a list of maps, a
-// missing/empty name, a non-string or empty component, an unknown key) — the
-// caller marks the kustomization unsupported, because a folder we cannot parse is
-// a folder we cannot claim to understand.
-func parseImageOverrides(raw map[string]interface{}, source string) ([]ImageOverride, bool) {
- v, present := raw["images"]
- if !present || isEmptyValue(v) {
- return nil, true
- }
- list, isList := v.([]interface{})
- if !isList {
- return nil, false
- }
- out := make([]ImageOverride, 0, len(list))
- for i, item := range list {
- m, isMap := item.(map[string]interface{})
- if !isMap || !hasOnlyKeys(m, "name", "newName", "newTag", "digest") {
- return nil, false
- }
- entry := ImageOverride{Source: source, Index: i}
- var ok bool
- if entry.Name, ok = requiredString(m, "name"); !ok {
- return nil, false
- }
- if entry.NewName, entry.HasNewName, ok = optionalString(m, "newName"); !ok {
- return nil, false
- }
- if entry.NewTag, entry.HasNewTag, ok = optionalString(m, "newTag"); !ok {
- return nil, false
- }
- if entry.Digest, entry.HasDigest, ok = optionalString(m, "digest"); !ok {
- return nil, false
- }
- out = append(out, entry)
- }
- return out, true
-}
-
-// parseReplicaOverrides parses the replicas: field of a kustomization. ok is
-// false when the field is present but malformed (see parseImageOverrides).
-func parseReplicaOverrides(raw map[string]interface{}, source string) ([]ReplicaOverride, bool) {
- v, present := raw["replicas"]
- if !present || isEmptyValue(v) {
- return nil, true
- }
- list, isList := v.([]interface{})
- if !isList {
- return nil, false
- }
- out := make([]ReplicaOverride, 0, len(list))
- for i, item := range list {
- m, isMap := item.(map[string]interface{})
- if !isMap || !hasOnlyKeys(m, "name", "count") {
- return nil, false
- }
- entry := ReplicaOverride{Source: source, Index: i}
- var ok bool
- if entry.Name, ok = requiredString(m, "name"); !ok {
- return nil, false
- }
- if entry.Count, ok = integerField(m, "count"); !ok || entry.Count < 0 {
- return nil, false
- }
- out = append(out, entry)
- }
- return out, true
-}
-
-func requiredString(m map[string]interface{}, key string) (string, bool) {
- s, isStr := m[key].(string)
- if !isStr || strings.TrimSpace(s) == "" {
- return "", false
- }
- return s, true
-}
-
-// optionalString reads an optional string key, returning (value, present, ok):
-// present reports whether the key exists, ok is false for a non-string or blank
-// value (declared-but-empty is a broken transform, not an unset one).
-func optionalString(m map[string]interface{}, key string) (string, bool, bool) {
- v, exists := m[key]
- if !exists {
- return "", false, true
- }
- s, isStr := v.(string)
- if !isStr || strings.TrimSpace(s) == "" {
- return "", false, false
- }
- return s, true, true
-}
-
-// integerField reads a whole-number field. sigs.k8s.io/yaml decodes YAML numbers
-// as float64 (via JSON), so an integral float is accepted; anything else is not.
-func integerField(m map[string]interface{}, key string) (int64, bool) {
- switch n := m[key].(type) {
- case float64:
- if n != math.Trunc(n) {
- return 0, false
- }
- return int64(n), true
- case int:
- return int64(n), true
- case int64:
- return n, true
- default:
- return 0, false
- }
-}
-
// overrideAssignment collects, per resource file, the distinct override chains
// the render roots reach it with. Exactly one distinct chain attaches its
// flattened overrides; more than one (with any overrides at stake) is the
diff --git a/internal/manifestanalyzer/overrides_projection.go b/internal/manifestanalyzer/overrides_projection.go
index 5a2446485..4f96ed02c 100644
--- a/internal/manifestanalyzer/overrides_projection.go
+++ b/internal/manifestanalyzer/overrides_projection.go
@@ -4,6 +4,7 @@ package manifestanalyzer
import (
"fmt"
+ "math"
"sort"
"strconv"
"strings"
@@ -453,12 +454,33 @@ func ReplicaCountEdit(dm *DocumentModel, count int64) (OverrideEdit, bool) {
}, true
}
+// sourceReplicaCount reads spec.replicas off a source KRM document. This is a
+// manifest field, not kustomize configuration — kustomize's types cover the
+// kustomization.yaml, not the documents it renders — so the decoding lives here.
+// sigs.k8s.io/yaml decodes YAML numbers as float64 (via JSON), so an integral
+// float is accepted; anything else is not a replica count.
+func sourceReplicaCount(spec map[string]interface{}) (int64, bool) {
+ switch n := spec["replicas"].(type) {
+ case float64:
+ if n != math.Trunc(n) {
+ return 0, false
+ }
+ return int64(n), true
+ case int:
+ return int64(n), true
+ case int64:
+ return n, true
+ default:
+ return 0, false
+ }
+}
+
// restoreSourceReplicas puts spec.replicas back to the source document's form on
// the desired copy: the source's own value when it has one, absent when the
// source omits it (the transformer supplies the field either way).
func restoreSourceReplicas(gitRaw map[string]interface{}, out *unstructured.Unstructured) {
spec, _ := gitRaw["spec"].(map[string]interface{})
- if src, ok := integerField(spec, "replicas"); ok {
+ if src, ok := sourceReplicaCount(spec); ok {
_ = unstructured.SetNestedField(out.Object, src, "spec", "replicas")
return
}
diff --git a/internal/manifestanalyzer/overrides_test.go b/internal/manifestanalyzer/overrides_test.go
index 12d5d1bcf..468e032c8 100644
--- a/internal/manifestanalyzer/overrides_test.go
+++ b/internal/manifestanalyzer/overrides_test.go
@@ -127,9 +127,14 @@ func TestKustomizeOverridesNestedBaseIsNotARoot(t *testing.T) {
}
// TestKustomizationOverrideParsing pins the malformed-overrides boundary: a
-// kustomization whose images:/replicas: cannot be parsed is unsupported (it
-// would fail kustomize build, and we can no longer vouch for the render), while
-// well-formed entries keep the folder supported.
+// kustomization whose images:/replicas: kustomize itself cannot decode is
+// unsupported (it would fail kustomize build, and we can no longer vouch for the
+// render), while entries kustomize accepts keep the folder supported.
+//
+// Three of these cases changed when the hand-written key check was replaced by
+// kustomize's own type, and each was verified against a real `kustomize build`:
+// we now agree with the renderer where we previously did not. The two marked
+// "kustomize honours" were refused before — folders Flux renders happily.
func TestKustomizationOverrideParsing(t *testing.T) {
cases := []struct {
name string
@@ -139,16 +144,42 @@ func TestKustomizationOverrideParsing(t *testing.T) {
{"well-formed images", "images:\n - name: a/b\n newTag: \"1.2\"\n", false},
{"well-formed replicas", "replicas:\n - name: web\n count: 3\n", false},
{"empty images list", "images: []\n", false},
+ // kustomize decodes into a typed struct via JSON, so a key differing only in
+ // case still binds: `newtag` sets NewTag, and the build applies it. Refusing
+ // it would refuse a folder that renders.
+ {"kustomize honours a case-variant key", "images:\n - name: a/b\n newtag: \"1.2\"\n", false},
+ // An empty component is not a declared-but-broken transform; kustomize skips
+ // it and applies the rest of the entry.
+ {
+ "kustomize honours a blank newName",
+ "images:\n - name: a/b\n newName: \"\"\n newTag: \"1.2\"\n",
+ false,
+ },
{"images not a list", "images: oops\n", true},
{"images entry not a map", "images:\n - just-a-string\n", true},
{"images entry missing name", "images:\n - newTag: \"1.2\"\n", true},
- {"images unknown key", "images:\n - name: a/b\n newtag: \"1.2\"\n", true},
+ {"images entry genuinely unknown key", "images:\n - name: a/b\n bogus: \"1.2\"\n", true},
+ // tagSuffix is a real kustomize field we do not model. Decoding it would
+ // silently ignore a transform kustomize applies, so it refuses the folder.
+ {"images tagSuffix is not modelled", "images:\n - name: a/b\n tagSuffix: -rc1\n", true},
{"non-string newTag", "images:\n - name: a/b\n newTag: 1.29\n", true},
- {"blank newName", "images:\n - name: a/b\n newName: \"\"\n", true},
{"replicas count string", "replicas:\n - name: web\n count: \"3\"\n", true},
{"replicas count fractional", "replicas:\n - name: web\n count: 2.5\n", true},
{"replicas count negative", "replicas:\n - name: web\n count: -1\n", true},
{"replicas unknown key", "replicas:\n - name: web\n count: 3\n kind: Deployment\n", true},
+ // Holes the hand-written deny-list had: neither was on it, and both change
+ // the render. They are refused now because they are not in the modelled set.
+ {
+ "vars",
+ "vars:\n - name: NS\n objref:\n kind: Service\n name: web\n apiVersion: v1\n",
+ true,
+ },
+ {"validators (plugin code)", "validators:\n - check.yaml\n", true},
+ // imageTags is the deprecated spelling of images, and FixKustomization folds
+ // it into images exactly as the builder does — so it is supported, and its
+ // override is seen, where before it was silently ignored.
+ {"imageTags is normalised into images", "imageTags:\n - name: a/b\n newTag: \"1.2\"\n", false},
+ {"bases is normalised into resources", "bases:\n - ../base\n", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
diff --git a/internal/manifestanalyzer/scan_repo.go b/internal/manifestanalyzer/scan_repo.go
index f75343be3..ab980ca08 100644
--- a/internal/manifestanalyzer/scan_repo.go
+++ b/internal/manifestanalyzer/scan_repo.go
@@ -12,8 +12,6 @@ import (
"sort"
"strings"
- "sigs.k8s.io/yaml"
-
"github.com/ConfigButler/gitops-reverser/internal/git/manifestedit"
)
@@ -448,41 +446,14 @@ func refusedStructuralDetail(content []byte) string {
if len(features) == 0 {
return "kustomization uses an unsupported feature the operator cannot map back to editable source"
}
- return "kustomization uses unsupported feature(s): " + strings.Join(features, ", ")
-}
-
-// unsupportedKustomizeFeatures returns the sorted, de-duplicated unsupported features a
-// kustomization declares: the identity/generator transformers, remote bases, and
-// malformed images/replicas. It shares unsupportedKustomizeFeatureKeys with the
-// acceptance gate so the two never drift.
-func unsupportedKustomizeFeatures(content []byte) []string {
- raw := map[string]interface{}{}
- if err := yaml.Unmarshal(content, &raw); err != nil {
- return []string{"unparseable"}
- }
- seen := map[string]struct{}{}
- add := func(f string) { seen[f] = struct{}{} }
- for _, key := range unsupportedKustomizeFeatureKeys() {
- if v, ok := raw[key]; ok && !isEmptyValue(v) {
- add(key)
- }
- }
- resources := append(stringList(raw, "resources"), stringList(raw, "bases")...)
- if hasRemoteResource(resources) {
- add("remote-base")
- }
- if _, ok := parseImageOverrides(raw, ""); !ok {
- add("malformed-images")
- }
- if _, ok := parseReplicaOverrides(raw, ""); !ok {
- add("malformed-replicas")
+ detail := "kustomization uses unsupported feature(s): " + strings.Join(features, ", ")
+ // "unparseable" on its own says nothing a user can act on. kustomize's decoder
+ // knows exactly what is wrong — that a resources: is a string, or that the file
+ // is a Flux Kustomization CR rather than a build file — so quote it.
+ if err := kustomizationDecodeError(content); err != "" {
+ detail += " (" + err + ")"
}
- out := make([]string, 0, len(seen))
- for f := range seen {
- out = append(out, f)
- }
- sort.Strings(out)
- return out
+ return detail
}
// renderRootNamespace resolves the namespace a render root renders under: the
diff --git a/internal/manifestanalyzer/store.go b/internal/manifestanalyzer/store.go
index 45330de92..d43546f17 100644
--- a/internal/manifestanalyzer/store.go
+++ b/internal/manifestanalyzer/store.go
@@ -11,7 +11,6 @@ import (
"strings"
"k8s.io/apimachinery/pkg/runtime/schema"
- "sigs.k8s.io/yaml"
"github.com/ConfigButler/gitops-reverser/internal/git/manifestedit"
"github.com/ConfigButler/gitops-reverser/internal/types"
@@ -758,83 +757,16 @@ func collapseAssignments(nsByFile map[string]map[string]string) map[string]names
return out
}
-// parseKustomizations reads every kustomization.yaml into a kustomizationDoc keyed by
-// its directory. An unparseable kustomization, or one using an unsupported feature, is
-// kept but marked unsupported so it never acts as a namespace source.
-func parseKustomizations(files []manifestedit.FileContent) map[string]*kustomizationDoc {
- out := map[string]*kustomizationDoc{}
- for _, f := range files {
- if !isKustomizationFile(f.Path) {
- continue
- }
- doc := &kustomizationDoc{path: filepathToSlash(f.Path)}
- raw := map[string]interface{}{}
- if err := yaml.Unmarshal(f.Content, &raw); err != nil {
- doc.unsupported = true
- out[slashDir(f.Path)] = doc
- continue
- }
- doc.namespace = strings.TrimSpace(stringField(raw, "namespace"))
- doc.resources = append(stringList(raw, "resources"), stringList(raw, "bases")...)
- images, imagesOK := parseImageOverrides(raw, doc.path)
- replicas, replicasOK := parseReplicaOverrides(raw, doc.path)
- doc.images, doc.replicas = images, replicas
- doc.unsupported = hasUnsupportedKustomizeFeature(raw) || hasRemoteResource(doc.resources) ||
- !imagesOK || !replicasOK
- out[slashDir(f.Path)] = doc
- }
- return out
-}
-
-// kustomizationUsesUnsupportedFeature reports whether a kustomization.yaml's bytes use a
-// feature outside the supported contextual-namespace subset — the same predicate that
-// disqualifies it as a namespace source (parseKustomizations), reused by the acceptance
-// gate to refuse the folder at the retention site. An unparseable kustomization is treated
-// as unsupported: if we cannot read it, we cannot vouch for what it produces.
-func kustomizationUsesUnsupportedFeature(content []byte) bool {
- raw := map[string]interface{}{}
- if err := yaml.Unmarshal(content, &raw); err != nil {
- return true
- }
- resources := append(stringList(raw, "resources"), stringList(raw, "bases")...)
- _, imagesOK := parseImageOverrides(raw, "")
- _, replicasOK := parseReplicaOverrides(raw, "")
- return hasUnsupportedKustomizeFeature(raw) || hasRemoteResource(resources) || !imagesOK || !replicasOK
-}
-
-// unsupportedKustomizeFeatureKeys returns the kustomization fields that create resources
-// or mutate resource identity (name/namespace) in ways the contextual-namespace writer
-// cannot map back to an editable source document. Their presence disqualifies a
-// kustomization as a namespace source. It is the single source of truth for both the
-// boolean gate (hasUnsupportedKustomizeFeature) and the repo scan's per-feature
-// refusal detail (unsupportedKustomizeFeatures). It returns a fresh slice on every call,
-// so no shared state can be mutated.
-func unsupportedKustomizeFeatureKeys() []string {
- return []string{
- "generators", "configMapGenerator", "secretGenerator",
- "helmCharts", "helmGlobals", "helmChartInflationGenerator",
- "patches", "patchesStrategicMerge", "patchesJson6902",
- "replacements", "components", "transformers",
- "configurations", "openapi", "crds",
- "namePrefix", "nameSuffix",
- }
-}
-
-// hasUnsupportedKustomizeFeature reports whether a kustomization uses a field that
-// creates resources or mutates resource identity (name/namespace) in ways the
-// contextual-namespace writer cannot map back to an editable source document. Their
-// presence disqualifies a kustomization as a namespace source; benign transformers
-// (labels, annotations) do not. images/replicas are parsed separately (overrides.go)
-// and disqualify only when malformed.
-func hasUnsupportedKustomizeFeature(raw map[string]interface{}) bool {
- for _, key := range unsupportedKustomizeFeatureKeys() {
- if v, ok := raw[key]; ok && !isEmptyValue(v) {
- return true
- }
- }
- return false
-}
-
+// hasRemoteResource reports whether any resources/bases entry points outside this
+// repository. It is the one piece of kustomize semantics the operator must keep
+// owning rather than delegate to the library.
+//
+// kustomize resolves a remote base by shelling out to `git fetch`, and it does so
+// under LoadRestrictionsRootOnly and under an in-memory filesystem alike (both
+// measured). No build option turns that off. Detecting a remote entry ourselves,
+// and refusing before any build is invoked, is therefore what keeps "the operator
+// never fetches a remote base" true — see
+// docs/design/support-boundary/kustomize-support-boundary.md §7.
func hasRemoteResource(entries []string) bool {
for _, e := range entries {
if isRemoteResource(e) {
@@ -859,44 +791,6 @@ func isRemoteResource(entry string) bool {
return false
}
-func stringField(raw map[string]interface{}, key string) string {
- if v, ok := raw[key].(string); ok {
- return v
- }
- return ""
-}
-
-func stringList(raw map[string]interface{}, key string) []string {
- v, ok := raw[key].([]interface{})
- if !ok {
- return nil
- }
- out := make([]string, 0, len(v))
- for _, e := range v {
- if s, ok := e.(string); ok {
- if s = strings.TrimSpace(s); s != "" {
- out = append(out, s)
- }
- }
- }
- return out
-}
-
-func isEmptyValue(v interface{}) bool {
- switch t := v.(type) {
- case nil:
- return true
- case string:
- return strings.TrimSpace(t) == ""
- case []interface{}:
- return len(t) == 0
- case map[string]interface{}:
- return len(t) == 0
- default:
- return false
- }
-}
-
// cleanJoin resolves a kustomization resources entry against the kustomization's
// directory into a clean slash path. A trailing slash is dropped so a directory base
// keys the same as its kustomization dir. An entry that escapes the scanned root (via
diff --git a/test/fixtures/gitops-layouts/support-today.md b/test/fixtures/gitops-layouts/support-today.md
index 5c51101a0..e61d06be4 100644
--- a/test/fixtures/gitops-layouts/support-today.md
+++ b/test/fixtures/gitops-layouts/support-today.md
@@ -41,7 +41,7 @@ Reading rules:
| 3-expanded/flux-resourceset-pull-requests | 0 | All reported candidates accepted | 1 | 0 | plain=1 | - | None |
| 4-machine-written/flux-image-automation | 0 | All reported candidates accepted | 3 | 0 | kustomize-single=3 | - | None |
| 5-opaque/sops-encrypted | 0 | All reported candidates accepted | 3 | 0 | kustomize-single=2, plain=1 | - | None |
-| 6-hostile/mixed-and-hostile | 0 | Partial | 3 | 2 | kustomize-single=1, plain=4 | - | non-krm-yaml: ci/.gitlab-ci.yml: YAML is not a Kubernetes manifest
non-krm-yaml: ci/docker-compose.yml: YAML is not a Kubernetes manifest
foreign-file: empty-dir/.gitkeep: foreign file empty-dir/.gitkeep is not a managed manifest; remove it or name it in .gittargetignore
mixed-managed-allowlisted: kustomization.yaml: managed resource kustomize.toolkit.fluxcd.io/v1/Kustomization/flux-system/apps must not live in the allowlisted build-directive file kustomization.yaml
+8 more |
+| 6-hostile/mixed-and-hostile | 0 | Partial | 3 | 2 | plain=4, refused-structural=1 | unparseable | refused-structural: kustomization uses unsupported feature(s): unparseable (invalid Kustomization: json: unknown field "spec")
impure-managed-file: bundle.yaml: a file with managed resources may contain only valid KRM documents; document #1 is a non-KRM document
impure-managed-file: bundle.yaml: a file with managed resources may contain only valid KRM documents; document #2 is an empty document
foreign-file: deployment.json: foreign file deployment.json is not a managed manifest; remove it or name it in .gittargetignore |
## 1-desired-state/argocd-app-of-apps
@@ -239,11 +239,11 @@ Unsupported constructs: `none`. Fleet root: `false`.
## 6-hostile/mixed-and-hostile
Reported rc `0`. Accepted `3`, refused `2`.
-Unsupported constructs: `none`. Fleet root: `false`.
+Unsupported constructs: `unparseable`. Fleet root: `false`.
| Candidate | Layout | Accepted today | Namespace | rendered/editable/non-KRM | Refusal reasons |
|---|---|---|---|---|---|
-| `.` | `kustomize-single` | false | `backend` | 0/0/6 | non-krm-yaml: ci/.gitlab-ci.yml: YAML is not a Kubernetes manifest
non-krm-yaml: ci/docker-compose.yml: YAML is not a Kubernetes manifest
foreign-file: empty-dir/.gitkeep: foreign file empty-dir/.gitkeep is not a managed manifest; remove it or name it in .gittargetignore
mixed-managed-allowlisted: kustomization.yaml: managed resource kustomize.toolkit.fluxcd.io/v1/Kustomization/flux-system/apps must not live in the allowlisted build-directive file kustomization.yaml
impure-managed-file: mixed/bundle.yaml: a file with managed resources may contain only valid KRM documents; document #1 is a non-KRM document
impure-managed-file: mixed/bundle.yaml: a file with managed resources may contain only valid KRM documents; document #2 is an empty document
foreign-file: mixed/deployment.json: foreign file mixed/deployment.json is not a managed manifest; remove it or name it in .gittargetignore
invalid-yaml: templates/deployment.yaml: invalid YAML: yaml: line 21: did not find expected key
non-krm-yaml: values.yaml: YAML is not a Kubernetes manifest |
+| `.` | `refused-structural` | false | `backend` | 0/0/6 | refused-structural: kustomization uses unsupported feature(s): unparseable (invalid Kustomization: json: unknown field "spec") |
| `crossplane` | `plain` | true | `-` | 1/1/0 | none |
| `kro` | `plain` | true | `-` | 1/1/0 | none |
| `mixed` | `plain` | false | `-` | 3/3/2 | impure-managed-file: bundle.yaml: a file with managed resources may contain only valid KRM documents; document #1 is a non-KRM document
impure-managed-file: bundle.yaml: a file with managed resources may contain only valid KRM documents; document #2 is an empty document
foreign-file: deployment.json: foreign file deployment.json is not a managed manifest; remove it or name it in .gittargetignore |