package hostops import ( "context" "crypto/sha256" "fmt" "sort" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostclaim" ) // Each exact saved host observation owns a distinct lifecycle reservation. func lifecycleHoldID(before hostclaim.Observed, id string) string { sum := sha256.Sum256([]byte(id + "\x00" + before.Host + "\x00" + before.ETag)) return fmt.Sprintf("lifecycle-%x", sum[:]) } func (a ProductionAuthority) startHeldClaim(ctx context.Context, before hostclaim.Observed, id string, scope hostactors.Scope, check func() error, transition func(*hostclaim.Claim) error) (hostclaim.Observed, error) { if err := check(); err != nil { return hostclaim.Observed{}, err } holdID := lifecycleHoldID(before, id) roster, err := a.Directory.Read(ctx) if err != nil { return hostclaim.Observed{}, err } if hold, exists := roster.Roster.Holds[holdID]; exists { if hold.Host != before.Host || hold.OpID != id || hold.ExpectedClaimETag != before.ETag { return hostclaim.Observed{}, ErrIdentity } } else { if _, err = a.Directory.ReserveLifecycle(ctx, a.Signer, a.Evidence, scope, before, holdID, id, a.Now()); err != nil { // A failed publication may have committed. CancelHold proves attachment // absence and invalidates the saved claim revision before removing it. _ = a.Directory.CancelHold(ctx, a.Signer, holdID) return hostclaim.Observed{}, err } } if err = check(); err == nil { var started hostclaim.Observed started, err = a.Directory.Start(ctx, before, holdID, transition) if err == nil { return started, nil } } // Read-through resolves an uncertain successful start without dropping its hold. actual, readErr := hostclaim.Read(ctx, a.Store, before.Host) if readErr == nil && !actual.Absent && actual.Claim.Op.ID == id && actual.Claim.Op.Intent["actors_hold"] == holdID { return actual, nil } _ = a.Directory.CancelHold(ctx, a.Signer, holdID) return hostclaim.Observed{}, err } func (a ProductionAuthority) lifecycleScope(ctx context.Context, before hostclaim.Observed) (hostactors.Scope, []string, []string, error) { regions, boxes, err := a.Inventory(ctx, before) if err != nil { return hostactors.Scope{}, nil, nil, err } roster, err := a.Directory.Read(ctx) if err != nil { return hostactors.Scope{}, nil, nil, err } set := map[string]bool{} for _, box := range roster.Roster.Boxes { if box.Namespace == a.Namespace { set[box.Pool] = true } } var pools []string for pool := range set { pools = append(pools, pool) } sort.Strings(pools) return hostactors.Scope{Namespace: a.Namespace, Regions: regions, Pools: pools, Participants: before.Claim.Op.Participants, ConsoleID: a.ConsoleID}, regions, boxes, nil } func (a ProductionAuthority) startAppLifecycle(ctx context.Context, before hostclaim.Observed, account, app, appID, id, by, target string, placing bool) (hostclaim.Observed, error) { if before.Absent || before.Claim.Holder == nil || before.Claim.Op.Kind != hostclaim.KindServing { return hostclaim.Observed{}, ErrPending } if before.Claim.Holder.Account != account || before.Claim.Holder.App != app { return hostclaim.Observed{}, ErrIdentity } kind := "purging" if placing { kind = "placing" } check := func() error { admission, err := ReadAdmission(ctx, a.Store, account, app, appID) if err != nil { return err } if admission.Value.State != "closed" || admission.Value.Op == nil || admission.Value.Op.ID != id || admission.Value.Op.Kind != kind || len(admission.Value.Registrations) != 0 { return ErrPending } for _, effect := range admission.Value.Effects { if effect.State != "complete" { return ErrPending } } if placing && admission.Value.Op.Phase != "claimed" { return ErrPending } if !placing { journal, _, err := ReadPurgeJournal(ctx, a.Store, account, app, id, appID) if err != nil { return err } if journal.Phase != "adopted" { return ErrPending } } return nil } scope, regions, boxes, err := a.lifecycleScope(ctx, before) if err != nil { return hostclaim.Observed{}, err } fenced := "" if !placing { // Ruling 86: a purge passes the completion box it verified dead. if fenced, err = a.completionFence(ctx, before.Claim); err != nil { return hostclaim.Observed{}, err } } return a.startHeldClaim(ctx, before, id, scope, check, func(claim *hostclaim.Claim) error { transition := BeginPurge(id, by, a.Now(), regions, boxes, fenced) if placing { transition = BeginPlacement(id, by, target, a.Now(), regions, boxes) } if err := transition(claim); err != nil { return err } if claim.Op.Intent == nil { claim.Op.Intent = map[string]string{} } claim.Op.Intent["account"] = account claim.Op.Intent["app"] = app claim.Op.Intent["app_id"] = appID return nil }) } func (a ProductionAuthority) StartPurgeClaim(ctx context.Context, before hostclaim.Observed, account, app, appID, id, by string) (hostclaim.Observed, error) { return a.startAppLifecycle(ctx, before, account, app, appID, id, by, "", false) } func (a ProductionAuthority) StartPlacementClaim(ctx context.Context, before hostclaim.Observed, account, app, appID, id, by, target string) (hostclaim.Observed, error) { return a.startAppLifecycle(ctx, before, account, app, appID, id, by, target, true) }