github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/job_plan_test.go (about)

     1  package command
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"strconv"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/hashicorp/nomad/api"
    11  	"github.com/hashicorp/nomad/ci"
    12  	"github.com/hashicorp/nomad/testutil"
    13  	"github.com/mitchellh/cli"
    14  	"github.com/shoenig/test/must"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestPlanCommand_Implements(t *testing.T) {
    19  	ci.Parallel(t)
    20  	var _ cli.Command = &JobRunCommand{}
    21  }
    22  
    23  func TestPlanCommand_Fails(t *testing.T) {
    24  	ci.Parallel(t)
    25  
    26  	// Create a server
    27  	s := testutil.NewTestServer(t, nil)
    28  	defer s.Stop()
    29  
    30  	ui := cli.NewMockUi()
    31  	cmd := &JobPlanCommand{Meta: Meta{Ui: ui, flagAddress: "http://" + s.HTTPAddr}}
    32  
    33  	// Fails on misuse
    34  	if code := cmd.Run([]string{"some", "bad", "args"}); code != 255 {
    35  		t.Fatalf("expected exit code 1, got: %d", code)
    36  	}
    37  	if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
    38  		t.Fatalf("expected help output, got: %s", out)
    39  	}
    40  	ui.ErrorWriter.Reset()
    41  
    42  	// Fails when specified file does not exist
    43  	if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 255 {
    44  		t.Fatalf("expect exit 255, got: %d", code)
    45  	}
    46  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") {
    47  		t.Fatalf("expect getting job struct error, got: %s", out)
    48  	}
    49  	ui.ErrorWriter.Reset()
    50  
    51  	// Fails on invalid HCL
    52  	fh1, err := ioutil.TempFile("", "nomad")
    53  	if err != nil {
    54  		t.Fatalf("err: %s", err)
    55  	}
    56  	defer os.Remove(fh1.Name())
    57  	if _, err := fh1.WriteString("nope"); err != nil {
    58  		t.Fatalf("err: %s", err)
    59  	}
    60  	if code := cmd.Run([]string{fh1.Name()}); code != 255 {
    61  		t.Fatalf("expect exit 255, got: %d", code)
    62  	}
    63  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") {
    64  		t.Fatalf("expect parsing error, got: %s", out)
    65  	}
    66  	ui.ErrorWriter.Reset()
    67  
    68  	// Fails on invalid job spec
    69  	fh2, err := ioutil.TempFile("", "nomad")
    70  	if err != nil {
    71  		t.Fatalf("err: %s", err)
    72  	}
    73  	defer os.Remove(fh2.Name())
    74  	if _, err := fh2.WriteString(`job "job1" {}`); err != nil {
    75  		t.Fatalf("err: %s", err)
    76  	}
    77  	if code := cmd.Run([]string{fh2.Name()}); code != 255 {
    78  		t.Fatalf("expect exit 255, got: %d", code)
    79  	}
    80  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error during plan") {
    81  		t.Fatalf("expect validation error, got: %s", out)
    82  	}
    83  	ui.ErrorWriter.Reset()
    84  
    85  	// Fails on connection failure (requires a valid job)
    86  	fh3, err := ioutil.TempFile("", "nomad")
    87  	if err != nil {
    88  		t.Fatalf("err: %s", err)
    89  	}
    90  	defer os.Remove(fh3.Name())
    91  	_, err = fh3.WriteString(`
    92  job "job1" {
    93  	type = "service"
    94  	datacenters = [ "dc1" ]
    95  	group "group1" {
    96  		count = 1
    97  		task "task1" {
    98  			driver = "exec"
    99  			resources {
   100  				cpu = 1000
   101  				memory = 512
   102  			}
   103  		}
   104  	}
   105  }`)
   106  	if err != nil {
   107  		t.Fatalf("err: %s", err)
   108  	}
   109  	if code := cmd.Run([]string{"-address=nope", fh3.Name()}); code != 255 {
   110  		t.Fatalf("expected exit code 255, got: %d", code)
   111  	}
   112  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error during plan") {
   113  		t.Fatalf("expected failed query error, got: %s", out)
   114  	}
   115  }
   116  
   117  func TestPlanCommand_hcl1_hcl2_strict(t *testing.T) {
   118  	ci.Parallel(t)
   119  
   120  	_, _, addr := testServer(t, false, nil)
   121  
   122  	t.Run("-hcl1 implies -hcl2-strict is false", func(t *testing.T) {
   123  		ui := cli.NewMockUi()
   124  		cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
   125  		got := cmd.Run([]string{
   126  			"-hcl1", "-hcl2-strict",
   127  			"-address", addr,
   128  			"assets/example-short.nomad",
   129  		})
   130  		// Exit code 1 here means that an alloc will be created, which is
   131  		// expected.
   132  		require.Equal(t, 1, got)
   133  	})
   134  }
   135  
   136  func TestPlanCommand_From_STDIN(t *testing.T) {
   137  	ci.Parallel(t)
   138  	stdinR, stdinW, err := os.Pipe()
   139  	if err != nil {
   140  		t.Fatalf("err: %s", err)
   141  	}
   142  
   143  	ui := cli.NewMockUi()
   144  	cmd := &JobPlanCommand{
   145  		Meta:      Meta{Ui: ui},
   146  		JobGetter: JobGetter{testStdin: stdinR},
   147  	}
   148  
   149  	go func() {
   150  		stdinW.WriteString(`
   151  job "job1" {
   152    type = "service"
   153    datacenters = [ "dc1" ]
   154    group "group1" {
   155                  count = 1
   156                  task "task1" {
   157                          driver = "exec"
   158                          resources {
   159                                  cpu = 1000
   160                                  memory = 512
   161                          }
   162                  }
   163          }
   164  }`)
   165  		stdinW.Close()
   166  	}()
   167  
   168  	args := []string{"-"}
   169  	if code := cmd.Run(args); code != 255 {
   170  		t.Fatalf("expected exit code 255, got %d: %q", code, ui.ErrorWriter.String())
   171  	}
   172  
   173  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "connection refused") {
   174  		t.Fatalf("expected connection refused error, got: %s", out)
   175  	}
   176  	ui.ErrorWriter.Reset()
   177  }
   178  
   179  func TestPlanCommand_From_Files(t *testing.T) {
   180  
   181  	// Create a Vault server
   182  	v := testutil.NewTestVault(t)
   183  	defer v.Stop()
   184  
   185  	// Create a Nomad server
   186  	s := testutil.NewTestServer(t, func(c *testutil.TestServerConfig) {
   187  		c.Vault.Address = v.HTTPAddr
   188  		c.Vault.Enabled = true
   189  		c.Vault.AllowUnauthenticated = false
   190  		c.Vault.Token = v.RootToken
   191  	})
   192  	defer s.Stop()
   193  
   194  	t.Run("fail to place", func(t *testing.T) {
   195  		ui := cli.NewMockUi()
   196  		cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
   197  		args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-basic.nomad"}
   198  		code := cmd.Run(args)
   199  		require.Equal(t, 1, code) // no client running, fail to place
   200  		must.StrContains(t, ui.OutputWriter.String(), "WARNING: Failed to place all allocations.")
   201  	})
   202  
   203  	t.Run("vault no token", func(t *testing.T) {
   204  		ui := cli.NewMockUi()
   205  		cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
   206  		args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-vault.nomad"}
   207  		code := cmd.Run(args)
   208  		must.Eq(t, 255, code)
   209  		must.StrContains(t, ui.ErrorWriter.String(), "* Vault used in the job but missing Vault token")
   210  	})
   211  
   212  	t.Run("vault bad token via flag", func(t *testing.T) {
   213  		ui := cli.NewMockUi()
   214  		cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
   215  		args := []string{"-address", "http://" + s.HTTPAddr, "-vault-token=abc123", "testdata/example-vault.nomad"}
   216  		code := cmd.Run(args)
   217  		must.Eq(t, 255, code)
   218  		must.StrContains(t, ui.ErrorWriter.String(), "* bad token")
   219  	})
   220  
   221  	t.Run("vault bad token via env", func(t *testing.T) {
   222  		t.Setenv("VAULT_TOKEN", "abc123")
   223  		ui := cli.NewMockUi()
   224  		cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
   225  		args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-vault.nomad"}
   226  		code := cmd.Run(args)
   227  		must.Eq(t, 255, code)
   228  		must.StrContains(t, ui.ErrorWriter.String(), "* bad token")
   229  	})
   230  }
   231  
   232  func TestPlanCommand_From_URL(t *testing.T) {
   233  	ci.Parallel(t)
   234  	ui := cli.NewMockUi()
   235  	cmd := &JobPlanCommand{
   236  		Meta: Meta{Ui: ui},
   237  	}
   238  
   239  	args := []string{"https://example.com/foo/bar"}
   240  	if code := cmd.Run(args); code != 255 {
   241  		t.Fatalf("expected exit code 255, got %d: %q", code, ui.ErrorWriter.String())
   242  	}
   243  
   244  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting jobfile") {
   245  		t.Fatalf("expected error getting jobfile, got: %s", out)
   246  	}
   247  }
   248  
   249  func TestPlanCommad_Preemptions(t *testing.T) {
   250  	ci.Parallel(t)
   251  	ui := cli.NewMockUi()
   252  	cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
   253  	require := require.New(t)
   254  
   255  	// Only one preempted alloc
   256  	resp1 := &api.JobPlanResponse{
   257  		Annotations: &api.PlanAnnotations{
   258  			PreemptedAllocs: []*api.AllocationListStub{
   259  				{
   260  					ID:        "alloc1",
   261  					JobID:     "jobID1",
   262  					TaskGroup: "meta",
   263  					JobType:   "batch",
   264  					Namespace: "test",
   265  				},
   266  			},
   267  		},
   268  	}
   269  	cmd.addPreemptions(resp1)
   270  	out := ui.OutputWriter.String()
   271  	require.Contains(out, "Alloc ID")
   272  	require.Contains(out, "alloc1")
   273  
   274  	// Less than 10 unique job ids
   275  	var preemptedAllocs []*api.AllocationListStub
   276  	for i := 0; i < 12; i++ {
   277  		job_id := "job" + strconv.Itoa(i%4)
   278  		alloc := &api.AllocationListStub{
   279  			ID:        "alloc",
   280  			JobID:     job_id,
   281  			TaskGroup: "meta",
   282  			JobType:   "batch",
   283  			Namespace: "test",
   284  		}
   285  		preemptedAllocs = append(preemptedAllocs, alloc)
   286  	}
   287  
   288  	resp2 := &api.JobPlanResponse{
   289  		Annotations: &api.PlanAnnotations{
   290  			PreemptedAllocs: preemptedAllocs,
   291  		},
   292  	}
   293  	ui.OutputWriter.Reset()
   294  	cmd.addPreemptions(resp2)
   295  	out = ui.OutputWriter.String()
   296  	require.Contains(out, "Job ID")
   297  	require.Contains(out, "Namespace")
   298  
   299  	// More than 10 unique job IDs
   300  	preemptedAllocs = make([]*api.AllocationListStub, 0)
   301  	var job_type string
   302  	for i := 0; i < 20; i++ {
   303  		job_id := "job" + strconv.Itoa(i)
   304  		if i%2 == 0 {
   305  			job_type = "service"
   306  		} else {
   307  			job_type = "batch"
   308  		}
   309  		alloc := &api.AllocationListStub{
   310  			ID:        "alloc",
   311  			JobID:     job_id,
   312  			TaskGroup: "meta",
   313  			JobType:   job_type,
   314  			Namespace: "test",
   315  		}
   316  		preemptedAllocs = append(preemptedAllocs, alloc)
   317  	}
   318  
   319  	resp3 := &api.JobPlanResponse{
   320  		Annotations: &api.PlanAnnotations{
   321  			PreemptedAllocs: preemptedAllocs,
   322  		},
   323  	}
   324  	ui.OutputWriter.Reset()
   325  	cmd.addPreemptions(resp3)
   326  	out = ui.OutputWriter.String()
   327  	require.Contains(out, "Job Type")
   328  	require.Contains(out, "batch")
   329  	require.Contains(out, "service")
   330  }
   331  
   332  func TestPlanCommad_JSON(t *testing.T) {
   333  	ui := cli.NewMockUi()
   334  	cmd := &JobPlanCommand{
   335  		Meta: Meta{Ui: ui},
   336  	}
   337  
   338  	args := []string{
   339  		"-address=http://nope",
   340  		"-json",
   341  		"testdata/example-short.json",
   342  	}
   343  	code := cmd.Run(args)
   344  	require.Equal(t, 255, code)
   345  	require.Contains(t, ui.ErrorWriter.String(), "Error during plan: Put")
   346  }