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 instanceexec
4
5import (
6 "encoding/json"
7 "errors"
8 "os"
9 "path/filepath"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/instancectl"
13 "github.com/carlosframework/platform/internal/provision"
14)
15
16func writeLaunch(t *testing.T, dir string, role instancectl.Role, l instancectl.Launch) {
17 t.Helper()
18 if err := os.MkdirAll(dir, 0o700); err != nil {
19 t.Fatal(err)
20 }
21 b, err := json.Marshal(l)
22 if err != nil {
23 t.Fatal(err)
24 }
25 if err := os.WriteFile(filepath.Join(dir, instancectl.LaunchFileName(role)), b, 0o600); err != nil {
26 t.Fatal(err)
27 }
28}
29
30type captured struct {
31 argv0 string
32 argv []string
33 env []string
34 calls int
35}
36
37func (c *captured) exec(argv0 string, argv, env []string) error {
38 c.calls++
39 c.argv0, c.argv, c.env = argv0, argv, env
40 return nil
41}
42
43func TestExecLaunchExecsExactlyWhatTheAgentWrote(t *testing.T) {
44 data := t.TempDir()
45 iid := "bill.example.com"
46 dir := filepath.Join(data, "instances", iid)
47 writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{
48 Schema: 1, Host: "bill.example.com",
49 Argv: []string{"/opt/carlos/live/bill.example.com", "--socket", "/run/carlos/instances/bill.example.com/app.sock", "--db", "/data/instances/bill.example.com/bill.example.com.db"},
50 Env: []string{"PATH=/usr/bin", "CARLOS_ADMIN_TOKEN=abc"},
51 })
52 var c captured
53 if err := execLaunch(data, instancectl.RoleRun, iid, c.exec); err != nil {
54 t.Fatalf("execLaunch: %v", err)
55 }
56 if c.calls != 1 || c.argv0 != "/opt/carlos/live/bill.example.com" || len(c.argv) != 5 || c.argv[2] != "/run/carlos/instances/bill.example.com/app.sock" {
57 t.Errorf("exec = %+v", c)
58 }
59 if len(c.env) != 2 || c.env[1] != "CARLOS_ADMIN_TOKEN=abc" {
60 t.Errorf("env = %v", c.env)
61 }
62}
63
64func TestExecLaunchRefusesAnArgv0OutsideTheLiveDir(t *testing.T) {
65 data := t.TempDir()
66 iid := "h.example.com"
67 dir := filepath.Join(data, "instances", iid)
68 for _, argv0 := range []string{"/bin/sh", "/opt/carlos/live/../carlos", "/opt/carlos/live/other.example.com", "opt/carlos/live/h.example.com", ""} {
69 writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{Schema: 1, Host: "h.example.com", Argv: []string{argv0}, Env: nil})
70 var c captured
71 err := execLaunch(data, instancectl.RoleRun, iid, c.exec)
72 if err == nil || c.calls != 0 {
73 t.Errorf("argv0 %q: err = %v, calls = %d; want a refusal and no exec", argv0, err, c.calls)
74 }
75 }
76}
77
78func TestExecLaunchRefusesUnknownSchemaAndBadEnv(t *testing.T) {
79 data := t.TempDir()
80 iid := "h.example.com"
81 dir := filepath.Join(data, "instances", iid)
82 good := []string{"/opt/carlos/live/h.example.com"}
83 writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{Schema: 2, Host: "h.example.com", Argv: good})
84 var c captured
85 if err := execLaunch(data, instancectl.RoleRun, iid, c.exec); err == nil || c.calls != 0 {
86 t.Errorf("schema 2: err = %v, calls = %d", err, c.calls)
87 }
88 writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{Schema: 1, Host: "h.example.com", Argv: good, Env: []string{"NOEQUALS"}})
89 if err := execLaunch(data, instancectl.RoleRun, iid, c.exec); err == nil || c.calls != 0 {
90 t.Errorf("bad env: err = %v, calls = %d", err, c.calls)
91 }
92 writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{Schema: 1, Host: "not a host", Argv: []string{"/opt/carlos/live/not a host"}})
93 if err := execLaunch(data, instancectl.RoleRun, iid, c.exec); err == nil || c.calls != 0 {
94 t.Errorf("bad host: err = %v, calls = %d", err, c.calls)
95 }
96}
97
98func TestExecLaunchRefusesASymlinkedLaunchFile(t *testing.T) {
99 data := t.TempDir()
100 iid := "h.example.com"
101 dir := filepath.Join(data, "instances", iid)
102 if err := os.MkdirAll(dir, 0o700); err != nil {
103 t.Fatal(err)
104 }
105 other := filepath.Join(t.TempDir(), "launch.run.json")
106 l := instancectl.Launch{Schema: 1, Host: "h.example.com", Argv: []string{"/opt/carlos/live/h.example.com"}}
107 b, _ := json.Marshal(l)
108 if err := os.WriteFile(other, b, 0o600); err != nil {
109 t.Fatal(err)
110 }
111 if err := os.Symlink(other, filepath.Join(dir, "launch.run.json")); err != nil {
112 t.Fatal(err)
113 }
114 var c captured
115 err := execLaunch(data, instancectl.RoleRun, iid, c.exec)
116 if err == nil || c.calls != 0 || !errors.Is(err, errSymlinkOrEscape) {
117 t.Errorf("symlinked launch file: err = %v, calls = %d", err, c.calls)
118 }
119}
120
121func TestExecLaunchRefusesAHostThatIsNotThisInstance(t *testing.T) {
122 // Case 1: launch file in instances/a.example.com/ with Host: b.example.com
123 data := t.TempDir()
124 iidA := "a.example.com"
125 dirA := filepath.Join(data, "instances", iidA)
126 writeLaunch(t, dirA, instancectl.RoleRun, instancectl.Launch{
127 Schema: 1, Host: "b.example.com",
128 Argv: []string{"/opt/carlos/live/b.example.com"},
129 })
130 var c captured
131 if err := execLaunch(data, instancectl.RoleRun, iidA, c.exec); err == nil || c.calls != 0 {
132 t.Errorf("host mismatch (short): err = %v, calls = %d; want refusal", err, c.calls)
133 }
134
135 // Case 2: long hostname (60 bytes), directory name is provision.InstanceID(long), Host is long
136 long := "a" + "b" + "c" + "d" + "e" + "f" + "g" + "h" + "i" + "j" +
137 "k" + "l" + "m" + "n" + "o" + "p" + "q" + "r" + "s" + "t" +
138 "u" + "v" + "w" + "x" + "y" + "z" + "0" + "1" + "2" + "3" +
139 "4" + "5" + "6" + "7" + "8" + "9" + "a" + "b" + "c" + "d" +
140 "e" + "f" + "g" + "h" + "i" + "j" + "k" + "l" + "m" + "n" +
141 "o" + "p" + "q" + "r" + "s" + "t" + "u" + "v" + "w" + "x"
142 if len(long) != 60 {
143 t.Fatalf("long host is %d bytes, want 60", len(long))
144 }
145 iidLong := provision.InstanceID(long)
146 dirLong := filepath.Join(data, "instances", iidLong)
147 writeLaunch(t, dirLong, instancectl.RoleRun, instancectl.Launch{
148 Schema: 1, Host: long,
149 Argv: []string{"/opt/carlos/live/" + long},
150 })
151 c2 := captured{}
152 if err := execLaunch(data, instancectl.RoleRun, iidLong, c2.exec); err != nil {
153 t.Errorf("host match (long): err = %v; want no error", err)
154 }
155 if c2.calls != 1 {
156 t.Errorf("host match (long): calls = %d; want 1", c2.calls)
157 }
158}
159
160func TestExecLaunchSplitsAGenerationInstance(t *testing.T) {
161 data := t.TempDir()
162 host := "bill.example.com"
163 dir := filepath.Join(data, "instances", host)
164 // The legacy launch file is a decoy: a generation must never read it.
165 writeLaunch(t, dir, instancectl.RoleRun, instancectl.Launch{Schema: 1, Host: host, Argv: []string{"/opt/carlos/live/" + host, "legacy"}})
166 raw, err := json.Marshal(instancectl.Launch{Schema: 1, Host: host, Argv: []string{"/opt/carlos/live/" + host, "--socket", "/run/carlos/instances/" + host + "/g7/app.sock"}})
167 if err != nil {
168 t.Fatal(err)
169 }
170 if err := os.WriteFile(filepath.Join(dir, instancectl.GenLaunchFileName(7)), raw, 0o600); err != nil {
171 t.Fatal(err)
172 }
173 var c captured
174 if err := execLaunch(data, instancectl.RoleRun, instancectl.GenInstance(host, 7), c.exec); err != nil {
175 t.Fatalf("execLaunch: %v", err)
176 }
177 if c.calls != 1 || len(c.argv) != 3 || c.argv[2] != "/run/carlos/instances/"+host+"/g7/app.sock" {
178 t.Errorf("exec = %+v", c)
179 }
180}
181
182// Each malformed instance string has an otherwise valid, host-matching
183// launch file planted wherever a lenient parser could look, so only the
184// parser can refuse it: the refusal must be launchTarget's own.
185func TestExecLaunchRefusesMalformedOrMisplacedGenerations(t *testing.T) {
186 data := t.TempDir()
187 host := "bill.example.com"
188 dir := filepath.Join(data, "instances", host)
189 plant := func(dir string, n uint64, l instancectl.Launch) {
190 t.Helper()
191 if err := os.MkdirAll(dir, 0o700); err != nil {
192 t.Fatal(err)
193 }
194 raw, err := json.Marshal(l)
195 if err != nil {
196 t.Fatal(err)
197 }
198 if err := os.WriteFile(filepath.Join(dir, instancectl.GenLaunchFileName(n)), raw, 0o600); err != nil {
199 t.Fatal(err)
200 }
201 }
202 valid := instancectl.Launch{Schema: 1, Host: host, Argv: []string{"/opt/carlos/live/" + host}}
203 for _, n := range []uint64{0, 1, 7} {
204 plant(dir, n, valid)
205 }
206 // Where a parser that let "", ".." or "a:b" through would look.
207 plant(filepath.Join(data, "instances"), 7, valid)
208 plant(filepath.Join(data, "x"), 1, valid)
209 plant(filepath.Join(data, "instances", "a:b"), 1, valid)
210 plant(filepath.Join(data, "instances", "a"), 1, valid)
211
212 // The planted file is otherwise valid: the well-formed instance runs it.
213 var ok captured
214 if err := execLaunch(data, instancectl.RoleRun, host+":7", ok.exec); err != nil || ok.calls != 1 {
215 t.Fatalf("the planted file was refused for a valid instance: %v, %d execs", err, ok.calls)
216 }
217
218 for _, tc := range []struct {
219 role instancectl.Role
220 inst string
221 }{
222 {instancectl.RoleRun, host + ":0"}, {instancectl.RoleRun, host + ":07"},
223 {instancectl.RoleRun, ":7"}, {instancectl.RoleRun, host + ":"},
224 {instancectl.RoleRun, "../x:1"}, {instancectl.RoleRun, "a:b:1"},
225 {instancectl.RoleSidecar, host + ":7"}, {instancectl.RoleSeed, host + ":7"},
226 } {
227 _, _, perr := launchTarget(tc.role, tc.inst)
228 if perr == nil {
229 t.Errorf("launchTarget(%s, %q) accepted it", tc.role, tc.inst)
230 continue
231 }
232 var c captured
233 err := execLaunch(data, tc.role, tc.inst, c.exec)
234 if err == nil || c.calls != 0 || err.Error() != perr.Error() {
235 t.Errorf("execLaunch(%s, %q) = %v after %d execs; want the parser's refusal %q and no exec", tc.role, tc.inst, err, c.calls, perr)
236 }
237 }
238
239 // Misplaced: a well-formed generation whose file names another host.
240 plant(dir, 9, instancectl.Launch{Schema: 1, Host: "other.example.com", Argv: []string{"/opt/carlos/live/other.example.com"}})
241 var c captured
242 if err := execLaunch(data, instancectl.RoleRun, host+":9", c.exec); err == nil || c.calls != 0 {
243 t.Errorf("a generation file naming another host: %v after %d execs; want a refusal", err, c.calls)
244 }
245}
246