2019-08-15 15:22:37 +00:00
|
|
|
package nomad
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
"github.com/hashicorp/nomad/helper"
|
2019-08-15 15:22:37 +00:00
|
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2020-07-08 15:19:36 +00:00
|
|
|
"github.com/pkg/errors"
|
2019-08-15 15:22:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2019-08-20 05:22:46 +00:00
|
|
|
// connectSidecarResources returns the set of resources used by default for
|
|
|
|
// the Consul Connect sidecar task
|
|
|
|
connectSidecarResources = func() *structs.Resources {
|
|
|
|
return &structs.Resources{
|
|
|
|
CPU: 250,
|
|
|
|
MemoryMB: 128,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
// connectSidecarDriverConfig is the driver configuration used by the injected
|
|
|
|
// connect proxy sidecar task.
|
|
|
|
connectSidecarDriverConfig = func() map[string]interface{} {
|
2020-07-02 07:48:09 +00:00
|
|
|
return map[string]interface{}{
|
2020-09-04 17:50:11 +00:00
|
|
|
"image": structs.EnvoyImageFormat,
|
2020-07-02 07:48:09 +00:00
|
|
|
"args": []interface{}{
|
|
|
|
"-c", structs.EnvoyBootstrapPath,
|
|
|
|
"-l", "${meta.connect.log_level}",
|
|
|
|
"--disable-hot-restart",
|
|
|
|
},
|
|
|
|
}
|
2019-08-15 15:22:37 +00:00
|
|
|
}
|
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
// connectGatewayDriverConfig is the Docker driver configuration used by the
|
|
|
|
// injected connect proxy sidecar task.
|
|
|
|
//
|
|
|
|
// A gateway may run in a group with bridge or host networking, and if host
|
|
|
|
// networking is being used the network_mode driver configuration is set here.
|
|
|
|
connectGatewayDriverConfig = func(hostNetwork bool) map[string]interface{} {
|
|
|
|
m := map[string]interface{}{
|
2020-09-04 17:50:11 +00:00
|
|
|
"image": structs.EnvoyImageFormat,
|
2020-07-28 20:12:08 +00:00
|
|
|
"args": []interface{}{
|
|
|
|
"-c", structs.EnvoyBootstrapPath,
|
|
|
|
"-l", "${meta.connect.log_level}",
|
|
|
|
"--disable-hot-restart",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if hostNetwork {
|
|
|
|
m["network_mode"] = "host"
|
|
|
|
}
|
|
|
|
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
|
|
|
// connectMinimalVersionConstraint is used when building the sidecar task to ensure
|
2020-03-07 03:15:22 +00:00
|
|
|
// the proper Consul version is used that supports the necessary Connect
|
|
|
|
// features. This includes bootstrapping envoy with a unix socket for Consul's
|
|
|
|
// gRPC xDS API.
|
2020-07-28 20:12:08 +00:00
|
|
|
connectMinimalVersionConstraint = func() *structs.Constraint {
|
2019-08-20 05:22:46 +00:00
|
|
|
return &structs.Constraint{
|
|
|
|
LTarget: "${attr.consul.version}",
|
2019-11-13 23:36:15 +00:00
|
|
|
RTarget: ">= 1.6.0-beta1",
|
|
|
|
Operand: structs.ConstraintSemver,
|
2019-08-20 05:22:46 +00:00
|
|
|
}
|
2019-08-15 15:22:37 +00:00
|
|
|
}
|
2020-07-28 20:12:08 +00:00
|
|
|
|
|
|
|
// connectGatewayVersionConstraint is used when building a connect gateway
|
|
|
|
// task to ensure proper Consul version is used that supports Connect Gateway
|
|
|
|
// features. This includes making use of Consul Configuration Entries of type
|
|
|
|
// {ingress,terminating,mesh}-gateway.
|
|
|
|
connectGatewayVersionConstraint = func() *structs.Constraint {
|
|
|
|
return &structs.Constraint{
|
|
|
|
LTarget: "${attr.consul.version}",
|
|
|
|
RTarget: ">= 1.8.0",
|
|
|
|
Operand: structs.ConstraintSemver,
|
|
|
|
}
|
|
|
|
}
|
2019-08-15 15:22:37 +00:00
|
|
|
)
|
|
|
|
|
2019-12-06 20:46:46 +00:00
|
|
|
// jobConnectHook implements a job Mutating and Validating admission controller
|
2019-08-15 15:22:37 +00:00
|
|
|
type jobConnectHook struct{}
|
|
|
|
|
|
|
|
func (jobConnectHook) Name() string {
|
|
|
|
return "connect"
|
|
|
|
}
|
|
|
|
|
|
|
|
func (jobConnectHook) Mutate(job *structs.Job) (_ *structs.Job, warnings []error, err error) {
|
|
|
|
for _, g := range job.TaskGroups {
|
2019-10-28 14:46:51 +00:00
|
|
|
// TG isn't validated yet, but validation
|
|
|
|
// may depend on mutation results.
|
|
|
|
// Do basic validation here and skip mutation,
|
|
|
|
// so Validate can return a meaningful error
|
|
|
|
// messages
|
|
|
|
if len(g.Networks) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-12-12 23:46:14 +00:00
|
|
|
if err := groupConnectHook(job, g); err != nil {
|
2019-08-15 15:22:37 +00:00
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return job, nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (jobConnectHook) Validate(job *structs.Job) (warnings []error, err error) {
|
|
|
|
for _, g := range job.TaskGroups {
|
|
|
|
w, err := groupConnectValidate(g)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if w != nil {
|
|
|
|
warnings = append(warnings, w...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return warnings, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getSidecarTaskForService looks for the sidecar task for a given service within
|
|
|
|
// a task group. If no sidecar task is found nil is returned
|
|
|
|
func getSidecarTaskForService(tg *structs.TaskGroup, svc string) *structs.Task {
|
|
|
|
for _, t := range tg.Tasks {
|
|
|
|
if isSidecarForService(t, svc) {
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func isSidecarForService(t *structs.Task, svc string) bool {
|
2020-07-28 20:12:08 +00:00
|
|
|
return t.Kind == structs.NewTaskKind(structs.ConnectProxyPrefix, svc)
|
|
|
|
}
|
|
|
|
|
|
|
|
func hasGatewayTaskForService(tg *structs.TaskGroup, svc string) bool {
|
|
|
|
for _, t := range tg.Tasks {
|
|
|
|
switch {
|
|
|
|
case isIngressGatewayForService(t, svc):
|
|
|
|
// also terminating and mesh in the future
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func isIngressGatewayForService(t *structs.Task, svc string) bool {
|
|
|
|
return t.Kind == structs.NewTaskKind(structs.ConnectIngressPrefix, svc)
|
2019-08-15 15:22:37 +00:00
|
|
|
}
|
|
|
|
|
2020-07-08 15:19:36 +00:00
|
|
|
// getNamedTaskForNativeService retrieves the Task with the name specified in the
|
|
|
|
// group service definition. If the task name is empty and there is only one task
|
|
|
|
// in the group, infer the name from the only option.
|
|
|
|
func getNamedTaskForNativeService(tg *structs.TaskGroup, serviceName, taskName string) (*structs.Task, error) {
|
|
|
|
if taskName == "" {
|
|
|
|
if len(tg.Tasks) == 1 {
|
|
|
|
return tg.Tasks[0], nil
|
|
|
|
}
|
|
|
|
return nil, errors.Errorf("task for Consul Connect Native service %s->%s is ambiguous and must be set", tg.Name, serviceName)
|
|
|
|
}
|
|
|
|
|
2020-05-13 20:15:55 +00:00
|
|
|
for _, t := range tg.Tasks {
|
|
|
|
if t.Name == taskName {
|
2020-07-08 15:19:36 +00:00
|
|
|
return t, nil
|
2020-05-13 20:15:55 +00:00
|
|
|
}
|
|
|
|
}
|
2020-07-08 15:19:36 +00:00
|
|
|
return nil, errors.Errorf("task %s named by Consul Connect Native service %s->%s does not exist", taskName, tg.Name, serviceName)
|
2020-05-13 20:15:55 +00:00
|
|
|
}
|
|
|
|
|
2020-03-07 03:15:22 +00:00
|
|
|
// probably need to hack this up to look for checks on the service, and if they
|
|
|
|
// qualify, configure a port for envoy to use to expose their paths.
|
2019-12-12 23:46:14 +00:00
|
|
|
func groupConnectHook(job *structs.Job, g *structs.TaskGroup) error {
|
2019-08-15 15:22:37 +00:00
|
|
|
for _, service := range g.Services {
|
2020-07-28 20:12:08 +00:00
|
|
|
switch {
|
|
|
|
// mutate depending on what the connect block is being used for
|
|
|
|
|
|
|
|
case service.Connect.HasSidecar():
|
2019-08-15 15:22:37 +00:00
|
|
|
// Check to see if the sidecar task already exists
|
|
|
|
task := getSidecarTaskForService(g, service.Name)
|
|
|
|
|
|
|
|
// If the task doesn't already exist, create a new one and add it to the job
|
|
|
|
if task == nil {
|
2019-12-12 23:46:14 +00:00
|
|
|
task = newConnectTask(service.Name)
|
2019-08-15 15:22:37 +00:00
|
|
|
|
|
|
|
// If there happens to be a task defined with the same name
|
|
|
|
// append an UUID fragment to the task name
|
|
|
|
for _, t := range g.Tasks {
|
|
|
|
if t.Name == task.Name {
|
|
|
|
task.Name = task.Name + "-" + uuid.Generate()[:6]
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
g.Tasks = append(g.Tasks, task)
|
|
|
|
}
|
|
|
|
|
2019-08-20 05:22:46 +00:00
|
|
|
if service.Connect.SidecarTask != nil {
|
|
|
|
service.Connect.SidecarTask.MergeIntoTask(task)
|
|
|
|
}
|
2019-08-15 15:22:37 +00:00
|
|
|
|
2019-12-12 23:46:14 +00:00
|
|
|
// Canonicalize task since this mutator runs after job canonicalization
|
|
|
|
task.Canonicalize(job, g)
|
|
|
|
|
2020-03-07 03:15:22 +00:00
|
|
|
makePort := func(label string) {
|
|
|
|
// check that port hasn't already been defined before adding it to tg
|
|
|
|
for _, p := range g.Networks[0].DynamicPorts {
|
|
|
|
if p.Label == label {
|
|
|
|
return
|
|
|
|
}
|
2019-08-15 15:22:37 +00:00
|
|
|
}
|
2020-03-07 03:15:22 +00:00
|
|
|
g.Networks[0].DynamicPorts = append(g.Networks[0].DynamicPorts, structs.Port{
|
|
|
|
Label: label,
|
|
|
|
// -1 is a sentinel value to instruct the
|
|
|
|
// scheduler to map the host's dynamic port to
|
|
|
|
// the same port in the netns.
|
|
|
|
To: -1,
|
|
|
|
})
|
2019-08-15 15:22:37 +00:00
|
|
|
}
|
2020-03-07 03:15:22 +00:00
|
|
|
|
|
|
|
// create a port for the sidecar task's proxy port
|
|
|
|
makePort(fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, service.Name))
|
2020-07-28 20:12:08 +00:00
|
|
|
|
|
|
|
case service.Connect.IsNative():
|
2020-07-08 15:19:36 +00:00
|
|
|
// find the task backing this connect native service and set the kind
|
2020-06-22 17:55:59 +00:00
|
|
|
nativeTaskName := service.TaskName
|
2020-07-08 15:19:36 +00:00
|
|
|
if t, err := getNamedTaskForNativeService(g, service.Name, nativeTaskName); err != nil {
|
|
|
|
return err
|
2020-05-13 20:15:55 +00:00
|
|
|
} else {
|
2020-07-08 15:19:36 +00:00
|
|
|
t.Kind = structs.NewTaskKind(structs.ConnectNativePrefix, service.Name)
|
|
|
|
service.TaskName = t.Name // in case the task was inferred
|
2020-05-13 20:15:55 +00:00
|
|
|
}
|
2020-07-28 20:12:08 +00:00
|
|
|
|
|
|
|
case service.Connect.IsGateway():
|
|
|
|
netHost := g.Networks[0].Mode == "host"
|
|
|
|
if !netHost && service.Connect.Gateway.Ingress != nil {
|
|
|
|
// Modify the gateway proxy service configuration to automatically
|
|
|
|
// do the correct envoy bind address plumbing when inside a net
|
|
|
|
// namespace, but only if things are not explicitly configured.
|
|
|
|
service.Connect.Gateway.Proxy = gatewayProxyForBridge(service.Connect.Gateway)
|
|
|
|
}
|
|
|
|
|
|
|
|
// inject the gateway task only if it does not yet already exist
|
|
|
|
if !hasGatewayTaskForService(g, service.Name) {
|
|
|
|
// use the default envoy image, for now there is no support for a custom task
|
|
|
|
task := newConnectGatewayTask(service.Name, netHost)
|
|
|
|
g.Tasks = append(g.Tasks, task)
|
|
|
|
task.Canonicalize(job, g)
|
|
|
|
}
|
2019-08-15 15:22:37 +00:00
|
|
|
}
|
|
|
|
}
|
2020-07-10 14:59:51 +00:00
|
|
|
|
|
|
|
// re-canonicalize group network since this hook runs after canonicalizaton
|
|
|
|
g.Networks[0].Canonicalize()
|
2019-08-15 15:22:37 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-28 20:12:08 +00:00
|
|
|
// gatewayProxyIsDefault returns false if any of these gateway proxy configuration
|
|
|
|
// have been modified from their default values, indicating the operator wants
|
|
|
|
// custom behavior. Otherwise, we assume the operator wants Nomad to do the Right
|
|
|
|
// Thing, setting the configuration automatically.
|
|
|
|
//
|
|
|
|
// - envoy_gateway_no_default_bind
|
|
|
|
// - envoy_gateway_bind_tagged_addresses
|
|
|
|
// - envoy_gateway_bind_addresses
|
|
|
|
func gatewayProxyIsDefault(proxy *structs.ConsulGatewayProxy) bool {
|
|
|
|
if proxy == nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if !proxy.EnvoyGatewayNoDefaultBind &&
|
|
|
|
!proxy.EnvoyGatewayBindTaggedAddresses &&
|
|
|
|
len(proxy.EnvoyGatewayBindAddresses) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// gatewayProxyForBridge scans an existing gateway proxy configuration and tweaks
|
|
|
|
// it given an associated configuration entry so that it works as intended from
|
|
|
|
// inside a network namespace.
|
|
|
|
func gatewayProxyForBridge(gateway *structs.ConsulGateway) *structs.ConsulGatewayProxy {
|
|
|
|
if gateway == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// operator has supplied custom proxy configuration, just use that without
|
|
|
|
// modification
|
|
|
|
if !gatewayProxyIsDefault(gateway.Proxy) {
|
|
|
|
return gateway.Proxy
|
|
|
|
}
|
|
|
|
|
|
|
|
// copy over unrelated fields if proxy block exists
|
|
|
|
proxy := new(structs.ConsulGatewayProxy)
|
|
|
|
if gateway.Proxy != nil {
|
|
|
|
proxy.ConnectTimeout = gateway.Proxy.ConnectTimeout
|
|
|
|
proxy.Config = gateway.Proxy.Config
|
|
|
|
}
|
|
|
|
|
|
|
|
// magically set the fields where Nomad knows what to do
|
|
|
|
proxy.EnvoyGatewayNoDefaultBind = true
|
|
|
|
proxy.EnvoyGatewayBindTaggedAddresses = false
|
|
|
|
proxy.EnvoyGatewayBindAddresses = gatewayBindAddresses(gateway.Ingress)
|
|
|
|
|
|
|
|
return proxy
|
|
|
|
}
|
|
|
|
|
|
|
|
func gatewayBindAddresses(ingress *structs.ConsulIngressConfigEntry) map[string]*structs.ConsulGatewayBindAddress {
|
|
|
|
if ingress == nil || len(ingress.Listeners) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
addresses := make(map[string]*structs.ConsulGatewayBindAddress)
|
|
|
|
for _, listener := range ingress.Listeners {
|
|
|
|
port := listener.Port
|
|
|
|
for _, service := range listener.Services {
|
|
|
|
addresses[service.Name] = &structs.ConsulGatewayBindAddress{
|
|
|
|
Address: "0.0.0.0",
|
|
|
|
Port: port,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return addresses
|
|
|
|
}
|
|
|
|
|
|
|
|
func newConnectGatewayTask(serviceName string, netHost bool) *structs.Task {
|
|
|
|
return &structs.Task{
|
|
|
|
// Name is used in container name so must start with '[A-Za-z0-9]'
|
|
|
|
Name: fmt.Sprintf("%s-%s", structs.ConnectIngressPrefix, serviceName),
|
|
|
|
Kind: structs.NewTaskKind(structs.ConnectIngressPrefix, serviceName),
|
|
|
|
Driver: "docker",
|
|
|
|
Config: connectGatewayDriverConfig(netHost),
|
|
|
|
ShutdownDelay: 5 * time.Second,
|
|
|
|
LogConfig: &structs.LogConfig{
|
|
|
|
MaxFiles: 2,
|
|
|
|
MaxFileSizeMB: 2,
|
|
|
|
},
|
|
|
|
Resources: connectSidecarResources(),
|
|
|
|
Constraints: structs.Constraints{
|
|
|
|
connectGatewayVersionConstraint(),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-12 23:46:14 +00:00
|
|
|
func newConnectTask(serviceName string) *structs.Task {
|
2020-07-28 20:12:08 +00:00
|
|
|
return &structs.Task{
|
2019-08-15 15:22:37 +00:00
|
|
|
// Name is used in container name so must start with '[A-Za-z0-9]'
|
2019-12-12 23:46:14 +00:00
|
|
|
Name: fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, serviceName),
|
2020-05-18 19:21:12 +00:00
|
|
|
Kind: structs.NewTaskKind(structs.ConnectProxyPrefix, serviceName),
|
2019-08-20 05:22:46 +00:00
|
|
|
Driver: "docker",
|
2020-07-28 20:12:08 +00:00
|
|
|
Config: connectSidecarDriverConfig(),
|
2019-08-15 15:22:37 +00:00
|
|
|
ShutdownDelay: 5 * time.Second,
|
|
|
|
LogConfig: &structs.LogConfig{
|
|
|
|
MaxFiles: 2,
|
|
|
|
MaxFileSizeMB: 2,
|
|
|
|
},
|
2019-08-20 05:22:46 +00:00
|
|
|
Resources: connectSidecarResources(),
|
2020-04-02 01:18:41 +00:00
|
|
|
Lifecycle: &structs.TaskLifecycleConfig{
|
2020-04-02 01:23:47 +00:00
|
|
|
Hook: structs.TaskLifecycleHookPrestart,
|
2020-04-02 01:18:41 +00:00
|
|
|
Sidecar: true,
|
|
|
|
},
|
2019-08-15 15:22:37 +00:00
|
|
|
Constraints: structs.Constraints{
|
2020-07-28 20:12:08 +00:00
|
|
|
connectMinimalVersionConstraint(),
|
2019-08-15 15:22:37 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func groupConnectValidate(g *structs.TaskGroup) (warnings []error, err error) {
|
|
|
|
for _, s := range g.Services {
|
2020-07-28 20:12:08 +00:00
|
|
|
switch {
|
|
|
|
case s.Connect.HasSidecar():
|
2020-06-22 17:55:59 +00:00
|
|
|
if err := groupConnectSidecarValidate(g); err != nil {
|
|
|
|
return nil, err
|
2019-08-15 15:22:37 +00:00
|
|
|
}
|
2020-07-28 20:12:08 +00:00
|
|
|
case s.Connect.IsNative():
|
2020-06-22 17:55:59 +00:00
|
|
|
if err := groupConnectNativeValidate(g, s); err != nil {
|
|
|
|
return nil, err
|
2019-08-15 15:22:37 +00:00
|
|
|
}
|
2020-07-28 20:12:08 +00:00
|
|
|
case s.Connect.IsGateway():
|
|
|
|
if err := groupConnectGatewayValidate(g); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-08-15 15:22:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, nil
|
|
|
|
}
|
2020-06-22 17:55:59 +00:00
|
|
|
|
|
|
|
func groupConnectSidecarValidate(g *structs.TaskGroup) error {
|
|
|
|
if n := len(g.Networks); n != 1 {
|
|
|
|
return fmt.Errorf("Consul Connect sidecars require exactly 1 network, found %d in group %q", n, g.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
if g.Networks[0].Mode != "bridge" {
|
|
|
|
return fmt.Errorf("Consul Connect sidecar requires bridge network, found %q in group %q", g.Networks[0].Mode, g.Name)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func groupConnectNativeValidate(g *structs.TaskGroup, s *structs.Service) error {
|
|
|
|
// note that network mode is not enforced for connect native services
|
|
|
|
|
2020-07-08 15:19:36 +00:00
|
|
|
if _, err := getNamedTaskForNativeService(g, s.Name, s.TaskName); err != nil {
|
|
|
|
return err
|
2020-06-22 17:55:59 +00:00
|
|
|
}
|
2020-07-08 15:19:36 +00:00
|
|
|
return nil
|
2020-06-22 17:55:59 +00:00
|
|
|
}
|
2020-07-28 20:12:08 +00:00
|
|
|
|
|
|
|
func groupConnectGatewayValidate(g *structs.TaskGroup) error {
|
|
|
|
// the group needs to be either bridge or host mode so we know how to configure
|
|
|
|
// the docker driver config
|
|
|
|
|
|
|
|
if n := len(g.Networks); n != 1 {
|
|
|
|
return fmt.Errorf("Consul Connect gateways require exactly 1 network, found %d in group %q", n, g.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
modes := []string{"bridge", "host"}
|
|
|
|
if !helper.SliceStringContains(modes, g.Networks[0].Mode) {
|
|
|
|
return fmt.Errorf(`Consul Connect Gateway service requires Task Group with network mode of type "bridge" or "host"`)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|