// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package api import ( "fmt" "sort" "strings" "testing" "time" "github.com/hashicorp/nomad/api/internal/testutil" "github.com/shoenig/test/must" "github.com/shoenig/test/wait" ) func TestAgent_Self(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() // Get a handle on the Agent endpoints a := c.Agent() // Query the endpoint res, err := a.Self() must.NoError(t, err) // Check that we got a valid response must.NotEq(t, "", res.Member.Name, must.Sprint("missing member name")) // Local cache was populated must.NotEq(t, "", a.nodeName, must.Sprint("cache should be populated")) must.NotEq(t, "", a.datacenter, must.Sprint("cache should be populated")) must.NotEq(t, "", a.region, must.Sprint("cache should be populated")) } func TestAgent_NodeName(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() a := c.Agent() // Query the agent for the node name nodeName, err := a.NodeName() must.NoError(t, err) must.NotEq(t, "", nodeName) } func TestAgent_Datacenter(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() a := c.Agent() // Query the agent for the datacenter dc, err := a.Datacenter() must.NoError(t, err) must.Eq(t, "dc1", dc) } func TestAgent_Join(t *testing.T) { testutil.Parallel(t) c1, s1 := makeClient(t, nil, nil) defer s1.Stop() a1 := c1.Agent() _, s2 := makeClient(t, nil, func(c *testutil.TestServerConfig) { c.Server.BootstrapExpect = 0 }) defer s2.Stop() // Attempting to join a nonexistent host returns error n, err := a1.Join("nope") must.Error(t, err) must.Zero(t, 0, must.Sprint("should be zero errors")) // Returns correctly if join succeeds n, err = a1.Join(s2.SerfAddr) must.NoError(t, err) must.One(t, n) } func TestAgent_Members(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() a := c.Agent() // Query nomad for all the known members mem, err := a.Members() must.NoError(t, err) // Check that we got the expected result must.Len(t, 1, mem.Members) must.NotEq(t, "", mem.Members[0].Name) must.NotEq(t, "", mem.Members[0].Addr) must.NotEq(t, 0, mem.Members[0].Port) } func TestAgent_ForceLeave(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() a := c.Agent() // Force-leave on a nonexistent node does not error err := a.ForceLeave("nope") must.NoError(t, err) // Force-leave on an existing node _, s2 := makeClient(t, nil, func(c *testutil.TestServerConfig) { c.Server.BootstrapExpect = 0 }) defer s2.Stop() // Create a new node to join n, err := a.Join(s2.SerfAddr) must.NoError(t, err) must.One(t, n) membersBefore, err := a.MembersOpts(&QueryOptions{}) must.NoError(t, err) must.Eq(t, membersBefore.Members[1].Status, "alive") err = a.ForceLeave(membersBefore.Members[1].Name) must.NoError(t, err) time.Sleep(3 * time.Second) f := func() error { membersAfter, err := a.MembersOpts(&QueryOptions{}) if err != nil { return err } for _, node := range membersAfter.Members { if node.Name == membersBefore.Members[1].Name { if node.Status != "leaving" { return fmt.Errorf("node did not leave") } } } return nil } must.Wait(t, wait.InitialSuccess( wait.ErrorFunc(f), wait.Timeout(3*time.Second), wait.Gap(100*time.Millisecond), )) } func TestAgent_ForceLeavePrune(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() a := c.Agent() nodeName := "foo" _, s2 := makeClient(t, nil, func(c *testutil.TestServerConfig) { c.NodeName = nodeName c.Server.BootstrapExpect = 0 }) n, err := a.Join(s2.SerfAddr) must.NoError(t, err) must.One(t, n) membersBefore, err := a.MembersOpts(&QueryOptions{}) must.NoError(t, err) s2.Stop() forceLeaveOpts := ForceLeaveOpts{ Prune: true, } nodeName = nodeName + ".global" err = a.ForceLeaveWithOptions(nodeName, forceLeaveOpts) must.NoError(t, err) f := func() error { membersAfter, err := a.MembersOpts(&QueryOptions{}) if err != nil { return err } if len(membersAfter.Members) == len(membersBefore.Members) { return fmt.Errorf("node did not get pruned") } return nil } must.Wait(t, wait.InitialSuccess( wait.ErrorFunc(f), wait.Timeout(5*time.Second), wait.Gap(100*time.Millisecond), )) } func (a *AgentMember) String() string { return "{Name: " + a.Name + " Region: " + a.Tags["region"] + " DC: " + a.Tags["dc"] + "}" } func TestAgents_Sort(t *testing.T) { testutil.Parallel(t) var sortTests = []struct { in []*AgentMember out []*AgentMember }{ { []*AgentMember{ {Name: "nomad-2.vac.us-east", Tags: map[string]string{"region": "us-east", "dc": "us-east-1c"}}, {Name: "nomad-1.global", Tags: map[string]string{"region": "global", "dc": "dc1"}}, {Name: "nomad-1.vac.us-east", Tags: map[string]string{"region": "us-east", "dc": "us-east-1c"}}, }, []*AgentMember{ {Name: "nomad-1.global", Tags: map[string]string{"region": "global", "dc": "dc1"}}, {Name: "nomad-1.vac.us-east", Tags: map[string]string{"region": "us-east", "dc": "us-east-1c"}}, {Name: "nomad-2.vac.us-east", Tags: map[string]string{"region": "us-east", "dc": "us-east-1c"}}, }, }, { []*AgentMember{ {Name: "nomad-02.tam.us-east", Tags: map[string]string{"region": "us-east", "dc": "tampa"}}, {Name: "nomad-02.pal.us-west", Tags: map[string]string{"region": "us-west", "dc": "palo_alto"}}, {Name: "nomad-01.pal.us-west", Tags: map[string]string{"region": "us-west", "dc": "palo_alto"}}, {Name: "nomad-01.tam.us-east", Tags: map[string]string{"region": "us-east", "dc": "tampa"}}, }, []*AgentMember{ {Name: "nomad-01.tam.us-east", Tags: map[string]string{"region": "us-east", "dc": "tampa"}}, {Name: "nomad-02.tam.us-east", Tags: map[string]string{"region": "us-east", "dc": "tampa"}}, {Name: "nomad-01.pal.us-west", Tags: map[string]string{"region": "us-west", "dc": "palo_alto"}}, {Name: "nomad-02.pal.us-west", Tags: map[string]string{"region": "us-west", "dc": "palo_alto"}}, }, }, { []*AgentMember{ {Name: "nomad-02.tam.us-east", Tags: map[string]string{"region": "us-east", "dc": "tampa"}}, {Name: "nomad-02.ams.europe", Tags: map[string]string{"region": "europe", "dc": "amsterdam"}}, {Name: "nomad-01.tam.us-east", Tags: map[string]string{"region": "us-east", "dc": "tampa"}}, {Name: "nomad-01.ams.europe", Tags: map[string]string{"region": "europe", "dc": "amsterdam"}}, }, []*AgentMember{ {Name: "nomad-01.ams.europe", Tags: map[string]string{"region": "europe", "dc": "amsterdam"}}, {Name: "nomad-02.ams.europe", Tags: map[string]string{"region": "europe", "dc": "amsterdam"}}, {Name: "nomad-01.tam.us-east", Tags: map[string]string{"region": "us-east", "dc": "tampa"}}, {Name: "nomad-02.tam.us-east", Tags: map[string]string{"region": "us-east", "dc": "tampa"}}, }, }, { []*AgentMember{ {Name: "nomad-02.ber.europe", Tags: map[string]string{"region": "europe", "dc": "berlin"}}, {Name: "nomad-02.ams.europe", Tags: map[string]string{"region": "europe", "dc": "amsterdam"}}, {Name: "nomad-01.ams.europe", Tags: map[string]string{"region": "europe", "dc": "amsterdam"}}, {Name: "nomad-01.ber.europe", Tags: map[string]string{"region": "europe", "dc": "berlin"}}, }, []*AgentMember{ {Name: "nomad-01.ams.europe", Tags: map[string]string{"region": "europe", "dc": "amsterdam"}}, {Name: "nomad-02.ams.europe", Tags: map[string]string{"region": "europe", "dc": "amsterdam"}}, {Name: "nomad-01.ber.europe", Tags: map[string]string{"region": "europe", "dc": "berlin"}}, {Name: "nomad-02.ber.europe", Tags: map[string]string{"region": "europe", "dc": "berlin"}}, }, }, { []*AgentMember{ {Name: "nomad-1.global"}, {Name: "nomad-3.global"}, {Name: "nomad-2.global"}, }, []*AgentMember{ {Name: "nomad-1.global"}, {Name: "nomad-2.global"}, {Name: "nomad-3.global"}, }, }, } for _, tt := range sortTests { sort.Sort(AgentMembersNameSort(tt.in)) must.Eq(t, tt.in, tt.out) } } func TestAgent_Health(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() a := c.Agent() health, err := a.Health() must.NoError(t, err) must.True(t, health.Server.Ok) } // TestAgent_MonitorWithNode tests the Monitor endpoint // passing in a log level and node ie, which tests monitor // functionality for a specific client node func TestAgent_MonitorWithNode(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) { c.DevMode = true }) defer s.Stop() agent := c.Agent() node := oneNodeFromNodeList(t, c.Nodes()) doneCh := make(chan struct{}) q := &QueryOptions{ Params: map[string]string{ "log_level": "debug", "node_id": node.ID, }, } frames, errCh := agent.Monitor(doneCh, q) defer close(doneCh) // make a request to generate some logs _, err := agent.NodeName() must.NoError(t, err) // Wait for a log message OUTER: for { select { case f := <-frames: if strings.Contains(string(f.Data), "[DEBUG]") { break OUTER } case err := <-errCh: t.Errorf("Error: %v", err) case <-time.After(2 * time.Second): t.Fatal("failed to get a DEBUG log message") } } } // TestAgent_Monitor tests the Monitor endpoint // passing in only a log level, which tests the servers // monitor functionality func TestAgent_Monitor(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() agent := c.Agent() q := &QueryOptions{ Params: map[string]string{ "log_level": "debug", }, } doneCh := make(chan struct{}) frames, errCh := agent.Monitor(doneCh, q) defer close(doneCh) // make a request to generate some logs _, err := agent.Region() must.NoError(t, err) // Wait for a log message OUTER: for { select { case log := <-frames: if log == nil { continue } if strings.Contains(string(log.Data), "[DEBUG]") { break OUTER } case err := <-errCh: t.Fatalf("error: %v", err) case <-time.After(2 * time.Second): must.Unreachable(t, must.Sprint("failed to get DEBUG log message")) } } } func TestAgentCPUProfile(t *testing.T) { testutil.Parallel(t) c, s, token := makeACLClient(t, nil, nil) defer s.Stop() agent := c.Agent() q := &QueryOptions{ AuthToken: token.SecretID, } // Valid local request { opts := PprofOptions{ Seconds: 1, } resp, err := agent.CPUProfile(opts, q) must.NoError(t, err) must.NotNil(t, resp) } // Invalid server request { opts := PprofOptions{ Seconds: 1, ServerID: "unknown.global", } resp, err := agent.CPUProfile(opts, q) must.Error(t, err) must.ErrorContains(t, err, "500 (unknown Nomad server unknown.global)") must.Nil(t, resp) } } func TestAgentTrace(t *testing.T) { testutil.Parallel(t) c, s, token := makeACLClient(t, nil, nil) defer s.Stop() agent := c.Agent() q := &QueryOptions{ AuthToken: token.SecretID, } resp, err := agent.Trace(PprofOptions{}, q) must.NoError(t, err) must.NotNil(t, resp) } func TestAgentProfile(t *testing.T) { testutil.Parallel(t) c, s, token := makeACLClient(t, nil, nil) defer s.Stop() agent := c.Agent() q := &QueryOptions{ AuthToken: token.SecretID, } { resp, err := agent.Lookup("heap", PprofOptions{}, q) must.NoError(t, err) must.NotNil(t, resp) } // unknown profile { resp, err := agent.Lookup("invalid", PprofOptions{}, q) must.Error(t, err) must.ErrorContains(t, err, "Unexpected response code: 404") must.Nil(t, resp) } } func TestAgent_SchedulerWorkerConfig(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() a := c.Agent() config, err := a.GetSchedulerWorkerConfig(nil) must.NoError(t, err) must.NotNil(t, config) newConfig := SchedulerWorkerPoolArgs{NumSchedulers: 0, EnabledSchedulers: []string{"_core", "system"}} resp, err := a.SetSchedulerWorkerConfig(newConfig, nil) must.NoError(t, err) must.NotEq(t, config, resp) } func TestAgent_SchedulerWorkerConfig_BadRequest(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() a := c.Agent() config, err := a.GetSchedulerWorkerConfig(nil) must.NoError(t, err) must.NotNil(t, config) newConfig := SchedulerWorkerPoolArgs{NumSchedulers: -1, EnabledSchedulers: []string{"_core", "system"}} _, err = a.SetSchedulerWorkerConfig(newConfig, nil) must.Error(t, err) must.ErrorContains(t, err, "400 (Invalid request)") } func TestAgent_SchedulerWorkersInfo(t *testing.T) { testutil.Parallel(t) c, s := makeClient(t, nil, nil) defer s.Stop() a := c.Agent() info, err := a.GetSchedulerWorkersInfo(nil) must.NoError(t, err) must.NotNil(t, info) defaultSchedulers := []string{"batch", "system", "sysbatch", "service", "_core"} for _, worker := range info.Schedulers { must.SliceContainsAll(t, defaultSchedulers, worker.EnabledSchedulers) } }