github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/nomad/structs/services.go (about)

     1  package structs
     2  
     3  import (
     4  	"crypto/sha1"
     5  	"encoding/binary"
     6  	"errors"
     7  	"fmt"
     8  	"hash"
     9  	"io"
    10  	"net/url"
    11  	"reflect"
    12  	"regexp"
    13  	"sort"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/hashicorp/consul/api"
    19  	"github.com/hashicorp/go-multierror"
    20  	"github.com/hashicorp/go-set"
    21  	"github.com/hashicorp/nomad/helper"
    22  	"github.com/hashicorp/nomad/helper/args"
    23  	"github.com/hashicorp/nomad/helper/pointer"
    24  	"github.com/mitchellh/copystructure"
    25  	"golang.org/x/exp/maps"
    26  	"golang.org/x/exp/slices"
    27  )
    28  
    29  const (
    30  	EnvoyBootstrapPath = "${NOMAD_SECRETS_DIR}/envoy_bootstrap.json"
    31  
    32  	ServiceCheckHTTP   = "http"
    33  	ServiceCheckTCP    = "tcp"
    34  	ServiceCheckScript = "script"
    35  	ServiceCheckGRPC   = "grpc"
    36  
    37  	OnUpdateRequireHealthy = "require_healthy"
    38  	OnUpdateIgnoreWarn     = "ignore_warnings"
    39  	OnUpdateIgnore         = "ignore"
    40  
    41  	// minCheckInterval is the minimum check interval permitted.  Consul
    42  	// currently has its MinInterval set to 1s.  Mirror that here for
    43  	// consistency.
    44  	minCheckInterval = 1 * time.Second
    45  
    46  	// minCheckTimeout is the minimum check timeout permitted for Consul
    47  	// script TTL checks.
    48  	minCheckTimeout = 1 * time.Second
    49  )
    50  
    51  // ServiceCheck represents a Nomad or Consul service health check.
    52  //
    53  // The fields available depend on the service provider the check is being
    54  // registered into.
    55  type ServiceCheck struct {
    56  	Name                   string              // Name of the check, defaults to a generated label
    57  	Type                   string              // Type of the check - tcp, http, docker and script
    58  	Command                string              // Command is the command to run for script checks
    59  	Args                   []string            // Args is a list of arguments for script checks
    60  	Path                   string              // path of the health check url for http type check
    61  	Protocol               string              // Protocol to use if check is http, defaults to http
    62  	PortLabel              string              // The port to use for tcp/http checks
    63  	Expose                 bool                // Whether to have Envoy expose the check path (connect-enabled group-services only)
    64  	AddressMode            string              // Must be empty, "alloc", "host", or "driver"
    65  	Interval               time.Duration       // Interval of the check
    66  	Timeout                time.Duration       // Timeout of the response from the check before consul fails the check
    67  	InitialStatus          string              // Initial status of the check
    68  	TLSSkipVerify          bool                // Skip TLS verification when Protocol=https
    69  	Method                 string              // HTTP Method to use (GET by default)
    70  	Header                 map[string][]string // HTTP Headers for Consul to set when making HTTP checks
    71  	CheckRestart           *CheckRestart       // If and when a task should be restarted based on checks
    72  	GRPCService            string              // Service for GRPC checks
    73  	GRPCUseTLS             bool                // Whether or not to use TLS for GRPC checks
    74  	TaskName               string              // What task to execute this check in
    75  	SuccessBeforePassing   int                 // Number of consecutive successes required before considered healthy
    76  	FailuresBeforeCritical int                 // Number of consecutive failures required before considered unhealthy
    77  	Body                   string              // Body to use in HTTP check
    78  	OnUpdate               string
    79  }
    80  
    81  // IsReadiness returns whether the configuration of the ServiceCheck is effectively
    82  // a readiness check - i.e. check failures do not affect a deployment.
    83  func (sc *ServiceCheck) IsReadiness() bool {
    84  	return sc != nil && sc.OnUpdate == OnUpdateIgnore
    85  }
    86  
    87  // Copy the stanza recursively. Returns nil if nil.
    88  func (sc *ServiceCheck) Copy() *ServiceCheck {
    89  	if sc == nil {
    90  		return nil
    91  	}
    92  	nsc := new(ServiceCheck)
    93  	*nsc = *sc
    94  	nsc.Args = slices.Clone(sc.Args)
    95  	nsc.Header = helper.CopyMapOfSlice(sc.Header)
    96  	nsc.CheckRestart = sc.CheckRestart.Copy()
    97  	return nsc
    98  }
    99  
   100  // Equal returns true if the structs are recursively equal.
   101  func (sc *ServiceCheck) Equal(o *ServiceCheck) bool {
   102  	if sc == nil || o == nil {
   103  		return sc == o
   104  	}
   105  
   106  	if sc.Name != o.Name {
   107  		return false
   108  	}
   109  
   110  	if sc.AddressMode != o.AddressMode {
   111  		return false
   112  	}
   113  
   114  	if !helper.SliceSetEq(sc.Args, o.Args) {
   115  		return false
   116  	}
   117  
   118  	if !sc.CheckRestart.Equal(o.CheckRestart) {
   119  		return false
   120  	}
   121  
   122  	if sc.TaskName != o.TaskName {
   123  		return false
   124  	}
   125  
   126  	if sc.SuccessBeforePassing != o.SuccessBeforePassing {
   127  		return false
   128  	}
   129  
   130  	if sc.FailuresBeforeCritical != o.FailuresBeforeCritical {
   131  		return false
   132  	}
   133  
   134  	if sc.Command != o.Command {
   135  		return false
   136  	}
   137  
   138  	if sc.GRPCService != o.GRPCService {
   139  		return false
   140  	}
   141  
   142  	if sc.GRPCUseTLS != o.GRPCUseTLS {
   143  		return false
   144  	}
   145  
   146  	// Use DeepEqual here as order of slice values could matter
   147  	if !reflect.DeepEqual(sc.Header, o.Header) {
   148  		return false
   149  	}
   150  
   151  	if sc.InitialStatus != o.InitialStatus {
   152  		return false
   153  	}
   154  
   155  	if sc.Interval != o.Interval {
   156  		return false
   157  	}
   158  
   159  	if sc.Method != o.Method {
   160  		return false
   161  	}
   162  
   163  	if sc.Path != o.Path {
   164  		return false
   165  	}
   166  
   167  	if sc.PortLabel != o.Path {
   168  		return false
   169  	}
   170  
   171  	if sc.Expose != o.Expose {
   172  		return false
   173  	}
   174  
   175  	if sc.Protocol != o.Protocol {
   176  		return false
   177  	}
   178  
   179  	if sc.TLSSkipVerify != o.TLSSkipVerify {
   180  		return false
   181  	}
   182  
   183  	if sc.Timeout != o.Timeout {
   184  		return false
   185  	}
   186  
   187  	if sc.Type != o.Type {
   188  		return false
   189  	}
   190  
   191  	if sc.Body != o.Body {
   192  		return false
   193  	}
   194  
   195  	if sc.OnUpdate != o.OnUpdate {
   196  		return false
   197  	}
   198  
   199  	return true
   200  }
   201  
   202  func (sc *ServiceCheck) Canonicalize(serviceName, taskName string) {
   203  	// Ensure empty maps/slices are treated as null to avoid scheduling
   204  	// issues when using DeepEquals.
   205  	if len(sc.Args) == 0 {
   206  		sc.Args = nil
   207  	}
   208  
   209  	// Ensure empty slices are nil
   210  	if len(sc.Header) == 0 {
   211  		sc.Header = nil
   212  	} else {
   213  		for k, v := range sc.Header {
   214  			if len(v) == 0 {
   215  				sc.Header[k] = nil
   216  			}
   217  		}
   218  	}
   219  
   220  	// Ensure a default name for the check
   221  	if sc.Name == "" {
   222  		sc.Name = fmt.Sprintf("service: %q check", serviceName)
   223  	}
   224  
   225  	// Set task name if not already set
   226  	if sc.TaskName == "" && taskName != "group" {
   227  		sc.TaskName = taskName
   228  	}
   229  
   230  	// Ensure OnUpdate defaults to require_healthy (i.e. healthiness check)
   231  	if sc.OnUpdate == "" {
   232  		sc.OnUpdate = OnUpdateRequireHealthy
   233  	}
   234  }
   235  
   236  // validateCommon validates the parts of ServiceCheck shared across providers.
   237  func (sc *ServiceCheck) validateCommon(allowableTypes []string) error {
   238  	// validate the type is allowable (different between nomad, consul checks)
   239  	checkType := strings.ToLower(sc.Type)
   240  	if !slices.Contains(allowableTypes, checkType) {
   241  		s := strings.Join(allowableTypes, ", ")
   242  		return fmt.Errorf(`invalid check type (%q), must be one of %s`, checkType, s)
   243  	}
   244  
   245  	// validate specific check types
   246  	switch checkType {
   247  	case ServiceCheckHTTP:
   248  		if sc.Path == "" {
   249  			return fmt.Errorf("http type must have http path")
   250  		}
   251  		checkPath, pathErr := url.Parse(sc.Path)
   252  		if pathErr != nil {
   253  			return fmt.Errorf("http type must have valid http path")
   254  		}
   255  		if checkPath.IsAbs() {
   256  			return fmt.Errorf("http type must have relative http path")
   257  		}
   258  	case ServiceCheckScript:
   259  		if sc.Command == "" {
   260  			return fmt.Errorf("script type must have a valid script path")
   261  		}
   262  	}
   263  
   264  	// validate interval
   265  	if sc.Interval == 0 {
   266  		return fmt.Errorf("missing required value interval. Interval cannot be less than %v", minCheckInterval)
   267  	} else if sc.Interval < minCheckInterval {
   268  		return fmt.Errorf("interval (%v) cannot be lower than %v", sc.Interval, minCheckInterval)
   269  	}
   270  
   271  	// validate timeout
   272  	if sc.Timeout == 0 {
   273  		return fmt.Errorf("missing required value timeout. Timeout cannot be less than %v", minCheckInterval)
   274  	} else if sc.Timeout < minCheckTimeout {
   275  		return fmt.Errorf("timeout (%v) is lower than required minimum timeout %v", sc.Timeout, minCheckInterval)
   276  	}
   277  
   278  	// validate the initial status
   279  	switch sc.InitialStatus {
   280  	case "":
   281  	case api.HealthPassing:
   282  	case api.HealthWarning:
   283  	case api.HealthCritical:
   284  	default:
   285  		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)
   286  	}
   287  
   288  	// validate address_mode
   289  	switch sc.AddressMode {
   290  	case "", AddressModeHost, AddressModeDriver, AddressModeAlloc:
   291  		// Ok
   292  	case AddressModeAuto:
   293  		return fmt.Errorf("invalid address_mode %q - %s only valid for services", sc.AddressMode, AddressModeAuto)
   294  	default:
   295  		return fmt.Errorf("invalid address_mode %q", sc.AddressMode)
   296  	}
   297  
   298  	// validate on_update
   299  	switch sc.OnUpdate {
   300  	case "", OnUpdateIgnore, OnUpdateRequireHealthy, OnUpdateIgnoreWarn:
   301  		// OK
   302  	default:
   303  		return fmt.Errorf("on_update must be %q, %q, or %q; got %q", OnUpdateRequireHealthy, OnUpdateIgnoreWarn, OnUpdateIgnore, sc.OnUpdate)
   304  	}
   305  
   306  	// validate check_restart and on_update do not conflict
   307  	if sc.CheckRestart != nil {
   308  		// CheckRestart and OnUpdate Ignore are incompatible If OnUpdate treats
   309  		// an error has healthy, and the deployment succeeds followed by check
   310  		// restart restarting failing checks, the deployment is left in an odd
   311  		// state
   312  		if sc.OnUpdate == OnUpdateIgnore {
   313  			return fmt.Errorf("on_update value %q is not compatible with check_restart", sc.OnUpdate)
   314  		}
   315  		// CheckRestart IgnoreWarnings must be true if a check has defined OnUpdate
   316  		// ignore_warnings
   317  		if !sc.CheckRestart.IgnoreWarnings && sc.OnUpdate == OnUpdateIgnoreWarn {
   318  			return fmt.Errorf("on_update value %q not supported with check_restart ignore_warnings value %q", sc.OnUpdate, strconv.FormatBool(sc.CheckRestart.IgnoreWarnings))
   319  		}
   320  	}
   321  
   322  	// validate check_restart
   323  	if err := sc.CheckRestart.Validate(); err != nil {
   324  		return err
   325  	}
   326  
   327  	return nil
   328  }
   329  
   330  // validate a Service's ServiceCheck in the context of the Nomad provider.
   331  func (sc *ServiceCheck) validateNomad() error {
   332  	allowable := []string{ServiceCheckTCP, ServiceCheckHTTP}
   333  	if err := sc.validateCommon(allowable); err != nil {
   334  		return err
   335  	}
   336  
   337  	// expose is connect (consul) specific
   338  	if sc.Expose {
   339  		return fmt.Errorf("expose may only be set for Consul service checks")
   340  	}
   341  
   342  	// nomad checks do not have warnings
   343  	if sc.OnUpdate == OnUpdateIgnoreWarn {
   344  		return fmt.Errorf("on_update may only be set to ignore_warnings for Consul service checks")
   345  	}
   346  
   347  	// below are temporary limitations on checks in nomad
   348  	// https://github.com/hashicorp/team-nomad/issues/354
   349  
   350  	// check_restart.ignore_warnings is not a thing in Nomad (which has no warnings in checks)
   351  	if sc.CheckRestart != nil {
   352  		if sc.CheckRestart.IgnoreWarnings {
   353  			return fmt.Errorf("ignore_warnings on check_restart only supported for Consul service checks")
   354  		}
   355  	}
   356  
   357  	// address_mode="driver" not yet supported on nomad
   358  	if sc.AddressMode == "driver" {
   359  		return fmt.Errorf("address_mode = driver may only be set for Consul service checks")
   360  	}
   361  
   362  	if sc.Type == "http" {
   363  		if sc.Method != "" && !helper.IsMethodHTTP(sc.Method) {
   364  			return fmt.Errorf("method type %q not supported in Nomad http check", sc.Method)
   365  		}
   366  	}
   367  
   368  	// success_before_passing is consul only
   369  	if sc.SuccessBeforePassing != 0 {
   370  		return fmt.Errorf("success_before_passing may only be set for Consul service checks")
   371  	}
   372  
   373  	// failures_before_critical is consul only
   374  	if sc.FailuresBeforeCritical != 0 {
   375  		return fmt.Errorf("failures_before_critical may only be set for Consul service checks")
   376  	}
   377  
   378  	return nil
   379  }
   380  
   381  // validate a Service's ServiceCheck in the context of the Consul provider.
   382  func (sc *ServiceCheck) validateConsul() error {
   383  	allowable := []string{ServiceCheckGRPC, ServiceCheckTCP, ServiceCheckHTTP, ServiceCheckScript}
   384  	if err := sc.validateCommon(allowable); err != nil {
   385  		return err
   386  	}
   387  
   388  	checkType := strings.ToLower(sc.Type)
   389  
   390  	// Note that we cannot completely validate the Expose field yet - we do not
   391  	// know whether this ServiceCheck belongs to a connect-enabled group-service.
   392  	// Instead, such validation will happen in a job admission controller.
   393  	//
   394  	// Consul only.
   395  	if sc.Expose {
   396  		// We can however immediately ensure expose is configured only for HTTP
   397  		// and gRPC checks.
   398  		switch checkType {
   399  		case ServiceCheckGRPC, ServiceCheckHTTP: // ok
   400  		default:
   401  			return fmt.Errorf("expose may only be set on HTTP or gRPC checks")
   402  		}
   403  	}
   404  
   405  	// passFailCheckTypes are intersection of check types supported by both Consul
   406  	// and Nomad when using the pass/fail check threshold features.
   407  	//
   408  	// Consul only.
   409  	passFailCheckTypes := []string{"tcp", "http", "grpc"}
   410  
   411  	if sc.SuccessBeforePassing < 0 {
   412  		return fmt.Errorf("success_before_passing must be non-negative")
   413  	} else if sc.SuccessBeforePassing > 0 && !slices.Contains(passFailCheckTypes, sc.Type) {
   414  		return fmt.Errorf("success_before_passing not supported for check of type %q", sc.Type)
   415  	}
   416  
   417  	if sc.FailuresBeforeCritical < 0 {
   418  		return fmt.Errorf("failures_before_critical must be non-negative")
   419  	} else if sc.FailuresBeforeCritical > 0 && !slices.Contains(passFailCheckTypes, sc.Type) {
   420  		return fmt.Errorf("failures_before_critical not supported for check of type %q", sc.Type)
   421  	}
   422  
   423  	return nil
   424  }
   425  
   426  // RequiresPort returns whether the service check requires the task has a port.
   427  func (sc *ServiceCheck) RequiresPort() bool {
   428  	switch sc.Type {
   429  	case ServiceCheckGRPC, ServiceCheckHTTP, ServiceCheckTCP:
   430  		return true
   431  	default:
   432  		return false
   433  	}
   434  }
   435  
   436  // TriggersRestarts returns true if this check should be watched and trigger a restart
   437  // on failure.
   438  func (sc *ServiceCheck) TriggersRestarts() bool {
   439  	return sc.CheckRestart != nil && sc.CheckRestart.Limit > 0
   440  }
   441  
   442  // Hash all ServiceCheck fields and the check's corresponding service ID to
   443  // create an identifier. The identifier is not guaranteed to be unique as if
   444  // the PortLabel is blank, the Service's PortLabel will be used after Hash is
   445  // called.
   446  func (sc *ServiceCheck) Hash(serviceID string) string {
   447  	h := sha1.New()
   448  	hashString(h, serviceID)
   449  	hashString(h, sc.Name)
   450  	hashString(h, sc.Type)
   451  	hashString(h, sc.Command)
   452  	hashString(h, strings.Join(sc.Args, ""))
   453  	hashString(h, sc.Path)
   454  	hashString(h, sc.Protocol)
   455  	hashString(h, sc.PortLabel)
   456  	hashString(h, sc.Interval.String())
   457  	hashString(h, sc.Timeout.String())
   458  	hashString(h, sc.Method)
   459  	hashString(h, sc.Body)
   460  	hashString(h, sc.OnUpdate)
   461  
   462  	// use name "true" to maintain ID stability
   463  	hashBool(h, sc.TLSSkipVerify, "true")
   464  
   465  	// maintain artisanal map hashing to maintain ID stability
   466  	hashHeader(h, sc.Header)
   467  
   468  	// Only include AddressMode if set to maintain ID stability with Nomad <0.7.1
   469  	hashStringIfNonEmpty(h, sc.AddressMode)
   470  
   471  	// Only include gRPC if set to maintain ID stability with Nomad <0.8.4
   472  	hashStringIfNonEmpty(h, sc.GRPCService)
   473  
   474  	// use name "true" to maintain ID stability
   475  	hashBool(h, sc.GRPCUseTLS, "true")
   476  
   477  	// Only include pass/fail if non-zero to maintain ID stability with Nomad < 0.12
   478  	hashIntIfNonZero(h, "success", sc.SuccessBeforePassing)
   479  	hashIntIfNonZero(h, "failures", sc.FailuresBeforeCritical)
   480  
   481  	// Hash is used for diffing against the Consul check definition, which does
   482  	// not have an expose parameter. Instead we rely on implied changes to
   483  	// other fields if the Expose setting is changed in a nomad service.
   484  	// hashBool(h, sc.Expose, "Expose")
   485  
   486  	// maintain use of hex (i.e. not b32) to maintain ID stability
   487  	return fmt.Sprintf("%x", h.Sum(nil))
   488  }
   489  
   490  func hashStringIfNonEmpty(h hash.Hash, s string) {
   491  	if len(s) > 0 {
   492  		hashString(h, s)
   493  	}
   494  }
   495  
   496  func hashIntIfNonZero(h hash.Hash, name string, i int) {
   497  	if i != 0 {
   498  		hashString(h, fmt.Sprintf("%s:%d", name, i))
   499  	}
   500  }
   501  
   502  func hashDuration(h hash.Hash, dur time.Duration) {
   503  	_ = binary.Write(h, binary.LittleEndian, dur)
   504  }
   505  
   506  func hashHeader(h hash.Hash, m map[string][]string) {
   507  	// maintain backwards compatibility for ID stability
   508  	// using the %v formatter on a map with string keys produces consistent
   509  	// output, but our existing format here is incompatible
   510  	if len(m) > 0 {
   511  		headers := make([]string, 0, len(m))
   512  		for k, v := range m {
   513  			headers = append(headers, k+strings.Join(v, ""))
   514  		}
   515  		sort.Strings(headers)
   516  		hashString(h, strings.Join(headers, ""))
   517  	}
   518  }
   519  
   520  const (
   521  	AddressModeAuto   = "auto"
   522  	AddressModeHost   = "host"
   523  	AddressModeDriver = "driver"
   524  	AddressModeAlloc  = "alloc"
   525  
   526  	// ServiceProviderConsul is the default service provider and the way Nomad
   527  	// worked before native service discovery.
   528  	ServiceProviderConsul = "consul"
   529  
   530  	// ServiceProviderNomad is the native service discovery provider. At the
   531  	// time of writing, there are a number of restrictions around its
   532  	// functionality and use.
   533  	ServiceProviderNomad = "nomad"
   534  )
   535  
   536  // Service represents a Consul service definition
   537  type Service struct {
   538  	// Name of the service registered with Consul. Consul defaults the
   539  	// Name to ServiceID if not specified.  The Name if specified is used
   540  	// as one of the seed values when generating a Consul ServiceID.
   541  	Name string
   542  
   543  	// Name of the Task associated with this service.
   544  	// Group services do not have a task name, unless they are a connect native
   545  	// service specifying the task implementing the service.
   546  	// Task-level services automatically have the task name plumbed through
   547  	// down to checks for convenience.
   548  	TaskName string
   549  
   550  	// PortLabel is either the numeric port number or the `host:port`.
   551  	// To specify the port number using the host's Consul Advertise
   552  	// address, specify an empty host in the PortLabel (e.g. `:port`).
   553  	PortLabel string
   554  
   555  	// AddressMode specifies how the address in service registration is
   556  	// determined. Must be "auto" (default), "host", "driver", or "alloc".
   557  	AddressMode string
   558  
   559  	// Address enables explicitly setting a custom address to use in service
   560  	// registration. AddressMode must be "auto" if Address is set.
   561  	Address string
   562  
   563  	// EnableTagOverride will disable Consul's anti-entropy mechanism for the
   564  	// tags of this service. External updates to the service definition via
   565  	// Consul will not be corrected to match the service definition set in the
   566  	// Nomad job specification.
   567  	//
   568  	// https://www.consul.io/docs/agent/services.html#service-definition
   569  	EnableTagOverride bool
   570  
   571  	Tags       []string          // List of tags for the service
   572  	CanaryTags []string          // List of tags for the service when it is a canary
   573  	Checks     []*ServiceCheck   // List of checks associated with the service
   574  	Connect    *ConsulConnect    // Consul Connect configuration
   575  	Meta       map[string]string // Consul service meta
   576  	CanaryMeta map[string]string // Consul service meta when it is a canary
   577  
   578  	// The values to set for tagged_addresses in Consul service registration.
   579  	// Does not affect Nomad networking, these are for Consul service discovery.
   580  	TaggedAddresses map[string]string
   581  
   582  	// The consul namespace in which this service will be registered. Namespace
   583  	// at the service.check level is not part of the Nomad API - it must be
   584  	// set at the job or group level. This field is managed internally so
   585  	// that Hash can work correctly.
   586  	Namespace string
   587  
   588  	// OnUpdate Specifies how the service and its checks should be evaluated
   589  	// during an update
   590  	OnUpdate string
   591  
   592  	// Provider dictates which service discovery provider to use. This can be
   593  	// either ServiceProviderConsul or ServiceProviderNomad and defaults to the former when
   594  	// left empty by the operator.
   595  	Provider string
   596  }
   597  
   598  // Copy the stanza recursively. Returns nil if nil.
   599  func (s *Service) Copy() *Service {
   600  	if s == nil {
   601  		return nil
   602  	}
   603  	ns := new(Service)
   604  	*ns = *s
   605  	ns.Tags = slices.Clone(ns.Tags)
   606  	ns.CanaryTags = slices.Clone(ns.CanaryTags)
   607  
   608  	if s.Checks != nil {
   609  		checks := make([]*ServiceCheck, len(ns.Checks))
   610  		for i, c := range ns.Checks {
   611  			checks[i] = c.Copy()
   612  		}
   613  		ns.Checks = checks
   614  	}
   615  
   616  	ns.Connect = s.Connect.Copy()
   617  
   618  	ns.Meta = maps.Clone(s.Meta)
   619  	ns.CanaryMeta = maps.Clone(s.CanaryMeta)
   620  	ns.TaggedAddresses = maps.Clone(s.TaggedAddresses)
   621  
   622  	return ns
   623  }
   624  
   625  // Canonicalize interpolates values of Job, Task Group and Task in the Service
   626  // Name. This also generates check names, service id and check ids.
   627  func (s *Service) Canonicalize(job, taskGroup, task, jobNamespace string) {
   628  	// Ensure empty lists are treated as null to avoid scheduler issues when
   629  	// using DeepEquals
   630  	if len(s.Tags) == 0 {
   631  		s.Tags = nil
   632  	}
   633  	if len(s.CanaryTags) == 0 {
   634  		s.CanaryTags = nil
   635  	}
   636  	if len(s.Checks) == 0 {
   637  		s.Checks = nil
   638  	}
   639  	if len(s.TaggedAddresses) == 0 {
   640  		s.TaggedAddresses = nil
   641  	}
   642  
   643  	// Set the task name if not already set
   644  	if s.TaskName == "" && task != "group" {
   645  		s.TaskName = task
   646  	}
   647  
   648  	s.Name = args.ReplaceEnv(s.Name, map[string]string{
   649  		"JOB":       job,
   650  		"TASKGROUP": taskGroup,
   651  		"TASK":      task,
   652  		"BASE":      fmt.Sprintf("%s-%s-%s", job, taskGroup, task),
   653  	})
   654  
   655  	for _, check := range s.Checks {
   656  		check.Canonicalize(s.Name, s.TaskName)
   657  	}
   658  
   659  	// Set the provider to its default value. The value of consul ensures this
   660  	// new feature and parameter behaves in the same manner a previous versions
   661  	// which did not include this.
   662  	if s.Provider == "" {
   663  		s.Provider = ServiceProviderConsul
   664  	}
   665  
   666  	// Consul API returns "default" whether the namespace is empty or set as
   667  	// such, so we coerce our copy of the service to be the same if using the
   668  	// consul provider.
   669  	//
   670  	// When using ServiceProviderNomad, set the namespace to that of the job. This
   671  	// makes modifications and diffs on the service correct.
   672  	if s.Namespace == "" && s.Provider == ServiceProviderConsul {
   673  		s.Namespace = "default"
   674  	} else if s.Provider == ServiceProviderNomad {
   675  		s.Namespace = jobNamespace
   676  	}
   677  }
   678  
   679  // Validate checks if the Service definition is valid
   680  func (s *Service) Validate() error {
   681  	var mErr multierror.Error
   682  
   683  	// Ensure the service name is valid per the below RFCs but make an exception
   684  	// for our interpolation syntax by first stripping any environment variables from the name
   685  
   686  	serviceNameStripped := args.ReplaceEnvWithPlaceHolder(s.Name, "ENV-VAR")
   687  
   688  	if err := s.ValidateName(serviceNameStripped); err != nil {
   689  		// Log actual service name, not the stripped version.
   690  		mErr.Errors = append(mErr.Errors, fmt.Errorf("%v: %q", err, s.Name))
   691  	}
   692  
   693  	switch s.AddressMode {
   694  	case "", AddressModeAuto:
   695  	case AddressModeHost, AddressModeDriver, AddressModeAlloc:
   696  		if s.Address != "" {
   697  			mErr.Errors = append(mErr.Errors, fmt.Errorf("Service address_mode must be %q if address is set", AddressModeAuto))
   698  		}
   699  	default:
   700  		mErr.Errors = append(mErr.Errors, fmt.Errorf("Service address_mode must be %q, %q, or %q; not %q", AddressModeAuto, AddressModeHost, AddressModeDriver, s.AddressMode))
   701  	}
   702  
   703  	switch s.OnUpdate {
   704  	case "", OnUpdateIgnore, OnUpdateRequireHealthy, OnUpdateIgnoreWarn:
   705  		// OK
   706  	default:
   707  		mErr.Errors = append(mErr.Errors, fmt.Errorf("Service on_update must be %q, %q, or %q; not %q", OnUpdateRequireHealthy, OnUpdateIgnoreWarn, OnUpdateIgnore, s.OnUpdate))
   708  	}
   709  
   710  	// Up until this point, all service validation has been independent of the
   711  	// provider. From this point on, we have different validation paths. We can
   712  	// also catch an incorrect provider parameter.
   713  	switch s.Provider {
   714  	case ServiceProviderConsul:
   715  		s.validateConsulService(&mErr)
   716  	case ServiceProviderNomad:
   717  		s.validateNomadService(&mErr)
   718  	default:
   719  		mErr.Errors = append(mErr.Errors, fmt.Errorf("Service provider must be %q, or %q; not %q",
   720  			ServiceProviderConsul, ServiceProviderNomad, s.Provider))
   721  	}
   722  
   723  	return mErr.ErrorOrNil()
   724  }
   725  
   726  func (s *Service) validateCheckPort(c *ServiceCheck) error {
   727  	if s.PortLabel == "" && c.PortLabel == "" && c.RequiresPort() {
   728  		return fmt.Errorf("Check %s invalid: check requires a port but neither check nor service %+q have a port", c.Name, s.Name)
   729  	}
   730  	return nil
   731  }
   732  
   733  // validateConsulService performs validation on a service which is using the
   734  // consul provider.
   735  func (s *Service) validateConsulService(mErr *multierror.Error) {
   736  	// check checks
   737  	for _, c := range s.Checks {
   738  		// validat ethe check port
   739  		if err := s.validateCheckPort(c); err != nil {
   740  			mErr.Errors = append(mErr.Errors, err)
   741  			continue
   742  		}
   743  
   744  		// TCP checks against a Consul Connect enabled service are not supported
   745  		// due to the service being bound to the loopback interface inside the
   746  		// network namespace
   747  		if c.Type == ServiceCheckTCP && s.Connect != nil && s.Connect.SidecarService != nil {
   748  			mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s invalid: tcp checks are not valid for Connect enabled services", c.Name))
   749  			continue
   750  		}
   751  
   752  		// validate the consul check
   753  		if err := c.validateConsul(); err != nil {
   754  			mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s invalid: %v", c.Name, err))
   755  		}
   756  	}
   757  
   758  	// check connect
   759  	if s.Connect != nil {
   760  		if err := s.Connect.Validate(); err != nil {
   761  			mErr.Errors = append(mErr.Errors, err)
   762  		}
   763  
   764  		// if service is connect native, service task must be set (which may
   765  		// happen implicitly in a job mutation if there is only one task)
   766  		if s.Connect.IsNative() && len(s.TaskName) == 0 {
   767  			mErr.Errors = append(mErr.Errors, fmt.Errorf("Service %s is Connect Native and requires setting the task", s.Name))
   768  		}
   769  	}
   770  }
   771  
   772  // validateNomadService performs validation on a service which is using the
   773  // nomad provider.
   774  func (s *Service) validateNomadService(mErr *multierror.Error) {
   775  	// check checks
   776  	for _, c := range s.Checks {
   777  		// validate the check port
   778  		if err := s.validateCheckPort(c); err != nil {
   779  			mErr.Errors = append(mErr.Errors, err)
   780  			continue
   781  		}
   782  
   783  		// validate the nomad check
   784  		if err := c.validateNomad(); err != nil {
   785  			mErr.Errors = append(mErr.Errors, err)
   786  		}
   787  	}
   788  
   789  	// Services using the Nomad provider do not support Consul connect.
   790  	if s.Connect != nil {
   791  		mErr.Errors = append(mErr.Errors, errors.New("Service with provider nomad cannot include Connect blocks"))
   792  	}
   793  }
   794  
   795  // ValidateName checks if the service Name is valid and should be called after
   796  // the name has been interpolated
   797  func (s *Service) ValidateName(name string) error {
   798  	// Ensure the service name is valid per RFC-952 §1
   799  	// (https://tools.ietf.org/html/rfc952), RFC-1123 §2.1
   800  	// (https://tools.ietf.org/html/rfc1123), and RFC-2782
   801  	// (https://tools.ietf.org/html/rfc2782).
   802  	re := regexp.MustCompile(`^(?i:[a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])$`)
   803  	if !re.MatchString(name) {
   804  		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")
   805  	}
   806  	return nil
   807  }
   808  
   809  // Hash returns a base32 encoded hash of a Service's contents excluding checks
   810  // as they're hashed independently and the provider in order to not cause churn
   811  // during cluster upgrades.
   812  func (s *Service) Hash(allocID, taskName string, canary bool) string {
   813  	h := sha1.New()
   814  	hashString(h, allocID)
   815  	hashString(h, taskName)
   816  	hashString(h, s.Name)
   817  	hashString(h, s.PortLabel)
   818  	hashString(h, s.AddressMode)
   819  	hashString(h, s.Address)
   820  	hashTags(h, s.Tags)
   821  	hashTags(h, s.CanaryTags)
   822  	hashBool(h, canary, "Canary")
   823  	hashBool(h, s.EnableTagOverride, "ETO")
   824  	hashMeta(h, s.Meta)
   825  	hashMeta(h, s.CanaryMeta)
   826  	hashMeta(h, s.TaggedAddresses)
   827  	hashConnect(h, s.Connect)
   828  	hashString(h, s.OnUpdate)
   829  	hashString(h, s.Namespace)
   830  
   831  	// Don't hash the provider parameter, so we don't cause churn of all
   832  	// registered services when upgrading Nomad versions. The provider is not
   833  	// used at the level the hash is and therefore is not needed to tell
   834  	// whether the service has changed.
   835  
   836  	// Base32 is used for encoding the hash as sha1 hashes can always be
   837  	// encoded without padding, only 4 bytes larger than base64, and saves
   838  	// 8 bytes vs hex. Since these hashes are used in Consul URLs it's nice
   839  	// to have a reasonably compact URL-safe representation.
   840  	return b32.EncodeToString(h.Sum(nil))
   841  }
   842  
   843  func hashConnect(h hash.Hash, connect *ConsulConnect) {
   844  	if connect != nil && connect.SidecarService != nil {
   845  		hashString(h, connect.SidecarService.Port)
   846  		hashTags(h, connect.SidecarService.Tags)
   847  		if p := connect.SidecarService.Proxy; p != nil {
   848  			hashString(h, p.LocalServiceAddress)
   849  			hashString(h, strconv.Itoa(p.LocalServicePort))
   850  			hashConfig(h, p.Config)
   851  			for _, upstream := range p.Upstreams {
   852  				hashString(h, upstream.DestinationName)
   853  				hashString(h, upstream.DestinationNamespace)
   854  				hashString(h, strconv.Itoa(upstream.LocalBindPort))
   855  				hashStringIfNonEmpty(h, upstream.Datacenter)
   856  				hashStringIfNonEmpty(h, upstream.LocalBindAddress)
   857  			}
   858  		}
   859  	}
   860  }
   861  
   862  func hashString(h hash.Hash, s string) {
   863  	_, _ = io.WriteString(h, s)
   864  }
   865  
   866  func hashBool(h hash.Hash, b bool, name string) {
   867  	if b {
   868  		hashString(h, name)
   869  	}
   870  }
   871  
   872  func hashTags(h hash.Hash, tags []string) {
   873  	for _, tag := range tags {
   874  		hashString(h, tag)
   875  	}
   876  }
   877  
   878  func hashMeta(h hash.Hash, m map[string]string) {
   879  	_, _ = fmt.Fprintf(h, "%v", m)
   880  }
   881  
   882  func hashConfig(h hash.Hash, c map[string]interface{}) {
   883  	_, _ = fmt.Fprintf(h, "%v", c)
   884  }
   885  
   886  // Equal returns true if the structs are recursively equal.
   887  func (s *Service) Equal(o *Service) bool {
   888  	if s == nil || o == nil {
   889  		return s == o
   890  	}
   891  
   892  	if s.Provider != o.Provider {
   893  		return false
   894  	}
   895  
   896  	if s.Namespace != o.Namespace {
   897  		return false
   898  	}
   899  
   900  	if s.AddressMode != o.AddressMode {
   901  		return false
   902  	}
   903  
   904  	if s.Address != o.Address {
   905  		return false
   906  	}
   907  
   908  	if s.OnUpdate != o.OnUpdate {
   909  		return false
   910  	}
   911  
   912  	if !helper.SliceSetEq(s.CanaryTags, o.CanaryTags) {
   913  		return false
   914  	}
   915  
   916  	if !helper.ElementsEqual(s.Checks, o.Checks) {
   917  		return false
   918  	}
   919  
   920  	if !s.Connect.Equal(o.Connect) {
   921  		return false
   922  	}
   923  
   924  	if s.Name != o.Name {
   925  		return false
   926  	}
   927  
   928  	if s.PortLabel != o.PortLabel {
   929  		return false
   930  	}
   931  
   932  	if !maps.Equal(s.Meta, o.Meta) {
   933  		return false
   934  	}
   935  
   936  	if !maps.Equal(s.CanaryMeta, o.CanaryMeta) {
   937  		return false
   938  	}
   939  
   940  	if !maps.Equal(s.TaggedAddresses, o.TaggedAddresses) {
   941  		return false
   942  	}
   943  
   944  	if !helper.SliceSetEq(s.Tags, o.Tags) {
   945  		return false
   946  	}
   947  
   948  	if s.EnableTagOverride != o.EnableTagOverride {
   949  		return false
   950  	}
   951  
   952  	return true
   953  }
   954  
   955  // ConsulConnect represents a Consul Connect jobspec stanza.
   956  type ConsulConnect struct {
   957  	// Native indicates whether the service is Consul Connect Native enabled.
   958  	Native bool
   959  
   960  	// SidecarService is non-nil if a service requires a sidecar.
   961  	SidecarService *ConsulSidecarService
   962  
   963  	// SidecarTask is non-nil if sidecar overrides are set
   964  	SidecarTask *SidecarTask
   965  
   966  	// Gateway is a Consul Connect Gateway Proxy.
   967  	Gateway *ConsulGateway
   968  }
   969  
   970  // Copy the stanza recursively. Returns nil if nil.
   971  func (c *ConsulConnect) Copy() *ConsulConnect {
   972  	if c == nil {
   973  		return nil
   974  	}
   975  
   976  	return &ConsulConnect{
   977  		Native:         c.Native,
   978  		SidecarService: c.SidecarService.Copy(),
   979  		SidecarTask:    c.SidecarTask.Copy(),
   980  		Gateway:        c.Gateway.Copy(),
   981  	}
   982  }
   983  
   984  // Equal returns true if the connect blocks are deeply equal.
   985  func (c *ConsulConnect) Equal(o *ConsulConnect) bool {
   986  	if c == nil || o == nil {
   987  		return c == o
   988  	}
   989  
   990  	if c.Native != o.Native {
   991  		return false
   992  	}
   993  
   994  	if !c.SidecarService.Equal(o.SidecarService) {
   995  		return false
   996  	}
   997  
   998  	if !c.SidecarTask.Equal(o.SidecarTask) {
   999  		return false
  1000  	}
  1001  
  1002  	if !c.Gateway.Equal(o.Gateway) {
  1003  		return false
  1004  	}
  1005  
  1006  	return true
  1007  }
  1008  
  1009  // HasSidecar checks if a sidecar task is configured.
  1010  func (c *ConsulConnect) HasSidecar() bool {
  1011  	return c != nil && c.SidecarService != nil
  1012  }
  1013  
  1014  // IsNative checks if the service is connect native.
  1015  func (c *ConsulConnect) IsNative() bool {
  1016  	return c != nil && c.Native
  1017  }
  1018  
  1019  // IsGateway checks if the service is any type of connect gateway.
  1020  func (c *ConsulConnect) IsGateway() bool {
  1021  	return c != nil && c.Gateway != nil
  1022  }
  1023  
  1024  // IsIngress checks if the service is an ingress gateway.
  1025  func (c *ConsulConnect) IsIngress() bool {
  1026  	return c.IsGateway() && c.Gateway.Ingress != nil
  1027  }
  1028  
  1029  // IsTerminating checks if the service is a terminating gateway.
  1030  func (c *ConsulConnect) IsTerminating() bool {
  1031  	return c.IsGateway() && c.Gateway.Terminating != nil
  1032  }
  1033  
  1034  // IsCustomizedTLS checks if the service customizes ingress tls config.
  1035  func (c *ConsulConnect) IsCustomizedTLS() bool {
  1036  	return c.IsIngress() && c.Gateway.Ingress.TLS != nil &&
  1037  		(c.Gateway.Ingress.TLS.TLSMinVersion != "" ||
  1038  			c.Gateway.Ingress.TLS.TLSMaxVersion != "" ||
  1039  			len(c.Gateway.Ingress.TLS.CipherSuites) != 0)
  1040  }
  1041  
  1042  func (c *ConsulConnect) IsMesh() bool {
  1043  	return c.IsGateway() && c.Gateway.Mesh != nil
  1044  }
  1045  
  1046  // Validate that the Connect block represents exactly one of:
  1047  // - Connect non-native service sidecar proxy
  1048  // - Connect native service
  1049  // - Connect gateway (any type)
  1050  func (c *ConsulConnect) Validate() error {
  1051  	if c == nil {
  1052  		return nil
  1053  	}
  1054  
  1055  	// Count the number of things actually configured. If that number is not 1,
  1056  	// the config is not valid.
  1057  	count := 0
  1058  
  1059  	if c.HasSidecar() {
  1060  		count++
  1061  	}
  1062  
  1063  	if c.IsNative() {
  1064  		count++
  1065  	}
  1066  
  1067  	if c.IsGateway() {
  1068  		count++
  1069  	}
  1070  
  1071  	if count != 1 {
  1072  		return fmt.Errorf("Consul Connect must be exclusively native, make use of a sidecar, or represent a Gateway")
  1073  	}
  1074  
  1075  	if c.IsGateway() {
  1076  		if err := c.Gateway.Validate(); err != nil {
  1077  			return err
  1078  		}
  1079  	}
  1080  
  1081  	// The Native and Sidecar cases are validated up at the service level.
  1082  
  1083  	return nil
  1084  }
  1085  
  1086  // ConsulSidecarService represents a Consul Connect SidecarService jobspec
  1087  // stanza.
  1088  type ConsulSidecarService struct {
  1089  	// Tags are optional service tags that get registered with the sidecar service
  1090  	// in Consul. If unset, the sidecar service inherits the parent service tags.
  1091  	Tags []string
  1092  
  1093  	// Port is the service's port that the sidecar will connect to. May be
  1094  	// a port label or a literal port number.
  1095  	Port string
  1096  
  1097  	// Proxy stanza defining the sidecar proxy configuration.
  1098  	Proxy *ConsulProxy
  1099  
  1100  	// DisableDefaultTCPCheck, if true, instructs Nomad to avoid setting a
  1101  	// default TCP check for the sidecar service.
  1102  	DisableDefaultTCPCheck bool
  1103  }
  1104  
  1105  // HasUpstreams checks if the sidecar service has any upstreams configured
  1106  func (s *ConsulSidecarService) HasUpstreams() bool {
  1107  	return s != nil && s.Proxy != nil && len(s.Proxy.Upstreams) > 0
  1108  }
  1109  
  1110  // Copy the stanza recursively. Returns nil if nil.
  1111  func (s *ConsulSidecarService) Copy() *ConsulSidecarService {
  1112  	if s == nil {
  1113  		return nil
  1114  	}
  1115  	return &ConsulSidecarService{
  1116  		Tags:                   slices.Clone(s.Tags),
  1117  		Port:                   s.Port,
  1118  		Proxy:                  s.Proxy.Copy(),
  1119  		DisableDefaultTCPCheck: s.DisableDefaultTCPCheck,
  1120  	}
  1121  }
  1122  
  1123  // Equal returns true if the structs are recursively equal.
  1124  func (s *ConsulSidecarService) Equal(o *ConsulSidecarService) bool {
  1125  	if s == nil || o == nil {
  1126  		return s == o
  1127  	}
  1128  
  1129  	if s.Port != o.Port {
  1130  		return false
  1131  	}
  1132  
  1133  	if s.DisableDefaultTCPCheck != o.DisableDefaultTCPCheck {
  1134  		return false
  1135  	}
  1136  
  1137  	if !helper.SliceSetEq(s.Tags, o.Tags) {
  1138  		return false
  1139  	}
  1140  
  1141  	return s.Proxy.Equal(o.Proxy)
  1142  }
  1143  
  1144  // SidecarTask represents a subset of Task fields that are able to be overridden
  1145  // from the sidecar_task stanza
  1146  type SidecarTask struct {
  1147  	// Name of the task
  1148  	Name string
  1149  
  1150  	// Driver is used to control which driver is used
  1151  	Driver string
  1152  
  1153  	// User is used to determine which user will run the task. It defaults to
  1154  	// the same user the Nomad client is being run as.
  1155  	User string
  1156  
  1157  	// Config is provided to the driver to initialize
  1158  	Config map[string]interface{}
  1159  
  1160  	// Map of environment variables to be used by the driver
  1161  	Env map[string]string
  1162  
  1163  	// Resources is the resources needed by this task
  1164  	Resources *Resources
  1165  
  1166  	// Meta is used to associate arbitrary metadata with this
  1167  	// task. This is opaque to Nomad.
  1168  	Meta map[string]string
  1169  
  1170  	// KillTimeout is the time between signaling a task that it will be
  1171  	// killed and killing it.
  1172  	KillTimeout *time.Duration
  1173  
  1174  	// LogConfig provides configuration for log rotation
  1175  	LogConfig *LogConfig
  1176  
  1177  	// ShutdownDelay is the duration of the delay between deregistering a
  1178  	// task from Consul and sending it a signal to shutdown. See #2441
  1179  	ShutdownDelay *time.Duration
  1180  
  1181  	// KillSignal is the kill signal to use for the task. This is an optional
  1182  	// specification and defaults to SIGINT
  1183  	KillSignal string
  1184  }
  1185  
  1186  func (t *SidecarTask) Equal(o *SidecarTask) bool {
  1187  	if t == nil || o == nil {
  1188  		return t == o
  1189  	}
  1190  
  1191  	if t.Name != o.Name {
  1192  		return false
  1193  	}
  1194  
  1195  	if t.Driver != o.Driver {
  1196  		return false
  1197  	}
  1198  
  1199  	if t.User != o.User {
  1200  		return false
  1201  	}
  1202  
  1203  	// config compare
  1204  	if !opaqueMapsEqual(t.Config, o.Config) {
  1205  		return false
  1206  	}
  1207  
  1208  	if !maps.Equal(t.Env, o.Env) {
  1209  		return false
  1210  	}
  1211  
  1212  	if !t.Resources.Equal(o.Resources) {
  1213  		return false
  1214  	}
  1215  
  1216  	if !maps.Equal(t.Meta, o.Meta) {
  1217  		return false
  1218  	}
  1219  
  1220  	if !pointer.Eq(t.KillTimeout, o.KillTimeout) {
  1221  		return false
  1222  	}
  1223  
  1224  	if !t.LogConfig.Equal(o.LogConfig) {
  1225  		return false
  1226  	}
  1227  
  1228  	if !pointer.Eq(t.ShutdownDelay, o.ShutdownDelay) {
  1229  		return false
  1230  	}
  1231  
  1232  	if t.KillSignal != o.KillSignal {
  1233  		return false
  1234  	}
  1235  
  1236  	return true
  1237  }
  1238  
  1239  func (t *SidecarTask) Copy() *SidecarTask {
  1240  	if t == nil {
  1241  		return nil
  1242  	}
  1243  	nt := new(SidecarTask)
  1244  	*nt = *t
  1245  	nt.Env = maps.Clone(nt.Env)
  1246  
  1247  	nt.Resources = nt.Resources.Copy()
  1248  	nt.LogConfig = nt.LogConfig.Copy()
  1249  	nt.Meta = maps.Clone(nt.Meta)
  1250  
  1251  	if i, err := copystructure.Copy(nt.Config); err != nil {
  1252  		panic(err.Error())
  1253  	} else {
  1254  		nt.Config = i.(map[string]interface{})
  1255  	}
  1256  
  1257  	if t.KillTimeout != nil {
  1258  		nt.KillTimeout = pointer.Of(*t.KillTimeout)
  1259  	}
  1260  
  1261  	if t.ShutdownDelay != nil {
  1262  		nt.ShutdownDelay = pointer.Of(*t.ShutdownDelay)
  1263  	}
  1264  
  1265  	return nt
  1266  }
  1267  
  1268  // MergeIntoTask merges the SidecarTask fields over the given task
  1269  func (t *SidecarTask) MergeIntoTask(task *Task) {
  1270  	if t.Name != "" {
  1271  		task.Name = t.Name
  1272  	}
  1273  
  1274  	// If the driver changes then the driver config can be overwritten.
  1275  	// Otherwise we'll merge the driver config together
  1276  	if t.Driver != "" && t.Driver != task.Driver {
  1277  		task.Driver = t.Driver
  1278  		task.Config = t.Config
  1279  	} else {
  1280  		for k, v := range t.Config {
  1281  			task.Config[k] = v
  1282  		}
  1283  	}
  1284  
  1285  	if t.User != "" {
  1286  		task.User = t.User
  1287  	}
  1288  
  1289  	if t.Env != nil {
  1290  		if task.Env == nil {
  1291  			task.Env = t.Env
  1292  		} else {
  1293  			for k, v := range t.Env {
  1294  				task.Env[k] = v
  1295  			}
  1296  		}
  1297  	}
  1298  
  1299  	if t.Resources != nil {
  1300  		task.Resources.Merge(t.Resources)
  1301  	}
  1302  
  1303  	if t.Meta != nil {
  1304  		if task.Meta == nil {
  1305  			task.Meta = t.Meta
  1306  		} else {
  1307  			for k, v := range t.Meta {
  1308  				task.Meta[k] = v
  1309  			}
  1310  		}
  1311  	}
  1312  
  1313  	if t.KillTimeout != nil {
  1314  		task.KillTimeout = *t.KillTimeout
  1315  	}
  1316  
  1317  	if t.LogConfig != nil {
  1318  		if task.LogConfig == nil {
  1319  			task.LogConfig = t.LogConfig
  1320  		} else {
  1321  			if t.LogConfig.MaxFiles > 0 {
  1322  				task.LogConfig.MaxFiles = t.LogConfig.MaxFiles
  1323  			}
  1324  			if t.LogConfig.MaxFileSizeMB > 0 {
  1325  				task.LogConfig.MaxFileSizeMB = t.LogConfig.MaxFileSizeMB
  1326  			}
  1327  		}
  1328  	}
  1329  
  1330  	if t.ShutdownDelay != nil {
  1331  		task.ShutdownDelay = *t.ShutdownDelay
  1332  	}
  1333  
  1334  	if t.KillSignal != "" {
  1335  		task.KillSignal = t.KillSignal
  1336  	}
  1337  }
  1338  
  1339  // ConsulProxy represents a Consul Connect sidecar proxy jobspec stanza.
  1340  type ConsulProxy struct {
  1341  
  1342  	// LocalServiceAddress is the address the local service binds to.
  1343  	// Usually 127.0.0.1 it is useful to customize in clusters with mixed
  1344  	// Connect and non-Connect services.
  1345  	LocalServiceAddress string
  1346  
  1347  	// LocalServicePort is the port the local service binds to. Usually
  1348  	// the same as the parent service's port, it is useful to customize
  1349  	// in clusters with mixed Connect and non-Connect services
  1350  	LocalServicePort int
  1351  
  1352  	// Upstreams configures the upstream services this service intends to
  1353  	// connect to.
  1354  	Upstreams []ConsulUpstream
  1355  
  1356  	// Expose configures the consul proxy.expose stanza to "open up" endpoints
  1357  	// used by task-group level service checks using HTTP or gRPC protocols.
  1358  	//
  1359  	// Use json tag to match with field name in api/
  1360  	Expose *ConsulExposeConfig `json:"ExposeConfig"`
  1361  
  1362  	// Config is a proxy configuration. It is opaque to Nomad and passed
  1363  	// directly to Consul.
  1364  	Config map[string]interface{}
  1365  }
  1366  
  1367  // Copy the stanza recursively. Returns nil if nil.
  1368  func (p *ConsulProxy) Copy() *ConsulProxy {
  1369  	if p == nil {
  1370  		return nil
  1371  	}
  1372  
  1373  	return &ConsulProxy{
  1374  		LocalServiceAddress: p.LocalServiceAddress,
  1375  		LocalServicePort:    p.LocalServicePort,
  1376  		Expose:              p.Expose.Copy(),
  1377  		Upstreams:           slices.Clone(p.Upstreams),
  1378  		Config:              maps.Clone(p.Config),
  1379  	}
  1380  }
  1381  
  1382  // opaqueMapsEqual compares map[string]interface{} commonly used for opaque
  1383  // config blocks. Interprets nil and {} as the same.
  1384  func opaqueMapsEqual(a, b map[string]interface{}) bool {
  1385  	if len(a) == 0 && len(b) == 0 {
  1386  		return true
  1387  	}
  1388  	return reflect.DeepEqual(a, b)
  1389  }
  1390  
  1391  // Equal returns true if the structs are recursively equal.
  1392  func (p *ConsulProxy) Equal(o *ConsulProxy) bool {
  1393  	if p == nil || o == nil {
  1394  		return p == o
  1395  	}
  1396  
  1397  	if p.LocalServiceAddress != o.LocalServiceAddress {
  1398  		return false
  1399  	}
  1400  
  1401  	if p.LocalServicePort != o.LocalServicePort {
  1402  		return false
  1403  	}
  1404  
  1405  	if !p.Expose.Equal(o.Expose) {
  1406  		return false
  1407  	}
  1408  
  1409  	if !upstreamsEquals(p.Upstreams, o.Upstreams) {
  1410  		return false
  1411  	}
  1412  
  1413  	if !opaqueMapsEqual(p.Config, o.Config) {
  1414  		return false
  1415  	}
  1416  
  1417  	return true
  1418  }
  1419  
  1420  // ConsulMeshGateway is used to configure mesh gateway usage when connecting to
  1421  // a connect upstream in another datacenter.
  1422  type ConsulMeshGateway struct {
  1423  	// Mode configures how an upstream should be accessed with regard to using
  1424  	// mesh gateways.
  1425  	//
  1426  	// local - the connect proxy makes outbound connections through mesh gateway
  1427  	// originating in the same datacenter.
  1428  	//
  1429  	// remote - the connect proxy makes outbound connections to a mesh gateway
  1430  	// in the destination datacenter.
  1431  	//
  1432  	// none (default) - no mesh gateway is used, the proxy makes outbound connections
  1433  	// directly to destination services.
  1434  	//
  1435  	// https://www.consul.io/docs/connect/gateways/mesh-gateway#modes-of-operation
  1436  	Mode string
  1437  }
  1438  
  1439  func (c *ConsulMeshGateway) Copy() ConsulMeshGateway {
  1440  	return ConsulMeshGateway{
  1441  		Mode: c.Mode,
  1442  	}
  1443  }
  1444  
  1445  func (c *ConsulMeshGateway) Equal(o ConsulMeshGateway) bool {
  1446  	return c.Mode == o.Mode
  1447  }
  1448  
  1449  func (c *ConsulMeshGateway) Validate() error {
  1450  	if c == nil {
  1451  		return nil
  1452  	}
  1453  
  1454  	switch c.Mode {
  1455  	case "local", "remote", "none":
  1456  		return nil
  1457  	default:
  1458  		return fmt.Errorf("Connect mesh_gateway mode %q not supported", c.Mode)
  1459  	}
  1460  }
  1461  
  1462  // ConsulUpstream represents a Consul Connect upstream jobspec stanza.
  1463  type ConsulUpstream struct {
  1464  	// DestinationName is the name of the upstream service.
  1465  	DestinationName string
  1466  
  1467  	// DestinationNamespace is the namespace of the upstream service.
  1468  	DestinationNamespace string
  1469  
  1470  	// LocalBindPort is the port the proxy will receive connections for the
  1471  	// upstream on.
  1472  	LocalBindPort int
  1473  
  1474  	// Datacenter is the datacenter in which to issue the discovery query to.
  1475  	Datacenter string
  1476  
  1477  	// LocalBindAddress is the address the proxy will receive connections for the
  1478  	// upstream on.
  1479  	LocalBindAddress string
  1480  
  1481  	// MeshGateway is the optional configuration of the mesh gateway for this
  1482  	// upstream to use.
  1483  	MeshGateway ConsulMeshGateway
  1484  }
  1485  
  1486  // Equal returns true if the structs are recursively equal.
  1487  func (u *ConsulUpstream) Equal(o *ConsulUpstream) bool {
  1488  	if u == nil || o == nil {
  1489  		return u == o
  1490  	}
  1491  	return *u == *o
  1492  }
  1493  
  1494  func upstreamsEquals(a, b []ConsulUpstream) bool {
  1495  	return set.From(a).Equal(set.From(b))
  1496  }
  1497  
  1498  // ConsulExposeConfig represents a Consul Connect expose jobspec stanza.
  1499  type ConsulExposeConfig struct {
  1500  	// Use json tag to match with field name in api/
  1501  	Paths []ConsulExposePath `json:"Path"`
  1502  }
  1503  
  1504  type ConsulExposePath struct {
  1505  	Path          string
  1506  	Protocol      string
  1507  	LocalPathPort int
  1508  	ListenerPort  string
  1509  }
  1510  
  1511  func exposePathsEqual(a, b []ConsulExposePath) bool {
  1512  	return helper.SliceSetEq(a, b)
  1513  }
  1514  
  1515  // Copy the stanza. Returns nil if e is nil.
  1516  func (e *ConsulExposeConfig) Copy() *ConsulExposeConfig {
  1517  	if e == nil {
  1518  		return nil
  1519  	}
  1520  	paths := make([]ConsulExposePath, len(e.Paths))
  1521  	copy(paths, e.Paths)
  1522  	return &ConsulExposeConfig{
  1523  		Paths: paths,
  1524  	}
  1525  }
  1526  
  1527  // Equal returns true if the structs are recursively equal.
  1528  func (e *ConsulExposeConfig) Equal(o *ConsulExposeConfig) bool {
  1529  	if e == nil || o == nil {
  1530  		return e == o
  1531  	}
  1532  	return exposePathsEqual(e.Paths, o.Paths)
  1533  }
  1534  
  1535  // ConsulGateway is used to configure one of the Consul Connect Gateway types.
  1536  type ConsulGateway struct {
  1537  	// Proxy is used to configure the Envoy instance acting as the gateway.
  1538  	Proxy *ConsulGatewayProxy
  1539  
  1540  	// Ingress represents the Consul Configuration Entry for an Ingress Gateway.
  1541  	Ingress *ConsulIngressConfigEntry
  1542  
  1543  	// Terminating represents the Consul Configuration Entry for a Terminating Gateway.
  1544  	Terminating *ConsulTerminatingConfigEntry
  1545  
  1546  	// Mesh indicates the Consul service should be a Mesh Gateway.
  1547  	Mesh *ConsulMeshConfigEntry
  1548  }
  1549  
  1550  func (g *ConsulGateway) Prefix() string {
  1551  	switch {
  1552  	case g.Mesh != nil:
  1553  		return ConnectMeshPrefix
  1554  	case g.Ingress != nil:
  1555  		return ConnectIngressPrefix
  1556  	default:
  1557  		return ConnectTerminatingPrefix
  1558  	}
  1559  }
  1560  
  1561  func (g *ConsulGateway) Copy() *ConsulGateway {
  1562  	if g == nil {
  1563  		return nil
  1564  	}
  1565  
  1566  	return &ConsulGateway{
  1567  		Proxy:       g.Proxy.Copy(),
  1568  		Ingress:     g.Ingress.Copy(),
  1569  		Terminating: g.Terminating.Copy(),
  1570  		Mesh:        g.Mesh.Copy(),
  1571  	}
  1572  }
  1573  
  1574  func (g *ConsulGateway) Equal(o *ConsulGateway) bool {
  1575  	if g == nil || o == nil {
  1576  		return g == o
  1577  	}
  1578  
  1579  	if !g.Proxy.Equal(o.Proxy) {
  1580  		return false
  1581  	}
  1582  
  1583  	if !g.Ingress.Equal(o.Ingress) {
  1584  		return false
  1585  	}
  1586  
  1587  	if !g.Terminating.Equal(o.Terminating) {
  1588  		return false
  1589  	}
  1590  
  1591  	if !g.Mesh.Equal(o.Mesh) {
  1592  		return false
  1593  	}
  1594  
  1595  	return true
  1596  }
  1597  
  1598  func (g *ConsulGateway) Validate() error {
  1599  	if g == nil {
  1600  		return nil
  1601  	}
  1602  
  1603  	if err := g.Proxy.Validate(); err != nil {
  1604  		return err
  1605  	}
  1606  
  1607  	if err := g.Ingress.Validate(); err != nil {
  1608  		return err
  1609  	}
  1610  
  1611  	if err := g.Terminating.Validate(); err != nil {
  1612  		return err
  1613  	}
  1614  
  1615  	if err := g.Mesh.Validate(); err != nil {
  1616  		return err
  1617  	}
  1618  
  1619  	// Exactly 1 of ingress/terminating/mesh must be set.
  1620  	count := 0
  1621  	if g.Ingress != nil {
  1622  		count++
  1623  	}
  1624  	if g.Terminating != nil {
  1625  		count++
  1626  	}
  1627  	if g.Mesh != nil {
  1628  		count++
  1629  	}
  1630  	if count != 1 {
  1631  		return fmt.Errorf("One Consul Gateway Configuration must be set")
  1632  	}
  1633  	return nil
  1634  }
  1635  
  1636  // ConsulGatewayBindAddress is equivalent to Consul's api/catalog.go ServiceAddress
  1637  // struct, as this is used to encode values to pass along to Envoy (i.e. via
  1638  // JSON encoding).
  1639  type ConsulGatewayBindAddress struct {
  1640  	Address string
  1641  	Port    int
  1642  }
  1643  
  1644  func (a *ConsulGatewayBindAddress) Equal(o *ConsulGatewayBindAddress) bool {
  1645  	if a == nil || o == nil {
  1646  		return a == o
  1647  	}
  1648  
  1649  	if a.Address != o.Address {
  1650  		return false
  1651  	}
  1652  
  1653  	if a.Port != o.Port {
  1654  		return false
  1655  	}
  1656  
  1657  	return true
  1658  }
  1659  
  1660  func (a *ConsulGatewayBindAddress) Copy() *ConsulGatewayBindAddress {
  1661  	if a == nil {
  1662  		return nil
  1663  	}
  1664  
  1665  	return &ConsulGatewayBindAddress{
  1666  		Address: a.Address,
  1667  		Port:    a.Port,
  1668  	}
  1669  }
  1670  
  1671  func (a *ConsulGatewayBindAddress) Validate() error {
  1672  	if a == nil {
  1673  		return nil
  1674  	}
  1675  
  1676  	if a.Address == "" {
  1677  		return fmt.Errorf("Consul Gateway Bind Address must be set")
  1678  	}
  1679  
  1680  	if a.Port <= 0 && a.Port != -1 { // port -1 => nomad autofill
  1681  		return fmt.Errorf("Consul Gateway Bind Address must set valid Port")
  1682  	}
  1683  
  1684  	return nil
  1685  }
  1686  
  1687  // ConsulGatewayProxy is used to tune parameters of the proxy instance acting as
  1688  // one of the forms of Connect gateways that Consul supports.
  1689  //
  1690  // https://www.consul.io/docs/connect/proxies/envoy#gateway-options
  1691  type ConsulGatewayProxy struct {
  1692  	ConnectTimeout                  *time.Duration
  1693  	EnvoyGatewayBindTaggedAddresses bool
  1694  	EnvoyGatewayBindAddresses       map[string]*ConsulGatewayBindAddress
  1695  	EnvoyGatewayNoDefaultBind       bool
  1696  	EnvoyDNSDiscoveryType           string
  1697  	Config                          map[string]interface{}
  1698  }
  1699  
  1700  func (p *ConsulGatewayProxy) Copy() *ConsulGatewayProxy {
  1701  	if p == nil {
  1702  		return nil
  1703  	}
  1704  
  1705  	return &ConsulGatewayProxy{
  1706  		ConnectTimeout:                  pointer.Of(*p.ConnectTimeout),
  1707  		EnvoyGatewayBindTaggedAddresses: p.EnvoyGatewayBindTaggedAddresses,
  1708  		EnvoyGatewayBindAddresses:       p.copyBindAddresses(),
  1709  		EnvoyGatewayNoDefaultBind:       p.EnvoyGatewayNoDefaultBind,
  1710  		EnvoyDNSDiscoveryType:           p.EnvoyDNSDiscoveryType,
  1711  		Config:                          maps.Clone(p.Config),
  1712  	}
  1713  }
  1714  
  1715  func (p *ConsulGatewayProxy) copyBindAddresses() map[string]*ConsulGatewayBindAddress {
  1716  	if p.EnvoyGatewayBindAddresses == nil {
  1717  		return nil
  1718  	}
  1719  
  1720  	bindAddresses := make(map[string]*ConsulGatewayBindAddress, len(p.EnvoyGatewayBindAddresses))
  1721  	for k, v := range p.EnvoyGatewayBindAddresses {
  1722  		bindAddresses[k] = v.Copy()
  1723  	}
  1724  
  1725  	return bindAddresses
  1726  }
  1727  
  1728  func (p *ConsulGatewayProxy) equalBindAddresses(o map[string]*ConsulGatewayBindAddress) bool {
  1729  	if len(p.EnvoyGatewayBindAddresses) != len(o) {
  1730  		return false
  1731  	}
  1732  
  1733  	for listener, addr := range p.EnvoyGatewayBindAddresses {
  1734  		if !o[listener].Equal(addr) {
  1735  			return false
  1736  		}
  1737  	}
  1738  
  1739  	return true
  1740  }
  1741  
  1742  func (p *ConsulGatewayProxy) Equal(o *ConsulGatewayProxy) bool {
  1743  	if p == nil || o == nil {
  1744  		return p == o
  1745  	}
  1746  
  1747  	if !pointer.Eq(p.ConnectTimeout, o.ConnectTimeout) {
  1748  		return false
  1749  	}
  1750  
  1751  	if p.EnvoyGatewayBindTaggedAddresses != o.EnvoyGatewayBindTaggedAddresses {
  1752  		return false
  1753  	}
  1754  
  1755  	if !p.equalBindAddresses(o.EnvoyGatewayBindAddresses) {
  1756  		return false
  1757  	}
  1758  
  1759  	if p.EnvoyGatewayNoDefaultBind != o.EnvoyGatewayNoDefaultBind {
  1760  		return false
  1761  	}
  1762  
  1763  	if p.EnvoyDNSDiscoveryType != o.EnvoyDNSDiscoveryType {
  1764  		return false
  1765  	}
  1766  
  1767  	if !opaqueMapsEqual(p.Config, o.Config) {
  1768  		return false
  1769  	}
  1770  
  1771  	return true
  1772  }
  1773  
  1774  const (
  1775  	strictDNS  = "STRICT_DNS"
  1776  	logicalDNS = "LOGICAL_DNS"
  1777  )
  1778  
  1779  func (p *ConsulGatewayProxy) Validate() error {
  1780  	if p == nil {
  1781  		return nil
  1782  	}
  1783  
  1784  	if p.ConnectTimeout == nil {
  1785  		return fmt.Errorf("Consul Gateway Proxy connection_timeout must be set")
  1786  	}
  1787  
  1788  	switch p.EnvoyDNSDiscoveryType {
  1789  	case "", strictDNS, logicalDNS:
  1790  		// Consul defaults to logical DNS, suitable for large scale workloads.
  1791  		// https://www.envoyproxy.io/docs/envoy/v1.16.1/intro/arch_overview/upstream/service_discovery
  1792  	default:
  1793  		return fmt.Errorf("Consul Gateway Proxy Envoy DNS Discovery type must be %s or %s", strictDNS, logicalDNS)
  1794  	}
  1795  
  1796  	for _, bindAddr := range p.EnvoyGatewayBindAddresses {
  1797  		if err := bindAddr.Validate(); err != nil {
  1798  			return err
  1799  		}
  1800  	}
  1801  
  1802  	return nil
  1803  }
  1804  
  1805  // ConsulGatewayTLSConfig is used to configure TLS for a gateway.
  1806  type ConsulGatewayTLSConfig struct {
  1807  	Enabled       bool
  1808  	TLSMinVersion string
  1809  	TLSMaxVersion string
  1810  	CipherSuites  []string
  1811  }
  1812  
  1813  func (c *ConsulGatewayTLSConfig) Copy() *ConsulGatewayTLSConfig {
  1814  	if c == nil {
  1815  		return nil
  1816  	}
  1817  
  1818  	return &ConsulGatewayTLSConfig{
  1819  		Enabled:       c.Enabled,
  1820  		TLSMinVersion: c.TLSMinVersion,
  1821  		TLSMaxVersion: c.TLSMaxVersion,
  1822  		CipherSuites:  slices.Clone(c.CipherSuites),
  1823  	}
  1824  }
  1825  
  1826  func (c *ConsulGatewayTLSConfig) Equal(o *ConsulGatewayTLSConfig) bool {
  1827  	if c == nil || o == nil {
  1828  		return c == o
  1829  	}
  1830  
  1831  	return c.Enabled == o.Enabled &&
  1832  		c.TLSMinVersion == o.TLSMinVersion &&
  1833  		c.TLSMaxVersion == o.TLSMaxVersion &&
  1834  		helper.SliceSetEq(c.CipherSuites, o.CipherSuites)
  1835  }
  1836  
  1837  // ConsulIngressService is used to configure a service fronted by the ingress gateway.
  1838  type ConsulIngressService struct {
  1839  	Name  string
  1840  	Hosts []string
  1841  }
  1842  
  1843  func (s *ConsulIngressService) Copy() *ConsulIngressService {
  1844  	if s == nil {
  1845  		return nil
  1846  	}
  1847  
  1848  	var hosts []string = nil
  1849  	if n := len(s.Hosts); n > 0 {
  1850  		hosts = make([]string, n)
  1851  		copy(hosts, s.Hosts)
  1852  	}
  1853  
  1854  	return &ConsulIngressService{
  1855  		Name:  s.Name,
  1856  		Hosts: hosts,
  1857  	}
  1858  }
  1859  
  1860  func (s *ConsulIngressService) Equal(o *ConsulIngressService) bool {
  1861  	if s == nil || o == nil {
  1862  		return s == o
  1863  	}
  1864  
  1865  	if s.Name != o.Name {
  1866  		return false
  1867  	}
  1868  
  1869  	return helper.SliceSetEq(s.Hosts, o.Hosts)
  1870  }
  1871  
  1872  func (s *ConsulIngressService) Validate(protocol string) error {
  1873  	if s == nil {
  1874  		return nil
  1875  	}
  1876  
  1877  	if s.Name == "" {
  1878  		return errors.New("Consul Ingress Service requires a name")
  1879  	}
  1880  
  1881  	// Validation of wildcard service name and hosts varies depending on the
  1882  	// protocol for the gateway.
  1883  	// https://www.consul.io/docs/connect/config-entries/ingress-gateway#hosts
  1884  	switch protocol {
  1885  	case "tcp":
  1886  		if s.Name == "*" {
  1887  			return errors.New(`Consul Ingress Service doesn't support wildcard name for "tcp" protocol`)
  1888  		}
  1889  
  1890  		if len(s.Hosts) != 0 {
  1891  			return errors.New(`Consul Ingress Service doesn't support associating hosts to a service for the "tcp" protocol`)
  1892  		}
  1893  	default:
  1894  		if s.Name == "*" {
  1895  			return nil
  1896  		}
  1897  
  1898  		if len(s.Hosts) == 0 {
  1899  			return fmt.Errorf("Consul Ingress Service requires one or more hosts when using %q protocol", protocol)
  1900  		}
  1901  	}
  1902  
  1903  	return nil
  1904  }
  1905  
  1906  // ConsulIngressListener is used to configure a listener on a Consul Ingress
  1907  // Gateway.
  1908  type ConsulIngressListener struct {
  1909  	Port     int
  1910  	Protocol string
  1911  	Services []*ConsulIngressService
  1912  }
  1913  
  1914  func (l *ConsulIngressListener) Copy() *ConsulIngressListener {
  1915  	if l == nil {
  1916  		return nil
  1917  	}
  1918  
  1919  	var services []*ConsulIngressService = nil
  1920  	if n := len(l.Services); n > 0 {
  1921  		services = make([]*ConsulIngressService, n)
  1922  		for i := 0; i < n; i++ {
  1923  			services[i] = l.Services[i].Copy()
  1924  		}
  1925  	}
  1926  
  1927  	return &ConsulIngressListener{
  1928  		Port:     l.Port,
  1929  		Protocol: l.Protocol,
  1930  		Services: services,
  1931  	}
  1932  }
  1933  
  1934  func (l *ConsulIngressListener) Equal(o *ConsulIngressListener) bool {
  1935  	if l == nil || o == nil {
  1936  		return l == o
  1937  	}
  1938  
  1939  	if l.Port != o.Port {
  1940  		return false
  1941  	}
  1942  
  1943  	if l.Protocol != o.Protocol {
  1944  		return false
  1945  	}
  1946  
  1947  	return ingressServicesEqual(l.Services, o.Services)
  1948  }
  1949  
  1950  func (l *ConsulIngressListener) Validate() error {
  1951  	if l == nil {
  1952  		return nil
  1953  	}
  1954  
  1955  	if l.Port <= 0 {
  1956  		return fmt.Errorf("Consul Ingress Listener requires valid Port")
  1957  	}
  1958  
  1959  	protocols := []string{"tcp", "http", "http2", "grpc"}
  1960  	if !slices.Contains(protocols, l.Protocol) {
  1961  		return fmt.Errorf(`Consul Ingress Listener requires protocol of %s, got %q`, strings.Join(protocols, ", "), l.Protocol)
  1962  	}
  1963  
  1964  	if len(l.Services) == 0 {
  1965  		return fmt.Errorf("Consul Ingress Listener requires one or more services")
  1966  	}
  1967  
  1968  	for _, service := range l.Services {
  1969  		if err := service.Validate(l.Protocol); err != nil {
  1970  			return err
  1971  		}
  1972  	}
  1973  
  1974  	return nil
  1975  }
  1976  
  1977  func ingressServicesEqual(a, b []*ConsulIngressService) bool {
  1978  	return helper.ElementsEqual(a, b)
  1979  }
  1980  
  1981  // ConsulIngressConfigEntry represents the Consul Configuration Entry type for
  1982  // an Ingress Gateway.
  1983  //
  1984  // https://www.consul.io/docs/agent/config-entries/ingress-gateway#available-fields
  1985  type ConsulIngressConfigEntry struct {
  1986  	TLS       *ConsulGatewayTLSConfig
  1987  	Listeners []*ConsulIngressListener
  1988  }
  1989  
  1990  func (e *ConsulIngressConfigEntry) Copy() *ConsulIngressConfigEntry {
  1991  	if e == nil {
  1992  		return nil
  1993  	}
  1994  
  1995  	var listeners []*ConsulIngressListener = nil
  1996  	if n := len(e.Listeners); n > 0 {
  1997  		listeners = make([]*ConsulIngressListener, n)
  1998  		for i := 0; i < n; i++ {
  1999  			listeners[i] = e.Listeners[i].Copy()
  2000  		}
  2001  	}
  2002  
  2003  	return &ConsulIngressConfigEntry{
  2004  		TLS:       e.TLS.Copy(),
  2005  		Listeners: listeners,
  2006  	}
  2007  }
  2008  
  2009  func (e *ConsulIngressConfigEntry) Equal(o *ConsulIngressConfigEntry) bool {
  2010  	if e == nil || o == nil {
  2011  		return e == o
  2012  	}
  2013  
  2014  	if !e.TLS.Equal(o.TLS) {
  2015  		return false
  2016  	}
  2017  
  2018  	return ingressListenersEqual(e.Listeners, o.Listeners)
  2019  }
  2020  
  2021  func (e *ConsulIngressConfigEntry) Validate() error {
  2022  	if e == nil {
  2023  		return nil
  2024  	}
  2025  
  2026  	if len(e.Listeners) == 0 {
  2027  		return fmt.Errorf("Consul Ingress Gateway requires at least one listener")
  2028  	}
  2029  
  2030  	for _, listener := range e.Listeners {
  2031  		if err := listener.Validate(); err != nil {
  2032  			return err
  2033  		}
  2034  	}
  2035  
  2036  	return nil
  2037  }
  2038  
  2039  func ingressListenersEqual(a, b []*ConsulIngressListener) bool {
  2040  	return helper.ElementsEqual(a, b)
  2041  }
  2042  
  2043  type ConsulLinkedService struct {
  2044  	Name     string
  2045  	CAFile   string
  2046  	CertFile string
  2047  	KeyFile  string
  2048  	SNI      string
  2049  }
  2050  
  2051  func (s *ConsulLinkedService) Copy() *ConsulLinkedService {
  2052  	if s == nil {
  2053  		return nil
  2054  	}
  2055  
  2056  	return &ConsulLinkedService{
  2057  		Name:     s.Name,
  2058  		CAFile:   s.CAFile,
  2059  		CertFile: s.CertFile,
  2060  		KeyFile:  s.KeyFile,
  2061  		SNI:      s.SNI,
  2062  	}
  2063  }
  2064  
  2065  func (s *ConsulLinkedService) Equal(o *ConsulLinkedService) bool {
  2066  	if s == nil || o == nil {
  2067  		return s == o
  2068  	}
  2069  
  2070  	switch {
  2071  	case s.Name != o.Name:
  2072  		return false
  2073  	case s.CAFile != o.CAFile:
  2074  		return false
  2075  	case s.CertFile != o.CertFile:
  2076  		return false
  2077  	case s.KeyFile != o.KeyFile:
  2078  		return false
  2079  	case s.SNI != o.SNI:
  2080  		return false
  2081  	}
  2082  
  2083  	return true
  2084  }
  2085  
  2086  func (s *ConsulLinkedService) Validate() error {
  2087  	if s == nil {
  2088  		return nil
  2089  	}
  2090  
  2091  	if s.Name == "" {
  2092  		return fmt.Errorf("Consul Linked Service requires Name")
  2093  	}
  2094  
  2095  	caSet := s.CAFile != ""
  2096  	certSet := s.CertFile != ""
  2097  	keySet := s.KeyFile != ""
  2098  	sniSet := s.SNI != ""
  2099  
  2100  	if (certSet || keySet) && !caSet {
  2101  		return fmt.Errorf("Consul Linked Service TLS requires CAFile")
  2102  	}
  2103  
  2104  	if certSet != keySet {
  2105  		return fmt.Errorf("Consul Linked Service TLS Cert and Key must both be set")
  2106  	}
  2107  
  2108  	if sniSet && !caSet {
  2109  		return fmt.Errorf("Consul Linked Service TLS SNI requires CAFile")
  2110  	}
  2111  
  2112  	return nil
  2113  }
  2114  
  2115  func linkedServicesEqual(a, b []*ConsulLinkedService) bool {
  2116  	return helper.ElementsEqual(a, b)
  2117  }
  2118  
  2119  type ConsulTerminatingConfigEntry struct {
  2120  	Services []*ConsulLinkedService
  2121  }
  2122  
  2123  func (e *ConsulTerminatingConfigEntry) Copy() *ConsulTerminatingConfigEntry {
  2124  	if e == nil {
  2125  		return nil
  2126  	}
  2127  
  2128  	var services []*ConsulLinkedService = nil
  2129  	if n := len(e.Services); n > 0 {
  2130  		services = make([]*ConsulLinkedService, n)
  2131  		for i := 0; i < n; i++ {
  2132  			services[i] = e.Services[i].Copy()
  2133  		}
  2134  	}
  2135  
  2136  	return &ConsulTerminatingConfigEntry{
  2137  		Services: services,
  2138  	}
  2139  }
  2140  
  2141  func (e *ConsulTerminatingConfigEntry) Equal(o *ConsulTerminatingConfigEntry) bool {
  2142  	if e == nil || o == nil {
  2143  		return e == o
  2144  	}
  2145  
  2146  	return linkedServicesEqual(e.Services, o.Services)
  2147  }
  2148  
  2149  func (e *ConsulTerminatingConfigEntry) Validate() error {
  2150  	if e == nil {
  2151  		return nil
  2152  	}
  2153  
  2154  	if len(e.Services) == 0 {
  2155  		return fmt.Errorf("Consul Terminating Gateway requires at least one service")
  2156  	}
  2157  
  2158  	for _, service := range e.Services {
  2159  		if err := service.Validate(); err != nil {
  2160  			return err
  2161  		}
  2162  	}
  2163  
  2164  	return nil
  2165  }
  2166  
  2167  // ConsulMeshConfigEntry is a stub used to represent that the gateway service
  2168  // type should be for a Mesh Gateway. Unlike Ingress and Terminating, there is no
  2169  // dedicated Consul Config Entry type for "mesh-gateway", for now. We still
  2170  // create a type for future proofing, and to keep underlying job-spec marshaling
  2171  // consistent with the other types.
  2172  type ConsulMeshConfigEntry struct {
  2173  	// nothing in here
  2174  }
  2175  
  2176  func (e *ConsulMeshConfigEntry) Copy() *ConsulMeshConfigEntry {
  2177  	if e == nil {
  2178  		return nil
  2179  	}
  2180  	return new(ConsulMeshConfigEntry)
  2181  }
  2182  
  2183  func (e *ConsulMeshConfigEntry) Equal(o *ConsulMeshConfigEntry) bool {
  2184  	if e == nil || o == nil {
  2185  		return e == o
  2186  	}
  2187  	return true
  2188  }
  2189  
  2190  func (e *ConsulMeshConfigEntry) Validate() error {
  2191  	return nil
  2192  }