open-nomad/nomad/structs/services.go
hc-github-team-nomad-core 2ef7a280b0
backport of commit d923fc554d09ceb51b530467a354860b25114fd3 (#18450)
Co-authored-by: James Rasell <jrasell@users.noreply.github.com>
2023-09-11 16:21:44 +01:00

2267 lines
59 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package structs
import (
"crypto/sha1"
"encoding/binary"
"errors"
"fmt"
"hash"
"io"
"net/url"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-set"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/args"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/mitchellh/copystructure"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
const (
EnvoyBootstrapPath = "${NOMAD_SECRETS_DIR}/envoy_bootstrap.json"
ServiceCheckHTTP = "http"
ServiceCheckTCP = "tcp"
ServiceCheckScript = "script"
ServiceCheckGRPC = "grpc"
OnUpdateRequireHealthy = "require_healthy"
OnUpdateIgnoreWarn = "ignore_warnings"
OnUpdateIgnore = "ignore"
// minCheckInterval is the minimum check interval permitted. Consul
// currently has its MinInterval set to 1s. Mirror that here for
// consistency.
minCheckInterval = 1 * time.Second
// minCheckTimeout is the minimum check timeout permitted for Consul
// script TTL checks.
minCheckTimeout = 1 * time.Second
)
// ServiceCheck represents a Nomad or Consul service health check.
//
// The fields available depend on the service provider the check is being
// registered into.
type ServiceCheck struct {
Name string // Name of the check, defaults to a generated label
Type string // Type of the check - tcp, http, docker and script
Command string // Command is the command to run for script checks
Args []string // Args is a list of arguments for script checks
Path string // path of the health check url for http type check
Protocol string // Protocol to use if check is http, defaults to http
PortLabel string // The port to use for tcp/http checks
Expose bool // Whether to have Envoy expose the check path (connect-enabled group-services only)
AddressMode string // Must be empty, "alloc", "host", or "driver"
Interval time.Duration // Interval of the check
Timeout time.Duration // Timeout of the response from the check before consul fails the check
InitialStatus string // Initial status of the check
TLSServerName string // ServerName to use for SNI and TLS verification when (Type=https and Protocol=https) or (Type=grpc and GRPCUseTLS=true)
TLSSkipVerify bool // Skip TLS verification when (type=https and Protocol=https) or (type=grpc and grpc_use_tls=true)
Method string // HTTP Method to use (GET by default)
Header map[string][]string // HTTP Headers for Consul to set when making HTTP checks
CheckRestart *CheckRestart // If and when a task should be restarted based on checks
GRPCService string // Service for GRPC checks
GRPCUseTLS bool // Whether or not to use TLS for GRPC checks
TaskName string // What task to execute this check in
SuccessBeforePassing int // Number of consecutive successes required before considered healthy
FailuresBeforeCritical int // Number of consecutive failures required before considered unhealthy
Body string // Body to use in HTTP check
OnUpdate string
}
// IsReadiness returns whether the configuration of the ServiceCheck is effectively
// a readiness check - i.e. check failures do not affect a deployment.
func (sc *ServiceCheck) IsReadiness() bool {
return sc != nil && sc.OnUpdate == OnUpdateIgnore
}
// Copy the block recursively. Returns nil if nil.
func (sc *ServiceCheck) Copy() *ServiceCheck {
if sc == nil {
return nil
}
nsc := new(ServiceCheck)
*nsc = *sc
nsc.Args = slices.Clone(sc.Args)
nsc.Header = helper.CopyMapOfSlice(sc.Header)
nsc.CheckRestart = sc.CheckRestart.Copy()
return nsc
}
// Equal returns true if the structs are recursively equal.
func (sc *ServiceCheck) Equal(o *ServiceCheck) bool {
if sc == nil || o == nil {
return sc == o
}
if sc.Name != o.Name {
return false
}
if sc.AddressMode != o.AddressMode {
return false
}
if !helper.SliceSetEq(sc.Args, o.Args) {
return false
}
if !sc.CheckRestart.Equal(o.CheckRestart) {
return false
}
if sc.TaskName != o.TaskName {
return false
}
if sc.SuccessBeforePassing != o.SuccessBeforePassing {
return false
}
if sc.FailuresBeforeCritical != o.FailuresBeforeCritical {
return false
}
if sc.Command != o.Command {
return false
}
if sc.GRPCService != o.GRPCService {
return false
}
if sc.GRPCUseTLS != o.GRPCUseTLS {
return false
}
// Use DeepEqual here as order of slice values could matter
if !reflect.DeepEqual(sc.Header, o.Header) {
return false
}
if sc.InitialStatus != o.InitialStatus {
return false
}
if sc.Interval != o.Interval {
return false
}
if sc.Method != o.Method {
return false
}
if sc.Path != o.Path {
return false
}
if sc.PortLabel != o.Path {
return false
}
if sc.Expose != o.Expose {
return false
}
if sc.Protocol != o.Protocol {
return false
}
if sc.TLSSkipVerify != o.TLSSkipVerify {
return false
}
if sc.TLSServerName != o.TLSServerName {
return false
}
if sc.Timeout != o.Timeout {
return false
}
if sc.Type != o.Type {
return false
}
if sc.Body != o.Body {
return false
}
if sc.OnUpdate != o.OnUpdate {
return false
}
return true
}
func (sc *ServiceCheck) Canonicalize(serviceName, taskName string) {
// Ensure empty maps/slices are treated as null to avoid scheduling
// issues when using DeepEquals.
if len(sc.Args) == 0 {
sc.Args = nil
}
// Ensure empty slices are nil
if len(sc.Header) == 0 {
sc.Header = nil
} else {
for k, v := range sc.Header {
if len(v) == 0 {
sc.Header[k] = nil
}
}
}
// Ensure a default name for the check
if sc.Name == "" {
sc.Name = fmt.Sprintf("service: %q check", serviceName)
}
// Set task name if not already set
if sc.TaskName == "" && taskName != "group" {
sc.TaskName = taskName
}
// Ensure OnUpdate defaults to require_healthy (i.e. healthiness check)
if sc.OnUpdate == "" {
sc.OnUpdate = OnUpdateRequireHealthy
}
}
// validateCommon validates the parts of ServiceCheck shared across providers.
func (sc *ServiceCheck) validateCommon(allowableTypes []string) error {
// validate the type is allowable (different between nomad, consul checks)
checkType := strings.ToLower(sc.Type)
if !slices.Contains(allowableTypes, checkType) {
s := strings.Join(allowableTypes, ", ")
return fmt.Errorf(`invalid check type (%q), must be one of %s`, checkType, s)
}
// validate specific check types
switch checkType {
case ServiceCheckHTTP:
if sc.Path == "" {
return fmt.Errorf("http type must have http path")
}
checkPath, pathErr := url.Parse(sc.Path)
if pathErr != nil {
return fmt.Errorf("http type must have valid http path")
}
if checkPath.IsAbs() {
return fmt.Errorf("http type must have relative http path")
}
case ServiceCheckScript:
if sc.Command == "" {
return fmt.Errorf("script type must have a valid script path")
}
}
// validate interval
if sc.Interval == 0 {
return fmt.Errorf("missing required value interval. Interval cannot be less than %v", minCheckInterval)
} else if sc.Interval < minCheckInterval {
return fmt.Errorf("interval (%v) cannot be lower than %v", sc.Interval, minCheckInterval)
}
// validate timeout
if sc.Timeout == 0 {
return fmt.Errorf("missing required value timeout. Timeout cannot be less than %v", minCheckInterval)
} else if sc.Timeout < minCheckTimeout {
return fmt.Errorf("timeout (%v) is lower than required minimum timeout %v", sc.Timeout, minCheckInterval)
}
// validate the initial status
switch sc.InitialStatus {
case "":
case api.HealthPassing:
case api.HealthWarning:
case api.HealthCritical:
default:
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)
}
// validate address_mode
switch sc.AddressMode {
case "", AddressModeHost, AddressModeDriver, AddressModeAlloc:
// Ok
case AddressModeAuto:
return fmt.Errorf("invalid address_mode %q - %s only valid for services", sc.AddressMode, AddressModeAuto)
default:
return fmt.Errorf("invalid address_mode %q", sc.AddressMode)
}
// validate on_update
switch sc.OnUpdate {
case "", OnUpdateIgnore, OnUpdateRequireHealthy, OnUpdateIgnoreWarn:
// OK
default:
return fmt.Errorf("on_update must be %q, %q, or %q; got %q", OnUpdateRequireHealthy, OnUpdateIgnoreWarn, OnUpdateIgnore, sc.OnUpdate)
}
// validate check_restart and on_update do not conflict
if sc.CheckRestart != nil {
// CheckRestart and OnUpdate Ignore are incompatible If OnUpdate treats
// an error has healthy, and the deployment succeeds followed by check
// restart restarting failing checks, the deployment is left in an odd
// state
if sc.OnUpdate == OnUpdateIgnore {
return fmt.Errorf("on_update value %q is not compatible with check_restart", sc.OnUpdate)
}
// CheckRestart IgnoreWarnings must be true if a check has defined OnUpdate
// ignore_warnings
if !sc.CheckRestart.IgnoreWarnings && sc.OnUpdate == OnUpdateIgnoreWarn {
return fmt.Errorf("on_update value %q not supported with check_restart ignore_warnings value %q", sc.OnUpdate, strconv.FormatBool(sc.CheckRestart.IgnoreWarnings))
}
}
// validate check_restart
if err := sc.CheckRestart.Validate(); err != nil {
return err
}
return nil
}
// validate a Service's ServiceCheck in the context of the Nomad provider.
func (sc *ServiceCheck) validateNomad() error {
allowable := []string{ServiceCheckTCP, ServiceCheckHTTP}
if err := sc.validateCommon(allowable); err != nil {
return err
}
// expose is connect (consul) specific
if sc.Expose {
return errors.New("expose may only be set for Consul service checks")
}
// nomad checks do not have warnings
if sc.OnUpdate == OnUpdateIgnoreWarn {
return errors.New("on_update may only be set to ignore_warnings for Consul service checks")
}
// below are temporary limitations on checks in nomad
// https://github.com/hashicorp/team-nomad/issues/354
// check_restart.ignore_warnings is not a thing in Nomad (which has no warnings in checks)
if sc.CheckRestart != nil {
if sc.CheckRestart.IgnoreWarnings {
return errors.New("ignore_warnings on check_restart only supported for Consul service checks")
}
}
// address_mode="driver" not yet supported on nomad
if sc.AddressMode == "driver" {
return errors.New("address_mode = driver may only be set for Consul service checks")
}
if sc.Type == "http" {
if sc.Method != "" && !helper.IsMethodHTTP(sc.Method) {
return fmt.Errorf("method type %q not supported in Nomad http check", sc.Method)
}
}
// success_before_passing is consul only
if sc.SuccessBeforePassing != 0 {
return errors.New("success_before_passing may only be set for Consul service checks")
}
// failures_before_critical is consul only
if sc.FailuresBeforeCritical != 0 {
return errors.New("failures_before_critical may only be set for Consul service checks")
}
// tls_server_name is consul only
if sc.TLSServerName != "" {
return errors.New("tls_server_name may only be set for Consul service checks")
}
// tls_skip_verify is consul only
if sc.TLSSkipVerify {
return errors.New("tls_skip_verify may only be set for Consul service checks")
}
return nil
}
// validate a Service's ServiceCheck in the context of the Consul provider.
func (sc *ServiceCheck) validateConsul() error {
allowable := []string{ServiceCheckGRPC, ServiceCheckTCP, ServiceCheckHTTP, ServiceCheckScript}
if err := sc.validateCommon(allowable); err != nil {
return err
}
checkType := strings.ToLower(sc.Type)
// Note that we cannot completely validate the Expose field yet - we do not
// know whether this ServiceCheck belongs to a connect-enabled group-service.
// Instead, such validation will happen in a job admission controller.
//
// Consul only.
if sc.Expose {
// We can however immediately ensure expose is configured only for HTTP
// and gRPC checks.
switch checkType {
case ServiceCheckGRPC, ServiceCheckHTTP: // ok
default:
return fmt.Errorf("expose may only be set on HTTP or gRPC checks")
}
}
// passFailCheckTypes are intersection of check types supported by both Consul
// and Nomad when using the pass/fail check threshold features.
//
// Consul only.
passFailCheckTypes := []string{"tcp", "http", "grpc"}
if sc.SuccessBeforePassing < 0 {
return fmt.Errorf("success_before_passing must be non-negative")
} else if sc.SuccessBeforePassing > 0 && !slices.Contains(passFailCheckTypes, sc.Type) {
return fmt.Errorf("success_before_passing not supported for check of type %q", sc.Type)
}
if sc.FailuresBeforeCritical < 0 {
return fmt.Errorf("failures_before_critical must be non-negative")
} else if sc.FailuresBeforeCritical > 0 && !slices.Contains(passFailCheckTypes, sc.Type) {
return fmt.Errorf("failures_before_critical not supported for check of type %q", sc.Type)
}
return nil
}
// RequiresPort returns whether the service check requires the task has a port.
func (sc *ServiceCheck) RequiresPort() bool {
switch sc.Type {
case ServiceCheckGRPC, ServiceCheckHTTP, ServiceCheckTCP:
return true
default:
return false
}
}
// TriggersRestarts returns true if this check should be watched and trigger a restart
// on failure.
func (sc *ServiceCheck) TriggersRestarts() bool {
return sc.CheckRestart != nil && sc.CheckRestart.Limit > 0
}
// Hash all ServiceCheck fields and the check's corresponding service ID to
// create an identifier. The identifier is not guaranteed to be unique as if
// the PortLabel is blank, the Service's PortLabel will be used after Hash is
// called.
func (sc *ServiceCheck) Hash(serviceID string) string {
h := sha1.New()
hashString(h, serviceID)
hashString(h, sc.Name)
hashString(h, sc.Type)
hashString(h, sc.Command)
hashString(h, strings.Join(sc.Args, ""))
hashString(h, sc.Path)
hashString(h, sc.Protocol)
hashString(h, sc.PortLabel)
hashString(h, sc.Interval.String())
hashString(h, sc.Timeout.String())
hashString(h, sc.Method)
hashString(h, sc.Body)
hashString(h, sc.OnUpdate)
// use name "true" to maintain ID stability
hashBool(h, sc.TLSSkipVerify, "true")
// Only include TLSServerName if set to maintain ID stability with Nomad <1.6.0
hashStringIfNonEmpty(h, sc.TLSServerName)
// maintain artisanal map hashing to maintain ID stability
hashHeader(h, sc.Header)
// Only include AddressMode if set to maintain ID stability with Nomad <0.7.1
hashStringIfNonEmpty(h, sc.AddressMode)
// Only include gRPC if set to maintain ID stability with Nomad <0.8.4
hashStringIfNonEmpty(h, sc.GRPCService)
// use name "true" to maintain ID stability
hashBool(h, sc.GRPCUseTLS, "true")
// Only include pass/fail if non-zero to maintain ID stability with Nomad < 0.12
hashIntIfNonZero(h, "success", sc.SuccessBeforePassing)
hashIntIfNonZero(h, "failures", sc.FailuresBeforeCritical)
// Hash is used for diffing against the Consul check definition, which does
// not have an expose parameter. Instead we rely on implied changes to
// other fields if the Expose setting is changed in a nomad service.
// hashBool(h, sc.Expose, "Expose")
// maintain use of hex (i.e. not b32) to maintain ID stability
return fmt.Sprintf("%x", h.Sum(nil))
}
func hashStringIfNonEmpty(h hash.Hash, s string) {
if len(s) > 0 {
hashString(h, s)
}
}
func hashIntIfNonZero(h hash.Hash, name string, i int) {
if i != 0 {
hashString(h, fmt.Sprintf("%s:%d", name, i))
}
}
func hashDuration(h hash.Hash, dur time.Duration) {
_ = binary.Write(h, binary.LittleEndian, dur)
}
func hashHeader(h hash.Hash, m map[string][]string) {
// maintain backwards compatibility for ID stability
// using the %v formatter on a map with string keys produces consistent
// output, but our existing format here is incompatible
if len(m) > 0 {
headers := make([]string, 0, len(m))
for k, v := range m {
headers = append(headers, k+strings.Join(v, ""))
}
sort.Strings(headers)
hashString(h, strings.Join(headers, ""))
}
}
const (
AddressModeAuto = "auto"
AddressModeHost = "host"
AddressModeDriver = "driver"
AddressModeAlloc = "alloc"
// ServiceProviderConsul is the default service provider and the way Nomad
// worked before native service discovery.
ServiceProviderConsul = "consul"
// ServiceProviderNomad is the native service discovery provider. At the
// time of writing, there are a number of restrictions around its
// functionality and use.
ServiceProviderNomad = "nomad"
)
// Service represents a Consul service definition
type Service struct {
// Name of the service registered with Consul. Consul defaults the
// Name to ServiceID if not specified. The Name if specified is used
// as one of the seed values when generating a Consul ServiceID.
Name string
// Name of the Task associated with this service.
// Group services do not have a task name, unless they are a connect native
// service specifying the task implementing the service.
// Task-level services automatically have the task name plumbed through
// down to checks for convenience.
TaskName string
// PortLabel is either the numeric port number or the `host:port`.
// To specify the port number using the host's Consul Advertise
// address, specify an empty host in the PortLabel (e.g. `:port`).
PortLabel string
// AddressMode specifies how the address in service registration is
// determined. Must be "auto" (default), "host", "driver", or "alloc".
AddressMode string
// Address enables explicitly setting a custom address to use in service
// registration. AddressMode must be "auto" if Address is set.
Address string
// EnableTagOverride will disable Consul's anti-entropy mechanism for the
// tags of this service. External updates to the service definition via
// Consul will not be corrected to match the service definition set in the
// Nomad job specification.
//
// https://www.consul.io/docs/agent/services.html#service-definition
EnableTagOverride bool
Tags []string // List of tags for the service
CanaryTags []string // List of tags for the service when it is a canary
Checks []*ServiceCheck // List of checks associated with the service
Connect *ConsulConnect // Consul Connect configuration
Meta map[string]string // Consul service meta
CanaryMeta map[string]string // Consul service meta when it is a canary
// The values to set for tagged_addresses in Consul service registration.
// Does not affect Nomad networking, these are for Consul service discovery.
TaggedAddresses map[string]string
// The consul namespace in which this service will be registered. Namespace
// at the service.check level is not part of the Nomad API - it must be
// set at the job or group level. This field is managed internally so
// that Hash can work correctly.
Namespace string
// OnUpdate Specifies how the service and its checks should be evaluated
// during an update
OnUpdate string
// Provider dictates which service discovery provider to use. This can be
// either ServiceProviderConsul or ServiceProviderNomad and defaults to the former when
// left empty by the operator.
Provider string
}
// Copy the block recursively. Returns nil if nil.
func (s *Service) Copy() *Service {
if s == nil {
return nil
}
ns := new(Service)
*ns = *s
ns.Tags = slices.Clone(ns.Tags)
ns.CanaryTags = slices.Clone(ns.CanaryTags)
if s.Checks != nil {
checks := make([]*ServiceCheck, len(ns.Checks))
for i, c := range ns.Checks {
checks[i] = c.Copy()
}
ns.Checks = checks
}
ns.Connect = s.Connect.Copy()
ns.Meta = maps.Clone(s.Meta)
ns.CanaryMeta = maps.Clone(s.CanaryMeta)
ns.TaggedAddresses = maps.Clone(s.TaggedAddresses)
return ns
}
// Canonicalize interpolates values of Job, Task Group and Task in the Service
// Name. This also generates check names, service id and check ids.
func (s *Service) Canonicalize(job, taskGroup, task, jobNamespace string) {
// Ensure empty lists are treated as null to avoid scheduler issues when
// using DeepEquals
if len(s.Tags) == 0 {
s.Tags = nil
}
if len(s.CanaryTags) == 0 {
s.CanaryTags = nil
}
if len(s.Checks) == 0 {
s.Checks = nil
}
if len(s.TaggedAddresses) == 0 {
s.TaggedAddresses = nil
}
// Set the task name if not already set
if s.TaskName == "" && task != "group" {
s.TaskName = task
}
s.Name = args.ReplaceEnv(s.Name, map[string]string{
"JOB": job,
"TASKGROUP": taskGroup,
"TASK": task,
"BASE": fmt.Sprintf("%s-%s-%s", job, taskGroup, task),
})
for _, check := range s.Checks {
check.Canonicalize(s.Name, s.TaskName)
}
// Set the provider to its default value. The value of consul ensures this
// new feature and parameter behaves in the same manner a previous versions
// which did not include this.
if s.Provider == "" {
s.Provider = ServiceProviderConsul
}
// Consul API returns "default" whether the namespace is empty or set as
// such, so we coerce our copy of the service to be the same if using the
// consul provider.
//
// When using ServiceProviderNomad, set the namespace to that of the job. This
// makes modifications and diffs on the service correct.
if s.Namespace == "" && s.Provider == ServiceProviderConsul {
s.Namespace = "default"
} else if s.Provider == ServiceProviderNomad {
s.Namespace = jobNamespace
}
}
// Validate checks if the Service definition is valid
func (s *Service) Validate() error {
var mErr multierror.Error
// Ensure the service name is valid per the below RFCs but make an exception
// for our interpolation syntax by first stripping any environment variables from the name
serviceNameStripped := args.ReplaceEnvWithPlaceHolder(s.Name, "ENV-VAR")
if err := s.ValidateName(serviceNameStripped); err != nil {
// Log actual service name, not the stripped version.
mErr.Errors = append(mErr.Errors, fmt.Errorf("%v: %q", err, s.Name))
}
switch s.AddressMode {
case "", AddressModeAuto:
case AddressModeHost, AddressModeDriver, AddressModeAlloc:
if s.Address != "" {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Service address_mode must be %q if address is set", AddressModeAuto))
}
default:
mErr.Errors = append(mErr.Errors, fmt.Errorf("Service address_mode must be %q, %q, or %q; not %q", AddressModeAuto, AddressModeHost, AddressModeDriver, s.AddressMode))
}
switch s.OnUpdate {
case "", OnUpdateIgnore, OnUpdateRequireHealthy, OnUpdateIgnoreWarn:
// OK
default:
mErr.Errors = append(mErr.Errors, fmt.Errorf("Service on_update must be %q, %q, or %q; not %q", OnUpdateRequireHealthy, OnUpdateIgnoreWarn, OnUpdateIgnore, s.OnUpdate))
}
// Up until this point, all service validation has been independent of the
// provider. From this point on, we have different validation paths. We can
// also catch an incorrect provider parameter.
switch s.Provider {
case ServiceProviderConsul:
s.validateConsulService(&mErr)
case ServiceProviderNomad:
s.validateNomadService(&mErr)
default:
mErr.Errors = append(mErr.Errors, fmt.Errorf("Service provider must be %q, or %q; not %q",
ServiceProviderConsul, ServiceProviderNomad, s.Provider))
}
return mErr.ErrorOrNil()
}
func (s *Service) validateCheckPort(c *ServiceCheck) error {
if s.PortLabel == "" && c.PortLabel == "" && c.RequiresPort() {
return fmt.Errorf("Check %s invalid: check requires a port but neither check nor service %+q have a port", c.Name, s.Name)
}
return nil
}
// validateConsulService performs validation on a service which is using the
// consul provider.
func (s *Service) validateConsulService(mErr *multierror.Error) {
// check checks
for _, c := range s.Checks {
// validat ethe check port
if err := s.validateCheckPort(c); err != nil {
mErr.Errors = append(mErr.Errors, err)
continue
}
// TCP checks against a Consul Connect enabled service are not supported
// due to the service being bound to the loopback interface inside the
// network namespace
if c.Type == ServiceCheckTCP && s.Connect != nil && s.Connect.SidecarService != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s invalid: tcp checks are not valid for Connect enabled services", c.Name))
continue
}
// validate the consul check
if err := c.validateConsul(); err != nil {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s invalid: %v", c.Name, err))
}
}
// check connect
if s.Connect != nil {
if err := s.Connect.Validate(); err != nil {
mErr.Errors = append(mErr.Errors, err)
}
// if service is connect native, service task must be set (which may
// happen implicitly in a job mutation if there is only one task)
if s.Connect.IsNative() && len(s.TaskName) == 0 {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Service %s is Connect Native and requires setting the task", s.Name))
}
}
}
// validateNomadService performs validation on a service which is using the
// nomad provider.
func (s *Service) validateNomadService(mErr *multierror.Error) {
// check checks
for _, c := range s.Checks {
// validate the check port
if err := s.validateCheckPort(c); err != nil {
mErr.Errors = append(mErr.Errors, err)
continue
}
// validate the nomad check
if err := c.validateNomad(); err != nil {
mErr.Errors = append(mErr.Errors, err)
}
}
// Services using the Nomad provider do not support Consul connect.
if s.Connect != nil {
mErr.Errors = append(mErr.Errors, errors.New("Service with provider nomad cannot include Connect blocks"))
}
}
// ValidateName checks if the service Name is valid and should be called after
// the name has been interpolated
func (s *Service) ValidateName(name string) error {
// Ensure the service name is valid per RFC-952 §1
// (https://tools.ietf.org/html/rfc952), RFC-1123 §2.1
// (https://tools.ietf.org/html/rfc1123), and RFC-2782
// (https://tools.ietf.org/html/rfc2782).
// This validation is enforced on Nomad, but not on Consul, however if
// consul-template is being used, service names with dots in them wont be
// admissible.
re := regexp.MustCompile(`^(?i:[a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])$`)
if !re.MatchString(name) {
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")
}
return nil
}
// Hash returns a base32 encoded hash of a Service's contents excluding checks
// as they're hashed independently and the provider in order to not cause churn
// during cluster upgrades.
func (s *Service) Hash(allocID, taskName string, canary bool) string {
h := sha1.New()
hashString(h, allocID)
hashString(h, taskName)
hashString(h, s.Name)
hashString(h, s.PortLabel)
hashString(h, s.AddressMode)
hashString(h, s.Address)
hashTags(h, s.Tags)
hashTags(h, s.CanaryTags)
hashBool(h, canary, "Canary")
hashBool(h, s.EnableTagOverride, "ETO")
hashMeta(h, s.Meta)
hashMeta(h, s.CanaryMeta)
hashMeta(h, s.TaggedAddresses)
hashConnect(h, s.Connect)
hashString(h, s.OnUpdate)
hashString(h, s.Namespace)
// Don't hash the provider parameter, so we don't cause churn of all
// registered services when upgrading Nomad versions. The provider is not
// used at the level the hash is and therefore is not needed to tell
// whether the service has changed.
// Base32 is used for encoding the hash as sha1 hashes can always be
// encoded without padding, only 4 bytes larger than base64, and saves
// 8 bytes vs hex. Since these hashes are used in Consul URLs it's nice
// to have a reasonably compact URL-safe representation.
return b32.EncodeToString(h.Sum(nil))
}
func hashConnect(h hash.Hash, connect *ConsulConnect) {
if connect != nil && connect.SidecarService != nil {
hashString(h, connect.SidecarService.Port)
hashTags(h, connect.SidecarService.Tags)
if p := connect.SidecarService.Proxy; p != nil {
hashString(h, p.LocalServiceAddress)
hashString(h, strconv.Itoa(p.LocalServicePort))
hashConfig(h, p.Config)
for _, upstream := range p.Upstreams {
hashString(h, upstream.DestinationName)
hashString(h, upstream.DestinationNamespace)
hashString(h, strconv.Itoa(upstream.LocalBindPort))
hashStringIfNonEmpty(h, upstream.Datacenter)
hashStringIfNonEmpty(h, upstream.LocalBindAddress)
hashString(h, upstream.DestinationPeer)
hashString(h, upstream.DestinationType)
hashString(h, upstream.LocalBindSocketPath)
hashString(h, upstream.LocalBindSocketMode)
hashConfig(h, upstream.Config)
}
}
}
}
func hashString(h hash.Hash, s string) {
_, _ = io.WriteString(h, s)
}
func hashBool(h hash.Hash, b bool, name string) {
if b {
hashString(h, name)
}
}
func hashTags(h hash.Hash, tags []string) {
for _, tag := range tags {
hashString(h, tag)
}
}
func hashMeta(h hash.Hash, m map[string]string) {
_, _ = fmt.Fprintf(h, "%v", m)
}
func hashConfig(h hash.Hash, c map[string]interface{}) {
_, _ = fmt.Fprintf(h, "%v", c)
}
// Equal returns true if the structs are recursively equal.
func (s *Service) Equal(o *Service) bool {
if s == nil || o == nil {
return s == o
}
if s.Provider != o.Provider {
return false
}
if s.Namespace != o.Namespace {
return false
}
if s.AddressMode != o.AddressMode {
return false
}
if s.Address != o.Address {
return false
}
if s.OnUpdate != o.OnUpdate {
return false
}
if !helper.SliceSetEq(s.CanaryTags, o.CanaryTags) {
return false
}
if !helper.ElementsEqual(s.Checks, o.Checks) {
return false
}
if !s.Connect.Equal(o.Connect) {
return false
}
if s.Name != o.Name {
return false
}
if s.PortLabel != o.PortLabel {
return false
}
if !maps.Equal(s.Meta, o.Meta) {
return false
}
if !maps.Equal(s.CanaryMeta, o.CanaryMeta) {
return false
}
if !maps.Equal(s.TaggedAddresses, o.TaggedAddresses) {
return false
}
if !helper.SliceSetEq(s.Tags, o.Tags) {
return false
}
if s.EnableTagOverride != o.EnableTagOverride {
return false
}
return true
}
// ConsulConnect represents a Consul Connect jobspec block.
type ConsulConnect struct {
// Native indicates whether the service is Consul Connect Native enabled.
Native bool
// SidecarService is non-nil if a service requires a sidecar.
SidecarService *ConsulSidecarService
// SidecarTask is non-nil if sidecar overrides are set
SidecarTask *SidecarTask
// Gateway is a Consul Connect Gateway Proxy.
Gateway *ConsulGateway
}
// Copy the block recursively. Returns nil if nil.
func (c *ConsulConnect) Copy() *ConsulConnect {
if c == nil {
return nil
}
return &ConsulConnect{
Native: c.Native,
SidecarService: c.SidecarService.Copy(),
SidecarTask: c.SidecarTask.Copy(),
Gateway: c.Gateway.Copy(),
}
}
// Equal returns true if the connect blocks are deeply equal.
func (c *ConsulConnect) Equal(o *ConsulConnect) bool {
if c == nil || o == nil {
return c == o
}
if c.Native != o.Native {
return false
}
if !c.SidecarService.Equal(o.SidecarService) {
return false
}
if !c.SidecarTask.Equal(o.SidecarTask) {
return false
}
if !c.Gateway.Equal(o.Gateway) {
return false
}
return true
}
// HasSidecar checks if a sidecar task is configured.
func (c *ConsulConnect) HasSidecar() bool {
return c != nil && c.SidecarService != nil
}
// IsNative checks if the service is connect native.
func (c *ConsulConnect) IsNative() bool {
return c != nil && c.Native
}
// IsGateway checks if the service is any type of connect gateway.
func (c *ConsulConnect) IsGateway() bool {
return c != nil && c.Gateway != nil
}
// IsIngress checks if the service is an ingress gateway.
func (c *ConsulConnect) IsIngress() bool {
return c.IsGateway() && c.Gateway.Ingress != nil
}
// IsTerminating checks if the service is a terminating gateway.
func (c *ConsulConnect) IsTerminating() bool {
return c.IsGateway() && c.Gateway.Terminating != nil
}
// IsCustomizedTLS checks if the service customizes ingress tls config.
func (c *ConsulConnect) IsCustomizedTLS() bool {
return c.IsIngress() && c.Gateway.Ingress.TLS != nil &&
(c.Gateway.Ingress.TLS.TLSMinVersion != "" ||
c.Gateway.Ingress.TLS.TLSMaxVersion != "" ||
len(c.Gateway.Ingress.TLS.CipherSuites) != 0)
}
func (c *ConsulConnect) IsMesh() bool {
return c.IsGateway() && c.Gateway.Mesh != nil
}
// Validate that the Connect block represents exactly one of:
// - Connect non-native service sidecar proxy
// - Connect native service
// - Connect gateway (any type)
func (c *ConsulConnect) Validate() error {
if c == nil {
return nil
}
// Count the number of things actually configured. If that number is not 1,
// the config is not valid.
count := 0
if c.HasSidecar() {
count++
}
if c.IsNative() {
count++
}
if c.IsGateway() {
count++
}
if count != 1 {
return fmt.Errorf("Consul Connect must be exclusively native, make use of a sidecar, or represent a Gateway")
}
if c.IsGateway() {
if err := c.Gateway.Validate(); err != nil {
return err
}
}
// The Native and Sidecar cases are validated up at the service level.
return nil
}
// ConsulSidecarService represents a Consul Connect SidecarService jobspec
// block.
type ConsulSidecarService struct {
// Tags are optional service tags that get registered with the sidecar service
// in Consul. If unset, the sidecar service inherits the parent service tags.
Tags []string
// Port is the service's port that the sidecar will connect to. May be
// a port label or a literal port number.
Port string
// Proxy block defining the sidecar proxy configuration.
Proxy *ConsulProxy
// DisableDefaultTCPCheck, if true, instructs Nomad to avoid setting a
// default TCP check for the sidecar service.
DisableDefaultTCPCheck bool
// Meta specifies arbitrary KV metadata linked to the sidecar service.
Meta map[string]string
}
// HasUpstreams checks if the sidecar service has any upstreams configured
func (s *ConsulSidecarService) HasUpstreams() bool {
return s != nil && s.Proxy != nil && len(s.Proxy.Upstreams) > 0
}
// Copy the block recursively. Returns nil if nil.
func (s *ConsulSidecarService) Copy() *ConsulSidecarService {
if s == nil {
return nil
}
return &ConsulSidecarService{
Tags: slices.Clone(s.Tags),
Port: s.Port,
Proxy: s.Proxy.Copy(),
DisableDefaultTCPCheck: s.DisableDefaultTCPCheck,
Meta: maps.Clone(s.Meta),
}
}
// Equal returns true if the structs are recursively equal.
func (s *ConsulSidecarService) Equal(o *ConsulSidecarService) bool {
if s == nil || o == nil {
return s == o
}
if s.Port != o.Port {
return false
}
if s.DisableDefaultTCPCheck != o.DisableDefaultTCPCheck {
return false
}
if !helper.SliceSetEq(s.Tags, o.Tags) {
return false
}
if !maps.Equal(s.Meta, o.Meta) {
return false
}
return s.Proxy.Equal(o.Proxy)
}
// SidecarTask represents a subset of Task fields that are able to be overridden
// from the sidecar_task block
type SidecarTask struct {
// Name of the task
Name string
// Driver is used to control which driver is used
Driver string
// User is used to determine which user will run the task. It defaults to
// the same user the Nomad client is being run as.
User string
// Config is provided to the driver to initialize
Config map[string]interface{}
// Map of environment variables to be used by the driver
Env map[string]string
// Resources is the resources needed by this task
Resources *Resources
// Meta is used to associate arbitrary metadata with this
// task. This is opaque to Nomad.
Meta map[string]string
// KillTimeout is the time between signaling a task that it will be
// killed and killing it.
KillTimeout *time.Duration
// LogConfig provides configuration for log rotation
LogConfig *LogConfig
// ShutdownDelay is the duration of the delay between deregistering a
// task from Consul and sending it a signal to shutdown. See #2441
ShutdownDelay *time.Duration
// KillSignal is the kill signal to use for the task. This is an optional
// specification and defaults to SIGINT
KillSignal string
}
func (t *SidecarTask) Equal(o *SidecarTask) bool {
if t == nil || o == nil {
return t == o
}
if t.Name != o.Name {
return false
}
if t.Driver != o.Driver {
return false
}
if t.User != o.User {
return false
}
// task config, use opaque maps equal
if !helper.OpaqueMapsEqual(t.Config, o.Config) {
return false
}
if !maps.Equal(t.Env, o.Env) {
return false
}
if !t.Resources.Equal(o.Resources) {
return false
}
if !maps.Equal(t.Meta, o.Meta) {
return false
}
if !pointer.Eq(t.KillTimeout, o.KillTimeout) {
return false
}
if !t.LogConfig.Equal(o.LogConfig) {
return false
}
if !pointer.Eq(t.ShutdownDelay, o.ShutdownDelay) {
return false
}
if t.KillSignal != o.KillSignal {
return false
}
return true
}
func (t *SidecarTask) Copy() *SidecarTask {
if t == nil {
return nil
}
nt := new(SidecarTask)
*nt = *t
nt.Env = maps.Clone(nt.Env)
nt.Resources = nt.Resources.Copy()
nt.LogConfig = nt.LogConfig.Copy()
nt.Meta = maps.Clone(nt.Meta)
if i, err := copystructure.Copy(nt.Config); err != nil {
panic(err.Error())
} else {
nt.Config = i.(map[string]interface{})
}
if t.KillTimeout != nil {
nt.KillTimeout = pointer.Of(*t.KillTimeout)
}
if t.ShutdownDelay != nil {
nt.ShutdownDelay = pointer.Of(*t.ShutdownDelay)
}
return nt
}
// MergeIntoTask merges the SidecarTask fields over the given task
func (t *SidecarTask) MergeIntoTask(task *Task) {
if t.Name != "" {
task.Name = t.Name
}
// If the driver changes then the driver config can be overwritten.
// Otherwise we'll merge the driver config together
if t.Driver != "" && t.Driver != task.Driver {
task.Driver = t.Driver
task.Config = t.Config
} else {
for k, v := range t.Config {
task.Config[k] = v
}
}
if t.User != "" {
task.User = t.User
}
if t.Env != nil {
if task.Env == nil {
task.Env = t.Env
} else {
for k, v := range t.Env {
task.Env[k] = v
}
}
}
if t.Resources != nil {
task.Resources.Merge(t.Resources)
}
if t.Meta != nil {
if task.Meta == nil {
task.Meta = t.Meta
} else {
for k, v := range t.Meta {
task.Meta[k] = v
}
}
}
if t.KillTimeout != nil {
task.KillTimeout = *t.KillTimeout
}
if t.LogConfig != nil {
if task.LogConfig == nil {
task.LogConfig = t.LogConfig
} else {
if t.LogConfig.MaxFiles > 0 {
task.LogConfig.MaxFiles = t.LogConfig.MaxFiles
}
if t.LogConfig.MaxFileSizeMB > 0 {
task.LogConfig.MaxFileSizeMB = t.LogConfig.MaxFileSizeMB
}
}
}
if t.ShutdownDelay != nil {
task.ShutdownDelay = *t.ShutdownDelay
}
if t.KillSignal != "" {
task.KillSignal = t.KillSignal
}
}
// ConsulProxy represents a Consul Connect sidecar proxy jobspec block.
type ConsulProxy struct {
// LocalServiceAddress is the address the local service binds to.
// Usually 127.0.0.1 it is useful to customize in clusters with mixed
// Connect and non-Connect services.
LocalServiceAddress string
// LocalServicePort is the port the local service binds to. Usually
// the same as the parent service's port, it is useful to customize
// in clusters with mixed Connect and non-Connect services
LocalServicePort int
// Upstreams configures the upstream services this service intends to
// connect to.
Upstreams []ConsulUpstream
// Expose configures the consul proxy.expose block to "open up" endpoints
// used by task-group level service checks using HTTP or gRPC protocols.
Expose *ConsulExposeConfig
// Config is a proxy configuration. It is opaque to Nomad and passed
// directly to Consul.
Config map[string]interface{}
}
// Copy the block recursively. Returns nil if nil.
func (p *ConsulProxy) Copy() *ConsulProxy {
if p == nil {
return nil
}
return &ConsulProxy{
LocalServiceAddress: p.LocalServiceAddress,
LocalServicePort: p.LocalServicePort,
Expose: p.Expose.Copy(),
Upstreams: slices.Clone(p.Upstreams),
Config: maps.Clone(p.Config),
}
}
// Equal returns true if the structs are recursively equal.
func (p *ConsulProxy) Equal(o *ConsulProxy) bool {
if p == nil || o == nil {
return p == o
}
if p.LocalServiceAddress != o.LocalServiceAddress {
return false
}
if p.LocalServicePort != o.LocalServicePort {
return false
}
if !p.Expose.Equal(o.Expose) {
return false
}
if !upstreamsEquals(p.Upstreams, o.Upstreams) {
return false
}
// envoy config, use reflect
if !reflect.DeepEqual(p.Config, o.Config) {
return false
}
return true
}
// ConsulMeshGateway is used to configure mesh gateway usage when connecting to
// a connect upstream in another datacenter.
type ConsulMeshGateway struct {
// Mode configures how an upstream should be accessed with regard to using
// mesh gateways.
//
// local - the connect proxy makes outbound connections through mesh gateway
// originating in the same datacenter.
//
// remote - the connect proxy makes outbound connections to a mesh gateway
// in the destination datacenter.
//
// none (default) - no mesh gateway is used, the proxy makes outbound connections
// directly to destination services.
//
// https://www.consul.io/docs/connect/gateways/mesh-gateway#modes-of-operation
Mode string
}
func (c *ConsulMeshGateway) Copy() ConsulMeshGateway {
return ConsulMeshGateway{
Mode: c.Mode,
}
}
func (c *ConsulMeshGateway) Equal(o ConsulMeshGateway) bool {
return c.Mode == o.Mode
}
func (c *ConsulMeshGateway) Validate() error {
if c == nil {
return nil
}
switch c.Mode {
case "local", "remote", "none":
return nil
default:
return fmt.Errorf("Connect mesh_gateway mode %q not supported", c.Mode)
}
}
// ConsulUpstream represents a Consul Connect upstream jobspec block.
type ConsulUpstream struct {
// DestinationName is the name of the upstream service.
DestinationName string
// DestinationNamespace is the namespace of the upstream service.
DestinationNamespace string
// DestinationPeer the destination service address
DestinationPeer string
// DestinationType is the type of destination. It can be an IP address,
// a DNS hostname, or a service name.
DestinationType string
// LocalBindPort is the port the proxy will receive connections for the
// upstream on.
LocalBindPort int
// Datacenter is the datacenter in which to issue the discovery query to.
Datacenter string
// LocalBindAddress is the address the proxy will receive connections for the
// upstream on.
LocalBindAddress string
// LocalBindSocketPath is the path of the local socket file that will be used
// to connect to the destination service
LocalBindSocketPath string
// LocalBindSocketMode defines access permissions to the local socket file
LocalBindSocketMode string
// MeshGateway is the optional configuration of the mesh gateway for this
// upstream to use.
MeshGateway ConsulMeshGateway
// Config is an upstream configuration. It is opaque to Nomad and passed
// directly to Consul.
Config map[string]any
}
// Equal returns true if the structs are recursively equal.
func (u *ConsulUpstream) Equal(o *ConsulUpstream) bool {
if u == nil || o == nil {
return u == o
}
switch {
case u.DestinationName != o.DestinationName:
return false
case u.DestinationNamespace != o.DestinationNamespace:
return false
case u.DestinationPeer != o.DestinationPeer:
return false
case u.DestinationType != o.DestinationType:
return false
case u.LocalBindPort != o.LocalBindPort:
return false
case u.LocalBindSocketPath != o.LocalBindSocketPath:
return false
case u.LocalBindSocketMode != o.LocalBindSocketMode:
return false
case u.Datacenter != o.Datacenter:
return false
case u.LocalBindAddress != o.LocalBindAddress:
return false
case !u.MeshGateway.Equal(o.MeshGateway):
return false
case !reflect.DeepEqual(u.Config, o.Config):
// envoy config, use reflect
return false
}
return true
}
// Hash implements a GoString based "hash" function for ConsulUpstream; because
// this struct now contains an opaque map we cannot do much better than this.
func (u ConsulUpstream) Hash() string {
return fmt.Sprintf("%#v", u)
}
func upstreamsEquals(a, b []ConsulUpstream) bool {
setA := set.HashSetFrom[ConsulUpstream, string](a)
setB := set.HashSetFrom[ConsulUpstream, string](b)
return setA.Equal(setB)
}
// ConsulExposeConfig represents a Consul Connect expose jobspec block.
type ConsulExposeConfig struct {
Paths []ConsulExposePath
}
type ConsulExposePath struct {
Path string
Protocol string
LocalPathPort int
ListenerPort string
}
func exposePathsEqual(a, b []ConsulExposePath) bool {
return helper.SliceSetEq(a, b)
}
// Copy the block. Returns nil if e is nil.
func (e *ConsulExposeConfig) Copy() *ConsulExposeConfig {
if e == nil {
return nil
}
paths := make([]ConsulExposePath, len(e.Paths))
copy(paths, e.Paths)
return &ConsulExposeConfig{
Paths: paths,
}
}
// Equal returns true if the structs are recursively equal.
func (e *ConsulExposeConfig) Equal(o *ConsulExposeConfig) bool {
if e == nil || o == nil {
return e == o
}
return exposePathsEqual(e.Paths, o.Paths)
}
// ConsulGateway is used to configure one of the Consul Connect Gateway types.
type ConsulGateway struct {
// Proxy is used to configure the Envoy instance acting as the gateway.
Proxy *ConsulGatewayProxy
// Ingress represents the Consul Configuration Entry for an Ingress Gateway.
Ingress *ConsulIngressConfigEntry
// Terminating represents the Consul Configuration Entry for a Terminating Gateway.
Terminating *ConsulTerminatingConfigEntry
// Mesh indicates the Consul service should be a Mesh Gateway.
Mesh *ConsulMeshConfigEntry
}
func (g *ConsulGateway) Prefix() string {
switch {
case g.Mesh != nil:
return ConnectMeshPrefix
case g.Ingress != nil:
return ConnectIngressPrefix
default:
return ConnectTerminatingPrefix
}
}
func (g *ConsulGateway) Copy() *ConsulGateway {
if g == nil {
return nil
}
return &ConsulGateway{
Proxy: g.Proxy.Copy(),
Ingress: g.Ingress.Copy(),
Terminating: g.Terminating.Copy(),
Mesh: g.Mesh.Copy(),
}
}
func (g *ConsulGateway) Equal(o *ConsulGateway) bool {
if g == nil || o == nil {
return g == o
}
if !g.Proxy.Equal(o.Proxy) {
return false
}
if !g.Ingress.Equal(o.Ingress) {
return false
}
if !g.Terminating.Equal(o.Terminating) {
return false
}
if !g.Mesh.Equal(o.Mesh) {
return false
}
return true
}
func (g *ConsulGateway) Validate() error {
if g == nil {
return nil
}
if err := g.Proxy.Validate(); err != nil {
return err
}
if err := g.Ingress.Validate(); err != nil {
return err
}
if err := g.Terminating.Validate(); err != nil {
return err
}
if err := g.Mesh.Validate(); err != nil {
return err
}
// Exactly 1 of ingress/terminating/mesh must be set.
count := 0
if g.Ingress != nil {
count++
}
if g.Terminating != nil {
count++
}
if g.Mesh != nil {
count++
}
if count != 1 {
return fmt.Errorf("One Consul Gateway Configuration must be set")
}
return nil
}
// ConsulGatewayBindAddress is equivalent to Consul's api/catalog.go ServiceAddress
// struct, as this is used to encode values to pass along to Envoy (i.e. via
// JSON encoding).
type ConsulGatewayBindAddress struct {
Address string
Port int
}
func (a *ConsulGatewayBindAddress) Equal(o *ConsulGatewayBindAddress) bool {
if a == nil || o == nil {
return a == o
}
if a.Address != o.Address {
return false
}
if a.Port != o.Port {
return false
}
return true
}
func (a *ConsulGatewayBindAddress) Copy() *ConsulGatewayBindAddress {
if a == nil {
return nil
}
return &ConsulGatewayBindAddress{
Address: a.Address,
Port: a.Port,
}
}
func (a *ConsulGatewayBindAddress) Validate() error {
if a == nil {
return nil
}
if a.Address == "" {
return fmt.Errorf("Consul Gateway Bind Address must be set")
}
if a.Port <= 0 && a.Port != -1 { // port -1 => nomad autofill
return fmt.Errorf("Consul Gateway Bind Address must set valid Port")
}
return nil
}
// ConsulGatewayProxy is used to tune parameters of the proxy instance acting as
// one of the forms of Connect gateways that Consul supports.
//
// https://www.consul.io/docs/connect/proxies/envoy#gateway-options
type ConsulGatewayProxy struct {
ConnectTimeout *time.Duration
EnvoyGatewayBindTaggedAddresses bool
EnvoyGatewayBindAddresses map[string]*ConsulGatewayBindAddress
EnvoyGatewayNoDefaultBind bool
EnvoyDNSDiscoveryType string
Config map[string]interface{}
}
func (p *ConsulGatewayProxy) Copy() *ConsulGatewayProxy {
if p == nil {
return nil
}
return &ConsulGatewayProxy{
ConnectTimeout: pointer.Of(*p.ConnectTimeout),
EnvoyGatewayBindTaggedAddresses: p.EnvoyGatewayBindTaggedAddresses,
EnvoyGatewayBindAddresses: p.copyBindAddresses(),
EnvoyGatewayNoDefaultBind: p.EnvoyGatewayNoDefaultBind,
EnvoyDNSDiscoveryType: p.EnvoyDNSDiscoveryType,
Config: maps.Clone(p.Config),
}
}
func (p *ConsulGatewayProxy) copyBindAddresses() map[string]*ConsulGatewayBindAddress {
if p.EnvoyGatewayBindAddresses == nil {
return nil
}
bindAddresses := make(map[string]*ConsulGatewayBindAddress, len(p.EnvoyGatewayBindAddresses))
for k, v := range p.EnvoyGatewayBindAddresses {
bindAddresses[k] = v.Copy()
}
return bindAddresses
}
func (p *ConsulGatewayProxy) equalBindAddresses(o map[string]*ConsulGatewayBindAddress) bool {
if len(p.EnvoyGatewayBindAddresses) != len(o) {
return false
}
for listener, addr := range p.EnvoyGatewayBindAddresses {
if !o[listener].Equal(addr) {
return false
}
}
return true
}
func (p *ConsulGatewayProxy) Equal(o *ConsulGatewayProxy) bool {
if p == nil || o == nil {
return p == o
}
if !pointer.Eq(p.ConnectTimeout, o.ConnectTimeout) {
return false
}
if p.EnvoyGatewayBindTaggedAddresses != o.EnvoyGatewayBindTaggedAddresses {
return false
}
if !p.equalBindAddresses(o.EnvoyGatewayBindAddresses) {
return false
}
if p.EnvoyGatewayNoDefaultBind != o.EnvoyGatewayNoDefaultBind {
return false
}
if p.EnvoyDNSDiscoveryType != o.EnvoyDNSDiscoveryType {
return false
}
// envoy config, use reflect
if !reflect.DeepEqual(p.Config, o.Config) {
return false
}
return true
}
const (
strictDNS = "STRICT_DNS"
logicalDNS = "LOGICAL_DNS"
)
func (p *ConsulGatewayProxy) Validate() error {
if p == nil {
return nil
}
if p.ConnectTimeout == nil {
return fmt.Errorf("Consul Gateway Proxy connection_timeout must be set")
}
switch p.EnvoyDNSDiscoveryType {
case "", strictDNS, logicalDNS:
// Consul defaults to logical DNS, suitable for large scale workloads.
// https://www.envoyproxy.io/docs/envoy/v1.16.1/intro/arch_overview/upstream/service_discovery
default:
return fmt.Errorf("Consul Gateway Proxy Envoy DNS Discovery type must be %s or %s", strictDNS, logicalDNS)
}
for _, bindAddr := range p.EnvoyGatewayBindAddresses {
if err := bindAddr.Validate(); err != nil {
return err
}
}
return nil
}
// ConsulGatewayTLSConfig is used to configure TLS for a gateway.
type ConsulGatewayTLSConfig struct {
Enabled bool
TLSMinVersion string
TLSMaxVersion string
CipherSuites []string
}
func (c *ConsulGatewayTLSConfig) Copy() *ConsulGatewayTLSConfig {
if c == nil {
return nil
}
return &ConsulGatewayTLSConfig{
Enabled: c.Enabled,
TLSMinVersion: c.TLSMinVersion,
TLSMaxVersion: c.TLSMaxVersion,
CipherSuites: slices.Clone(c.CipherSuites),
}
}
func (c *ConsulGatewayTLSConfig) Equal(o *ConsulGatewayTLSConfig) bool {
if c == nil || o == nil {
return c == o
}
return c.Enabled == o.Enabled &&
c.TLSMinVersion == o.TLSMinVersion &&
c.TLSMaxVersion == o.TLSMaxVersion &&
helper.SliceSetEq(c.CipherSuites, o.CipherSuites)
}
// ConsulIngressService is used to configure a service fronted by the ingress gateway.
type ConsulIngressService struct {
Name string
Hosts []string
}
func (s *ConsulIngressService) Copy() *ConsulIngressService {
if s == nil {
return nil
}
var hosts []string = nil
if n := len(s.Hosts); n > 0 {
hosts = make([]string, n)
copy(hosts, s.Hosts)
}
return &ConsulIngressService{
Name: s.Name,
Hosts: hosts,
}
}
func (s *ConsulIngressService) Equal(o *ConsulIngressService) bool {
if s == nil || o == nil {
return s == o
}
if s.Name != o.Name {
return false
}
return helper.SliceSetEq(s.Hosts, o.Hosts)
}
func (s *ConsulIngressService) Validate(protocol string) error {
if s == nil {
return nil
}
// pre-validate service Name and Hosts before passing along to consul:
// https://developer.hashicorp.com/consul/docs/connect/config-entries/ingress-gateway#services
if s.Name == "" {
return errors.New("Consul Ingress Service requires a name")
}
switch protocol {
case "tcp":
if s.Name == "*" {
return errors.New(`Consul Ingress Service doesn't support wildcard name for "tcp" protocol`)
}
if len(s.Hosts) != 0 {
return errors.New(`Consul Ingress Service doesn't support associating hosts to a service for the "tcp" protocol`)
}
default:
if s.Name == "*" && len(s.Hosts) != 0 {
return errors.New(`Consul Ingress Service with a wildcard "*" service name can not also specify hosts`)
}
}
return nil
}
// ConsulIngressListener is used to configure a listener on a Consul Ingress
// Gateway.
type ConsulIngressListener struct {
Port int
Protocol string
Services []*ConsulIngressService
}
func (l *ConsulIngressListener) Copy() *ConsulIngressListener {
if l == nil {
return nil
}
var services []*ConsulIngressService = nil
if n := len(l.Services); n > 0 {
services = make([]*ConsulIngressService, n)
for i := 0; i < n; i++ {
services[i] = l.Services[i].Copy()
}
}
return &ConsulIngressListener{
Port: l.Port,
Protocol: l.Protocol,
Services: services,
}
}
func (l *ConsulIngressListener) Equal(o *ConsulIngressListener) bool {
if l == nil || o == nil {
return l == o
}
if l.Port != o.Port {
return false
}
if l.Protocol != o.Protocol {
return false
}
return ingressServicesEqual(l.Services, o.Services)
}
func (l *ConsulIngressListener) Validate() error {
if l == nil {
return nil
}
if l.Port <= 0 {
return fmt.Errorf("Consul Ingress Listener requires valid Port")
}
protocols := []string{"tcp", "http", "http2", "grpc"}
if !slices.Contains(protocols, l.Protocol) {
return fmt.Errorf(`Consul Ingress Listener requires protocol of %s, got %q`, strings.Join(protocols, ", "), l.Protocol)
}
if len(l.Services) == 0 {
return fmt.Errorf("Consul Ingress Listener requires one or more services")
}
for _, service := range l.Services {
if err := service.Validate(l.Protocol); err != nil {
return err
}
}
return nil
}
func ingressServicesEqual(a, b []*ConsulIngressService) bool {
return helper.ElementsEqual(a, b)
}
// ConsulIngressConfigEntry represents the Consul Configuration Entry type for
// an Ingress Gateway.
//
// https://www.consul.io/docs/agent/config-entries/ingress-gateway#available-fields
type ConsulIngressConfigEntry struct {
TLS *ConsulGatewayTLSConfig
Listeners []*ConsulIngressListener
}
func (e *ConsulIngressConfigEntry) Copy() *ConsulIngressConfigEntry {
if e == nil {
return nil
}
var listeners []*ConsulIngressListener = nil
if n := len(e.Listeners); n > 0 {
listeners = make([]*ConsulIngressListener, n)
for i := 0; i < n; i++ {
listeners[i] = e.Listeners[i].Copy()
}
}
return &ConsulIngressConfigEntry{
TLS: e.TLS.Copy(),
Listeners: listeners,
}
}
func (e *ConsulIngressConfigEntry) Equal(o *ConsulIngressConfigEntry) bool {
if e == nil || o == nil {
return e == o
}
if !e.TLS.Equal(o.TLS) {
return false
}
return ingressListenersEqual(e.Listeners, o.Listeners)
}
func (e *ConsulIngressConfigEntry) Validate() error {
if e == nil {
return nil
}
if len(e.Listeners) == 0 {
return fmt.Errorf("Consul Ingress Gateway requires at least one listener")
}
for _, listener := range e.Listeners {
if err := listener.Validate(); err != nil {
return err
}
}
return nil
}
func ingressListenersEqual(a, b []*ConsulIngressListener) bool {
return helper.ElementsEqual(a, b)
}
type ConsulLinkedService struct {
Name string
CAFile string
CertFile string
KeyFile string
SNI string
}
func (s *ConsulLinkedService) Copy() *ConsulLinkedService {
if s == nil {
return nil
}
return &ConsulLinkedService{
Name: s.Name,
CAFile: s.CAFile,
CertFile: s.CertFile,
KeyFile: s.KeyFile,
SNI: s.SNI,
}
}
func (s *ConsulLinkedService) Equal(o *ConsulLinkedService) bool {
if s == nil || o == nil {
return s == o
}
switch {
case s.Name != o.Name:
return false
case s.CAFile != o.CAFile:
return false
case s.CertFile != o.CertFile:
return false
case s.KeyFile != o.KeyFile:
return false
case s.SNI != o.SNI:
return false
}
return true
}
func (s *ConsulLinkedService) Validate() error {
if s == nil {
return nil
}
if s.Name == "" {
return fmt.Errorf("Consul Linked Service requires Name")
}
caSet := s.CAFile != ""
certSet := s.CertFile != ""
keySet := s.KeyFile != ""
sniSet := s.SNI != ""
if (certSet || keySet) && !caSet {
return fmt.Errorf("Consul Linked Service TLS requires CAFile")
}
if certSet != keySet {
return fmt.Errorf("Consul Linked Service TLS Cert and Key must both be set")
}
if sniSet && !caSet {
return fmt.Errorf("Consul Linked Service TLS SNI requires CAFile")
}
return nil
}
func linkedServicesEqual(a, b []*ConsulLinkedService) bool {
return helper.ElementsEqual(a, b)
}
type ConsulTerminatingConfigEntry struct {
Services []*ConsulLinkedService
}
func (e *ConsulTerminatingConfigEntry) Copy() *ConsulTerminatingConfigEntry {
if e == nil {
return nil
}
var services []*ConsulLinkedService = nil
if n := len(e.Services); n > 0 {
services = make([]*ConsulLinkedService, n)
for i := 0; i < n; i++ {
services[i] = e.Services[i].Copy()
}
}
return &ConsulTerminatingConfigEntry{
Services: services,
}
}
func (e *ConsulTerminatingConfigEntry) Equal(o *ConsulTerminatingConfigEntry) bool {
if e == nil || o == nil {
return e == o
}
return linkedServicesEqual(e.Services, o.Services)
}
func (e *ConsulTerminatingConfigEntry) Validate() error {
if e == nil {
return nil
}
if len(e.Services) == 0 {
return fmt.Errorf("Consul Terminating Gateway requires at least one service")
}
for _, service := range e.Services {
if err := service.Validate(); err != nil {
return err
}
}
return nil
}
// ConsulMeshConfigEntry is a stub used to represent that the gateway service
// type should be for a Mesh Gateway. Unlike Ingress and Terminating, there is no
// dedicated Consul Config Entry type for "mesh-gateway", for now. We still
// create a type for future proofing, and to keep underlying job-spec marshaling
// consistent with the other types.
type ConsulMeshConfigEntry struct {
// nothing in here
}
func (e *ConsulMeshConfigEntry) Copy() *ConsulMeshConfigEntry {
if e == nil {
return nil
}
return new(ConsulMeshConfigEntry)
}
func (e *ConsulMeshConfigEntry) Equal(o *ConsulMeshConfigEntry) bool {
if e == nil || o == nil {
return e == o
}
return true
}
func (e *ConsulMeshConfigEntry) Validate() error {
return nil
}