//go:build linux package host import ( "encoding/json" "errors" "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostfiles" "github.com/carlosframework/platform/internal/lease/leasetest" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) type filesSeedFixture struct { t *testing.T claims release.Store data *leasetest.VersionedBucket files *movesFiles route registry.Route observed hostclaim.Observed actor hostbox.Actor } func newFilesSeedFixture(t *testing.T) *filesSeedFixture { t.Helper() t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1") t.Setenv("CARLOS_HOST_CLAIM", "1") claims := release.NewDirStore(t.TempDir()) o, err := hostclaim.Read(t.Context(), claims, filesHost) if err != nil { t.Fatal(err) } nh := hostclaim.NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + filesHost + ".json", Account: "bab", App: "jam", Host: filesHost, Region: "ie", CreatedAt: filesCreated} o, err = hostclaim.Update(t.Context(), claims, o, hostclaim.Adopt(nh, "box-1", nil, nil, "adopt", func() string { return "adopt" })) if err != nil { t.Fatal(err) } data := leasetest.NewVersionedBucket() data.AddBucket("data") dir := t.TempDir() rt := registry.Route{Host: filesHost, Kind: "instance", Account: "bab", App: "jam", Hibernate: true, DBPath: provision.NestedDBPath(dir, filesHost), Socket: provision.NestedSocket(filesHost)} env := filepath.Dir(rt.DBPath) if err := os.MkdirAll(filepath.Join(env, filesHost), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(env, filesHost+".env"), []byte("KEY=value\n"), 0600); err != nil { t.Fatal(err) } f := &movesFiles{Files: casBucketStore{b: data, bucket: "data"}, Data: data, Bucket: "data", Box: "box-1", Region: "ie", HomeRegion: "ie", DataDir: dir, EnvFile: claimEnvFileFor(dir, testClaimRoutes{rows: map[string]registry.Route{filesHost: rt}}), Proof: ClaimOriginProof{Claims: claims, HomeRegion: "ie", Region: "ie"}} return &filesSeedFixture{t: t, claims: claims, data: data, files: f, route: rt, observed: o, actor: hostbox.Actor{Box: "box-1", Region: "ie", HomeRegion: "ie", Bucket: "data", Enrollment: "enrolled", Revision: 1, Binding: "1", Eligible: true}} } func (f *filesSeedFixture) admit(gen string) { f.t.Helper() o := f.observed a := hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: f.actor.Box, Region: f.actor.Region, PredecessorRegion: f.actor.Region, Mode: "cold", NewGeneration: gen} var err error f.observed, err = hostclaim.Update(f.t.Context(), f.claims, o, hostclaim.AdmitWriter(a)) if err != nil { f.t.Fatal(err) } } func (f *filesSeedFixture) inspect() (filesSeedSelection, error) { return inspectFilesSeed(f.t.Context(), f.claims, f.files, f.observed, f.actor, &f.route) } func TestFilesSeedClassificationRealOrigins(t *testing.T) { f := newFilesSeedFixture(t) for _, gen := range []string{"", "first", "retry"} { if gen != "" { f.admit(gen) } before := f.observed.ETag got, err := f.inspect() if err != nil || got.State != "eligible" { t.Fatal(gen, got, err) } fresh, err := hostclaim.Read(t.Context(), f.claims, filesHost) if err != nil || fresh.ETag != before { t.Fatal("classifier wrote claim", err) } } if f.observed.Claim.Origins == nil { t.Fatal("admissions did not record real origins") } } func TestFilesSeedClassificationRefusals(t *testing.T) { for _, fault := range []string{"missing-files", "flat", "symlink", "replica", "sidecar", "backing", "seed", "unit", "tunnel", "no-row", "stale", "foreign", "chain-gap", "foreign-predecessor", "cycle"} { t.Run(fault, func(t *testing.T) { f := newFilesSeedFixture(t) f.admit("first") f.admit("second") var row *registry.Route = &f.route switch fault { case "missing-files": if err := os.Remove(filepath.Join(filepath.Dir(f.route.DBPath), filesHost)); err != nil { t.Fatal(err) } case "flat": f.route.DBPath = filepath.Join(f.files.DataDir, filesHost+".db") case "symlink": path := filepath.Join(filepath.Dir(f.route.DBPath), filesHost) if err := os.Remove(path); err != nil { t.Fatal(err) } if err := os.Symlink(t.TempDir(), path); err != nil { t.Fatal(err) } case "replica": f.route.Replica = true case "sidecar": f.route.Kind = "sidecar" case "backing": f.route.Backing = "foreign" case "seed": f.route.Seed = "seed" case "unit": f.route.Unit = "unit" case "tunnel": f.route.Tunnel = "tunnel" case "no-row": row = nil case "stale": old := f.observed f.admit("third") f.observed = old default: o, err := hostclaim.Update(t.Context(), f.claims, f.observed, func(c *hostclaim.Claim) error { switch fault { case "foreign": c.Op.Exec = "other" case "chain-gap": c.Holder.Admissions = c.Holder.Admissions[1:] case "foreign-predecessor": c.Holder.Admissions[0].Box = "other" case "cycle": c.Holder.Admissions[0].Predecessor = "second" } return nil }) if err != nil { t.Fatal(err) } f.observed = o } got, err := inspectFilesSeed(t.Context(), f.claims, f.files, f.observed, f.actor, row) if err == nil && got.State == "eligible" { t.Fatal("eligible", fault) } }) } } func TestFilesSeedExistingManifestNeverSeededPast(t *testing.T) { f := newFilesSeedFixture(t) f.admit("first") f.admit("second") prefix := movesFilesPrefix(f.observed.Claim.Holder.Location, "first") p, err := hostfiles.NewFromCopy(f.files.Files, prefix, emptyManifest(), nil) if err != nil { t.Fatal(err) } p.SetOrigins([]string{prefix}) if _, err = p.Commit(t.Context()); err != nil { t.Fatal(err) } got, err := f.inspect() if err != nil || got.State != "established" { t.Fatal(got, err) } // An unreadable older manifest still refuses after a newer valid one. f.data.Put("data", movesFilesPrefix(f.observed.Claim.Holder.Location, "")+"manifest.json", []byte("broken")) if _, err = f.inspect(); err == nil { t.Fatal("seeded past malformed older manifest") } } func TestFilesSeedManifestCapIncludesTombstonesAndSkips(t *testing.T) { for _, entry := range []hostfiles.Entry{{Deleted: true, Seq: 1}, {Skipped: "too-large", Seq: 1}} { f := newFilesSeedFixture(t) f.admit("first") prefix := movesFilesPrefix(f.observed.Claim.Holder.Location, "first") m := emptyManifest() m.Seq = 1 // Valid path components, beyond the cumulative path cap. path := "a" for len(path) < 5000 { path += "/a" } m.Entries = map[string]hostfiles.Entry{path: entry} body, _ := json.Marshal(m) f.data.Put("data", prefix+"manifest.json", body) if _, err := f.inspect(); !errors.Is(err, hostfiles.ErrTooDeep) { t.Fatal("cap", err) } } } func TestFilesSeedCandidatesUseClaimsWithoutRegistry(t *testing.T) { f := newFilesSeedFixture(t) got, err := filesSeedCandidates(t.Context(), f.claims, f.actor.Box, "") if err != nil || len(got) != 1 || got[0].ETag != f.observed.ETag { t.Fatal(got, err) } got, err = filesSeedCandidates(t.Context(), f.claims, "other-box", "") if err != nil || len(got) != 0 { t.Fatal(got, err) } got, err = filesSeedCandidates(t.Context(), f.claims, "other-box", filesHost) if err != nil || len(got) != 1 { t.Fatal(got, err) } if _, err = filesSeedCandidates(t.Context(), f.claims, f.actor.Box, "../bad"); err == nil { t.Fatal("accepted path selector") } if _, err = f.claims.Put(t.Context(), "control/hosts/bad-key", []byte("{}"), ""); err != nil { t.Fatal(err) } if _, err = filesSeedCandidates(t.Context(), f.claims, f.actor.Box, ""); err == nil { t.Fatal("ignored malformed census") } }