github.com/hernad/nomad@v1.6.112/command/job_run_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/hernad/nomad/ci"
    16  	"github.com/hernad/nomad/testutil"
    17  	"github.com/mitchellh/cli"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  var _ cli.Command = (*JobRunCommand)(nil)
    22  
    23  func TestRunCommand_Output_Json(t *testing.T) {
    24  	ci.Parallel(t)
    25  	ui := cli.NewMockUi()
    26  	cmd := &JobRunCommand{Meta: Meta{Ui: ui}}
    27  
    28  	fh, err := os.CreateTemp("", "nomad")
    29  	if err != nil {
    30  		t.Fatalf("err: %s", err)
    31  	}
    32  	defer os.Remove(fh.Name())
    33  	_, err = fh.WriteString(`
    34  job "job1" {
    35  	type = "service"
    36  	datacenters = [ "dc1" ]
    37  	group "group1" {
    38  		count = 1
    39  		task "task1" {
    40  			driver = "exec"
    41  			resources {
    42  				cpu = 1000
    43  				memory = 512
    44  			}
    45  		}
    46  	}
    47  }`)
    48  	if err != nil {
    49  		t.Fatalf("err: %s", err)
    50  	}
    51  	if code := cmd.Run([]string{"-output", fh.Name()}); code != 0 {
    52  		t.Fatalf("expected exit code 0, got: %d", code)
    53  	}
    54  	if out := ui.OutputWriter.String(); !strings.Contains(out, `"Type": "service",`) {
    55  		t.Fatalf("Expected JSON output: %v", out)
    56  	}
    57  }
    58  
    59  func TestRunCommand_hcl1_hcl2_strict(t *testing.T) {
    60  	ci.Parallel(t)
    61  
    62  	_, _, addr := testServer(t, false, nil)
    63  
    64  	t.Run("-hcl1 implies -hcl2-strict is false", func(t *testing.T) {
    65  		ui := cli.NewMockUi()
    66  		cmd := &JobRunCommand{Meta: Meta{Ui: ui}}
    67  		got := cmd.Run([]string{
    68  			"-hcl1", "-hcl2-strict",
    69  			"-address", addr,
    70  			"-detach",
    71  			"asset/example-short.nomad.hcl",
    72  		})
    73  		require.Equal(t, 0, got, ui.ErrorWriter.String())
    74  	})
    75  }
    76  
    77  func TestRunCommand_Fails(t *testing.T) {
    78  	ci.Parallel(t)
    79  
    80  	// Create a server
    81  	s := testutil.NewTestServer(t, nil)
    82  	defer s.Stop()
    83  
    84  	ui := cli.NewMockUi()
    85  	cmd := &JobRunCommand{Meta: Meta{Ui: ui, flagAddress: "http://" + s.HTTPAddr}}
    86  
    87  	// Fails on misuse
    88  	if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
    89  		t.Fatalf("expected exit code 1, got: %d", code)
    90  	}
    91  	if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
    92  		t.Fatalf("expected help output, got: %s", out)
    93  	}
    94  	ui.ErrorWriter.Reset()
    95  
    96  	// Fails when specified file does not exist
    97  	if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 1 {
    98  		t.Fatalf("expect exit 1, got: %d", code)
    99  	}
   100  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") {
   101  		t.Fatalf("expect getting job struct error, got: %s", out)
   102  	}
   103  	ui.ErrorWriter.Reset()
   104  
   105  	// Fails on invalid HCL
   106  	fh1, err := os.CreateTemp("", "nomad")
   107  	if err != nil {
   108  		t.Fatalf("err: %s", err)
   109  	}
   110  	defer os.Remove(fh1.Name())
   111  	if _, err := fh1.WriteString("nope"); err != nil {
   112  		t.Fatalf("err: %s", err)
   113  	}
   114  	if code := cmd.Run([]string{fh1.Name()}); code != 1 {
   115  		t.Fatalf("expect exit 1, got: %d", code)
   116  	}
   117  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") {
   118  		t.Fatalf("expect parsing error, got: %s", out)
   119  	}
   120  	ui.ErrorWriter.Reset()
   121  
   122  	// Fails on invalid job spec
   123  	fh2, err := os.CreateTemp("", "nomad")
   124  	if err != nil {
   125  		t.Fatalf("err: %s", err)
   126  	}
   127  	defer os.Remove(fh2.Name())
   128  	if _, err := fh2.WriteString(`job "job1" {}`); err != nil {
   129  		t.Fatalf("err: %s", err)
   130  	}
   131  	if code := cmd.Run([]string{fh2.Name()}); code != 1 {
   132  		t.Fatalf("expect exit 1, got: %d", code)
   133  	}
   134  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error submitting job") {
   135  		t.Fatalf("expect validation error, got: %s", out)
   136  	}
   137  	ui.ErrorWriter.Reset()
   138  
   139  	// Fails on connection failure (requires a valid job)
   140  	fh3, err := os.CreateTemp("", "nomad")
   141  	if err != nil {
   142  		t.Fatalf("err: %s", err)
   143  	}
   144  	defer os.Remove(fh3.Name())
   145  	_, err = fh3.WriteString(`
   146  job "job1" {
   147  	type = "service"
   148  	datacenters = [ "dc1" ]
   149  	group "group1" {
   150  		count = 1
   151  		task "task1" {
   152  			driver = "exec"
   153  			resources {
   154  				cpu = 1000
   155  				memory = 512
   156  			}
   157  		}
   158  	}
   159  }`)
   160  	if err != nil {
   161  		t.Fatalf("err: %s", err)
   162  	}
   163  	if code := cmd.Run([]string{"-address=nope", fh3.Name()}); code != 1 {
   164  		t.Fatalf("expected exit code 1, got: %d", code)
   165  	}
   166  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error submitting job") {
   167  		t.Fatalf("expected failed query error, got: %s", out)
   168  	}
   169  
   170  	// Fails on invalid check-index (requires a valid job)
   171  	if code := cmd.Run([]string{"-check-index=bad", fh3.Name()}); code != 1 {
   172  		t.Fatalf("expected exit code 1, got: %d", code)
   173  	}
   174  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "parsing check-index") {
   175  		t.Fatalf("expected parse error, got: %s", out)
   176  	}
   177  	ui.ErrorWriter.Reset()
   178  
   179  }
   180  
   181  func TestRunCommand_From_STDIN(t *testing.T) {
   182  	ci.Parallel(t)
   183  	stdinR, stdinW, err := os.Pipe()
   184  	if err != nil {
   185  		t.Fatalf("err: %s", err)
   186  	}
   187  
   188  	ui := cli.NewMockUi()
   189  	cmd := &JobRunCommand{
   190  		Meta:      Meta{Ui: ui},
   191  		JobGetter: JobGetter{testStdin: stdinR},
   192  	}
   193  
   194  	go func() {
   195  		stdinW.WriteString(`
   196  job "job1" {
   197    type = "service"
   198    datacenters = [ "dc1" ]
   199    group "group1" {
   200  		count = 1
   201  		task "task1" {
   202  			driver = "exec"
   203  			resources {
   204  				cpu = 1000
   205  				memory = 512
   206  			}
   207  		}
   208  	}
   209  }`)
   210  		stdinW.Close()
   211  	}()
   212  
   213  	args := []string{"-address=nope", "-"}
   214  	if code := cmd.Run(args); code != 1 {
   215  		t.Fatalf("expected exit code 1, got %d: %q", code, ui.ErrorWriter.String())
   216  	}
   217  
   218  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error submitting job") {
   219  		t.Fatalf("expected submission error, got: %s", out)
   220  	}
   221  	ui.ErrorWriter.Reset()
   222  }
   223  
   224  func TestRunCommand_From_URL(t *testing.T) {
   225  	ci.Parallel(t)
   226  	ui := cli.NewMockUi()
   227  	cmd := &JobRunCommand{
   228  		Meta: Meta{Ui: ui},
   229  	}
   230  
   231  	args := []string{"https://example.com/foo/bar"}
   232  	if code := cmd.Run(args); code != 1 {
   233  		t.Fatalf("expected exit code 1, got %d: %q", code, ui.ErrorWriter.String())
   234  	}
   235  
   236  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting jobfile") {
   237  		t.Fatalf("expected error getting jobfile, got: %s", out)
   238  	}
   239  }
   240  
   241  // TestRunCommand_JSON asserts that `nomad job run -json` accepts JSON jobs
   242  // with or without a top level Job key.
   243  func TestRunCommand_JSON(t *testing.T) {
   244  	ci.Parallel(t)
   245  	run := func(args ...string) (stdout string, stderr string, code int) {
   246  		ui := cli.NewMockUi()
   247  		cmd := &JobRunCommand{
   248  			Meta: Meta{Ui: ui},
   249  		}
   250  		t.Logf("run: nomad job run %s", strings.Join(args, " "))
   251  		code = cmd.Run(args)
   252  		return ui.OutputWriter.String(), ui.ErrorWriter.String(), code
   253  	}
   254  
   255  	// Agent startup is slow, do some work while we wait
   256  	agentReady := make(chan string)
   257  	go func() {
   258  		_, _, addr := testServer(t, false, nil)
   259  		agentReady <- addr
   260  	}()
   261  
   262  	// First convert HCL -> JSON with -output
   263  	stdout, stderr, code := run("-output", "asset/example-short.nomad.hcl")
   264  	require.Zero(t, code, stderr)
   265  	require.Empty(t, stderr)
   266  	require.NotEmpty(t, stdout)
   267  	t.Logf("run -output==> %s...", stdout[:12])
   268  
   269  	jsonFile := filepath.Join(t.TempDir(), "redis.json")
   270  	require.NoError(t, os.WriteFile(jsonFile, []byte(stdout), 0o640))
   271  
   272  	// Wait for agent to start and get its address
   273  	addr := ""
   274  	select {
   275  	case addr = <-agentReady:
   276  	case <-time.After(10 * time.Second):
   277  		t.Fatalf("timed out waiting for agent to start")
   278  	}
   279  
   280  	// Submit JSON
   281  	stdout, stderr, code = run("-detach", "-address", addr, "-json", jsonFile)
   282  	require.Zero(t, code, stderr)
   283  	require.Empty(t, stderr)
   284  
   285  	// Read the JSON from the API as it omits the Job envelope and
   286  	// therefore differs from -output
   287  	resp, err := http.Get(addr + "/v1/job/example")
   288  	require.NoError(t, err)
   289  	buf, err := io.ReadAll(resp.Body)
   290  	require.NoError(t, err)
   291  	require.NoError(t, resp.Body.Close())
   292  	require.NotEmpty(t, buf)
   293  	t.Logf("/v1/job/example==> %s...", string(buf[:12]))
   294  	require.NoError(t, os.WriteFile(jsonFile, buf, 0o640))
   295  
   296  	// Submit JSON
   297  	stdout, stderr, code = run("-detach", "-address", addr, "-json", jsonFile)
   298  	require.Zerof(t, code, "stderr: %s\njson: %s\n", stderr, string(buf))
   299  	require.Empty(t, stderr)
   300  	require.NotEmpty(t, stdout)
   301  }