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