package rollout import ( "crypto/ed25519" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "os" "time" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) const RootFailuresDir = "/var/lib/carlos-release/private/swap-failures" const rootFailuresName = "book.json" type rootFailureRecord struct { Schema int `json:"schema"` Migration string `json:"migration"` Book failureFile `json:"book"` Seen map[string]bool `json:"seen"` } type RootFailures struct { *FailureBook seen map[string]bool } func rootFailureWriter(s rootstate.Store, migration string, seen map[string]bool) func([]byte) error { return func(raw []byte) error { var f failureFile if json.Unmarshal(raw, &f) != nil { return os.ErrPermission } body, err := json.Marshal(rootFailureRecord{1, migration, f, seen}) if err != nil { return err } if len(body) > rootstate.MaxRecord { return os.ErrPermission } return s.Write(rootFailuresName, body) } } func OpenRootFailures(s rootstate.Store, migration string) (*RootFailures, error) { if s == nil || migration == "" { return nil, os.ErrPermission } raw, err := s.Read(rootFailuresName) if err != nil { return nil, err } var r rootFailureRecord 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 == "" { return nil, os.ErrPermission } f := r.Book if r.Seen == nil { r.Seen = map[string]bool{} } 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()} b.epochOlderLocked() b.reconcileLocked() // Re-persist even an unchanged visible record before granting anything. // The prior writer may have died after rename and before directory fsync. if err := b.Flush(); err != nil { return nil, err } return &RootFailures{b, r.Seen}, nil } func InitializeRootFailures(s rootstate.Store, migration string, legacy []byte) (*RootFailures, error) { if b, err := OpenRootFailures(s, migration); err == nil { return b, nil } else if !errors.Is(err, os.ErrNotExist) { return nil, err } if migration == "" || len(legacy) > rootstate.MaxRecord { return nil, os.ErrPermission } id, err := newBookID() if err != nil { return nil, err } seen := map[string]bool{} b := &FailureBook{write: rootFailureWriter(s, migration, seen), id: id} root := &RootFailures{b, seen} if err := root.ImportLegacy(legacy); err != nil { return nil, err } if err := b.Flush(); err != nil { return nil, err } return root, nil } func (b *RootFailures) ImportLegacy(raw []byte) error { if len(raw) > rootstate.MaxRecord { return os.ErrPermission } var f failureFile if len(raw) == 0 || json.Unmarshal(raw, &f) != nil || (f.Schema != 1 && f.Schema != 2 && f.Schema != failureSchema) { return b.Flush() } b.mu.Lock() defer b.mu.Unlock() add := func(value any, effect func()) { data, _ := json.Marshal(value) sum := sha256.Sum256(data) key := hex.EncodeToString(sum[:]) if !b.seen[key] { b.seen[key] = true effect() } } for _, r := range f.Records { if !release.ValidAccount(r.Account) || !release.ValidApp(r.App) || !release.ValidVersion(r.Build) { continue } r.Retried = nil r.Epochs = nil r.Seq = 0 add(r, func() { b.recordLocked(r) }) } for host, h := range f.HoldRan { add(struct { Host string Mark HoldRan }{host, h}, func() { b.markLocked(host, h) }) } for host, i := range f.Intents { add(struct { Host string Intent Intent }{host, i}, func() { r := FailureRecord{Account: i.Account, App: i.App, Build: i.Build, Host: host, At: i.Start, Mode: i.Mode} if i.Mode == "hold" { r.HeldDown = []string{host} } b.recordLocked(r) }) } return b.saveLocked() } func (b *RootFailures) VerifiedRetry(pub ed25519.PublicKey, wire, acct, app, host string, now time.Time) error { g, err := release.VerifyHostRetry(pub, wire, acct, app, host, now) if err != nil { return err } _, _, _, err = b.RetryHostAt(acct, app, g.Build, host, g.Book, g.Epoch) return err } // LegacyRetryIdentity identifies only the agent mirror root has already read. // It cannot clear a root restriction or name a later agent failure. func LegacyRetryIdentity(raw []byte, acct, app string) (string, uint64) { var f failureFile if len(raw) > rootstate.MaxRecord || json.Unmarshal(raw, &f) != nil || f.Schema != failureSchema || !f.Durable || f.Book == "" { return "", 0 } return f.Book, f.Gens[genKey(acct, app)] }