carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package instancectl
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 "time"
12)
13
14// newClient serves a Server on a real unix socket with the given fake
15// systemctl and returns the agent-side Client for it.
16func newClient(t *testing.T, ctl *fakeCtl, configure ...func(*Server)) (*Server, *Client) {
17 t.Helper()
18 s, sock := serveOnSocket(t, ctl, configure...)
19 return s, NewClient(sock)
20}
21
22func TestClientStartCarriesTierAndReturnsInvocation(t *testing.T) {
23 ctl := &fakeCtl{answers: map[string]string{"show -p InvocationID --value": "inv1\n"}}
24 _, c := newClient(t, ctl)
25 inv, err := c.Start(context.Background(), "h.example.com", RoleRun, 536870912)
26 if err != nil || inv != "inv1" {
27 t.Fatalf("Start = %q, %v", inv, err)
28 }
29 if !strings.Contains(ctl.joined(), "MemoryMax=536870912") {
30 t.Errorf("tier not sent: %s", ctl.joined())
31 }
32 if !strings.Contains(ctl.joined(), "start "+UnitName(RoleRun, "h.example.com")) {
33 t.Errorf("run unit not started: %s", ctl.joined())
34 }
35}
36
37func TestClientSeedStopResetSweep(t *testing.T) {
38 ctl := &fakeCtl{answers: map[string]string{
39 "show -p InvocationID,Result,ExecMainStatus,ExecMainCode": "InvocationID=s1\nResult=success\nExecMainStatus=0\nExecMainCode=exited\n",
40 }}
41 _, c := newClient(t, ctl)
42 res, err := c.Seed(context.Background(), "h.example.com", 536870912)
43 if err != nil || res.Result != "success" || res.InvocationID != "s1" || res.Status != "0" || res.Code != "exited" {
44 t.Fatalf("Seed = %+v, %v", res, err)
45 }
46 if err := c.Stop(context.Background(), "h.example.com", RoleSidecar); err != nil {
47 t.Fatalf("Stop: %v", err)
48 }
49 if !strings.Contains(ctl.joined(), "stop "+UnitName(RoleSidecar, "h.example.com")) {
50 t.Errorf("sidecar unit not stopped: %s", ctl.joined())
51 }
52 if err := c.Reset(context.Background(), "h.example.com", RoleRun); err != nil {
53 t.Fatalf("Reset: %v", err)
54 }
55 sw, err := c.Sweep(context.Background())
56 if err != nil || len(sw.Units) != 0 || len(sw.Failed) != 0 {
57 t.Fatalf("Sweep = %+v, %v", sw, err)
58 }
59}
60
61func TestClientReadyIsNilOn204AndCarriesTheReportOn503(t *testing.T) {
62 ctl := &fakeCtl{}
63 s, c := newClient(t, ctl)
64 // Before the first probe the server answers 503 with one line saying
65 // so; the client surfaces it as a NotReadyError whose message names
66 // the failing line.
67 err := c.Ready(context.Background())
68 var nr *NotReadyError
69 if !errors.As(err, &nr) || len(nr.Report.Lines) != 1 || nr.Report.Lines[0].Check != "probe" {
70 t.Fatalf("Ready before a probe = %v", err)
71 }
72 if !strings.Contains(err.Error(), "not yet run") {
73 t.Errorf("message = %q", err.Error())
74 }
75 b, _ := json.Marshal(ProbeReport{Ready: true, Lines: []VerdictLine{{Check: "pid1", Verdict: "ok"}}})
76 if err := os.WriteFile(s.ProbeOut, b, 0o644); err != nil {
77 t.Fatal(err)
78 }
79 s.RunProbe(context.Background())
80 if err := c.Ready(context.Background()); err != nil {
81 t.Fatalf("Ready after a passing probe: %v", err)
82 }
83}
84
85func TestClientSurfacesTheServersMessageOnA502(t *testing.T) {
86 ctl := &fakeCtl{fail: map[string]error{"start": errors.New("boom")}}
87 _, c := newClient(t, ctl)
88 _, err := c.Start(context.Background(), "h.example.com", RoleRun, 536870912)
89 if err == nil || !strings.Contains(err.Error(), "boom") || !strings.Contains(err.Error(), "/start") {
90 t.Fatalf("Start error = %v, want the server's message and the verb", err)
91 }
92}
93
94func TestClientSweepReturnsThePartialListWithTheError(t *testing.T) {
95 // One unit is listed, its stop fails, and the server answers 502 with
96 // the partial result; the client returns both.
97 unit := UnitName(RoleRun, "h.example.com")
98 ctl := &fakeCtl{
99 answers: map[string]string{
100 "list-units": unit + " loaded active running carlos instance\n",
101 },
102 fail: map[string]error{"stop": errors.New("would not stop")},
103 }
104 _, c := newClient(t, ctl)
105 sw, err := c.Sweep(context.Background())
106 if err == nil {
107 t.Fatalf("Sweep = %+v, want an error", sw)
108 }
109 if _, listed := sw.Failed[unit]; !listed {
110 t.Errorf("Failed = %+v, want %s", sw.Failed, unit)
111 }
112}
113
114func TestClientRefusesWhenTheSocketIsAbsent(t *testing.T) {
115 c := NewClient(filepath.Join(t.TempDir(), "absent.sock"))
116 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
117 defer cancel()
118 if err := c.Ready(ctx); err == nil || !strings.Contains(err.Error(), "instance control socket") {
119 t.Fatalf("absent socket: %v", err)
120 }
121}
122
123// The agent side of #256: a 204 from a service that reports another build,
124// or none at all (a release before the header), is not ready. Both are
125// the service carlos system apply or the roll script left running across
126// a binary change, and the error names the restart.
127func TestClientReadyRefusesAControlServiceOfAnotherBuild(t *testing.T) {
128 ctl := &fakeCtl{}
129 s, c := newClient(t, ctl, func(s *Server) { s.Build = "" })
130 b, _ := json.Marshal(ProbeReport{Ready: true, Lines: []VerdictLine{{Check: "pid1", Verdict: "ok"}}})
131 if err := os.WriteFile(s.ProbeOut, b, 0o644); err != nil {
132 t.Fatal(err)
133 }
134 s.RunProbe(context.Background())
135 c.Build = "new1234"
136 err := c.Ready(context.Background())
137 var stale *StaleBuildError
138 if !errors.As(err, &stale) || stale.Running != "" || !strings.Contains(err.Error(), "older build") || !strings.Contains(err.Error(), "restart carlos-instance-control.service") {
139 t.Fatalf("no header: Ready = %v", err)
140 }
141 s.Build = "old5678"
142 err = c.Ready(context.Background())
143 if !errors.As(err, &stale) || stale.Running != "old5678" || !strings.Contains(err.Error(), "running build old5678, this agent is new1234") {
144 t.Fatalf("other build: Ready = %v", err)
145 }
146 s.Build = "new1234"
147 if err := c.Ready(context.Background()); err != nil {
148 t.Fatalf("same build: Ready = %v", err)
149 }
150 // A client with no build of its own (a test's literal) does not judge.
151 c.Build = ""
152 s.Build = ""
153 if err := c.Ready(context.Background()); err != nil {
154 t.Fatalf("unjudged: Ready = %v", err)
155 }
156 // NewClient judges with this process's own build.
157 if NewClient(c.Path).Build == "" {
158 t.Fatal("NewClient left Build empty; production would never check")
159 }
160}
161