package host import ( "context" "errors" "fmt" "github.com/carlosframework/platform/internal/moves" "os" "path/filepath" "strconv" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/park" ) // claimArtifactRetirement parks a superseded database and marker under the // admitted installation's descriptor guard. Landing probes, restoring and // reverting share the same exact inventory checks. Initial source recovery // uses the source cut's journal and report; later sessions require root's // completed writer-stop receipt. Files and environment paths are unaffected. // Continue is bound only by the moves C1 integration; nil refuses. type claimArtifactRetirement struct { Local park.Local Continue func(kind, from, to string) bool } func (r claimArtifactRetirement) Retire(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, generation string) error { box := r.Local.Box if r.Continue == nil || r.Local.Claims == nil || box == "" || !filepath.IsAbs(r.Local.DataDir) || admitted.Claim == nil || admitted.Claim.Holder == nil || !claimArtifactKind(admitted.Claim.Op.Kind) { return hostbox.ErrChanged } h := admitted.Claim.Holder if generation == "" || generation == h.Generation || !filepath.IsAbs(in.DBPath) { return hostbox.ErrChanged } marker, err := filepath.Rel(r.Local.DataDir, in.DBPath+".gen") if err != nil || !filepath.IsLocal(marker) { return hostbox.ErrChanged } database, err := filepath.Rel(r.Local.DataDir, in.DBPath) if err != nil || !filepath.IsLocal(database) { return hostbox.ErrChanged } marker, database = filepath.ToSlash(marker), filepath.ToSlash(database) fresh, err := hostclaim.Read(ctx, r.Local.Claims, admitted.Host) if err != nil { return err } if !claimSameWriter(admitted, fresh, r.Continue) { return hostbox.ErrChanged } c := fresh.Claim if _, ok := claimArtifactWriter(c, box); !ok { return hostbox.ErrChanged } // The superseded database is this op's: its prepared generation, named by // the op id, or a generation an earlier session on this box was admitted // into under it. known := c.Op.Kind == hostclaim.KindLanding && generation == c.Op.ID if claimRecoveryKind(c.Op.Kind) && generation == c.Op.Journal[hostclaim.JournalSrcGen] { known = true } for _, a := range c.Holder.Admissions { if a.Generation == generation && a.Box == box && a.Session < c.Holder.Session { known = true } } if !known { return hostbox.ErrChanged } paths := c.Inventory[box].Paths stamp := paths[marker] if stamp == 0 || stamp >= c.Holder.Stamp || paths[database] != stamp { return hostbox.ErrChanged } if err := r.artifactsStopped(ctx, admitted.Host, c, box, stamp, generation); err != nil { return err } check := func(now *hostclaim.Claim) error { if !claimSameWriter(fresh, hostclaim.Observed{Host: admitted.Host, Claim: now}, r.Continue) { return hostbox.ErrChanged } if _, ok := claimArtifactWriter(now, box); !ok { return hostbox.ErrChanged } return nil } retirement := fmt.Sprintf("superseded-%d-%d", c.Holder.Session, stamp) before, after, err := r.Local.RetireSuperseded(ctx, admitted.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, c.Op.ID, retirement, stamp, []string{database, marker}, check) if err != nil { return err } return in.observeClaimWrite(before, after) } // stoppedAt requires root's completed full stop of the writer session that ran // installation stamp on box under the op: every writer-session receipt the // guard journalled for that installation names one session, verifies byte for // byte, and that session was admitted on box before the current one. At most // one session runs a stamp: a cold admission takes its own stamp unless it is // the first writer since the stamp was issued. No receipt refuses. func (r claimArtifactRetirement) stoppedAt(ctx context.Context, host string, c *hostclaim.Claim, box string, stamp uint64) error { receipts, err := r.Local.Guard.CompletedWriterStops(ctx, host, hostguard.Captured{Incarnation: c.Holder.Incarnation, Stamp: stamp}, c.Op.ID, box) if err != nil { return err } if len(receipts) == 0 { return hostguard.ErrNoFenceReceipt } session := receipts[0].Target.Session for _, receipt := range receipts { if receipt.Target.Session != session || verifyRetirementReceipt(ctx, receipt) != nil { return ErrClaimStopReceipt } } for _, a := range c.Holder.Admissions { if a.Session == session && a.Box == box && session < c.Holder.Session { return nil } } return hostbox.ErrChanged } // Recover re-drives retirement before any restore/materialization when a // failed inventory CAS left both database and marker parked. The absent // marker cannot be the only retry trigger: ownership must be recorded before // the successor creates new bytes at those paths. Current tags mean the // completed retirement already recorded that ownership. func (r claimArtifactRetirement) Recover(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs) error { if r.Continue == nil || r.Local.Claims == nil || admitted.Claim == nil || admitted.Claim.Holder == nil || !filepath.IsAbs(in.DBPath) { return hostbox.ErrChanged } if !claimArtifactKind(admitted.Claim.Op.Kind) { return nil } if _, err := os.Lstat(in.DBPath + ".gen"); err == nil { return nil } else if !errors.Is(err, os.ErrNotExist) { return err } fresh, err := hostclaim.Read(ctx, r.Local.Claims, admitted.Host) if err != nil { return err } if !claimSameWriter(admitted, fresh, r.Continue) { return hostbox.ErrChanged } c, box := fresh.Claim, r.Local.Box if _, ok := claimArtifactWriter(c, box); !ok { return hostbox.ErrChanged } db, err := filepath.Rel(r.Local.DataDir, in.DBPath) if err != nil || !filepath.IsLocal(db) { return hostbox.ErrChanged } db = filepath.ToSlash(db) paths := c.Inventory[box].Paths stamp := paths[db+".gen"] if stamp == 0 || paths[db] != stamp { return hostbox.ErrChanged } if stamp == c.Holder.Stamp { return nil } if stamp > c.Holder.Stamp { return hostbox.ErrChanged } if claimRecoveryKind(c.Op.Kind) { sourceStamp, e := strconv.ParseUint(c.Op.Journal[hostclaim.JournalSourceStamp], 10, 64) if e == nil && stamp == sourceStamp { return r.Retire(ctx, admitted, in, c.Op.Journal[hostclaim.JournalSrcGen]) } } receipts, err := r.Local.Guard.CompletedWriterStops(ctx, admitted.Host, hostguard.Captured{Incarnation: c.Holder.Incarnation, Stamp: stamp}, c.Op.ID, box) if err != nil { return err } if len(receipts) == 0 { return hostguard.ErrNoFenceReceipt } session := receipts[0].Target.Session for _, a := range c.Holder.Admissions { if a.Session == session && a.Box == box && a.Session < c.Holder.Session { // Retire repeats all receipt, generation and guarded inventory checks; // park.Local resumes the deterministic journal, even with both paths absent. return r.Retire(ctx, admitted, in, a.Generation) } } return hostbox.ErrChanged } func claimArtifactKind(kind string) bool { return kind == hostclaim.KindLanding || claimRecoveryKind(kind) } func claimArtifactWriter(c *hostclaim.Claim, box string) (hostclaim.AdmissionRecord, bool) { if c != nil && claimRecoveryKind(c.Op.Kind) { return hostguard.RecoveryWriter(c, box) } return hostguard.LandingWriter(c, box) } func (r claimArtifactRetirement) artifactsStopped(ctx context.Context, host string, c *hostclaim.Claim, box string, stamp uint64, generation string) error { sourceStamp, err := strconv.ParseUint(c.Op.Journal[hostclaim.JournalSourceStamp], 10, 64) if !claimRecoveryKind(c.Op.Kind) || err != nil || stamp != sourceStamp { return r.stoppedAt(ctx, host, c, box, stamp) } if c.Op.Intent["source"] != box || generation != c.Op.Journal[hostclaim.JournalSrcGen] { return hostbox.ErrChanged } session, err := strconv.ParseUint(c.Op.Journal[hostclaim.JournalCutSession], 10, 64) if err != nil || session >= c.Holder.Session { return hostbox.ErrChanged } key := moves.StoppedKey(moves.ReportPrefix(c.Holder.Account, c.Holder.App, host, c.Op.ID), "source") if c.Op.Journal[hostclaim.JournalEvidenceSource] != key { return hostbox.ErrChanged } report, err := readStoppedReport(ctx, r.Local.Claims, key) if err != nil { return err } if report.Box != box || report.Role != "source" || report.Host != host || report.OpID != c.Op.ID || report.Incarnation != c.Holder.Incarnation { return hostbox.ErrChanged } return nil }