| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "path/filepath" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostbox" |
| 11 | "github.com/carlosframework/platform/internal/hostwrite" |
| 12 | "github.com/carlosframework/platform/internal/lease" |
| 13 | "github.com/carlosframework/platform/internal/lease/leasetest" |
| 14 | "github.com/carlosframework/platform/internal/registry" |
| 15 | "github.com/carlosframework/platform/internal/release" |
| 16 | ) |
| 17 | |
| 18 | // fakeFilesSource is a C8 files source that records its opens and closes. |
| 19 | type fakeFilesSource struct { |
| 20 | opens, closes int |
| 21 | actor hostbox.Actor |
| 22 | err error |
| 23 | files ClaimFilesManifest |
| 24 | } |
| 25 | |
| 26 | func (s *fakeFilesSource) Open(_ context.Context, _ release.Store, _ lease.VersionedStore, actor hostbox.Actor) (ClaimFilesManifest, error) { |
| 27 | s.opens++ |
| 28 | s.actor = actor |
| 29 | return s.files, s.err |
| 30 | } |
| 31 | |
| 32 | func (s *fakeFilesSource) Close(context.Context) error { s.closes++; return nil } |
| 33 | |
| 34 | func withClaimFilesSource(t *testing.T, s ClaimProductionFilesSource) { |
| 35 | t.Helper() |
| 36 | claimFilesSources.Lock() |
| 37 | prev := claimFilesSources.Source |
| 38 | claimFilesSources.Source = s |
| 39 | claimFilesSources.Unlock() |
| 40 | t.Cleanup(func() { |
| 41 | claimFilesSources.Lock() |
| 42 | claimFilesSources.Source = prev |
| 43 | claimFilesSources.Unlock() |
| 44 | }) |
| 45 | } |
| 46 | |
| 47 | // The C8 provider registers once at process start, under |
| 48 | // CARLOS_HOST_CLAIM alone (platform-1e, 2026-10-12): a nil source and a |
| 49 | // second registration are refused, and neither can replace the first. |
| 50 | func TestRegisterProductionClaimFilesIsOneShot(t *testing.T) { |
| 51 | withClaimFilesSource(t, nil) |
| 52 | if err := RegisterProductionClaimFiles(nil); !errors.Is(err, hostwrite.ErrUnavailable) { |
| 53 | t.Fatalf("nil source: %v", err) |
| 54 | } |
| 55 | first := &fakeFilesSource{files: &movesFiles{}} |
| 56 | if err := RegisterProductionClaimFiles(first); err != nil { |
| 57 | t.Fatal(err) |
| 58 | } |
| 59 | if err := RegisterProductionClaimFiles(&fakeFilesSource{}); !errors.Is(err, hostwrite.ErrUnavailable) { |
| 60 | t.Fatalf("second registration: %v", err) |
| 61 | } |
| 62 | if productionClaimFilesSource() != ClaimProductionFilesSource(first) { |
| 63 | t.Fatal("the registered source is not the first") |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | // openClaimFilesManifest binds the registered source's provider into the |
| 68 | // serving sources with the claim on and moves off or on; with the claim |
| 69 | // off, or nothing registered, the bindings are untouched and nothing is |
| 70 | // opened. A provider the moves source already bound is a wiring error, |
| 71 | // never silently replaced; an Open error or a nil provider refuses. |
| 72 | func TestOpenClaimFilesManifestBindsUnderTheClaimAlone(t *testing.T) { |
| 73 | store := release.NewDirStore(t.TempDir()) |
| 74 | data := leasetest.NewVersionedBucket() |
| 75 | actor := hostbox.Actor{Box: "box-1", Region: "ie", Bucket: "data", HomeRegion: "ie", Enrollment: "enr-1"} |
| 76 | claimOn := func(k string) string { |
| 77 | if k == "CARLOS_HOST_CLAIM" { |
| 78 | return "1" |
| 79 | } |
| 80 | return "" |
| 81 | } |
| 82 | bothOn := func(k string) string { |
| 83 | if k == "CARLOS_HOST_CLAIM" || k == "CARLOS_MOVES_PHASE1" { |
| 84 | return "1" |
| 85 | } |
| 86 | return "" |
| 87 | } |
| 88 | off := func(string) string { return "" } |
| 89 | |
| 90 | pass := func(context.Context) error { return nil } |
| 91 | provider := &movesFiles{} |
| 92 | for name, getenv := range map[string]func(string) string{"claim alone": claimOn, "both": bothOn} { |
| 93 | src := &fakeFilesSource{files: provider} |
| 94 | var out ClaimOwnerBindings |
| 95 | closeFn, err := openClaimFilesManifest(context.Background(), getenv, src, store, data, actor, &out, pass) |
| 96 | if err != nil || closeFn == nil { |
| 97 | t.Fatalf("%s: %v %v", name, err, closeFn != nil) |
| 98 | } |
| 99 | if out.Serving.Files != ClaimFilesManifest(provider) || src.opens != 1 || src.actor != actor { |
| 100 | t.Fatalf("%s: bound %v, opens %d, actor %+v", name, out.Serving.Files, src.opens, src.actor) |
| 101 | } |
| 102 | if err := closeFn(context.Background()); err != nil || src.closes != 1 { |
| 103 | t.Fatalf("%s: close %v %d", name, err, src.closes) |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | // Claim off: untouched, not opened. |
| 108 | src := &fakeFilesSource{files: provider} |
| 109 | var out ClaimOwnerBindings |
| 110 | closeFn, err := openClaimFilesManifest(context.Background(), off, src, store, data, actor, &out, pass) |
| 111 | if err != nil || closeFn != nil || out.Serving.Files != nil || src.opens != 0 { |
| 112 | t.Fatalf("claim off: %v %v %v %d", err, closeFn != nil, out.Serving.Files, src.opens) |
| 113 | } |
| 114 | // Nothing registered: untouched. |
| 115 | if closeFn, err := openClaimFilesManifest(context.Background(), claimOn, nil, store, data, actor, &out, pass); err != nil || closeFn != nil || out.Serving.Files != nil { |
| 116 | t.Fatalf("nothing registered: %v %v %v", err, closeFn != nil, out.Serving.Files) |
| 117 | } |
| 118 | // Already bound by the moves source: refused, not replaced. |
| 119 | already := &movesFiles{Box: "other"} |
| 120 | out = ClaimOwnerBindings{Serving: ClaimServingSources{Files: already}} |
| 121 | src = &fakeFilesSource{files: provider} |
| 122 | if _, err := openClaimFilesManifest(context.Background(), claimOn, src, store, data, actor, &out, pass); !errors.Is(err, hostwrite.ErrUnavailable) || out.Serving.Files != ClaimFilesManifest(already) || src.opens != 0 { |
| 123 | t.Fatalf("two providers: %v %v %d", err, out.Serving.Files, src.opens) |
| 124 | } |
| 125 | // An Open error refuses; a nil provider refuses. |
| 126 | out = ClaimOwnerBindings{} |
| 127 | src = &fakeFilesSource{err: errors.New("no bucket")} |
| 128 | if _, err := openClaimFilesManifest(context.Background(), claimOn, src, store, data, actor, &out, pass); err == nil || out.Serving.Files != nil { |
| 129 | t.Fatalf("open error: %v %v", err, out.Serving.Files) |
| 130 | } |
| 131 | src = &fakeFilesSource{} |
| 132 | if _, err := openClaimFilesManifest(context.Background(), claimOn, src, store, data, actor, &out, pass); !errors.Is(err, hostwrite.ErrUnavailable) || out.Serving.Files != nil { |
| 133 | t.Fatalf("nil provider: %v %v", err, out.Serving.Files) |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | // C8 reads the claim's data bucket as the serving and moves owners do |
| 138 | // (whole-branch fixes, item 5): through hostbox.RequestStore, so every |
| 139 | // version listing and exact-version read it makes is bounded by the |
| 140 | // claim's request limit and preceded by the box's identity check, a |
| 141 | // refused identity issuing no bucket call. An establishment under a |
| 142 | // refused identity refuses with hostbox.ErrChanged and writes nothing. |
| 143 | func TestClaimFilesReadTheBucketThroughTheRequestStore(t *testing.T) { |
| 144 | inner := &mwBucket{VersionedBucket: leasetest.NewVersionedBucket()} |
| 145 | inner.AddBucket("data") |
| 146 | var refuse error |
| 147 | checks := 0 |
| 148 | check := func(context.Context) error { checks++; return refuse } |
| 149 | src := &movesFilesSource{DataDir: t.TempDir(), Routes: testClaimRoutes{}, newFiles: func(context.Context, string, string) (release.Store, error) { |
| 150 | return casBucketStore{b: inner.VersionedBucket, bucket: "data"}, nil |
| 151 | }} |
| 152 | actor := hostbox.Actor{Box: "uk-box", Region: "uk", Bucket: "data", HomeRegion: "ie", Cloud: hostbox.CloudIdentity{Region: "eu-west-2"}} |
| 153 | var out ClaimOwnerBindings |
| 154 | out.Data = inner |
| 155 | closeFn, err := openClaimFilesManifest(t.Context(), func(k string) string { return "1" }, src, release.NewDirStore(t.TempDir()), inner, actor, &out, check) |
| 156 | if err != nil || closeFn == nil { |
| 157 | t.Fatalf("open: %v", err) |
| 158 | } |
| 159 | f, ok := out.Serving.Files.(*movesFiles) |
| 160 | if !ok { |
| 161 | t.Fatalf("%T", out.Serving.Files) |
| 162 | } |
| 163 | if _, ok := f.Data.(hostbox.RequestStore); !ok { |
| 164 | t.Fatalf("C8's bucket is %T, not the claim's request store", f.Data) |
| 165 | } |
| 166 | before := time.Now() |
| 167 | if _, err := f.Data.ListVersions(t.Context(), "data", "instances/"); err != nil { |
| 168 | t.Fatal(err) |
| 169 | } |
| 170 | if inner.lists != 1 || checks != 1 || inner.deadlines[0].IsZero() || inner.deadlines[0].After(time.Now().Add(hostbox.RequestLimit)) || inner.deadlines[0].Before(before.Add(hostbox.RequestLimit-time.Minute)) { |
| 171 | t.Fatalf("the request's deadline %v (lists %d, checks %d), want the request limit", inner.deadlines, inner.lists, checks) |
| 172 | } |
| 173 | refuse = hostbox.ErrChanged |
| 174 | if _, err := f.Data.ListVersions(t.Context(), "data", "instances/"); !errors.Is(err, hostbox.ErrChanged) || inner.lists != 1 { |
| 175 | t.Fatalf("a refused identity: %v, %d bucket calls", err, inner.lists) |
| 176 | } |
| 177 | if _, err := f.Data.GetVersion(t.Context(), "data", "k", "v"); !errors.Is(err, hostbox.ErrChanged) { |
| 178 | t.Fatalf("a refused identity's read: %v", err) |
| 179 | } |
| 180 | // An establishment under a refused identity: the chain's first listing |
| 181 | // refuses, nothing is written. |
| 182 | x := &filesFixture{t: t, bucket: inner.VersionedBucket, files: casBucketStore{b: inner.VersionedBucket, bucket: "data"}, dir: src.DataDir} |
| 183 | x.f = f |
| 184 | f.EnvFile = flatClaimEnvFile(x.dir) |
| 185 | x.writeEnv("K=1\n") |
| 186 | o := servingObserved("g2", "g1:uk:@root:uk", "g2:uk:g1:uk") |
| 187 | if err := f.Establish(t.Context(), o, launchInputs("g2"), x); !errors.Is(err, hostbox.ErrChanged) { |
| 188 | t.Fatalf("establish under a refused identity: %v", err) |
| 189 | } |
| 190 | if _, found := x.manifest("g2"); found { |
| 191 | t.Fatal("a manifest was written under a refused identity") |
| 192 | } |
| 193 | if inner.lists != 1 { |
| 194 | t.Fatalf("%d bucket calls, want none from the refused establishment", inner.lists) |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | // The production source opens a movesFiles over the verified actor: the |
| 199 | // data bucket by exact version, the files store for that bucket in its |
| 200 | // cloud region, this box's engine region and the home alias, and the |
| 201 | // host's files directory and env file under the data directory. No pin |
| 202 | // reader is wired on this branch, so Evidence is nil. |
| 203 | func TestProductionFilesSourceOpensOverTheActor(t *testing.T) { |
| 204 | data := leasetest.NewVersionedBucket() |
| 205 | var gotBucket, gotRegion string |
| 206 | src := &movesFilesSource{DataDir: "/data", Routes: testClaimRoutes{}, newFiles: func(_ context.Context, bucket, region string) (release.Store, error) { |
| 207 | gotBucket, gotRegion = bucket, region |
| 208 | return release.NewDirStore(t.TempDir()), nil |
| 209 | }} |
| 210 | actor := hostbox.Actor{Box: "box-1", Region: "", HomeRegion: "ie", Bucket: "data-ie", Cloud: hostbox.CloudIdentity{Region: "eu-west-1"}} |
| 211 | got, err := src.Open(context.Background(), release.NewDirStore(t.TempDir()), data, actor) |
| 212 | if err != nil { |
| 213 | t.Fatal(err) |
| 214 | } |
| 215 | f, ok := got.(*movesFiles) |
| 216 | if !ok { |
| 217 | t.Fatalf("%T", got) |
| 218 | } |
| 219 | if gotBucket != "data-ie" || gotRegion != "eu-west-1" || f.Bucket != "data-ie" || f.Box != "box-1" || f.HomeRegion != "ie" || f.Region != "@home" || f.Data == nil || f.Files == nil || f.Evidence != nil { |
| 220 | t.Fatalf("provider: %+v (%s %s)", f, gotBucket, gotRegion) |
| 221 | } |
| 222 | if f.EnvFile == nil || f.DataDir != "/data" { |
| 223 | t.Fatalf("env file resolver set %v, data %s", f.EnvFile != nil, f.DataDir) |
| 224 | } |
| 225 | // An actor without a bucket, box or region cannot be opened, nor a |
| 226 | // source without the box's registry to find env files by. |
| 227 | if _, err := src.Open(context.Background(), release.NewDirStore(t.TempDir()), data, hostbox.Actor{Box: "box-1"}); !errors.Is(err, hostwrite.ErrUnavailable) { |
| 228 | t.Fatalf("bare actor: %v", err) |
| 229 | } |
| 230 | noRoutes := *src |
| 231 | noRoutes.Routes = nil |
| 232 | if _, err := noRoutes.Open(context.Background(), release.NewDirStore(t.TempDir()), data, actor); !errors.Is(err, hostwrite.ErrUnavailable) { |
| 233 | t.Fatalf("no registry: %v", err) |
| 234 | } |
| 235 | // Registration happens only with the claim on, and needs the registry. |
| 236 | reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) |
| 237 | if err != nil { |
| 238 | t.Fatal(err) |
| 239 | } |
| 240 | t.Cleanup(func() { reg.Close() }) |
| 241 | withClaimFilesSource(t, nil) |
| 242 | if err := registerProductionClaimFiles(func(string) string { return "" }, "/data", reg); err != nil || productionClaimFilesSource() != nil { |
| 243 | t.Fatalf("claim off registered: %v %v", err, productionClaimFilesSource()) |
| 244 | } |
| 245 | if err := registerProductionClaimFiles(func(k string) string { return "1" }, "/data", nil); !errors.Is(err, hostwrite.ErrUnavailable) || productionClaimFilesSource() != nil { |
| 246 | t.Fatalf("claim on without a registry: %v", err) |
| 247 | } |
| 248 | if err := registerProductionClaimFiles(func(k string) string { return "1" }, "/data", reg); err != nil || productionClaimFilesSource() == nil { |
| 249 | t.Fatalf("claim on: %v", err) |
| 250 | } |
| 251 | } |
| 252 | |