package host import ( "bytes" "context" "encoding/json" "errors" "os" "reflect" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" ) // A session-zero witness is retained separately from fence receipts. Admission // changes its ETag; keeping the original observation binds it to the first cold // admission without pretending that session zero held a serving fence. type servingLegacyStop struct { Observed hostclaim.Observed Result StopResult } type servingLegacyEvidence struct { Selector servingLegacyPredecessor NoProcess *servingLegacyStop `json:",omitempty"` } func servingLegacyKey(kind string, a servingArtifactIdentity) string { return kind + "-" + servingParkKey(a.Box, a.Host+"\x00"+a.Incarnation) + ".json" } func validateServingLegacyStop(s servingLegacyStop) error { o, result := s.Observed, s.Result if o.Absent || o.ETag == "" || o.Claim == nil || o.Claim.Holder == nil || o.Claim.Host != o.Host { return hostbox.ErrChanged } c, h, t := o.Claim, o.Claim.Holder, result.Target if c.Validate() != nil { return hostbox.ErrChanged } 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 { return hostbox.ErrChanged } var r StopReceipt 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) { return ErrClaimStopReceipt } 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} if t.Region == "" || t.Box == "" || t.Enrollment == "" || t.BootID == "" || t.ControllerID == "" || t.DirectoryRevision == "" || t.RosterRevision == 0 { return ErrClaimStopReceipt } _, err := validateTransportStop(req, result) return err } func (l *servingParkLedger) legacyStop(a servingArtifactIdentity) (servingLegacyStop, error) { var s servingLegacyStop b, err := l.state.Read(servingLegacyKey("legacy-stop", a)) if err != nil { return s, err } if servingParkDecode(b, &s) != nil || validateServingLegacyStop(s) != nil { return s, hostbox.ErrChanged } t := s.Result.Target if t.Host != a.Host || t.Box != a.Box || t.Region != a.Region || t.Incarnation != a.Incarnation { return s, hostbox.ErrChanged } return s, nil } func (l *servingParkLedger) legacyConsumption(a servingArtifactIdentity) (*servingLegacyConsumption, error) { b, err := l.state.Read(servingLegacyKey("legacy", a)) if errors.Is(err, os.ErrNotExist) { return nil, nil } if err != nil { return nil, err } var c servingLegacyConsumption 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 { return nil, hostbox.ErrChanged } r := servingParkRecord{Authorization: c.Authorization, Source: c.Source} if err := l.bindRecord(&r, false); err != nil { return nil, err } c.Authorization, c.Source = r.Authorization, r.Source return &c, nil } func sameServingLegacyEvidence(a, b *servingLegacyEvidence) bool { return reflect.DeepEqual(a, b) } func validateServingLegacyEvidence(a servingParkAuthorization, evidence *servingLegacyEvidence) error { if !a.Legacy || evidence == nil { return hostbox.ErrChanged } p := evidence.Selector if (p.Session == 0) != (p.Generation == "") || p.Session >= a.OriginalSuccessor.Session || (p.Empty && p.Session != 0) { return hostbox.ErrChanged } if p.Session != 0 { if evidence.NoProcess != nil { return hostbox.ErrChanged } return nil } if evidence.NoProcess == nil || validateServingLegacyStop(*evidence.NoProcess) != nil { return hostbox.ErrChanged } s, current := *evidence.NoProcess, a.OriginalSuccessor h, t := s.Observed.Claim.Holder, s.Result.Target 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 == "") { return hostbox.ErrChanged } return nil } // The current writer must still be only a preparation. Even an uncompleted // launch intention makes untagged bytes ambiguous and forbids legacy capture. func (l *servingParkLedger) legacyUnlaunched(ctx context.Context, a servingArtifactIdentity, enrollment string) error { records, err := l.journal.RootSessionLaunches(ctx, a.Host, a.Incarnation, a.Stamp, a.Session, a.Box, enrollment) if err != nil { return err } for _, r := range records { for _, reservation := range r.Reservations { if reservation.Role == "main" { return hostbox.ErrChanged } } } return nil } func sameServingLegacyConsumption(c servingLegacyConsumption, r servingParkRecord) bool { 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 { return false } for i, item := range c.Source.Files { other := r.Source.Files[i] item.Moved, item.DeleteIntent, item.Deleted = false, false, false other.Moved, other.DeleteIntent, other.Deleted = false, false, false if item != other { return false } } return true }