package host import ( "context" "encoding/json" "reflect" "slices" "strings" lss3 "github.com/benbjohnson/litestream/s3" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostfiles" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/instancekey" ) type claimEmptyRecovery func(context.Context, hostclaim.Observed, []hostclaim.AdmissionRecord) (bool, error) func rootEmptyRecovery(g hostguard.Guard) claimEmptyRecovery { return func(ctx context.Context, o hostclaim.Observed, admissions []hostclaim.AdmissionRecord) (bool, error) { if o.Claim == nil || o.Claim.Holder == nil || len(admissions) == 0 { return false, nil } h := o.Claim.Holder if len(admissions) > len(h.Admissions) || len(admissions) < len(h.Admissions)-1 || !reflect.DeepEqual(admissions, h.Admissions[:len(admissions)]) { return false, hostbox.ErrChanged } captured := hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp} var view hostguard.FenceView var err error if g.Root != nil { view, err = g.Root.View(ctx, o.Host, captured) } else if g.FenceView != nil { view, err = g.FenceView(ctx, o.Host, captured) } else { return false, nil } if err != nil { return false, err } if view.Installation.Host != o.Host || view.Installation.Incarnation != h.Incarnation || view.Installation.Stamp != h.Stamp { return false, hostbox.ErrChanged } predecessor := "" for _, a := range admissions { if a.Mode != "cold" || a.Covered || a.Box != view.Installation.Box || a.Predecessor != predecessor || a.Generation == "" { return false, nil } found := false for _, r := range view.Receipts { if r.Target.Host != o.Host || r.Target.Incarnation != h.Incarnation || r.Target.Session != a.Session || r.Target.Box != a.Box || r.Target.Enrollment != view.Installation.Enrollment || r.Target.Op != "" { continue } capturedReceipt := hostguard.Captured{Incarnation: r.Target.Incarnation, Stamp: r.Target.Stamp} if r.Target.Stamp != h.Stamp && !slices.Contains(view.Retired, capturedReceipt) { continue } var receipt StopReceipt if json.Unmarshal(r.Bytes, &receipt) != nil { return false, hostbox.ErrChanged } if receipt.Kind == StopReceiptStopped && receipt.Host == r.Target.Host && receipt.Incarnation == r.Target.Incarnation && receipt.InstallationStamp == r.Target.Stamp && receipt.WriterSession == r.Target.Session && receipt.FenceID == r.Target.FenceID && slices.Contains(receipt.CgroupIdentities, "unlaunched:"+digestStop(r.Target)) { found = true break } } if !found { return false, nil } predecessor = a.Generation } return true, nil } } func (s *claimServingOwners) emptyBootstrap(ctx context.Context, o hostclaim.Observed) (bool, error) { if claimEmptyBootstrap(o.Claim.Holder) { return true, nil } if s.emptyRecovery == nil { return false, nil } yes, err := s.emptyRecovery(ctx, o, o.Claim.Holder.Admissions) if err != nil || !yes { return false, err } return true, s.emptyLocation(ctx, o.Claim.Holder.Location, o.Claim.Holder.Admissions...) } func (s *claimServingOwners) emptyLocation(ctx context.Context, location string, admissions ...hostclaim.AdmissionRecord) error { versions, err := s.Data.ListVersions(ctx, s.Bucket, strings.TrimSuffix(location, "/")+"/") if err != nil { return err } leaseKey := strings.TrimSuffix(location, "/") + "/" + lss3.DefaultLeasePath allowed := map[string]bool{leaseKey: true} for _, a := range admissions { prefix := movesFilesPrefix(location, a.Generation) key := hostfiles.ManifestKey(prefix) for _, v := range versions { if v.Key != key { continue } if v.DeleteMarker || v.Size > 1<<20 { return hostbox.ErrChanged } raw, err := s.Data.GetVersion(ctx, s.Bucket, key, v.VersionID) if err != nil { return err } var m hostfiles.Manifest if json.Unmarshal(raw, &m) != nil || m.V != hostfiles.ManifestVersion || len(m.Entries) != 0 { return hostbox.ErrChanged } for name, e := range m.Platform { if name == instancekey.AdoptionItem && e.Deleted { continue } if name != hostfiles.EnvItem || e.Deleted || e.Torn || e.Skipped != "" || !strings.HasPrefix(e.Object, prefix+"objects/") || !claimHexDigest(e.SHA256) { return hostbox.ErrChanged } allowed[e.Object] = true } allowed[key] = true } } for _, v := range versions { if !allowed[v.Key] || v.Key != leaseKey && v.DeleteMarker { return hostbox.ErrChanged } } return nil }