// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package allocrunner import ( "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/hashicorp/go-hclog" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/client/allocrunner/interfaces" "github.com/hashicorp/nomad/client/serviceregistration/checks/checkstore" "github.com/hashicorp/nomad/client/state" "github.com/hashicorp/nomad/client/taskenv" "github.com/hashicorp/nomad/helper/testlog" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" "github.com/shoenig/test/must" ) var ( _ interfaces.RunnerPrerunHook = (*checksHook)(nil) _ interfaces.RunnerUpdateHook = (*checksHook)(nil) _ interfaces.RunnerPreKillHook = (*checksHook)(nil) ) func makeCheckStore(logger hclog.Logger) checkstore.Shim { db := state.NewMemDB(logger) checkStore := checkstore.NewStore(logger, db) return checkStore } func allocWithNomadChecks(addr, port string, onGroup bool) *structs.Allocation { alloc := mock.Alloc() group := alloc.Job.LookupTaskGroup(alloc.TaskGroup) task := "task-one" if onGroup { task = "" } services := []*structs.Service{ { Name: "service-one", TaskName: "web", PortLabel: port, AddressMode: "auto", Address: addr, Provider: "nomad", Checks: []*structs.ServiceCheck{ { Name: "check-ok", Type: "http", Path: "/", Protocol: "http", PortLabel: port, AddressMode: "auto", Interval: 250 * time.Millisecond, Timeout: 1 * time.Second, Method: "GET", TaskName: task, }, { Name: "check-error", Type: "http", Path: "/fail", Protocol: "http", PortLabel: port, AddressMode: "auto", Interval: 250 * time.Millisecond, Timeout: 1 * time.Second, Method: "GET", TaskName: task, }, { Name: "check-hang", Type: "http", Path: "/hang", Protocol: "http", PortLabel: port, AddressMode: "auto", Interval: 250 * time.Millisecond, Timeout: 500 * time.Millisecond, Method: "GET", TaskName: task, }, }, }, } switch onGroup { case true: group.Tasks[0].Services = nil group.Services = services case false: group.Services = nil group.Tasks[0].Services = services } return alloc } func allocWithDifferentNomadChecks(id, addr, port string) *structs.Allocation { alloc := allocWithNomadChecks(addr, port, true) alloc.ID = id group := alloc.Job.LookupTaskGroup(alloc.TaskGroup) group.Services[0].Checks[2].Path = "/" // the hanging check is now ok // append 4th check, this one is failing group.Services[0].Checks = append(group.Services[0].Checks, &structs.ServiceCheck{ Name: "check-error-2", Type: "http", Path: "/fail", Protocol: "http", PortLabel: port, AddressMode: "auto", Interval: 250 * time.Millisecond, Timeout: 1 * time.Second, Method: "GET", }) return alloc } var checkHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/fail": w.WriteHeader(http.StatusInternalServerError) _, _ = io.WriteString(w, "500 problem") case "/hang": time.Sleep(2 * time.Second) _, _ = io.WriteString(w, "too slow") default: w.WriteHeader(http.StatusOK) _, _ = io.WriteString(w, "200 ok") } }) func TestCheckHook_Checks_ResultsSet(t *testing.T) { ci.Parallel(t) logger := testlog.HCLogger(t) // create an http server with various responses ts := httptest.NewServer(checkHandler) defer ts.Close() cases := []struct { name string onGroup bool }{ {name: "group-level", onGroup: true}, {name: "task-level", onGroup: false}, } for _, tc := range cases { checkStore := makeCheckStore(logger) // get the address and port for http server tokens := strings.Split(ts.URL, ":") addr, port := strings.TrimPrefix(tokens[1], "//"), tokens[2] network := mock.NewNetworkStatus(addr) alloc := allocWithNomadChecks(addr, port, tc.onGroup) envBuilder := taskenv.NewBuilder(mock.Node(), alloc, nil, alloc.Job.Region) h := newChecksHook(logger, alloc, checkStore, network, envBuilder.Build()) // initialize is called; observers are created but not started yet must.MapEmpty(t, h.observers) // calling pre-run starts the observers err := h.Prerun() must.NoError(t, err) testutil.WaitForResultUntil( 2*time.Second, func() (bool, error) { results := checkStore.List(alloc.ID) passing, failing, pending := 0, 0, 0 for _, result := range results { switch result.Status { case structs.CheckSuccess: passing++ case structs.CheckFailure: failing++ case structs.CheckPending: pending++ } } if passing != 1 || failing != 2 || pending != 0 { fmt.Printf("results %v\n", results) return false, fmt.Errorf( "expected 1 passing, 2 failing, 0 pending, got %d passing, %d failing, %d pending", passing, failing, pending, ) } return true, nil }, func(err error) { t.Fatalf(err.Error()) }, ) h.PreKill() // stop observers, cleanup // assert shim no longer contains results for the alloc results := checkStore.List(alloc.ID) must.MapEmpty(t, results) } } func TestCheckHook_Checks_UpdateSet(t *testing.T) { ci.Parallel(t) logger := testlog.HCLogger(t) // create an http server with various responses ts := httptest.NewServer(checkHandler) defer ts.Close() // get the address and port for http server tokens := strings.Split(ts.URL, ":") addr, port := strings.TrimPrefix(tokens[1], "//"), tokens[2] shim := makeCheckStore(logger) network := mock.NewNetworkStatus(addr) alloc := allocWithNomadChecks(addr, port, true) envBuilder := taskenv.NewBuilder(mock.Node(), alloc, nil, alloc.Job.Region) h := newChecksHook(logger, alloc, shim, network, envBuilder.Build()) // calling pre-run starts the observers err := h.Prerun() must.NoError(t, err) // initial set of checks testutil.WaitForResultUntil( 2*time.Second, func() (bool, error) { results := shim.List(alloc.ID) passing, failing, pending := 0, 0, 0 for _, result := range results { switch result.Status { case structs.CheckSuccess: passing++ case structs.CheckFailure: failing++ case structs.CheckPending: pending++ } } if passing != 1 || failing != 2 || pending != 0 { fmt.Printf("results %v\n", results) return false, fmt.Errorf( "(initial set) expected 1 passing, 2 failing, 0 pending, got %d passing, %d failing, %d pending", passing, failing, pending, ) } return true, nil }, func(err error) { t.Fatalf(err.Error()) }, ) request := &interfaces.RunnerUpdateRequest{ Alloc: allocWithDifferentNomadChecks(alloc.ID, addr, port), } err = h.Update(request) must.NoError(t, err) // updated set of checks testutil.WaitForResultUntil( 2*time.Second, func() (bool, error) { results := shim.List(alloc.ID) passing, failing, pending := 0, 0, 0 for _, result := range results { switch result.Status { case structs.CheckSuccess: passing++ case structs.CheckFailure: failing++ case structs.CheckPending: pending++ } } if passing != 2 || failing != 2 || pending != 0 { fmt.Printf("results %v\n", results) return false, fmt.Errorf( "(updated set) expected 2 passing, 2 failing, 0 pending, got %d passing, %d failing, %d pending", passing, failing, pending, ) } return true, nil }, func(err error) { t.Fatalf(err.Error()) }, ) h.PreKill() // stop observers, cleanup // assert shim no longer contains results for the alloc results := shim.List(alloc.ID) must.MapEmpty(t, results) }