From 3b2adda1c88c67cbd2a2bee8fc4fbdf99fa24f98 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 6 Oct 2026 14:34:16 +0200 Subject: [PATCH] Say a mark-only provider's retirement as mark only A provider that cannot disable keeps the consumer reachable until a person deletes it (hq ADR 0230); the listing and the approval say so instead of claiming access was disabled. --- cmd/mesh-controller/retire_verbs.go | 12 ++++++++++-- cmd/mesh-controller/retirement_test.go | 23 +++++++++++++++++++++++ internal/link/retirement.go | 23 +++++++++++++++-------- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/cmd/mesh-controller/retire_verbs.go b/cmd/mesh-controller/retire_verbs.go index 681b3a7..30cb4e2 100644 --- a/cmd/mesh-controller/retire_verbs.go +++ b/cmd/mesh-controller/retire_verbs.go @@ -195,7 +195,10 @@ func answerRetirement(ctx context.Context, conn *nats.Conn, p providerInstance, if answer.Error != "" { return fmt.Errorf("%s on %s refused: %s", p.Module, p.Node, answer.Error) } - if approve { + if approve && state.RetiresBy == "mark-only" { + fmt.Printf("%s on %s marked %s retired, MARK ONLY: this provider cannot disable a consumer, so they keep "+ + "their access until `cleanup delete`; `cleanup list` shows them\n", p.Module, p.Node, strings.Join(names, ", ")) + } else if approve { fmt.Printf("%s on %s retired %s: access disabled, data kept; `cleanup list` shows them\n", p.Module, p.Node, strings.Join(names, ", ")) } else { @@ -280,6 +283,8 @@ type retiredRow struct { AgeDays int `json:"age-days"` SizeBytes *int64 `json:"size-bytes,omitempty"` Why string `json:"why,omitempty"` + // Access is "kept" for a consumer of a mark-only provider: retired on record, still reachable. + Access string `json:"access,omitempty"` } // retiredListing is every provider's retired consumers, and the providers that could not say. @@ -311,7 +316,7 @@ func retiredOf(ctx context.Context, conn *nats.Conn, instances []providerInstanc } for _, c := range state.Retired { row := retiredRow{Node: p.Node, Module: p.Module, Consumer: c.Consumer, ConsumerNode: c.Node, Kind: c.Kind, - RetiredAt: c.RetiredAt, AgeDays: -1, SizeBytes: c.SizeBytes, Why: c.Why} + RetiredAt: c.RetiredAt, AgeDays: -1, SizeBytes: c.SizeBytes, Why: c.Why, Access: c.Access} if at := retiredAt(c); !at.IsZero() { row.AgeDays = int(now.Sub(at).Hours() / 24) } @@ -338,6 +343,9 @@ func printRetired(l retiredListing, asJSON bool) error { if r.Kind != "" && r.Kind != "consumer" { kind = " [" + r.Kind + "]" } + if r.Access == "kept" { + kind += " [MARK ONLY: access kept until deleted]" + } fmt.Printf("%s on %s: %s%s — retired %s, %s, %s\n %s\n", r.Module, r.Node, r.Consumer, kind, age, sizeWords(r.SizeBytes), r.RetiredAt, orWhy("", r.Why)) } diff --git a/cmd/mesh-controller/retirement_test.go b/cmd/mesh-controller/retirement_test.go index 8b3a089..2784f09 100644 --- a/cmd/mesh-controller/retirement_test.go +++ b/cmd/mesh-controller/retirement_test.go @@ -458,3 +458,26 @@ func TestTheRetireAndCleanupVerbsComposeTheirCommandLines(t *testing.T) { t.Error("the generic verb would let a retirement or a deletion through without a why") } } + +// A mark-only provider's retired consumer keeps its access; the listing and the approval say so +// rather than claiming it was disabled (ADR 0230). +func TestNatsAMarkOnlyRetirementIsSaidAsSuch(t *testing.T) { + conn := onATestBus(t) + fake := &fakeProvider{} + kept := retiredDaysAgo("ledger", 3) + kept.Access = "kept" + fake.state.Retired = []link.RetiredConsumer{kept} + fake.state.RetiresBy = "mark-only" + fake.state.Waiting = &link.RetirementWaiting{Consumers: []link.RetiredConsumer{{Consumer: "a"}, {Consumer: "b"}}, Held: 2} + fake.serve(t, conn, "vault-mark", "anchor") + p := providerInstance{Node: "anchor", Module: "vault-mark"} + listing := retiredOf(t.Context(), conn, []providerInstance{p}, time.Now()) + said := printed(t, func() error { return printRetired(listing, false) }) + if !strings.Contains(said, "MARK ONLY") || listing.Retired[0].Access != "kept" { + t.Fatalf("%s %+v", said, listing) + } + said = printed(t, func() error { return answerRetirement(t.Context(), conn, p, true, whyFlags(t, "gone")) }) + if !strings.Contains(said, "MARK ONLY") || strings.Contains(said, "access disabled") { + t.Fatal(said) + } +} diff --git a/internal/link/retirement.go b/internal/link/retirement.go index 9a1f70b..fe6b532 100644 --- a/internal/link/retirement.go +++ b/internal/link/retirement.go @@ -49,6 +49,9 @@ type RetiredConsumer struct { SizeBytes *int64 `json:"size_bytes,omitempty"` // Kind is "consumer", or a backend's own word for something set aside ("set-aside-database"). Kind string `json:"kind,omitempty"` + // Access is "disabled" when the provider disabled it, "kept" for a mark-only provider whose consumer + // keeps its access until a person deletes it (ADR 0230); absent is disabled. + Access string `json:"access,omitempty"` } // Retirement is one provider's word about consumers the mesh stopped asking for. @@ -230,12 +233,16 @@ type RetirementRejected struct { // RetirementState is a provider's answer to provisioner_retirement. type RetirementState struct { - Resource string `json:"resource"` - Node string `json:"node"` - Held []string `json:"held"` - StablePasses int `json:"stable_passes"` - Bound string `json:"bound"` - Waiting *RetirementWaiting `json:"waiting"` - Rejected *RetirementRejected `json:"rejected"` - Retired []RetiredConsumer `json:"retired"` + Resource string `json:"resource"` + Node string `json:"node"` + Held []string `json:"held"` + StablePasses int `json:"stable_passes"` + // StableForSeconds is how long the same unasked set must hold as well (ADR 0230: ten minutes). + StableForSeconds int `json:"stable_for_seconds,omitempty"` + // RetiresBy is "disable", or "mark-only" for a provider that cannot disable a consumer. + RetiresBy string `json:"retires_by,omitempty"` + Bound string `json:"bound"` + Waiting *RetirementWaiting `json:"waiting"` + Rejected *RetirementRejected `json:"rejected"` + Retired []RetiredConsumer `json:"retired"` }