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