| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "reflect" |
| 7 | "slices" |
| 8 | "strings" |
| 9 | |
| 10 | lss3 "github.com/benbjohnson/litestream/s3" |
| 11 | "github.com/carlosframework/platform/internal/hostbox" |
| 12 | "github.com/carlosframework/platform/internal/hostclaim" |
| 13 | "github.com/carlosframework/platform/internal/hostfiles" |
| 14 | "github.com/carlosframework/platform/internal/hostguard" |
| 15 | "github.com/carlosframework/platform/internal/instancekey" |
| 16 | ) |
| 17 | |
| 18 | type claimEmptyRecovery func(context.Context, hostclaim.Observed, []hostclaim.AdmissionRecord) (bool, error) |
| 19 | |
| 20 | func rootEmptyRecovery(g hostguard.Guard) claimEmptyRecovery { |
| 21 | return func(ctx context.Context, o hostclaim.Observed, admissions []hostclaim.AdmissionRecord) (bool, error) { |
| 22 | if o.Claim == nil || o.Claim.Holder == nil || len(admissions) == 0 { |
| 23 | return false, nil |
| 24 | } |
| 25 | h := o.Claim.Holder |
| 26 | if len(admissions) > len(h.Admissions) || len(admissions) < len(h.Admissions)-1 || !reflect.DeepEqual(admissions, h.Admissions[:len(admissions)]) { |
| 27 | return false, hostbox.ErrChanged |
| 28 | } |
| 29 | captured := hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp} |
| 30 | var view hostguard.FenceView |
| 31 | var err error |
| 32 | if g.Root != nil { |
| 33 | view, err = g.Root.View(ctx, o.Host, captured) |
| 34 | } else if g.FenceView != nil { |
| 35 | view, err = g.FenceView(ctx, o.Host, captured) |
| 36 | } else { |
| 37 | return false, nil |
| 38 | } |
| 39 | if err != nil { |
| 40 | return false, err |
| 41 | } |
| 42 | if view.Installation.Host != o.Host || view.Installation.Incarnation != h.Incarnation || view.Installation.Stamp != h.Stamp { |
| 43 | return false, hostbox.ErrChanged |
| 44 | } |
| 45 | predecessor := "" |
| 46 | for _, a := range admissions { |
| 47 | if a.Mode != "cold" || a.Covered || a.Box != view.Installation.Box || a.Predecessor != predecessor || a.Generation == "" { |
| 48 | return false, nil |
| 49 | } |
| 50 | found := false |
| 51 | for _, r := range view.Receipts { |
| 52 | 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 != "" { |
| 53 | continue |
| 54 | } |
| 55 | capturedReceipt := hostguard.Captured{Incarnation: r.Target.Incarnation, Stamp: r.Target.Stamp} |
| 56 | if r.Target.Stamp != h.Stamp && !slices.Contains(view.Retired, capturedReceipt) { |
| 57 | continue |
| 58 | } |
| 59 | var receipt StopReceipt |
| 60 | if json.Unmarshal(r.Bytes, &receipt) != nil { |
| 61 | return false, hostbox.ErrChanged |
| 62 | } |
| 63 | 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)) { |
| 64 | found = true |
| 65 | break |
| 66 | } |
| 67 | } |
| 68 | if !found { |
| 69 | return false, nil |
| 70 | } |
| 71 | predecessor = a.Generation |
| 72 | } |
| 73 | return true, nil |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | func (s *claimServingOwners) emptyBootstrap(ctx context.Context, o hostclaim.Observed) (bool, error) { |
| 78 | if claimEmptyBootstrap(o.Claim.Holder) { |
| 79 | return true, nil |
| 80 | } |
| 81 | if s.emptyRecovery == nil { |
| 82 | return false, nil |
| 83 | } |
| 84 | yes, err := s.emptyRecovery(ctx, o, o.Claim.Holder.Admissions) |
| 85 | if err != nil || !yes { |
| 86 | return false, err |
| 87 | } |
| 88 | return true, s.emptyLocation(ctx, o.Claim.Holder.Location, o.Claim.Holder.Admissions...) |
| 89 | } |
| 90 | |
| 91 | func (s *claimServingOwners) emptyLocation(ctx context.Context, location string, admissions ...hostclaim.AdmissionRecord) error { |
| 92 | versions, err := s.Data.ListVersions(ctx, s.Bucket, strings.TrimSuffix(location, "/")+"/") |
| 93 | if err != nil { |
| 94 | return err |
| 95 | } |
| 96 | leaseKey := strings.TrimSuffix(location, "/") + "/" + lss3.DefaultLeasePath |
| 97 | allowed := map[string]bool{leaseKey: true} |
| 98 | for _, a := range admissions { |
| 99 | prefix := movesFilesPrefix(location, a.Generation) |
| 100 | key := hostfiles.ManifestKey(prefix) |
| 101 | for _, v := range versions { |
| 102 | if v.Key != key { |
| 103 | continue |
| 104 | } |
| 105 | if v.DeleteMarker || v.Size > 1<<20 { |
| 106 | return hostbox.ErrChanged |
| 107 | } |
| 108 | raw, err := s.Data.GetVersion(ctx, s.Bucket, key, v.VersionID) |
| 109 | if err != nil { |
| 110 | return err |
| 111 | } |
| 112 | var m hostfiles.Manifest |
| 113 | if json.Unmarshal(raw, &m) != nil || m.V != hostfiles.ManifestVersion || len(m.Entries) != 0 { |
| 114 | return hostbox.ErrChanged |
| 115 | } |
| 116 | for name, e := range m.Platform { |
| 117 | if name == instancekey.AdoptionItem && e.Deleted { |
| 118 | continue |
| 119 | } |
| 120 | if name != hostfiles.EnvItem || e.Deleted || e.Torn || e.Skipped != "" || !strings.HasPrefix(e.Object, prefix+"objects/") || !claimHexDigest(e.SHA256) { |
| 121 | return hostbox.ErrChanged |
| 122 | } |
| 123 | allowed[e.Object] = true |
| 124 | } |
| 125 | allowed[key] = true |
| 126 | } |
| 127 | } |
| 128 | for _, v := range versions { |
| 129 | if !allowed[v.Key] || v.Key != leaseKey && v.DeleteMarker { |
| 130 | return hostbox.ErrChanged |
| 131 | } |
| 132 | } |
| 133 | return nil |
| 134 | } |
| 135 | |