package activator import ( "context" "encoding/json" "errors" "os" "path/filepath" "slices" "strings" "testing" "github.com/carlosframework/platform/internal/instancectl" "github.com/carlosframework/platform/internal/instancekey" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // envlessProc is a generation handle that kept no launch environment. type envlessProc struct{ Proc } // A config swap is queued: config sync has delivered a new revision, and // the serving generation still runs with the one it launched with. A run // gets the serving generation's environment, not the delivered files // (merge ruling M3). func TestARunGetsTheServingGenerationsEnvironment(t *testing.T) { ctl := &fakeControl{inv: "inv1", cmdInv: "inv2"} unit := &fakeUnit{} b, rt := nestedBackend(t, ctl, unit) b.layoutFn = func(registry.Route) (Layout, string) { return LayoutNested, "" } paths := b.pathsFor(rt) if err := os.MkdirAll(filepath.Dir(paths.configEnv), 0o700); err != nil { t.Fatal(err) } deliver := func(rev string) { t.Helper() if err := os.WriteFile(paths.configEnv, []byte("APP_KEY="+rev+"\n"), 0o600); err != nil { t.Fatal(err) } } deliver("old") var stopServing func() ctl.onStart = func() { stopServing = serveOK(t, rt.Socket) } defer func() { if stopServing != nil { stopServing() } }() unit.set("active", "inv1", true, true) p, _, err := b.Start(context.Background(), rt) if err != nil { t.Fatalf("Start: %v", err) } lp, ok := p.(launchEnvProc) if !ok || lp.LaunchEnv()["APP_KEY"] != "old" { t.Fatalf("the generation's handle kept %v, want its launch environment", p) } deliver("new") // delivered, not activated: the config swap is queued dir := filepath.Join(provision.InstanceDir(b.DataDir, rt.Host), CmdRunDirName, "run_a") spec := CmdSpec{RunID: "run_a", RunDir: dir, Stdout: &syncBuffer{}, Stderr: &syncBuffer{}, genEnv: lp.LaunchEnv()} u, err := b.StartCmd(context.Background(), rt, spec) if err != nil { t.Fatalf("StartCmd: %v", err) } u.(*cmdUnit).out.Close() u.(*cmdUnit).errs.Close() raw, err := os.ReadFile(filepath.Join(provision.InstanceDir(b.DataDir, rt.Host), "launch.cmd.json")) if err != nil { t.Fatal(err) } var l instancectl.Launch if err := json.Unmarshal(raw, &l); err != nil { t.Fatal(err) } if !slices.Contains(l.Env, "APP_KEY=old") || slices.Contains(l.Env, "APP_KEY=new") { t.Fatalf("the run's env %q, want the serving generation's APP_KEY=old", l.Env) } token := lp.LaunchEnv()["CARLOS_ADMIN_TOKEN"] if token == "" || !slices.Contains(l.Env, "CARLOS_ADMIN_TOKEN="+token) { t.Fatal("the run did not get the serving generation's admin token") } // The instance key the launch minted is in the generation's snapshot, // and the command gets exactly it: the command opens the instance's // sealed data and must never be left to mint a key of its own. key := lp.LaunchEnv()[instancekey.Var] if key == "" || !slices.Contains(l.Env, instancekey.Var+"="+key) { t.Fatalf("the run's env lacks the serving generation's %s", instancekey.Var) } if kl, ok := p.(launchKeyLayerProc); !ok || kl.LaunchKeyLayer() != instancekey.LayerOwn { t.Fatalf("the generation's handle did not keep its key's layer") } for _, kv := range l.Env { if strings.HasPrefix(kv, "CARLOS_CONTROL_SOCKET=") || strings.HasPrefix(kv, "CARLOS_PURGE=") { t.Errorf("a run's env carries %s", kv) } } // A run with no generation environment is refused before anything is // laid out or started: the delivered files are never its fallback. before := ctl.joined() if _, err := b.StartCmd(context.Background(), rt, CmdSpec{RunID: "run_b", RunDir: filepath.Join(filepath.Dir(dir), "run_b")}); !errors.Is(err, errNoGenEnv) { t.Fatalf("StartCmd without a generation environment = %v, want errNoGenEnv", err) } if ctl.joined() != before { t.Fatalf("the refused run reached the control service:\n%s", ctl.joined()) } } // RunCmd hands StartCmd the environment the current generation's handle // kept, and nothing for a handle that kept none, which StartCmd refuses // (merge ruling M3). func TestRunCmdHandsOverTheGenerationsLaunchEnvironment(t *testing.T) { a, cb, _ := testCmdActivator(t, Config{}) rt := testRoute("c.example.com") var got []map[string]string cb.startCmd = func(_ context.Context, _ registry.Route, spec CmdSpec) (CmdUnit, error) { got = append(got, spec.genEnv) if spec.genEnv == nil { return nil, errNoGenEnv } return cb.unit, nil } arrangeAwake(t, a, rt) a.mu.Lock() e := a.entries[rt.Host] e.proc.(*fakeProc).launchEnv = map[string]string{"APP_KEY": "serving"} a.mu.Unlock() h, err := a.RunCmd(context.Background(), rt, CmdSpec{RunID: "run_a"}) if err != nil { t.Fatal(err) } if err := h.Stop(context.Background()); err != nil { t.Fatal(err) } h.Release() a.mu.Lock() e.proc = envlessProc{e.proc} a.mu.Unlock() if _, err := a.RunCmd(context.Background(), rt, CmdSpec{RunID: "run_b"}); !errors.Is(err, errNoGenEnv) { t.Fatalf("a run on a handle with no launch environment = %v, want errNoGenEnv", err) } if len(got) != 2 || got[0]["APP_KEY"] != "serving" || got[1] != nil { t.Fatalf("StartCmd got %v, want the handle's environment, then none", got) } } // A run's command on a replica row passes the replica gate like every // other launch (instance-key rulings 26 to 29), on the environment it is // given: no key and no control socket is refused, a key that is not the // home's is refused, and the home's own key starts. A refusal reaches // neither the control service nor the instance's directory. func TestStartCmdPassesTheReplicaKeyGate(t *testing.T) { const key = "aG9tZS1rZXktaG9tZS1rZXktaG9tZS1rZXktaG9tZSE=" for _, c := range []struct { name string env map[string]string layer instancekey.Layer listed string ok bool }{ {"no key", map[string]string{}, instancekey.LayerNone, instancekey.Digest(key), false}, {"not the home's key", map[string]string{instancekey.Var: key}, instancekey.LayerOwn, instancekey.Digest("another"), false}, {"the home's key", map[string]string{instancekey.Var: key}, instancekey.LayerOwn, instancekey.Digest(key), true}, } { t.Run(c.name, func(t *testing.T) { ctl := &fakeControl{cmdInv: "inv1"} b, rt := nestedBackend(t, ctl, &fakeUnit{}) b.layoutFn = func(registry.Route) (Layout, string) { return LayoutNested, "" } rt.Replica = true b.ReplicaHomeKeys = func(string) (string, bool) { return c.listed, true } dir := filepath.Join(provision.InstanceDir(b.DataDir, rt.Host), CmdRunDirName, "run_a") spec := CmdSpec{RunID: "run_a", RunDir: dir, Stdout: &syncBuffer{}, Stderr: &syncBuffer{}, genEnv: c.env, genKeyLayer: c.layer} u, err := b.StartCmd(context.Background(), rt, spec) if c.ok { if err != nil { t.Fatalf("StartCmd: %v", err) } u.(*cmdUnit).out.Close() u.(*cmdUnit).errs.Close() return } if !errors.Is(err, ErrReplicaKey) || u != nil { t.Fatalf("StartCmd = %v, %v; want ErrReplicaKey and no unit", u, err) } if strings.Contains(ctl.joined(), "cmd ") { t.Fatalf("a refused replica command reached the control service:\n%s", ctl.joined()) } if _, err := os.Stat(filepath.Join(provision.InstanceDir(b.DataDir, rt.Host), "launch.cmd.json")); !os.IsNotExist(err) { t.Fatalf("a refused replica command left a launch file (%v)", err) } }) } }