package host import ( "context" "errors" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/instancekey" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // Ruling 26 on the claim path, which has no control socket before O6: a // keyless replica is never selected (its helper would mint), a key that is // not the home's is refused, and the home's key is selected and named. func TestClaimReplicaLaunchIsGated(t *testing.T) { t.Parallel() homeKey := strings.Repeat("H", 43) + "=" for _, tc := range []struct { name, config string refused bool }{ {"keyless", "", true}, {"another key", activator.InstanceKeyVar + "=configured-B\n", true}, {"the home's key", activator.InstanceKeyVar + "=" + homeKey + "\n", false}, } { t.Run(tc.name, func(t *testing.T) { f := newAdminTokenFixture(t, "box-1") f.rt.Replica = true f.backend.ReplicaHomeKeys = func(string) (string, bool) { return instancekey.Digest(homeKey), true } f.backend.ConfiguredKey = func(context.Context, registry.Route) (string, error) { return instancekey.Digest(homeKey), nil } own := "CARLOS_ADMIN_TOKEN=" + strings.Repeat("c", 64) + "\n" if err := os.WriteFile(f.envPath(), []byte(own), 0o600); err != nil { t.Fatal(err) } if tc.config != "" { if err := os.WriteFile(filepath.Join(f.data, adminTokenHost+".config.env"), []byte(tc.config), 0o600); err != nil { t.Fatal(err) } } launch, err := claimServingLaunchSource(f.backend, f.tokens)(t.Context(), f.rt) if tc.refused { if !errors.Is(err, activator.ErrReplicaKey) { t.Fatalf("launch source = %v, want ErrReplicaKey", err) } } else { found := false for _, kv := range launch.Env { found = found || kv == activator.InstanceKeyVar+"="+homeKey } if err != nil || !found { t.Fatalf("launch source = %v, key delivered %v", err, found) } } if raw, _ := os.ReadFile(f.envPath()); string(raw) != own { t.Fatal("a replica's claim launch changed its own env file: a replica never mints") } }) } } // Final review I4: the claim's input policy asks whether a Work's identity // still names this host's inputs, and launches nothing. It snapshots // without the replica gate, so a configured-key replica costs it no bucket // read, and its answer keeps the policy's contract (nil or ErrChanged), // never a launch refusal. The launch source on the same backend runs the // gate, and its fresh read, as before. func TestClaimInputPolicyNeverReadsTheBucket(t *testing.T) { homeKey := strings.Repeat("H", 43) + "=" f := newAdminTokenFixture(t, "box-1") f.rt.Replica = true f.backend.ReplicaHomeKeys = func(string) (string, bool) { return instancekey.Digest(homeKey), true } reads := 0 f.backend.ConfiguredKey = func(context.Context, registry.Route) (string, error) { reads++ return "", errors.New("bucket unreachable") // a gate that ran here would refuse } if err := os.WriteFile(f.envPath(), []byte("CARLOS_ADMIN_TOKEN="+strings.Repeat("c", 64)+"\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(f.data, adminTokenHost+".config.env"), []byte(activator.InstanceKeyVar+"="+homeKey+"\n"), 0o600); err != nil { t.Fatal(err) } snap, err := f.backend.SnapshotClaimLaunch(t.Context(), f.rt) if err != nil { t.Fatalf("snapshot: %v", err) } o, err := hostclaim.Read(t.Context(), f.claims, adminTokenHost) if err != nil || o.Claim == nil || o.Claim.Holder == nil { t.Fatalf("claim: %+v, %v", o, err) } h := o.Claim.Holder sel := hostbox.Selection{Host: adminTokenHost, Captured: hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}} policy := claimInputPolicy(func(string) (*registry.Route, error) { return &f.rt, nil }, f.claims, nil, f.backend) if err := policy(t.Context(), o.Claim, sel, snap.Identity); err != nil { t.Fatalf("the policy refused the snapshot's own identity: %v", err) } changed := snap.Identity changed.Config = "0000" if err := policy(t.Context(), o.Claim, sel, changed); !errors.Is(err, hostbox.ErrChanged) { t.Fatalf("another identity = %v, want ErrChanged", err) } if reads != 0 { t.Fatalf("the policy read the bucket %d times", reads) } if _, err := claimServingLaunchSource(f.backend, f.tokens)(t.Context(), f.rt); !errors.Is(err, activator.ErrReplicaKey) || reads != 1 { t.Fatalf("the launch source = %v after %d bucket reads, want ErrReplicaKey after one", err, reads) } } // Final review I1: the claim path's backend is built by claimExecBackend // with the same readers the agent's backend gets, so a keyed replica // whose configured key is listed and still the bucket's starts on the // claim path (ruling 26), and a first wake asks the bucket before it mints // (ruling 5). The fixture's backend is not used: the readers are the ones // the production constructor wires, built from a box of this test's. func TestClaimBackendIsWiredWithTheKeyReaders(t *testing.T) { homeKey := strings.Repeat("H", 43) + "=" f := newAdminTokenFixture(t, "box-1") f.rt.Replica, f.rt.Account, f.rt.App = true, "bab", "jam" bucket := release.NewDirStore(t.TempDir()) priv, _, err := appconfig.GenerateKey() if err != nil { t.Fatal(err) } if err := appconfig.SetEnv(t.Context(), bucket, "bab", "jam", map[string]string{instancekey.Var: homeKey}); err != nil { t.Fatal(err) } list := []byte(adminTokenHost + "=" + instancekey.Digest(homeKey) + "\n") keys := newInstanceKeyReaders( func(k string) string { if k == "CARLOS_SECRETS_KEY" { return priv } return "" }, func(context.Context) (release.Store, error) { return bucket, nil }, func() ([]byte, error) { return list, nil }) backend := claimExecBackend(ClaimRuntimeResources{DataDir: f.backend.DataDir, OptDir: f.backend.OptDir}, keys) if backend.ReplicaHomeKeys == nil || backend.ConfiguredKey == nil { t.Fatal("claimExecBackend left a reader nil") } if err := os.WriteFile(f.envPath(), []byte("CARLOS_ADMIN_TOKEN="+strings.Repeat("c", 64)+"\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(f.data, adminTokenHost+".config.env"), []byte(activator.InstanceKeyVar+"="+homeKey+"\n"), 0o600); err != nil { t.Fatal(err) } launch := claimServingLaunchSource(backend, f.tokens) got, err := launch(t.Context(), f.rt) if err != nil { t.Fatalf("a listed replica on the claim path: %v", err) } found := false for _, kv := range got.Env { found = found || kv == activator.InstanceKeyVar+"="+homeKey } if !found { t.Fatal("the replica's launch does not deliver its home's key") } // The list no longer names it: refused, by the reader the constructor wired. list = nil if _, err := launch(t.Context(), f.rt); !errors.Is(err, activator.ErrReplicaKey) { t.Fatalf("an unlisted replica = %v, want ErrReplicaKey", err) } // A first wake of a home row whose configuration sets a key this box // has not delivered is deferred by the same backend (ruling 5). f.rt.Replica = false if err := os.Remove(filepath.Join(f.data, adminTokenHost+".config.env")); err != nil { t.Fatal(err) } if _, err := launch(t.Context(), f.rt); !errors.Is(err, activator.ErrInstanceKeyDeferred) { t.Fatalf("a first wake before delivery = %v, want ErrInstanceKeyDeferred", err) } prod := productionInstanceKeyReaders() if prod.HomeKeys == nil || prod.Configured == nil { t.Fatal("the production readers are not both built") } }