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