github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/allocrunner/taskrunner/envoy_bootstrap_hook_test.go (about)

     1  //go:build !windows
     2  // +build !windows
     3  
     4  // todo(shoenig): Once Connect is supported on Windows, we'll need to make this
     5  //  set of tests work there too.
     6  
     7  package taskrunner
     8  
     9  import (
    10  	"context"
    11  	"encoding/json"
    12  	"fmt"
    13  	"io/ioutil"
    14  	"os"
    15  	"path/filepath"
    16  	"testing"
    17  	"time"
    18  
    19  	consulapi "github.com/hashicorp/consul/api"
    20  	"github.com/hashicorp/nomad/ci"
    21  	"github.com/hashicorp/nomad/client/allocdir"
    22  	"github.com/hashicorp/nomad/client/allocrunner/interfaces"
    23  	"github.com/hashicorp/nomad/client/taskenv"
    24  	"github.com/hashicorp/nomad/client/testutil"
    25  	agentconsul "github.com/hashicorp/nomad/command/agent/consul"
    26  	"github.com/hashicorp/nomad/helper/args"
    27  	"github.com/hashicorp/nomad/helper/pointer"
    28  	"github.com/hashicorp/nomad/helper/testlog"
    29  	"github.com/hashicorp/nomad/helper/uuid"
    30  	"github.com/hashicorp/nomad/nomad/mock"
    31  	"github.com/hashicorp/nomad/nomad/structs"
    32  	"github.com/hashicorp/nomad/nomad/structs/config"
    33  	"github.com/stretchr/testify/require"
    34  	"golang.org/x/sys/unix"
    35  )
    36  
    37  var _ interfaces.TaskPrestartHook = (*envoyBootstrapHook)(nil)
    38  
    39  const (
    40  	// consulNamespace is empty string in OSS, because Consul OSS does not like
    41  	// having even the default namespace set.
    42  	consulNamespace = ""
    43  )
    44  
    45  func writeTmp(t *testing.T, s string, fm os.FileMode) string {
    46  	dir := t.TempDir()
    47  
    48  	fPath := filepath.Join(dir, sidsTokenFile)
    49  	err := ioutil.WriteFile(fPath, []byte(s), fm)
    50  	require.NoError(t, err)
    51  
    52  	return dir
    53  }
    54  
    55  func TestEnvoyBootstrapHook_maybeLoadSIToken(t *testing.T) {
    56  	ci.Parallel(t)
    57  
    58  	// This test fails when running as root because the test case for checking
    59  	// the error condition when the file is unreadable fails (root can read the
    60  	// file even though the permissions are set to 0200).
    61  	if unix.Geteuid() == 0 {
    62  		t.Skip("test only works as non-root")
    63  	}
    64  
    65  	t.Run("file does not exist", func(t *testing.T) {
    66  		h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)})
    67  		cfg, err := h.maybeLoadSIToken("task1", "/does/not/exist")
    68  		require.NoError(t, err) // absence of token is not an error
    69  		require.Equal(t, "", cfg)
    70  	})
    71  
    72  	t.Run("load token from file", func(t *testing.T) {
    73  		token := uuid.Generate()
    74  		f := writeTmp(t, token, 0440)
    75  
    76  		h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)})
    77  		cfg, err := h.maybeLoadSIToken("task1", f)
    78  		require.NoError(t, err)
    79  		require.Equal(t, token, cfg)
    80  	})
    81  
    82  	t.Run("file is unreadable", func(t *testing.T) {
    83  		token := uuid.Generate()
    84  		f := writeTmp(t, token, 0200)
    85  
    86  		h := newEnvoyBootstrapHook(&envoyBootstrapHookConfig{logger: testlog.HCLogger(t)})
    87  		cfg, err := h.maybeLoadSIToken("task1", f)
    88  		require.Error(t, err)
    89  		require.False(t, os.IsNotExist(err))
    90  		require.Equal(t, "", cfg)
    91  	})
    92  }
    93  
    94  func TestEnvoyBootstrapHook_decodeTriState(t *testing.T) {
    95  	ci.Parallel(t)
    96  
    97  	require.Equal(t, "", decodeTriState(nil))
    98  	require.Equal(t, "true", decodeTriState(pointer.Of(true)))
    99  	require.Equal(t, "false", decodeTriState(pointer.Of(false)))
   100  }
   101  
   102  var (
   103  	consulPlainConfig = consulTransportConfig{
   104  		HTTPAddr: "2.2.2.2",
   105  	}
   106  
   107  	consulTLSConfig = consulTransportConfig{
   108  		HTTPAddr:  "2.2.2.2",            // arg
   109  		Auth:      "user:password",      // env
   110  		SSL:       "true",               // env
   111  		VerifySSL: "true",               // env
   112  		CAFile:    "/etc/tls/ca-file",   // arg
   113  		CertFile:  "/etc/tls/cert-file", // arg
   114  		KeyFile:   "/etc/tls/key-file",  // arg
   115  	}
   116  )
   117  
   118  func TestEnvoyBootstrapHook_envoyBootstrapArgs(t *testing.T) {
   119  	ci.Parallel(t)
   120  
   121  	t.Run("excluding SI token", func(t *testing.T) {
   122  		ebArgs := envoyBootstrapArgs{
   123  			proxyID:        "s1-sidecar-proxy",
   124  			grpcAddr:       "1.1.1.1",
   125  			consulConfig:   consulPlainConfig,
   126  			envoyAdminBind: "127.0.0.2:19000",
   127  			envoyReadyBind: "127.0.0.1:19100",
   128  		}
   129  		result := ebArgs.args()
   130  		require.Equal(t, []string{"connect", "envoy",
   131  			"-grpc-addr", "1.1.1.1",
   132  			"-http-addr", "2.2.2.2",
   133  			"-admin-bind", "127.0.0.2:19000",
   134  			"-address", "127.0.0.1:19100",
   135  			"-proxy-id", "s1-sidecar-proxy",
   136  			"-bootstrap",
   137  		}, result)
   138  	})
   139  
   140  	t.Run("including SI token", func(t *testing.T) {
   141  		token := uuid.Generate()
   142  		ebArgs := envoyBootstrapArgs{
   143  			proxyID:        "s1-sidecar-proxy",
   144  			grpcAddr:       "1.1.1.1",
   145  			consulConfig:   consulPlainConfig,
   146  			envoyAdminBind: "127.0.0.2:19000",
   147  			envoyReadyBind: "127.0.0.1:19100",
   148  			siToken:        token,
   149  		}
   150  		result := ebArgs.args()
   151  		require.Equal(t, []string{"connect", "envoy",
   152  			"-grpc-addr", "1.1.1.1",
   153  			"-http-addr", "2.2.2.2",
   154  			"-admin-bind", "127.0.0.2:19000",
   155  			"-address", "127.0.0.1:19100",
   156  			"-proxy-id", "s1-sidecar-proxy",
   157  			"-bootstrap",
   158  			"-token", token,
   159  		}, result)
   160  	})
   161  
   162  	t.Run("including certificates", func(t *testing.T) {
   163  		ebArgs := envoyBootstrapArgs{
   164  			proxyID:        "s1-sidecar-proxy",
   165  			grpcAddr:       "1.1.1.1",
   166  			consulConfig:   consulTLSConfig,
   167  			envoyAdminBind: "127.0.0.2:19000",
   168  			envoyReadyBind: "127.0.0.1:19100",
   169  		}
   170  		result := ebArgs.args()
   171  		require.Equal(t, []string{"connect", "envoy",
   172  			"-grpc-addr", "1.1.1.1",
   173  			"-http-addr", "2.2.2.2",
   174  			"-admin-bind", "127.0.0.2:19000",
   175  			"-address", "127.0.0.1:19100",
   176  			"-proxy-id", "s1-sidecar-proxy",
   177  			"-bootstrap",
   178  			"-ca-file", "/etc/tls/ca-file",
   179  			"-client-cert", "/etc/tls/cert-file",
   180  			"-client-key", "/etc/tls/key-file",
   181  		}, result)
   182  	})
   183  
   184  	t.Run("ingress gateway", func(t *testing.T) {
   185  		ebArgs := envoyBootstrapArgs{
   186  			consulConfig:   consulPlainConfig,
   187  			grpcAddr:       "1.1.1.1",
   188  			envoyAdminBind: "127.0.0.2:19000",
   189  			envoyReadyBind: "127.0.0.1:19100",
   190  			gateway:        "my-ingress-gateway",
   191  			proxyID:        "_nomad-task-803cb569-881c-b0d8-9222-360bcc33157e-group-ig-ig-8080",
   192  		}
   193  		result := ebArgs.args()
   194  		require.Equal(t, []string{"connect", "envoy",
   195  			"-grpc-addr", "1.1.1.1",
   196  			"-http-addr", "2.2.2.2",
   197  			"-admin-bind", "127.0.0.2:19000",
   198  			"-address", "127.0.0.1:19100",
   199  			"-proxy-id", "_nomad-task-803cb569-881c-b0d8-9222-360bcc33157e-group-ig-ig-8080",
   200  			"-bootstrap",
   201  			"-gateway", "my-ingress-gateway",
   202  		}, result)
   203  	})
   204  
   205  	t.Run("mesh gateway", func(t *testing.T) {
   206  		ebArgs := envoyBootstrapArgs{
   207  			consulConfig:   consulPlainConfig,
   208  			grpcAddr:       "1.1.1.1",
   209  			envoyAdminBind: "127.0.0.2:19000",
   210  			envoyReadyBind: "127.0.0.1:19100",
   211  			gateway:        "my-mesh-gateway",
   212  			proxyID:        "_nomad-task-803cb569-881c-b0d8-9222-360bcc33157e-group-mesh-mesh-8080",
   213  		}
   214  		result := ebArgs.args()
   215  		require.Equal(t, []string{"connect", "envoy",
   216  			"-grpc-addr", "1.1.1.1",
   217  			"-http-addr", "2.2.2.2",
   218  			"-admin-bind", "127.0.0.2:19000",
   219  			"-address", "127.0.0.1:19100",
   220  			"-proxy-id", "_nomad-task-803cb569-881c-b0d8-9222-360bcc33157e-group-mesh-mesh-8080",
   221  			"-bootstrap",
   222  			"-gateway", "my-mesh-gateway",
   223  		}, result)
   224  	})
   225  }
   226  
   227  func TestEnvoyBootstrapHook_envoyBootstrapEnv(t *testing.T) {
   228  	ci.Parallel(t)
   229  
   230  	environment := []string{"foo=bar", "baz=1"}
   231  
   232  	t.Run("plain consul config", func(t *testing.T) {
   233  		require.Equal(t, []string{
   234  			"foo=bar", "baz=1",
   235  		}, envoyBootstrapArgs{
   236  			proxyID:        "s1-sidecar-proxy",
   237  			grpcAddr:       "1.1.1.1",
   238  			consulConfig:   consulPlainConfig,
   239  			envoyAdminBind: "localhost:3333",
   240  		}.env(environment))
   241  	})
   242  
   243  	t.Run("tls consul config", func(t *testing.T) {
   244  		require.Equal(t, []string{
   245  			"foo=bar", "baz=1",
   246  			"CONSUL_HTTP_AUTH=user:password",
   247  			"CONSUL_HTTP_SSL=true",
   248  			"CONSUL_HTTP_SSL_VERIFY=true",
   249  		}, envoyBootstrapArgs{
   250  			proxyID:        "s1-sidecar-proxy",
   251  			grpcAddr:       "1.1.1.1",
   252  			consulConfig:   consulTLSConfig,
   253  			envoyAdminBind: "localhost:3333",
   254  		}.env(environment))
   255  	})
   256  }
   257  
   258  // envoyConfig is used to unmarshal an envoy bootstrap configuration file, so that
   259  // we can inspect the contents in tests.
   260  type envoyConfig struct {
   261  	Admin struct {
   262  		Address struct {
   263  			SocketAddress struct {
   264  				Address string `json:"address"`
   265  				Port    int    `json:"port_value"`
   266  			} `json:"socket_address"`
   267  		} `json:"address"`
   268  	} `json:"admin"`
   269  	Node struct {
   270  		Cluster  string `json:"cluster"`
   271  		ID       string `json:"id"`
   272  		Metadata struct {
   273  			Namespace string `json:"namespace"`
   274  			Version   string `json:"envoy_version"`
   275  		}
   276  	}
   277  	DynamicResources struct {
   278  		ADSConfig struct {
   279  			GRPCServices struct {
   280  				InitialMetadata []struct {
   281  					Key   string `json:"key"`
   282  					Value string `json:"value"`
   283  				} `json:"initial_metadata"`
   284  			} `json:"grpc_services"`
   285  		} `json:"ads_config"`
   286  	} `json:"dynamic_resources"`
   287  }
   288  
   289  // TestEnvoyBootstrapHook_with_SI_token asserts the bootstrap file written for
   290  // Envoy contains a Consul SI token.
   291  func TestEnvoyBootstrapHook_with_SI_token(t *testing.T) {
   292  	ci.Parallel(t)
   293  	testutil.RequireConsul(t)
   294  
   295  	testConsul := getTestConsul(t)
   296  	defer testConsul.Stop()
   297  
   298  	alloc := mock.ConnectAlloc()
   299  	alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{
   300  		{
   301  			Mode: "bridge",
   302  			IP:   "10.0.0.1",
   303  			DynamicPorts: []structs.Port{
   304  				{
   305  					Label: "connect-proxy-foo",
   306  					Value: 9999,
   307  					To:    9999,
   308  				},
   309  			},
   310  		},
   311  	}
   312  	tg := alloc.Job.TaskGroups[0]
   313  	tg.Services = []*structs.Service{
   314  		{
   315  			Name:      "foo",
   316  			PortLabel: "9999", // Just need a valid port, nothing will bind to it
   317  			Connect: &structs.ConsulConnect{
   318  				SidecarService: &structs.ConsulSidecarService{},
   319  			},
   320  		},
   321  	}
   322  	sidecarTask := &structs.Task{
   323  		Name: "sidecar",
   324  		Kind: "connect-proxy:foo",
   325  	}
   326  	tg.Tasks = append(tg.Tasks, sidecarTask)
   327  
   328  	logger := testlog.HCLogger(t)
   329  
   330  	allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap", alloc.ID)
   331  	defer cleanup()
   332  
   333  	// Register Group Services
   334  	consulConfig := consulapi.DefaultConfig()
   335  	consulConfig.Address = testConsul.HTTPAddr
   336  	consulAPIClient, err := consulapi.NewClient(consulConfig)
   337  	require.NoError(t, err)
   338  	namespacesClient := agentconsul.NewNamespacesClient(consulAPIClient.Namespaces(), consulAPIClient.Agent())
   339  
   340  	consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), namespacesClient, logger, true)
   341  	go consulClient.Run()
   342  	defer consulClient.Shutdown()
   343  	require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter())))
   344  
   345  	// Run Connect bootstrap Hook
   346  	h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{
   347  		Addr: consulConfig.Address,
   348  	}, consulNamespace, logger))
   349  	req := &interfaces.TaskPrestartRequest{
   350  		Task:    sidecarTask,
   351  		TaskDir: allocDir.NewTaskDir(sidecarTask.Name),
   352  		TaskEnv: taskenv.NewEmptyTaskEnv(),
   353  	}
   354  	require.NoError(t, req.TaskDir.Build(false, nil))
   355  
   356  	// Insert service identity token in the secrets directory
   357  	token := uuid.Generate()
   358  	siTokenFile := filepath.Join(req.TaskDir.SecretsDir, sidsTokenFile)
   359  	err = ioutil.WriteFile(siTokenFile, []byte(token), 0440)
   360  	require.NoError(t, err)
   361  
   362  	resp := &interfaces.TaskPrestartResponse{}
   363  
   364  	// Run the hook
   365  	require.NoError(t, h.Prestart(context.Background(), req, resp))
   366  
   367  	// Assert it is Done
   368  	require.True(t, resp.Done)
   369  
   370  	// Ensure the default path matches
   371  	env := map[string]string{
   372  		taskenv.SecretsDir: req.TaskDir.SecretsDir,
   373  	}
   374  	f, err := os.Open(args.ReplaceEnv(structs.EnvoyBootstrapPath, env))
   375  	require.NoError(t, err)
   376  	defer f.Close()
   377  
   378  	// Assert bootstrap configuration is valid json
   379  	var out envoyConfig
   380  	require.NoError(t, json.NewDecoder(f).Decode(&out))
   381  
   382  	// Assert the SI token got set
   383  	key := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Key
   384  	value := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Value
   385  	require.Equal(t, "x-consul-token", key)
   386  	require.Equal(t, token, value)
   387  }
   388  
   389  // TestTaskRunner_EnvoyBootstrapHook_sidecar_ok asserts the EnvoyBootstrapHook
   390  // creates Envoy's bootstrap.json configuration based on Connect proxy sidecars
   391  // registered for the task.
   392  func TestTaskRunner_EnvoyBootstrapHook_sidecar_ok(t *testing.T) {
   393  	ci.Parallel(t)
   394  	testutil.RequireConsul(t)
   395  
   396  	testConsul := getTestConsul(t)
   397  	defer testConsul.Stop()
   398  
   399  	alloc := mock.ConnectAlloc()
   400  	alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{
   401  		{
   402  			Mode: "bridge",
   403  			IP:   "10.0.0.1",
   404  			DynamicPorts: []structs.Port{
   405  				{
   406  					Label: "connect-proxy-foo",
   407  					Value: 9999,
   408  					To:    9999,
   409  				},
   410  			},
   411  		},
   412  	}
   413  	tg := alloc.Job.TaskGroups[0]
   414  	tg.Services = []*structs.Service{
   415  		{
   416  			Name:      "foo",
   417  			PortLabel: "9999", // Just need a valid port, nothing will bind to it
   418  			Connect: &structs.ConsulConnect{
   419  				SidecarService: &structs.ConsulSidecarService{},
   420  			},
   421  		},
   422  	}
   423  	sidecarTask := &structs.Task{
   424  		Name: "sidecar",
   425  		Kind: structs.NewTaskKind(structs.ConnectProxyPrefix, "foo"),
   426  	}
   427  	tg.Tasks = append(tg.Tasks, sidecarTask)
   428  
   429  	logger := testlog.HCLogger(t)
   430  
   431  	allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap", alloc.ID)
   432  	defer cleanup()
   433  
   434  	// Register Group Services
   435  	consulConfig := consulapi.DefaultConfig()
   436  	consulConfig.Address = testConsul.HTTPAddr
   437  	consulAPIClient, err := consulapi.NewClient(consulConfig)
   438  	require.NoError(t, err)
   439  	namespacesClient := agentconsul.NewNamespacesClient(consulAPIClient.Namespaces(), consulAPIClient.Agent())
   440  
   441  	consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), namespacesClient, logger, true)
   442  	go consulClient.Run()
   443  	defer consulClient.Shutdown()
   444  	require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter())))
   445  
   446  	// Run Connect bootstrap Hook
   447  	h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{
   448  		Addr: consulConfig.Address,
   449  	}, consulNamespace, logger))
   450  	req := &interfaces.TaskPrestartRequest{
   451  		Task:    sidecarTask,
   452  		TaskDir: allocDir.NewTaskDir(sidecarTask.Name),
   453  		TaskEnv: taskenv.NewEmptyTaskEnv(),
   454  	}
   455  	require.NoError(t, req.TaskDir.Build(false, nil))
   456  
   457  	resp := &interfaces.TaskPrestartResponse{}
   458  
   459  	// Run the hook
   460  	require.NoError(t, h.Prestart(context.Background(), req, resp))
   461  
   462  	// Assert it is Done
   463  	require.True(t, resp.Done)
   464  
   465  	require.NotNil(t, resp.Env)
   466  	require.Equal(t, "127.0.0.2:19001", resp.Env[envoyAdminBindEnvPrefix+"foo"])
   467  
   468  	// Ensure the default path matches
   469  	env := map[string]string{
   470  		taskenv.SecretsDir: req.TaskDir.SecretsDir,
   471  	}
   472  	f, err := os.Open(args.ReplaceEnv(structs.EnvoyBootstrapPath, env))
   473  	require.NoError(t, err)
   474  	defer f.Close()
   475  
   476  	// Assert bootstrap configuration is valid json
   477  	var out envoyConfig
   478  	require.NoError(t, json.NewDecoder(f).Decode(&out))
   479  
   480  	// Assert no SI token got set
   481  	key := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Key
   482  	value := out.DynamicResources.ADSConfig.GRPCServices.InitialMetadata[0].Value
   483  	require.Equal(t, "x-consul-token", key)
   484  	require.Equal(t, "", value)
   485  }
   486  
   487  func TestTaskRunner_EnvoyBootstrapHook_gateway_ok(t *testing.T) {
   488  	ci.Parallel(t)
   489  	logger := testlog.HCLogger(t)
   490  
   491  	testConsul := getTestConsul(t)
   492  	defer testConsul.Stop()
   493  
   494  	// Setup an Allocation
   495  	alloc := mock.ConnectIngressGatewayAlloc("bridge")
   496  	allocDir, cleanupDir := allocdir.TestAllocDir(t, logger, "EnvoyBootstrapIngressGateway", alloc.ID)
   497  	defer cleanupDir()
   498  
   499  	// Get a Consul client
   500  	consulConfig := consulapi.DefaultConfig()
   501  	consulConfig.Address = testConsul.HTTPAddr
   502  	consulAPIClient, err := consulapi.NewClient(consulConfig)
   503  	require.NoError(t, err)
   504  	namespacesClient := agentconsul.NewNamespacesClient(consulAPIClient.Namespaces(), consulAPIClient.Agent())
   505  
   506  	// Register Group Services
   507  	serviceClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), namespacesClient, logger, true)
   508  	go serviceClient.Run()
   509  	defer serviceClient.Shutdown()
   510  	require.NoError(t, serviceClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter())))
   511  
   512  	// Register Configuration Entry
   513  	ceClient := consulAPIClient.ConfigEntries()
   514  	set, _, err := ceClient.Set(&consulapi.IngressGatewayConfigEntry{
   515  		Kind: consulapi.IngressGateway,
   516  		Name: "gateway-service", // matches job
   517  		Listeners: []consulapi.IngressListener{{
   518  			Port:     2000,
   519  			Protocol: "tcp",
   520  			Services: []consulapi.IngressService{{
   521  				Name: "service1",
   522  			}},
   523  		}},
   524  	}, nil)
   525  	require.NoError(t, err)
   526  	require.True(t, set)
   527  
   528  	// Run Connect bootstrap hook
   529  	h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{
   530  		Addr: consulConfig.Address,
   531  	}, consulNamespace, logger))
   532  
   533  	req := &interfaces.TaskPrestartRequest{
   534  		Task:    alloc.Job.TaskGroups[0].Tasks[0],
   535  		TaskDir: allocDir.NewTaskDir(alloc.Job.TaskGroups[0].Tasks[0].Name),
   536  		TaskEnv: taskenv.NewEmptyTaskEnv(),
   537  	}
   538  	require.NoError(t, req.TaskDir.Build(false, nil))
   539  
   540  	var resp interfaces.TaskPrestartResponse
   541  
   542  	// Run the hook
   543  	require.NoError(t, h.Prestart(context.Background(), req, &resp))
   544  
   545  	// Assert the hook is Done
   546  	require.True(t, resp.Done)
   547  	require.NotNil(t, resp.Env)
   548  
   549  	// Read the Envoy Config file
   550  	env := map[string]string{
   551  		taskenv.SecretsDir: req.TaskDir.SecretsDir,
   552  	}
   553  	f, err := os.Open(args.ReplaceEnv(structs.EnvoyBootstrapPath, env))
   554  	require.NoError(t, err)
   555  	defer f.Close()
   556  
   557  	var out envoyConfig
   558  	require.NoError(t, json.NewDecoder(f).Decode(&out))
   559  
   560  	// The only interesting thing on bootstrap is the presence of the cluster,
   561  	// and its associated ID that Nomad sets. Everything is configured at runtime
   562  	// through xDS.
   563  	expID := fmt.Sprintf("_nomad-task-%s-group-web-my-ingress-service-9999", alloc.ID)
   564  	require.Equal(t, expID, out.Node.ID)
   565  	require.Equal(t, "ingress-gateway", out.Node.Cluster)
   566  }
   567  
   568  // TestTaskRunner_EnvoyBootstrapHook_Noop asserts that the Envoy bootstrap hook
   569  // is a noop for non-Connect proxy sidecar / gateway tasks.
   570  func TestTaskRunner_EnvoyBootstrapHook_Noop(t *testing.T) {
   571  	ci.Parallel(t)
   572  	logger := testlog.HCLogger(t)
   573  
   574  	alloc := mock.Alloc()
   575  	task := alloc.Job.LookupTaskGroup(alloc.TaskGroup).Tasks[0]
   576  	allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap", alloc.ID)
   577  	defer cleanup()
   578  
   579  	// Run Envoy bootstrap Hook. Use invalid Consul address as it should
   580  	// not get hit.
   581  	h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{
   582  		Addr: "http://127.0.0.2:1",
   583  	}, consulNamespace, logger))
   584  	req := &interfaces.TaskPrestartRequest{
   585  		Task:    task,
   586  		TaskDir: allocDir.NewTaskDir(task.Name),
   587  	}
   588  	require.NoError(t, req.TaskDir.Build(false, nil))
   589  
   590  	resp := &interfaces.TaskPrestartResponse{}
   591  
   592  	// Run the hook
   593  	require.NoError(t, h.Prestart(context.Background(), req, resp))
   594  
   595  	// Assert it is Done
   596  	require.True(t, resp.Done)
   597  
   598  	// Assert no file was written
   599  	_, err := os.Open(filepath.Join(req.TaskDir.SecretsDir, "envoy_bootstrap.json"))
   600  	require.Error(t, err)
   601  	require.True(t, os.IsNotExist(err))
   602  }
   603  
   604  // TestTaskRunner_EnvoyBootstrapHook_RecoverableError asserts the Envoy
   605  // bootstrap hook returns a Recoverable error if the bootstrap command runs but
   606  // fails.
   607  func TestTaskRunner_EnvoyBootstrapHook_RecoverableError(t *testing.T) {
   608  	ci.Parallel(t)
   609  	testutil.RequireConsul(t)
   610  
   611  	testConsul := getTestConsul(t)
   612  	defer testConsul.Stop()
   613  
   614  	alloc := mock.ConnectAlloc()
   615  	alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{
   616  		{
   617  			Mode: "bridge",
   618  			IP:   "10.0.0.1",
   619  			DynamicPorts: []structs.Port{
   620  				{
   621  					Label: "connect-proxy-foo",
   622  					Value: 9999,
   623  					To:    9999,
   624  				},
   625  			},
   626  		},
   627  	}
   628  	tg := alloc.Job.TaskGroups[0]
   629  	tg.Services = []*structs.Service{
   630  		{
   631  			Name:      "foo",
   632  			PortLabel: "9999", // Just need a valid port, nothing will bind to it
   633  			Connect: &structs.ConsulConnect{
   634  				SidecarService: &structs.ConsulSidecarService{},
   635  			},
   636  		},
   637  	}
   638  	sidecarTask := &structs.Task{
   639  		Name: "sidecar",
   640  		Kind: "connect-proxy:foo",
   641  	}
   642  	tg.Tasks = append(tg.Tasks, sidecarTask)
   643  
   644  	logger := testlog.HCLogger(t)
   645  
   646  	allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap", alloc.ID)
   647  	defer cleanup()
   648  
   649  	// Unlike the successful test above, do NOT register the group services
   650  	// yet. This should cause a recoverable error similar to if Consul was
   651  	// not running.
   652  
   653  	// Run Connect bootstrap Hook
   654  	h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{
   655  		Addr: testConsul.HTTPAddr,
   656  	}, consulNamespace, logger))
   657  
   658  	// Lower the allowable wait time for testing
   659  	h.envoyBootstrapWaitTime = 1 * time.Second
   660  	h.envoyBoostrapInitialGap = 100 * time.Millisecond
   661  
   662  	req := &interfaces.TaskPrestartRequest{
   663  		Task:    sidecarTask,
   664  		TaskDir: allocDir.NewTaskDir(sidecarTask.Name),
   665  		TaskEnv: taskenv.NewEmptyTaskEnv(),
   666  	}
   667  	require.NoError(t, req.TaskDir.Build(false, nil))
   668  
   669  	resp := &interfaces.TaskPrestartResponse{}
   670  
   671  	// Run the hook
   672  	err := h.Prestart(context.Background(), req, resp)
   673  	require.EqualError(t, err, "error creating bootstrap configuration for Connect proxy sidecar: exit status 1")
   674  	require.True(t, structs.IsRecoverable(err))
   675  
   676  	// Assert it is not Done
   677  	require.False(t, resp.Done)
   678  
   679  	// Assert no file was written
   680  	_, err = os.Open(filepath.Join(req.TaskDir.SecretsDir, "envoy_bootstrap.json"))
   681  	require.Error(t, err)
   682  	require.True(t, os.IsNotExist(err))
   683  }
   684  
   685  func TestTaskRunner_EnvoyBootstrapHook_retryTimeout(t *testing.T) {
   686  	ci.Parallel(t)
   687  	logger := testlog.HCLogger(t)
   688  
   689  	testConsul := getTestConsul(t)
   690  	defer testConsul.Stop()
   691  
   692  	begin := time.Now()
   693  
   694  	// Setup an Allocation
   695  	alloc := mock.ConnectAlloc()
   696  	alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{
   697  		{
   698  			Mode: "bridge",
   699  			IP:   "10.0.0.1",
   700  			DynamicPorts: []structs.Port{
   701  				{
   702  					Label: "connect-proxy-foo",
   703  					Value: 9999,
   704  					To:    9999,
   705  				},
   706  			},
   707  		},
   708  	}
   709  	tg := alloc.Job.TaskGroups[0]
   710  	tg.Services = []*structs.Service{
   711  		{
   712  			Name:      "foo",
   713  			PortLabel: "9999", // Just need a valid port, nothing will bind to it
   714  			Connect: &structs.ConsulConnect{
   715  				SidecarService: &structs.ConsulSidecarService{},
   716  			},
   717  		},
   718  	}
   719  	sidecarTask := &structs.Task{
   720  		Name: "sidecar",
   721  		Kind: structs.NewTaskKind(structs.ConnectProxyPrefix, "foo"),
   722  	}
   723  	tg.Tasks = append(tg.Tasks, sidecarTask)
   724  	allocDir, cleanupAlloc := allocdir.TestAllocDir(t, logger, "EnvoyBootstrapRetryTimeout", alloc.ID)
   725  	defer cleanupAlloc()
   726  
   727  	// Get a Consul client
   728  	consulConfig := consulapi.DefaultConfig()
   729  	consulConfig.Address = testConsul.HTTPAddr
   730  
   731  	// Do NOT register group services, causing the hook to retry until timeout
   732  
   733  	// Run Connect bootstrap hook
   734  	h := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(alloc, &config.ConsulConfig{
   735  		Addr: consulConfig.Address,
   736  	}, consulNamespace, logger))
   737  
   738  	// Keep track of the retry backoff iterations
   739  	iterations := 0
   740  
   741  	// Lower the allowable wait time for testing
   742  	h.envoyBootstrapWaitTime = 3 * time.Second
   743  	h.envoyBoostrapInitialGap = 1 * time.Second
   744  	h.envoyBootstrapExpSleep = func(d time.Duration) {
   745  		iterations++
   746  		time.Sleep(d)
   747  	}
   748  
   749  	// Create the prestart request
   750  	req := &interfaces.TaskPrestartRequest{
   751  		Task:    sidecarTask,
   752  		TaskDir: allocDir.NewTaskDir(sidecarTask.Name),
   753  		TaskEnv: taskenv.NewEmptyTaskEnv(),
   754  	}
   755  	require.NoError(t, req.TaskDir.Build(false, nil))
   756  
   757  	var resp interfaces.TaskPrestartResponse
   758  
   759  	// Run the hook and get the error
   760  	err := h.Prestart(context.Background(), req, &resp)
   761  	require.EqualError(t, err, "error creating bootstrap configuration for Connect proxy sidecar: exit status 1")
   762  
   763  	// Current time should be at least start time + total wait time
   764  	minimum := begin.Add(h.envoyBootstrapWaitTime)
   765  	require.True(t, time.Now().After(minimum))
   766  
   767  	// Should hit at least 2 iterations
   768  	require.Greater(t, 2, iterations)
   769  
   770  	// Make sure we captured the recoverable-ness of the error
   771  	_, ok := err.(*structs.RecoverableError)
   772  	require.True(t, ok)
   773  
   774  	// Assert the hook is not done (it failed)
   775  	require.False(t, resp.Done)
   776  }
   777  
   778  func TestTaskRunner_EnvoyBootstrapHook_extractNameAndKind(t *testing.T) {
   779  	t.Run("connect sidecar", func(t *testing.T) {
   780  		kind, name, err := (*envoyBootstrapHook)(nil).extractNameAndKind(
   781  			structs.NewTaskKind(structs.ConnectProxyPrefix, "foo"),
   782  		)
   783  		require.Nil(t, err)
   784  		require.Equal(t, "connect-proxy", kind)
   785  		require.Equal(t, "foo", name)
   786  	})
   787  
   788  	t.Run("connect gateway", func(t *testing.T) {
   789  		kind, name, err := (*envoyBootstrapHook)(nil).extractNameAndKind(
   790  			structs.NewTaskKind(structs.ConnectIngressPrefix, "foo"),
   791  		)
   792  		require.Nil(t, err)
   793  		require.Equal(t, "connect-ingress", kind)
   794  		require.Equal(t, "foo", name)
   795  	})
   796  
   797  	t.Run("connect native", func(t *testing.T) {
   798  		_, _, err := (*envoyBootstrapHook)(nil).extractNameAndKind(
   799  			structs.NewTaskKind(structs.ConnectNativePrefix, "foo"),
   800  		)
   801  		require.EqualError(t, err, "envoy must be used as connect sidecar or gateway")
   802  	})
   803  
   804  	t.Run("normal task", func(t *testing.T) {
   805  		_, _, err := (*envoyBootstrapHook)(nil).extractNameAndKind(
   806  			structs.TaskKind(""),
   807  		)
   808  		require.EqualError(t, err, "envoy must be used as connect sidecar or gateway")
   809  	})
   810  }
   811  
   812  func TestTaskRunner_EnvoyBootstrapHook_grpcAddress(t *testing.T) {
   813  	ci.Parallel(t)
   814  
   815  	bridgeH := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(
   816  		mock.ConnectIngressGatewayAlloc("bridge"),
   817  		new(config.ConsulConfig),
   818  		consulNamespace,
   819  		testlog.HCLogger(t),
   820  	))
   821  
   822  	hostH := newEnvoyBootstrapHook(newEnvoyBootstrapHookConfig(
   823  		mock.ConnectIngressGatewayAlloc("host"),
   824  		new(config.ConsulConfig),
   825  		consulNamespace,
   826  		testlog.HCLogger(t),
   827  	))
   828  
   829  	t.Run("environment", func(t *testing.T) {
   830  		env := map[string]string{
   831  			grpcConsulVariable: "1.2.3.4:9000",
   832  		}
   833  		require.Equal(t, "1.2.3.4:9000", bridgeH.grpcAddress(env))
   834  		require.Equal(t, "1.2.3.4:9000", hostH.grpcAddress(env))
   835  	})
   836  
   837  	t.Run("defaults", func(t *testing.T) {
   838  		require.Equal(t, "unix://alloc/tmp/consul_grpc.sock", bridgeH.grpcAddress(nil))
   839  		require.Equal(t, "127.0.0.1:8502", hostH.grpcAddress(nil))
   840  	})
   841  }
   842  
   843  func TestTaskRunner_EnvoyBootstrapHook_isConnectKind(t *testing.T) {
   844  	ci.Parallel(t)
   845  
   846  	require.True(t, isConnectKind(structs.ConnectProxyPrefix))
   847  	require.True(t, isConnectKind(structs.ConnectIngressPrefix))
   848  	require.True(t, isConnectKind(structs.ConnectTerminatingPrefix))
   849  	require.True(t, isConnectKind(structs.ConnectMeshPrefix))
   850  	require.False(t, isConnectKind(""))
   851  	require.False(t, isConnectKind("something"))
   852  }