| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "errors" |
| 8 | "os" |
| 9 | "reflect" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/hostbox" |
| 12 | "github.com/carlosframework/platform/internal/hostclaim" |
| 13 | ) |
| 14 | |
| 15 | // A session-zero witness is retained separately from fence receipts. Admission |
| 16 | // changes its ETag; keeping the original observation binds it to the first cold |
| 17 | // admission without pretending that session zero held a serving fence. |
| 18 | type servingLegacyStop struct { |
| 19 | Observed hostclaim.Observed |
| 20 | Result StopResult |
| 21 | } |
| 22 | |
| 23 | type servingLegacyEvidence struct { |
| 24 | Selector servingLegacyPredecessor |
| 25 | NoProcess *servingLegacyStop `json:",omitempty"` |
| 26 | } |
| 27 | |
| 28 | func servingLegacyKey(kind string, a servingArtifactIdentity) string { |
| 29 | return kind + "-" + servingParkKey(a.Box, a.Host+"\x00"+a.Incarnation) + ".json" |
| 30 | } |
| 31 | |
| 32 | func validateServingLegacyStop(s servingLegacyStop) error { |
| 33 | o, result := s.Observed, s.Result |
| 34 | if o.Absent || o.ETag == "" || o.Claim == nil || o.Claim.Holder == nil || o.Claim.Host != o.Host { |
| 35 | return hostbox.ErrChanged |
| 36 | } |
| 37 | c, h, t := o.Claim, o.Claim.Holder, result.Target |
| 38 | if c.Validate() != nil { |
| 39 | return hostbox.ErrChanged |
| 40 | } |
| 41 | if c.Op.Kind != hostclaim.KindServing || c.Op.ID != "" || c.Op.Phase != "" || c.Op.Exec != t.Box || h.Session != 0 || h.Generation != "" || len(h.Admissions) != 0 || (h.WriterBox != "" && h.WriterBox != t.Box) || t.Host != o.Host || t.Incarnation != h.Incarnation || t.Stamp != h.Stamp || t.Stamp == 0 || t.Session != 0 || t.FenceID != "" || t.Op != "" || t.Phase != "" || t.ObligationID != "" || t.ClaimGen != c.Gen || t.ClaimETag != o.ETag { |
| 42 | return hostbox.ErrChanged |
| 43 | } |
| 44 | var r StopReceipt |
| 45 | if json.Unmarshal(result.Bytes, &r) != nil || !bytes.Equal(result.Bytes, mustMarshalStopReceipt(r)) || r.V != 1 || r.Kind != StopReceiptNoProcess || !stopComponent(r.ID) || r.ProducerDrainDigest != "" || !stopDigest(r.InventorySHA256) || !stopDigest(r.LocalJournalSHA256) || len(r.CgroupIdentities) == 0 || len(r.UnitInvocationIDs) != 0 || r.ClaimETag != o.ETag || r.ClaimGen != c.Gen || result.EvidenceAt.IsZero() || result.Key != stopEvidenceKey(o.Host, "", t.Box, r.ID) { |
| 46 | return ErrClaimStopReceipt |
| 47 | } |
| 48 | req := StopRequest{Region: t.Region, Host: t.Host, Incarnation: t.Incarnation, InstallationStamp: t.Stamp, Box: t.Box, Enrollment: t.Enrollment, BootID: t.BootID, ControllerID: t.ControllerID, ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: t.DirectoryRevision, RosterRevision: t.RosterRevision} |
| 49 | if t.Region == "" || t.Box == "" || t.Enrollment == "" || t.BootID == "" || t.ControllerID == "" || t.DirectoryRevision == "" || t.RosterRevision == 0 { |
| 50 | return ErrClaimStopReceipt |
| 51 | } |
| 52 | _, err := validateTransportStop(req, result) |
| 53 | return err |
| 54 | } |
| 55 | |
| 56 | func (l *servingParkLedger) legacyStop(a servingArtifactIdentity) (servingLegacyStop, error) { |
| 57 | var s servingLegacyStop |
| 58 | b, err := l.state.Read(servingLegacyKey("legacy-stop", a)) |
| 59 | if err != nil { |
| 60 | return s, err |
| 61 | } |
| 62 | if servingParkDecode(b, &s) != nil || validateServingLegacyStop(s) != nil { |
| 63 | return s, hostbox.ErrChanged |
| 64 | } |
| 65 | t := s.Result.Target |
| 66 | if t.Host != a.Host || t.Box != a.Box || t.Region != a.Region || t.Incarnation != a.Incarnation { |
| 67 | return s, hostbox.ErrChanged |
| 68 | } |
| 69 | return s, nil |
| 70 | } |
| 71 | |
| 72 | func (l *servingParkLedger) legacyConsumption(a servingArtifactIdentity) (*servingLegacyConsumption, error) { |
| 73 | b, err := l.state.Read(servingLegacyKey("legacy", a)) |
| 74 | if errors.Is(err, os.ErrNotExist) { |
| 75 | return nil, nil |
| 76 | } |
| 77 | if err != nil { |
| 78 | return nil, err |
| 79 | } |
| 80 | var c servingLegacyConsumption |
| 81 | if servingParkDecode(b, &c) != nil || !c.Authorization.Legacy || c.Authorization.Successor.Host != a.Host || c.Authorization.Successor.Box != a.Box || c.Authorization.Successor.Incarnation != a.Incarnation { |
| 82 | return nil, hostbox.ErrChanged |
| 83 | } |
| 84 | r := servingParkRecord{Authorization: c.Authorization, Source: c.Source} |
| 85 | if err := l.bindRecord(&r, false); err != nil { |
| 86 | return nil, err |
| 87 | } |
| 88 | c.Authorization, c.Source = r.Authorization, r.Source |
| 89 | return &c, nil |
| 90 | } |
| 91 | |
| 92 | func sameServingLegacyEvidence(a, b *servingLegacyEvidence) bool { return reflect.DeepEqual(a, b) } |
| 93 | |
| 94 | func validateServingLegacyEvidence(a servingParkAuthorization, evidence *servingLegacyEvidence) error { |
| 95 | if !a.Legacy || evidence == nil { |
| 96 | return hostbox.ErrChanged |
| 97 | } |
| 98 | p := evidence.Selector |
| 99 | if (p.Session == 0) != (p.Generation == "") || p.Session >= a.OriginalSuccessor.Session || (p.Empty && p.Session != 0) { |
| 100 | return hostbox.ErrChanged |
| 101 | } |
| 102 | if p.Session != 0 { |
| 103 | if evidence.NoProcess != nil { |
| 104 | return hostbox.ErrChanged |
| 105 | } |
| 106 | return nil |
| 107 | } |
| 108 | if evidence.NoProcess == nil || validateServingLegacyStop(*evidence.NoProcess) != nil { |
| 109 | return hostbox.ErrChanged |
| 110 | } |
| 111 | s, current := *evidence.NoProcess, a.OriginalSuccessor |
| 112 | h, t := s.Observed.Claim.Holder, s.Result.Target |
| 113 | if t.Host != current.Host || t.Box != current.Box || t.Region != current.Region || t.Incarnation != current.Incarnation || h.Location != current.Location || h.Stamp >= current.Stamp || s.Observed.Claim.Gen >= current.Stamp || p.Empty != (h.WriterBox == "") { |
| 114 | return hostbox.ErrChanged |
| 115 | } |
| 116 | return nil |
| 117 | } |
| 118 | |
| 119 | // The current writer must still be only a preparation. Even an uncompleted |
| 120 | // launch intention makes untagged bytes ambiguous and forbids legacy capture. |
| 121 | func (l *servingParkLedger) legacyUnlaunched(ctx context.Context, a servingArtifactIdentity, enrollment string) error { |
| 122 | records, err := l.journal.RootSessionLaunches(ctx, a.Host, a.Incarnation, a.Stamp, a.Session, a.Box, enrollment) |
| 123 | if err != nil { |
| 124 | return err |
| 125 | } |
| 126 | for _, r := range records { |
| 127 | for _, reservation := range r.Reservations { |
| 128 | if reservation.Role == "main" { |
| 129 | return hostbox.ErrChanged |
| 130 | } |
| 131 | } |
| 132 | } |
| 133 | return nil |
| 134 | } |
| 135 | |
| 136 | func sameServingLegacyConsumption(c servingLegacyConsumption, r servingParkRecord) bool { |
| 137 | if c.Authorization != r.Authorization || !sameServingRootStops(c.Receipts, r.Receipts) || !sameServingLegacyEvidence(c.LegacyEvidence, r.LegacyEvidence) || c.Source.Directory != r.Source.Directory || c.Source.Parent != r.Source.Parent || c.Source.Persistent != r.Source.Persistent || c.Source.BootID != r.Source.BootID { |
| 138 | return false |
| 139 | } |
| 140 | for i, item := range c.Source.Files { |
| 141 | other := r.Source.Files[i] |
| 142 | item.Moved, item.DeleteIntent, item.Deleted = false, false, false |
| 143 | other.Moved, other.DeleteIntent, other.Deleted = false, false, false |
| 144 | if item != other { |
| 145 | return false |
| 146 | } |
| 147 | } |
| 148 | return true |
| 149 | } |
| 150 | |