carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
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.
14func 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}
18func (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}
54func (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}
76func (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}
142func (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}
145func (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