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  }