package hostops import ( "context" "encoding/json" "reflect" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostfence" "github.com/carlosframework/platform/internal/hostresolve" ) // Local destruction is a separate declaration. An active target still takes // the data-loss path; its newly queued local chore needs a fresh inspection. // A committed move can discharge its retired source's local installation. func moveLocalLossEligible(c *hostclaim.Claim, box string) bool { if c == nil || c.Holder == nil || box == "" || c.Op.Exec == box { return false } inactive := slicesContain(c.Lost, box) || (c.Op.Kind == hostclaim.KindLanding && c.Op.Phase == hostclaim.PhaseCommitted) || !slicesContain(c.Op.Participants, box) if !inactive { return false } for _, o := range c.Obligations { if o.Kind == "park-local" && o.Box == box { return true } } return false } func isMoveLocalLoss(actual hostclaim.LossConsequences) bool { return hostclaim.IsDestroyedLocalOnly(actual) } // The signed claim ETag binds each original operation, chore and retirement // identity to its inventory. The operator attests exactly this displayed set. func moveLocalLossConsequences(c *hostclaim.Claim, box string) (hostfence.Consequences, hostclaim.LossConsequences, error) { if !moveLocalLossEligible(c, box) { return hostfence.Consequences{}, hostclaim.LossConsequences{}, hostresolve.ErrResolve } shown := hostfence.Consequences{Transition: "destroyed-local-only"} actual := hostclaim.LossConsequences{RecoveryIntent: json.RawMessage(`{"kind":"destroyed-local-only"}`)} for _, o := range c.Obligations { if o.Kind != "park-local" || o.Box != box { continue } ref := hostclaim.ObligationRef{ID: o.ID, Op: o.Op, Retirement: o.Retirement} key := o.Op + "/" + o.ID + "/" + o.Retirement shown.DischargeLocal = append(shown.DischargeLocal, ref) shown.DestroyedStorageAssertion = append(shown.DestroyedStorageAssertion, key) actual.DischargeLocal = append(actual.DischargeLocal, ref) actual.DestroyedStorage = append(actual.DestroyedStorage, key) } return shown, actual, nil } // No record rollback, release, execution handover or post-commit effect is // authorized by this declaration. The generic claim CAS is its entire effect. func (m *moveLossOwner) localLossHook(ctx context.Context, o hostclaim.Observed, loss hostclaim.LossRecord, finish bool) error { durable, err := m.durableLoss(ctx, loss) if err != nil { return err } if durable.Validate() != nil || !reflect.DeepEqual(durable.ConfirmedConsequences, loss.ConfirmedConsequences) || durable.Host != o.Host { return hostresolve.ErrResolve } if finish { if durable.State != "committed" || o.Claim == nil || !slicesContain(o.Claim.LossRefs, loss.ID) { return hostresolve.ErrResolve } return nil } if durable.State == "committed" { return nil } if durable.State != "prepared" || o.Absent || o.ETag != durable.ExpectedClaimETag { return hostresolve.ErrResolve } _, want, err := moveLocalLossConsequences(o.Claim, durable.Box) if err != nil || !reflect.DeepEqual(want, durable.ConfirmedConsequences) { return hostresolve.ErrResolve } return nil }