carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "errors"
8 "github.com/carlosframework/platform/internal/hostwrite"
9 "os"
10 "path/filepath"
11 "reflect"
12 "strings"
13 "testing"
14
15 "github.com/carlosframework/platform/internal/activator"
16 "github.com/carlosframework/platform/internal/cgroup"
17 "github.com/carlosframework/platform/internal/hostbox"
18 "github.com/carlosframework/platform/internal/hostclaim"
19 "github.com/carlosframework/platform/internal/provision"
20 "github.com/carlosframework/platform/internal/registry"
21)
22
23const shapeData = "/data"
24
25func shapeAssembly() *ClaimRoleAssembly {
26 return &ClaimRoleAssembly{Root: fakeLaunchRoot{protocol: hostbox.RoleLaunchProtocol}, ManagedRoots: map[string]string{"data": shapeData}}
27}
28
29func shapeInputs(host string) ClaimLaunchInputs {
30 db := filepath.Join(provision.InstanceDir(shapeData, host), host+".db")
31 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,
32 Args: []string{"--socket", provision.NestedSocket(host), "--db", db},
33 Env: []string{"CARLOS_ADMIN_TOKEN=secret", "PATH=/usr/bin"}}
34}
35
36func TestRev2ShapePreflightLaunchAdmitsValidMain(t *testing.T) {
37 if err := shapeAssembly().PreflightLaunch("shop.example", shapeInputs("shop.example")); err != nil {
38 t.Fatal(err)
39 }
40}
41
42func TestRev2ShapePreflightLaunchRefusesMalformedLaunch(t *testing.T) {
43 host := "shop.example"
44 good := shapeInputs(host)
45 db, sock := good.DBPath, provision.NestedSocket(host)
46 other := filepath.Join(provision.InstanceDir(shapeData, host), "other.db")
47 for name, mutate := range map[string]func(*ClaimLaunchInputs){
48 "seed role": func(in *ClaimLaunchInputs) { in.Roles = []string{"main", "seed"} },
49 "sidecar role": func(in *ClaimLaunchInputs) { in.Roles = []string{"main", "sidecar"} },
50 "no roles": func(in *ClaimLaunchInputs) { in.Roles = nil },
51 "no args": func(in *ClaimLaunchInputs) { in.Args = nil },
52 "extra flag": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "--verbose", "1") },
53 "extra positional": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "serve") },
54 "duplicate socket": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "--socket", sock) },
55 "duplicate db": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "--db", db) },
56 "missing socket": func(in *ClaimLaunchInputs) { in.Args = []string{"--db", db} },
57 "missing db": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", sock} },
58 "NUL in arg": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", sock + "\x00x", "--db", db} },
59 "db mismatch": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", sock, "--db", other} },
60 "db flat": func(in *ClaimLaunchInputs) { in.Args = []string{"--socket", sock, "--db", "/data/shop.example.db"} },
61 "socket flat": func(in *ClaimLaunchInputs) {
62 in.Args = []string{"--socket", provision.SocketDir + "/shop.example.sock", "--db", db}
63 },
64 "socket other unit": func(in *ClaimLaunchInputs) {
65 in.Args = []string{"--socket", provision.NestedSocket("other.example"), "--db", db}
66 },
67 "socket not app": func(in *ClaimLaunchInputs) {
68 in.Args = []string{"--socket", filepath.Join(filepath.Dir(sock), "x.sock"), "--db", db}
69 },
70 "no env": func(in *ClaimLaunchInputs) { in.Env = nil },
71 "no admin token": func(in *ClaimLaunchInputs) { in.Env = []string{"PATH=/usr/bin"} },
72 "empty admin token": func(in *ClaimLaunchInputs) { in.Env = []string{"CARLOS_ADMIN_TOKEN=", "PATH=/usr/bin"} },
73 "malformed env": func(in *ClaimLaunchInputs) { in.Env = append(in.Env, "NOEQUALS") },
74 "empty env key": func(in *ClaimLaunchInputs) { in.Env = append(in.Env, "=v") },
75 "duplicate env": func(in *ClaimLaunchInputs) { in.Env = append(in.Env, "PATH=/bin") },
76 "NUL in env": func(in *ClaimLaunchInputs) { in.Env = append(in.Env, "A=b\x00c") },
77 "bad tier": func(in *ClaimLaunchInputs) { in.Limit = cgroup.Limit{MemoryMax: 7} },
78 } {
79 t.Run(name, func(t *testing.T) {
80 in := good
81 in.Args, in.Env = append([]string(nil), good.Args...), append([]string(nil), good.Env...)
82 mutate(&in)
83 err := shapeAssembly().PreflightLaunch(host, in)
84 if !errors.Is(err, hostbox.ErrRoleLaunchShape) && !errors.Is(err, hostbox.ErrUnsupported) {
85 t.Fatalf("accepted: %v", err)
86 }
87 })
88 }
89}
90
91// Select reports the roles the real start would spawn, not a hardcoded main.
92func TestRev2ShapeServingSelectReportsLaunchRoles(t *testing.T) {
93 for name, tc := range map[string]struct {
94 roles []string
95 want []string
96 }{
97 "main": {[]string{"main"}, []string{"main"}},
98 "seed": {[]string{"main", "seed"}, []string{"main", "seed"}},
99 "sidecar": {[]string{"main", "sidecar"}, []string{"main", "sidecar"}},
100 } {
101 t.Run(name, func(t *testing.T) {
102 f := plan9D5Fixture(t)
103 f.launch.Roles = tc.roles
104 in, err := f.owners.Select(t.Context(), plan9D5Route, plan9D5Observed(hostclaim.Holder{Stamp: 3}), activator.StartPolicy{MaxStarts: 5})
105 if err != nil {
106 t.Fatal(err)
107 }
108 if !reflect.DeepEqual(in.Roles, tc.want) {
109 t.Fatalf("roles=%v want %v", in.Roles, tc.want)
110 }
111 })
112 }
113}
114
115// The production launch source refuses a route that would spawn a seed or a
116// co-spawned sidecar before the admin token is minted.
117func TestRev2ShapeLaunchSourceRefusesSeedAndSidecarBeforeTokenMint(t *testing.T) {
118 t.Parallel()
119 for name, setup := range map[string]func(*adminTokenFixture){
120 "seed": func(f *adminTokenFixture) { f.rt.Seed = `["init"]` },
121 "sidecar": func(f *adminTokenFixture) {
122 if err := os.WriteFile(filepath.Join(f.data, adminTokenHost+"-sidecar.env"), []byte("A=1\n"), 0o600); err != nil {
123 t.Fatal(err)
124 }
125 },
126 } {
127 t.Run(name, func(t *testing.T) {
128 f := newAdminTokenFixture(t, "box-1")
129 setup(f)
130 _, err := claimServingLaunchSource(f.backend, f.tokens)(t.Context(), f.rt)
131 if !errors.Is(err, hostbox.ErrUnsupported) {
132 t.Fatalf("err=%v", err)
133 }
134 if _, err := os.Stat(f.envPath()); !errors.Is(err, os.ErrNotExist) {
135 t.Fatalf("token minted for refused shape: %v", err)
136 }
137 })
138 }
139}
140
141func TestRev2ShapeLaunchSourceReportsMainOnly(t *testing.T) {
142 f := newAdminTokenFixture(t, "box-1")
143 l, err := claimServingLaunchSource(f.backend, f.tokens)(t.Context(), f.rt)
144 if err != nil {
145 t.Fatal(err)
146 }
147 if !reflect.DeepEqual(l.Roles, []string{"main"}) {
148 t.Fatalf("roles=%v", l.Roles)
149 }
150}
151
152type shapeMutatedInputs struct {
153 recoveryInputs
154 mutate func(*ClaimLaunchInputs)
155}
156
157func (s shapeMutatedInputs) Select(ctx context.Context, rt registry.Route, o hostclaim.Observed, p activator.StartPolicy) (ClaimLaunchInputs, error) {
158 in, err := s.recoveryInputs.Select(ctx, rt, o, p)
159 s.mutate(&in)
160 return in, err
161}
162
163// A malformed launch is refused in Prerequisites before any retry reservation,
164// guard record, stop request, lease or journal entry.
165func TestRev2ShapeMalformedLaunchRefusedBeforeStateChanges(t *testing.T) {
166 for name, mutate := range map[string]func(*ClaimLaunchInputs){
167 "extra arg": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "--x", "y") },
168 "duplicate flag": func(in *ClaimLaunchInputs) { in.Args = append(in.Args, "--db", in.DBPath) },
169 "db mismatch": func(in *ClaimLaunchInputs) { in.Args[3] = in.DBPath + "-other" },
170 "bad socket": func(in *ClaimLaunchInputs) { in.Args[1] = "/tmp/app.sock" },
171 "no env": func(in *ClaimLaunchInputs) { in.Env = nil },
172 "seed role": func(in *ClaimLaunchInputs) { in.Roles = []string{"main", "seed"} },
173 } {
174 t.Run(name, func(t *testing.T) {
175 f := newRecoveryFixture(t)
176 c := f.controller("boot-1", "controller-1", 4101, "41")
177 c.wake.Roles = &ClaimRoleAssembly{Root: fakeLaunchRoot{protocol: hostbox.RoleLaunchProtocol}, ManagedRoots: map[string]string{"data": shapeData}, Manager: c.wake.Manager, Journal: f.journal}
178 route := registry.Route{Host: recoveryHost, Kind: "instance", Hibernate: true,
179 DBPath: provision.NestedDBPath(f.guard.DataDir, recoveryHost), Socket: provision.NestedSocket(recoveryHost)}
180 c.wake.Inputs = shapeMutatedInputs{recoveryInputs{f}, func(in *ClaimLaunchInputs) {
181 base := shapeInputs(recoveryHost)
182 base.DBPath = provision.NestedDBPath(shapeData, recoveryHost)
183 base.Args = []string{"--socket", provision.NestedSocket(recoveryHost), "--db", base.DBPath}
184 in.DBPath, in.Args, in.Env, in.Limit = base.DBPath, base.Args, base.Env, cgroup.TierDefault
185 mutate(in)
186 }}
187 before := f.read()
188 guardBefore, err := os.ReadDir(filepath.Join(f.guard.DataDir, "hostguard"))
189 if err != nil {
190 t.Fatal(err)
191 }
192 err = c.runtime.Prerequisites(t.Context(), route, before, recoveryPolicy)
193 if !errors.Is(err, hostbox.ErrRoleLaunchShape) && !errors.Is(err, hostbox.ErrUnsupported) {
194 t.Fatalf("accepted: %v", err)
195 }
196 if after := f.read(); after.ETag != before.ETag || after.Claim.Holder.WakeRetry != nil {
197 t.Fatalf("refusal changed claim or spent retry: %+v", after)
198 }
199 guardAfter, err := os.ReadDir(filepath.Join(f.guard.DataDir, "hostguard"))
200 if err != nil || !reflect.DeepEqual(guardBefore, guardAfter) || len(c.wake.pending) != 0 || len(f.requests) != 0 {
201 t.Fatalf("refusal reached guard/stop/lease: err=%v pending=%d requests=%d", err, len(c.wake.pending), len(f.requests))
202 }
203 if _, err := f.journal.Read(t.Context(), recoveryTuple(before, c.local)); err == nil {
204 t.Fatal("refusal journaled a launch")
205 }
206 })
207 }
208}
209