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
5 changes: 5 additions & 0 deletions scripts/replace-wedding-standby/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion scripts/replace-wedding-standby/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
29 changes: 29 additions & 0 deletions scripts/replace-wedding-standby/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Loading