carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package rollout
2
3import (
4 "crypto/ed25519"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "os"
10 "time"
11
12 "github.com/carlosframework/platform/internal/release"
13 "github.com/carlosframework/platform/internal/rootstate"
14)
15
16const RootFailuresDir = "/var/lib/carlos-release/private/swap-failures"
17const rootFailuresName = "book.json"
18
19type rootFailureRecord struct {
20 Schema int `json:"schema"`
21 Migration string `json:"migration"`
22 Book failureFile `json:"book"`
23 Seen map[string]bool `json:"seen"`
24}
25
26type RootFailures struct {
27 *FailureBook
28 seen map[string]bool
29}
30
31func rootFailureWriter(s rootstate.Store, migration string, seen map[string]bool) func([]byte) error {
32 return func(raw []byte) error {
33 var f failureFile
34 if json.Unmarshal(raw, &f) != nil {
35 return os.ErrPermission
36 }
37 body, err := json.Marshal(rootFailureRecord{1, migration, f, seen})
38 if err != nil {
39 return err
40 }
41 if len(body) > rootstate.MaxRecord {
42 return os.ErrPermission
43 }
44 return s.Write(rootFailuresName, body)
45 }
46}
47
48func OpenRootFailures(s rootstate.Store, migration string) (*RootFailures, error) {
49 if s == nil || migration == "" {
50 return nil, os.ErrPermission
51 }
52 raw, err := s.Read(rootFailuresName)
53 if err != nil {
54 return nil, err
55 }
56 var r rootFailureRecord
57 if len(raw) > rootstate.MaxRecord || json.Unmarshal(raw, &r) != nil || r.Schema != 1 || r.Migration != migration || r.Book.Schema != failureSchema || !r.Book.Durable || r.Book.Book == "" {
58 return nil, os.ErrPermission
59 }
60 f := r.Book
61 if r.Seen == nil {
62 r.Seen = map[string]bool{}
63 }
64 b := &FailureBook{write: rootFailureWriter(s, migration, r.Seen), recs: f.Records, held: f.HoldRan, intents: f.Intents, gens: f.Gens, id: f.Book, seq: f.highestSaved()}
65 b.epochOlderLocked()
66 b.reconcileLocked()
67 // Re-persist even an unchanged visible record before granting anything.
68 // The prior writer may have died after rename and before directory fsync.
69 if err := b.Flush(); err != nil {
70 return nil, err
71 }
72 return &RootFailures{b, r.Seen}, nil
73}
74
75func InitializeRootFailures(s rootstate.Store, migration string, legacy []byte) (*RootFailures, error) {
76 if b, err := OpenRootFailures(s, migration); err == nil {
77 return b, nil
78 } else if !errors.Is(err, os.ErrNotExist) {
79 return nil, err
80 }
81 if migration == "" || len(legacy) > rootstate.MaxRecord {
82 return nil, os.ErrPermission
83 }
84 id, err := newBookID()
85 if err != nil {
86 return nil, err
87 }
88 seen := map[string]bool{}
89 b := &FailureBook{write: rootFailureWriter(s, migration, seen), id: id}
90 root := &RootFailures{b, seen}
91 if err := root.ImportLegacy(legacy); err != nil {
92 return nil, err
93 }
94 if err := b.Flush(); err != nil {
95 return nil, err
96 }
97 return root, nil
98}
99
100func (b *RootFailures) ImportLegacy(raw []byte) error {
101 if len(raw) > rootstate.MaxRecord {
102 return os.ErrPermission
103 }
104 var f failureFile
105 if len(raw) == 0 || json.Unmarshal(raw, &f) != nil || (f.Schema != 1 && f.Schema != 2 && f.Schema != failureSchema) {
106 return b.Flush()
107 }
108 b.mu.Lock()
109 defer b.mu.Unlock()
110 add := func(value any, effect func()) {
111 data, _ := json.Marshal(value)
112 sum := sha256.Sum256(data)
113 key := hex.EncodeToString(sum[:])
114 if !b.seen[key] {
115 b.seen[key] = true
116 effect()
117 }
118 }
119 for _, r := range f.Records {
120 if !release.ValidAccount(r.Account) || !release.ValidApp(r.App) || !release.ValidVersion(r.Build) {
121 continue
122 }
123 r.Retried = nil
124 r.Epochs = nil
125 r.Seq = 0
126 add(r, func() { b.recordLocked(r) })
127 }
128 for host, h := range f.HoldRan {
129 add(struct {
130 Host string
131 Mark HoldRan
132 }{host, h}, func() { b.markLocked(host, h) })
133 }
134 for host, i := range f.Intents {
135 add(struct {
136 Host string
137 Intent Intent
138 }{host, i}, func() {
139 r := FailureRecord{Account: i.Account, App: i.App, Build: i.Build, Host: host, At: i.Start, Mode: i.Mode}
140 if i.Mode == "hold" {
141 r.HeldDown = []string{host}
142 }
143 b.recordLocked(r)
144 })
145 }
146 return b.saveLocked()
147}
148
149func (b *RootFailures) VerifiedRetry(pub ed25519.PublicKey, wire, acct, app, host string, now time.Time) error {
150 g, err := release.VerifyHostRetry(pub, wire, acct, app, host, now)
151 if err != nil {
152 return err
153 }
154 _, _, _, err = b.RetryHostAt(acct, app, g.Build, host, g.Book, g.Epoch)
155 return err
156}
157
158// LegacyRetryIdentity identifies only the agent mirror root has already read.
159// It cannot clear a root restriction or name a later agent failure.
160func LegacyRetryIdentity(raw []byte, acct, app string) (string, uint64) {
161 var f failureFile
162 if len(raw) > rootstate.MaxRecord || json.Unmarshal(raw, &f) != nil || f.Schema != failureSchema || !f.Durable || f.Book == "" {
163 return "", 0
164 }
165 return f.Book, f.Gens[genKey(acct, app)]
166}
167