package host import ( "context" "encoding/json" "go/ast" "go/parser" "go/token" "io" "net/http" "net/http/httptest" "os" "path/filepath" "sync" "testing" "github.com/carlosframework/platform/internal/provision" ) // TestStuckMarkBeforeMetricsReachesTheSink (fix round 1, finding 1): the // agent arms paging at startup from the sink env alone (armAgentAlerts), so // a stuck mark reaches a configured webhook whether or not the metrics loop // has started, and when its store fails to initialise. The revert's stuck // mark runs with the production hook, PageOperator. func TestStuckMarkBeforeMetricsReachesTheSink(t *testing.T) { for _, metrics := range []string{"never started", "store init failed"} { t.Run(metrics, func(t *testing.T) { var mu sync.Mutex var bodies [][]byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) mu.Lock() bodies = append(bodies, b) mu.Unlock() })) defer srv.Close() env := map[string]string{"CARLOS_ALERT_WEBHOOK": srv.URL} sinks := armAgentAlerts(context.Background(), func(k string) string { return env[k] }, &alertCounters{}, (&fakeClientFactory{}).build) t.Cleanup(func() { armPaging(nil) }) if metrics == "store init failed" { // No bucket, and a fleet name that does not validate: // openStatusStore fails and the loop returns at once. t.Setenv("CARLOS_STATUS_BUCKET", "") t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "") t.Setenv("CARLOS_FLEET", "not a fleet!") if _, _, err := openStatusStore(context.Background(), os.Getenv); err == nil { t.Fatal("fixture: the metrics store opened") } metricsLoop(t.TempDir(), nil, sinks) } h := newRevertHarness(t) h.fromStopped(t) h.arm(t) h.cfg.Page = PageOperator if err := os.WriteFile(filepath.Join(h.cfg.DataDir, p1Host, "upload.txt"), []byte("changed while stopped"), 0o644); err != nil { t.Fatal(err) } h.pass(t) if m := h.move(t); m.State != provision.MoveStuck { t.Fatalf("not stuck: %+v", m) } mu.Lock() defer mu.Unlock() if len(bodies) != 1 { t.Fatalf("webhook received %d posts, want the one page", len(bodies)) } var p struct{ Kind, Level, Subject, Body string } if err := json.Unmarshal(bodies[0], &p); err != nil { t.Fatal(err) } if p.Kind != "page" || p.Level != "incident" || p.Subject != "carlos move stuck: "+p1Host || p.Body != stuckPageBody("ie-box", "source", provision.StuckBaselineMismatch) { t.Fatalf("the page: %+v", p) } }) } } // TestAgentArmsPagingBeforeMovesConsumers (fix round 1, finding 1): in // runAgent the alert sinks are built and paging armed once, before // armMovesPass and before any loop that runs a moves pass starts; the // metrics loop is handed that same instance and neither builds sinks nor // arms paging itself. func TestAgentArmsPagingBeforeMovesConsumers(t *testing.T) { fset := token.NewFileSet() parse := func(name string) *ast.File { f, err := parser.ParseFile(fset, name, nil, 0) if err != nil { t.Fatal(err) } return f } body := func(f *ast.File, name string) *ast.BlockStmt { for _, d := range f.Decls { if fd, ok := d.(*ast.FuncDecl); ok && fd.Recv == nil && fd.Name.Name == name { return fd.Body } } t.Fatalf("no func %s", name) return nil } callee := func(c *ast.CallExpr) string { if id, ok := c.Fun.(*ast.Ident); ok { return id.Name } return "" } // first call position of each name in runAgent, and how many times. at := map[string]token.Pos{} count := map[string]int{} var armed string // the variable armAgentAlerts' result is assigned to var metricsArg ast.Expr ast.Inspect(body(parse("agent.go"), "runAgent"), func(n ast.Node) bool { switch n := n.(type) { case *ast.AssignStmt: if len(n.Rhs) == 1 && len(n.Lhs) == 1 { if c, ok := n.Rhs[0].(*ast.CallExpr); ok && callee(c) == "armAgentAlerts" { if id, ok := n.Lhs[0].(*ast.Ident); ok { armed = id.Name } } } case *ast.CallExpr: name := callee(n) if name == "" { return true } count[name]++ if _, ok := at[name]; !ok { at[name] = n.Pos() } if name == "metricsLoop" && len(n.Args) == 3 { metricsArg = n.Args[2] } } return true }) if count["armAgentAlerts"] != 1 || armed == "" { t.Fatalf("runAgent calls armAgentAlerts %d times, assigned to %q; want once, kept", count["armAgentAlerts"], armed) } for _, later := range []string{"armMovesPass", "adoptLoop", "fastConvergeLoop", "metricsLoop"} { if at[later] == token.NoPos { t.Fatalf("runAgent no longer calls %s; update this test", later) } if at["armAgentAlerts"] > at[later] { t.Errorf("paging is armed after %s starts (%s before %s)", later, fset.Position(at[later]), fset.Position(at["armAgentAlerts"])) } } if id, ok := metricsArg.(*ast.Ident); !ok || id.Name != armed { t.Errorf("metricsLoop is not handed the armed sinks %q", armed) } ast.Inspect(body(parse("metrics_loop.go"), "metricsLoop"), func(n ast.Node) bool { if c, ok := n.(*ast.CallExpr); ok { if name := callee(c); name == "buildAlertSinks" || name == "armPaging" || name == "armAgentAlerts" { t.Errorf("metricsLoop calls %s; it must reuse the armed sinks", name) } } return true }) }