cli: use shared logic for resolving job prefix (#16306)
Several `nomad job` subcommands had duplicate or slightly similar logic for resolving a job ID from a CLI argument prefix, while others did not have this functionality at all. This commit pulls the shared logic to the command Meta and updates all `nomad job` subcommands to use it.
This commit is contained in:
parent
8747059b86
commit
1d051d834d
|
@ -0,0 +1,7 @@
|
|||
```release-note:improvement
|
||||
cli: Add job prefix match to the `nomad job dispatch`, `nomad job eval`, `nomad job scale`, and `nomad job scaling-events` commands
|
||||
```
|
||||
|
||||
```release-note:improvement
|
||||
cli: Add support for the wildcard namespace `*` to the `nomad job dispatch`, `nomad job eval`, `nomad job scale`, and `nomad job scaling-events` commands
|
||||
```
|
|
@ -105,27 +105,15 @@ func (c *JobAllocsCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
jobID := strings.TrimSpace(args[0])
|
||||
|
||||
// Check if the job exists
|
||||
jobs, _, err := client.Jobs().PrefixList(jobID)
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error listing jobs: %s", err))
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
|
||||
return 1
|
||||
}
|
||||
if len(jobs) > 1 {
|
||||
if (jobID != jobs[0].ID) || (c.allNamespaces() && jobs[0].ID == jobs[1].ID) {
|
||||
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs, c.allNamespaces())))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
jobID = jobs[0].ID
|
||||
q := &api.QueryOptions{Namespace: jobs[0].JobSummary.Namespace}
|
||||
q := &api.QueryOptions{Namespace: namespace}
|
||||
|
||||
allocs, _, err := client.Jobs().Allocations(jobID, all, q)
|
||||
if err != nil {
|
||||
|
|
|
@ -36,7 +36,7 @@ func TestJobAllocsCommand_Fails(t *testing.T) {
|
|||
code = cmd.Run([]string{"-address=nope", "foo"})
|
||||
outerr = ui.ErrorWriter.String()
|
||||
require.Equalf(t, 1, code, "expected exit code 1, got: %d", code)
|
||||
require.Containsf(t, outerr, "Error listing jobs", "expected failed query error, got: %s", outerr)
|
||||
require.Containsf(t, outerr, "Error querying job prefix", "expected failed query error, got: %s", outerr)
|
||||
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
|
@ -44,7 +44,7 @@ func TestJobAllocsCommand_Fails(t *testing.T) {
|
|||
code = cmd.Run([]string{"-address=" + url, "foo"})
|
||||
outerr = ui.ErrorWriter.String()
|
||||
require.Equalf(t, 1, code, "expected exit 1, got: %d", code)
|
||||
require.Containsf(t, outerr, "No job(s) with prefix or id \"foo\" found", "expected no job found, got: %s", outerr)
|
||||
require.Containsf(t, outerr, "No job(s) with prefix or ID \"foo\" found", "expected no job found, got: %s", outerr)
|
||||
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
||||
|
|
|
@ -110,27 +110,15 @@ func (c *JobDeploymentsCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
jobID := strings.TrimSpace(args[0])
|
||||
|
||||
// Check if the job exists
|
||||
jobs, _, err := client.Jobs().PrefixList(jobID)
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error listing jobs: %s", err))
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
|
||||
return 1
|
||||
}
|
||||
if len(jobs) > 1 {
|
||||
if (jobID != jobs[0].ID) || (c.allNamespaces() && jobs[0].ID == jobs[1].ID) {
|
||||
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs, c.allNamespaces())))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
jobID = jobs[0].ID
|
||||
q := &api.QueryOptions{Namespace: jobs[0].JobSummary.Namespace}
|
||||
q := &api.QueryOptions{Namespace: namespace}
|
||||
|
||||
// Truncate the id unless full length is requested
|
||||
length := shortId
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestJobDeploymentsCommand_Fails(t *testing.T) {
|
|||
if code := cmd.Run([]string{"-address=nope", "foo"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error listing jobs") {
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying job prefix") {
|
||||
t.Fatalf("expected failed query error, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
|
|
@ -138,7 +138,6 @@ func (c *JobDispatchCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
job := args[0]
|
||||
var payload []byte
|
||||
var readErr error
|
||||
|
||||
|
@ -175,11 +174,22 @@ func (c *JobDispatchCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
// Check if the job exists
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, func(j *api.JobListStub) bool {
|
||||
return j.ParameterizedJob
|
||||
})
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Dispatch the job
|
||||
w := &api.WriteOptions{
|
||||
IdempotencyToken: idempotencyToken,
|
||||
Namespace: namespace,
|
||||
}
|
||||
resp, _, err := client.Jobs().Dispatch(job, metaMap, payload, idPrefixTemplate, w)
|
||||
resp, _, err := client.Jobs().Dispatch(jobID, metaMap, payload, idPrefixTemplate, w)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to dispatch job: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -43,7 +43,7 @@ func TestJobDispatchCommand_Fails(t *testing.T) {
|
|||
if code := cmd.Run([]string{"-address=nope", "foo"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Failed to dispatch") {
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying job prefix") {
|
||||
t.Fatalf("expected failed query error, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
|
|
@ -110,13 +110,23 @@ func (c *JobEvalCommand) Run(args []string) int {
|
|||
if verbose {
|
||||
length = fullId
|
||||
}
|
||||
// Call eval endpoint
|
||||
jobID := args[0]
|
||||
|
||||
// Check if the job exists
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Call eval endpoint
|
||||
opts := api.EvalOptions{
|
||||
ForceReschedule: c.forceRescheduling,
|
||||
}
|
||||
evalId, _, err := client.Jobs().EvaluateWithOpts(jobID, opts, nil)
|
||||
w := &api.WriteOptions{
|
||||
Namespace: namespace,
|
||||
}
|
||||
evalId, _, err := client.Jobs().EvaluateWithOpts(jobID, opts, w)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error evaluating job: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -121,29 +121,18 @@ func (c *JobHistoryCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
jobID := strings.TrimSpace(args[0])
|
||||
|
||||
// Check if the job exists
|
||||
jobs, _, err := client.Jobs().PrefixList(jobID)
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error listing jobs: %s", err))
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
|
||||
return 1
|
||||
}
|
||||
if len(jobs) > 1 {
|
||||
if (jobID != jobs[0].ID) || (c.allNamespaces() && jobs[0].ID == jobs[1].ID) {
|
||||
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs, c.allNamespaces())))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
q := &api.QueryOptions{Namespace: jobs[0].JobSummary.Namespace}
|
||||
q := &api.QueryOptions{Namespace: namespace}
|
||||
|
||||
// Prefix lookup matched a single job
|
||||
versions, diffs, _, err := client.Jobs().Versions(jobs[0].ID, diff, q)
|
||||
versions, diffs, _, err := client.Jobs().Versions(jobID, diff, q)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error retrieving job versions: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestJobHistoryCommand_Fails(t *testing.T) {
|
|||
if code := cmd.Run([]string{"-address=nope", "foo"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error listing jobs") {
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying job prefix") {
|
||||
t.Fatalf("expected failed query error, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
|
|
@ -117,24 +117,14 @@ func (c *JobInspectCommand) Run(args []string) int {
|
|||
c.Ui.Error(commandErrorText(c))
|
||||
return 1
|
||||
}
|
||||
jobID := strings.TrimSpace(args[0])
|
||||
|
||||
// Check if the job exists
|
||||
jobs, _, err := client.Jobs().PrefixList(jobID)
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err))
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
|
||||
return 1
|
||||
}
|
||||
if len(jobs) > 1 {
|
||||
if (jobID != jobs[0].ID) || (c.allNamespaces() && jobs[0].ID == jobs[1].ID) {
|
||||
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs, c.allNamespaces())))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
var version *uint64
|
||||
if versionStr != "" {
|
||||
|
@ -148,7 +138,7 @@ func (c *JobInspectCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Prefix lookup matched a single job
|
||||
job, err := getJob(client, jobs[0].JobSummary.Namespace, jobs[0].ID, version)
|
||||
job, err := getJob(client, namespace, jobID, version)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -38,7 +38,7 @@ func TestInspectCommand_Fails(t *testing.T) {
|
|||
if code := cmd.Run([]string{"-address=" + url, "nope"}); code != 1 {
|
||||
t.Fatalf("expect exit 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "No job(s) with prefix or id") {
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "No job(s) with prefix or ID") {
|
||||
t.Fatalf("expect not found error, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
@ -47,7 +47,7 @@ func TestInspectCommand_Fails(t *testing.T) {
|
|||
if code := cmd.Run([]string{"-address=nope", "nope"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error inspecting job") {
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying job prefix") {
|
||||
t.Fatalf("expected failed query error, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
@ -112,31 +113,19 @@ func (c *JobPeriodicForceCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Check if the job exists
|
||||
jobID := args[0]
|
||||
jobs, _, err := client.Jobs().PrefixList(jobID)
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, func(j *api.JobListStub) bool {
|
||||
return j.Periodic
|
||||
})
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error forcing periodic job: %s", err))
|
||||
var noPrefixErr *NoJobWithPrefixError
|
||||
if errors.As(err, &noPrefixErr) {
|
||||
err = fmt.Errorf("No periodic job(s) with prefix or ID %q found", jobIDPrefix)
|
||||
}
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
// filter non-periodic jobs
|
||||
periodicJobs := make([]*api.JobListStub, 0, len(jobs))
|
||||
for _, j := range jobs {
|
||||
if j.Periodic {
|
||||
periodicJobs = append(periodicJobs, j)
|
||||
}
|
||||
}
|
||||
if len(periodicJobs) == 0 {
|
||||
c.Ui.Error(fmt.Sprintf("No periodic job(s) with prefix or id %q found", jobID))
|
||||
return 1
|
||||
}
|
||||
// preriodicJobs is sorted by job ID
|
||||
// so if there is a job whose ID is equal to jobID then it must be the first item
|
||||
if len(periodicJobs) > 1 && periodicJobs[0].ID != jobID {
|
||||
c.Ui.Error(fmt.Sprintf("Prefix matched multiple periodic jobs\n\n%s", createStatusListOutput(periodicJobs, c.allNamespaces())))
|
||||
return 1
|
||||
}
|
||||
jobID = periodicJobs[0].ID
|
||||
q := &api.WriteOptions{Namespace: periodicJobs[0].JobSummary.Namespace}
|
||||
q := &api.WriteOptions{Namespace: namespace}
|
||||
|
||||
// force the evaluation
|
||||
evalID, _, err := client.Jobs().PeriodicForce(jobID, q)
|
||||
|
|
|
@ -35,7 +35,7 @@ func TestJobPeriodicForceCommand_Fails(t *testing.T) {
|
|||
code = cmd.Run([]string{"-address=nope", "12"})
|
||||
require.Equal(t, code, 1, "expected error")
|
||||
out = ui.ErrorWriter.String()
|
||||
require.Contains(t, out, "Error forcing periodic job", "expected force error")
|
||||
require.Contains(t, out, "Error querying job prefix", "expected force error")
|
||||
}
|
||||
|
||||
func TestJobPeriodicForceCommand_AutocompleteArgs(t *testing.T) {
|
||||
|
|
|
@ -117,24 +117,13 @@ func (c *JobPromoteCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Check if the job exists
|
||||
jobID := strings.TrimSpace(args[0])
|
||||
jobs, _, err := client.Jobs().PrefixList(jobID)
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error promoting job: %s", err))
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
|
||||
return 1
|
||||
}
|
||||
if len(jobs) > 1 {
|
||||
if (jobID != jobs[0].ID) || (c.allNamespaces() && jobs[0].ID == jobs[1].ID) {
|
||||
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs, c.allNamespaces())))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
jobID = jobs[0].ID
|
||||
q := &api.QueryOptions{Namespace: jobs[0].JobSummary.Namespace}
|
||||
q := &api.QueryOptions{Namespace: namespace}
|
||||
|
||||
// Do a prefix lookup
|
||||
deploy, _, err := client.Jobs().LatestDeployment(jobID, q)
|
||||
|
@ -148,7 +137,7 @@ func (c *JobPromoteCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
wq := &api.WriteOptions{Namespace: jobs[0].JobSummary.Namespace}
|
||||
wq := &api.WriteOptions{Namespace: namespace}
|
||||
var u *api.DeploymentUpdateResponse
|
||||
if len(groups) == 0 {
|
||||
u, _, err = client.Deployments().PromoteAll(deploy.ID, wq)
|
||||
|
|
|
@ -35,7 +35,7 @@ func TestJobPromoteCommand_Fails(t *testing.T) {
|
|||
if code := cmd.Run([]string{"-address=nope", "12"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error promoting") {
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying job prefix") {
|
||||
t.Fatalf("expected failed to promote error, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
|
|
@ -126,7 +126,7 @@ func (c *JobRevertCommand) Run(args []string) int {
|
|||
vaultToken = os.Getenv("VAULT_TOKEN")
|
||||
}
|
||||
|
||||
jobID := strings.TrimSpace(args[0])
|
||||
// Parse the job version
|
||||
revertVersion, ok, err := parseVersion(args[1])
|
||||
if !ok {
|
||||
c.Ui.Error("The job version to revert to must be specified using the -job-version flag")
|
||||
|
@ -138,25 +138,16 @@ func (c *JobRevertCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Check if the job exists
|
||||
jobs, _, err := client.Jobs().PrefixList(jobID)
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error listing jobs: %s", err))
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
|
||||
return 1
|
||||
}
|
||||
if len(jobs) > 1 {
|
||||
if (jobID != jobs[0].ID) || (c.allNamespaces() && jobs[0].ID == jobs[1].ID) {
|
||||
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs, c.allNamespaces())))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix lookup matched a single job
|
||||
q := &api.WriteOptions{Namespace: jobs[0].JobSummary.Namespace}
|
||||
resp, _, err := client.Jobs().Revert(jobs[0].ID, revertVersion, nil, q, consulToken, vaultToken)
|
||||
q := &api.WriteOptions{Namespace: namespace}
|
||||
resp, _, err := client.Jobs().Revert(jobID, revertVersion, nil, q, consulToken, vaultToken)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error retrieving job versions: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestJobRevertCommand_Fails(t *testing.T) {
|
|||
if code := cmd.Run([]string{"-address=nope", "foo", "1"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error listing jobs") {
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying job prefix") {
|
||||
t.Fatalf("expected failed query error, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
|
|
@ -80,7 +80,7 @@ func (j *JobScaleCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
var jobString, countString, groupString string
|
||||
var countString, groupString string
|
||||
args = flags.Args()
|
||||
|
||||
// It is possible to specify either 2 or 3 arguments. Check and assign the
|
||||
|
@ -94,7 +94,6 @@ func (j *JobScaleCommand) Run(args []string) int {
|
|||
} else {
|
||||
countString = args[1]
|
||||
}
|
||||
jobString = args[0]
|
||||
|
||||
// Convert the count string arg to an int as required by the API.
|
||||
count, err := strconv.Atoi(countString)
|
||||
|
@ -110,9 +109,18 @@ func (j *JobScaleCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
// Check if the job exists
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := j.JobIDByPrefix(client, jobIDPrefix, nil)
|
||||
if err != nil {
|
||||
j.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Detail the job so we can perform addition checks before submitting the
|
||||
// scaling request.
|
||||
job, _, err := client.Jobs().ScaleStatus(jobString, nil)
|
||||
q := &api.QueryOptions{Namespace: namespace}
|
||||
job, _, err := client.Jobs().ScaleStatus(jobID, q)
|
||||
if err != nil {
|
||||
j.Ui.Error(fmt.Sprintf("Error querying job: %v", err))
|
||||
return 1
|
||||
|
@ -127,7 +135,8 @@ func (j *JobScaleCommand) Run(args []string) int {
|
|||
msg := "submitted using the Nomad CLI"
|
||||
|
||||
// Perform the scaling action.
|
||||
resp, _, err := client.Jobs().Scale(jobString, groupString, &count, msg, false, nil, nil)
|
||||
w := &api.WriteOptions{Namespace: namespace}
|
||||
resp, _, err := client.Jobs().Scale(jobID, groupString, &count, msg, false, nil, w)
|
||||
if err != nil {
|
||||
j.Ui.Error(fmt.Sprintf("Error submitting scaling request: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -76,9 +76,6 @@ func (j *JobScalingEventsCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
// Get the job ID.
|
||||
jobID := args[0]
|
||||
|
||||
// Get the HTTP client.
|
||||
client, err := j.Meta.Client()
|
||||
if err != nil {
|
||||
|
@ -86,7 +83,16 @@ func (j *JobScalingEventsCommand) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
|
||||
events, _, err := client.Jobs().ScaleStatus(jobID, nil)
|
||||
// Check if the job exists
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := j.JobIDByPrefix(client, jobIDPrefix, nil)
|
||||
if err != nil {
|
||||
j.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
q := &api.QueryOptions{Namespace: namespace}
|
||||
events, _, err := client.Jobs().ScaleStatus(jobID, q)
|
||||
if err != nil {
|
||||
j.Ui.Error(fmt.Sprintf("Error listing scaling events: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -145,27 +145,16 @@ func (c *JobStatusCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Try querying the job
|
||||
jobID := strings.TrimSpace(args[0])
|
||||
|
||||
jobs, _, err := client.Jobs().PrefixList(jobID)
|
||||
jobIDPrefix := strings.TrimSpace(args[0])
|
||||
jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error querying job: %s", err))
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
|
||||
return 1
|
||||
}
|
||||
if len(jobs) > 1 {
|
||||
if (jobID != jobs[0].ID) || (allNamespaces && jobs[0].ID == jobs[1].ID) {
|
||||
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs, allNamespaces)))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix lookup matched a single job
|
||||
q := &api.QueryOptions{Namespace: jobs[0].JobSummary.Namespace}
|
||||
job, _, err := client.Jobs().Info(jobs[0].ID, q)
|
||||
q := &api.QueryOptions{Namespace: namespace}
|
||||
job, _, err := client.Jobs().Info(jobID, q)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error querying job: %s", err))
|
||||
return 1
|
||||
|
|
|
@ -156,30 +156,9 @@ func (c *JobStopCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Check if the job exists
|
||||
jobs, _, err := client.Jobs().PrefixList(jobID)
|
||||
job, err := c.JobByPrefix(client, jobID, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error finding jobs with prefix: %s err: %s", jobID, err))
|
||||
statusCh <- 1
|
||||
return
|
||||
}
|
||||
if len(jobs) == 0 {
|
||||
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
|
||||
statusCh <- 1
|
||||
return
|
||||
}
|
||||
if len(jobs) > 1 {
|
||||
if (jobID != jobs[0].ID) || (c.allNamespaces() && jobs[0].ID == jobs[1].ID) {
|
||||
c.Ui.Error(fmt.Sprintf("Prefix %q matched multiple jobs\n\n%s", jobID, createStatusListOutput(jobs, c.allNamespaces())))
|
||||
statusCh <- 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix lookup matched a single job
|
||||
q := &api.QueryOptions{Namespace: jobs[0].JobSummary.Namespace}
|
||||
job, _, err := client.Jobs().Info(jobs[0].ID, q)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error deregistering job with id %s err: %s", jobID, err))
|
||||
c.Ui.Error(err.Error())
|
||||
statusCh <- 1
|
||||
return
|
||||
}
|
||||
|
@ -233,7 +212,7 @@ func (c *JobStopCommand) Run(args []string) int {
|
|||
|
||||
// Invoke the stop
|
||||
opts := &api.DeregisterOptions{Purge: purge, Global: global, EvalPriority: evalPriority, NoShutdownDelay: noShutdownDelay}
|
||||
wq := &api.WriteOptions{Namespace: jobs[0].JobSummary.Namespace}
|
||||
wq := &api.WriteOptions{Namespace: *job.Namespace}
|
||||
evalID, _, err := client.Jobs().DeregisterOpts(*job.ID, opts, wq)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error deregistering job with id %s err: %s", jobID, err))
|
||||
|
|
|
@ -114,7 +114,7 @@ func TestStopCommand_Fails(t *testing.T) {
|
|||
must.One(t, code)
|
||||
|
||||
out = ui.ErrorWriter.String()
|
||||
must.StrContains(t, out, "No job(s) with prefix or id")
|
||||
must.StrContains(t, out, "No job(s) with prefix or ID")
|
||||
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
|
@ -123,7 +123,7 @@ func TestStopCommand_Fails(t *testing.T) {
|
|||
must.One(t, code)
|
||||
|
||||
out = ui.ErrorWriter.String()
|
||||
must.StrContains(t, out, "Error finding jobs with prefix: nope")
|
||||
must.StrContains(t, out, "Error querying job prefix")
|
||||
}
|
||||
|
||||
func TestStopCommand_AutocompleteArgs(t *testing.T) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
colorable "github.com/mattn/go-colorable"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/colorstring"
|
||||
|
@ -226,6 +227,77 @@ func (m *Meta) FormatWarnings(header string, warnings string) string {
|
|||
))
|
||||
}
|
||||
|
||||
// JobByPrefixFilterFunc is a function used to filter jobs when performing a
|
||||
// prefix match. Only jobs that return true are included in the prefix match.
|
||||
type JobByPrefixFilterFunc func(*api.JobListStub) bool
|
||||
|
||||
// NoJobWithPrefixError is the error returned when the job prefix doesn't
|
||||
// return any matches.
|
||||
type NoJobWithPrefixError struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (e *NoJobWithPrefixError) Error() string {
|
||||
return fmt.Sprintf("No job(s) with prefix or ID %q found", e.Prefix)
|
||||
}
|
||||
|
||||
// JobByPrefix returns the job that best matches the given prefix. Returns an
|
||||
// error if there are no matches or if there are more than one exact match
|
||||
// across namespaces.
|
||||
func (m *Meta) JobByPrefix(client *api.Client, prefix string, filter JobByPrefixFilterFunc) (*api.Job, error) {
|
||||
jobID, namespace, err := m.JobIDByPrefix(client, prefix, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := &api.QueryOptions{Namespace: namespace}
|
||||
job, _, err := client.Jobs().Info(jobID, q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error querying job %q: %s", jobID, err)
|
||||
}
|
||||
job.Namespace = pointer.Of(namespace)
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// JobIDByPrefix returns the job that best matches the given prefix and its
|
||||
// namespace. Returns an error if there are no matches or if there are more
|
||||
// than one exact match across namespaces.
|
||||
func (m *Meta) JobIDByPrefix(client *api.Client, prefix string, filter JobByPrefixFilterFunc) (string, string, error) {
|
||||
// Search job by prefix. Return an error if there is not an exact match.
|
||||
jobs, _, err := client.Jobs().PrefixList(prefix)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Error querying job prefix %q: %s", prefix, err)
|
||||
}
|
||||
|
||||
if filter != nil {
|
||||
var filtered []*api.JobListStub
|
||||
for _, j := range jobs {
|
||||
if filter(j) {
|
||||
filtered = append(filtered, j)
|
||||
}
|
||||
}
|
||||
jobs = filtered
|
||||
}
|
||||
|
||||
if len(jobs) == 0 {
|
||||
return "", "", &NoJobWithPrefixError{Prefix: prefix}
|
||||
}
|
||||
if len(jobs) > 1 {
|
||||
exactMatch := prefix == jobs[0].ID
|
||||
matchInMultipleNamespaces := m.allNamespaces() && jobs[0].ID == jobs[1].ID
|
||||
if !exactMatch || matchInMultipleNamespaces {
|
||||
return "", "", fmt.Errorf(
|
||||
"Prefix %q matched multiple jobs\n\n%s",
|
||||
prefix,
|
||||
createStatusListOutput(jobs, m.allNamespaces()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return jobs[0].ID, jobs[0].JobSummary.Namespace, nil
|
||||
}
|
||||
|
||||
type usageOptsFlags uint8
|
||||
|
||||
const (
|
||||
|
|
|
@ -8,8 +8,11 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -153,3 +156,95 @@ func TestMeta_Colorize(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue