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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package agent
     5  
     6  import (
     7  	"math"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/hernad/nomad/ci"
    14  	"github.com/hernad/nomad/helper/pointer"
    15  	"github.com/mitchellh/cli"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  
    19  	"github.com/hernad/nomad/nomad/structs"
    20  	"github.com/hernad/nomad/nomad/structs/config"
    21  	"github.com/hernad/nomad/version"
    22  )
    23  
    24  func TestCommand_Implements(t *testing.T) {
    25  	ci.Parallel(t)
    26  	var _ cli.Command = &Command{}
    27  }
    28  
    29  func TestCommand_Args(t *testing.T) {
    30  	ci.Parallel(t)
    31  	tmpDir := t.TempDir()
    32  
    33  	type tcase struct {
    34  		args   []string
    35  		errOut string
    36  	}
    37  	tcases := []tcase{
    38  		{
    39  			[]string{},
    40  			"Must specify either server, client or dev mode for the agent.",
    41  		},
    42  		{
    43  			[]string{"-client", "-data-dir=" + tmpDir, "-bootstrap-expect=1"},
    44  			"Bootstrap requires server mode to be enabled",
    45  		},
    46  		{
    47  			[]string{"-data-dir=" + tmpDir, "-server", "-bootstrap-expect=1"},
    48  			"WARNING: Bootstrap mode enabled!",
    49  		},
    50  		{
    51  			[]string{"-data-dir=" + tmpDir, "-server", "-bootstrap-expect=2"},
    52  			"Number of bootstrap servers should ideally be set to an odd number",
    53  		},
    54  		{
    55  			[]string{"-server"},
    56  			"Must specify \"data_dir\" config option or \"data-dir\" CLI flag",
    57  		},
    58  		{
    59  			[]string{"-client", "-alloc-dir="},
    60  			"Must specify the state, alloc dir, and plugin dir if data-dir is omitted.",
    61  		},
    62  		{
    63  			[]string{"-client", "-data-dir=" + tmpDir, "-meta=invalid..key=inaccessible-value"},
    64  			"Invalid Client.Meta key: invalid..key",
    65  		},
    66  		{
    67  			[]string{"-client", "-data-dir=" + tmpDir, "-meta=.invalid=inaccessible-value"},
    68  			"Invalid Client.Meta key: .invalid",
    69  		},
    70  		{
    71  			[]string{"-client", "-data-dir=" + tmpDir, "-meta=invalid.=inaccessible-value"},
    72  			"Invalid Client.Meta key: invalid.",
    73  		},
    74  		{
    75  			[]string{"-client", "-node-pool=not@valid"},
    76  			"Invalid node pool",
    77  		},
    78  	}
    79  	for _, tc := range tcases {
    80  		// Make a new command. We preemptively close the shutdownCh
    81  		// so that the command exits immediately instead of blocking.
    82  		ui := cli.NewMockUi()
    83  		shutdownCh := make(chan struct{})
    84  		close(shutdownCh)
    85  		cmd := &Command{
    86  			Version:    version.GetVersion(),
    87  			Ui:         ui,
    88  			ShutdownCh: shutdownCh,
    89  		}
    90  
    91  		// To prevent test failures on hosts whose hostname resolves to
    92  		// a loopback address, we must append a bind address
    93  		tc.args = append(tc.args, "-bind=169.254.0.1")
    94  		if code := cmd.Run(tc.args); code != 1 {
    95  			t.Fatalf("args: %v\nexit: %d\n", tc.args, code)
    96  		}
    97  
    98  		if expect := tc.errOut; expect != "" {
    99  			out := ui.ErrorWriter.String()
   100  			if !strings.Contains(out, expect) {
   101  				t.Fatalf("expect to find %q\n\n%s", expect, out)
   102  			}
   103  		}
   104  	}
   105  }
   106  
   107  func TestCommand_MetaConfigValidation(t *testing.T) {
   108  	ci.Parallel(t)
   109  
   110  	tmpDir := t.TempDir()
   111  
   112  	tcases := []string{
   113  		"foo..invalid",
   114  		".invalid",
   115  		"invalid.",
   116  	}
   117  	for _, tc := range tcases {
   118  		configFile := filepath.Join(tmpDir, "conf1.hcl")
   119  		err := os.WriteFile(configFile, []byte(`client{
   120  			enabled = true
   121  			meta = {
   122  				"valid" = "yes"
   123  				"`+tc+`" = "kaboom!"
   124  				"nested.var" = "is nested"
   125  				"deeply.nested.var" = "is deeply nested"
   126  			}
   127      	}`), 0600)
   128  		if err != nil {
   129  			t.Fatalf("err: %s", err)
   130  		}
   131  
   132  		// Make a new command. We preemptively close the shutdownCh
   133  		// so that the command exits immediately instead of blocking.
   134  		ui := cli.NewMockUi()
   135  		shutdownCh := make(chan struct{})
   136  		close(shutdownCh)
   137  		cmd := &Command{
   138  			Version:    version.GetVersion(),
   139  			Ui:         ui,
   140  			ShutdownCh: shutdownCh,
   141  		}
   142  
   143  		// To prevent test failures on hosts whose hostname resolves to
   144  		// a loopback address, we must append a bind address
   145  		args := []string{"-client", "-data-dir=" + tmpDir, "-config=" + configFile, "-bind=169.254.0.1"}
   146  		if code := cmd.Run(args); code != 1 {
   147  			t.Fatalf("args: %v\nexit: %d\n", args, code)
   148  		}
   149  
   150  		expect := "Invalid Client.Meta key: " + tc
   151  		out := ui.ErrorWriter.String()
   152  		if !strings.Contains(out, expect) {
   153  			t.Fatalf("expect to find %q\n\n%s", expect, out)
   154  		}
   155  	}
   156  }
   157  
   158  func TestCommand_InvalidCharInDatacenter(t *testing.T) {
   159  	ci.Parallel(t)
   160  
   161  	tmpDir := t.TempDir()
   162  
   163  	tcases := []string{
   164  		"char-\\000-in-the-middle",
   165  		"ends-with-\\000",
   166  		"\\000-at-the-beginning",
   167  		"char-*-in-the-middle",
   168  		"ends-with-*",
   169  		"*-at-the-beginning",
   170  	}
   171  	for _, tc := range tcases {
   172  		configFile := filepath.Join(tmpDir, "conf1.hcl")
   173  		err := os.WriteFile(configFile, []byte(`
   174          datacenter = "`+tc+`"
   175          client{
   176  			enabled = true
   177      	}`), 0600)
   178  		if err != nil {
   179  			t.Fatalf("err: %s", err)
   180  		}
   181  
   182  		// Make a new command. We preemptively close the shutdownCh
   183  		// so that the command exits immediately instead of blocking.
   184  		ui := cli.NewMockUi()
   185  		shutdownCh := make(chan struct{})
   186  		close(shutdownCh)
   187  		cmd := &Command{
   188  			Version:    version.GetVersion(),
   189  			Ui:         ui,
   190  			ShutdownCh: shutdownCh,
   191  		}
   192  
   193  		// To prevent test failures on hosts whose hostname resolves to
   194  		// a loopback address, we must append a bind address
   195  		args := []string{"-client", "-data-dir=" + tmpDir, "-config=" + configFile, "-bind=169.254.0.1"}
   196  		if code := cmd.Run(args); code != 1 {
   197  			t.Fatalf("args: %v\nexit: %d\n", args, code)
   198  		}
   199  
   200  		out := ui.ErrorWriter.String()
   201  		exp := "Datacenter contains invalid characters (null or '*')"
   202  		if !strings.Contains(out, exp) {
   203  			t.Fatalf("expect to find %q\n\n%s", exp, out)
   204  		}
   205  	}
   206  }
   207  
   208  func TestCommand_NullCharInRegion(t *testing.T) {
   209  	ci.Parallel(t)
   210  
   211  	tmpDir := t.TempDir()
   212  
   213  	tcases := []string{
   214  		"char-\\000-in-the-middle",
   215  		"ends-with-\\000",
   216  		"\\000-at-the-beginning",
   217  	}
   218  	for _, tc := range tcases {
   219  		configFile := filepath.Join(tmpDir, "conf1.hcl")
   220  		err := os.WriteFile(configFile, []byte(`
   221          region = "`+tc+`"
   222          client{
   223  			enabled = true
   224      	}`), 0600)
   225  		if err != nil {
   226  			t.Fatalf("err: %s", err)
   227  		}
   228  
   229  		// Make a new command. We preemptively close the shutdownCh
   230  		// so that the command exits immediately instead of blocking.
   231  		ui := cli.NewMockUi()
   232  		shutdownCh := make(chan struct{})
   233  		close(shutdownCh)
   234  		cmd := &Command{
   235  			Version:    version.GetVersion(),
   236  			Ui:         ui,
   237  			ShutdownCh: shutdownCh,
   238  		}
   239  
   240  		// To prevent test failures on hosts whose hostname resolves to
   241  		// a loopback address, we must append a bind address
   242  		args := []string{"-client", "-data-dir=" + tmpDir, "-config=" + configFile, "-bind=169.254.0.1"}
   243  		if code := cmd.Run(args); code != 1 {
   244  			t.Fatalf("args: %v\nexit: %d\n", args, code)
   245  		}
   246  
   247  		out := ui.ErrorWriter.String()
   248  		exp := "Region contains invalid characters"
   249  		if !strings.Contains(out, exp) {
   250  			t.Fatalf("expect to find %q\n\n%s", exp, out)
   251  		}
   252  	}
   253  }
   254  
   255  // TestIsValidConfig asserts that invalid configurations return false.
   256  func TestIsValidConfig(t *testing.T) {
   257  	ci.Parallel(t)
   258  
   259  	cases := []struct {
   260  		name string
   261  		conf Config // merged into DefaultConfig()
   262  
   263  		// err should appear in error output; success expected if err
   264  		// is empty
   265  		err string
   266  	}{
   267  		{
   268  			name: "Default",
   269  			conf: Config{
   270  				DataDir: "/tmp",
   271  				Client:  &ClientConfig{Enabled: true},
   272  			},
   273  		},
   274  		{
   275  			name: "NoMode",
   276  			conf: Config{
   277  				Client: &ClientConfig{Enabled: false},
   278  				Server: &ServerConfig{Enabled: false},
   279  			},
   280  			err: "Must specify either",
   281  		},
   282  		{
   283  			name: "InvalidRegion",
   284  			conf: Config{
   285  				Client: &ClientConfig{
   286  					Enabled: true,
   287  				},
   288  				Region: "Hello\000World",
   289  			},
   290  			err: "Region contains",
   291  		},
   292  		{
   293  			name: "InvalidDatacenter",
   294  			conf: Config{
   295  				Client: &ClientConfig{
   296  					Enabled: true,
   297  				},
   298  				Datacenter: "Hello\000World",
   299  			},
   300  			err: "Datacenter contains",
   301  		},
   302  		{
   303  			name: "RelativeDir",
   304  			conf: Config{
   305  				Client: &ClientConfig{
   306  					Enabled: true,
   307  				},
   308  				DataDir: "foo/bar",
   309  			},
   310  			err: "must be given as an absolute",
   311  		},
   312  		{
   313  			name: "InvalidNodePoolChar",
   314  			conf: Config{
   315  				Client: &ClientConfig{
   316  					Enabled:  true,
   317  					NodePool: "not@valid",
   318  				},
   319  			},
   320  			err: "Invalid node pool",
   321  		},
   322  		{
   323  			name: "InvalidNodePoolName",
   324  			conf: Config{
   325  				Client: &ClientConfig{
   326  					Enabled:  true,
   327  					NodePool: structs.NodePoolAll,
   328  				},
   329  			},
   330  			err: "not allowed",
   331  		},
   332  		{
   333  			name: "NegativeMinDynamicPort",
   334  			conf: Config{
   335  				Client: &ClientConfig{
   336  					Enabled:        true,
   337  					MinDynamicPort: -1,
   338  				},
   339  			},
   340  			err: "min_dynamic_port",
   341  		},
   342  		{
   343  			name: "NegativeMaxDynamicPort",
   344  			conf: Config{
   345  				Client: &ClientConfig{
   346  					Enabled:        true,
   347  					MaxDynamicPort: -1,
   348  				},
   349  			},
   350  			err: "max_dynamic_port",
   351  		},
   352  		{
   353  			name: "BigMinDynamicPort",
   354  			conf: Config{
   355  				Client: &ClientConfig{
   356  					Enabled:        true,
   357  					MinDynamicPort: math.MaxInt32,
   358  				},
   359  			},
   360  			err: "min_dynamic_port",
   361  		},
   362  		{
   363  			name: "BigMaxDynamicPort",
   364  			conf: Config{
   365  				Client: &ClientConfig{
   366  					Enabled:        true,
   367  					MaxDynamicPort: math.MaxInt32,
   368  				},
   369  			},
   370  			err: "max_dynamic_port",
   371  		},
   372  		{
   373  			name: "MinMaxDynamicPortSwitched",
   374  			conf: Config{
   375  				Client: &ClientConfig{
   376  					Enabled:        true,
   377  					MinDynamicPort: 5000,
   378  					MaxDynamicPort: 4000,
   379  				},
   380  			},
   381  			err: "and max",
   382  		},
   383  		{
   384  			name: "DynamicPortOk",
   385  			conf: Config{
   386  				DataDir: "/tmp",
   387  				Client: &ClientConfig{
   388  					Enabled:        true,
   389  					MinDynamicPort: 4000,
   390  					MaxDynamicPort: 5000,
   391  				},
   392  			},
   393  		},
   394  		{
   395  			name: "BadReservedPorts",
   396  			conf: Config{
   397  				Client: &ClientConfig{
   398  					Enabled: true,
   399  					Reserved: &Resources{
   400  						ReservedPorts: "3-2147483647",
   401  					},
   402  				},
   403  			},
   404  			err: `reserved.reserved_ports "3-2147483647" invalid: port must be < 65536 but found 2147483647`,
   405  		},
   406  		{
   407  			name: "BadHostNetworkReservedPorts",
   408  			conf: Config{
   409  				Client: &ClientConfig{
   410  					Enabled: true,
   411  					HostNetworks: []*structs.ClientHostNetworkConfig{
   412  						&structs.ClientHostNetworkConfig{
   413  							Name:          "test",
   414  							ReservedPorts: "3-2147483647",
   415  						},
   416  					},
   417  				},
   418  			},
   419  			err: `host_network["test"].reserved_ports "3-2147483647" invalid: port must be < 65536 but found 2147483647`,
   420  		},
   421  		{
   422  			name: "BadArtifact",
   423  			conf: Config{
   424  				Client: &ClientConfig{
   425  					Enabled: true,
   426  					Artifact: &config.ArtifactConfig{
   427  						HTTPReadTimeout: pointer.Of("-10m"),
   428  					},
   429  				},
   430  			},
   431  			err: "client.artifact block invalid: http_read_timeout must be > 0",
   432  		},
   433  		{
   434  			name: "BadHostVolumeConfig",
   435  			conf: Config{
   436  				DataDir: "/tmp",
   437  				Client: &ClientConfig{
   438  					Enabled: true,
   439  					HostVolumes: []*structs.ClientHostVolumeConfig{
   440  						{
   441  							Name:     "test",
   442  							ReadOnly: true,
   443  						},
   444  						{
   445  							Name:     "test",
   446  							ReadOnly: true,
   447  							Path:     "/random/path",
   448  						},
   449  					},
   450  				},
   451  			},
   452  			err: "Missing path in host_volume config",
   453  		},
   454  		{
   455  			name: "ValidHostVolumeConfig",
   456  			conf: Config{
   457  				DataDir: "/tmp",
   458  				Client: &ClientConfig{
   459  					Enabled: true,
   460  					HostVolumes: []*structs.ClientHostVolumeConfig{
   461  						{
   462  							Name:     "test",
   463  							ReadOnly: true,
   464  							Path:     "/random/path1",
   465  						},
   466  						{
   467  							Name:     "test",
   468  							ReadOnly: true,
   469  							Path:     "/random/path2",
   470  						},
   471  					},
   472  				},
   473  			},
   474  		},
   475  	}
   476  
   477  	for _, tc := range cases {
   478  		t.Run(tc.name, func(t *testing.T) {
   479  			mui := cli.NewMockUi()
   480  			cmd := &Command{Ui: mui}
   481  			config := DefaultConfig().Merge(&tc.conf)
   482  			result := cmd.IsValidConfig(config, DefaultConfig())
   483  			if tc.err == "" {
   484  				// No error expected
   485  				assert.True(t, result, mui.ErrorWriter.String())
   486  				return
   487  			}
   488  
   489  			// Error expected
   490  			assert.False(t, result)
   491  			require.Contains(t, mui.ErrorWriter.String(), tc.err)
   492  			t.Logf("%s returned: %s", tc.name, mui.ErrorWriter.String())
   493  		})
   494  	}
   495  }