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.
This commit is contained in:
jochen
2026-10-06 14:41:15 +02:00
parent 68009b16fe
commit 3b2adda1c8
3 changed files with 48 additions and 10 deletions
+10 -2
View File
@@ -195,7 +195,10 @@ func answerRetirement(ctx context.Context, conn *nats.Conn, p providerInstance,
if answer.Error != "" { if answer.Error != "" {
return fmt.Errorf("%s on %s refused: %s", p.Module, p.Node, 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, fmt.Printf("%s on %s retired %s: access disabled, data kept; `cleanup list` shows them\n", p.Module, p.Node,
strings.Join(names, ", ")) strings.Join(names, ", "))
} else { } else {
@@ -280,6 +283,8 @@ type retiredRow struct {
AgeDays int `json:"age-days"` AgeDays int `json:"age-days"`
SizeBytes *int64 `json:"size-bytes,omitempty"` SizeBytes *int64 `json:"size-bytes,omitempty"`
Why string `json:"why,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. // 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 { for _, c := range state.Retired {
row := retiredRow{Node: p.Node, Module: p.Module, Consumer: c.Consumer, ConsumerNode: c.Node, Kind: c.Kind, 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() { if at := retiredAt(c); !at.IsZero() {
row.AgeDays = int(now.Sub(at).Hours() / 24) 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" { if r.Kind != "" && r.Kind != "consumer" {
kind = " [" + r.Kind + "]" 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, 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)) sizeWords(r.SizeBytes), r.RetiredAt, orWhy("", r.Why))
} }
+23
View File
@@ -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") 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)
}
}
+15 -8
View File
@@ -49,6 +49,9 @@ type RetiredConsumer struct {
SizeBytes *int64 `json:"size_bytes,omitempty"` SizeBytes *int64 `json:"size_bytes,omitempty"`
// Kind is "consumer", or a backend's own word for something set aside ("set-aside-database"). // Kind is "consumer", or a backend's own word for something set aside ("set-aside-database").
Kind string `json:"kind,omitempty"` 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. // 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. // RetirementState is a provider's answer to provisioner_retirement.
type RetirementState struct { type RetirementState struct {
Resource string `json:"resource"` Resource string `json:"resource"`
Node string `json:"node"` Node string `json:"node"`
Held []string `json:"held"` Held []string `json:"held"`
StablePasses int `json:"stable_passes"` StablePasses int `json:"stable_passes"`
Bound string `json:"bound"` // StableForSeconds is how long the same unasked set must hold as well (ADR 0230: ten minutes).
Waiting *RetirementWaiting `json:"waiting"` StableForSeconds int `json:"stable_for_seconds,omitempty"`
Rejected *RetirementRejected `json:"rejected"` // RetiresBy is "disable", or "mark-only" for a provider that cannot disable a consumer.
Retired []RetiredConsumer `json:"retired"` RetiresBy string `json:"retires_by,omitempty"`
Bound string `json:"bound"`
Waiting *RetirementWaiting `json:"waiting"`
Rejected *RetirementRejected `json:"rejected"`
Retired []RetiredConsumer `json:"retired"`
} }