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