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

     1  package command
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"os"
    10  	"reflect"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/hashicorp/nomad/api"
    16  	"github.com/hashicorp/nomad/ci"
    17  	"github.com/hashicorp/nomad/helper/flatmap"
    18  	"github.com/hashicorp/nomad/helper/pointer"
    19  	"github.com/kr/pretty"
    20  	"github.com/mitchellh/cli"
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  func TestHelpers_FormatKV(t *testing.T) {
    25  	ci.Parallel(t)
    26  	in := []string{"alpha|beta", "charlie|delta", "echo|"}
    27  	out := formatKV(in)
    28  
    29  	expect := "alpha   = beta\n"
    30  	expect += "charlie = delta\n"
    31  	expect += "echo    = <none>"
    32  
    33  	if out != expect {
    34  		t.Fatalf("expect: %s, got: %s", expect, out)
    35  	}
    36  }
    37  
    38  func TestHelpers_FormatList(t *testing.T) {
    39  	ci.Parallel(t)
    40  	in := []string{"alpha|beta||delta"}
    41  	out := formatList(in)
    42  
    43  	expect := "alpha  beta  <none>  delta"
    44  
    45  	if out != expect {
    46  		t.Fatalf("expect: %s, got: %s", expect, out)
    47  	}
    48  }
    49  
    50  func TestHelpers_NodeID(t *testing.T) {
    51  	ci.Parallel(t)
    52  	srv, _, _ := testServer(t, false, nil)
    53  	defer srv.Shutdown()
    54  
    55  	meta := Meta{Ui: cli.NewMockUi()}
    56  	client, err := meta.Client()
    57  	if err != nil {
    58  		t.FailNow()
    59  	}
    60  
    61  	// This is because there is no client
    62  	if _, err := getLocalNodeID(client); err == nil {
    63  		t.Fatalf("getLocalNodeID() should fail")
    64  	}
    65  }
    66  
    67  func TestHelpers_LineLimitReader_NoTimeLimit(t *testing.T) {
    68  	ci.Parallel(t)
    69  	helloString := `hello
    70  world
    71  this
    72  is
    73  a
    74  test`
    75  
    76  	noLines := "jskdfhjasdhfjkajkldsfdlsjkahfkjdsafa"
    77  
    78  	cases := []struct {
    79  		Input       string
    80  		Output      string
    81  		Lines       int
    82  		SearchLimit int
    83  	}{
    84  		{
    85  			Input:       helloString,
    86  			Output:      helloString,
    87  			Lines:       6,
    88  			SearchLimit: 1000,
    89  		},
    90  		{
    91  			Input: helloString,
    92  			Output: `world
    93  this
    94  is
    95  a
    96  test`,
    97  			Lines:       5,
    98  			SearchLimit: 1000,
    99  		},
   100  		{
   101  			Input:       helloString,
   102  			Output:      `test`,
   103  			Lines:       1,
   104  			SearchLimit: 1000,
   105  		},
   106  		{
   107  			Input:       helloString,
   108  			Output:      "",
   109  			Lines:       0,
   110  			SearchLimit: 1000,
   111  		},
   112  		{
   113  			Input:       helloString,
   114  			Output:      helloString,
   115  			Lines:       6,
   116  			SearchLimit: 1, // Exceed the limit
   117  		},
   118  		{
   119  			Input:       noLines,
   120  			Output:      noLines,
   121  			Lines:       10,
   122  			SearchLimit: 1000,
   123  		},
   124  		{
   125  			Input:       noLines,
   126  			Output:      noLines,
   127  			Lines:       10,
   128  			SearchLimit: 2,
   129  		},
   130  	}
   131  
   132  	for i, c := range cases {
   133  		in := ioutil.NopCloser(strings.NewReader(c.Input))
   134  		limit := NewLineLimitReader(in, c.Lines, c.SearchLimit, 0)
   135  		outBytes, err := ioutil.ReadAll(limit)
   136  		if err != nil {
   137  			t.Fatalf("case %d failed: %v", i, err)
   138  		}
   139  
   140  		out := string(outBytes)
   141  		if out != c.Output {
   142  			t.Fatalf("case %d: got %q; want %q", i, out, c.Output)
   143  		}
   144  	}
   145  }
   146  
   147  type testReadCloser struct {
   148  	data chan []byte
   149  }
   150  
   151  func (t *testReadCloser) Read(p []byte) (n int, err error) {
   152  	select {
   153  	case b, ok := <-t.data:
   154  		if !ok {
   155  			return 0, io.EOF
   156  		}
   157  
   158  		return copy(p, b), nil
   159  	case <-time.After(10 * time.Millisecond):
   160  		return 0, nil
   161  	}
   162  }
   163  
   164  func (t *testReadCloser) Close() error {
   165  	close(t.data)
   166  	return nil
   167  }
   168  
   169  func TestHelpers_LineLimitReader_TimeLimit(t *testing.T) {
   170  	ci.Parallel(t)
   171  	// Create the test reader
   172  	in := &testReadCloser{data: make(chan []byte)}
   173  
   174  	// Set up the reader such that it won't hit the line/buffer limit and could
   175  	// only terminate if it hits the time limit
   176  	limit := NewLineLimitReader(in, 1000, 1000, 100*time.Millisecond)
   177  
   178  	expected := []byte("hello world")
   179  
   180  	errCh := make(chan error)
   181  	resultCh := make(chan []byte)
   182  	go func() {
   183  		defer close(resultCh)
   184  		defer close(errCh)
   185  		outBytes, err := ioutil.ReadAll(limit)
   186  		if err != nil {
   187  			errCh <- fmt.Errorf("ReadAll failed: %v", err)
   188  			return
   189  		}
   190  		resultCh <- outBytes
   191  	}()
   192  
   193  	// Send the data
   194  	in.data <- expected
   195  	in.Close()
   196  
   197  	select {
   198  	case err := <-errCh:
   199  		if err != nil {
   200  			t.Fatalf("ReadAll: %v", err)
   201  		}
   202  	case outBytes := <-resultCh:
   203  		if !reflect.DeepEqual(outBytes, expected) {
   204  			t.Fatalf("got:%s, expected,%s", string(outBytes), string(expected))
   205  		}
   206  	case <-time.After(1 * time.Second):
   207  		t.Fatalf("did not exit by time limit")
   208  	}
   209  }
   210  
   211  const (
   212  	job = `job "job1" {
   213    type        = "service"
   214    datacenters = ["dc1"]
   215    group "group1" {
   216      count = 1
   217      task "task1" {
   218        driver = "exec"
   219        resources {}
   220      }
   221      restart {
   222        attempts = 10
   223        mode     = "delay"
   224        interval = "15s"
   225      }
   226    }
   227  }`
   228  )
   229  
   230  var (
   231  	expectedApiJob = &api.Job{
   232  		ID:          pointer.Of("job1"),
   233  		Name:        pointer.Of("job1"),
   234  		Type:        pointer.Of("service"),
   235  		Datacenters: []string{"dc1"},
   236  		TaskGroups: []*api.TaskGroup{
   237  			{
   238  				Name:  pointer.Of("group1"),
   239  				Count: pointer.Of(1),
   240  				RestartPolicy: &api.RestartPolicy{
   241  					Attempts: pointer.Of(10),
   242  					Interval: pointer.Of(15 * time.Second),
   243  					Mode:     pointer.Of("delay"),
   244  				},
   245  
   246  				Tasks: []*api.Task{
   247  					{
   248  						Driver:    "exec",
   249  						Name:      "task1",
   250  						Resources: &api.Resources{},
   251  					},
   252  				},
   253  			},
   254  		},
   255  	}
   256  )
   257  
   258  // Test APIJob with local jobfile
   259  func TestJobGetter_LocalFile(t *testing.T) {
   260  	ci.Parallel(t)
   261  	fh, err := ioutil.TempFile("", "nomad")
   262  	if err != nil {
   263  		t.Fatalf("err: %s", err)
   264  	}
   265  	defer os.Remove(fh.Name())
   266  	_, err = fh.WriteString(job)
   267  	if err != nil {
   268  		t.Fatalf("err: %s", err)
   269  	}
   270  
   271  	j := &JobGetter{}
   272  	aj, err := j.ApiJob(fh.Name())
   273  	if err != nil {
   274  		t.Fatalf("err: %s", err)
   275  	}
   276  
   277  	if !reflect.DeepEqual(expectedApiJob, aj) {
   278  		eflat := flatmap.Flatten(expectedApiJob, nil, false)
   279  		aflat := flatmap.Flatten(aj, nil, false)
   280  		t.Fatalf("got:\n%v\nwant:\n%v", aflat, eflat)
   281  	}
   282  }
   283  
   284  // TestJobGetter_LocalFile_InvalidHCL2 asserts that a custom message is emited
   285  // if the file is a valid HCL1 but not HCL2
   286  func TestJobGetter_LocalFile_InvalidHCL2(t *testing.T) {
   287  	ci.Parallel(t)
   288  
   289  	cases := []struct {
   290  		name              string
   291  		hcl               string
   292  		expectHCL1Message bool
   293  	}{
   294  		{
   295  			"invalid HCL",
   296  			"nothing",
   297  			false,
   298  		},
   299  		{
   300  			"invalid HCL2",
   301  			`job "example" {
   302    meta { "key.with.dot" = "b" }
   303  }`,
   304  			true,
   305  		},
   306  	}
   307  
   308  	for _, c := range cases {
   309  		t.Run(c.name, func(t *testing.T) {
   310  			fh, err := ioutil.TempFile("", "nomad")
   311  			require.NoError(t, err)
   312  			defer os.Remove(fh.Name())
   313  			defer fh.Close()
   314  
   315  			_, err = fh.WriteString(c.hcl)
   316  			require.NoError(t, err)
   317  
   318  			j := &JobGetter{}
   319  			_, err = j.ApiJob(fh.Name())
   320  			require.Error(t, err)
   321  
   322  			exptMessage := "Failed to parse using HCL 2. Use the HCL 1"
   323  			if c.expectHCL1Message {
   324  				require.Contains(t, err.Error(), exptMessage)
   325  			} else {
   326  				require.NotContains(t, err.Error(), exptMessage)
   327  			}
   328  		})
   329  	}
   330  }
   331  
   332  // TestJobGetter_HCL2_Variables asserts variable arguments from CLI
   333  // and varfiles are both honored
   334  func TestJobGetter_HCL2_Variables(t *testing.T) {
   335  
   336  	hcl := `
   337  variables {
   338    var1 = "default-val"
   339    var2 = "default-val"
   340    var3 = "default-val"
   341    var4 = "default-val"
   342  }
   343  
   344  job "example" {
   345    datacenters = ["${var.var1}", "${var.var2}", "${var.var3}", "${var.var4}"]
   346  }
   347  `
   348  	t.Setenv("NOMAD_VAR_var4", "from-envvar")
   349  
   350  	cliArgs := []string{`var2=from-cli`}
   351  	fileVars := `var3 = "from-varfile"`
   352  	expected := []string{"default-val", "from-cli", "from-varfile", "from-envvar"}
   353  
   354  	hclf, err := ioutil.TempFile("", "hcl")
   355  	require.NoError(t, err)
   356  	defer os.Remove(hclf.Name())
   357  	defer hclf.Close()
   358  
   359  	_, err = hclf.WriteString(hcl)
   360  	require.NoError(t, err)
   361  
   362  	vf, err := ioutil.TempFile("", "var.hcl")
   363  	require.NoError(t, err)
   364  	defer os.Remove(vf.Name())
   365  	defer vf.Close()
   366  
   367  	_, err = vf.WriteString(fileVars + "\n")
   368  	require.NoError(t, err)
   369  
   370  	j, err := (&JobGetter{}).ApiJobWithArgs(hclf.Name(), cliArgs, []string{vf.Name()}, true)
   371  	require.NoError(t, err)
   372  
   373  	require.NotNil(t, j)
   374  	require.Equal(t, expected, j.Datacenters)
   375  }
   376  
   377  func TestJobGetter_HCL2_Variables_StrictFalse(t *testing.T) {
   378  
   379  	hcl := `
   380  variables {
   381    var1 = "default-val"
   382    var2 = "default-val"
   383    var3 = "default-val"
   384    var4 = "default-val"
   385  }
   386  
   387  job "example" {
   388    datacenters = ["${var.var1}", "${var.var2}", "${var.var3}", "${var.var4}"]
   389  }
   390  `
   391  
   392  	t.Setenv("NOMAD_VAR_var4", "from-envvar")
   393  
   394  	// Both the CLI and var file contain variables that are not used with the
   395  	// template and therefore would error, if hcl2-strict was true.
   396  	cliArgs := []string{`var2=from-cli`, `unsedVar1=from-cli`}
   397  	fileVars := `
   398  var3 = "from-varfile"
   399  unsedVar2 = "from-varfile"
   400  `
   401  	expected := []string{"default-val", "from-cli", "from-varfile", "from-envvar"}
   402  
   403  	hclf, err := ioutil.TempFile("", "hcl")
   404  	require.NoError(t, err)
   405  	defer os.Remove(hclf.Name())
   406  	defer hclf.Close()
   407  
   408  	_, err = hclf.WriteString(hcl)
   409  	require.NoError(t, err)
   410  
   411  	vf, err := ioutil.TempFile("", "var.hcl")
   412  	require.NoError(t, err)
   413  	defer os.Remove(vf.Name())
   414  	defer vf.Close()
   415  
   416  	_, err = vf.WriteString(fileVars + "\n")
   417  	require.NoError(t, err)
   418  
   419  	j, err := (&JobGetter{}).ApiJobWithArgs(hclf.Name(), cliArgs, []string{vf.Name()}, false)
   420  	require.NoError(t, err)
   421  
   422  	require.NotNil(t, j)
   423  	require.Equal(t, expected, j.Datacenters)
   424  }
   425  
   426  // Test StructJob with jobfile from HTTP Server
   427  func TestJobGetter_HTTPServer(t *testing.T) {
   428  	ci.Parallel(t)
   429  	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   430  		fmt.Fprintf(w, job)
   431  	})
   432  	go http.ListenAndServe("127.0.0.1:12345", nil)
   433  
   434  	// Wait until HTTP Server starts certainly
   435  	time.Sleep(100 * time.Millisecond)
   436  
   437  	j := &JobGetter{}
   438  	aj, err := j.ApiJob("http://127.0.0.1:12345/")
   439  	if err != nil {
   440  		t.Fatalf("err: %s", err)
   441  	}
   442  	if !reflect.DeepEqual(expectedApiJob, aj) {
   443  		for _, d := range pretty.Diff(expectedApiJob, aj) {
   444  			t.Logf(d)
   445  		}
   446  		t.Fatalf("Unexpected file")
   447  	}
   448  }
   449  
   450  func TestJobGetter_Validate(t *testing.T) {
   451  	cases := []struct {
   452  		name        string
   453  		jg          JobGetter
   454  		errContains string
   455  	}{
   456  		{
   457  			"StrictAndHCL1",
   458  			JobGetter{
   459  				HCL1:   true,
   460  				Strict: true,
   461  			},
   462  			"HCLv1 and HCLv2 strict",
   463  		},
   464  		{
   465  			"JSONandHCL1",
   466  			JobGetter{
   467  				HCL1: true,
   468  				JSON: true,
   469  			},
   470  			"HCL and JSON",
   471  		},
   472  		{
   473  			"VarsAndHCL1",
   474  			JobGetter{
   475  				HCL1: true,
   476  				Vars: []string{"foo"},
   477  			},
   478  			"variables with HCLv1",
   479  		},
   480  		{
   481  			"VarFilesAndHCL1",
   482  			JobGetter{
   483  				HCL1:     true,
   484  				VarFiles: []string{"foo.var"},
   485  			},
   486  			"variables with HCLv1",
   487  		},
   488  		{
   489  			"VarsAndJSON",
   490  			JobGetter{
   491  				JSON: true,
   492  				Vars: []string{"foo"},
   493  			},
   494  			"variables with JSON",
   495  		},
   496  		{
   497  			"VarFilesAndJSON",
   498  			JobGetter{
   499  				JSON:     true,
   500  				VarFiles: []string{"foo.var"},
   501  			},
   502  			"variables with JSON files",
   503  		},
   504  		{
   505  			"JSON_OK",
   506  			JobGetter{
   507  				JSON: true,
   508  			},
   509  			"",
   510  		},
   511  	}
   512  
   513  	for _, tc := range cases {
   514  		t.Run(tc.name, func(t *testing.T) {
   515  			err := tc.jg.Validate()
   516  
   517  			switch tc.errContains {
   518  			case "":
   519  				require.NoError(t, err)
   520  			default:
   521  				require.ErrorContains(t, err, tc.errContains)
   522  			}
   523  
   524  		})
   525  	}
   526  }
   527  
   528  func TestPrettyTimeDiff(t *testing.T) {
   529  	// Grab the time and truncate to the nearest second. This allows our tests
   530  	// to be deterministic since we don't have to worry about rounding.
   531  	now := time.Now().Truncate(time.Second)
   532  
   533  	test_cases := []struct {
   534  		t1  time.Time
   535  		t2  time.Time
   536  		exp string
   537  	}{
   538  		{now, time.Unix(0, 0), ""}, // This is the upgrade path case
   539  		{now, now.Add(-10 * time.Millisecond), "0s ago"},
   540  		{now, now.Add(-740 * time.Second), "12m20s ago"},
   541  		{now, now.Add(-12 * time.Minute), "12m ago"},
   542  		{now, now.Add(-60 * time.Minute), "1h ago"},
   543  		{now, now.Add(-80 * time.Minute), "1h20m ago"},
   544  		{now, now.Add(-6 * time.Hour), "6h ago"},
   545  		{now.Add(-6 * time.Hour), now, "6h from now"},
   546  		{now, now.Add(-22165 * time.Second), "6h9m ago"},
   547  		{now, now.Add(-100 * time.Hour), "4d4h ago"},
   548  		{now, now.Add(-438000 * time.Minute), "10mo4d ago"},
   549  		{now, now.Add(-20460 * time.Hour), "2y4mo ago"},
   550  	}
   551  	for _, tc := range test_cases {
   552  		t.Run(tc.exp, func(t *testing.T) {
   553  			out := prettyTimeDiff(tc.t2, tc.t1)
   554  			if out != tc.exp {
   555  				t.Fatalf("expected :%v but got :%v", tc.exp, out)
   556  			}
   557  		})
   558  	}
   559  
   560  	var t1 time.Time
   561  	out := prettyTimeDiff(t1, time.Now())
   562  
   563  	if out != "" {
   564  		t.Fatalf("Expected empty output but got:%v", out)
   565  	}
   566  
   567  }
   568  
   569  // TestUiErrorWriter asserts that writer buffers and
   570  func TestUiErrorWriter(t *testing.T) {
   571  	ci.Parallel(t)
   572  
   573  	var outBuf, errBuf bytes.Buffer
   574  	ui := &cli.BasicUi{
   575  		Writer:      &outBuf,
   576  		ErrorWriter: &errBuf,
   577  	}
   578  
   579  	w := &uiErrorWriter{ui: ui}
   580  
   581  	inputs := []string{
   582  		"some line\n",
   583  		"multiple\nlines\r\nhere",
   584  		" with  followup\nand",
   585  		" more lines ",
   586  		" without new line ",
   587  		"until here\nand then",
   588  		"some more",
   589  	}
   590  
   591  	partialAcc := ""
   592  	for _, in := range inputs {
   593  		n, err := w.Write([]byte(in))
   594  		require.NoError(t, err)
   595  		require.Equal(t, len(in), n)
   596  
   597  		// assert that writer emits partial result until last new line
   598  		partialAcc += strings.ReplaceAll(in, "\r\n", "\n")
   599  		lastNL := strings.LastIndex(partialAcc, "\n")
   600  		require.Equal(t, partialAcc[:lastNL+1], errBuf.String())
   601  	}
   602  
   603  	require.Empty(t, outBuf.String())
   604  
   605  	// note that the \r\n got replaced by \n
   606  	expectedErr := "some line\nmultiple\nlines\nhere with  followup\nand more lines  without new line until here\n"
   607  	require.Equal(t, expectedErr, errBuf.String())
   608  
   609  	// close emits the final line
   610  	err := w.Close()
   611  	require.NoError(t, err)
   612  
   613  	expectedErr += "and thensome more\n"
   614  	require.Equal(t, expectedErr, errBuf.String())
   615  }