carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "encoding/json"
6 "go/ast"
7 "go/parser"
8 "go/token"
9 "io"
10 "net/http"
11 "net/http/httptest"
12 "os"
13 "path/filepath"
14 "sync"
15 "testing"
16
17 "github.com/carlosframework/platform/internal/provision"
18)
19
20// TestStuckMarkBeforeMetricsReachesTheSink (fix round 1, finding 1): the
21// agent arms paging at startup from the sink env alone (armAgentAlerts), so
22// a stuck mark reaches a configured webhook whether or not the metrics loop
23// has started, and when its store fails to initialise. The revert's stuck
24// mark runs with the production hook, PageOperator.
25func TestStuckMarkBeforeMetricsReachesTheSink(t *testing.T) {
26 for _, metrics := range []string{"never started", "store init failed"} {
27 t.Run(metrics, func(t *testing.T) {
28 var mu sync.Mutex
29 var bodies [][]byte
30 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31 b, _ := io.ReadAll(r.Body)
32 mu.Lock()
33 bodies = append(bodies, b)
34 mu.Unlock()
35 }))
36 defer srv.Close()
37 env := map[string]string{"CARLOS_ALERT_WEBHOOK": srv.URL}
38 sinks := armAgentAlerts(context.Background(), func(k string) string { return env[k] }, &alertCounters{}, (&fakeClientFactory{}).build)
39 t.Cleanup(func() { armPaging(nil) })
40
41 if metrics == "store init failed" {
42 // No bucket, and a fleet name that does not validate:
43 // openStatusStore fails and the loop returns at once.
44 t.Setenv("CARLOS_STATUS_BUCKET", "")
45 t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "")
46 t.Setenv("CARLOS_FLEET", "not a fleet!")
47 if _, _, err := openStatusStore(context.Background(), os.Getenv); err == nil {
48 t.Fatal("fixture: the metrics store opened")
49 }
50 metricsLoop(t.TempDir(), nil, sinks)
51 }
52
53 h := newRevertHarness(t)
54 h.fromStopped(t)
55 h.arm(t)
56 h.cfg.Page = PageOperator
57 if err := os.WriteFile(filepath.Join(h.cfg.DataDir, p1Host, "upload.txt"), []byte("changed while stopped"), 0o644); err != nil {
58 t.Fatal(err)
59 }
60 h.pass(t)
61 if m := h.move(t); m.State != provision.MoveStuck {
62 t.Fatalf("not stuck: %+v", m)
63 }
64 mu.Lock()
65 defer mu.Unlock()
66 if len(bodies) != 1 {
67 t.Fatalf("webhook received %d posts, want the one page", len(bodies))
68 }
69 var p struct{ Kind, Level, Subject, Body string }
70 if err := json.Unmarshal(bodies[0], &p); err != nil {
71 t.Fatal(err)
72 }
73 if p.Kind != "page" || p.Level != "incident" || p.Subject != "carlos move stuck: "+p1Host || p.Body != stuckPageBody("ie-box", "source", provision.StuckBaselineMismatch) {
74 t.Fatalf("the page: %+v", p)
75 }
76 })
77 }
78}
79
80// TestAgentArmsPagingBeforeMovesConsumers (fix round 1, finding 1): in
81// runAgent the alert sinks are built and paging armed once, before
82// armMovesPass and before any loop that runs a moves pass starts; the
83// metrics loop is handed that same instance and neither builds sinks nor
84// arms paging itself.
85func TestAgentArmsPagingBeforeMovesConsumers(t *testing.T) {
86 fset := token.NewFileSet()
87 parse := func(name string) *ast.File {
88 f, err := parser.ParseFile(fset, name, nil, 0)
89 if err != nil {
90 t.Fatal(err)
91 }
92 return f
93 }
94 body := func(f *ast.File, name string) *ast.BlockStmt {
95 for _, d := range f.Decls {
96 if fd, ok := d.(*ast.FuncDecl); ok && fd.Recv == nil && fd.Name.Name == name {
97 return fd.Body
98 }
99 }
100 t.Fatalf("no func %s", name)
101 return nil
102 }
103 callee := func(c *ast.CallExpr) string {
104 if id, ok := c.Fun.(*ast.Ident); ok {
105 return id.Name
106 }
107 return ""
108 }
109 // first call position of each name in runAgent, and how many times.
110 at := map[string]token.Pos{}
111 count := map[string]int{}
112 var armed string // the variable armAgentAlerts' result is assigned to
113 var metricsArg ast.Expr
114 ast.Inspect(body(parse("agent.go"), "runAgent"), func(n ast.Node) bool {
115 switch n := n.(type) {
116 case *ast.AssignStmt:
117 if len(n.Rhs) == 1 && len(n.Lhs) == 1 {
118 if c, ok := n.Rhs[0].(*ast.CallExpr); ok && callee(c) == "armAgentAlerts" {
119 if id, ok := n.Lhs[0].(*ast.Ident); ok {
120 armed = id.Name
121 }
122 }
123 }
124 case *ast.CallExpr:
125 name := callee(n)
126 if name == "" {
127 return true
128 }
129 count[name]++
130 if _, ok := at[name]; !ok {
131 at[name] = n.Pos()
132 }
133 if name == "metricsLoop" && len(n.Args) == 3 {
134 metricsArg = n.Args[2]
135 }
136 }
137 return true
138 })
139 if count["armAgentAlerts"] != 1 || armed == "" {
140 t.Fatalf("runAgent calls armAgentAlerts %d times, assigned to %q; want once, kept", count["armAgentAlerts"], armed)
141 }
142 for _, later := range []string{"armMovesPass", "adoptLoop", "fastConvergeLoop", "metricsLoop"} {
143 if at[later] == token.NoPos {
144 t.Fatalf("runAgent no longer calls %s; update this test", later)
145 }
146 if at["armAgentAlerts"] > at[later] {
147 t.Errorf("paging is armed after %s starts (%s before %s)", later, fset.Position(at[later]), fset.Position(at["armAgentAlerts"]))
148 }
149 }
150 if id, ok := metricsArg.(*ast.Ident); !ok || id.Name != armed {
151 t.Errorf("metricsLoop is not handed the armed sinks %q", armed)
152 }
153 ast.Inspect(body(parse("metrics_loop.go"), "metricsLoop"), func(n ast.Node) bool {
154 if c, ok := n.(*ast.CallExpr); ok {
155 if name := callee(c); name == "buildAlertSinks" || name == "armPaging" || name == "armAgentAlerts" {
156 t.Errorf("metricsLoop calls %s; it must reuse the armed sinks", name)
157 }
158 }
159 return true
160 })
161}
162