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 }