package host import ( "context" "github.com/carlosframework/platform/internal/activator" "os" "path/filepath" "reflect" "sort" "strings" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostfiles" "github.com/carlosframework/platform/internal/instancefs" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/regionname" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) type filesSeedSelection struct { State string Observation hostclaim.Observed Chain []string Env claimEnvFile } func inspectFilesSeed(ctx context.Context, claims release.Store, f *movesFiles, o hostclaim.Observed, actor hostbox.Actor, rt *registry.Route) (out filesSeedSelection, resultErr error) { if ctx != nil { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, hostbox.RequestLimit) defer cancel() } out.Observation = o out.State = "blocked" if ctx == nil || claims == nil || f == nil || f.Data == nil || f.EnvFile == nil || !actor.Eligible || actor.Quarantined || actor.Enrollment == "" || actor.Revision == 0 || actor.Binding == "" || actor.Box != f.Box || actor.Bucket != f.Bucket || !regionname.SameRegion(f.HomeRegion, actor.Region, f.Region) { return out, hostbox.ErrUnsupported } check := func() error { fresh, err := hostclaim.Read(ctx, claims, o.Host) if err != nil { return err } if fresh.ETag != o.ETag || fresh.Absent != o.Absent || !reflect.DeepEqual(fresh.Claim, o.Claim) { return hostbox.ErrChanged } return ctx.Err() } if err := check(); err != nil { return out, err } defer func() { if resultErr == nil { resultErr = check() } }() if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.Claim.Op.Kind != hostclaim.KindServing || o.Claim.Op.Exec != actor.Box || o.Claim.Op.ID != "" || o.Claim.Op.Phase != "" { out.State = "outOfScope" return out, nil } if rt == nil || rt.Host != o.Host { out.State = "noRow" return out, hostbox.ErrChanged } if rt.Replica { out.State = "replica" return out, nil } if rt.Seed != "" || rt.Backing != "" || rt.Unit != "" || rt.IsSidecar() || rt.Tunnel != "" { out.State = "unsupported" return out, nil } if rt.Account != o.Claim.Holder.Account || rt.App != o.Claim.Holder.App { return out, hostbox.ErrChanged } env, err := checkFilesSeedLayout(f, *rt, claimEmptyBootstrap(o.Claim.Holder)) if err != nil { out.State = "layoutBlocked" return out, err } out.Env = env if _, pending := hostclaim.PendingMoveFenceLoss(o.Claim); pending { return out, hostclaim.ErrFence } for key := range o.Claim.Op.Journal { if strings.HasPrefix(key, "fence.") { return out, hostclaim.ErrFence } } h := o.Claim.Holder if claimEmptyBootstrap(h) { versions, err := f.Data.ListVersions(ctx, f.Bucket, strings.TrimSuffix(h.Location, "/")+"/") if err != nil { return out, err } if len(versions) != 0 { return out, hostbox.ErrChanged } out.State = "emptyBootstrap" return out, nil } chain, err := filesSeedChain(h, f.HomeRegion, f.Region, f.Box, o.Host) if err != nil { return out, err } out.Chain = chain established, err := f.inspectSeedManifests(ctx, o, chain) if err != nil { return out, err } if established { out.State = "established" return out, nil } root, err := instancefs.OpenDirBeneath(env.Dir, o.Host) if err != nil { out.State = "sourceMissing" return out, err } if err := root.Close(); err != nil { return out, err } out.State = "eligible" return out, nil } func checkFilesSeedLayout(f *movesFiles, rt registry.Route, emptyBootstrap bool) (claimEnvFile, error) { var zero claimEnvFile if f == nil || !filepath.IsAbs(f.DataDir) || rt.DBPath != provision.NestedDBPath(f.DataDir, rt.Host) { return zero, hostbox.ErrChanged } env, err := f.EnvFile(rt.Host, rt.DBPath) if err != nil { return zero, err } if !env.Nested || env.Dir != provision.InstanceDir(f.DataDir, rt.Host) || env.Name != rt.Host+".env" { return zero, hostbox.ErrChanged } parent, err := instancefs.OpenDirBeneath(f.DataDir, filepath.Join("instances", provision.InstanceID(rt.Host))) if err != nil { if emptyBootstrap && os.IsNotExist(err) { return env, nil } return zero, err } if err := parent.Close(); err != nil { return zero, err } return env, nil } // Retained admissions constrain the complete chain; they never prove an origin. // A gap or a foreign cold writer refuses even when no manifest is present. func filesSeedChain(h *hostclaim.Holder, home, region, box, host string) ([]string, error) { if h == nil || h.WriterBox != box || h.Location != "instances/"+host || h.Incarnation == "" { return nil, hostbox.ErrChanged } current := h.Generation if current == "" { if h.Session != 0 || len(h.Admissions) != 0 || h.Stamp == 0 { return nil, hostbox.ErrChanged } return []string{""}, nil } seen := map[string]bool{} var chain []string session := h.Session for current != "" { if !movesFilesGeneration(current) || seen[current] { return nil, hostbox.ErrChanged } seen[current] = true chain = append(chain, current) var entry *hostclaim.AdmissionRecord for i := range h.Admissions { a := &h.Admissions[i] if a.Generation == current { if entry != nil { return nil, hostbox.ErrChanged } entry = a } } if entry == nil || entry.Session != session || entry.Box != box || entry.Mode != "cold" || !regionname.SameRegion(home, entry.Region, region) || !regionname.SameRegion(home, entry.PredecessorRegion, region) { return nil, hostbox.ErrChanged } current = entry.Predecessor if current != "" { found := false for _, a := range h.Admissions { if a.Generation == current && a.Session < session { session = a.Session found = true break } } if !found { return nil, hostbox.ErrChanged } } } return append(chain, ""), nil } // Inspect the whole chain, including older entries after an established one. // A bad manifest is never absence and never grants permission to seed past it. func (f *movesFiles) inspectSeedManifests(ctx context.Context, o hostclaim.Observed, chain []string) (bool, error) { found := false for _, gen := range chain { prefix := movesFilesPrefix(o.Claim.Holder.Location, gen) key := hostfiles.ManifestKey(prefix) version, err := f.manifestVersion(ctx, key) if err != nil { return false, err } if version == "" { continue } found = true m, present, err := hostfiles.Load(ctx, versionStore{data: f.Data, bucket: f.Bucket, key: key, version: version}, prefix) if err != nil { return false, err } if !present { return false, hostfiles.ErrNoEvidence } if err := hostfiles.RestorableWithinCap(m); err != nil { return false, err } if gen == "" { return false, hostfiles.ErrOrigin } if _, err := f.movesFilesAllowed(ctx, o, hostclaim.Admission{}, o.Claim.Holder.Location, gen, m); err != nil { return false, err } } return found, nil } // Claims, including a local claim without a route, define the census. The // registry is consulted only after each authoritative observation is read. func filesSeedCandidates(ctx context.Context, claims release.Store, box, host string) ([]hostclaim.Observed, error) { if claims == nil || box == "" || ctx == nil { return nil, hostbox.ErrUnsupported } ctx, cancel := context.WithTimeout(ctx, hostbox.RequestLimit) defer cancel() if host != "" { o, err := hostclaim.Read(ctx, claims, host) if err != nil { return nil, err } return []hostclaim.Observed{o}, nil } keys, err := claims.List(ctx, "control/hosts/") if err != nil { return nil, err } var out []hostclaim.Observed for _, key := range keys { tail, ok := strings.CutPrefix(key, "control/hosts/") if !ok { return nil, hostbox.ErrChanged } if strings.Contains(tail, "/") { continue } host := strings.TrimSuffix(tail, ".json") want, err := hostclaim.Key(host) if err != nil || want != key { return nil, hostbox.ErrChanged } o, err := hostclaim.Read(ctx, claims, host) if err != nil { return nil, err } if !o.Absent && o.Claim != nil && o.Claim.Op.Exec == box { out = append(out, o) } } sort.Slice(out, func(i, j int) bool { return out[i].Host < out[j].Host }) return out, nil } func selectFilesSeedInputs(ctx context.Context, inputs ClaimWakeInputs, rt registry.Route, selected filesSeedSelection) (ClaimLaunchInputs, error) { if inputs == nil || selected.State != "eligible" { return ClaimLaunchInputs{}, hostbox.ErrUnsupported } in, err := inputs.Select(ctx, rt, selected.Observation, activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second}) if err != nil { return ClaimLaunchInputs{}, err } if len(in.Roles) != 1 || in.Roles[0] != "main" || in.Executable == "" || len(in.ExecutableSHA256) != 64 || in.DBPath != rt.DBPath || in.Identity.Config == "" || in.Identity.Release == "" || in.Admission.Mode != "cold" || in.Baseline == nil || in.Empty || in.Prepared { return ClaimLaunchInputs{}, hostbox.ErrUnsupported } return in, nil }