github.com/uchennaokeke444/nomad@v0.11.8/nomad/job_endpoint_hook_expose_check.go (about)

     1  package nomad
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  
     8  	"github.com/hashicorp/nomad/helper/uuid"
     9  	"github.com/hashicorp/nomad/nomad/structs"
    10  	"github.com/pkg/errors"
    11  )
    12  
    13  type jobExposeCheckHook struct{}
    14  
    15  func (jobExposeCheckHook) Name() string {
    16  	return "expose-check"
    17  }
    18  
    19  // Mutate will scan every task group for group-services which have checks defined
    20  // that have the Expose field configured, and generate expose path configurations
    21  // extrapolated from those check definitions.
    22  func (jobExposeCheckHook) Mutate(job *structs.Job) (_ *structs.Job, warnings []error, err error) {
    23  	for _, tg := range job.TaskGroups {
    24  		for _, s := range tg.Services {
    25  			for _, c := range s.Checks {
    26  				if c.Expose {
    27  					if exposePath, err := exposePathForCheck(tg, s, c); err != nil {
    28  						return nil, nil, err
    29  					} else if exposePath != nil {
    30  						serviceExposeConfig := serviceExposeConfig(s)
    31  						// insert only if not already present - required for job
    32  						// updates which would otherwise create duplicates
    33  						if !containsExposePath(serviceExposeConfig.Paths, *exposePath) {
    34  							serviceExposeConfig.Paths = append(
    35  								serviceExposeConfig.Paths, *exposePath,
    36  							)
    37  						}
    38  					}
    39  				}
    40  			}
    41  		}
    42  	}
    43  	return job, nil, nil
    44  }
    45  
    46  // Validate will ensure:
    47  // - The job contains valid network configuration for each task group in which
    48  //	an expose path is configured. The network must be of type bridge mode.
    49  // - The check Expose field is configured only for connect-enabled group-services.
    50  func (jobExposeCheckHook) Validate(job *structs.Job) (warnings []error, err error) {
    51  	for _, tg := range job.TaskGroups {
    52  		// Make sure any group that contains a group-service that enables expose
    53  		// is configured with one network that is in "bridge" mode. This check
    54  		// is being done independently of the preceding Connect task injection
    55  		// hook, because at some point in the future Connect will not require the
    56  		// use of network namespaces, whereas the use of "expose" does not make
    57  		// sense without the use of network namespace.
    58  		if err := tgValidateUseOfBridgeMode(tg); err != nil {
    59  			return nil, err
    60  		}
    61  		// Make sure any group-service that contains a check that enables expose
    62  		// is connect-enabled and does not specify a custom sidecar task. We only
    63  		// support the expose feature when using the built-in Envoy integration.
    64  		if err := tgValidateUseOfCheckExpose(tg); err != nil {
    65  			return nil, err
    66  		}
    67  	}
    68  	return nil, nil
    69  }
    70  
    71  // serviceExposeConfig digs through s to extract the connect sidecar service proxy
    72  // expose configuration. It is not required of the user to provide this, so it
    73  // is created on demand here as needed in the case where any service check exposes
    74  // itself.
    75  //
    76  // The service, connect, and sidecar_service are assumed not to be nil, as they
    77  // are enforced in previous hooks / validation.
    78  func serviceExposeConfig(s *structs.Service) *structs.ConsulExposeConfig {
    79  	if s.Connect.SidecarService.Proxy == nil {
    80  		s.Connect.SidecarService.Proxy = new(structs.ConsulProxy)
    81  	}
    82  	if s.Connect.SidecarService.Proxy.Expose == nil {
    83  		s.Connect.SidecarService.Proxy.Expose = new(structs.ConsulExposeConfig)
    84  	}
    85  	return s.Connect.SidecarService.Proxy.Expose
    86  }
    87  
    88  // containsExposePath returns true if path is contained in paths.
    89  func containsExposePath(paths []structs.ConsulExposePath, path structs.ConsulExposePath) bool {
    90  	for _, p := range paths {
    91  		if p == path {
    92  			return true
    93  		}
    94  	}
    95  	return false
    96  }
    97  
    98  // tgValidateUseOfCheckExpose ensures that any service check in tg making use
    99  // of the expose field is within an appropriate context to do so. The check must
   100  // be a group level check, and must use the builtin envoy proxy.
   101  func tgValidateUseOfCheckExpose(tg *structs.TaskGroup) error {
   102  	// validation for group services (which must use built-in connect proxy)
   103  	for _, s := range tg.Services {
   104  		for _, check := range s.Checks {
   105  			if check.Expose && !serviceUsesConnectEnvoy(s) {
   106  				return errors.Errorf(
   107  					"exposed service check %s->%s->%s requires use of Nomad's builtin Connect proxy",
   108  					tg.Name, s.Name, check.Name,
   109  				)
   110  			}
   111  		}
   112  	}
   113  
   114  	// validation for task services (which must not be configured to use Expose)
   115  	for _, t := range tg.Tasks {
   116  		for _, s := range t.Services {
   117  			for _, check := range s.Checks {
   118  				if check.Expose {
   119  					return errors.Errorf(
   120  						"exposed service check %s[%s]->%s->%s is not a task-group service",
   121  						tg.Name, t.Name, s.Name, check.Name,
   122  					)
   123  				}
   124  			}
   125  		}
   126  	}
   127  	return nil
   128  }
   129  
   130  // tgValidateUseOfBridgeMode ensures there is exactly 1 network configured for
   131  // the task group, and that it makes use of "bridge" mode (i.e. enables network
   132  // namespaces).
   133  func tgValidateUseOfBridgeMode(tg *structs.TaskGroup) error {
   134  	if tgUsesExposeCheck(tg) {
   135  		if len(tg.Networks) != 1 {
   136  			return errors.Errorf("group %q must specify one bridge network for exposing service check(s)", tg.Name)
   137  		}
   138  		if tg.Networks[0].Mode != "bridge" {
   139  			return errors.Errorf("group %q must use bridge network for exposing service check(s)", tg.Name)
   140  		}
   141  	}
   142  	return nil
   143  }
   144  
   145  // tgUsesExposeCheck returns true if any group service in the task group makes
   146  // use of the expose field.
   147  func tgUsesExposeCheck(tg *structs.TaskGroup) bool {
   148  	for _, s := range tg.Services {
   149  		for _, check := range s.Checks {
   150  			if check.Expose {
   151  				return true
   152  			}
   153  		}
   154  	}
   155  	return false
   156  }
   157  
   158  // serviceUsesConnectEnvoy returns true if the service is going to end up using
   159  // the built-in envoy proxy.
   160  //
   161  // This implementation is kind of reading tea leaves - firstly Connect
   162  // must be enabled, and second the sidecar_task must not be overridden. If these
   163  // conditions are met, the preceding connect hook will have injected a Connect
   164  // sidecar task, the configuration of which is interpolated at runtime.
   165  func serviceUsesConnectEnvoy(s *structs.Service) bool {
   166  	// A non-nil connect stanza implies this service isn't connect enabled in
   167  	// the first place.
   168  	if s.Connect == nil {
   169  		return false
   170  	}
   171  
   172  	// A non-nil connect.sidecar_task stanza implies the sidecar task is being
   173  	// overridden (i.e. the default Envoy is not being uesd).
   174  	if s.Connect.SidecarTask != nil {
   175  		return false
   176  	}
   177  
   178  	return true
   179  }
   180  
   181  // checkIsExposable returns true if check is qualified for automatic generation
   182  // of connect proxy expose path configuration based on configured consul checks.
   183  // To qualify, the check must be of type "http" or "grpc", and must have a Path
   184  // configured.
   185  func checkIsExposable(check *structs.ServiceCheck) bool {
   186  	switch strings.ToLower(check.Type) {
   187  	case "grpc", "http":
   188  		return strings.HasPrefix(check.Path, "/")
   189  	default:
   190  		return false
   191  	}
   192  }
   193  
   194  // exposePathForCheck extrapolates the necessary expose path configuration for
   195  // the given consul service check. If the check is not compatible, nil is
   196  // returned.
   197  func exposePathForCheck(tg *structs.TaskGroup, s *structs.Service, check *structs.ServiceCheck) (*structs.ConsulExposePath, error) {
   198  	if !checkIsExposable(check) {
   199  		return nil, nil
   200  	}
   201  
   202  	// If the check is exposable but doesn't have a port label set build
   203  	// a port with a generated label, add it to the group's Dynamic ports
   204  	// and set the check port label to the generated label.
   205  	//
   206  	// This lets PortLabel be optional for any exposed check.
   207  	if check.PortLabel == "" {
   208  		port := structs.Port{
   209  			Label: fmt.Sprintf("svc_%s_ck_%s", s.Name, uuid.Generate()[:6]),
   210  			To:    -1,
   211  		}
   212  
   213  		tg.Networks[0].DynamicPorts = append(tg.Networks[0].DynamicPorts, port)
   214  		check.PortLabel = port.Label
   215  	}
   216  
   217  	// Determine the local service port (i.e. what port the service is actually
   218  	// listening to inside the network namespace).
   219  	//
   220  	// Similar logic exists in getAddress of client.go which is used for
   221  	// creating check & service registration objects.
   222  	//
   223  	// The difference here is the address is predestined to be localhost since
   224  	// it is binding inside the namespace.
   225  	var port int
   226  	if _, port = tg.Networks.Port(s.PortLabel); port <= 0 { // try looking up by port label
   227  		if port, _ = strconv.Atoi(s.PortLabel); port <= 0 { // then try direct port value
   228  			return nil, errors.Errorf(
   229  				"unable to determine local service port for service check %s->%s->%s",
   230  				tg.Name, s.Name, check.Name,
   231  			)
   232  		}
   233  	}
   234  
   235  	// The Path, Protocol, and PortLabel are just copied over from the service
   236  	// check definition.
   237  	return &structs.ConsulExposePath{
   238  		Path:          check.Path,
   239  		Protocol:      check.Protocol,
   240  		LocalPathPort: port,
   241  		ListenerPort:  check.PortLabel,
   242  	}, nil
   243  }