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

     1  package structs
     2  
     3  import (
     4  	"crypto/sha1"
     5  	"fmt"
     6  	"hash"
     7  	"io"
     8  	"net/url"
     9  	"reflect"
    10  	"regexp"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/hashicorp/consul/api"
    17  	multierror "github.com/hashicorp/go-multierror"
    18  	"github.com/hashicorp/nomad/helper"
    19  	"github.com/hashicorp/nomad/helper/args"
    20  	"github.com/mitchellh/copystructure"
    21  )
    22  
    23  const (
    24  	EnvoyBootstrapPath = "${NOMAD_SECRETS_DIR}/envoy_bootstrap.json"
    25  
    26  	ServiceCheckHTTP   = "http"
    27  	ServiceCheckTCP    = "tcp"
    28  	ServiceCheckScript = "script"
    29  	ServiceCheckGRPC   = "grpc"
    30  
    31  	// minCheckInterval is the minimum check interval permitted.  Consul
    32  	// currently has its MinInterval set to 1s.  Mirror that here for
    33  	// consistency.
    34  	minCheckInterval = 1 * time.Second
    35  
    36  	// minCheckTimeout is the minimum check timeout permitted for Consul
    37  	// script TTL checks.
    38  	minCheckTimeout = 1 * time.Second
    39  )
    40  
    41  // ServiceCheck represents the Consul health check.
    42  type ServiceCheck struct {
    43  	Name          string              // Name of the check, defaults to id
    44  	Type          string              // Type of the check - tcp, http, docker and script
    45  	Command       string              // Command is the command to run for script checks
    46  	Args          []string            // Args is a list of arguments for script checks
    47  	Path          string              // path of the health check url for http type check
    48  	Protocol      string              // Protocol to use if check is http, defaults to http
    49  	PortLabel     string              // The port to use for tcp/http checks
    50  	Expose        bool                // Whether to have Envoy expose the check path (connect-enabled group-services only)
    51  	AddressMode   string              // 'host' to use host ip:port or 'driver' to use driver's
    52  	Interval      time.Duration       // Interval of the check
    53  	Timeout       time.Duration       // Timeout of the response from the check before consul fails the check
    54  	InitialStatus string              // Initial status of the check
    55  	TLSSkipVerify bool                // Skip TLS verification when Protocol=https
    56  	Method        string              // HTTP Method to use (GET by default)
    57  	Header        map[string][]string // HTTP Headers for Consul to set when making HTTP checks
    58  	CheckRestart  *CheckRestart       // If and when a task should be restarted based on checks
    59  	GRPCService   string              // Service for GRPC checks
    60  	GRPCUseTLS    bool                // Whether or not to use TLS for GRPC checks
    61  	TaskName      string              // What task to execute this check in
    62  }
    63  
    64  // Copy the stanza recursively. Returns nil if nil.
    65  func (sc *ServiceCheck) Copy() *ServiceCheck {
    66  	if sc == nil {
    67  		return nil
    68  	}
    69  	nsc := new(ServiceCheck)
    70  	*nsc = *sc
    71  	nsc.Args = helper.CopySliceString(sc.Args)
    72  	nsc.Header = helper.CopyMapStringSliceString(sc.Header)
    73  	nsc.CheckRestart = sc.CheckRestart.Copy()
    74  	return nsc
    75  }
    76  
    77  // Equals returns true if the structs are recursively equal.
    78  func (sc *ServiceCheck) Equals(o *ServiceCheck) bool {
    79  	if sc == nil || o == nil {
    80  		return sc == o
    81  	}
    82  
    83  	if sc.Name != o.Name {
    84  		return false
    85  	}
    86  
    87  	if sc.AddressMode != o.AddressMode {
    88  		return false
    89  	}
    90  
    91  	if !helper.CompareSliceSetString(sc.Args, o.Args) {
    92  		return false
    93  	}
    94  
    95  	if !sc.CheckRestart.Equals(o.CheckRestart) {
    96  		return false
    97  	}
    98  
    99  	if sc.TaskName != o.TaskName {
   100  		return false
   101  	}
   102  
   103  	if sc.Command != o.Command {
   104  		return false
   105  	}
   106  
   107  	if sc.GRPCService != o.GRPCService {
   108  		return false
   109  	}
   110  
   111  	if sc.GRPCUseTLS != o.GRPCUseTLS {
   112  		return false
   113  	}
   114  
   115  	// Use DeepEqual here as order of slice values could matter
   116  	if !reflect.DeepEqual(sc.Header, o.Header) {
   117  		return false
   118  	}
   119  
   120  	if sc.InitialStatus != o.InitialStatus {
   121  		return false
   122  	}
   123  
   124  	if sc.Interval != o.Interval {
   125  		return false
   126  	}
   127  
   128  	if sc.Method != o.Method {
   129  		return false
   130  	}
   131  
   132  	if sc.Path != o.Path {
   133  		return false
   134  	}
   135  
   136  	if sc.PortLabel != o.Path {
   137  		return false
   138  	}
   139  
   140  	if sc.Expose != o.Expose {
   141  		return false
   142  	}
   143  
   144  	if sc.Protocol != o.Protocol {
   145  		return false
   146  	}
   147  
   148  	if sc.TLSSkipVerify != o.TLSSkipVerify {
   149  		return false
   150  	}
   151  
   152  	if sc.Timeout != o.Timeout {
   153  		return false
   154  	}
   155  
   156  	if sc.Type != o.Type {
   157  		return false
   158  	}
   159  
   160  	return true
   161  }
   162  
   163  func (sc *ServiceCheck) Canonicalize(serviceName string) {
   164  	// Ensure empty maps/slices are treated as null to avoid scheduling
   165  	// issues when using DeepEquals.
   166  	if len(sc.Args) == 0 {
   167  		sc.Args = nil
   168  	}
   169  
   170  	if len(sc.Header) == 0 {
   171  		sc.Header = nil
   172  	} else {
   173  		for k, v := range sc.Header {
   174  			if len(v) == 0 {
   175  				sc.Header[k] = nil
   176  			}
   177  		}
   178  	}
   179  
   180  	if sc.Name == "" {
   181  		sc.Name = fmt.Sprintf("service: %q check", serviceName)
   182  	}
   183  }
   184  
   185  // validate a Service's ServiceCheck
   186  func (sc *ServiceCheck) validate() error {
   187  	// Validate Type
   188  	checkType := strings.ToLower(sc.Type)
   189  	switch checkType {
   190  	case ServiceCheckGRPC:
   191  	case ServiceCheckTCP:
   192  	case ServiceCheckHTTP:
   193  		if sc.Path == "" {
   194  			return fmt.Errorf("http type must have a valid http path")
   195  		}
   196  		checkPath, err := url.Parse(sc.Path)
   197  		if err != nil {
   198  			return fmt.Errorf("http type must have a valid http path")
   199  		}
   200  		if checkPath.IsAbs() {
   201  			return fmt.Errorf("http type must have a relative http path")
   202  		}
   203  
   204  	case ServiceCheckScript:
   205  		if sc.Command == "" {
   206  			return fmt.Errorf("script type must have a valid script path")
   207  		}
   208  
   209  	default:
   210  		return fmt.Errorf(`invalid type (%+q), must be one of "http", "tcp", or "script" type`, sc.Type)
   211  	}
   212  
   213  	// Validate interval and timeout
   214  	if sc.Interval == 0 {
   215  		return fmt.Errorf("missing required value interval. Interval cannot be less than %v", minCheckInterval)
   216  	} else if sc.Interval < minCheckInterval {
   217  		return fmt.Errorf("interval (%v) cannot be lower than %v", sc.Interval, minCheckInterval)
   218  	}
   219  
   220  	if sc.Timeout == 0 {
   221  		return fmt.Errorf("missing required value timeout. Timeout cannot be less than %v", minCheckInterval)
   222  	} else if sc.Timeout < minCheckTimeout {
   223  		return fmt.Errorf("timeout (%v) is lower than required minimum timeout %v", sc.Timeout, minCheckInterval)
   224  	}
   225  
   226  	// Validate InitialStatus
   227  	switch sc.InitialStatus {
   228  	case "":
   229  	case api.HealthPassing:
   230  	case api.HealthWarning:
   231  	case api.HealthCritical:
   232  	default:
   233  		return fmt.Errorf(`invalid initial check state (%s), must be one of %q, %q, %q or empty`, sc.InitialStatus, api.HealthPassing, api.HealthWarning, api.HealthCritical)
   234  
   235  	}
   236  
   237  	// Validate AddressMode
   238  	switch sc.AddressMode {
   239  	case "", AddressModeHost, AddressModeDriver:
   240  		// Ok
   241  	case AddressModeAuto:
   242  		return fmt.Errorf("invalid address_mode %q - %s only valid for services", sc.AddressMode, AddressModeAuto)
   243  	default:
   244  		return fmt.Errorf("invalid address_mode %q", sc.AddressMode)
   245  	}
   246  
   247  	// Note that we cannot completely validate the Expose field yet - we do not
   248  	// know whether this ServiceCheck belongs to a connect-enabled group-service.
   249  	// Instead, such validation will happen in a job admission controller.
   250  	if sc.Expose {
   251  		// We can however immediately ensure expose is configured only for HTTP
   252  		// and gRPC checks.
   253  		switch checkType {
   254  		case ServiceCheckGRPC, ServiceCheckHTTP: // ok
   255  		default:
   256  			return fmt.Errorf("expose may only be set on HTTP or gRPC checks")
   257  		}
   258  	}
   259  
   260  	return sc.CheckRestart.Validate()
   261  }
   262  
   263  // RequiresPort returns whether the service check requires the task has a port.
   264  func (sc *ServiceCheck) RequiresPort() bool {
   265  	switch sc.Type {
   266  	case ServiceCheckGRPC, ServiceCheckHTTP, ServiceCheckTCP:
   267  		return true
   268  	default:
   269  		return false
   270  	}
   271  }
   272  
   273  // TriggersRestarts returns true if this check should be watched and trigger a restart
   274  // on failure.
   275  func (sc *ServiceCheck) TriggersRestarts() bool {
   276  	return sc.CheckRestart != nil && sc.CheckRestart.Limit > 0
   277  }
   278  
   279  // Hash all ServiceCheck fields and the check's corresponding service ID to
   280  // create an identifier. The identifier is not guaranteed to be unique as if
   281  // the PortLabel is blank, the Service's PortLabel will be used after Hash is
   282  // called.
   283  func (sc *ServiceCheck) Hash(serviceID string) string {
   284  	h := sha1.New()
   285  	hashString(h, serviceID)
   286  	hashString(h, sc.Name)
   287  	hashString(h, sc.Type)
   288  	hashString(h, sc.Command)
   289  	hashString(h, strings.Join(sc.Args, ""))
   290  	hashString(h, sc.Path)
   291  	hashString(h, sc.Protocol)
   292  	hashString(h, sc.PortLabel)
   293  	hashString(h, sc.Interval.String())
   294  	hashString(h, sc.Timeout.String())
   295  	hashString(h, sc.Method)
   296  
   297  	// use name "true" to maintain ID stability
   298  	hashBool(h, sc.TLSSkipVerify, "true")
   299  
   300  	// maintain artisanal map hashing to maintain ID stability
   301  	hashHeader(h, sc.Header)
   302  
   303  	// Only include AddressMode if set to maintain ID stability with Nomad <0.7.1
   304  	hashStringIfNonEmpty(h, sc.AddressMode)
   305  
   306  	// Only include gRPC if set to maintain ID stability with Nomad <0.8.4
   307  	hashStringIfNonEmpty(h, sc.GRPCService)
   308  
   309  	// use name "true" to maintain ID stability
   310  	hashBool(h, sc.GRPCUseTLS, "true")
   311  
   312  	// Hash is used for diffing against the Consul check definition, which does
   313  	// not have an expose parameter. Instead we rely on implied changes to
   314  	// other fields if the Expose setting is changed in a nomad service.
   315  	// hashBool(h, sc.Expose, "Expose")
   316  
   317  	// maintain use of hex (i.e. not b32) to maintain ID stability
   318  	return fmt.Sprintf("%x", h.Sum(nil))
   319  }
   320  
   321  func hashStringIfNonEmpty(h hash.Hash, s string) {
   322  	if len(s) > 0 {
   323  		hashString(h, s)
   324  	}
   325  }
   326  
   327  func hashHeader(h hash.Hash, m map[string][]string) {
   328  	// maintain backwards compatibility for ID stability
   329  	// using the %v formatter on a map with string keys produces consistent
   330  	// output, but our existing format here is incompatible
   331  	if len(m) > 0 {
   332  		headers := make([]string, 0, len(m))
   333  		for k, v := range m {
   334  			headers = append(headers, k+strings.Join(v, ""))
   335  		}
   336  		sort.Strings(headers)
   337  		hashString(h, strings.Join(headers, ""))
   338  	}
   339  }
   340  
   341  const (
   342  	AddressModeAuto   = "auto"
   343  	AddressModeHost   = "host"
   344  	AddressModeDriver = "driver"
   345  )
   346  
   347  // Service represents a Consul service definition
   348  type Service struct {
   349  	// Name of the service registered with Consul. Consul defaults the
   350  	// Name to ServiceID if not specified.  The Name if specified is used
   351  	// as one of the seed values when generating a Consul ServiceID.
   352  	Name string
   353  
   354  	// PortLabel is either the numeric port number or the `host:port`.
   355  	// To specify the port number using the host's Consul Advertise
   356  	// address, specify an empty host in the PortLabel (e.g. `:port`).
   357  	PortLabel string
   358  
   359  	// AddressMode specifies whether or not to use the host ip:port for
   360  	// this service.
   361  	AddressMode string
   362  
   363  	// EnableTagOverride will disable Consul's anti-entropy mechanism for the
   364  	// tags of this service. External updates to the service definition via
   365  	// Consul will not be corrected to match the service definition set in the
   366  	// Nomad job specification.
   367  	//
   368  	// https://www.consul.io/docs/agent/services.html#service-definition
   369  	EnableTagOverride bool
   370  
   371  	Tags       []string          // List of tags for the service
   372  	CanaryTags []string          // List of tags for the service when it is a canary
   373  	Checks     []*ServiceCheck   // List of checks associated with the service
   374  	Connect    *ConsulConnect    // Consul Connect configuration
   375  	Meta       map[string]string // Consul service meta
   376  	CanaryMeta map[string]string // Consul service meta when it is a canary
   377  }
   378  
   379  // Copy the stanza recursively. Returns nil if nil.
   380  func (s *Service) Copy() *Service {
   381  	if s == nil {
   382  		return nil
   383  	}
   384  	ns := new(Service)
   385  	*ns = *s
   386  	ns.Tags = helper.CopySliceString(ns.Tags)
   387  	ns.CanaryTags = helper.CopySliceString(ns.CanaryTags)
   388  
   389  	if s.Checks != nil {
   390  		checks := make([]*ServiceCheck, len(ns.Checks))
   391  		for i, c := range ns.Checks {
   392  			checks[i] = c.Copy()
   393  		}
   394  		ns.Checks = checks
   395  	}
   396  
   397  	ns.Connect = s.Connect.Copy()
   398  
   399  	ns.Meta = helper.CopyMapStringString(s.Meta)
   400  	ns.CanaryMeta = helper.CopyMapStringString(s.CanaryMeta)
   401  
   402  	return ns
   403  }
   404  
   405  // Canonicalize interpolates values of Job, Task Group and Task in the Service
   406  // Name. This also generates check names, service id and check ids.
   407  func (s *Service) Canonicalize(job string, taskGroup string, task string) {
   408  	// Ensure empty lists are treated as null to avoid scheduler issues when
   409  	// using DeepEquals
   410  	if len(s.Tags) == 0 {
   411  		s.Tags = nil
   412  	}
   413  	if len(s.CanaryTags) == 0 {
   414  		s.CanaryTags = nil
   415  	}
   416  	if len(s.Checks) == 0 {
   417  		s.Checks = nil
   418  	}
   419  
   420  	s.Name = args.ReplaceEnv(s.Name, map[string]string{
   421  		"JOB":       job,
   422  		"TASKGROUP": taskGroup,
   423  		"TASK":      task,
   424  		"BASE":      fmt.Sprintf("%s-%s-%s", job, taskGroup, task),
   425  	},
   426  	)
   427  
   428  	for _, check := range s.Checks {
   429  		check.Canonicalize(s.Name)
   430  	}
   431  }
   432  
   433  // Validate checks if the Service definition is valid
   434  func (s *Service) Validate() error {
   435  	var mErr multierror.Error
   436  
   437  	// Ensure the service name is valid per the below RFCs but make an exception
   438  	// for our interpolation syntax by first stripping any environment variables from the name
   439  
   440  	serviceNameStripped := args.ReplaceEnvWithPlaceHolder(s.Name, "ENV-VAR")
   441  
   442  	if err := s.ValidateName(serviceNameStripped); err != nil {
   443  		mErr.Errors = append(mErr.Errors, fmt.Errorf("Service name must be valid per RFC 1123 and can contain only alphanumeric characters or dashes: %q", s.Name))
   444  	}
   445  
   446  	switch s.AddressMode {
   447  	case "", AddressModeAuto, AddressModeHost, AddressModeDriver:
   448  		// OK
   449  	default:
   450  		mErr.Errors = append(mErr.Errors, fmt.Errorf("Service address_mode must be %q, %q, or %q; not %q", AddressModeAuto, AddressModeHost, AddressModeDriver, s.AddressMode))
   451  	}
   452  
   453  	for _, c := range s.Checks {
   454  		if s.PortLabel == "" && c.PortLabel == "" && c.RequiresPort() {
   455  			mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s invalid: check requires a port but neither check nor service %+q have a port", c.Name, s.Name))
   456  			continue
   457  		}
   458  
   459  		// TCP checks against a Consul Connect enabled service are not supported
   460  		// due to the service being bound to the loopback interface inside the
   461  		// network namespace
   462  		if c.Type == ServiceCheckTCP && s.Connect != nil && s.Connect.SidecarService != nil {
   463  			mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s invalid: tcp checks are not valid for Connect enabled services", c.Name))
   464  			continue
   465  		}
   466  
   467  		if err := c.validate(); err != nil {
   468  			mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s invalid: %v", c.Name, err))
   469  		}
   470  	}
   471  
   472  	if s.Connect != nil {
   473  		if err := s.Connect.Validate(); err != nil {
   474  			mErr.Errors = append(mErr.Errors, err)
   475  		}
   476  	}
   477  
   478  	return mErr.ErrorOrNil()
   479  }
   480  
   481  // ValidateName checks if the service Name is valid and should be called after
   482  // the name has been interpolated
   483  func (s *Service) ValidateName(name string) error {
   484  	// Ensure the service name is valid per RFC-952 §1
   485  	// (https://tools.ietf.org/html/rfc952), RFC-1123 §2.1
   486  	// (https://tools.ietf.org/html/rfc1123), and RFC-2782
   487  	// (https://tools.ietf.org/html/rfc2782).
   488  	re := regexp.MustCompile(`^(?i:[a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])$`)
   489  	if !re.MatchString(name) {
   490  		return fmt.Errorf("Service name must be valid per RFC 1123 and can contain only alphanumeric characters or dashes and must be no longer than 63 characters: %q", name)
   491  	}
   492  	return nil
   493  }
   494  
   495  // Hash returns a base32 encoded hash of a Service's contents excluding checks
   496  // as they're hashed independently.
   497  func (s *Service) Hash(allocID, taskName string, canary bool) string {
   498  	h := sha1.New()
   499  	hashString(h, allocID)
   500  	hashString(h, taskName)
   501  	hashString(h, s.Name)
   502  	hashString(h, s.PortLabel)
   503  	hashString(h, s.AddressMode)
   504  	hashTags(h, s.Tags)
   505  	hashTags(h, s.CanaryTags)
   506  	hashBool(h, canary, "Canary")
   507  	hashBool(h, s.EnableTagOverride, "ETO")
   508  	hashMeta(h, s.Meta)
   509  	hashMeta(h, s.CanaryMeta)
   510  	hashConnect(h, s.Connect)
   511  
   512  	// Base32 is used for encoding the hash as sha1 hashes can always be
   513  	// encoded without padding, only 4 bytes larger than base64, and saves
   514  	// 8 bytes vs hex. Since these hashes are used in Consul URLs it's nice
   515  	// to have a reasonably compact URL-safe representation.
   516  	return b32.EncodeToString(h.Sum(nil))
   517  }
   518  
   519  func hashConnect(h hash.Hash, connect *ConsulConnect) {
   520  	if connect != nil && connect.SidecarService != nil {
   521  		hashString(h, connect.SidecarService.Port)
   522  		hashTags(h, connect.SidecarService.Tags)
   523  		if p := connect.SidecarService.Proxy; p != nil {
   524  			hashString(h, p.LocalServiceAddress)
   525  			hashString(h, strconv.Itoa(p.LocalServicePort))
   526  			hashConfig(h, p.Config)
   527  			for _, upstream := range p.Upstreams {
   528  				hashString(h, upstream.DestinationName)
   529  				hashString(h, strconv.Itoa(upstream.LocalBindPort))
   530  			}
   531  		}
   532  	}
   533  }
   534  
   535  func hashString(h hash.Hash, s string) {
   536  	_, _ = io.WriteString(h, s)
   537  }
   538  
   539  func hashBool(h hash.Hash, b bool, name string) {
   540  	if b {
   541  		hashString(h, name)
   542  	}
   543  }
   544  
   545  func hashTags(h hash.Hash, tags []string) {
   546  	for _, tag := range tags {
   547  		hashString(h, tag)
   548  	}
   549  }
   550  
   551  func hashMeta(h hash.Hash, m map[string]string) {
   552  	_, _ = fmt.Fprintf(h, "%v", m)
   553  }
   554  
   555  func hashConfig(h hash.Hash, c map[string]interface{}) {
   556  	_, _ = fmt.Fprintf(h, "%v", c)
   557  }
   558  
   559  // Equals returns true if the structs are recursively equal.
   560  func (s *Service) Equals(o *Service) bool {
   561  	if s == nil || o == nil {
   562  		return s == o
   563  	}
   564  
   565  	if s.AddressMode != o.AddressMode {
   566  		return false
   567  	}
   568  
   569  	if !helper.CompareSliceSetString(s.CanaryTags, o.CanaryTags) {
   570  		return false
   571  	}
   572  
   573  	if len(s.Checks) != len(o.Checks) {
   574  		return false
   575  	}
   576  
   577  OUTER:
   578  	for i := range s.Checks {
   579  		for ii := range o.Checks {
   580  			if s.Checks[i].Equals(o.Checks[ii]) {
   581  				// Found match; continue with next check
   582  				continue OUTER
   583  			}
   584  		}
   585  
   586  		// No match
   587  		return false
   588  	}
   589  
   590  	if !s.Connect.Equals(o.Connect) {
   591  		return false
   592  	}
   593  
   594  	if s.Name != o.Name {
   595  		return false
   596  	}
   597  
   598  	if s.PortLabel != o.PortLabel {
   599  		return false
   600  	}
   601  
   602  	if !reflect.DeepEqual(s.Meta, o.Meta) {
   603  		return false
   604  	}
   605  
   606  	if !reflect.DeepEqual(s.CanaryMeta, o.CanaryMeta) {
   607  		return false
   608  	}
   609  
   610  	if !helper.CompareSliceSetString(s.Tags, o.Tags) {
   611  		return false
   612  	}
   613  
   614  	if s.EnableTagOverride != o.EnableTagOverride {
   615  		return false
   616  	}
   617  
   618  	return true
   619  }
   620  
   621  // ConsulConnect represents a Consul Connect jobspec stanza.
   622  type ConsulConnect struct {
   623  	// Native is true if a service implements Connect directly and does not
   624  	// need a sidecar.
   625  	Native bool
   626  
   627  	// SidecarService is non-nil if a service requires a sidecar.
   628  	SidecarService *ConsulSidecarService
   629  
   630  	// SidecarTask is non-nil if sidecar overrides are set
   631  	SidecarTask *SidecarTask
   632  }
   633  
   634  // Copy the stanza recursively. Returns nil if nil.
   635  func (c *ConsulConnect) Copy() *ConsulConnect {
   636  	if c == nil {
   637  		return nil
   638  	}
   639  
   640  	return &ConsulConnect{
   641  		Native:         c.Native,
   642  		SidecarService: c.SidecarService.Copy(),
   643  		SidecarTask:    c.SidecarTask.Copy(),
   644  	}
   645  }
   646  
   647  // Equals returns true if the structs are recursively equal.
   648  func (c *ConsulConnect) Equals(o *ConsulConnect) bool {
   649  	if c == nil || o == nil {
   650  		return c == o
   651  	}
   652  
   653  	if c.Native != o.Native {
   654  		return false
   655  	}
   656  
   657  	return c.SidecarService.Equals(o.SidecarService)
   658  }
   659  
   660  // HasSidecar checks if a sidecar task is needed
   661  func (c *ConsulConnect) HasSidecar() bool {
   662  	return c != nil && c.SidecarService != nil
   663  }
   664  
   665  // Validate that the Connect stanza has exactly one of Native or sidecar.
   666  func (c *ConsulConnect) Validate() error {
   667  	if c == nil {
   668  		return nil
   669  	}
   670  
   671  	if c.Native && c.SidecarService != nil {
   672  		return fmt.Errorf("Consul Connect must be native or use a sidecar service; not both")
   673  	}
   674  
   675  	if !c.Native && c.SidecarService == nil {
   676  		return fmt.Errorf("Consul Connect must be native or use a sidecar service")
   677  	}
   678  
   679  	return nil
   680  }
   681  
   682  // ConsulSidecarService represents a Consul Connect SidecarService jobspec
   683  // stanza.
   684  type ConsulSidecarService struct {
   685  	// Tags are optional service tags that get registered with the sidecar service
   686  	// in Consul. If unset, the sidecar service inherits the parent service tags.
   687  	Tags []string
   688  
   689  	// Port is the service's port that the sidecar will connect to. May be
   690  	// a port label or a literal port number.
   691  	Port string
   692  
   693  	// Proxy stanza defining the sidecar proxy configuration.
   694  	Proxy *ConsulProxy
   695  }
   696  
   697  // HasUpstreams checks if the sidecar service has any upstreams configured
   698  func (s *ConsulSidecarService) HasUpstreams() bool {
   699  	return s != nil && s.Proxy != nil && len(s.Proxy.Upstreams) > 0
   700  }
   701  
   702  // Copy the stanza recursively. Returns nil if nil.
   703  func (s *ConsulSidecarService) Copy() *ConsulSidecarService {
   704  	if s == nil {
   705  		return nil
   706  	}
   707  	return &ConsulSidecarService{
   708  		Tags:  helper.CopySliceString(s.Tags),
   709  		Port:  s.Port,
   710  		Proxy: s.Proxy.Copy(),
   711  	}
   712  }
   713  
   714  // Equals returns true if the structs are recursively equal.
   715  func (s *ConsulSidecarService) Equals(o *ConsulSidecarService) bool {
   716  	if s == nil || o == nil {
   717  		return s == o
   718  	}
   719  
   720  	if s.Port != o.Port {
   721  		return false
   722  	}
   723  
   724  	if !helper.CompareSliceSetString(s.Tags, o.Tags) {
   725  		return false
   726  	}
   727  
   728  	return s.Proxy.Equals(o.Proxy)
   729  }
   730  
   731  // SidecarTask represents a subset of Task fields that are able to be overridden
   732  // from the sidecar_task stanza
   733  type SidecarTask struct {
   734  	// Name of the task
   735  	Name string
   736  
   737  	// Driver is used to control which driver is used
   738  	Driver string
   739  
   740  	// User is used to determine which user will run the task. It defaults to
   741  	// the same user the Nomad client is being run as.
   742  	User string
   743  
   744  	// Config is provided to the driver to initialize
   745  	Config map[string]interface{}
   746  
   747  	// Map of environment variables to be used by the driver
   748  	Env map[string]string
   749  
   750  	// Resources is the resources needed by this task
   751  	Resources *Resources
   752  
   753  	// Meta is used to associate arbitrary metadata with this
   754  	// task. This is opaque to Nomad.
   755  	Meta map[string]string
   756  
   757  	// KillTimeout is the time between signaling a task that it will be
   758  	// killed and killing it.
   759  	KillTimeout *time.Duration
   760  
   761  	// LogConfig provides configuration for log rotation
   762  	LogConfig *LogConfig
   763  
   764  	// ShutdownDelay is the duration of the delay between deregistering a
   765  	// task from Consul and sending it a signal to shutdown. See #2441
   766  	ShutdownDelay *time.Duration
   767  
   768  	// KillSignal is the kill signal to use for the task. This is an optional
   769  	// specification and defaults to SIGINT
   770  	KillSignal string
   771  }
   772  
   773  func (t *SidecarTask) Copy() *SidecarTask {
   774  	if t == nil {
   775  		return nil
   776  	}
   777  	nt := new(SidecarTask)
   778  	*nt = *t
   779  	nt.Env = helper.CopyMapStringString(nt.Env)
   780  
   781  	nt.Resources = nt.Resources.Copy()
   782  	nt.LogConfig = nt.LogConfig.Copy()
   783  	nt.Meta = helper.CopyMapStringString(nt.Meta)
   784  
   785  	if i, err := copystructure.Copy(nt.Config); err != nil {
   786  		panic(err.Error())
   787  	} else {
   788  		nt.Config = i.(map[string]interface{})
   789  	}
   790  
   791  	if t.KillTimeout != nil {
   792  		nt.KillTimeout = helper.TimeToPtr(*t.KillTimeout)
   793  	}
   794  
   795  	if t.ShutdownDelay != nil {
   796  		nt.ShutdownDelay = helper.TimeToPtr(*t.ShutdownDelay)
   797  	}
   798  
   799  	return nt
   800  }
   801  
   802  // MergeIntoTask merges the SidecarTask fields over the given task
   803  func (t *SidecarTask) MergeIntoTask(task *Task) {
   804  	if t.Name != "" {
   805  		task.Name = t.Name
   806  	}
   807  
   808  	// If the driver changes then the driver config can be overwritten.
   809  	// Otherwise we'll merge the driver config together
   810  	if t.Driver != "" && t.Driver != task.Driver {
   811  		task.Driver = t.Driver
   812  		task.Config = t.Config
   813  	} else {
   814  		for k, v := range t.Config {
   815  			task.Config[k] = v
   816  		}
   817  	}
   818  
   819  	if t.User != "" {
   820  		task.User = t.User
   821  	}
   822  
   823  	if t.Env != nil {
   824  		if task.Env == nil {
   825  			task.Env = t.Env
   826  		} else {
   827  			for k, v := range t.Env {
   828  				task.Env[k] = v
   829  			}
   830  		}
   831  	}
   832  
   833  	if t.Resources != nil {
   834  		task.Resources.Merge(t.Resources)
   835  	}
   836  
   837  	if t.Meta != nil {
   838  		if task.Meta == nil {
   839  			task.Meta = t.Meta
   840  		} else {
   841  			for k, v := range t.Meta {
   842  				task.Meta[k] = v
   843  			}
   844  		}
   845  	}
   846  
   847  	if t.KillTimeout != nil {
   848  		task.KillTimeout = *t.KillTimeout
   849  	}
   850  
   851  	if t.LogConfig != nil {
   852  		if task.LogConfig == nil {
   853  			task.LogConfig = t.LogConfig
   854  		} else {
   855  			if t.LogConfig.MaxFiles > 0 {
   856  				task.LogConfig.MaxFiles = t.LogConfig.MaxFiles
   857  			}
   858  			if t.LogConfig.MaxFileSizeMB > 0 {
   859  				task.LogConfig.MaxFileSizeMB = t.LogConfig.MaxFileSizeMB
   860  			}
   861  		}
   862  	}
   863  
   864  	if t.ShutdownDelay != nil {
   865  		task.ShutdownDelay = *t.ShutdownDelay
   866  	}
   867  
   868  	if t.KillSignal != "" {
   869  		task.KillSignal = t.KillSignal
   870  	}
   871  }
   872  
   873  // ConsulProxy represents a Consul Connect sidecar proxy jobspec stanza.
   874  type ConsulProxy struct {
   875  
   876  	// LocalServiceAddress is the address the local service binds to.
   877  	// Usually 127.0.0.1 it is useful to customize in clusters with mixed
   878  	// Connect and non-Connect services.
   879  	LocalServiceAddress string
   880  
   881  	// LocalServicePort is the port the local service binds to. Usually
   882  	// the same as the parent service's port, it is useful to customize
   883  	// in clusters with mixed Connect and non-Connect services
   884  	LocalServicePort int
   885  
   886  	// Upstreams configures the upstream services this service intends to
   887  	// connect to.
   888  	Upstreams []ConsulUpstream
   889  
   890  	// Expose configures the consul proxy.expose stanza to "open up" endpoints
   891  	// used by task-group level service checks using HTTP or gRPC protocols.
   892  	//
   893  	// Use json tag to match with field name in api/
   894  	Expose *ConsulExposeConfig `json:"ExposeConfig"`
   895  
   896  	// Config is a proxy configuration. It is opaque to Nomad and passed
   897  	// directly to Consul.
   898  	Config map[string]interface{}
   899  }
   900  
   901  // Copy the stanza recursively. Returns nil if nil.
   902  func (p *ConsulProxy) Copy() *ConsulProxy {
   903  	if p == nil {
   904  		return nil
   905  	}
   906  
   907  	newP := &ConsulProxy{
   908  		LocalServiceAddress: p.LocalServiceAddress,
   909  		LocalServicePort:    p.LocalServicePort,
   910  		Expose:              p.Expose.Copy(),
   911  	}
   912  
   913  	if n := len(p.Upstreams); n > 0 {
   914  		newP.Upstreams = make([]ConsulUpstream, n)
   915  
   916  		for i := range p.Upstreams {
   917  			newP.Upstreams[i] = *p.Upstreams[i].Copy()
   918  		}
   919  	}
   920  
   921  	if n := len(p.Config); n > 0 {
   922  		newP.Config = make(map[string]interface{}, n)
   923  
   924  		for k, v := range p.Config {
   925  			newP.Config[k] = v
   926  		}
   927  	}
   928  
   929  	return newP
   930  }
   931  
   932  // Equals returns true if the structs are recursively equal.
   933  func (p *ConsulProxy) Equals(o *ConsulProxy) bool {
   934  	if p == nil || o == nil {
   935  		return p == o
   936  	}
   937  
   938  	if p.LocalServiceAddress != o.LocalServiceAddress {
   939  		return false
   940  	}
   941  
   942  	if p.LocalServicePort != o.LocalServicePort {
   943  		return false
   944  	}
   945  
   946  	if !p.Expose.Equals(o.Expose) {
   947  		return false
   948  	}
   949  
   950  	if !upstreamsEquals(p.Upstreams, o.Upstreams) {
   951  		return false
   952  	}
   953  
   954  	// Avoid nil vs {} differences
   955  	if len(p.Config) != 0 && len(o.Config) != 0 {
   956  		if !reflect.DeepEqual(p.Config, o.Config) {
   957  			return false
   958  		}
   959  	}
   960  
   961  	return true
   962  }
   963  
   964  // ConsulUpstream represents a Consul Connect upstream jobspec stanza.
   965  type ConsulUpstream struct {
   966  	// DestinationName is the name of the upstream service.
   967  	DestinationName string
   968  
   969  	// LocalBindPort is the port the proxy will receive connections for the
   970  	// upstream on.
   971  	LocalBindPort int
   972  }
   973  
   974  func upstreamsEquals(a, b []ConsulUpstream) bool {
   975  	if len(a) != len(b) {
   976  		return false
   977  	}
   978  
   979  LOOP: // order does not matter
   980  	for _, upA := range a {
   981  		for _, upB := range b {
   982  			if upA.Equals(&upB) {
   983  				continue LOOP
   984  			}
   985  		}
   986  		return false
   987  	}
   988  	return true
   989  }
   990  
   991  // Copy the stanza recursively. Returns nil if u is nil.
   992  func (u *ConsulUpstream) Copy() *ConsulUpstream {
   993  	if u == nil {
   994  		return nil
   995  	}
   996  
   997  	return &ConsulUpstream{
   998  		DestinationName: u.DestinationName,
   999  		LocalBindPort:   u.LocalBindPort,
  1000  	}
  1001  }
  1002  
  1003  // Equals returns true if the structs are recursively equal.
  1004  func (u *ConsulUpstream) Equals(o *ConsulUpstream) bool {
  1005  	if u == nil || o == nil {
  1006  		return u == o
  1007  	}
  1008  
  1009  	return (*u) == (*o)
  1010  }
  1011  
  1012  // ExposeConfig represents a Consul Connect expose jobspec stanza.
  1013  type ConsulExposeConfig struct {
  1014  	// Use json tag to match with field name in api/
  1015  	Paths []ConsulExposePath `json:"Path"`
  1016  }
  1017  
  1018  type ConsulExposePath struct {
  1019  	Path          string
  1020  	Protocol      string
  1021  	LocalPathPort int
  1022  	ListenerPort  string
  1023  }
  1024  
  1025  func exposePathsEqual(pathsA, pathsB []ConsulExposePath) bool {
  1026  	if len(pathsA) != len(pathsB) {
  1027  		return false
  1028  	}
  1029  
  1030  LOOP: // order does not matter
  1031  	for _, pathA := range pathsA {
  1032  		for _, pathB := range pathsB {
  1033  			if pathA == pathB {
  1034  				continue LOOP
  1035  			}
  1036  		}
  1037  		return false
  1038  	}
  1039  	return true
  1040  }
  1041  
  1042  // Copy the stanza. Returns nil if e is nil.
  1043  func (e *ConsulExposeConfig) Copy() *ConsulExposeConfig {
  1044  	if e == nil {
  1045  		return nil
  1046  	}
  1047  	paths := make([]ConsulExposePath, len(e.Paths))
  1048  	for i := 0; i < len(e.Paths); i++ {
  1049  		paths[i] = e.Paths[i]
  1050  	}
  1051  	return &ConsulExposeConfig{
  1052  		Paths: paths,
  1053  	}
  1054  }
  1055  
  1056  // Equals returns true if the structs are recursively equal.
  1057  func (e *ConsulExposeConfig) Equals(o *ConsulExposeConfig) bool {
  1058  	if e == nil || o == nil {
  1059  		return e == o
  1060  	}
  1061  	return exposePathsEqual(e.Paths, o.Paths)
  1062  }