github.com/diptanu/nomad@v0.5.7-0.20170516172507-d72e86cbe3d9/command/agent/consul/client.go (about) 1 package consul 2 3 import ( 4 "fmt" 5 "log" 6 "net" 7 "net/url" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 metrics "github.com/armon/go-metrics" 14 "github.com/hashicorp/consul/api" 15 "github.com/hashicorp/nomad/client/driver" 16 "github.com/hashicorp/nomad/nomad/structs" 17 ) 18 19 const ( 20 // nomadServicePrefix is the first prefix that scopes all Nomad registered 21 // services 22 nomadServicePrefix = "_nomad" 23 24 // defaultRetryInterval is how quickly to retry syncing services and 25 // checks to Consul when an error occurs. Will backoff up to a max. 26 defaultRetryInterval = time.Second 27 28 // defaultMaxRetryInterval is the default max retry interval. 29 defaultMaxRetryInterval = 30 * time.Second 30 31 // ttlCheckBuffer is the time interval that Nomad can take to report Consul 32 // the check result 33 ttlCheckBuffer = 31 * time.Second 34 35 // defaultShutdownWait is how long Shutdown() should block waiting for 36 // enqueued operations to sync to Consul by default. 37 defaultShutdownWait = time.Minute 38 39 // DefaultQueryWaitDuration is the max duration the Consul Agent will 40 // spend waiting for a response from a Consul Query. 41 DefaultQueryWaitDuration = 2 * time.Second 42 43 // ServiceTagHTTP is the tag assigned to HTTP services 44 ServiceTagHTTP = "http" 45 46 // ServiceTagRPC is the tag assigned to RPC services 47 ServiceTagRPC = "rpc" 48 49 // ServiceTagSerf is the tag assigned to Serf services 50 ServiceTagSerf = "serf" 51 ) 52 53 // CatalogAPI is the consul/api.Catalog API used by Nomad. 54 type CatalogAPI interface { 55 Datacenters() ([]string, error) 56 Service(service, tag string, q *api.QueryOptions) ([]*api.CatalogService, *api.QueryMeta, error) 57 } 58 59 // AgentAPI is the consul/api.Agent API used by Nomad. 60 type AgentAPI interface { 61 Services() (map[string]*api.AgentService, error) 62 Checks() (map[string]*api.AgentCheck, error) 63 CheckRegister(check *api.AgentCheckRegistration) error 64 CheckDeregister(checkID string) error 65 ServiceRegister(service *api.AgentServiceRegistration) error 66 ServiceDeregister(serviceID string) error 67 UpdateTTL(id, output, status string) error 68 } 69 70 // addrParser is usually the Task.FindHostAndPortFor method for turning a 71 // portLabel into an address and port. 72 type addrParser func(portLabel string) (string, int) 73 74 // operations are submitted to the main loop via commit() for synchronizing 75 // with Consul. 76 type operations struct { 77 regServices []*api.AgentServiceRegistration 78 regChecks []*api.AgentCheckRegistration 79 scripts []*scriptCheck 80 81 deregServices []string 82 deregChecks []string 83 } 84 85 // ServiceClient handles task and agent service registration with Consul. 86 type ServiceClient struct { 87 client AgentAPI 88 logger *log.Logger 89 retryInterval time.Duration 90 maxRetryInterval time.Duration 91 92 // skipVerifySupport is true if the local Consul agent suppots TLSSkipVerify 93 skipVerifySupport bool 94 95 // exitCh is closed when the main Run loop exits 96 exitCh chan struct{} 97 98 // shutdownCh is closed when the client should shutdown 99 shutdownCh chan struct{} 100 101 // shutdownWait is how long Shutdown() blocks waiting for the final 102 // sync() to finish. Defaults to defaultShutdownWait 103 shutdownWait time.Duration 104 105 opCh chan *operations 106 107 services map[string]*api.AgentServiceRegistration 108 checks map[string]*api.AgentCheckRegistration 109 scripts map[string]*scriptCheck 110 runningScripts map[string]*scriptHandle 111 112 // agent services and checks record entries for the agent itself which 113 // should be removed on shutdown 114 agentServices map[string]struct{} 115 agentChecks map[string]struct{} 116 agentLock sync.Mutex 117 } 118 119 // NewServiceClient creates a new Consul ServiceClient from an existing Consul API 120 // Client and logger. 121 func NewServiceClient(consulClient AgentAPI, skipVerifySupport bool, logger *log.Logger) *ServiceClient { 122 return &ServiceClient{ 123 client: consulClient, 124 skipVerifySupport: skipVerifySupport, 125 logger: logger, 126 retryInterval: defaultRetryInterval, 127 maxRetryInterval: defaultMaxRetryInterval, 128 exitCh: make(chan struct{}), 129 shutdownCh: make(chan struct{}), 130 shutdownWait: defaultShutdownWait, 131 opCh: make(chan *operations, 8), 132 services: make(map[string]*api.AgentServiceRegistration), 133 checks: make(map[string]*api.AgentCheckRegistration), 134 scripts: make(map[string]*scriptCheck), 135 runningScripts: make(map[string]*scriptHandle), 136 agentServices: make(map[string]struct{}), 137 agentChecks: make(map[string]struct{}), 138 } 139 } 140 141 // Run the Consul main loop which retries operations against Consul. It should 142 // be called exactly once. 143 func (c *ServiceClient) Run() { 144 defer close(c.exitCh) 145 retryTimer := time.NewTimer(0) 146 <-retryTimer.C // disabled by default 147 failures := 0 148 for { 149 select { 150 case <-retryTimer.C: 151 case <-c.shutdownCh: 152 case ops := <-c.opCh: 153 c.merge(ops) 154 } 155 156 if err := c.sync(); err != nil { 157 if failures == 0 { 158 c.logger.Printf("[WARN] consul.sync: failed to update services in Consul: %v", err) 159 } 160 failures++ 161 if !retryTimer.Stop() { 162 // Timer already expired, since the timer may 163 // or may not have been read in the select{} 164 // above, conditionally receive on it 165 select { 166 case <-retryTimer.C: 167 default: 168 } 169 } 170 backoff := c.retryInterval * time.Duration(failures) 171 if backoff > c.maxRetryInterval { 172 backoff = c.maxRetryInterval 173 } 174 retryTimer.Reset(backoff) 175 } else { 176 if failures > 0 { 177 c.logger.Printf("[INFO] consul.sync: successfully updated services in Consul") 178 failures = 0 179 } 180 } 181 182 select { 183 case <-c.shutdownCh: 184 // Exit only after sync'ing all outstanding operations 185 if len(c.opCh) > 0 { 186 for len(c.opCh) > 0 { 187 c.merge(<-c.opCh) 188 } 189 continue 190 } 191 return 192 default: 193 } 194 195 } 196 } 197 198 // commit operations unless already shutting down. 199 func (c *ServiceClient) commit(ops *operations) { 200 select { 201 case c.opCh <- ops: 202 case <-c.shutdownCh: 203 } 204 } 205 206 // merge registrations into state map prior to sync'ing with Consul 207 func (c *ServiceClient) merge(ops *operations) { 208 for _, s := range ops.regServices { 209 c.services[s.ID] = s 210 } 211 for _, check := range ops.regChecks { 212 c.checks[check.ID] = check 213 } 214 for _, s := range ops.scripts { 215 c.scripts[s.id] = s 216 } 217 for _, sid := range ops.deregServices { 218 delete(c.services, sid) 219 } 220 for _, cid := range ops.deregChecks { 221 if script, ok := c.runningScripts[cid]; ok { 222 script.cancel() 223 delete(c.scripts, cid) 224 } 225 delete(c.checks, cid) 226 } 227 metrics.SetGauge([]string{"client", "consul", "services"}, float32(len(c.services))) 228 metrics.SetGauge([]string{"client", "consul", "checks"}, float32(len(c.checks))) 229 metrics.SetGauge([]string{"client", "consul", "script_checks"}, float32(len(c.runningScripts))) 230 } 231 232 // sync enqueued operations. 233 func (c *ServiceClient) sync() error { 234 sreg, creg, sdereg, cdereg := 0, 0, 0, 0 235 236 consulServices, err := c.client.Services() 237 if err != nil { 238 metrics.IncrCounter([]string{"client", "consul", "sync_failure"}, 1) 239 return fmt.Errorf("error querying Consul services: %v", err) 240 } 241 242 consulChecks, err := c.client.Checks() 243 if err != nil { 244 metrics.IncrCounter([]string{"client", "consul", "sync_failure"}, 1) 245 return fmt.Errorf("error querying Consul checks: %v", err) 246 } 247 248 // Remove Nomad services in Consul but unknown locally 249 for id := range consulServices { 250 if _, ok := c.services[id]; ok { 251 // Known service, skip 252 continue 253 } 254 if !isNomadService(id) { 255 // Not managed by Nomad, skip 256 continue 257 } 258 // Unknown Nomad managed service; kill 259 if err := c.client.ServiceDeregister(id); err != nil { 260 metrics.IncrCounter([]string{"client", "consul", "sync_failure"}, 1) 261 return err 262 } 263 sdereg++ 264 metrics.IncrCounter([]string{"client", "consul", "service_deregisrations"}, 1) 265 } 266 267 // Track services whose ports have changed as their checks may also 268 // need updating 269 portsChanged := make(map[string]struct{}, len(c.services)) 270 271 // Add Nomad services missing from Consul 272 for id, locals := range c.services { 273 if remotes, ok := consulServices[id]; ok { 274 if locals.Port == remotes.Port { 275 // Already exists in Consul; skip 276 continue 277 } 278 // Port changed, reregister it and its checks 279 portsChanged[id] = struct{}{} 280 } 281 if err = c.client.ServiceRegister(locals); err != nil { 282 metrics.IncrCounter([]string{"client", "consul", "sync_failure"}, 1) 283 return err 284 } 285 sreg++ 286 metrics.IncrCounter([]string{"client", "consul", "service_regisrations"}, 1) 287 } 288 289 // Remove Nomad checks in Consul but unknown locally 290 for id, check := range consulChecks { 291 if _, ok := c.checks[id]; ok { 292 // Known check, leave it 293 continue 294 } 295 if !isNomadService(check.ServiceID) { 296 // Not managed by Nomad, skip 297 continue 298 } 299 // Unknown Nomad managed check; kill 300 if err := c.client.CheckDeregister(id); err != nil { 301 metrics.IncrCounter([]string{"client", "consul", "sync_failure"}, 1) 302 return err 303 } 304 cdereg++ 305 metrics.IncrCounter([]string{"client", "consul", "check_deregisrations"}, 1) 306 } 307 308 // Add Nomad checks missing from Consul 309 for id, check := range c.checks { 310 if check, ok := consulChecks[id]; ok { 311 if _, changed := portsChanged[check.ServiceID]; !changed { 312 // Already in Consul and ports didn't change; skipping 313 continue 314 } 315 } 316 if err := c.client.CheckRegister(check); err != nil { 317 metrics.IncrCounter([]string{"client", "consul", "sync_failure"}, 1) 318 return err 319 } 320 creg++ 321 metrics.IncrCounter([]string{"client", "consul", "check_regisrations"}, 1) 322 323 // Handle starting scripts 324 if script, ok := c.scripts[id]; ok { 325 // If it's already running, cancel and replace 326 if oldScript, running := c.runningScripts[id]; running { 327 oldScript.cancel() 328 } 329 // Start and store the handle 330 c.runningScripts[id] = script.run() 331 } 332 } 333 334 c.logger.Printf("[DEBUG] consul.sync: registered %d services, %d checks; deregistered %d services, %d checks", 335 sreg, creg, sdereg, cdereg) 336 return nil 337 } 338 339 // RegisterAgent registers Nomad agents (client or server). The 340 // Service.PortLabel should be a literal port to be parsed with SplitHostPort. 341 // Script checks are not supported and will return an error. Registration is 342 // asynchronous. 343 // 344 // Agents will be deregistered when Shutdown is called. 345 func (c *ServiceClient) RegisterAgent(role string, services []*structs.Service) error { 346 ops := operations{} 347 348 for _, service := range services { 349 id := makeAgentServiceID(role, service) 350 351 // Unlike tasks, agents don't use port labels. Agent ports are 352 // stored directly in the PortLabel. 353 host, rawport, err := net.SplitHostPort(service.PortLabel) 354 if err != nil { 355 return fmt.Errorf("error parsing port label %q from service %q: %v", service.PortLabel, service.Name, err) 356 } 357 port, err := strconv.Atoi(rawport) 358 if err != nil { 359 return fmt.Errorf("error parsing port %q from service %q: %v", rawport, service.Name, err) 360 } 361 serviceReg := &api.AgentServiceRegistration{ 362 ID: id, 363 Name: service.Name, 364 Tags: service.Tags, 365 Address: host, 366 Port: port, 367 } 368 ops.regServices = append(ops.regServices, serviceReg) 369 370 for _, check := range service.Checks { 371 checkID := createCheckID(id, check) 372 if check.Type == structs.ServiceCheckScript { 373 return fmt.Errorf("service %q contains invalid check: agent checks do not support scripts", service.Name) 374 } 375 checkHost, checkPort := serviceReg.Address, serviceReg.Port 376 if check.PortLabel != "" { 377 // Unlike tasks, agents don't use port labels. Agent ports are 378 // stored directly in the PortLabel. 379 host, rawport, err := net.SplitHostPort(check.PortLabel) 380 if err != nil { 381 return fmt.Errorf("error parsing port label %q from check %q: %v", service.PortLabel, check.Name, err) 382 } 383 port, err := strconv.Atoi(rawport) 384 if err != nil { 385 return fmt.Errorf("error parsing port %q from check %q: %v", rawport, check.Name, err) 386 } 387 checkHost, checkPort = host, port 388 } 389 checkReg, err := createCheckReg(id, checkID, check, checkHost, checkPort) 390 if err != nil { 391 return fmt.Errorf("failed to add check %q: %v", check.Name, err) 392 } 393 ops.regChecks = append(ops.regChecks, checkReg) 394 } 395 } 396 397 // Don't bother committing agent checks if we're already shutting down 398 c.agentLock.Lock() 399 defer c.agentLock.Unlock() 400 select { 401 case <-c.shutdownCh: 402 return nil 403 default: 404 } 405 406 // Now add them to the registration queue 407 c.commit(&ops) 408 409 // Record IDs for deregistering on shutdown 410 for _, id := range ops.regServices { 411 c.agentServices[id.ID] = struct{}{} 412 } 413 for _, id := range ops.regChecks { 414 c.agentChecks[id.ID] = struct{}{} 415 } 416 return nil 417 } 418 419 // serviceRegs creates service registrations, check registrations, and script 420 // checks from a service. 421 func (c *ServiceClient) serviceRegs(ops *operations, allocID string, service *structs.Service, 422 exec driver.ScriptExecutor, task *structs.Task) error { 423 424 id := makeTaskServiceID(allocID, task.Name, service) 425 host, port := task.FindHostAndPortFor(service.PortLabel) 426 serviceReg := &api.AgentServiceRegistration{ 427 ID: id, 428 Name: service.Name, 429 Tags: make([]string, len(service.Tags)), 430 Address: host, 431 Port: port, 432 } 433 // copy isn't strictly necessary but can avoid bugs especially 434 // with tests that may reuse Tasks 435 copy(serviceReg.Tags, service.Tags) 436 ops.regServices = append(ops.regServices, serviceReg) 437 438 for _, check := range service.Checks { 439 if check.TLSSkipVerify && !c.skipVerifySupport { 440 c.logger.Printf("[WARN] consul.sync: skipping check %q for task %q alloc %q because Consul doesn't support tls_skip_verify. Please upgrade to Consul >= 0.7.2.", 441 check.Name, task.Name, allocID) 442 continue 443 } 444 checkID := createCheckID(id, check) 445 if check.Type == structs.ServiceCheckScript { 446 if exec == nil { 447 return fmt.Errorf("driver doesn't support script checks") 448 } 449 ops.scripts = append(ops.scripts, newScriptCheck( 450 allocID, task.Name, checkID, check, exec, c.client, c.logger, c.shutdownCh)) 451 452 } 453 454 host, port := serviceReg.Address, serviceReg.Port 455 if check.PortLabel != "" { 456 host, port = task.FindHostAndPortFor(check.PortLabel) 457 } 458 checkReg, err := createCheckReg(id, checkID, check, host, port) 459 if err != nil { 460 return fmt.Errorf("failed to add check %q: %v", check.Name, err) 461 } 462 ops.regChecks = append(ops.regChecks, checkReg) 463 } 464 return nil 465 } 466 467 // RegisterTask with Consul. Adds all sevice entries and checks to Consul. If 468 // exec is nil and a script check exists an error is returned. 469 // 470 // Actual communication with Consul is done asynchrously (see Run). 471 func (c *ServiceClient) RegisterTask(allocID string, task *structs.Task, exec driver.ScriptExecutor) error { 472 ops := &operations{} 473 for _, service := range task.Services { 474 if err := c.serviceRegs(ops, allocID, service, exec, task); err != nil { 475 return err 476 } 477 } 478 c.commit(ops) 479 return nil 480 } 481 482 // UpdateTask in Consul. Does not alter the service if only checks have 483 // changed. 484 func (c *ServiceClient) UpdateTask(allocID string, existing, newTask *structs.Task, exec driver.ScriptExecutor) error { 485 ops := &operations{} 486 487 existingIDs := make(map[string]*structs.Service, len(existing.Services)) 488 for _, s := range existing.Services { 489 existingIDs[makeTaskServiceID(allocID, existing.Name, s)] = s 490 } 491 newIDs := make(map[string]*structs.Service, len(newTask.Services)) 492 for _, s := range newTask.Services { 493 newIDs[makeTaskServiceID(allocID, newTask.Name, s)] = s 494 } 495 496 // Loop over existing Service IDs to see if they have been removed or 497 // updated. 498 for existingID, existingSvc := range existingIDs { 499 newSvc, ok := newIDs[existingID] 500 if !ok { 501 // Existing sevice entry removed 502 ops.deregServices = append(ops.deregServices, existingID) 503 for _, check := range existingSvc.Checks { 504 ops.deregChecks = append(ops.deregChecks, createCheckID(existingID, check)) 505 } 506 continue 507 } 508 509 if newSvc.PortLabel == existingSvc.PortLabel { 510 // Service exists and hasn't changed, don't add it later 511 delete(newIDs, existingID) 512 } 513 514 // Check to see what checks were updated 515 existingChecks := make(map[string]struct{}, len(existingSvc.Checks)) 516 for _, check := range existingSvc.Checks { 517 existingChecks[createCheckID(existingID, check)] = struct{}{} 518 } 519 520 // Register new checks 521 for _, check := range newSvc.Checks { 522 checkID := createCheckID(existingID, check) 523 if _, exists := existingChecks[checkID]; exists { 524 // Check exists, so don't remove it 525 delete(existingChecks, checkID) 526 } 527 } 528 529 // Remove existing checks not in updated service 530 for cid := range existingChecks { 531 ops.deregChecks = append(ops.deregChecks, cid) 532 } 533 } 534 535 // Any remaining services should just be enqueued directly 536 for _, newSvc := range newIDs { 537 err := c.serviceRegs(ops, allocID, newSvc, exec, newTask) 538 if err != nil { 539 return err 540 } 541 } 542 543 c.commit(ops) 544 return nil 545 } 546 547 // RemoveTask from Consul. Removes all service entries and checks. 548 // 549 // Actual communication with Consul is done asynchrously (see Run). 550 func (c *ServiceClient) RemoveTask(allocID string, task *structs.Task) { 551 ops := operations{} 552 553 for _, service := range task.Services { 554 id := makeTaskServiceID(allocID, task.Name, service) 555 ops.deregServices = append(ops.deregServices, id) 556 557 for _, check := range service.Checks { 558 ops.deregChecks = append(ops.deregChecks, createCheckID(id, check)) 559 } 560 } 561 562 // Now add them to the deregistration fields; main Run loop will update 563 c.commit(&ops) 564 } 565 566 // Shutdown the Consul client. Update running task registations and deregister 567 // agent from Consul. On first call blocks up to shutdownWait before giving up 568 // on syncing operations. 569 func (c *ServiceClient) Shutdown() error { 570 // Serialize Shutdown calls with RegisterAgent to prevent leaking agent 571 // entries. 572 c.agentLock.Lock() 573 select { 574 case <-c.shutdownCh: 575 return nil 576 default: 577 } 578 579 // Deregister Nomad agent Consul entries before closing shutdown. 580 ops := operations{} 581 for id := range c.agentServices { 582 ops.deregServices = append(ops.deregServices, id) 583 } 584 for id := range c.agentChecks { 585 ops.deregChecks = append(ops.deregChecks, id) 586 } 587 c.commit(&ops) 588 589 // Then signal shutdown 590 close(c.shutdownCh) 591 592 // Safe to unlock after shutdownCh closed as RegisterAgent will check 593 // shutdownCh before committing. 594 c.agentLock.Unlock() 595 596 // Give run loop time to sync, but don't block indefinitely 597 deadline := time.After(c.shutdownWait) 598 599 // Wait for Run to finish any outstanding operations and exit 600 select { 601 case <-c.exitCh: 602 case <-deadline: 603 // Don't wait forever though 604 return fmt.Errorf("timed out waiting for Consul operations to complete") 605 } 606 607 // Give script checks time to exit (no need to lock as Run() has exited) 608 for _, h := range c.runningScripts { 609 select { 610 case <-h.wait(): 611 case <-deadline: 612 return fmt.Errorf("timed out waiting for script checks to run") 613 } 614 } 615 return nil 616 } 617 618 // makeAgentServiceID creates a unique ID for identifying an agent service in 619 // Consul. 620 // 621 // Agent service IDs are of the form: 622 // 623 // {nomadServicePrefix}-{ROLE}-{Service.Name}-{Service.Tags...} 624 // Example Server ID: _nomad-server-nomad-serf 625 // Example Client ID: _nomad-client-nomad-client-http 626 // 627 func makeAgentServiceID(role string, service *structs.Service) string { 628 parts := make([]string, len(service.Tags)+3) 629 parts[0] = nomadServicePrefix 630 parts[1] = role 631 parts[2] = service.Name 632 copy(parts[3:], service.Tags) 633 return strings.Join(parts, "-") 634 } 635 636 // makeTaskServiceID creates a unique ID for identifying a task service in 637 // Consul. 638 // 639 // Task service IDs are of the form: 640 // 641 // {nomadServicePrefix}-executor-{ALLOC_ID}-{Service.Name}-{Service.Tags...} 642 // Example Service ID: _nomad-executor-1234-echo-http-tag1-tag2-tag3 643 // 644 func makeTaskServiceID(allocID, taskName string, service *structs.Service) string { 645 parts := make([]string, len(service.Tags)+5) 646 parts[0] = nomadServicePrefix 647 parts[1] = "executor" 648 parts[2] = allocID 649 parts[3] = taskName 650 parts[4] = service.Name 651 copy(parts[5:], service.Tags) 652 return strings.Join(parts, "-") 653 } 654 655 // createCheckID creates a unique ID for a check. 656 func createCheckID(serviceID string, check *structs.ServiceCheck) string { 657 return check.Hash(serviceID) 658 } 659 660 // createCheckReg creates a Check that can be registered with Consul. 661 // 662 // Script checks simply have a TTL set and the caller is responsible for 663 // running the script and heartbeating. 664 func createCheckReg(serviceID, checkID string, check *structs.ServiceCheck, host string, port int) (*api.AgentCheckRegistration, error) { 665 chkReg := api.AgentCheckRegistration{ 666 ID: checkID, 667 Name: check.Name, 668 ServiceID: serviceID, 669 } 670 chkReg.Status = check.InitialStatus 671 chkReg.Timeout = check.Timeout.String() 672 chkReg.Interval = check.Interval.String() 673 674 switch check.Type { 675 case structs.ServiceCheckHTTP: 676 if check.Protocol == "" { 677 check.Protocol = "http" 678 } 679 if check.TLSSkipVerify { 680 chkReg.TLSSkipVerify = true 681 } 682 base := url.URL{ 683 Scheme: check.Protocol, 684 Host: net.JoinHostPort(host, strconv.Itoa(port)), 685 } 686 relative, err := url.Parse(check.Path) 687 if err != nil { 688 return nil, err 689 } 690 url := base.ResolveReference(relative) 691 chkReg.HTTP = url.String() 692 case structs.ServiceCheckTCP: 693 chkReg.TCP = net.JoinHostPort(host, strconv.Itoa(port)) 694 case structs.ServiceCheckScript: 695 chkReg.TTL = (check.Interval + ttlCheckBuffer).String() 696 default: 697 return nil, fmt.Errorf("check type %+q not valid", check.Type) 698 } 699 return &chkReg, nil 700 } 701 702 // isNomadService returns true if the ID matches the pattern of a Nomad managed 703 // service. 704 func isNomadService(id string) bool { 705 return strings.HasPrefix(id, nomadServicePrefix) 706 }