github.com/hernad/nomad@v1.6.112/nomad/job_endpoint_hook_connect_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package nomad
     5  
     6  import (
     7  	"fmt"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/hernad/nomad/ci"
    12  	"github.com/hernad/nomad/helper"
    13  	"github.com/hernad/nomad/helper/pointer"
    14  	"github.com/hernad/nomad/helper/testlog"
    15  	"github.com/hernad/nomad/nomad/mock"
    16  	"github.com/hernad/nomad/nomad/structs"
    17  	"github.com/shoenig/test/must"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  func TestJobEndpointConnect_isSidecarForService(t *testing.T) {
    22  	ci.Parallel(t)
    23  
    24  	cases := []struct {
    25  		task    *structs.Task
    26  		sidecar string
    27  		exp     bool
    28  	}{
    29  		{
    30  			&structs.Task{},
    31  			"api",
    32  			false,
    33  		},
    34  		{
    35  			&structs.Task{
    36  				Kind: "connect-proxy:api",
    37  			},
    38  			"api",
    39  			true,
    40  		},
    41  		{
    42  			&structs.Task{
    43  				Kind: "connect-proxy:api",
    44  			},
    45  			"db",
    46  			false,
    47  		},
    48  		{
    49  			&structs.Task{
    50  				Kind: "api",
    51  			},
    52  			"api",
    53  			false,
    54  		},
    55  	}
    56  
    57  	for _, c := range cases {
    58  		require.Equal(t, c.exp, isSidecarForService(c.task, c.sidecar))
    59  	}
    60  }
    61  
    62  func TestJobEndpointConnect_groupConnectGuessTaskDriver(t *testing.T) {
    63  	ci.Parallel(t)
    64  
    65  	cases := []struct {
    66  		name    string
    67  		drivers []string
    68  		exp     string
    69  	}{
    70  		{
    71  			name:    "none",
    72  			drivers: nil,
    73  			exp:     "docker",
    74  		},
    75  		{
    76  			name:    "neither",
    77  			drivers: []string{"exec", "raw_exec", "rkt"},
    78  			exp:     "docker",
    79  		},
    80  		{
    81  			name:    "docker only",
    82  			drivers: []string{"docker"},
    83  			exp:     "docker",
    84  		},
    85  		{
    86  			name:    "podman only",
    87  			drivers: []string{"podman"},
    88  			exp:     "podman",
    89  		},
    90  		{
    91  			name:    "mix with docker",
    92  			drivers: []string{"podman", "docker", "exec"},
    93  			exp:     "docker",
    94  		},
    95  		{
    96  			name:    "mix without docker",
    97  			drivers: []string{"exec", "podman", "raw_exec"},
    98  			exp:     "podman",
    99  		},
   100  	}
   101  
   102  	for _, tc := range cases {
   103  		tasks := helper.ConvertSlice(tc.drivers, func(driver string) *structs.Task {
   104  			return &structs.Task{Driver: driver}
   105  		})
   106  		tg := &structs.TaskGroup{Tasks: tasks}
   107  		result := groupConnectGuessTaskDriver(tg)
   108  		must.Eq(t, tc.exp, result)
   109  	}
   110  }
   111  
   112  func TestJobEndpointConnect_newConnectSidecarTask(t *testing.T) {
   113  	ci.Parallel(t)
   114  
   115  	task := newConnectSidecarTask("redis", "podman")
   116  	must.Eq(t, "connect-proxy-redis", task.Name)
   117  	must.Eq(t, "podman", task.Driver)
   118  
   119  	task2 := newConnectSidecarTask("db", "docker")
   120  	must.Eq(t, "connect-proxy-db", task2.Name)
   121  	must.Eq(t, "docker", task2.Driver)
   122  }
   123  
   124  func TestJobEndpointConnect_groupConnectHook(t *testing.T) {
   125  	ci.Parallel(t)
   126  
   127  	// Test that connect-proxy task is inserted for backend service
   128  	job := mock.Job()
   129  
   130  	job.Meta = map[string]string{
   131  		"backend_name": "backend",
   132  		"admin_name":   "admin",
   133  	}
   134  
   135  	job.TaskGroups[0] = &structs.TaskGroup{
   136  		Networks: structs.Networks{{
   137  			Mode: "bridge",
   138  		}},
   139  		Services: []*structs.Service{{
   140  			Name:      "${NOMAD_META_backend_name}",
   141  			PortLabel: "8080",
   142  			Connect: &structs.ConsulConnect{
   143  				SidecarService: &structs.ConsulSidecarService{},
   144  			},
   145  		}, {
   146  			Name:      "${NOMAD_META_admin_name}",
   147  			PortLabel: "9090",
   148  			Connect: &structs.ConsulConnect{
   149  				SidecarService: &structs.ConsulSidecarService{},
   150  			},
   151  		}},
   152  	}
   153  
   154  	// Expected tasks
   155  	tgExp := job.TaskGroups[0].Copy()
   156  	tgExp.Tasks = []*structs.Task{
   157  		newConnectSidecarTask("backend", "docker"),
   158  		newConnectSidecarTask("admin", "docker"),
   159  	}
   160  	tgExp.Services[0].Name = "backend"
   161  	tgExp.Services[1].Name = "admin"
   162  
   163  	// Expect sidecar tasks to be in canonical form.
   164  	tgExp.Tasks[0].Canonicalize(job, tgExp)
   165  	tgExp.Tasks[1].Canonicalize(job, tgExp)
   166  	tgExp.Networks[0].DynamicPorts = []structs.Port{{
   167  		Label: fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, "backend"),
   168  		To:    -1,
   169  	}, {
   170  		Label: fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, "admin"),
   171  		To:    -1,
   172  	}}
   173  	tgExp.Networks[0].Canonicalize()
   174  
   175  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   176  	require.Exactly(t, tgExp, job.TaskGroups[0])
   177  
   178  	// Test that hook is idempotent
   179  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   180  	require.Exactly(t, tgExp, job.TaskGroups[0])
   181  }
   182  
   183  func TestJobEndpointConnect_groupConnectHook_IngressGateway_BridgeNetwork(t *testing.T) {
   184  	ci.Parallel(t)
   185  
   186  	// Test that the connect ingress gateway task is inserted if a gateway service
   187  	// exists and since this is a bridge network, will rewrite the default gateway proxy
   188  	// block with correct configuration.
   189  	job := mock.ConnectIngressGatewayJob("bridge", false)
   190  	job.Meta = map[string]string{
   191  		"gateway_name": "my-gateway",
   192  	}
   193  	job.TaskGroups[0].Services[0].Name = "${NOMAD_META_gateway_name}"
   194  	job.TaskGroups[0].Services[0].Connect.Gateway.Ingress.TLS = &structs.ConsulGatewayTLSConfig{
   195  		Enabled:       true,
   196  		TLSMinVersion: "TLSv1_2",
   197  	}
   198  
   199  	// setup expectations
   200  	expTG := job.TaskGroups[0].Copy()
   201  	expTG.Tasks = []*structs.Task{
   202  		// inject the gateway task
   203  		newConnectGatewayTask(structs.ConnectIngressPrefix, "my-gateway", false, true),
   204  	}
   205  	expTG.Services[0].Name = "my-gateway"
   206  	expTG.Tasks[0].Canonicalize(job, expTG)
   207  	expTG.Networks[0].Canonicalize()
   208  
   209  	// rewrite the service gateway proxy configuration
   210  	expTG.Services[0].Connect.Gateway.Proxy = gatewayProxy(expTG.Services[0].Connect.Gateway, "bridge")
   211  
   212  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   213  	require.Exactly(t, expTG, job.TaskGroups[0])
   214  
   215  	// Test that the hook is idempotent
   216  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   217  	require.Exactly(t, expTG, job.TaskGroups[0])
   218  
   219  	// Test that the hook populates the correct constraint for customized tls
   220  	require.Contains(t, job.TaskGroups[0].Tasks[0].Constraints, &structs.Constraint{
   221  		LTarget: "${attr.consul.version}",
   222  		RTarget: ">= 1.11.2",
   223  		Operand: structs.ConstraintSemver,
   224  	})
   225  }
   226  
   227  func TestJobEndpointConnect_groupConnectHook_IngressGateway_HostNetwork(t *testing.T) {
   228  	ci.Parallel(t)
   229  
   230  	// Test that the connect ingress gateway task is inserted if a gateway service
   231  	// exists. In host network mode, the default values are used.
   232  	job := mock.ConnectIngressGatewayJob("host", false)
   233  	job.Meta = map[string]string{
   234  		"gateway_name": "my-gateway",
   235  	}
   236  	job.TaskGroups[0].Services[0].Name = "${NOMAD_META_gateway_name}"
   237  
   238  	// setup expectations
   239  	expTG := job.TaskGroups[0].Copy()
   240  	expTG.Tasks = []*structs.Task{
   241  		// inject the gateway task
   242  		newConnectGatewayTask(structs.ConnectIngressPrefix, "my-gateway", true, false),
   243  	}
   244  	expTG.Services[0].Name = "my-gateway"
   245  	expTG.Tasks[0].Canonicalize(job, expTG)
   246  	expTG.Networks[0].Canonicalize()
   247  
   248  	// rewrite the service gateway proxy configuration
   249  	expTG.Services[0].Connect.Gateway.Proxy = gatewayProxy(expTG.Services[0].Connect.Gateway, "host")
   250  
   251  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   252  	require.Exactly(t, expTG, job.TaskGroups[0])
   253  
   254  	// Test that the hook is idempotent
   255  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   256  	require.Exactly(t, expTG, job.TaskGroups[0])
   257  }
   258  
   259  func TestJobEndpointConnect_groupConnectHook_IngressGateway_CustomTask(t *testing.T) {
   260  	ci.Parallel(t)
   261  
   262  	// Test that the connect gateway task is inserted if a gateway service exists
   263  	// and since this is a bridge network, will rewrite the default gateway proxy
   264  	// block with correct configuration.
   265  	job := mock.ConnectIngressGatewayJob("bridge", false)
   266  	job.Meta = map[string]string{
   267  		"gateway_name": "my-gateway",
   268  	}
   269  	job.TaskGroups[0].Services[0].Name = "${NOMAD_META_gateway_name}"
   270  	job.TaskGroups[0].Services[0].Connect.SidecarTask = &structs.SidecarTask{
   271  		Driver: "raw_exec",
   272  		User:   "sidecars",
   273  		Config: map[string]interface{}{
   274  			"command": "/bin/sidecar",
   275  			"args":    []string{"a", "b"},
   276  		},
   277  		Resources: &structs.Resources{
   278  			CPU: 400,
   279  			// Memory: inherit 128
   280  		},
   281  		KillSignal: "SIGHUP",
   282  	}
   283  
   284  	// setup expectations
   285  	expTG := job.TaskGroups[0].Copy()
   286  	expTG.Tasks = []*structs.Task{
   287  		// inject merged gateway task
   288  		{
   289  			Name:   "connect-ingress-my-gateway",
   290  			Kind:   structs.NewTaskKind(structs.ConnectIngressPrefix, "my-gateway"),
   291  			Driver: "raw_exec",
   292  			User:   "sidecars",
   293  			Config: map[string]interface{}{
   294  				"command": "/bin/sidecar",
   295  				"args":    []string{"a", "b"},
   296  			},
   297  			Resources: &structs.Resources{
   298  				CPU:      400,
   299  				MemoryMB: 128,
   300  			},
   301  			LogConfig: &structs.LogConfig{
   302  				MaxFiles:      2,
   303  				MaxFileSizeMB: 2,
   304  			},
   305  			ShutdownDelay: 5 * time.Second,
   306  			KillSignal:    "SIGHUP",
   307  			Constraints: structs.Constraints{
   308  				connectGatewayVersionConstraint(),
   309  				connectListenerConstraint(),
   310  			},
   311  		},
   312  	}
   313  	expTG.Services[0].Name = "my-gateway"
   314  	expTG.Tasks[0].Canonicalize(job, expTG)
   315  	expTG.Networks[0].Canonicalize()
   316  
   317  	// rewrite the service gateway proxy configuration
   318  	expTG.Services[0].Connect.Gateway.Proxy = gatewayProxy(expTG.Services[0].Connect.Gateway, "bridge")
   319  
   320  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   321  	require.Exactly(t, expTG, job.TaskGroups[0])
   322  
   323  	// Test that the hook is idempotent
   324  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   325  	require.Exactly(t, expTG, job.TaskGroups[0])
   326  }
   327  
   328  func TestJobEndpointConnect_groupConnectHook_TerminatingGateway(t *testing.T) {
   329  	ci.Parallel(t)
   330  
   331  	// Tests that the connect terminating gateway task is inserted if a gateway
   332  	// service exists and since this is a bridge network, will rewrite the default
   333  	// gateway proxy block with correct configuration.
   334  	job := mock.ConnectTerminatingGatewayJob("bridge", false)
   335  	job.Meta = map[string]string{
   336  		"gateway_name": "my-gateway",
   337  	}
   338  	job.TaskGroups[0].Services[0].Name = "${NOMAD_META_gateway_name}"
   339  
   340  	// setup expectations
   341  	expTG := job.TaskGroups[0].Copy()
   342  	expTG.Tasks = []*structs.Task{
   343  		// inject the gateway task
   344  		newConnectGatewayTask(structs.ConnectTerminatingPrefix, "my-gateway", false, false),
   345  	}
   346  	expTG.Services[0].Name = "my-gateway"
   347  	expTG.Tasks[0].Canonicalize(job, expTG)
   348  	expTG.Networks[0].Canonicalize()
   349  
   350  	// rewrite the service gateway proxy configuration
   351  	expTG.Services[0].Connect.Gateway.Proxy = gatewayProxy(expTG.Services[0].Connect.Gateway, "bridge")
   352  
   353  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   354  	require.Exactly(t, expTG, job.TaskGroups[0])
   355  
   356  	// Test that the hook is idempotent
   357  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   358  	require.Exactly(t, expTG, job.TaskGroups[0])
   359  }
   360  
   361  func TestJobEndpointConnect_groupConnectHook_MeshGateway(t *testing.T) {
   362  	ci.Parallel(t)
   363  
   364  	// Test that the connect mesh gateway task is inserted if a gateway service
   365  	// exists and since this is a bridge network, will rewrite the default gateway
   366  	// proxy block with correct configuration, injecting a dynamic port for use
   367  	// by the envoy lan listener.
   368  	job := mock.ConnectMeshGatewayJob("bridge", false)
   369  	job.Meta = map[string]string{
   370  		"gateway_name": "my-gateway",
   371  	}
   372  	job.TaskGroups[0].Services[0].Name = "${NOMAD_META_gateway_name}"
   373  
   374  	// setup expectations
   375  	expTG := job.TaskGroups[0].Copy()
   376  	expTG.Tasks = []*structs.Task{
   377  		// inject the gateway task
   378  		newConnectGatewayTask(structs.ConnectMeshPrefix, "my-gateway", false, false),
   379  	}
   380  	expTG.Services[0].Name = "my-gateway"
   381  	expTG.Services[0].PortLabel = "public_port"
   382  	expTG.Networks[0].DynamicPorts = []structs.Port{{
   383  		Label:       "connect-mesh-my-gateway-lan",
   384  		Value:       0,
   385  		To:          -1,
   386  		HostNetwork: "default",
   387  	}}
   388  	expTG.Tasks[0].Canonicalize(job, expTG)
   389  	expTG.Networks[0].Canonicalize()
   390  
   391  	// rewrite the service gateway proxy configuration
   392  	expTG.Services[0].Connect.Gateway.Proxy = gatewayProxy(expTG.Services[0].Connect.Gateway, "bridge")
   393  
   394  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   395  	require.Exactly(t, expTG, job.TaskGroups[0])
   396  
   397  	// Test that the hook is idempotent
   398  	require.NoError(t, groupConnectHook(job, job.TaskGroups[0]))
   399  	require.Exactly(t, expTG, job.TaskGroups[0])
   400  }
   401  
   402  // TestJobEndpoint_ConnectInterpolation asserts that when a Connect sidecar
   403  // proxy task is being created for a group service with an interpolated name,
   404  // the service name is interpolated *before the task is created.
   405  //
   406  // See https://github.com/hernad/nomad/issues/6853
   407  func TestJobEndpointConnect_ConnectInterpolation(t *testing.T) {
   408  	ci.Parallel(t)
   409  
   410  	server := &Server{logger: testlog.HCLogger(t)}
   411  	jobEndpoint := NewJobEndpoints(server, nil)
   412  
   413  	j := mock.ConnectJob()
   414  	j.TaskGroups[0].Services[0].Name = "${JOB}-api"
   415  	j, warnings, err := jobEndpoint.admissionMutators(j)
   416  	require.NoError(t, err)
   417  	require.Nil(t, warnings)
   418  
   419  	require.Len(t, j.TaskGroups[0].Tasks, 2)
   420  	require.Equal(t, "connect-proxy-my-job-api", j.TaskGroups[0].Tasks[1].Name)
   421  }
   422  
   423  func TestJobEndpointConnect_groupConnectSidecarValidate(t *testing.T) {
   424  	ci.Parallel(t)
   425  
   426  	// network validation
   427  
   428  	makeService := func(name string) *structs.Service {
   429  		return &structs.Service{Name: name, Connect: &structs.ConsulConnect{
   430  			SidecarService: new(structs.ConsulSidecarService),
   431  		}}
   432  	}
   433  
   434  	t.Run("sidecar 0 networks", func(t *testing.T) {
   435  		require.EqualError(t, groupConnectSidecarValidate(&structs.TaskGroup{
   436  			Name:     "g1",
   437  			Networks: nil,
   438  		}, makeService("connect-service")), `Consul Connect sidecars require exactly 1 network, found 0 in group "g1"`)
   439  	})
   440  
   441  	t.Run("sidecar non bridge", func(t *testing.T) {
   442  		require.EqualError(t, groupConnectSidecarValidate(&structs.TaskGroup{
   443  			Name: "g2",
   444  			Networks: structs.Networks{{
   445  				Mode: "host",
   446  			}},
   447  		}, makeService("connect-service")), `Consul Connect sidecar requires bridge network, found "host" in group "g2"`)
   448  	})
   449  
   450  	t.Run("sidecar okay", func(t *testing.T) {
   451  		require.NoError(t, groupConnectSidecarValidate(&structs.TaskGroup{
   452  			Name: "g3",
   453  			Networks: structs.Networks{{
   454  				Mode: "bridge",
   455  			}},
   456  		}, makeService("connect-service")))
   457  	})
   458  
   459  	// group and service name validation
   460  
   461  	t.Run("non-connect service contains uppercase characters", func(t *testing.T) {
   462  		err := groupConnectValidate(&structs.TaskGroup{
   463  			Name:     "group",
   464  			Networks: structs.Networks{{Mode: "bridge"}},
   465  			Services: []*structs.Service{{
   466  				Name: "Other-Service",
   467  			}},
   468  		})
   469  		require.NoError(t, err)
   470  	})
   471  
   472  	t.Run("connect service contains uppercase characters", func(t *testing.T) {
   473  		err := groupConnectValidate(&structs.TaskGroup{
   474  			Name:     "group",
   475  			Networks: structs.Networks{{Mode: "bridge"}},
   476  			Services: []*structs.Service{{
   477  				Name: "Other-Service",
   478  			}, makeService("Connect-Service")},
   479  		})
   480  		require.EqualError(t, err, `Consul Connect service name "Connect-Service" in group "group" must not contain uppercase characters`)
   481  	})
   482  
   483  	t.Run("non-connect group contains uppercase characters", func(t *testing.T) {
   484  		err := groupConnectValidate(&structs.TaskGroup{
   485  			Name:     "Other-Group",
   486  			Networks: structs.Networks{{Mode: "bridge"}},
   487  			Services: []*structs.Service{{
   488  				Name: "other-service",
   489  			}},
   490  		})
   491  		require.NoError(t, err)
   492  	})
   493  
   494  	t.Run("connect-group contains uppercase characters", func(t *testing.T) {
   495  		err := groupConnectValidate(&structs.TaskGroup{
   496  			Name:     "Connect-Group",
   497  			Networks: structs.Networks{{Mode: "bridge"}},
   498  			Services: []*structs.Service{{
   499  				Name: "other-service",
   500  			}, makeService("connect-service")},
   501  		})
   502  		require.EqualError(t, err, `Consul Connect group "Connect-Group" with service "connect-service" must not contain uppercase characters`)
   503  	})
   504  
   505  	t.Run("connect group and service lowercase", func(t *testing.T) {
   506  		err := groupConnectValidate(&structs.TaskGroup{
   507  			Name:     "connect-group",
   508  			Networks: structs.Networks{{Mode: "bridge"}},
   509  			Services: []*structs.Service{{
   510  				Name: "other-service",
   511  			}, makeService("connect-service")},
   512  		})
   513  		require.NoError(t, err)
   514  	})
   515  
   516  	t.Run("connect group overlap upstreams", func(t *testing.T) {
   517  		s1 := makeService("s1")
   518  		s2 := makeService("s2")
   519  		s1.Connect.SidecarService.Proxy = &structs.ConsulProxy{
   520  			Upstreams: []structs.ConsulUpstream{{
   521  				LocalBindPort: 8999,
   522  			}},
   523  		}
   524  		s2.Connect.SidecarService.Proxy = &structs.ConsulProxy{
   525  			Upstreams: []structs.ConsulUpstream{{
   526  				LocalBindPort: 8999,
   527  			}},
   528  		}
   529  		err := groupConnectValidate(&structs.TaskGroup{
   530  			Name:     "connect-group",
   531  			Networks: structs.Networks{{Mode: "bridge"}},
   532  			Services: []*structs.Service{s1, s2},
   533  		})
   534  		require.EqualError(t, err, `Consul Connect services "s2" and "s1" in group "connect-group" using same address for upstreams (:8999)`)
   535  	})
   536  }
   537  
   538  func TestJobEndpointConnect_groupConnectUpstreamsValidate(t *testing.T) {
   539  	ci.Parallel(t)
   540  
   541  	t.Run("no connect services", func(t *testing.T) {
   542  		err := groupConnectUpstreamsValidate("group",
   543  			[]*structs.Service{{Name: "s1"}, {Name: "s2"}})
   544  		require.NoError(t, err)
   545  	})
   546  
   547  	t.Run("connect services no overlap", func(t *testing.T) {
   548  		err := groupConnectUpstreamsValidate("group",
   549  			[]*structs.Service{
   550  				{
   551  					Name: "s1",
   552  					Connect: &structs.ConsulConnect{
   553  						SidecarService: &structs.ConsulSidecarService{
   554  							Proxy: &structs.ConsulProxy{
   555  								Upstreams: []structs.ConsulUpstream{{
   556  									LocalBindAddress: "127.0.0.1",
   557  									LocalBindPort:    9001,
   558  								}, {
   559  									LocalBindAddress: "127.0.0.1",
   560  									LocalBindPort:    9002,
   561  								}},
   562  							},
   563  						},
   564  					},
   565  				},
   566  				{
   567  					Name: "s2",
   568  					Connect: &structs.ConsulConnect{
   569  						SidecarService: &structs.ConsulSidecarService{
   570  							Proxy: &structs.ConsulProxy{
   571  								Upstreams: []structs.ConsulUpstream{{
   572  									LocalBindAddress: "10.0.0.1",
   573  									LocalBindPort:    9001,
   574  								}, {
   575  									LocalBindAddress: "127.0.0.1",
   576  									LocalBindPort:    9003,
   577  								}},
   578  							},
   579  						},
   580  					},
   581  				},
   582  			})
   583  		require.NoError(t, err)
   584  	})
   585  
   586  	t.Run("connect services overlap port", func(t *testing.T) {
   587  		err := groupConnectUpstreamsValidate("group",
   588  			[]*structs.Service{
   589  				{
   590  					Name: "s1",
   591  					Connect: &structs.ConsulConnect{
   592  						SidecarService: &structs.ConsulSidecarService{
   593  							Proxy: &structs.ConsulProxy{
   594  								Upstreams: []structs.ConsulUpstream{{
   595  									LocalBindAddress: "127.0.0.1",
   596  									LocalBindPort:    9001,
   597  								}, {
   598  									LocalBindAddress: "127.0.0.1",
   599  									LocalBindPort:    9002,
   600  								}},
   601  							},
   602  						},
   603  					},
   604  				},
   605  				{
   606  					Name: "s2",
   607  					Connect: &structs.ConsulConnect{
   608  						SidecarService: &structs.ConsulSidecarService{
   609  							Proxy: &structs.ConsulProxy{
   610  								Upstreams: []structs.ConsulUpstream{{
   611  									LocalBindAddress: "127.0.0.1",
   612  									LocalBindPort:    9002,
   613  								}, {
   614  									LocalBindAddress: "127.0.0.1",
   615  									LocalBindPort:    9003,
   616  								}},
   617  							},
   618  						},
   619  					},
   620  				},
   621  			})
   622  		require.EqualError(t, err, `Consul Connect services "s2" and "s1" in group "group" using same address for upstreams (127.0.0.1:9002)`)
   623  	})
   624  }
   625  
   626  func TestJobEndpointConnect_getNamedTaskForNativeService(t *testing.T) {
   627  	ci.Parallel(t)
   628  
   629  	t.Run("named exists", func(t *testing.T) {
   630  		task, err := getNamedTaskForNativeService(&structs.TaskGroup{
   631  			Name:  "g1",
   632  			Tasks: []*structs.Task{{Name: "t1"}, {Name: "t2"}},
   633  		}, "s1", "t2")
   634  		require.NoError(t, err)
   635  		require.Equal(t, "t2", task.Name)
   636  	})
   637  
   638  	t.Run("infer exists", func(t *testing.T) {
   639  		task, err := getNamedTaskForNativeService(&structs.TaskGroup{
   640  			Name:  "g1",
   641  			Tasks: []*structs.Task{{Name: "t2"}},
   642  		}, "s1", "")
   643  		require.NoError(t, err)
   644  		require.Equal(t, "t2", task.Name)
   645  	})
   646  
   647  	t.Run("infer ambiguous", func(t *testing.T) {
   648  		task, err := getNamedTaskForNativeService(&structs.TaskGroup{
   649  			Name:  "g1",
   650  			Tasks: []*structs.Task{{Name: "t1"}, {Name: "t2"}},
   651  		}, "s1", "")
   652  		require.EqualError(t, err, "task for Consul Connect Native service g1->s1 is ambiguous and must be set")
   653  		require.Nil(t, task)
   654  	})
   655  
   656  	t.Run("named absent", func(t *testing.T) {
   657  		task, err := getNamedTaskForNativeService(&structs.TaskGroup{
   658  			Name:  "g1",
   659  			Tasks: []*structs.Task{{Name: "t1"}, {Name: "t2"}},
   660  		}, "s1", "t3")
   661  		require.EqualError(t, err, "task t3 named by Consul Connect Native service g1->s1 does not exist")
   662  		require.Nil(t, task)
   663  	})
   664  }
   665  
   666  func TestJobEndpointConnect_groupConnectGatewayValidate(t *testing.T) {
   667  	ci.Parallel(t)
   668  
   669  	t.Run("no group network", func(t *testing.T) {
   670  		err := groupConnectGatewayValidate(&structs.TaskGroup{
   671  			Name:     "g1",
   672  			Networks: nil,
   673  		})
   674  		require.EqualError(t, err, `Consul Connect gateways require exactly 1 network, found 0 in group "g1"`)
   675  	})
   676  
   677  	t.Run("bad network mode", func(t *testing.T) {
   678  		err := groupConnectGatewayValidate(&structs.TaskGroup{
   679  			Name: "g1",
   680  			Networks: structs.Networks{{
   681  				Mode: "",
   682  			}},
   683  		})
   684  		require.EqualError(t, err, `Consul Connect Gateway service requires Task Group with network mode of type "bridge" or "host"`)
   685  	})
   686  }
   687  
   688  func TestJobEndpointConnect_newConnectGatewayTask_host(t *testing.T) {
   689  	ci.Parallel(t)
   690  
   691  	t.Run("ingress", func(t *testing.T) {
   692  		task := newConnectGatewayTask(structs.ConnectIngressPrefix, "foo", true, false)
   693  		require.Equal(t, "connect-ingress-foo", task.Name)
   694  		require.Equal(t, "connect-ingress:foo", string(task.Kind))
   695  		require.Equal(t, ">= 1.8.0", task.Constraints[0].RTarget)
   696  		require.Equal(t, "host", task.Config["network_mode"])
   697  		require.Nil(t, task.Lifecycle)
   698  	})
   699  
   700  	t.Run("terminating", func(t *testing.T) {
   701  		task := newConnectGatewayTask(structs.ConnectTerminatingPrefix, "bar", true, false)
   702  		require.Equal(t, "connect-terminating-bar", task.Name)
   703  		require.Equal(t, "connect-terminating:bar", string(task.Kind))
   704  		require.Equal(t, ">= 1.8.0", task.Constraints[0].RTarget)
   705  		require.Equal(t, "host", task.Config["network_mode"])
   706  		require.Nil(t, task.Lifecycle)
   707  	})
   708  }
   709  
   710  func TestJobEndpointConnect_newConnectGatewayTask_bridge(t *testing.T) {
   711  	ci.Parallel(t)
   712  
   713  	task := newConnectGatewayTask(structs.ConnectIngressPrefix, "service1", false, false)
   714  	require.NotContains(t, task.Config, "network_mode")
   715  }
   716  
   717  func TestJobEndpointConnect_hasGatewayTaskForService(t *testing.T) {
   718  	ci.Parallel(t)
   719  
   720  	t.Run("no gateway task", func(t *testing.T) {
   721  		result := hasGatewayTaskForService(&structs.TaskGroup{
   722  			Name: "group",
   723  			Tasks: []*structs.Task{{
   724  				Name: "task1",
   725  				Kind: "",
   726  			}},
   727  		}, "my-service")
   728  		require.False(t, result)
   729  	})
   730  
   731  	t.Run("has ingress task", func(t *testing.T) {
   732  		result := hasGatewayTaskForService(&structs.TaskGroup{
   733  			Name: "group",
   734  			Tasks: []*structs.Task{{
   735  				Name: "ingress-gateway-my-service",
   736  				Kind: structs.NewTaskKind(structs.ConnectIngressPrefix, "my-service"),
   737  			}},
   738  		}, "my-service")
   739  		require.True(t, result)
   740  	})
   741  
   742  	t.Run("has terminating task", func(t *testing.T) {
   743  		result := hasGatewayTaskForService(&structs.TaskGroup{
   744  			Name: "group",
   745  			Tasks: []*structs.Task{{
   746  				Name: "terminating-gateway-my-service",
   747  				Kind: structs.NewTaskKind(structs.ConnectTerminatingPrefix, "my-service"),
   748  			}},
   749  		}, "my-service")
   750  		require.True(t, result)
   751  	})
   752  
   753  	t.Run("has mesh task", func(t *testing.T) {
   754  		result := hasGatewayTaskForService(&structs.TaskGroup{
   755  			Name: "group",
   756  			Tasks: []*structs.Task{{
   757  				Name: "mesh-gateway-my-service",
   758  				Kind: structs.NewTaskKind(structs.ConnectMeshPrefix, "my-service"),
   759  			}},
   760  		}, "my-service")
   761  		require.True(t, result)
   762  	})
   763  }
   764  
   765  func TestJobEndpointConnect_gatewayProxyIsDefault(t *testing.T) {
   766  	ci.Parallel(t)
   767  
   768  	t.Run("nil", func(t *testing.T) {
   769  		result := gatewayProxyIsDefault(nil)
   770  		require.True(t, result)
   771  	})
   772  
   773  	t.Run("unrelated fields set", func(t *testing.T) {
   774  		result := gatewayProxyIsDefault(&structs.ConsulGatewayProxy{
   775  			ConnectTimeout: pointer.Of(2 * time.Second),
   776  			Config:         map[string]interface{}{"foo": 1},
   777  		})
   778  		require.True(t, result)
   779  	})
   780  
   781  	t.Run("no-bind set", func(t *testing.T) {
   782  		result := gatewayProxyIsDefault(&structs.ConsulGatewayProxy{
   783  			EnvoyGatewayNoDefaultBind: true,
   784  		})
   785  		require.False(t, result)
   786  	})
   787  
   788  	t.Run("bind-tagged set", func(t *testing.T) {
   789  		result := gatewayProxyIsDefault(&structs.ConsulGatewayProxy{
   790  			EnvoyGatewayBindTaggedAddresses: true,
   791  		})
   792  		require.False(t, result)
   793  	})
   794  
   795  	t.Run("bind-addresses set", func(t *testing.T) {
   796  		result := gatewayProxyIsDefault(&structs.ConsulGatewayProxy{
   797  			EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{
   798  				"listener1": {
   799  					Address: "1.1.1.1",
   800  					Port:    9000,
   801  				},
   802  			},
   803  		})
   804  		require.False(t, result)
   805  	})
   806  }
   807  
   808  func TestJobEndpointConnect_gatewayBindAddressesForBridge(t *testing.T) {
   809  	ci.Parallel(t)
   810  
   811  	t.Run("nil", func(t *testing.T) {
   812  
   813  		result := gatewayBindAddressesIngressForBridge(nil)
   814  		require.Empty(t, result)
   815  	})
   816  
   817  	t.Run("no listeners", func(t *testing.T) {
   818  		result := gatewayBindAddressesIngressForBridge(&structs.ConsulIngressConfigEntry{Listeners: nil})
   819  		require.Empty(t, result)
   820  	})
   821  
   822  	t.Run("simple", func(t *testing.T) {
   823  		result := gatewayBindAddressesIngressForBridge(&structs.ConsulIngressConfigEntry{
   824  			Listeners: []*structs.ConsulIngressListener{{
   825  				Port:     3000,
   826  				Protocol: "tcp",
   827  				Services: []*structs.ConsulIngressService{{
   828  					Name: "service1",
   829  				}},
   830  			}},
   831  		})
   832  		require.Equal(t, map[string]*structs.ConsulGatewayBindAddress{
   833  			"service1": {
   834  				Address: "0.0.0.0",
   835  				Port:    3000,
   836  			},
   837  		}, result)
   838  	})
   839  
   840  	t.Run("complex", func(t *testing.T) {
   841  		result := gatewayBindAddressesIngressForBridge(&structs.ConsulIngressConfigEntry{
   842  			Listeners: []*structs.ConsulIngressListener{{
   843  				Port:     3000,
   844  				Protocol: "tcp",
   845  				Services: []*structs.ConsulIngressService{{
   846  					Name: "service1",
   847  				}, {
   848  					Name: "service2",
   849  				}},
   850  			}, {
   851  				Port:     3001,
   852  				Protocol: "http",
   853  				Services: []*structs.ConsulIngressService{{
   854  					Name: "service3",
   855  				}},
   856  			}},
   857  		})
   858  		require.Equal(t, map[string]*structs.ConsulGatewayBindAddress{
   859  			"service1": {
   860  				Address: "0.0.0.0",
   861  				Port:    3000,
   862  			},
   863  			"service2": {
   864  				Address: "0.0.0.0",
   865  				Port:    3000,
   866  			},
   867  			"service3": {
   868  				Address: "0.0.0.0",
   869  				Port:    3001,
   870  			},
   871  		}, result)
   872  	})
   873  }
   874  
   875  func TestJobEndpointConnect_gatewayProxy(t *testing.T) {
   876  	ci.Parallel(t)
   877  
   878  	t.Run("nil", func(t *testing.T) {
   879  		result := gatewayProxy(nil, "bridge")
   880  		require.Nil(t, result)
   881  	})
   882  
   883  	t.Run("nil proxy", func(t *testing.T) {
   884  		result := gatewayProxy(&structs.ConsulGateway{
   885  			Ingress: &structs.ConsulIngressConfigEntry{
   886  				Listeners: []*structs.ConsulIngressListener{{
   887  					Port:     3000,
   888  					Protocol: "tcp",
   889  					Services: []*structs.ConsulIngressService{{
   890  						Name: "service1",
   891  					}},
   892  				}},
   893  			},
   894  		}, "bridge")
   895  		require.Equal(t, &structs.ConsulGatewayProxy{
   896  			ConnectTimeout:                  pointer.Of(defaultConnectTimeout),
   897  			EnvoyGatewayNoDefaultBind:       true,
   898  			EnvoyGatewayBindTaggedAddresses: false,
   899  			EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{
   900  				"service1": {
   901  					Address: "0.0.0.0",
   902  					Port:    3000,
   903  				}},
   904  		}, result)
   905  	})
   906  
   907  	t.Run("ingress set defaults", func(t *testing.T) {
   908  		result := gatewayProxy(&structs.ConsulGateway{
   909  			Proxy: &structs.ConsulGatewayProxy{
   910  				ConnectTimeout: pointer.Of(2 * time.Second),
   911  				Config:         map[string]interface{}{"foo": 1},
   912  			},
   913  			Ingress: &structs.ConsulIngressConfigEntry{
   914  				Listeners: []*structs.ConsulIngressListener{{
   915  					Port:     3000,
   916  					Protocol: "tcp",
   917  					Services: []*structs.ConsulIngressService{{
   918  						Name: "service1",
   919  					}},
   920  				}},
   921  			},
   922  		}, "bridge")
   923  		require.Equal(t, &structs.ConsulGatewayProxy{
   924  			ConnectTimeout:                  pointer.Of(2 * time.Second),
   925  			Config:                          map[string]interface{}{"foo": 1},
   926  			EnvoyGatewayNoDefaultBind:       true,
   927  			EnvoyGatewayBindTaggedAddresses: false,
   928  			EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{
   929  				"service1": {
   930  					Address: "0.0.0.0",
   931  					Port:    3000,
   932  				}},
   933  		}, result)
   934  	})
   935  
   936  	t.Run("ingress leave as-is", func(t *testing.T) {
   937  		result := gatewayProxy(&structs.ConsulGateway{
   938  			Proxy: &structs.ConsulGatewayProxy{
   939  				Config:                          map[string]interface{}{"foo": 1},
   940  				EnvoyGatewayBindTaggedAddresses: true,
   941  			},
   942  			Ingress: &structs.ConsulIngressConfigEntry{
   943  				Listeners: []*structs.ConsulIngressListener{{
   944  					Port:     3000,
   945  					Protocol: "tcp",
   946  					Services: []*structs.ConsulIngressService{{
   947  						Name: "service1",
   948  					}},
   949  				}},
   950  			},
   951  		}, "bridge")
   952  		require.Equal(t, &structs.ConsulGatewayProxy{
   953  			ConnectTimeout:                  nil,
   954  			Config:                          map[string]interface{}{"foo": 1},
   955  			EnvoyGatewayNoDefaultBind:       false,
   956  			EnvoyGatewayBindTaggedAddresses: true,
   957  			EnvoyGatewayBindAddresses:       nil,
   958  		}, result)
   959  	})
   960  
   961  	t.Run("terminating set defaults", func(t *testing.T) {
   962  		result := gatewayProxy(&structs.ConsulGateway{
   963  			Proxy: &structs.ConsulGatewayProxy{
   964  				ConnectTimeout:        pointer.Of(2 * time.Second),
   965  				EnvoyDNSDiscoveryType: "STRICT_DNS",
   966  			},
   967  			Terminating: &structs.ConsulTerminatingConfigEntry{
   968  				Services: []*structs.ConsulLinkedService{{
   969  					Name:     "service1",
   970  					CAFile:   "/cafile.pem",
   971  					CertFile: "/certfile.pem",
   972  					KeyFile:  "/keyfile.pem",
   973  					SNI:      "",
   974  				}},
   975  			},
   976  		}, "bridge")
   977  		require.Equal(t, &structs.ConsulGatewayProxy{
   978  			ConnectTimeout:                  pointer.Of(2 * time.Second),
   979  			EnvoyGatewayNoDefaultBind:       true,
   980  			EnvoyGatewayBindTaggedAddresses: false,
   981  			EnvoyDNSDiscoveryType:           "STRICT_DNS",
   982  			EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{
   983  				"default": {
   984  					Address: "0.0.0.0",
   985  					Port:    -1,
   986  				},
   987  			},
   988  		}, result)
   989  	})
   990  
   991  	t.Run("terminating leave as-is", func(t *testing.T) {
   992  		result := gatewayProxy(&structs.ConsulGateway{
   993  			Proxy: &structs.ConsulGatewayProxy{
   994  				Config:                          map[string]interface{}{"foo": 1},
   995  				EnvoyGatewayBindTaggedAddresses: true,
   996  			},
   997  			Terminating: &structs.ConsulTerminatingConfigEntry{
   998  				Services: []*structs.ConsulLinkedService{{
   999  					Name: "service1",
  1000  				}},
  1001  			},
  1002  		}, "bridge")
  1003  		require.Equal(t, &structs.ConsulGatewayProxy{
  1004  			ConnectTimeout:                  nil,
  1005  			Config:                          map[string]interface{}{"foo": 1},
  1006  			EnvoyGatewayNoDefaultBind:       false,
  1007  			EnvoyGatewayBindTaggedAddresses: true,
  1008  			EnvoyGatewayBindAddresses:       nil,
  1009  		}, result)
  1010  	})
  1011  
  1012  	t.Run("mesh set defaults in bridge", func(t *testing.T) {
  1013  		result := gatewayProxy(&structs.ConsulGateway{
  1014  			Proxy: &structs.ConsulGatewayProxy{
  1015  				ConnectTimeout: pointer.Of(2 * time.Second),
  1016  			},
  1017  			Mesh: &structs.ConsulMeshConfigEntry{
  1018  				// nothing
  1019  			},
  1020  		}, "bridge")
  1021  		require.Equal(t, &structs.ConsulGatewayProxy{
  1022  			ConnectTimeout:                  pointer.Of(2 * time.Second),
  1023  			EnvoyGatewayNoDefaultBind:       true,
  1024  			EnvoyGatewayBindTaggedAddresses: false,
  1025  			EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{
  1026  				"lan": {
  1027  					Address: "0.0.0.0",
  1028  					Port:    -1,
  1029  				},
  1030  				"wan": {
  1031  					Address: "0.0.0.0",
  1032  					Port:    -1,
  1033  				},
  1034  			},
  1035  		}, result)
  1036  	})
  1037  
  1038  	t.Run("mesh set defaults in host", func(t *testing.T) {
  1039  		result := gatewayProxy(&structs.ConsulGateway{
  1040  			Proxy: &structs.ConsulGatewayProxy{
  1041  				ConnectTimeout: pointer.Of(2 * time.Second),
  1042  			},
  1043  			Mesh: &structs.ConsulMeshConfigEntry{
  1044  				// nothing
  1045  			},
  1046  		}, "host")
  1047  		require.Equal(t, &structs.ConsulGatewayProxy{
  1048  			ConnectTimeout: pointer.Of(2 * time.Second),
  1049  		}, result)
  1050  	})
  1051  
  1052  	t.Run("mesh leave as-is", func(t *testing.T) {
  1053  		result := gatewayProxy(&structs.ConsulGateway{
  1054  			Proxy: &structs.ConsulGatewayProxy{
  1055  				Config:                          map[string]interface{}{"foo": 1},
  1056  				EnvoyGatewayBindTaggedAddresses: true,
  1057  			},
  1058  			Mesh: &structs.ConsulMeshConfigEntry{
  1059  				// nothing
  1060  			},
  1061  		}, "bridge")
  1062  		require.Equal(t, &structs.ConsulGatewayProxy{
  1063  			ConnectTimeout:                  nil,
  1064  			Config:                          map[string]interface{}{"foo": 1},
  1065  			EnvoyGatewayNoDefaultBind:       false,
  1066  			EnvoyGatewayBindTaggedAddresses: true,
  1067  			EnvoyGatewayBindAddresses:       nil,
  1068  		}, result)
  1069  	})
  1070  }