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 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "os"
10 "strings"
11 "time"
12
13 "github.com/carlosframework/platform/internal/controlname"
14 "github.com/carlosframework/platform/internal/hostclaim"
15 "github.com/carlosframework/platform/internal/provision"
16 "github.com/carlosframework/platform/internal/release"
17)
18
19var (
20 ErrPending = errors.New("hostops: pending")
21 ErrContract = errors.New("hostops: required contract unavailable")
22 ErrIdentity = errors.New("hostops: identity mismatch")
23)
24
25type Record struct {
26 VerifiedOwnership bool // Set only after current DNS/platform proof; never decoded from JSON.
27 Holder hostclaim.NewHolder
28 Body []byte
29 ETag string
30}
31
32func decode(body []byte, out any) error {
33 d := json.NewDecoder(bytes.NewReader(body))
34 if err := d.Decode(out); err != nil {
35 return err
36 }
37 if _, err := d.Token(); err != io.EOF {
38 return ErrIdentity
39 }
40 return nil
41}
42
43func ReadRecord(ctx context.Context, s release.Store, key string) (Record, error) {
44 parts := strings.Split(key, "/")
45 if len(parts) != 5 || parts[0] != "apps" || !controlname.ValidAccount(parts[1]) || !controlname.ValidApp(parts[2]) || !strings.HasSuffix(parts[4], ".json") {
46 return Record{}, ErrIdentity
47 }
48 body, etag, err := s.Get(ctx, key)
49 if err != nil {
50 return Record{}, err
51 }
52 if etag == "" {
53 return Record{}, ErrIdentity
54 }
55 var binding struct {
56 Acquisition string `json:"acq"`
57 }
58 if err := decode(body, &binding); err != nil {
59 return Record{}, err
60 }
61 n := hostclaim.NewHolder{Acquisition: binding.Acquisition, Key: key, Account: parts[1], App: parts[2]}
62 switch parts[3] {
63 case "instances":
64 var r provision.Record
65 if err := decode(body, &r); err != nil {
66 return Record{}, err
67 }
68 if r.Account != n.Account || r.App != n.App || key != provision.RecordKey(r.Account, r.App, r.Host) || !controlname.ValidHostShape(r.Host) {
69 return Record{}, ErrIdentity
70 }
71 // DecodeRecord is also the ordinary instance reader's shape validator.
72 if _, err := provision.DecodeRecord(body); err != nil {
73 return Record{}, err
74 }
75 n.Family, n.Host, n.RecordRegion, n.CreatedAt = "instance", r.Host, r.Region, r.CreatedAt
76 case "sidecars":
77 var r provision.SidecarRecord
78 if err := decode(body, &r); err != nil {
79 return Record{}, err
80 }
81 if r.Account != n.Account || r.App != n.App || !controlname.ValidLabel(r.Name) || key != provision.SidecarRecordKey(r.Account, r.App, r.Name) || r.Host != provision.SidecarHost(r.Name, r.App, r.Account) {
82 return Record{}, ErrIdentity
83 }
84 n.Family, n.Host, n.Name, n.RecordRegion, n.CreatedAt = "sidecar", r.Host, r.Name, r.Region, r.CreatedAt
85 default:
86 return Record{}, ErrIdentity
87 }
88 return Record{Holder: n, Body: body, ETag: etag}, nil
89}
90
91func (r Record) Incarnation() (string, error) {
92 n := r.Holder
93 return hostclaim.Incarnation(n.Account, n.App, n.Host, n.CreatedAt)
94}
95
96func matches(r Record, h *hostclaim.Holder) bool {
97 if h == nil {
98 return false
99 }
100 n := r.Holder
101 inc, err := r.Incarnation()
102 return err == nil && n.Acquisition == h.Acquisition && n.Key == h.Key && n.Family == h.Family && n.Account == h.Account && n.App == h.App && n.Name == h.Name && n.CreatedAt.Equal(h.CreatedAt) && inc == h.Incarnation
103}
104
105// The caller reads the record first. This function performs the later claim read.
106func RemoveOrphan(ctx context.Context, s release.Store, r Record) (bool, error) {
107 if err := CheckHostQuarantine(ctx, s, r.Holder.Host); err != nil {
108 return false, err
109 }
110 inc, err := r.Incarnation()
111 if err != nil {
112 return false, err
113 }
114 obs, err := hostclaim.Read(ctx, s, r.Holder.Host)
115 if err != nil {
116 return false, err
117 }
118 if !hostclaim.IsOrphan(inc, obs) {
119 return false, nil
120 }
121 if err := release.DeleteIfMatch(ctx, s, r.Holder.Key, r.ETag); err != nil {
122 return false, err
123 }
124 return true, nil
125}
126
127func Records(ctx context.Context, s release.Store, acct, app string) ([]Record, error) {
128 if !controlname.ValidAccount(acct) || !controlname.ValidApp(app) {
129 return nil, ErrIdentity
130 }
131 var out []Record
132 for _, prefix := range []string{provision.RecordsPrefix(acct, app), provision.SidecarsPrefix(acct, app)} {
133 keys, err := s.List(ctx, prefix)
134 if err != nil {
135 return nil, err
136 }
137 for _, key := range keys {
138 tail, ok := strings.CutPrefix(key, prefix)
139 if !ok {
140 return nil, ErrIdentity
141 }
142 if strings.Contains(tail, "/") || !strings.HasSuffix(tail, ".json") {
143 continue
144 }
145 r, err := ReadRecord(ctx, s, key)
146 if errors.Is(err, os.ErrNotExist) {
147 continue
148 }
149 if err != nil {
150 return nil, err
151 }
152 out = append(out, r)
153 }
154 }
155 return out, nil
156}
157
158// PrepareInstance accepts only an unstamped internal request, never a caller time.
159func PrepareInstance(r provision.Record, now time.Time, region string) (Record, error) {
160 if !r.CreatedAt.IsZero() || now.IsZero() || region == "" || len(r.Maintenance) != 0 {
161 return Record{}, ErrIdentity
162 }
163 r.CreatedAt = now.UTC()
164 body, err := json.Marshal(r)
165 if err != nil {
166 return Record{}, err
167 }
168 n := hostclaim.NewHolder{Acquisition: r.Acquisition, Family: "instance", Key: provision.RecordKey(r.Account, r.App, r.Host), Account: r.Account, App: r.App, Host: r.Host, RecordRegion: r.Region, Region: region, CreatedAt: r.CreatedAt}
169 return Record{Holder: n, Body: body}, nil
170}
171
172func PrepareSidecar(r provision.SidecarRecord, now time.Time, region string) (Record, error) {
173 if !r.CreatedAt.IsZero() || now.IsZero() || region == "" {
174 return Record{}, ErrIdentity
175 }
176 r.CreatedAt = now.UTC()
177 body, err := json.Marshal(r)
178 if err != nil {
179 return Record{}, err
180 }
181 n := hostclaim.NewHolder{Acquisition: r.Acquisition, Family: "sidecar", Key: provision.SidecarRecordKey(r.Account, r.App, r.Name), Account: r.Account, App: r.App, Name: r.Name, Host: r.Host, RecordRegion: r.Region, Region: region, CreatedAt: r.CreatedAt}
182 return Record{Holder: n, Body: body}, nil
183}
184