carlos / platform Public

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>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev 48db5f00fa61de87d164bad0ecba0bff482f6161 parent 3f1db4e
2 files changed, +125 −0
  • internal/hostops/retirement.go +72 −0
  • internal/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)
+}