carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostresolve
2
3import (
4 "context"
5 "encoding/json"
6 "github.com/carlosframework/platform/internal/hostactors"
7 "github.com/carlosframework/platform/internal/hostclaim"
8 "github.com/carlosframework/platform/internal/hostfloor"
9 "github.com/carlosframework/platform/internal/hostquarantine"
10 "reflect"
11 "slices"
12 "strings"
13 "time"
14)
15
16// Readmit keeps the global deny until every declaration, claim and receipt
17// check is complete. It grants no executor or writer session.
18func (s Service) Readmit(ctx context.Context, box, enrollment string, revision uint64, lossIDs []string) error {
19 before, err := s.Directory.Read(ctx)
20 if err != nil {
21 return err
22 }
23 b, ok := before.Roster.Boxes[box]
24 if !ok || b.Enrollment != enrollment || b.State != "quarantined" || b.QuarantineRevision != revision || !reflect.DeepEqual(b.LossIDs, lossIDs) {
25 return ErrResolve
26 }
27 fenced, err := s.Directory.TerminationFence(ctx, before, box, s.Now())
28 if err != nil {
29 return err
30 }
31 if fenced {
32 return ErrResolve
33 }
34 report, err := s.Directory.PrivateBoxReport(ctx, box, b)
35 if err != nil {
36 return err
37 }
38 if report.Enrollment != enrollment || report.InstanceID != b.Cloud.InstanceID || report.BootID == "" || report.GeneratedAt.IsZero() || s.Now().Sub(report.GeneratedAt) > 15*time.Minute || report.GeneratedAt.After(s.Now().Add(5*time.Second)) {
39 return ErrResolve
40 }
41 floor, err := hostfloor.Read(ctx, s.Directory.Store, s.Directory.PublicKey, s.Directory.Bucket)
42 if err != nil {
43 return err
44 }
45 evidence := hostactors.ExactEvidence{PublicKey: s.Directory.PublicKey, Store: s.Directory.Store, Boxes: s.Directory.PrivateBoxReport}
46 if err = hostactors.VerifyReport(ctx, evidence, s.Directory.PublicKey, floor, report, enrollment, b.Cloud.InstanceID, s.Now()); err != nil {
47 return err
48 }
49 if !report.HostClaimEnabled {
50 return ErrResolve
51 }
52 key, err := hostquarantine.RetirementKey(box, enrollment, revision)
53 if err != nil {
54 return err
55 }
56 raw, _, err := s.Directory.Store.Get(ctx, key)
57 if err != nil {
58 return err
59 }
60 if report.RetirementReceiptSHA256 != hostfloor.Digest(raw) {
61 return ErrResolve
62 }
63 var receipt hostquarantine.RetirementCompletion
64 if json.Unmarshal(raw, &receipt) != nil || receipt.Validate() != nil || !receipt.Complete || receipt.Box != box || receipt.Enrollment != enrollment || receipt.InstanceID != b.Cloud.InstanceID || receipt.QuarantineRevision != revision || receipt.BootID != report.BootID || receipt.CompletedAt.After(report.GeneratedAt) {
65 return ErrResolve
66 }
67 losses := make(map[string][]hostclaim.LossRecord)
68 for _, id := range lossIDs {
69 loss, err := hostclaim.ReadLossRecord(ctx, s.Directory.Store, s.Directory.PublicKey, id)
70 if err != nil {
71 return err
72 }
73 if (loss.Record.State != "committed" && loss.Record.State != "conflicted") || loss.Record.Box != box || loss.Record.Enrollment != enrollment || loss.Record.QuarantineRevision > revision {
74 return ErrResolve
75 }
76 proven := false
77 for _, entry := range receipt.Entries {
78 if entry.Host == loss.Record.Host && entry.Incarnation == loss.Record.HolderIncarnation && entry.Stamp == loss.Record.HolderStamp {
79 proven = true
80 }
81 }
82 if !proven && (loss.Record.State == "conflicted" || len(loss.Record.ConfirmedConsequences.DischargeLocal) == 0) {
83 return ErrResolve
84 }
85 losses[loss.Record.Host] = append(losses[loss.Record.Host], loss.Record)
86 }
87 // Store.List is a complete listing in both real implementations. Its S3
88 // implementation follows every continuation token; any error refuses.
89 keys, err := s.Directory.Store.List(ctx, "control/hosts/")
90 if err != nil {
91 return err
92 }
93 var claims []hostclaim.Observed
94 for _, key := range keys {
95 host, ok := claimHost(key)
96 if !ok {
97 continue
98 }
99 observed, err := hostclaim.Read(ctx, s.Directory.Store, host)
100 if err != nil {
101 return err
102 }
103 if observed.Absent {
104 continue
105 }
106 c := observed.Claim
107 for _, o := range c.Obligations {
108 if o.Kind == "park-local" && o.Box == box {
109 return ErrResolve
110 }
111 }
112 if slices.Contains(c.Lost, box) {
113 covered := false
114 for _, loss := range losses[host] {
115 if loss.State == "committed" && loss.QuarantineRevision == c.LostRevisions[box] && slices.Contains(c.LossRefs, loss.ID) {
116 covered = true
117 break
118 }
119 }
120 if !covered {
121 return ErrResolve
122 }
123 claims = append(claims, observed)
124 }
125 }
126 for _, observed := range claims {
127 if _, err = hostclaim.Update(ctx, s.Directory.Store, observed, hostclaim.Readmit(box, observed.Claim.LostRevisions[box])); err != nil {
128 return err
129 }
130 }
131 // CAS against the original roster prevents a concurrent declaration,
132 // reservation or membership change being overwritten by this readmission.
133 next := hostactors.Clone(before.Roster)
134 b.State = "active"
135 next.Boxes[box] = b
136 _, err = s.Directory.Replace(ctx, s.Signer, before, next)
137 return err
138}
139
140func claimHost(key string) (string, bool) {
141 s, ok := strings.CutPrefix(key, "control/hosts/")
142 if !ok || strings.Contains(s, "/") {
143 return "", false
144 }
145 host, ok := strings.CutSuffix(s, ".json")
146 return host, ok && host != ""
147}
148