carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "encoding/json"
5 "errors"
6 "os"
7 "slices"
8 "strings"
9
10 "github.com/carlosframework/platform/internal/rootstate"
11)
12
13const RootHistoryStatusDir = "/var/lib/carlos-release/history-status"
14
15type HistoryHostStatus struct {
16 RetirementDigests map[string]string `json:"retirement_digests,omitempty"`
17 HistoryReconstructionHost
18 Programs historyPrograms `json:"programs"`
19 RefusalStatus historyRefusals `json:"refusal_status"`
20 Recoveries []HistoryRecovery `json:"recoveries,omitempty"`
21 RecoveryDigests map[string]string `json:"recovery_digests,omitempty"`
22 RetiredRecoveries []string `json:"retired_recoveries,omitempty"`
23 Attempt *HistoryAttempt `json:"attempt,omitempty"`
24}
25
26type HistoryRecoverySources struct {
27 Known bool `json:"known"`
28 Unresolved []HistoryRecovery `json:"unresolved,omitempty"`
29}
30
31type HistoryMigrationStatus struct {
32 HistoryReconstructionStatus
33 Schema int `json:"schema"`
34 RecoverySources *HistoryRecoverySources `json:"recovery_sources,omitempty"`
35 Account string `json:"account"`
36 App string `json:"app"`
37 AppRevision uint64 `json:"app_revision"`
38 AppComplete bool `json:"app_complete"`
39 BakeRevision uint64 `json:"bake_revision"`
40 BakeComplete uint64 `json:"bake_complete"`
41 Hosts []HistoryHostStatus `json:"hosts"`
42 Clock FirstSeen `json:"clock"`
43 PendingDigest string `json:"pending_digest,omitempty"`
44 PendingGrant *HistoryReconstruction `json:"pending_grant,omitempty"`
45}
46
47func HistoryStatusKey(acct, app string) string { return historyAppKey(acct, app) + ".status" }
48
49func (h *RootHistory) MigrationStatus(b HistoryBinding, resolve func(HistoryBinding) (string, error)) (HistoryMigrationStatus, error) {
50 var out HistoryMigrationStatus
51 if !b.valid() || resolve == nil {
52 return out, os.ErrPermission
53 }
54 status, err := h.ReconstructionStatus()
55 if err != nil {
56 return out, err
57 }
58 out = HistoryMigrationStatus{HistoryReconstructionStatus: status, Schema: 1, Account: b.Account, App: b.App}
59 state, err := h.reconstructionState()
60 if err != nil {
61 return out, err
62 }
63 hosts := []string{b.Host}
64 if state.Pending != nil {
65 raw, err := json.Marshal(state.Pending.Grant)
66 if err != nil || reconstructionDigest(raw) != state.Pending.Digest {
67 return out, os.ErrPermission
68 }
69 if state.Pending.Grant.Account == b.Account && state.Pending.Grant.App == b.App {
70 out.PendingDigest = state.Pending.Digest
71 grant := state.Pending.Grant
72 out.PendingGrant = &grant
73 for _, host := range grant.Hosts {
74 hosts = append(hosts, host.Host)
75 }
76 }
77 }
78 read := func(key string, v any, valid func() bool) error {
79 raw, err := h.Store.Read(key)
80 if errors.Is(err, os.ErrNotExist) {
81 return nil
82 }
83 if err != nil {
84 return err
85 }
86 if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, v) != nil || !valid() {
87 return os.ErrPermission
88 }
89 return h.Store.Write(key, raw)
90 }
91 var app historyApp
92 if err := read(historyAppKey(b.Account, b.App), &app, func() bool { return validHistoryApp(app) && app.Account == b.Account && app.App == b.App }); err != nil {
93 return out, err
94 }
95 out.AppRevision, out.AppComplete = app.Revision, app.Complete
96 hosts = append(hosts, app.Hosts...)
97 var bake rootBakeRecord
98 if err := read(rootBakeKey, &bake, func() bool { return validRootBake(bake) }); err != nil {
99 return out, err
100 }
101 out.BakeRevision, out.BakeComplete = bake.Revision, bake.Complete[prodKey(b.Account, b.App)]
102 out.Clock = FirstSeen{Production: map[string]*prodMemory{}, Holds: map[string]*holdMemory{}}
103 if p := bake.Clock.Production[prodKey(b.Account, b.App)]; p != nil {
104 out.Clock.Production[prodKey(b.Account, b.App)] = p
105 }
106 for key, hold := range bake.Clock.Holds {
107 if strings.HasPrefix(key, holdKeyPrefix(b.Account, b.App)) {
108 out.Clock.Holds[key] = hold
109 }
110 }
111 names, err := h.Store.Names()
112 if err != nil {
113 return out, err
114 }
115 for _, name := range names {
116 if name == rootBakeKey || !strings.HasSuffix(name, ".json") {
117 continue
118 }
119 var record historyRecord
120 if err := read(name, &record, func() bool { return validHistory(record, record.Binding) && name == historyKey(record.Binding) }); err != nil {
121 return out, err
122 }
123 if record.Binding.Account == b.Account && record.Binding.App == b.App {
124 hosts = append(hosts, record.Binding.Host)
125 }
126 }
127 outcomes, err := h.recoveryOutcomes()
128 if err != nil {
129 return out, err
130 }
131 for _, r := range outcomes {
132 if r.Binding.Account == b.Account && r.Binding.App == b.App {
133 hosts = append(hosts, r.Binding.Host)
134 }
135 }
136 slices.Sort(hosts)
137 hosts = slices.Compact(hosts)
138 if len(hosts) > historyLimit {
139 return out, os.ErrPermission
140 }
141 for _, host := range hosts {
142 binding := HistoryBinding{Host: host, Account: b.Account, App: b.App}
143 source, err := resolve(binding)
144 if err != nil {
145 return out, err
146 }
147 if source == "" {
148 return out, os.ErrPermission
149 }
150 record, err := h.readRecord(binding)
151 if err != nil && !errors.Is(err, os.ErrNotExist) {
152 return out, err
153 }
154 var recoveries []HistoryRecovery
155 digests := map[string]string{}
156 var retired []string
157 retirementDigests := map[string]string{}
158 for _, r := range outcomes {
159 if r.Binding == binding {
160 recoveries = append(recoveries, r)
161 digests[r.Attempt.ID] = historyRecoveryDigest(r)
162 if state.Pending == nil {
163 digest, err := h.recoveryRetirementDigest(r, outcomes)
164 if err != nil {
165 return out, err
166 }
167 if digest != "" {
168 retirementRaw, err := h.Store.Read(historyRetirementKey(r))
169 var grant HistoryRecoveryRetirement
170 if err != nil {
171 return out, err
172 }
173 if reconstructionDigest(retirementRaw) != digest || strictCanonicalJSON(retirementRaw, &grant) != nil {
174 return out, os.ErrPermission
175 }
176 if grant.Source != source {
177 continue
178 }
179 retirementDigests[r.Attempt.ID] = digest
180 retired = append(retired, r.Attempt.ID)
181 }
182 }
183 }
184 }
185 out.Hosts = append(out.Hosts, HistoryHostStatus{RetirementDigests: retirementDigests, Recoveries: recoveries, RecoveryDigests: digests, RetiredRecoveries: retired, HistoryReconstructionHost: HistoryReconstructionHost{Host: host, Source: source, Revision: record.Revision, Refusals: record.Refusals.Values}, Programs: record.Programs, RefusalStatus: record.Refusals, Attempt: record.Attempt})
186 }
187 for _, host := range out.Hosts {
188 source, err := resolve(HistoryBinding{Host: host.Host, Account: b.Account, App: b.App})
189 if err != nil {
190 return out, err
191 }
192 if source != host.Source {
193 return out, os.ErrPermission
194 }
195 }
196 return out, nil
197}
198
199func (h *RootHistory) MigrationStatusWithSources(b HistoryBinding, resolve func(HistoryBinding) (string, error), sources ...HistoryRecoverySources) (HistoryMigrationStatus, error) {
200 if len(sources) > 1 {
201 return HistoryMigrationStatus{}, os.ErrPermission
202 }
203 view, err := h.MigrationStatus(b, resolve)
204 if err != nil {
205 return HistoryMigrationStatus{}, err
206 }
207 if len(sources) == 1 {
208 effective := HistoryRecoverySources{Known: sources[0].Known}
209 if len(sources[0].Unresolved) > historyLimit {
210 return HistoryMigrationStatus{}, os.ErrPermission
211 }
212 for _, r := range sources[0].Unresolved {
213 if !validHistoryRecovery(r) || r.Binding.Account != b.Account || r.Binding.App != b.App {
214 return HistoryMigrationStatus{}, os.ErrPermission
215 }
216 if r.Settled {
217 retired, err := h.RecoveryRetired(r)
218 if err != nil {
219 effective.Known = false
220 }
221 if err == nil && retired {
222 continue
223 }
224 }
225 effective.Unresolved = append(effective.Unresolved, r)
226 for i := range view.Hosts {
227 if view.Hosts[i].Host == r.Binding.Host {
228 delete(view.Hosts[i].RetirementDigests, r.Attempt.ID)
229 view.Hosts[i].RetiredRecoveries = slices.DeleteFunc(view.Hosts[i].RetiredRecoveries, func(id string) bool { return id == r.Attempt.ID })
230 }
231 }
232 }
233 view.RecoverySources = &effective
234 }
235 return view, nil
236}
237
238func (h *RootHistory) ProjectMigrationStatus(dst rootstate.Store, b HistoryBinding, resolve func(HistoryBinding) (string, error), sources ...HistoryRecoverySources) error {
239 if dst == nil {
240 return os.ErrPermission
241 }
242 view, err := h.MigrationStatusWithSources(b, resolve, sources...)
243 if err != nil {
244 return err
245 }
246 raw, err := json.Marshal(view)
247 if err != nil {
248 return err
249 }
250 if len(raw) > rootstate.MaxRecord {
251 return os.ErrPermission
252 }
253 return dst.Write(HistoryStatusKey(b.Account, b.App), raw)
254}
255