github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/client/allocrunner/taskrunner/envoybootstrap_hook_test.go (about)

     1  // +build !windows
     2  // todo(shoenig): Once Connect is supported on Windows, we'll need to make this
     3  //  set of tests work there too.
     4  
     5  package taskrunner
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"io/ioutil"
    11  	"os"
    12  	"path/filepath"
    13  	"testing"
    14  
    15  	consulapi "github.com/hashicorp/consul/api"
    16  	consultest "github.com/hashicorp/consul/sdk/testutil"
    17  	"github.com/hashicorp/nomad/client/allocdir"
    18  	"github.com/hashicorp/nomad/client/allocrunner/interfaces"
    19  	"github.com/hashicorp/nomad/client/taskenv"
    20  	"github.com/hashicorp/nomad/client/testutil"
    21  	agentconsul "github.com/hashicorp/nomad/command/agent/consul"
    22  	"github.com/hashicorp/nomad/helper"
    23  	"github.com/hashicorp/nomad/helper/args"
    24  	"github.com/hashicorp/nomad/helper/testlog"
    25  	"github.com/hashicorp/nomad/helper/uuid"
    26  	"github.com/hashicorp/nomad/nomad/mock"
    27  	"github.com/hashicorp/nomad/nomad/structs"
    28  	"github.com/hashicorp/nomad/nomad/structs/config"
    29  	"github.com/stretchr/testify/require"
    30  	"golang.org/x/sys/unix"
    31  )
    32  
    33  var _ interfaces.TaskPrestartHook = (*envoyBootstrapHook)(nil)
    34  
    35  func writeTmp(t *testing.T, s string, fm os.FileMode) string {
    36  	dir, err := ioutil.TempDir("", "envoy-")
    37  	require.NoError(t, err)
    38  
    39  	fPath := filepath.Join(dir, sidsTokenFile)
    40  	err = ioutil.WriteFile(fPath, []byte(s), fm)
    41  	require.NoError(t, err)
    42  
    43  	return dir
    44  }
    45  
    46  func TestEnvoyBootstrapHook_maybeLoadSIToken(t *testing.T) {
    47  	t.Parallel()
    48  
    49  	// This test fails when running as root because the test case for checking
    50  	// the error condition when the file is unreadable fails (root can read the
    51  	// file even though the permissions are set to 0200).
    52  	if unix.Geteuid() == 0 {
    53  		t.Skip("test only works as non-root")
    54  	}
    55  
    56  	t.Run("file does not exist", func(t *testing.T) {
    57  		h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)})
    58  		cfg, err := h.maybeLoadSIToken("task1", "/does/not/exist")
    59  		require.NoError(t, err) // absence of token is not an error
    60  		require.Equal(t, "", cfg)
    61  	})
    62  
    63  	t.Run("load token from file", func(t *testing.T) {
    64  		token := uuid.Generate()
    65  		f := writeTmp(t, token, 0440)
    66  		defer cleanupDir(t, f)
    67  
    68  		h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)})
    69  		cfg, err := h.maybeLoadSIToken("task1", f)
    70  		require.NoError(t, err)
    71  		require.Equal(t, token, cfg)
    72  	})
    73  
    74  	t.Run("file is unreadable", func(t *testing.T) {
    75  		token := uuid.Generate()
    76  		f := writeTmp(t, token, 0200)
    77  		defer cleanupDir(t, f)
    78  
    79  		h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)})
    80  		cfg, err := h.maybeLoadSIToken("task1", f)
    81  		require.Error(t, err)
    82  		require.False(t, os.IsNotExist(err))
    83  		require.Equal(t, "", cfg)
    84  	})
    85  }
    86  
    87  func TestEnvoyBootstrapHook_decodeTriState(t *testing.T) {
    88  	t.Parallel()
    89  
    90  	require.Equal(t, "", decodeTriState(nil))
    91  	require.Equal(t, "true", decodeTriState(helper.BoolToPtr(true)))
    92  	require.Equal(t, "false", decodeTriState(helper.BoolToPtr(false)))
    93  }
    94  
    95  var (
    96  	consulPlainConfig = envoyBootstrapConsulConfig{
    97  		HTTPAddr: "2.2.2.2",
    98  	}
    99  
   100  	consulTLSConfig = envoyBootstrapConsulConfig{
   101  		HTTPAddr:  "2.2.2.2",            // arg
   102  		Auth:      "user:password",      // env
   103  		SSL:       "true",               // env
   104  		VerifySSL: "true",               // env
   105  		CAFile:    "/etc/tls/ca-file",   // arg
   106  		CertFile:  "/etc/tls/cert-file", // arg
   107  		KeyFile:   "/etc/tls/key-file",  // arg
   108  	}
   109  )
   110  
   111  func TestEnvoyBootstrapHook_envoyBootstrapArgs(t *testing.T) {
   112  	t.Parallel()
   113  
   114  	t.Run("excluding SI token", func(t *testing.T) {
   115  		ebArgs := envoyBootstrapArgs{
   116  			sidecarFor:     "s1",
   117  			grpcAddr:       "1.1.1.1",
   118  			consulConfig:   consulPlainConfig,
   119  			envoyAdminBind: "localhost:3333",
   120  		}
   121  		result := ebArgs.args()
   122  		require.Equal(t, []string{"connect", "envoy",
   123  			"-grpc-addr", "1.1.1.1",
   124  			"-http-addr", "2.2.2.2",
   125  			"-admin-bind", "localhost:3333",
   126  			"-bootstrap",
   127  			"-sidecar-for", "s1",
   128  		}, result)
   129  	})
   130  
   131  	t.Run("including SI token", func(t *testing.T) {
   132  		token := uuid.Generate()
   133  		ebArgs := envoyBootstrapArgs{
   134  			sidecarFor:     "s1",
   135  			grpcAddr:       "1.1.1.1",
   136  			consulConfig:   consulPlainConfig,
   137  			envoyAdminBind: "localhost:3333",
   138  			siToken:        token,
   139  		}
   140  		result := ebArgs.args()
   141  		require.Equal(t, []string{"connect", "envoy",
   142  			"-grpc-addr", "1.1.1.1",
   143  			"-http-addr", "2.2.2.2",
   144  			"-admin-bind", "localhost:3333",
   145  			"-bootstrap",
   146  			"-sidecar-for", "s1",
   147  			"-token", token,
   148  		}, result)
   149  	})
   150  
   151  	t.Run("including certificates", func(t *testing.T) {
   152  		ebArgs := envoyBootstrapArgs{
   153  			sidecarFor:     "s1",
   154  			grpcAddr:       "1.1.1.1",
   155  			consulConfig:   consulTLSConfig,
   156  			envoyAdminBind: "localhost:3333",
   157  		}
   158  		result := ebArgs.args()
   159  		require.Equal(t, []string{"connect", "envoy",
   160  			"-grpc-addr", "1.1.1.1",
   161  			"-http-addr", "2.2.2.2",
   162  			"-admin-bind", "localhost:3333",
   163  			"-bootstrap",
   164  			"-sidecar-for", "s1",
   165  			"-ca-file", "/etc/tls/ca-file",
   166  			"-client-cert", "/etc/tls/cert-file",
   167  			"-client-key", "/etc/tls/key-file",
   168  		}, result)
   169  	})
   170  }
   171  
   172  func TestEnvoyBootstrapHook_envoyBootstrapEnv(t *testing.T) {
   173  	t.Parallel()
   174  
   175  	environment := []string{"foo=bar", "baz=1"}
   176  
   177  	t.Run("plain consul config", func(t *testing.T) {
   178  		require.Equal(t, []string{
   179  			"foo=bar", "baz=1",
   180  		}, envoyBootstrapArgs{
   181  			sidecarFor:     "s1",
   182  			grpcAddr:       "1.1.1.1",
   183  			consulConfig:   consulPlainConfig,
   184  			envoyAdminBind: "localhost:3333",
   185  		}.env(environment))
   186  	})
   187  
   188  	t.Run("tls consul config", func(t *testing.T) {
   189  		require.Equal(t, []string{
   190  			"foo=bar", "baz=1",
   191  			"CONSUL_HTTP_AUTH=user:password",
   192  			"CONSUL_HTTP_SSL=true",
   193  			"CONSUL_HTTP_SSL_VERIFY=true",
   194  		}, envoyBootstrapArgs{
   195  			sidecarFor:     "s1",
   196  			grpcAddr:       "1.1.1.1",
   197  			consulConfig:   consulTLSConfig,
   198  			envoyAdminBind: "localhost:3333",
   199  		}.env(environment))
   200  	})
   201  }
   202  
   203  // dig through envoy config to look for consul token
   204  type envoyConfig struct {
   205  	DynamicResources struct {
   206  		ADSConfig struct {
   207  			GRPCServices struct {
   208  				InitialMetadata []struct {
   209  					Key   string `json:"key"`
   210  					Value string `json:"value"`
   211  				} `json:"initial_metadata"`
   212  			} `json:"grpc_services"`
   213  		} `json:"ads_config"`
   214  	} `json:"dynamic_resources"`
   215  }
   216  
   217  // TestEnvoyBootstrapHook_with_SI_token asserts the bootstrap file written for
   218  // Envoy contains a Consul SI token.
   219  func TestEnvoyBootstrapHook_with_SI_token(t *testing.T) {
   220  	t.Parallel()
   221  	testutil.RequireConsul(t)
   222  
   223  	testconsul, err := consultest.NewTestServerConfig(func(c *consultest.TestServerConfig) {
   224  		// If -v wasn't specified squelch consul logging
   225  		if !testing.Verbose() {
   226  			c.Stdout = ioutil.Discard
   227  			c.Stderr = ioutil.Discard
   228  		}
   229  	})
   230  	if err != nil {
   231  		t.Fatalf("error starting test consul server: %v", err)
   232  	}
   233  	defer testconsul.Stop()
   234  
   235  	alloc := mock.Alloc()
   236  	alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{
   237  		{
   238  			Mode: "bridge",
   239  			IP:   "10.0.0.1",
   240  			DynamicPorts: []structs.Port{
   241  				{
   242  					Label: "connect-proxy-foo",
   243  					Value: 9999,
   244  					To:    9999,
   245  				},
   246  			},
   247  		},
   248  	}
   249  	tg := alloc.Job.TaskGroups[0]
   250  	tg.Services = []*structs.Service{
   251  		{
   252  			Name:      "foo",
   253  			PortLabel: "9999", // Just need a valid port, nothing will bind to it
   254  			Connect: &structs.ConsulConnect{
   255  				SidecarService: &structs.ConsulSidecarService{},
   256  			},
   257  		},
   258  	}
   259  	sidecarTask := &structs.Task{
   260  		Name: "sidecar",
   261  		Kind: "connect-proxy:foo",
   262  	}
   263  	tg.Tasks = append(tg.Tasks, sidecarTask)
   264  
   265  	logger := testlog.HCLogger(t)
   266  
   267  	allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap")
   268  	defer cleanup()
   269  
   270  	// Register Group Services
   271  	consulConfig := consulapi.DefaultConfig()
   272  	consulConfig.Address = testconsul.HTTPAddr
   273  	consulAPIClient, err := consulapi.NewClient(consulConfig)
   274  	require.NoError(t, err)
   275  
   276  	consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true)
   277  	go consulClient.Run()
   278  	defer consulClient.Shutdown()
   279  	require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter())))
   280  
   281  	// Run Connect bootstrap Hook
   282  	h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{
   283  		Addr: consulConfig.Address,
   284  	}, logger))
   285  	req := &interfaces.TaskPrestartRequest{
   286  		Task:    sidecarTask,
   287  		TaskDir: allocDir.NewTaskDir(sidecarTask.Name),
   288  	}
   289  	require.NoError(t, req.TaskDir.Build(false, nil))
   290  
   291  	// Insert service identity token in the secrets directory
   292  	token := uuid.Generate()
   293  	siTokenFile := filepath.Join(req.TaskDir.SecretsDir, sidsTokenFile)
   294  	err = ioutil.WriteFile(siTokenFile, []byte(token), 0440)
   295  	require.NoError(t, err)
   296  
   297  	resp := &interfaces.TaskPrestartResponse{}
   298  
   299  	// Run the hook
   300  	require.NoError(t, h.Prestart(context.Background(), req, resp))
   301  
   302  	// Assert it is Done
   303  	require.True(t, resp.Done)
   304  
   305  	// Ensure the default path matches
   306  	env := map[string]string{
   307  		taskenv.SecretsDir: req.TaskDir.SecretsDir,
   308  	}
   309  	f, err := os.Open(args.ReplaceEnv(structs.EnvoyBootstrapPath, env))
   310  	require.NoError(t, err)
   311  	defer f.Close()
   312  
   313  	// Assert bootstrap configuration is valid json
   314  	var out envoyConfig
   315  	require.NoError(t, json.NewDecoder(f).Decode(&out))
   316  
   317  	// Assert the SI token got set
   318  	key := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Key
   319  	value := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Value
   320  	require.Equal(t, "x-consul-token", key)
   321  	require.Equal(t, token, value)
   322  }
   323  
   324  // TestTaskRunner_EnvoyBootstrapHook_Prestart asserts the EnvoyBootstrapHook
   325  // creates Envoy's bootstrap.json configuration based on Connect proxy sidecars
   326  // registered for the task.
   327  func TestTaskRunner_EnvoyBootstrapHook_Ok(t *testing.T) {
   328  	t.Parallel()
   329  	testutil.RequireConsul(t)
   330  
   331  	testconsul, err := consultest.NewTestServerConfig(func(c *consultest.TestServerConfig) {
   332  		// If -v wasn't specified squelch consul logging
   333  		if !testing.Verbose() {
   334  			c.Stdout = ioutil.Discard
   335  			c.Stderr = ioutil.Discard
   336  		}
   337  	})
   338  	if err != nil {
   339  		t.Fatalf("error starting test consul server: %v", err)
   340  	}
   341  	defer testconsul.Stop()
   342  
   343  	alloc := mock.Alloc()
   344  	alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{
   345  		{
   346  			Mode: "bridge",
   347  			IP:   "10.0.0.1",
   348  			DynamicPorts: []structs.Port{
   349  				{
   350  					Label: "connect-proxy-foo",
   351  					Value: 9999,
   352  					To:    9999,
   353  				},
   354  			},
   355  		},
   356  	}
   357  	tg := alloc.Job.TaskGroups[0]
   358  	tg.Services = []*structs.Service{
   359  		{
   360  			Name:      "foo",
   361  			PortLabel: "9999", // Just need a valid port, nothing will bind to it
   362  			Connect: &structs.ConsulConnect{
   363  				SidecarService: &structs.ConsulSidecarService{},
   364  			},
   365  		},
   366  	}
   367  	sidecarTask := &structs.Task{
   368  		Name: "sidecar",
   369  		Kind: "connect-proxy:foo",
   370  	}
   371  	tg.Tasks = append(tg.Tasks, sidecarTask)
   372  
   373  	logger := testlog.HCLogger(t)
   374  
   375  	allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap")
   376  	defer cleanup()
   377  
   378  	// Register Group Services
   379  	consulConfig := consulapi.DefaultConfig()
   380  	consulConfig.Address = testconsul.HTTPAddr
   381  	consulAPIClient, err := consulapi.NewClient(consulConfig)
   382  	require.NoError(t, err)
   383  
   384  	consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true)
   385  	go consulClient.Run()
   386  	defer consulClient.Shutdown()
   387  	require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter())))
   388  
   389  	// Run Connect bootstrap Hook
   390  	h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{
   391  		Addr: consulConfig.Address,
   392  	}, logger))
   393  	req := &interfaces.TaskPrestartRequest{
   394  		Task:    sidecarTask,
   395  		TaskDir: allocDir.NewTaskDir(sidecarTask.Name),
   396  	}
   397  	require.NoError(t, req.TaskDir.Build(false, nil))
   398  
   399  	resp := &interfaces.TaskPrestartResponse{}
   400  
   401  	// Run the hook
   402  	require.NoError(t, h.Prestart(context.Background(), req, resp))
   403  
   404  	// Assert it is Done
   405  	require.True(t, resp.Done)
   406  
   407  	require.NotNil(t, resp.Env)
   408  	require.Equal(t, "localhost:19001", resp.Env[envoyAdminBindEnvPrefix+"foo"])
   409  
   410  	// Ensure the default path matches
   411  	env := map[string]string{
   412  		taskenv.SecretsDir: req.TaskDir.SecretsDir,
   413  	}
   414  	f, err := os.Open(args.ReplaceEnv(structs.EnvoyBootstrapPath, env))
   415  	require.NoError(t, err)
   416  	defer f.Close()
   417  
   418  	// Assert bootstrap configuration is valid json
   419  	var out envoyConfig
   420  	require.NoError(t, json.NewDecoder(f).Decode(&out))
   421  
   422  	// Assert no SI token got set
   423  	key := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Key
   424  	value := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Value
   425  	require.Equal(t, "x-consul-token", key)
   426  	require.Equal(t, "", value)
   427  }
   428  
   429  // TestTaskRunner_EnvoyBootstrapHook_Noop asserts that the Envoy bootstrap hook
   430  // is a noop for non-Connect proxy sidecar tasks.
   431  func TestTaskRunner_EnvoyBootstrapHook_Noop(t *testing.T) {
   432  	t.Parallel()
   433  	logger := testlog.HCLogger(t)
   434  
   435  	allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap")
   436  	defer cleanup()
   437  
   438  	alloc := mock.Alloc()
   439  	task := alloc.Job.LookupTaskGroup(alloc.TaskGroup).Tasks[0]
   440  
   441  	// Run Envoy bootstrap Hook. Use invalid Consul address as it should
   442  	// not get hit.
   443  	h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{
   444  		Addr: "http://127.0.0.2:1",
   445  	}, logger))
   446  	req := &interfaces.TaskPrestartRequest{
   447  		Task:    task,
   448  		TaskDir: allocDir.NewTaskDir(task.Name),
   449  	}
   450  	require.NoError(t, req.TaskDir.Build(false, nil))
   451  
   452  	resp := &interfaces.TaskPrestartResponse{}
   453  
   454  	// Run the hook
   455  	require.NoError(t, h.Prestart(context.Background(), req, resp))
   456  
   457  	// Assert it is Done
   458  	require.True(t, resp.Done)
   459  
   460  	// Assert no file was written
   461  	_, err := os.Open(filepath.Join(req.TaskDir.SecretsDir, "envoy_bootstrap.json"))
   462  	require.Error(t, err)
   463  	require.True(t, os.IsNotExist(err))
   464  }
   465  
   466  // TestTaskRunner_EnvoyBootstrapHook_RecoverableError asserts the Envoy
   467  // bootstrap hook returns a Recoverable error if the bootstrap command runs but
   468  // fails.
   469  func TestTaskRunner_EnvoyBootstrapHook_RecoverableError(t *testing.T) {
   470  	t.Parallel()
   471  	testutil.RequireConsul(t)
   472  
   473  	testconsul, err := consultest.NewTestServerConfig(func(c *consultest.TestServerConfig) {
   474  		// If -v wasn't specified squelch consul logging
   475  		if !testing.Verbose() {
   476  			c.Stdout = ioutil.Discard
   477  			c.Stderr = ioutil.Discard
   478  		}
   479  	})
   480  	if err != nil {
   481  		t.Fatalf("error starting test consul server: %v", err)
   482  	}
   483  	defer testconsul.Stop()
   484  
   485  	alloc := mock.Alloc()
   486  	alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{
   487  		{
   488  			Mode: "bridge",
   489  			IP:   "10.0.0.1",
   490  			DynamicPorts: []structs.Port{
   491  				{
   492  					Label: "connect-proxy-foo",
   493  					Value: 9999,
   494  					To:    9999,
   495  				},
   496  			},
   497  		},
   498  	}
   499  	tg := alloc.Job.TaskGroups[0]
   500  	tg.Services = []*structs.Service{
   501  		{
   502  			Name:      "foo",
   503  			PortLabel: "9999", // Just need a valid port, nothing will bind to it
   504  			Connect: &structs.ConsulConnect{
   505  				SidecarService: &structs.ConsulSidecarService{},
   506  			},
   507  		},
   508  	}
   509  	sidecarTask := &structs.Task{
   510  		Name: "sidecar",
   511  		Kind: "connect-proxy:foo",
   512  	}
   513  	tg.Tasks = append(tg.Tasks, sidecarTask)
   514  
   515  	logger := testlog.HCLogger(t)
   516  
   517  	allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap")
   518  	defer cleanup()
   519  
   520  	// Unlike the successful test above, do NOT register the group services
   521  	// yet. This should cause a recoverable error similar to if Consul was
   522  	// not running.
   523  
   524  	// Run Connect bootstrap Hook
   525  	h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{
   526  		Addr: testconsul.HTTPAddr,
   527  	}, logger))
   528  	req := &interfaces.TaskPrestartRequest{
   529  		Task:    sidecarTask,
   530  		TaskDir: allocDir.NewTaskDir(sidecarTask.Name),
   531  	}
   532  	require.NoError(t, req.TaskDir.Build(false, nil))
   533  
   534  	resp := &interfaces.TaskPrestartResponse{}
   535  
   536  	// Run the hook
   537  	err = h.Prestart(context.Background(), req, resp)
   538  	require.EqualError(t, err, "error creating bootstrap configuration for Connect proxy sidecar: exit status 1")
   539  	require.True(t, structs.IsRecoverable(err))
   540  
   541  	// Assert it is not Done
   542  	require.False(t, resp.Done)
   543  
   544  	// Assert no file was written
   545  	_, err = os.Open(filepath.Join(req.TaskDir.SecretsDir, "envoy_bootstrap.json"))
   546  	require.Error(t, err)
   547  	require.True(t, os.IsNotExist(err))
   548  }