github.com/hernad/nomad@v1.6.112/command/job_scale_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 "strings" 9 "testing" 10 11 "github.com/hernad/nomad/api" 12 "github.com/hernad/nomad/ci" 13 "github.com/hernad/nomad/command/agent" 14 "github.com/hernad/nomad/helper/pointer" 15 "github.com/hernad/nomad/nomad/mock" 16 "github.com/hernad/nomad/nomad/structs" 17 "github.com/hernad/nomad/testutil" 18 "github.com/mitchellh/cli" 19 "github.com/shoenig/test/must" 20 ) 21 22 func TestJobScaleCommand_SingleGroup(t *testing.T) { 23 ci.Parallel(t) 24 srv, client, url := testServer(t, true, nil) 25 defer srv.Shutdown() 26 testutil.WaitForResult(func() (bool, error) { 27 nodes, _, err := client.Nodes().List(nil) 28 if err != nil { 29 return false, err 30 } 31 if len(nodes) == 0 { 32 return false, fmt.Errorf("missing node") 33 } 34 if _, ok := nodes[0].Drivers["mock_driver"]; !ok { 35 return false, fmt.Errorf("mock_driver not ready") 36 } 37 return true, nil 38 }, func(err error) { 39 t.Fatalf("err: %s", err) 40 }) 41 42 ui := cli.NewMockUi() 43 cmd := &JobScaleCommand{Meta: Meta{Ui: ui}} 44 45 // Register a test job and ensure it is running before moving on. 46 resp, _, err := client.Jobs().Register(testJob("scale_cmd_single_group"), nil) 47 if err != nil { 48 t.Fatalf("err: %s", err) 49 } 50 if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { 51 t.Fatalf("expected waitForSuccess exit code 0, got: %d", code) 52 } 53 54 // Perform the scaling action. 55 if code := cmd.Run([]string{"-address=" + url, "-detach", "scale_cmd_single_group", "2"}); code != 0 { 56 t.Fatalf("expected cmd run exit code 0, got: %d", code) 57 } 58 if out := ui.OutputWriter.String(); !strings.Contains(out, "Evaluation ID:") { 59 t.Fatalf("Expected Evaluation ID within output: %v", out) 60 } 61 } 62 63 func TestJobScaleCommand_MultiGroup(t *testing.T) { 64 ci.Parallel(t) 65 srv, client, url := testServer(t, true, nil) 66 defer srv.Shutdown() 67 testutil.WaitForResult(func() (bool, error) { 68 nodes, _, err := client.Nodes().List(nil) 69 if err != nil { 70 return false, err 71 } 72 if len(nodes) == 0 { 73 return false, fmt.Errorf("missing node") 74 } 75 if _, ok := nodes[0].Drivers["mock_driver"]; !ok { 76 return false, fmt.Errorf("mock_driver not ready") 77 } 78 return true, nil 79 }, func(err error) { 80 t.Fatalf("err: %s", err) 81 }) 82 83 ui := cli.NewMockUi() 84 cmd := &JobScaleCommand{Meta: Meta{Ui: ui}} 85 86 // Create a job with two task groups. 87 job := testJob("scale_cmd_multi_group") 88 task := api.NewTask("task2", "mock_driver"). 89 SetConfig("kill_after", "1s"). 90 SetConfig("run_for", "5s"). 91 SetConfig("exit_code", 0). 92 Require(&api.Resources{ 93 MemoryMB: pointer.Of(256), 94 CPU: pointer.Of(100), 95 }). 96 SetLogConfig(&api.LogConfig{ 97 MaxFiles: pointer.Of(1), 98 MaxFileSizeMB: pointer.Of(2), 99 }) 100 group2 := api.NewTaskGroup("group2", 1). 101 AddTask(task). 102 RequireDisk(&api.EphemeralDisk{ 103 SizeMB: pointer.Of(20), 104 }) 105 job.AddTaskGroup(group2) 106 107 // Register a test job and ensure it is running before moving on. 108 resp, _, err := client.Jobs().Register(job, nil) 109 if err != nil { 110 t.Fatalf("err: %s", err) 111 } 112 if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 { 113 t.Fatalf("expected waitForSuccess exit code 0, got: %d", code) 114 } 115 116 // Attempt to scale without specifying the task group which should fail. 117 if code := cmd.Run([]string{"-address=" + url, "-detach", "scale_cmd_multi_group", "2"}); code != 1 { 118 t.Fatalf("expected cmd run exit code 1, got: %d", code) 119 } 120 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Group name required") { 121 t.Fatalf("unexpected error message: %v", out) 122 } 123 124 // Specify the target group which should be successful. 125 if code := cmd.Run([]string{"-address=" + url, "-detach", "scale_cmd_multi_group", "group1", "2"}); code != 0 { 126 t.Fatalf("expected cmd run exit code 0, got: %d", code) 127 } 128 if out := ui.OutputWriter.String(); !strings.Contains(out, "Evaluation ID:") { 129 t.Fatalf("Expected Evaluation ID within output: %v", out) 130 } 131 } 132 133 func TestJobScaleCommand_ACL(t *testing.T) { 134 ci.Parallel(t) 135 136 // Start server with ACL enabled. 137 srv, client, url := testServer(t, true, func(c *agent.Config) { 138 c.ACL.Enabled = true 139 }) 140 defer srv.Shutdown() 141 142 testCases := []struct { 143 name string 144 jobPrefix bool 145 aclPolicy string 146 expectedErr string 147 }{ 148 { 149 name: "no token", 150 aclPolicy: "", 151 expectedErr: api.PermissionDeniedErrorContent, 152 }, 153 { 154 name: "missing scale-job or job-submit", 155 aclPolicy: ` 156 namespace "default" { 157 capabilities = ["read-job-scaling"] 158 } 159 `, 160 expectedErr: api.PermissionDeniedErrorContent, 161 }, 162 { 163 name: "missing read-job-scaling", 164 aclPolicy: ` 165 namespace "default" { 166 capabilities = ["scale-job"] 167 } 168 `, 169 expectedErr: api.PermissionDeniedErrorContent, 170 }, 171 { 172 name: "read-job-scaling and scale-job allowed but can't monitor eval without read-job", 173 aclPolicy: ` 174 namespace "default" { 175 capabilities = ["read-job-scaling", "scale-job"] 176 } 177 `, 178 expectedErr: "No evaluation with id", 179 }, 180 { 181 name: "read-job-scaling and submit-job allowed but can't monitor eval without read-job", 182 aclPolicy: ` 183 namespace "default" { 184 capabilities = ["read-job-scaling", "submit-job"] 185 } 186 `, 187 expectedErr: "No evaluation with id", 188 }, 189 { 190 name: "read-job-scaling and scale-job allowed and can monitor eval with read-job", 191 aclPolicy: ` 192 namespace "default" { 193 capabilities = ["read-job", "read-job-scaling", "scale-job"] 194 } 195 `, 196 }, 197 { 198 name: "read-job-scaling and submit-job allowed and can monitor eval with read-job", 199 aclPolicy: ` 200 namespace "default" { 201 capabilities = ["read-job", "read-job-scaling", "submit-job"] 202 } 203 `, 204 }, 205 { 206 name: "job prefix requires list-job", 207 jobPrefix: true, 208 aclPolicy: ` 209 namespace "default" { 210 capabilities = ["read-job-scaling", "scale-job"] 211 } 212 `, 213 expectedErr: "job not found", 214 }, 215 { 216 name: "job prefix works with list-job but can't monitor eval without read-job", 217 jobPrefix: true, 218 aclPolicy: ` 219 namespace "default" { 220 capabilities = ["read-job-scaling", "scale-job", "list-jobs"] 221 } 222 `, 223 expectedErr: "No evaluation with id", 224 }, 225 { 226 name: "job prefix works with list-job and can monitor eval with read-job", 227 jobPrefix: true, 228 aclPolicy: ` 229 namespace "default" { 230 capabilities = ["read-job", "read-job-scaling", "scale-job", "list-jobs"] 231 } 232 `, 233 }, 234 } 235 236 for i, tc := range testCases { 237 t.Run(tc.name, func(t *testing.T) { 238 ui := cli.NewMockUi() 239 cmd := &JobScaleCommand{Meta: Meta{Ui: ui}} 240 args := []string{ 241 "-address", url, 242 } 243 244 // Create a job. 245 job := mock.MinJob() 246 state := srv.Agent.Server().State() 247 err := state.UpsertJob(structs.MsgTypeTestSetup, uint64(300+i), nil, job) 248 must.NoError(t, err) 249 defer func() { 250 client.Jobs().Deregister(job.ID, true, &api.WriteOptions{ 251 AuthToken: srv.RootToken.SecretID, 252 }) 253 }() 254 255 if tc.aclPolicy != "" { 256 // Create ACL token with test case policy and add it to the 257 // command. 258 policyName := nonAlphaNum.ReplaceAllString(tc.name, "-") 259 token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy) 260 args = append(args, "-token", token.SecretID) 261 } 262 263 // Add job ID or job ID prefix to the command. 264 if tc.jobPrefix { 265 args = append(args, job.ID[:3]) 266 } else { 267 args = append(args, job.ID) 268 } 269 270 // Run command scaling job to 2. 271 args = append(args, "2") 272 code := cmd.Run(args) 273 if tc.expectedErr == "" { 274 must.Zero(t, code) 275 } else { 276 must.One(t, code) 277 must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr) 278 } 279 }) 280 } 281 }