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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/hernad/nomad/ci"
    13  	"github.com/hernad/nomad/helper/uuid"
    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  )
    21  
    22  // static check
    23  var _ cli.Command = &AllocExecCommand{}
    24  
    25  func TestAllocExecCommand_Fails(t *testing.T) {
    26  	ci.Parallel(t)
    27  	srv, client, url := testServer(t, true, nil)
    28  	defer srv.Shutdown()
    29  
    30  	cases := []struct {
    31  		name          string
    32  		args          []string
    33  		expectedError string
    34  	}{
    35  		{
    36  			"alloc id missing",
    37  			[]string{},
    38  			`An allocation ID is required`,
    39  		},
    40  		{
    41  			"alloc id too short",
    42  			[]string{"-address=" + url, "2", "/bin/bash"},
    43  			`Alloc ID must contain at least two characters`,
    44  		},
    45  		{
    46  			"alloc not found",
    47  			[]string{"-address=" + url, "26470238-5CF2-438F-8772-DC67CFB0705C", "/bin/bash"},
    48  			`No allocation(s) with prefix or id "26470238-5CF2-438F-8772-DC67CFB0705C"`,
    49  		},
    50  		{
    51  			"alloc not found with odd-length prefix",
    52  			[]string{"-address=" + url, "26470238-5CF", "/bin/bash"},
    53  			`No allocation(s) with prefix or id "26470238-5CF"`,
    54  		},
    55  		{
    56  			"job id missing",
    57  			[]string{"-job"},
    58  			`A job ID is required`,
    59  		},
    60  		{
    61  			"job not found",
    62  			[]string{"-address=" + url, "-job", "example", "/bin/bash"},
    63  			`job "example" doesn't exist`,
    64  		},
    65  		{
    66  			"command missing",
    67  			[]string{"-address=" + url, "26470238-5CF2-438F-8772-DC67CFB0705C"},
    68  			`A command is required`,
    69  		},
    70  		{
    71  			"connection failure",
    72  			[]string{"-address=nope", "26470238-5CF2-438F-8772-DC67CFB0705C", "/bin/bash"},
    73  			`Error querying allocation`,
    74  		},
    75  		{
    76  			"escape char too long",
    77  			[]string{"-address=" + url, "-e", "es", "26470238-5CF2-438F-8772-DC67CFB0705C", "/bin/bash"},
    78  			`-e requires 'none' or a single character`,
    79  		},
    80  	}
    81  
    82  	for _, c := range cases {
    83  		t.Run(c.name, func(t *testing.T) {
    84  			ui := cli.NewMockUi()
    85  			cmd := &AllocExecCommand{Meta: Meta{Ui: ui}}
    86  
    87  			code := cmd.Run(c.args)
    88  			must.One(t, code)
    89  
    90  			out := ui.ErrorWriter.String()
    91  			must.StrContains(t, out, c.expectedError)
    92  
    93  			ui.ErrorWriter.Reset()
    94  			ui.OutputWriter.Reset()
    95  		})
    96  	}
    97  
    98  	// Wait for a node to be ready
    99  	waitForNodes(t, client)
   100  
   101  	t.Run("non existent task", func(t *testing.T) {
   102  		ui := cli.NewMockUi()
   103  		cmd := &AllocExecCommand{Meta: Meta{Ui: ui}}
   104  
   105  		jobID := "job1_sfx"
   106  		job1 := testJob(jobID)
   107  
   108  		resp, _, err := client.Jobs().Register(job1, nil)
   109  		must.NoError(t, err)
   110  
   111  		code := waitForSuccess(ui, client, fullId, t, resp.EvalID)
   112  		must.Zero(t, code)
   113  
   114  		// get an alloc id
   115  		allocId1 := ""
   116  		if allocs, _, err := client.Jobs().Allocations(jobID, false, nil); err == nil {
   117  			if len(allocs) > 0 {
   118  				allocId1 = allocs[0].ID
   119  			}
   120  		}
   121  		must.NotEq(t, "", allocId1)
   122  
   123  		// by alloc
   124  		code = cmd.Run([]string{"-address=" + url, "-task=nonexistenttask1", allocId1, "/bin/bash"})
   125  		must.One(t, code)
   126  
   127  		out := ui.ErrorWriter.String()
   128  		must.StrContains(t, out, "Could not find task named: nonexistenttask1")
   129  
   130  		ui.ErrorWriter.Reset()
   131  
   132  		// by jobID
   133  		code = cmd.Run([]string{"-address=" + url, "-task=nonexistenttask2", "-job", jobID, "/bin/bash"})
   134  		must.One(t, code)
   135  
   136  		out = ui.ErrorWriter.String()
   137  		must.StrContains(t, out, "Could not find task named: nonexistenttask2")
   138  
   139  		ui.ErrorWriter.Reset()
   140  	})
   141  
   142  }
   143  
   144  func TestAllocExecCommand_AutocompleteArgs(t *testing.T) {
   145  	ci.Parallel(t)
   146  
   147  	srv, _, url := testServer(t, true, nil)
   148  	defer srv.Shutdown()
   149  
   150  	ui := cli.NewMockUi()
   151  	cmd := &AllocExecCommand{Meta: Meta{Ui: ui, flagAddress: url}}
   152  
   153  	// Create a fake alloc
   154  	state := srv.Agent.Server().State()
   155  	a := mock.Alloc()
   156  	must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{a}))
   157  
   158  	prefix := a.ID[:5]
   159  	args := complete.Args{Last: prefix}
   160  	predictor := cmd.AutocompleteArgs()
   161  
   162  	res := predictor.Predict(args)
   163  	must.Len(t, 1, res)
   164  	must.Eq(t, a.ID, res[0])
   165  }
   166  
   167  func TestAllocExecCommand_Run(t *testing.T) {
   168  	ci.Parallel(t)
   169  	srv, client, url := testServer(t, true, nil)
   170  	defer srv.Shutdown()
   171  
   172  	// Wait for a node to be ready
   173  	waitForNodes(t, client)
   174  
   175  	jobID := uuid.Generate()
   176  	job := testJob(jobID)
   177  	job.TaskGroups[0].Tasks[0].Config = map[string]interface{}{
   178  		"run_for": "10s",
   179  		"exec_command": map[string]interface{}{
   180  			"run_for":       "1ms",
   181  			"exit_code":     21,
   182  			"stdout_string": "sample stdout output\n",
   183  			"stderr_string": "sample stderr output\n",
   184  		},
   185  	}
   186  	resp, _, err := client.Jobs().Register(job, nil)
   187  	must.NoError(t, err)
   188  
   189  	evalUi := cli.NewMockUi()
   190  	code := waitForSuccess(evalUi, client, fullId, t, resp.EvalID)
   191  	must.Zero(t, code)
   192  
   193  	allocId := ""
   194  
   195  	testutil.WaitForResult(func() (bool, error) {
   196  		allocs, _, err := client.Jobs().Allocations(jobID, false, nil)
   197  		if err != nil {
   198  			return false, fmt.Errorf("failed to get allocations: %v", err)
   199  		}
   200  
   201  		if len(allocs) < 0 {
   202  			return false, fmt.Errorf("no allocations yet")
   203  		}
   204  
   205  		alloc := allocs[0]
   206  		if alloc.ClientStatus != "running" {
   207  			return false, fmt.Errorf("alloc is not running yet: %v", alloc.ClientStatus)
   208  		}
   209  
   210  		allocId = alloc.ID
   211  		return true, nil
   212  	}, func(err error) { must.NoError(t, err) })
   213  
   214  	cases := []struct {
   215  		name    string
   216  		command string
   217  		stdin   string
   218  
   219  		stdout   string
   220  		stderr   string
   221  		exitCode int
   222  	}{
   223  		{
   224  			name:     "basic stdout/err",
   225  			command:  "simplecommand",
   226  			stdin:    "",
   227  			stdout:   "sample stdout output",
   228  			stderr:   "sample stderr output",
   229  			exitCode: 21,
   230  		},
   231  		{
   232  			name:     "notty: streamining input",
   233  			command:  "showinput",
   234  			stdin:    "hello from stdin",
   235  			stdout:   "TTY: false\nStdin:\nhello from stdin",
   236  			exitCode: 0,
   237  		},
   238  	}
   239  
   240  	for _, c := range cases {
   241  		t.Run("by id: "+c.name, func(t *testing.T) {
   242  			ui := cli.NewMockUi()
   243  			var stdout, stderr bufferCloser
   244  
   245  			cmd := &AllocExecCommand{
   246  				Meta:   Meta{Ui: ui},
   247  				Stdin:  strings.NewReader(c.stdin),
   248  				Stdout: &stdout,
   249  				Stderr: &stderr,
   250  			}
   251  
   252  			code = cmd.Run([]string{"-address=" + url, allocId, c.command})
   253  			must.Eq(t, c.exitCode, code)
   254  			must.Eq(t, c.stdout, strings.TrimSpace(stdout.String()))
   255  			must.Eq(t, c.stderr, strings.TrimSpace(stderr.String()))
   256  		})
   257  		t.Run("by job: "+c.name, func(t *testing.T) {
   258  			ui := cli.NewMockUi()
   259  			var stdout, stderr bufferCloser
   260  
   261  			cmd := &AllocExecCommand{
   262  				Meta:   Meta{Ui: ui},
   263  				Stdin:  strings.NewReader(c.stdin),
   264  				Stdout: &stdout,
   265  				Stderr: &stderr,
   266  			}
   267  
   268  			code = cmd.Run([]string{"-address=" + url, "-job", jobID, c.command})
   269  			must.Eq(t, c.exitCode, code)
   270  			must.Eq(t, c.stdout, strings.TrimSpace(stdout.String()))
   271  			must.Eq(t, c.stderr, strings.TrimSpace(stderr.String()))
   272  		})
   273  	}
   274  }
   275  
   276  type bufferCloser struct {
   277  	bytes.Buffer
   278  }
   279  
   280  func (b *bufferCloser) Close() error {
   281  	return nil
   282  }