mirror of
https://github.com/kemko/nomad.git
synced 2026-01-02 00:15:43 +03:00
* Basic implementation for server members and node status * Commands for alloc status and job status * -ui flag for most commands * url hints for variables * url hints for job dispatch, evals, and deployments * agent config ui.cli_url_links to disable * Fix an issue where path prefix was presumed for variables * driver uncomment and general cleanup * -ui flag on the generic status endpoint * Job run command gets namespaces, and no longer gets ui hints for --output flag * Dispatch command hints get a namespace, and bunch o tests * Lots of tests depend on specific output, so let's not mess with them * figured out what flagAddress is all about for testServer, oof * Parallel outside of test instances * Browser-opening test, sorta * Env var for disabling/enabling CLI hints * Addressing a few PR comments * CLI docs available flags now all have -ui * PR comments addressed; switched the env var to be consistent and scrunched monitor-adjacent hints a bit more * ui.Output -> ui.Warn; moves hints from stdout to stderr * isTerminal check and parseBool on command option * terminal.IsTerminal check removed for test-runner-not-being-terminal reasons
597 lines
13 KiB
Go
597 lines
13 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/creack/pty"
|
|
"github.com/hashicorp/cli"
|
|
"github.com/hashicorp/nomad/api"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/command/agent"
|
|
"github.com/hashicorp/nomad/helper/pointer"
|
|
"github.com/shoenig/test/must"
|
|
)
|
|
|
|
func TestMeta_FlagSet(t *testing.T) {
|
|
ci.Parallel(t)
|
|
cases := []struct {
|
|
Flags FlagSetFlags
|
|
Expected []string
|
|
}{
|
|
{
|
|
FlagSetNone,
|
|
[]string{},
|
|
},
|
|
{
|
|
FlagSetClient,
|
|
[]string{
|
|
"address",
|
|
"no-color",
|
|
"force-color",
|
|
"region",
|
|
"namespace",
|
|
"ca-cert",
|
|
"ca-path",
|
|
"client-cert",
|
|
"client-key",
|
|
"insecure",
|
|
"tls-server-name",
|
|
"tls-skip-verify",
|
|
"token",
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
var m Meta
|
|
fs := m.FlagSet("foo", tc.Flags)
|
|
|
|
actual := make([]string, 0, 0)
|
|
fs.VisitAll(func(f *flag.Flag) {
|
|
actual = append(actual, f.Name)
|
|
})
|
|
sort.Strings(actual)
|
|
sort.Strings(tc.Expected)
|
|
|
|
if !reflect.DeepEqual(actual, tc.Expected) {
|
|
t.Fatalf("%d: flags: %#v\n\nExpected: %#v\nGot: %#v",
|
|
i, tc.Flags, tc.Expected, actual)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMeta_Colorize(t *testing.T) {
|
|
|
|
type testCaseSetupFn func(*testing.T, *Meta)
|
|
|
|
cases := []struct {
|
|
Name string
|
|
SetupFn testCaseSetupFn
|
|
ExpectColor bool
|
|
}{
|
|
{
|
|
Name: "disable colors if UI is not colored",
|
|
ExpectColor: false,
|
|
},
|
|
{
|
|
Name: "colors if UI is colored",
|
|
SetupFn: func(t *testing.T, m *Meta) {
|
|
m.Ui = &cli.ColoredUi{}
|
|
},
|
|
ExpectColor: true,
|
|
},
|
|
{
|
|
Name: "disable colors via CLI flag",
|
|
SetupFn: func(t *testing.T, m *Meta) {
|
|
m.SetupUi([]string{"-no-color"})
|
|
},
|
|
ExpectColor: false,
|
|
},
|
|
{
|
|
Name: "disable colors via env var",
|
|
SetupFn: func(t *testing.T, m *Meta) {
|
|
t.Setenv(EnvNomadCLINoColor, "1")
|
|
m.SetupUi([]string{})
|
|
},
|
|
ExpectColor: false,
|
|
},
|
|
{
|
|
Name: "force colors via CLI flag",
|
|
SetupFn: func(t *testing.T, m *Meta) {
|
|
m.SetupUi([]string{"-force-color"})
|
|
},
|
|
ExpectColor: true,
|
|
},
|
|
{
|
|
Name: "force colors via env var",
|
|
SetupFn: func(t *testing.T, m *Meta) {
|
|
t.Setenv(EnvNomadCLIForceColor, "1")
|
|
m.SetupUi([]string{})
|
|
},
|
|
ExpectColor: true,
|
|
},
|
|
{
|
|
Name: "no color take predecence over force color via CLI flag",
|
|
SetupFn: func(t *testing.T, m *Meta) {
|
|
m.SetupUi([]string{"-no-color", "-force-color"})
|
|
},
|
|
ExpectColor: false,
|
|
},
|
|
{
|
|
Name: "no color take predecence over force color via env var",
|
|
SetupFn: func(t *testing.T, m *Meta) {
|
|
t.Setenv(EnvNomadCLINoColor, "1")
|
|
m.SetupUi([]string{"-force-color"})
|
|
},
|
|
ExpectColor: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
// Create fake test terminal.
|
|
_, tty, err := pty.Open()
|
|
must.NoError(t, err)
|
|
defer tty.Close()
|
|
|
|
oldStdout := os.Stdout
|
|
defer func() { os.Stdout = oldStdout }()
|
|
os.Stdout = tty
|
|
|
|
// Make sure color related environment variables are clean.
|
|
t.Setenv(EnvNomadCLIForceColor, "")
|
|
t.Setenv(EnvNomadCLINoColor, "")
|
|
|
|
// Run test case.
|
|
m := &Meta{}
|
|
if tc.SetupFn != nil {
|
|
tc.SetupFn(t, m)
|
|
}
|
|
|
|
must.Eq(t, !tc.ExpectColor, m.Colorize().Disable)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMeta_JobByPrefix(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
srv, client, _ := testServer(t, true, nil)
|
|
defer srv.Shutdown()
|
|
|
|
// Wait for a node to be ready
|
|
waitForNodes(t, client)
|
|
|
|
ui := cli.NewMockUi()
|
|
meta := &Meta{Ui: ui, namespace: api.AllNamespacesNamespace}
|
|
client.SetNamespace(api.AllNamespacesNamespace)
|
|
|
|
jobs := []struct {
|
|
namespace string
|
|
id string
|
|
}{
|
|
{namespace: "default", id: "example"},
|
|
{namespace: "default", id: "job"},
|
|
{namespace: "default", id: "job-1"},
|
|
{namespace: "default", id: "job-2"},
|
|
{namespace: "prod", id: "job-1"},
|
|
}
|
|
for _, j := range jobs {
|
|
job := testJob(j.id)
|
|
job.Namespace = pointer.Of(j.namespace)
|
|
|
|
_, err := client.Namespaces().Register(&api.Namespace{Name: j.namespace}, nil)
|
|
must.NoError(t, err)
|
|
|
|
w := &api.WriteOptions{Namespace: j.namespace}
|
|
resp, _, err := client.Jobs().Register(job, w)
|
|
must.NoError(t, err)
|
|
|
|
code := waitForSuccess(ui, client, fullId, t, resp.EvalID)
|
|
must.Zero(t, code)
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
prefix string
|
|
filterFunc JobByPrefixFilterFunc
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "exact match",
|
|
prefix: "job",
|
|
},
|
|
{
|
|
name: "partial match",
|
|
prefix: "exam",
|
|
},
|
|
{
|
|
name: "match with filter",
|
|
prefix: "job-",
|
|
filterFunc: func(j *api.JobListStub) bool {
|
|
// Filter out jobs with "job-" so that only "job-2" matches.
|
|
return j.ID == "job-2"
|
|
},
|
|
},
|
|
{
|
|
name: "multiple matches",
|
|
prefix: "job-",
|
|
expectedError: "matched multiple jobs",
|
|
},
|
|
{
|
|
name: "no match",
|
|
prefix: "not-found",
|
|
expectedError: "No job(s) with prefix or ID",
|
|
},
|
|
{
|
|
name: "multiple matches across namespaces",
|
|
prefix: "job-1",
|
|
expectedError: "matched multiple jobs",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
job, err := meta.JobByPrefix(client, tc.prefix, tc.filterFunc)
|
|
if tc.expectedError != "" {
|
|
must.Nil(t, job)
|
|
must.ErrorContains(t, err, tc.expectedError)
|
|
} else {
|
|
must.NoError(t, err)
|
|
must.NotNil(t, job)
|
|
must.StrContains(t, *job.ID, tc.prefix)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMeta_ShowUIPath(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// Create a test server with UI enabled but CLI URL links disabled
|
|
server, client, url := testServer(t, true, func(c *agent.Config) {
|
|
c.UI.ShowCLIHints = pointer.Of(true)
|
|
})
|
|
defer server.Shutdown()
|
|
waitForNodes(t, client)
|
|
|
|
m := &Meta{
|
|
Ui: cli.NewMockUi(),
|
|
flagAddress: url,
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
context UIHintContext
|
|
expectedURL string
|
|
expectedOpened bool
|
|
}{
|
|
{
|
|
name: "server members",
|
|
context: UIHintContext{
|
|
Command: "server members",
|
|
},
|
|
expectedURL: url + "/ui/servers",
|
|
},
|
|
{
|
|
name: "node status (many)",
|
|
context: UIHintContext{
|
|
Command: "node status",
|
|
},
|
|
expectedURL: url + "/ui/clients",
|
|
},
|
|
{
|
|
name: "node status (single)",
|
|
context: UIHintContext{
|
|
Command: "node status single",
|
|
PathParams: map[string]string{
|
|
"nodeID": "node-1",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/clients/node-1",
|
|
},
|
|
{
|
|
name: "job status (many)",
|
|
context: UIHintContext{
|
|
Command: "job status",
|
|
},
|
|
expectedURL: url + "/ui/jobs",
|
|
},
|
|
{
|
|
name: "job status (single)",
|
|
context: UIHintContext{
|
|
Command: "job status single",
|
|
PathParams: map[string]string{
|
|
"jobID": "example-job",
|
|
"namespace": "default",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/jobs/example-job@default",
|
|
},
|
|
{
|
|
name: "job run (default ns)",
|
|
context: UIHintContext{
|
|
Command: "job run",
|
|
PathParams: map[string]string{
|
|
"jobID": "example-job",
|
|
"namespace": "default",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/jobs/example-job@default",
|
|
},
|
|
{
|
|
name: "job run (non-default ns)",
|
|
context: UIHintContext{
|
|
Command: "job run",
|
|
PathParams: map[string]string{
|
|
"jobID": "example-job",
|
|
"namespace": "prod",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/jobs/example-job@prod",
|
|
},
|
|
{
|
|
name: "job dispatch (default ns)",
|
|
context: UIHintContext{
|
|
Command: "job dispatch",
|
|
PathParams: map[string]string{
|
|
"dispatchID": "dispatch-1",
|
|
"namespace": "default",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/jobs/dispatch-1@default",
|
|
},
|
|
{
|
|
name: "job dispatch (non-default ns)",
|
|
context: UIHintContext{
|
|
Command: "job dispatch",
|
|
PathParams: map[string]string{
|
|
"dispatchID": "dispatch-1",
|
|
"namespace": "toronto",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/jobs/dispatch-1@toronto",
|
|
},
|
|
{
|
|
name: "eval list",
|
|
context: UIHintContext{
|
|
Command: "eval list",
|
|
},
|
|
expectedURL: url + "/ui/evaluations",
|
|
},
|
|
{
|
|
name: "eval status",
|
|
context: UIHintContext{
|
|
Command: "eval status",
|
|
PathParams: map[string]string{
|
|
"evalID": "eval-1",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/evaluations?currentEval=eval-1",
|
|
},
|
|
{
|
|
name: "deployment status",
|
|
context: UIHintContext{
|
|
Command: "deployment status",
|
|
PathParams: map[string]string{
|
|
"jobID": "example-job",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/jobs/example-job/deployments",
|
|
},
|
|
{
|
|
name: "var list (root)",
|
|
context: UIHintContext{
|
|
Command: "var list",
|
|
},
|
|
expectedURL: url + "/ui/variables",
|
|
},
|
|
{
|
|
name: "var list (path)",
|
|
context: UIHintContext{
|
|
Command: "var list prefix",
|
|
PathParams: map[string]string{
|
|
"prefix": "foo",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/variables/path/foo",
|
|
},
|
|
{
|
|
name: "var get",
|
|
context: UIHintContext{
|
|
Command: "var get",
|
|
PathParams: map[string]string{
|
|
"path": "foo",
|
|
"namespace": "default",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/variables/var/foo@default",
|
|
},
|
|
{
|
|
name: "var put",
|
|
context: UIHintContext{
|
|
Command: "var put",
|
|
PathParams: map[string]string{
|
|
"path": "foo",
|
|
"namespace": "default",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/variables/var/foo@default",
|
|
},
|
|
{
|
|
name: "alloc status",
|
|
context: UIHintContext{
|
|
Command: "alloc status",
|
|
PathParams: map[string]string{
|
|
"allocID": "alloc-1",
|
|
},
|
|
},
|
|
expectedURL: url + "/ui/allocations/alloc-1",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
route := CommandUIRoutes[tc.context.Command]
|
|
expectedHint := fmt.Sprintf("\n\n==> %s in the Web UI: %s", route.Description, tc.expectedURL)
|
|
|
|
hint, err := m.showUIPath(tc.context)
|
|
must.NoError(t, err)
|
|
must.Eq(t, expectedHint, hint)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMeta_ShowUIPath_ShowCLIHintsEnabled(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// Create a test server with UI enabled and CLI URL links enabled
|
|
server, client, url := testServer(t, true, func(c *agent.Config) {
|
|
c.UI.ShowCLIHints = pointer.Of(true)
|
|
})
|
|
defer server.Shutdown()
|
|
waitForNodes(t, client)
|
|
|
|
m := &Meta{
|
|
Ui: cli.NewMockUi(),
|
|
flagAddress: url,
|
|
}
|
|
|
|
hint, err := m.showUIPath(UIHintContext{
|
|
Command: "job status",
|
|
})
|
|
must.NoError(t, err)
|
|
|
|
must.StrContains(t, hint, url+"/ui/jobs")
|
|
}
|
|
|
|
func TestMeta_ShowUIPath_ShowCLIHintsDisabled(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// Create a test server with UI enabled and CLI URL links disabled
|
|
server, client, url := testServer(t, true, func(c *agent.Config) {
|
|
c.UI.ShowCLIHints = pointer.Of(false)
|
|
})
|
|
defer server.Shutdown()
|
|
waitForNodes(t, client)
|
|
|
|
m := &Meta{
|
|
Ui: cli.NewMockUi(),
|
|
flagAddress: url,
|
|
}
|
|
|
|
hint, err := m.showUIPath(UIHintContext{
|
|
Command: "job status",
|
|
})
|
|
must.NoError(t, err)
|
|
|
|
must.StrNotContains(t, hint, url+"/ui/jobs")
|
|
}
|
|
|
|
func TestMeta_ShowUIPath_EnvVarOverride(t *testing.T) {
|
|
|
|
testCases := []struct {
|
|
name string
|
|
envValue string
|
|
serverEnabled bool
|
|
expectHints bool
|
|
}{
|
|
{
|
|
name: "env var true overrides server false",
|
|
envValue: "true",
|
|
serverEnabled: false,
|
|
expectHints: true,
|
|
},
|
|
{
|
|
name: "env var false overrides server true",
|
|
envValue: "false",
|
|
serverEnabled: true,
|
|
expectHints: false,
|
|
},
|
|
{
|
|
name: "empty env var falls back to server true",
|
|
envValue: "",
|
|
serverEnabled: true,
|
|
expectHints: true,
|
|
},
|
|
{
|
|
name: "empty env var falls back to server false",
|
|
envValue: "",
|
|
serverEnabled: false,
|
|
expectHints: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc // capture range variable
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
// Set environment variable
|
|
if tc.envValue != "" {
|
|
t.Setenv("NOMAD_CLI_SHOW_HINTS", tc.envValue)
|
|
}
|
|
|
|
// Create a test server with UI enabled and CLI hints as per test case
|
|
server, client, url := testServer(t, true, func(c *agent.Config) {
|
|
c.UI.ShowCLIHints = pointer.Of(tc.serverEnabled)
|
|
})
|
|
defer server.Shutdown()
|
|
waitForNodes(t, client)
|
|
|
|
m := &Meta{
|
|
Ui: cli.NewMockUi(),
|
|
flagAddress: url,
|
|
}
|
|
m.SetupUi([]string{})
|
|
|
|
hint, err := m.showUIPath(UIHintContext{
|
|
Command: "job status",
|
|
})
|
|
must.NoError(t, err)
|
|
|
|
if tc.expectHints {
|
|
must.StrContains(t, hint, url+"/ui/jobs")
|
|
} else {
|
|
must.StrNotContains(t, hint, url+"/ui/jobs")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMeta_ShowUIPath_BrowserOpening(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
server, client, url := testServer(t, true, func(c *agent.Config) {
|
|
c.UI.ShowCLIHints = pointer.Of(true)
|
|
})
|
|
defer server.Shutdown()
|
|
waitForNodes(t, client)
|
|
|
|
ui := cli.NewMockUi()
|
|
|
|
m := &Meta{
|
|
Ui: ui,
|
|
flagAddress: url,
|
|
}
|
|
|
|
hint, err := m.showUIPath(UIHintContext{
|
|
Command: "job status",
|
|
OpenURL: true,
|
|
})
|
|
must.NoError(t, err)
|
|
must.StrContains(t, hint, url+"/ui/jobs")
|
|
|
|
// Not a perfect test, but it's a start: make sure showUIPath isn't warning about
|
|
// being unable to open the browser.
|
|
must.StrNotContains(t, ui.ErrorWriter.String(), "Failed to open browser")
|
|
}
|