github.com/hernad/nomad@v1.6.112/command/job_periodic_force_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "fmt" 8 "testing" 9 10 "github.com/hernad/nomad/api" 11 "github.com/hernad/nomad/ci" 12 "github.com/hernad/nomad/command/agent" 13 "github.com/hernad/nomad/helper/pointer" 14 "github.com/hernad/nomad/nomad/mock" 15 "github.com/hernad/nomad/nomad/structs" 16 "github.com/hernad/nomad/testutil" 17 "github.com/mitchellh/cli" 18 "github.com/posener/complete" 19 "github.com/shoenig/test/must" 20 "github.com/stretchr/testify/require" 21 ) 22 23 func TestJobPeriodicForceCommand_Implements(t *testing.T) { 24 ci.Parallel(t) 25 var _ cli.Command = &JobPeriodicForceCommand{} 26 } 27 28 func TestJobPeriodicForceCommand_Fails(t *testing.T) { 29 ci.Parallel(t) 30 ui := cli.NewMockUi() 31 cmd := &JobPeriodicForceCommand{Meta: Meta{Ui: ui}} 32 33 // Fails on misuse 34 code := cmd.Run([]string{"some", "bad", "args"}) 35 require.Equal(t, code, 1, "expected error") 36 out := ui.ErrorWriter.String() 37 require.Contains(t, out, commandErrorText(cmd), "expected help output") 38 ui.ErrorWriter.Reset() 39 40 code = cmd.Run([]string{"-address=nope", "12"}) 41 require.Equal(t, code, 1, "expected error") 42 out = ui.ErrorWriter.String() 43 require.Contains(t, out, "Error querying job prefix", "expected force error") 44 } 45 46 func TestJobPeriodicForceCommand_AutocompleteArgs(t *testing.T) { 47 ci.Parallel(t) 48 49 srv, _, url := testServer(t, true, nil) 50 defer srv.Shutdown() 51 52 ui := cli.NewMockUi() 53 cmd := &JobPeriodicForceCommand{Meta: Meta{Ui: ui, flagAddress: url}} 54 55 // Create a fake job, not periodic 56 state := srv.Agent.Server().State() 57 j := mock.Job() 58 require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, j)) 59 60 predictor := cmd.AutocompleteArgs() 61 62 res := predictor.Predict(complete.Args{Last: j.ID[:len(j.ID)-5]}) 63 require.Empty(t, res) 64 65 // Create another fake job, periodic 66 state = srv.Agent.Server().State() 67 j2 := mock.Job() 68 j2.Periodic = &structs.PeriodicConfig{ 69 Enabled: true, 70 Spec: "spec", 71 SpecType: "cron", 72 ProhibitOverlap: true, 73 TimeZone: "test zone", 74 } 75 require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, j2)) 76 77 res = predictor.Predict(complete.Args{Last: j2.ID[:len(j.ID)-5]}) 78 require.Equal(t, []string{j2.ID}, res) 79 80 res = predictor.Predict(complete.Args{}) 81 require.Equal(t, []string{j2.ID}, res) 82 } 83 84 func TestJobPeriodicForceCommand_NonPeriodicJob(t *testing.T) { 85 ci.Parallel(t) 86 srv, client, url := testServer(t, true, nil) 87 defer srv.Shutdown() 88 testutil.WaitForResult(func() (bool, error) { 89 nodes, _, err := client.Nodes().List(nil) 90 if err != nil { 91 return false, err 92 } 93 if len(nodes) == 0 { 94 return false, fmt.Errorf("missing node") 95 } 96 if _, ok := nodes[0].Drivers["mock_driver"]; !ok { 97 return false, fmt.Errorf("mock_driver not ready") 98 } 99 return true, nil 100 }, func(err error) { 101 require.NoError(t, err) 102 }) 103 104 // Register a job 105 j := testJob("job_not_periodic") 106 107 ui := cli.NewMockUi() 108 cmd := &JobPeriodicForceCommand{Meta: Meta{Ui: ui, flagAddress: url}} 109 110 resp, _, err := client.Jobs().Register(j, nil) 111 require.NoError(t, err) 112 code := waitForSuccess(ui, client, fullId, t, resp.EvalID) 113 require.Equal(t, 0, code) 114 115 code = cmd.Run([]string{"-address=" + url, "job_not_periodic"}) 116 require.Equal(t, 1, code, "expected exit code") 117 out := ui.ErrorWriter.String() 118 require.Contains(t, out, "No periodic job(s)", "non-periodic error message") 119 } 120 121 func TestJobPeriodicForceCommand_SuccessfulPeriodicForceDetach(t *testing.T) { 122 ci.Parallel(t) 123 srv, client, url := testServer(t, true, nil) 124 defer srv.Shutdown() 125 testutil.WaitForResult(func() (bool, error) { 126 nodes, _, err := client.Nodes().List(nil) 127 if err != nil { 128 return false, err 129 } 130 if len(nodes) == 0 { 131 return false, fmt.Errorf("missing node") 132 } 133 if _, ok := nodes[0].Drivers["mock_driver"]; !ok { 134 return false, fmt.Errorf("mock_driver not ready") 135 } 136 return true, nil 137 }, func(err error) { 138 require.NoError(t, err) 139 }) 140 141 // Register a job 142 j := testJob("job1_is_periodic") 143 j.Periodic = &api.PeriodicConfig{ 144 SpecType: pointer.Of(api.PeriodicSpecCron), 145 Spec: pointer.Of("*/15 * * * * *"), 146 ProhibitOverlap: pointer.Of(true), 147 TimeZone: pointer.Of("Europe/Minsk"), 148 } 149 150 ui := cli.NewMockUi() 151 cmd := &JobPeriodicForceCommand{Meta: Meta{Ui: ui, flagAddress: url}} 152 153 _, _, err := client.Jobs().Register(j, nil) 154 require.NoError(t, err) 155 156 code := cmd.Run([]string{"-address=" + url, "-detach", "job1_is_periodic"}) 157 require.Equal(t, 0, code, "expected no error code") 158 out := ui.OutputWriter.String() 159 require.Contains(t, out, "Force periodic successful") 160 require.Contains(t, out, "Evaluation ID:") 161 } 162 163 func TestJobPeriodicForceCommand_SuccessfulPeriodicForce(t *testing.T) { 164 ci.Parallel(t) 165 srv, client, url := testServer(t, true, nil) 166 defer srv.Shutdown() 167 testutil.WaitForResult(func() (bool, error) { 168 nodes, _, err := client.Nodes().List(nil) 169 if err != nil { 170 return false, err 171 } 172 if len(nodes) == 0 { 173 return false, fmt.Errorf("missing node") 174 } 175 if _, ok := nodes[0].Drivers["mock_driver"]; !ok { 176 return false, fmt.Errorf("mock_driver not ready") 177 } 178 return true, nil 179 }, func(err error) { 180 require.NoError(t, err) 181 }) 182 183 // Register a job 184 j := testJob("job2_is_periodic") 185 j.Periodic = &api.PeriodicConfig{ 186 SpecType: pointer.Of(api.PeriodicSpecCron), 187 Spec: pointer.Of("*/15 * * * * *"), 188 ProhibitOverlap: pointer.Of(true), 189 TimeZone: pointer.Of("Europe/Minsk"), 190 } 191 192 ui := cli.NewMockUi() 193 cmd := &JobPeriodicForceCommand{Meta: Meta{Ui: ui, flagAddress: url}} 194 195 _, _, err := client.Jobs().Register(j, nil) 196 require.NoError(t, err) 197 198 code := cmd.Run([]string{"-address=" + url, "job2_is_periodic"}) 199 require.Equal(t, 0, code, "expected no error code") 200 out := ui.OutputWriter.String() 201 require.Contains(t, out, "Monitoring evaluation") 202 require.Contains(t, out, "finished with status \"complete\"") 203 } 204 205 func TestJobPeriodicForceCommand_SuccessfulIfJobIDEqualsPrefix(t *testing.T) { 206 ci.Parallel(t) 207 srv, client, url := testServer(t, true, nil) 208 defer srv.Shutdown() 209 testutil.WaitForResult(func() (bool, error) { 210 nodes, _, err := client.Nodes().List(nil) 211 if err != nil { 212 return false, err 213 } 214 if len(nodes) == 0 { 215 return false, fmt.Errorf("missing node") 216 } 217 if _, ok := nodes[0].Drivers["mock_driver"]; !ok { 218 return false, fmt.Errorf("mock_driver not ready") 219 } 220 return true, nil 221 }, func(err error) { 222 require.NoError(t, err) 223 }) 224 225 j1 := testJob("periodic-prefix") 226 j1.Periodic = &api.PeriodicConfig{ 227 SpecType: pointer.Of(api.PeriodicSpecCron), 228 Spec: pointer.Of("*/15 * * * * *"), 229 ProhibitOverlap: pointer.Of(true), 230 TimeZone: pointer.Of("Europe/Minsk"), 231 } 232 j2 := testJob("periodic-prefix-another-job") 233 j2.Periodic = &api.PeriodicConfig{ 234 SpecType: pointer.Of(api.PeriodicSpecCron), 235 Spec: pointer.Of("*/15 * * * * *"), 236 ProhibitOverlap: pointer.Of(true), 237 TimeZone: pointer.Of("Europe/Minsk"), 238 } 239 240 ui := cli.NewMockUi() 241 cmd := &JobPeriodicForceCommand{Meta: Meta{Ui: ui, flagAddress: url}} 242 243 _, _, err := client.Jobs().Register(j1, nil) 244 require.NoError(t, err) 245 _, _, err = client.Jobs().Register(j2, nil) 246 require.NoError(t, err) 247 248 code := cmd.Run([]string{"-address=" + url, "periodic-prefix"}) 249 require.Equal(t, 0, code, "expected no error code") 250 out := ui.OutputWriter.String() 251 require.Contains(t, out, "Monitoring evaluation") 252 require.Contains(t, out, "finished with status \"complete\"") 253 } 254 255 func TestJobPeriodicForceCommand_ACL(t *testing.T) { 256 ci.Parallel(t) 257 258 // Start server with ACL enabled. 259 srv, client, url := testServer(t, true, func(c *agent.Config) { 260 c.ACL.Enabled = true 261 }) 262 defer srv.Shutdown() 263 client.SetSecretID(srv.RootToken.SecretID) 264 265 // Create a periodic job. 266 jobID := "test_job_periodic_force_acl" 267 job := testJob(jobID) 268 job.Periodic = &api.PeriodicConfig{ 269 SpecType: pointer.Of(api.PeriodicSpecCron), 270 Spec: pointer.Of("*/15 * * * * *"), 271 } 272 273 rootTokenOpts := &api.WriteOptions{ 274 AuthToken: srv.RootToken.SecretID, 275 } 276 _, _, err := client.Jobs().Register(job, rootTokenOpts) 277 must.NoError(t, err) 278 279 testCases := []struct { 280 name string 281 jobPrefix bool 282 aclPolicy string 283 expectedErr string 284 }{ 285 { 286 name: "no token", 287 aclPolicy: "", 288 expectedErr: api.PermissionDeniedErrorContent, 289 }, 290 { 291 name: "missing submit-job", 292 aclPolicy: ` 293 namespace "default" { 294 capabilities = ["list-jobs"] 295 } 296 `, 297 expectedErr: api.PermissionDeniedErrorContent, 298 }, 299 { 300 name: "submit-job allowed but can't monitor eval without read-job", 301 aclPolicy: ` 302 namespace "default" { 303 capabilities = ["submit-job"] 304 } 305 `, 306 expectedErr: "No evaluation with id", 307 }, 308 { 309 name: "submit-job allowed and can monitor eval with read-job", 310 aclPolicy: ` 311 namespace "default" { 312 capabilities = ["submit-job", "read-job"] 313 } 314 `, 315 }, 316 { 317 name: "job prefix requires list-job", 318 jobPrefix: true, 319 aclPolicy: ` 320 namespace "default" { 321 capabilities = ["submit-job"] 322 } 323 `, 324 expectedErr: "job not found", 325 }, 326 { 327 name: "job prefix works with list-job but can't monitor eval without read-job", 328 jobPrefix: true, 329 aclPolicy: ` 330 namespace "default" { 331 capabilities = ["submit-job", "list-jobs"] 332 } 333 `, 334 expectedErr: "No evaluation with id", 335 }, 336 { 337 name: "job prefix works with list-job and can monitor eval with read-job", 338 jobPrefix: true, 339 aclPolicy: ` 340 namespace "default" { 341 capabilities = ["read-job", "submit-job", "list-jobs"] 342 } 343 `, 344 }, 345 } 346 347 for i, tc := range testCases { 348 t.Run(tc.name, func(t *testing.T) { 349 ui := cli.NewMockUi() 350 cmd := &JobPeriodicForceCommand{Meta: Meta{Ui: ui}} 351 args := []string{ 352 "-address", url, 353 } 354 355 if tc.aclPolicy != "" { 356 state := srv.Agent.Server().State() 357 358 // Create ACL token with test case policy and add it to the 359 // command. 360 policyName := nonAlphaNum.ReplaceAllString(tc.name, "-") 361 token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy) 362 args = append(args, "-token", token.SecretID) 363 } 364 365 // Add job ID or job ID prefix to the command. 366 if tc.jobPrefix { 367 args = append(args, jobID[:3]) 368 } else { 369 args = append(args, jobID) 370 } 371 372 // Run command. 373 code := cmd.Run(args) 374 if tc.expectedErr == "" { 375 must.Zero(t, code) 376 } else { 377 must.One(t, code) 378 must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr) 379 } 380 }) 381 } 382 }