//go:build linux package host import ( "context" "errors" "github.com/carlosframework/platform/internal/hostwrite" "os" "path/filepath" "reflect" "strings" "testing" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/cgroup" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) const shapeData = "/data" func shapeAssembly() *ClaimRoleAssembly { return &ClaimRoleAssembly{Root: fakeLaunchRoot{protocol: hostbox.RoleLaunchProtocol}, ManagedRoots: map[string]string{"data": shapeData}} } func shapeInputs(host string) ClaimLaunchInputs { db := filepath.Join(provision.InstanceDir(shapeData, host), host+".db") return ClaimLaunchInputs{Executable: "/opt/carlos/versions/v1/app", ExecutableSHA256: strings.Repeat("a", 64), Identity: hostwrite.Identity{Config: "config", Release: "release"}, Roles: []string{"main"}, DBPath: db, Limit: cgroup.TierDefault, Args: []string{"--socket", provision.NestedSocket(host), "--db", db}, Env: []string{"CARLOS_ADMIN_TOKEN=secret", "PATH=/usr/bin"}} } func TestRev2ShapePreflightLaunchAdmitsValidMain(t *testing.T) { if err := shapeAssembly().PreflightLaunch("shop.example", shapeInputs("shop.example")); err != nil { t.Fatal(err) } } func TestRev2ShapePreflightLaunchRefusesMalformedLaunch(t *testing.T) { host := "shop.example" good := shapeInputs(host) db, sock := good.DBPath, provision.NestedSocket(host) other := filepath.Join(provision.InstanceDir(shapeData, host), "other.db") for name, mutate := range map[string]func(*ClaimLaunchInputs){ "seed role": func(in *ClaimLaunchInputs) { in.Roles = []string{"main", "seed"} }, "sidecar role": func(in *ClaimLaunchInputs) { in.Roles = []string{"main", "sidecar"} }, "no roles": func(in *ClaimLaunchInputs) { in.Roles = nil }, "no args": func(in *ClaimLaunchInputs) { in.Args = nil }, "extra flag": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "--verbose", "1") }, "extra positional": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "serve") }, "duplicate socket": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "--socket", sock) }, "duplicate db": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "--db", db) }, "missing socket": func(in *ClaimLaunchInputs) { in.Args = []string{"--db", db} }, "missing db": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", sock} }, "NUL in arg": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", sock + "\x00x", "--db", db} }, "db mismatch": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", sock, "--db", other} }, "db flat": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", sock, "--db", "/data/shop.example.db"} }, "socket flat": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", provision.SocketDir + "/shop.example.sock", "--db", db} }, "socket other unit": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", provision.NestedSocket("other.example"), "--db", db} }, "socket not app": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", filepath.Join(filepath.Dir(sock), "x.sock"), "--db", db} }, "no env": func(in *ClaimLaunchInputs) { in.Env = nil }, "no admin token": func(in *ClaimLaunchInputs) { in.Env = []string{"PATH=/usr/bin"} }, "empty admin token": func(in *ClaimLaunchInputs) { in.Env = []string{"CARLOS_ADMIN_TOKEN=", "PATH=/usr/bin"} }, "malformed env": func(in *ClaimLaunchInputs) { in.Env = append(in.Env, "NOEQUALS") }, "empty env key": func(in *ClaimLaunchInputs) { in.Env = append(in.Env, "=v") }, "duplicate env": func(in *ClaimLaunchInputs) { in.Env = append(in.Env, "PATH=/bin") }, "NUL in env": func(in *ClaimLaunchInputs) { in.Env = append(in.Env, "A=b\x00c") }, "bad tier": func(in *ClaimLaunchInputs) { in.Limit = cgroup.Limit{MemoryMax: 7} }, } { t.Run(name, func(t *testing.T) { in := good in.Args, in.Env = append([]string(nil), good.Args...), append([]string(nil), good.Env...) mutate(&in) err := shapeAssembly().PreflightLaunch(host, in) if !errors.Is(err, hostbox.ErrRoleLaunchShape) && !errors.Is(err, hostbox.ErrUnsupported) { t.Fatalf("accepted: %v", err) } }) } } // Select reports the roles the real start would spawn, not a hardcoded main. func TestRev2ShapeServingSelectReportsLaunchRoles(t *testing.T) { for name, tc := range map[string]struct { roles []string want []string }{ "main": {[]string{"main"}, []string{"main"}}, "seed": {[]string{"main", "seed"}, []string{"main", "seed"}}, "sidecar": {[]string{"main", "sidecar"}, []string{"main", "sidecar"}}, } { t.Run(name, func(t *testing.T) { f := plan9D5Fixture(t) f.launch.Roles = tc.roles in, err := f.owners.Select(t.Context(), plan9D5Route, plan9D5Observed(hostclaim.Holder{Stamp: 3}), activator.StartPolicy{MaxStarts: 5}) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(in.Roles, tc.want) { t.Fatalf("roles=%v want %v", in.Roles, tc.want) } }) } } // The production launch source refuses a route that would spawn a seed or a // co-spawned sidecar before the admin token is minted. func TestRev2ShapeLaunchSourceRefusesSeedAndSidecarBeforeTokenMint(t *testing.T) { t.Parallel() for name, setup := range map[string]func(*adminTokenFixture){ "seed": func(f *adminTokenFixture) { f.rt.Seed = `["init"]` }, "sidecar": func(f *adminTokenFixture) { if err := os.WriteFile(filepath.Join(f.data, adminTokenHost+"-sidecar.env"), []byte("A=1\n"), 0o600); err != nil { t.Fatal(err) } }, } { t.Run(name, func(t *testing.T) { f := newAdminTokenFixture(t, "box-1") setup(f) _, err := claimServingLaunchSource(f.backend, f.tokens)(t.Context(), f.rt) if !errors.Is(err, hostbox.ErrUnsupported) { t.Fatalf("err=%v", err) } if _, err := os.Stat(f.envPath()); !errors.Is(err, os.ErrNotExist) { t.Fatalf("token minted for refused shape: %v", err) } }) } } func TestRev2ShapeLaunchSourceReportsMainOnly(t *testing.T) { f := newAdminTokenFixture(t, "box-1") l, err := claimServingLaunchSource(f.backend, f.tokens)(t.Context(), f.rt) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(l.Roles, []string{"main"}) { t.Fatalf("roles=%v", l.Roles) } } type shapeMutatedInputs struct { recoveryInputs mutate func(*ClaimLaunchInputs) } func (s shapeMutatedInputs) Select(ctx context.Context, rt registry.Route, o hostclaim.Observed, p activator.StartPolicy) (ClaimLaunchInputs, error) { in, err := s.recoveryInputs.Select(ctx, rt, o, p) s.mutate(&in) return in, err } // A malformed launch is refused in Prerequisites before any retry reservation, // guard record, stop request, lease or journal entry. func TestRev2ShapeMalformedLaunchRefusedBeforeStateChanges(t *testing.T) { for name, mutate := range map[string]func(*ClaimLaunchInputs){ "extra arg": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "--x", "y") }, "duplicate flag": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "--db", in.DBPath) }, "db mismatch": func(in *ClaimLaunchInputs) { in.Args[3] = in.DBPath + "-other" }, "bad socket": func(in *ClaimLaunchInputs) { in.Args[1] = "/tmp/app.sock" }, "no env": func(in *ClaimLaunchInputs) { in.Env = nil }, "seed role": func(in *ClaimLaunchInputs) { in.Roles = []string{"main", "seed"} }, } { t.Run(name, func(t *testing.T) { f := newRecoveryFixture(t) c := f.controller("boot-1", "controller-1", 4101, "41") c.wake.Roles = &ClaimRoleAssembly{Root: fakeLaunchRoot{protocol: hostbox.RoleLaunchProtocol}, ManagedRoots: map[string]string{"data": shapeData}, Manager: c.wake.Manager, Journal: f.journal} route := registry.Route{Host: recoveryHost, Kind: "instance", Hibernate: true, DBPath: provision.NestedDBPath(f.guard.DataDir, recoveryHost), Socket: provision.NestedSocket(recoveryHost)} c.wake.Inputs = shapeMutatedInputs{recoveryInputs{f}, func(in *ClaimLaunchInputs) { base := shapeInputs(recoveryHost) base.DBPath = provision.NestedDBPath(shapeData, recoveryHost) base.Args = []string{"--socket", provision.NestedSocket(recoveryHost), "--db", base.DBPath} in.DBPath, in.Args, in.Env, in.Limit = base.DBPath, base.Args, base.Env, cgroup.TierDefault mutate(in) }} before := f.read() guardBefore, err := os.ReadDir(filepath.Join(f.guard.DataDir, "hostguard")) if err != nil { t.Fatal(err) } err = c.runtime.Prerequisites(t.Context(), route, before, recoveryPolicy) if !errors.Is(err, hostbox.ErrRoleLaunchShape) && !errors.Is(err, hostbox.ErrUnsupported) { t.Fatalf("accepted: %v", err) } if after := f.read(); after.ETag != before.ETag || after.Claim.Holder.WakeRetry != nil { t.Fatalf("refusal changed claim or spent retry: %+v", after) } guardAfter, err := os.ReadDir(filepath.Join(f.guard.DataDir, "hostguard")) if err != nil || !reflect.DeepEqual(guardBefore, guardAfter) || len(c.wake.pending) != 0 || len(f.requests) != 0 { t.Fatalf("refusal reached guard/stop/lease: err=%v pending=%d requests=%d", err, len(c.wake.pending), len(f.requests)) } if _, err := f.journal.Read(t.Context(), recoveryTuple(before, c.local)); err == nil { t.Fatal("refusal journaled a launch") } }) } }