github.com/hernad/nomad@v1.6.112/command/job_stop_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"encoding/json"
     8  	"os"
     9  	"path/filepath"
    10  	"testing"
    11  
    12  	"github.com/hernad/nomad/api"
    13  	"github.com/hernad/nomad/ci"
    14  	"github.com/hernad/nomad/command/agent"
    15  	"github.com/hernad/nomad/helper/pointer"
    16  	"github.com/hernad/nomad/helper/uuid"
    17  	"github.com/hernad/nomad/nomad/mock"
    18  	"github.com/hernad/nomad/nomad/structs"
    19  	"github.com/mitchellh/cli"
    20  	"github.com/posener/complete"
    21  	"github.com/shoenig/test/must"
    22  )
    23  
    24  var _ cli.Command = (*JobStopCommand)(nil)
    25  
    26  func TestStopCommand_multi(t *testing.T) {
    27  	ci.Parallel(t)
    28  
    29  	srv, _, addr := testServer(t, true, func(c *agent.Config) {
    30  		c.DevMode = true
    31  	})
    32  	defer srv.Shutdown()
    33  
    34  	// the number of jobs we want to run
    35  	numJobs := 10
    36  
    37  	// create and run a handful of jobs
    38  	jobIDs := make([]string, 0, numJobs)
    39  	for i := 0; i < numJobs; i++ {
    40  		jobID := uuid.Generate()
    41  		jobIDs = append(jobIDs, jobID)
    42  	}
    43  
    44  	jobFilePath := func(jobID string) string {
    45  		return filepath.Join(os.TempDir(), jobID+".nomad")
    46  	}
    47  
    48  	// cleanup job files we will create
    49  	t.Cleanup(func() {
    50  		for _, jobID := range jobIDs {
    51  			_ = os.Remove(jobFilePath(jobID))
    52  		}
    53  	})
    54  
    55  	// record cli output
    56  	ui := cli.NewMockUi()
    57  
    58  	for _, jobID := range jobIDs {
    59  		job := testJob(jobID)
    60  		job.TaskGroups[0].Tasks[0].Resources.MemoryMB = pointer.Of(16)
    61  		job.TaskGroups[0].Tasks[0].Resources.DiskMB = pointer.Of(32)
    62  		job.TaskGroups[0].Tasks[0].Resources.CPU = pointer.Of(10)
    63  		job.TaskGroups[0].Tasks[0].Config = map[string]interface{}{
    64  			"run_for": "30s",
    65  		}
    66  
    67  		jobJSON, err := json.MarshalIndent(job, "", " ")
    68  		must.NoError(t, err)
    69  
    70  		jobFile := jobFilePath(jobID)
    71  		err = os.WriteFile(jobFile, []byte(jobJSON), 0o644)
    72  		must.NoError(t, err)
    73  
    74  		cmd := &JobRunCommand{Meta: Meta{Ui: ui}}
    75  		code := cmd.Run([]string{"-address", addr, "-json", jobFile})
    76  		must.Zero(t, code,
    77  			must.Sprintf("job stop stdout: %s", ui.OutputWriter.String()),
    78  			must.Sprintf("job stop stderr: %s", ui.ErrorWriter.String()),
    79  		)
    80  	}
    81  
    82  	// helper for stopping a list of jobs
    83  	stop := func(args ...string) (stdout string, stderr string, code int) {
    84  		cmd := &JobStopCommand{Meta: Meta{Ui: ui}}
    85  		code = cmd.Run(args)
    86  		return ui.OutputWriter.String(), ui.ErrorWriter.String(), code
    87  	}
    88  
    89  	// stop all jobs in one command
    90  	args := []string{"-address", addr, "-detach"}
    91  	args = append(args, jobIDs...)
    92  	stdout, stderr, code := stop(args...)
    93  	must.Zero(t, code,
    94  		must.Sprintf("job stop stdout: %s", stdout),
    95  		must.Sprintf("job stop stderr: %s", stderr),
    96  	)
    97  }
    98  
    99  func TestStopCommand_Fails(t *testing.T) {
   100  	ci.Parallel(t)
   101  	srv, _, url := testServer(t, false, nil)
   102  	defer srv.Shutdown()
   103  
   104  	ui := cli.NewMockUi()
   105  	cmd := &JobStopCommand{Meta: Meta{Ui: ui}}
   106  
   107  	// Fails on misuse
   108  	code := cmd.Run([]string{"-some", "-bad", "-args"})
   109  	must.One(t, code)
   110  
   111  	out := ui.ErrorWriter.String()
   112  	must.StrContains(t, out, "flag provided but not defined: -some")
   113  
   114  	ui.ErrorWriter.Reset()
   115  
   116  	// Fails on nonexistent job ID
   117  	code = cmd.Run([]string{"-address=" + url, "nope"})
   118  	must.One(t, code)
   119  
   120  	out = ui.ErrorWriter.String()
   121  	must.StrContains(t, out, "No job(s) with prefix or ID")
   122  
   123  	ui.ErrorWriter.Reset()
   124  
   125  	// Fails on connection failure
   126  	code = cmd.Run([]string{"-address=nope", "nope"})
   127  	must.One(t, code)
   128  
   129  	out = ui.ErrorWriter.String()
   130  	must.StrContains(t, out, "Error querying job prefix")
   131  }
   132  
   133  func TestStopCommand_AutocompleteArgs(t *testing.T) {
   134  	ci.Parallel(t)
   135  
   136  	srv, _, url := testServer(t, true, nil)
   137  	defer srv.Shutdown()
   138  
   139  	ui := cli.NewMockUi()
   140  	cmd := &JobStopCommand{Meta: Meta{Ui: ui, flagAddress: url}}
   141  
   142  	// Create a fake job
   143  	state := srv.Agent.Server().State()
   144  	j := mock.Job()
   145  	must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, j))
   146  
   147  	prefix := j.ID[:len(j.ID)-5]
   148  	args := complete.Args{Last: prefix}
   149  	predictor := cmd.AutocompleteArgs()
   150  
   151  	res := predictor.Predict(args)
   152  	must.Len(t, 1, res)
   153  	must.Eq(t, j.ID, res[0])
   154  }
   155  
   156  func TestJobStopCommand_ACL(t *testing.T) {
   157  	ci.Parallel(t)
   158  
   159  	// Start server with ACL enabled.
   160  	srv, client, url := testServer(t, true, func(c *agent.Config) {
   161  		c.ACL.Enabled = true
   162  	})
   163  	defer srv.Shutdown()
   164  
   165  	testCases := []struct {
   166  		name        string
   167  		jobPrefix   bool
   168  		aclPolicy   string
   169  		expectedErr string
   170  	}{
   171  		{
   172  			name:        "no token",
   173  			aclPolicy:   "",
   174  			expectedErr: api.PermissionDeniedErrorContent,
   175  		},
   176  		{
   177  			name: "missing submit-job",
   178  			aclPolicy: `
   179  namespace "default" {
   180  	capabilities = ["read-job"]
   181  }
   182  `,
   183  			expectedErr: api.PermissionDeniedErrorContent,
   184  		},
   185  		{
   186  			name: "missing read-job",
   187  			aclPolicy: `
   188  namespace "default" {
   189  	capabilities = ["submit-job"]
   190  }
   191  `,
   192  			expectedErr: api.PermissionDeniedErrorContent,
   193  		},
   194  		{
   195  			name: "read-job and submit-job allowed",
   196  			aclPolicy: `
   197  namespace "default" {
   198  	capabilities = ["read-job", "submit-job"]
   199  }
   200  `,
   201  		},
   202  		{
   203  			name:      "job prefix requires list-job",
   204  			jobPrefix: true,
   205  			aclPolicy: `
   206  namespace "default" {
   207  	capabilities = ["read-job", "submit-job"]
   208  }
   209  `,
   210  			expectedErr: "job not found",
   211  		},
   212  		{
   213  			name:      "job prefix works with list-job",
   214  			jobPrefix: true,
   215  			aclPolicy: `
   216  namespace "default" {
   217  	capabilities = ["list-jobs", "read-job", "submit-job"]
   218  }
   219  `,
   220  		},
   221  	}
   222  
   223  	for i, tc := range testCases {
   224  		t.Run(tc.name, func(t *testing.T) {
   225  			ui := cli.NewMockUi()
   226  			cmd := &JobStopCommand{Meta: Meta{Ui: ui}}
   227  			args := []string{
   228  				"-address", url,
   229  				"-yes",
   230  			}
   231  
   232  			// Create a job.
   233  			job := mock.MinJob()
   234  			state := srv.Agent.Server().State()
   235  			err := state.UpsertJob(structs.MsgTypeTestSetup, uint64(300+i), nil, job)
   236  			must.NoError(t, err)
   237  			defer func() {
   238  				client.Jobs().Deregister(job.ID, true, &api.WriteOptions{
   239  					AuthToken: srv.RootToken.SecretID,
   240  				})
   241  			}()
   242  
   243  			if tc.aclPolicy != "" {
   244  				// Create ACL token with test case policy and add it to the
   245  				// command.
   246  				policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
   247  				token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
   248  				args = append(args, "-token", token.SecretID)
   249  			}
   250  
   251  			// Add job ID or job ID prefix to the command.
   252  			if tc.jobPrefix {
   253  				args = append(args, job.ID[:3])
   254  			} else {
   255  				args = append(args, job.ID)
   256  			}
   257  
   258  			// Run command.
   259  			code := cmd.Run(args)
   260  			if tc.expectedErr == "" {
   261  				must.Zero(t, code)
   262  			} else {
   263  				must.One(t, code)
   264  				must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
   265  			}
   266  		})
   267  	}
   268  }