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  }