| 1 | package hostops |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/sha256" |
| 6 | "fmt" |
| 7 | "sort" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/hostactors" |
| 10 | "github.com/carlosframework/platform/internal/hostclaim" |
| 11 | ) |
| 12 | |
| 13 | // Each exact saved host observation owns a distinct lifecycle reservation. |
| 14 | func lifecycleHoldID(before hostclaim.Observed, id string) string { |
| 15 | sum := sha256.Sum256([]byte(id + "\x00" + before.Host + "\x00" + before.ETag)) |
| 16 | return fmt.Sprintf("lifecycle-%x", sum[:]) |
| 17 | } |
| 18 | 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) { |
| 19 | if err := check(); err != nil { |
| 20 | return hostclaim.Observed{}, err |
| 21 | } |
| 22 | holdID := lifecycleHoldID(before, id) |
| 23 | roster, err := a.Directory.Read(ctx) |
| 24 | if err != nil { |
| 25 | return hostclaim.Observed{}, err |
| 26 | } |
| 27 | if hold, exists := roster.Roster.Holds[holdID]; exists { |
| 28 | if hold.Host != before.Host || hold.OpID != id || hold.ExpectedClaimETag != before.ETag { |
| 29 | return hostclaim.Observed{}, ErrIdentity |
| 30 | } |
| 31 | } else { |
| 32 | if _, err = a.Directory.ReserveLifecycle(ctx, a.Signer, a.Evidence, scope, before, holdID, id, a.Now()); err != nil { |
| 33 | // A failed publication may have committed. CancelHold proves attachment |
| 34 | // absence and invalidates the saved claim revision before removing it. |
| 35 | _ = a.Directory.CancelHold(ctx, a.Signer, holdID) |
| 36 | return hostclaim.Observed{}, err |
| 37 | } |
| 38 | } |
| 39 | if err = check(); err == nil { |
| 40 | var started hostclaim.Observed |
| 41 | started, err = a.Directory.Start(ctx, before, holdID, transition) |
| 42 | if err == nil { |
| 43 | return started, nil |
| 44 | } |
| 45 | } |
| 46 | // Read-through resolves an uncertain successful start without dropping its hold. |
| 47 | actual, readErr := hostclaim.Read(ctx, a.Store, before.Host) |
| 48 | if readErr == nil && !actual.Absent && actual.Claim.Op.ID == id && actual.Claim.Op.Intent["actors_hold"] == holdID { |
| 49 | return actual, nil |
| 50 | } |
| 51 | _ = a.Directory.CancelHold(ctx, a.Signer, holdID) |
| 52 | return hostclaim.Observed{}, err |
| 53 | } |
| 54 | func (a ProductionAuthority) lifecycleScope(ctx context.Context, before hostclaim.Observed) (hostactors.Scope, []string, []string, error) { |
| 55 | regions, boxes, err := a.Inventory(ctx, before) |
| 56 | if err != nil { |
| 57 | return hostactors.Scope{}, nil, nil, err |
| 58 | } |
| 59 | roster, err := a.Directory.Read(ctx) |
| 60 | if err != nil { |
| 61 | return hostactors.Scope{}, nil, nil, err |
| 62 | } |
| 63 | set := map[string]bool{} |
| 64 | for _, box := range roster.Roster.Boxes { |
| 65 | if box.Namespace == a.Namespace { |
| 66 | set[box.Pool] = true |
| 67 | } |
| 68 | } |
| 69 | var pools []string |
| 70 | for pool := range set { |
| 71 | pools = append(pools, pool) |
| 72 | } |
| 73 | sort.Strings(pools) |
| 74 | return hostactors.Scope{Namespace: a.Namespace, Regions: regions, Pools: pools, Participants: before.Claim.Op.Participants, ConsoleID: a.ConsoleID}, regions, boxes, nil |
| 75 | } |
| 76 | func (a ProductionAuthority) startAppLifecycle(ctx context.Context, before hostclaim.Observed, account, app, appID, id, by, target string, placing bool) (hostclaim.Observed, error) { |
| 77 | if before.Absent || before.Claim.Holder == nil || before.Claim.Op.Kind != hostclaim.KindServing { |
| 78 | return hostclaim.Observed{}, ErrPending |
| 79 | } |
| 80 | if before.Claim.Holder.Account != account || before.Claim.Holder.App != app { |
| 81 | return hostclaim.Observed{}, ErrIdentity |
| 82 | } |
| 83 | kind := "purging" |
| 84 | if placing { |
| 85 | kind = "placing" |
| 86 | } |
| 87 | check := func() error { |
| 88 | admission, err := ReadAdmission(ctx, a.Store, account, app, appID) |
| 89 | if err != nil { |
| 90 | return err |
| 91 | } |
| 92 | if admission.Value.State != "closed" || admission.Value.Op == nil || admission.Value.Op.ID != id || admission.Value.Op.Kind != kind || len(admission.Value.Registrations) != 0 { |
| 93 | return ErrPending |
| 94 | } |
| 95 | for _, effect := range admission.Value.Effects { |
| 96 | if effect.State != "complete" { |
| 97 | return ErrPending |
| 98 | } |
| 99 | } |
| 100 | if placing && admission.Value.Op.Phase != "claimed" { |
| 101 | return ErrPending |
| 102 | } |
| 103 | if !placing { |
| 104 | journal, _, err := ReadPurgeJournal(ctx, a.Store, account, app, id, appID) |
| 105 | if err != nil { |
| 106 | return err |
| 107 | } |
| 108 | if journal.Phase != "adopted" { |
| 109 | return ErrPending |
| 110 | } |
| 111 | } |
| 112 | return nil |
| 113 | } |
| 114 | scope, regions, boxes, err := a.lifecycleScope(ctx, before) |
| 115 | if err != nil { |
| 116 | return hostclaim.Observed{}, err |
| 117 | } |
| 118 | fenced := "" |
| 119 | if !placing { |
| 120 | // Ruling 86: a purge passes the completion box it verified dead. |
| 121 | if fenced, err = a.completionFence(ctx, before.Claim); err != nil { |
| 122 | return hostclaim.Observed{}, err |
| 123 | } |
| 124 | } |
| 125 | return a.startHeldClaim(ctx, before, id, scope, check, func(claim *hostclaim.Claim) error { |
| 126 | transition := BeginPurge(id, by, a.Now(), regions, boxes, fenced) |
| 127 | if placing { |
| 128 | transition = BeginPlacement(id, by, target, a.Now(), regions, boxes) |
| 129 | } |
| 130 | if err := transition(claim); err != nil { |
| 131 | return err |
| 132 | } |
| 133 | if claim.Op.Intent == nil { |
| 134 | claim.Op.Intent = map[string]string{} |
| 135 | } |
| 136 | claim.Op.Intent["account"] = account |
| 137 | claim.Op.Intent["app"] = app |
| 138 | claim.Op.Intent["app_id"] = appID |
| 139 | return nil |
| 140 | }) |
| 141 | } |
| 142 | func (a ProductionAuthority) StartPurgeClaim(ctx context.Context, before hostclaim.Observed, account, app, appID, id, by string) (hostclaim.Observed, error) { |
| 143 | return a.startAppLifecycle(ctx, before, account, app, appID, id, by, "", false) |
| 144 | } |
| 145 | func (a ProductionAuthority) StartPlacementClaim(ctx context.Context, before hostclaim.Observed, account, app, appID, id, by, target string) (hostclaim.Observed, error) { |
| 146 | return a.startAppLifecycle(ctx, before, account, app, appID, id, by, target, true) |
| 147 | } |
| 148 | |