diff --git a/scripts/replace-wedding-standby/README.md b/scripts/replace-wedding-standby/README.md index b61f40e9ac..1264fb1f1b 100644 --- a/scripts/replace-wedding-standby/README.md +++ b/scripts/replace-wedding-standby/README.md @@ -29,6 +29,11 @@ three Ready instances, fresh replacement storage, healthy archiving, and the old claim and volume still bound to their original identities. Only then may ordinary maintenance proceed. This proves database repair, not overall platform health. +Every write names the field manager `wedding-standby-repair`. Flux takes over +fields written by a default `kubectl` manager and removes what Git does not +declare, so a fence written that way is deleted at the next reconciliation and +the repair stops before the claim is detached. + ## Read-only plan From current main, get the current Cluster and failed Pod UIDs with OIDC reads, diff --git a/scripts/replace-wedding-standby/main.go b/scripts/replace-wedding-standby/main.go index 2e63b4a55f..5c07faff46 100644 --- a/scripts/replace-wedding-standby/main.go +++ b/scripts/replace-wedding-standby/main.go @@ -137,12 +137,17 @@ func testPath(path string, v any) object { return object{"op": "test", "path": p // editPath creates one reviewed JSON patch operation. func editPath(op, path string, v any) object { return object{"op": op, "path": path, "value": v} } +// fieldManager attributes every repair write to this procedure. Flux takes over +// fields whose manager starts with "kubectl" and removes what Git does not +// declare, which deleted the fence mid-repair on 2026-10-09. +const fieldManager = "wedding-standby-repair" + // patch scopes mutations to the observed resource and explicit API group. func (c client) patch(ctx context.Context, kind string, o object, ops []object) error { if kind == "cluster" { kind = "cluster.postgresql.cnpg.io" } - args := []string{"patch", kind, id(o).name, "--type=json", "--patch-file=/dev/stdin"} + args := []string{"patch", kind, id(o).name, "--type=json", "--patch-file=/dev/stdin", "--field-manager=" + fieldManager} if kind != "pv" { args = append(args, "-n", namespace) } diff --git a/scripts/replace-wedding-standby/main_test.go b/scripts/replace-wedding-standby/main_test.go index 9da62b75ee..e533b6b1fa 100644 --- a/scripts/replace-wedding-standby/main_test.go +++ b/scripts/replace-wedding-standby/main_test.go @@ -540,3 +540,32 @@ func TestFixtureDecodesLikeKubernetes(t *testing.T) { t.Fatal(err) } } + +// TestPatchUsesOwnFieldManager keeps Flux from reverting the repair's writes. +// kustomize-controller takes over every field whose manager starts with +// "kubectl" and then removes what the Git source does not declare, so a fence +// written by a default kubectl patch disappears on the next reconciliation. +func TestPatchUsesOwnFieldManager(t *testing.T) { + for _, kind := range []string{"pv", "cluster", "pvc"} { + var got []string + c := client{command: func(_ context.Context, args []string, _ []byte) ([]byte, error) { + got = args + return nil, nil + }} + if err := c.patch(context.Background(), kind, object{"metadata": map[string]any{"name": "n", "uid": "u"}}, nil); err != nil { + t.Fatal(err) + } + managers := 0 + for _, arg := range got { + if name, ok := strings.CutPrefix(arg, "--field-manager="); ok { + managers++ + if name == "" || strings.HasPrefix(name, "kubectl") || name == "before-first-apply" || name == "kustomize-controller" { + t.Fatalf("%s patch uses field manager %q, which Flux reverts", kind, name) + } + } + } + if managers != 1 { + t.Fatalf("%s patch names %d field managers, want exactly 1: %v", kind, managers, got) + } + } +}