package host import ( "context" "errors" "os" "path/filepath" "slices" "strings" "testing" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/instancekey" "github.com/carlosframework/platform/internal/lease/leasetest" "github.com/carlosframework/platform/internal/registry" ) // keyCapturingFiles is a C8 provider that records the instance's own env // file as it stands when Establish runs: the moment the provider must // publish it (ruling 6). type keyCapturingFiles struct { path string seen []byte } func (k *keyCapturingFiles) Establish(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error { raw, err := os.ReadFile(k.path) k.seen = raw return err } type keyTestMarkers struct{} func (keyTestMarkers) PutMarker(context.Context, string, string, []byte, string, func(context.Context) error) (string, error) { return "v1", nil } type passWork struct{} func (passWork) With(_ context.Context, _ hostwrite.Identity, fn func() error) error { return fn() } // The claim's first wake mints the key in Select (the launch source), so // the env file the C8 provider publishes in Materialize already carries it, // and the launch env the app will run with carries the same value. func TestClaimFirstWakeKeyIsOnDiskBeforeTheFilesManifest(t *testing.T) { f := newAdminTokenFixture(t, "box-1") files := &keyCapturingFiles{path: f.envPath()} owners := &claimServingOwners{Data: leasetest.NewVersionedBucket(), Bucket: "data", Region: "ie", Box: "box-1", NewID: func() string { return "g1" }, Now: func() time.Time { return time.Date(2026, 10, 10, 9, 0, 0, 0, time.UTC) }, Sources: ClaimServingSources{Launch: claimServingLaunchSource(f.backend, f.tokens), Markers: keyTestMarkers{}, Files: files}} o, err := hostclaim.Read(t.Context(), f.claims, adminTokenHost) if err != nil { t.Fatal(err) } in, err := owners.Select(t.Context(), f.rt, o, activator.StartPolicy{}) if err != nil { t.Fatalf("select: %v", err) } if err := owners.Materialize(t.Context(), o, in, passWork{}); err != nil { t.Fatalf("materialize: %v", err) } var key string for _, line := range strings.Split(string(files.seen), "\n") { if v, ok := strings.CutPrefix(line, activator.InstanceKeyVar+"="); ok { key = v } } if len(key) != 44 { t.Fatalf("the env file at Establish carries no minted key: %d bytes", len(files.seen)) } found := false for _, kv := range in.Env { found = found || kv == activator.InstanceKeyVar+"="+key } if !found { t.Fatal("the launch env does not carry the key the provider published") } } // Final review I2 on the claim path: a first wake whose configuration is // still on its way is deferred by the launch source and nothing is minted; // once the config sync has delivered the configured key, the launch // delivers that key and the own file still holds none. func TestClaimFirstWakeWaitsForTheConfiguredKey(t *testing.T) { f := newAdminTokenFixture(t, "box-1") f.backend.ConfiguredKey = func(context.Context, registry.Route) (string, error) { return instancekey.Digest("key-A"), nil } launch := claimServingLaunchSource(f.backend, f.tokens) if _, err := launch(t.Context(), f.rt); !errors.Is(err, activator.ErrInstanceKeyDeferred) || !strings.Contains(err.Error(), adminTokenHost) { t.Fatalf("launch before delivery = %v, want ErrInstanceKeyDeferred naming the host", err) } raw, _ := os.ReadFile(f.envPath()) if strings.Contains(string(raw), activator.InstanceKeyVar) { t.Fatal("a key was minted while the configured one was on its way") } if err := os.WriteFile(filepath.Join(f.data, adminTokenHost+".config.env"), []byte(activator.InstanceKeyVar+"=key-A\n"), 0o600); err != nil { t.Fatal(err) } got, err := launch(t.Context(), f.rt) if err != nil { t.Fatalf("launch after delivery: %v", err) } if !slices.Contains(got.Env, activator.InstanceKeyVar+"=key-A") { t.Fatal("the launch does not deliver the configured key") } if raw, _ := os.ReadFile(f.envPath()); strings.Contains(string(raw), activator.InstanceKeyVar) { t.Fatal("a key was minted beside the configured one") } } // An instance from before this build has a token and no key. Its next // claim launch mints the key under the same guarded write, leaves the // token alone, and a retry keeps both. func TestClaimLaunchMintsAKeyBesideAnExistingToken(t *testing.T) { f := newAdminTokenFixture(t, "box-1") tok := strings.Repeat("c", 64) if err := os.WriteFile(f.envPath(), []byte("CARLOS_ADMIN_TOKEN="+tok+"\n"), 0o600); err != nil { t.Fatal(err) } launch := claimServingLaunchSource(f.backend, f.tokens) first, err := launch(t.Context(), f.rt) if err != nil { t.Fatal(err) } raw, _ := os.ReadFile(f.envPath()) if f.token() != tok || !strings.HasPrefix(string(raw), "CARLOS_ADMIN_TOKEN="+tok+"\n"+activator.InstanceKeyVar+"=") { t.Fatalf("env file after the launch: %d bytes, token kept %v", len(raw), f.token() == tok) } again, err := launch(t.Context(), f.rt) if err != nil { t.Fatal(err) } raw2, _ := os.ReadFile(f.envPath()) if string(raw2) != string(raw) || again.Identity != first.Identity { t.Fatal("a retried launch changed the env file or the launch identity") } } // Ruling 21: the claim snapshot names the key its env delivers, by digest, // and the file that supplied it, for O6 to check a process's report // against. The identity is the env's alone, so it does not change. func TestClaimSnapshotNamesTheDeliveredKeyAndItsSource(t *testing.T) { f := newAdminTokenFixture(t, "box-1") first, err := claimServingLaunchSource(f.backend, f.tokens)(t.Context(), f.rt) if err != nil { t.Fatal(err) } snap, err := f.backend.SnapshotClaimLaunch(t.Context(), f.rt) if err != nil { t.Fatal(err) } own, _ := os.ReadFile(f.envPath()) var key string for _, line := range strings.Split(string(own), "\n") { if v, ok := strings.CutPrefix(line, activator.InstanceKeyVar+"="); ok { key = v } } if snap.InstanceKeySource != instancekey.SourcePlatform || snap.InstanceKeySHA256 != instancekey.Digest(key) || snap.Identity != first.Identity { t.Fatalf("platform key: source %q, digest match %v, identity kept %v", snap.InstanceKeySource, snap.InstanceKeySHA256 == instancekey.Digest(key), snap.Identity == first.Identity) } tok := "CARLOS_ADMIN_TOKEN=" + f.token() + "\n" if err := os.WriteFile(f.envPath(), []byte(tok+activator.InstanceKeyVar+"=\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(f.data, adminTokenHost+".config.env"), []byte(activator.InstanceKeyVar+"=from-config\n"), 0o600); err != nil { t.Fatal(err) } snap, err = f.backend.SnapshotClaimLaunch(t.Context(), f.rt) if err != nil || snap.InstanceKeySource != instancekey.SourceConfig || snap.InstanceKeySHA256 != instancekey.Digest("from-config") { t.Fatalf("configured key: source %q, %v", snap.InstanceKeySource, err) } // The named key is the delivered key: Env carries the configured value, // not the own file's empty override (mergeEnv applies Effective). if !slices.Contains(snap.Env, activator.InstanceKeyVar+"=from-config") { t.Fatal("the snapshot names the configured key but its env does not deliver it") } }