provision, hostops: the shared retirement entry point (claim Plan 5 Task 3)
Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+125
−0
internal/hostops/retirement.go+72 −0internal/provision/retirement.go+53 −0
diff --git a/internal/hostops/retirement.go b/internal/hostops/retirement.go| new file mode 100644 |
| index 000000000..573d30b40 |
| --- /dev/null |
| +++ b/internal/hostops/retirement.go |
| @@ -0,0 +1,72 @@ |
| +package hostops |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +func (c Controller) RetireRecord(ctx context.Context, ref provision.RetireRef) error { |
| + if err := c.ready(); err != nil { |
| + return err |
| + } |
| + key := provision.RecordKey(ref.Account, ref.App, ref.Host) |
| + if ref.Family == "sidecar" { |
| + key = provision.SidecarRecordKey(ref.Account, ref.App, ref.Name) |
| + } |
| + rec, rerr := ReadRecord(ctx, c.Store, key) |
| + if rerr != nil && !errors.Is(rerr, os.ErrNotExist) { |
| + return rerr |
| + } |
| + obs, err := hostclaim.Read(ctx, c.Store, ref.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if obs.Absent { |
| + if errors.Is(rerr, os.ErrNotExist) { |
| + return nil |
| + } |
| + obs, err = c.Adopt(ctx, rec) |
| + if err != nil { |
| + return err |
| + } |
| + } |
| + h := obs.Claim.Holder |
| + if h == nil { |
| + if rerr == nil { |
| + _, err = RemoveOrphan(ctx, c.Store, rec) |
| + return err |
| + } |
| + if obs.Claim.Free() { |
| + return nil |
| + } |
| + return ErrPending |
| + } |
| + if h.Key != key || h.Family != ref.Family || h.Account != ref.Account || h.App != ref.App || (ref.Incarnation != "" && ref.Incarnation != h.Incarnation) { |
| + return ErrIdentity |
| + } |
| + if rerr == nil && !matches(rec, h) { |
| + return ErrIdentity |
| + } |
| + switch obs.Claim.Op.Kind { |
| + case hostclaim.KindServing: |
| + if rerr != nil { |
| + return ErrPending |
| + } // Old-console deletion needs operator recovery. |
| + obs, err = c.BeginRetire(ctx, key, ref.By, h.Incarnation) |
| + if err != nil { |
| + return err |
| + } |
| + return c.Retire(ctx, ref.Host, obs.Claim.Op.ID) |
| + case hostclaim.KindDeleting: |
| + return c.Retire(ctx, ref.Host, obs.Claim.Op.ID) |
| + case hostclaim.KindParking: |
| + // Record withdrawal completed. Chores still block reuse in the claim. |
| + return nil |
| + default: |
| + return ErrPending // A purge is driven by its journal, never this path. |
| + } |
| +} |
diff --git a/internal/provision/retirement.go b/internal/provision/retirement.go| new file mode 100644 |
| index 000000000..aab7a2564 |
| --- /dev/null |
| +++ b/internal/provision/retirement.go |
| @@ -0,0 +1,53 @@ |
| +package provision |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + |
| + "github.com/carlosframework/platform/internal/controlname" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +type RetireRef struct { |
| + Family, Account, App, Host, Name, By string |
| + // Required by rollbacks: never retire a successor to the failed attempt. |
| + Incarnation string |
| +} |
| +type ClaimRetirer interface { |
| + RetireRecord(context.Context, RetireRef) error |
| +} |
| +type RetireOptions struct { |
| + HostClaim bool |
| + Claims ClaimRetirer |
| +} |
| + |
| +var ErrClaimRetirer = errors.New("provision: claim retirement unavailable") |
| + |
| +func RetireRecord(ctx context.Context, s release.Store, ref RetireRef, opt RetireOptions) error { |
| + if !opt.HostClaim { |
| + switch ref.Family { |
| + case "instance": |
| + return DeleteRecord(ctx, s, ref.Account, ref.App, ref.Host) |
| + case "sidecar": |
| + return DeleteSidecarRecord(ctx, s, ref.Account, ref.App, ref.Name) |
| + default: |
| + return ErrClaimRetirer |
| + } |
| + } |
| + if opt.Claims == nil || !controlname.ValidAccount(ref.Account) || !controlname.ValidApp(ref.App) || !controlname.ValidHostShape(ref.Host) { |
| + return ErrClaimRetirer |
| + } |
| + switch ref.Family { |
| + case "instance": |
| + if ref.Name != "" { |
| + return ErrClaimRetirer |
| + } |
| + case "sidecar": |
| + if !controlname.ValidLabel(ref.Name) || ref.Host != SidecarHost(ref.Name, ref.App, ref.Account) { |
| + return ErrClaimRetirer |
| + } |
| + default: |
| + return ErrClaimRetirer |
| + } |
| + return opt.Claims.RetireRecord(ctx, ref) |
| +} |