github.com/bigcommerce/nomad@v0.9.3-bc/client/allocrunner/taskrunner/template/template.go (about) 1 package template 2 3 import ( 4 "context" 5 "fmt" 6 "math/rand" 7 "os" 8 "path/filepath" 9 "sort" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 15 ctconf "github.com/hashicorp/consul-template/config" 16 "github.com/hashicorp/consul-template/manager" 17 "github.com/hashicorp/consul-template/signals" 18 envparse "github.com/hashicorp/go-envparse" 19 multierror "github.com/hashicorp/go-multierror" 20 "github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces" 21 "github.com/hashicorp/nomad/client/config" 22 "github.com/hashicorp/nomad/client/taskenv" 23 "github.com/hashicorp/nomad/helper" 24 "github.com/hashicorp/nomad/nomad/structs" 25 ) 26 27 const ( 28 // consulTemplateSourceName is the source name when using the TaskHooks. 29 consulTemplateSourceName = "Template" 30 31 // hostSrcOption is the Client option that determines whether the template 32 // source may be from the host 33 hostSrcOption = "template.allow_host_source" 34 35 // missingDepEventLimit is the number of missing dependencies that will be 36 // logged before we switch to showing just the number of missing 37 // dependencies. 38 missingDepEventLimit = 3 39 40 // DefaultMaxTemplateEventRate is the default maximum rate at which a 41 // template event should be fired. 42 DefaultMaxTemplateEventRate = 3 * time.Second 43 ) 44 45 // TaskTemplateManager is used to run a set of templates for a given task 46 type TaskTemplateManager struct { 47 // config holds the template managers configuration 48 config *TaskTemplateManagerConfig 49 50 // lookup allows looking up the set of Nomad templates by their consul-template ID 51 lookup map[string][]*structs.Template 52 53 // runner is the consul-template runner 54 runner *manager.Runner 55 56 // signals is a lookup map from the string representation of a signal to its 57 // actual signal 58 signals map[string]os.Signal 59 60 // shutdownCh is used to signal and started goroutine to shutdown 61 shutdownCh chan struct{} 62 63 // shutdown marks whether the manager has been shutdown 64 shutdown bool 65 shutdownLock sync.Mutex 66 } 67 68 // TaskTemplateManagerConfig is used to configure an instance of the 69 // TaskTemplateManager 70 type TaskTemplateManagerConfig struct { 71 // UnblockCh is closed when the template has been rendered 72 UnblockCh chan struct{} 73 74 // Lifecycle is used to interact with the task the template manager is being 75 // run for 76 Lifecycle interfaces.TaskLifecycle 77 78 // Events is used to emit events for the task 79 Events interfaces.EventEmitter 80 81 // Templates is the set of templates we are managing 82 Templates []*structs.Template 83 84 // ClientConfig is the Nomad Client configuration 85 ClientConfig *config.Config 86 87 // VaultToken is the Vault token for the task. 88 VaultToken string 89 90 // TaskDir is the task's directory 91 TaskDir string 92 93 // EnvBuilder is the environment variable builder for the task. 94 EnvBuilder *taskenv.Builder 95 96 // MaxTemplateEventRate is the maximum rate at which we should emit events. 97 MaxTemplateEventRate time.Duration 98 99 // retryRate is only used for testing and is used to increase the retry rate 100 retryRate time.Duration 101 } 102 103 // Validate validates the configuration. 104 func (c *TaskTemplateManagerConfig) Validate() error { 105 if c == nil { 106 return fmt.Errorf("Nil config passed") 107 } else if c.UnblockCh == nil { 108 return fmt.Errorf("Invalid unblock channel given") 109 } else if c.Lifecycle == nil { 110 return fmt.Errorf("Invalid lifecycle hooks given") 111 } else if c.Events == nil { 112 return fmt.Errorf("Invalid event hook given") 113 } else if c.ClientConfig == nil { 114 return fmt.Errorf("Invalid client config given") 115 } else if c.TaskDir == "" { 116 return fmt.Errorf("Invalid task directory given: %q", c.TaskDir) 117 } else if c.EnvBuilder == nil { 118 return fmt.Errorf("Invalid task environment given") 119 } else if c.MaxTemplateEventRate == 0 { 120 return fmt.Errorf("Invalid max template event rate given") 121 } 122 123 return nil 124 } 125 126 func NewTaskTemplateManager(config *TaskTemplateManagerConfig) (*TaskTemplateManager, error) { 127 // Check pre-conditions 128 if err := config.Validate(); err != nil { 129 return nil, err 130 } 131 132 tm := &TaskTemplateManager{ 133 config: config, 134 shutdownCh: make(chan struct{}), 135 } 136 137 // Parse the signals that we need 138 for _, tmpl := range config.Templates { 139 if tmpl.ChangeSignal == "" { 140 continue 141 } 142 143 sig, err := signals.Parse(tmpl.ChangeSignal) 144 if err != nil { 145 return nil, fmt.Errorf("Failed to parse signal %q", tmpl.ChangeSignal) 146 } 147 148 if tm.signals == nil { 149 tm.signals = make(map[string]os.Signal) 150 } 151 152 tm.signals[tmpl.ChangeSignal] = sig 153 } 154 155 // Build the consul-template runner 156 runner, lookup, err := templateRunner(config) 157 if err != nil { 158 return nil, err 159 } 160 tm.runner = runner 161 tm.lookup = lookup 162 163 go tm.run() 164 return tm, nil 165 } 166 167 // Stop is used to stop the consul-template runner 168 func (tm *TaskTemplateManager) Stop() { 169 tm.shutdownLock.Lock() 170 defer tm.shutdownLock.Unlock() 171 172 if tm.shutdown { 173 return 174 } 175 176 close(tm.shutdownCh) 177 tm.shutdown = true 178 179 // Stop the consul-template runner 180 if tm.runner != nil { 181 tm.runner.Stop() 182 } 183 } 184 185 // run is the long lived loop that handles errors and templates being rendered 186 func (tm *TaskTemplateManager) run() { 187 // Runner is nil if there is no templates 188 if tm.runner == nil { 189 // Unblock the start if there is nothing to do 190 close(tm.config.UnblockCh) 191 return 192 } 193 194 // Start the runner 195 go tm.runner.Start() 196 197 // Block till all the templates have been rendered 198 tm.handleFirstRender() 199 200 // Detect if there was a shutdown. 201 select { 202 case <-tm.shutdownCh: 203 return 204 default: 205 } 206 207 // Read environment variables from env templates before we unblock 208 envMap, err := loadTemplateEnv(tm.config.Templates, tm.config.TaskDir, tm.config.EnvBuilder.Build()) 209 if err != nil { 210 tm.config.Lifecycle.Kill(context.Background(), 211 structs.NewTaskEvent(structs.TaskKilling). 212 SetFailsTask(). 213 SetDisplayMessage(fmt.Sprintf("Template failed to read environment variables: %v", err))) 214 return 215 } 216 tm.config.EnvBuilder.SetTemplateEnv(envMap) 217 218 // Unblock the task 219 close(tm.config.UnblockCh) 220 221 // If all our templates are change mode no-op, then we can exit here 222 if tm.allTemplatesNoop() { 223 return 224 } 225 226 // handle all subsequent render events. 227 tm.handleTemplateRerenders(time.Now()) 228 } 229 230 // handleFirstRender blocks till all templates have been rendered 231 func (tm *TaskTemplateManager) handleFirstRender() { 232 // missingDependencies is the set of missing dependencies. 233 var missingDependencies map[string]struct{} 234 235 // eventTimer is used to trigger the firing of an event showing the missing 236 // dependencies. 237 eventTimer := time.NewTimer(tm.config.MaxTemplateEventRate) 238 if !eventTimer.Stop() { 239 <-eventTimer.C 240 } 241 242 // outstandingEvent tracks whether there is an outstanding event that should 243 // be fired. 244 outstandingEvent := false 245 246 // Wait till all the templates have been rendered 247 WAIT: 248 for { 249 select { 250 case <-tm.shutdownCh: 251 return 252 case err, ok := <-tm.runner.ErrCh: 253 if !ok { 254 continue 255 } 256 257 tm.config.Lifecycle.Kill(context.Background(), 258 structs.NewTaskEvent(structs.TaskKilling). 259 SetFailsTask(). 260 SetDisplayMessage(fmt.Sprintf("Template failed: %v", err))) 261 case <-tm.runner.TemplateRenderedCh(): 262 // A template has been rendered, figure out what to do 263 events := tm.runner.RenderEvents() 264 265 // Not all templates have been rendered yet 266 if len(events) < len(tm.lookup) { 267 continue 268 } 269 270 for _, event := range events { 271 // This template hasn't been rendered 272 if event.LastWouldRender.IsZero() { 273 continue WAIT 274 } 275 } 276 277 break WAIT 278 case <-tm.runner.RenderEventCh(): 279 events := tm.runner.RenderEvents() 280 joinedSet := make(map[string]struct{}) 281 for _, event := range events { 282 missing := event.MissingDeps 283 if missing == nil { 284 continue 285 } 286 287 for _, dep := range missing.List() { 288 joinedSet[dep.String()] = struct{}{} 289 } 290 } 291 292 // Check to see if the new joined set is the same as the old 293 different := len(joinedSet) != len(missingDependencies) 294 if !different { 295 for k := range joinedSet { 296 if _, ok := missingDependencies[k]; !ok { 297 different = true 298 break 299 } 300 } 301 } 302 303 // Nothing to do 304 if !different { 305 continue 306 } 307 308 // Update the missing set 309 missingDependencies = joinedSet 310 311 // Update the event timer channel 312 if !outstandingEvent { 313 // We got new data so reset 314 outstandingEvent = true 315 eventTimer.Reset(tm.config.MaxTemplateEventRate) 316 } 317 case <-eventTimer.C: 318 if missingDependencies == nil { 319 continue 320 } 321 322 // Clear the outstanding event 323 outstandingEvent = false 324 325 // Build the missing set 326 missingSlice := make([]string, 0, len(missingDependencies)) 327 for k := range missingDependencies { 328 missingSlice = append(missingSlice, k) 329 } 330 sort.Strings(missingSlice) 331 332 if l := len(missingSlice); l > missingDepEventLimit { 333 missingSlice[missingDepEventLimit] = fmt.Sprintf("and %d more", l-missingDepEventLimit) 334 missingSlice = missingSlice[:missingDepEventLimit+1] 335 } 336 337 missingStr := strings.Join(missingSlice, ", ") 338 tm.config.Events.EmitEvent(structs.NewTaskEvent(consulTemplateSourceName).SetDisplayMessage(fmt.Sprintf("Missing: %s", missingStr))) 339 } 340 } 341 } 342 343 // handleTemplateRerenders is used to handle template render events after they 344 // have all rendered. It takes action based on which set of templates re-render. 345 // The passed allRenderedTime is the time at which all templates have rendered. 346 // This is used to avoid signaling the task for any render event before hand. 347 func (tm *TaskTemplateManager) handleTemplateRerenders(allRenderedTime time.Time) { 348 // A lookup for the last time the template was handled 349 handledRenders := make(map[string]time.Time, len(tm.config.Templates)) 350 351 for { 352 select { 353 case <-tm.shutdownCh: 354 return 355 case err, ok := <-tm.runner.ErrCh: 356 if !ok { 357 continue 358 } 359 360 tm.config.Lifecycle.Kill(context.Background(), 361 structs.NewTaskEvent(structs.TaskKilling). 362 SetFailsTask(). 363 SetDisplayMessage(fmt.Sprintf("Template failed: %v", err))) 364 case <-tm.runner.TemplateRenderedCh(): 365 // A template has been rendered, figure out what to do 366 var handling []string 367 signals := make(map[string]struct{}) 368 restart := false 369 var splay time.Duration 370 371 events := tm.runner.RenderEvents() 372 for id, event := range events { 373 374 // First time through 375 if allRenderedTime.After(event.LastDidRender) || allRenderedTime.Equal(event.LastDidRender) { 376 handledRenders[id] = allRenderedTime 377 continue 378 } 379 380 // We have already handled this one 381 if htime := handledRenders[id]; htime.After(event.LastDidRender) || htime.Equal(event.LastDidRender) { 382 continue 383 } 384 385 // Lookup the template and determine what to do 386 tmpls, ok := tm.lookup[id] 387 if !ok { 388 tm.config.Lifecycle.Kill(context.Background(), 389 structs.NewTaskEvent(structs.TaskKilling). 390 SetFailsTask(). 391 SetDisplayMessage(fmt.Sprintf("Template runner returned unknown template id %q", id))) 392 return 393 } 394 395 // Read environment variables from templates 396 envMap, err := loadTemplateEnv(tm.config.Templates, tm.config.TaskDir, tm.config.EnvBuilder.Build()) 397 if err != nil { 398 tm.config.Lifecycle.Kill(context.Background(), 399 structs.NewTaskEvent(structs.TaskKilling). 400 SetFailsTask(). 401 SetDisplayMessage(fmt.Sprintf("Template failed to read environment variables: %v", err))) 402 return 403 } 404 tm.config.EnvBuilder.SetTemplateEnv(envMap) 405 406 for _, tmpl := range tmpls { 407 switch tmpl.ChangeMode { 408 case structs.TemplateChangeModeSignal: 409 signals[tmpl.ChangeSignal] = struct{}{} 410 case structs.TemplateChangeModeRestart: 411 restart = true 412 case structs.TemplateChangeModeNoop: 413 continue 414 } 415 416 if tmpl.Splay > splay { 417 splay = tmpl.Splay 418 } 419 } 420 421 handling = append(handling, id) 422 } 423 424 if restart || len(signals) != 0 { 425 if splay != 0 { 426 ns := splay.Nanoseconds() 427 offset := rand.Int63n(ns) 428 t := time.Duration(offset) 429 430 select { 431 case <-time.After(t): 432 case <-tm.shutdownCh: 433 return 434 } 435 } 436 437 // Update handle time 438 for _, id := range handling { 439 handledRenders[id] = events[id].LastDidRender 440 } 441 442 if restart { 443 tm.config.Lifecycle.Restart(context.Background(), 444 structs.NewTaskEvent(structs.TaskRestartSignal). 445 SetDisplayMessage("Template with change_mode restart re-rendered"), false) 446 } else if len(signals) != 0 { 447 var mErr multierror.Error 448 for signal := range signals { 449 s := tm.signals[signal] 450 event := structs.NewTaskEvent(structs.TaskSignaling).SetTaskSignal(s).SetDisplayMessage("Template re-rendered") 451 if err := tm.config.Lifecycle.Signal(event, signal); err != nil { 452 multierror.Append(&mErr, err) 453 } 454 } 455 456 if err := mErr.ErrorOrNil(); err != nil { 457 flat := make([]os.Signal, 0, len(signals)) 458 for signal := range signals { 459 flat = append(flat, tm.signals[signal]) 460 } 461 462 tm.config.Lifecycle.Kill(context.Background(), 463 structs.NewTaskEvent(structs.TaskKilling). 464 SetFailsTask(). 465 SetDisplayMessage(fmt.Sprintf("Template failed to send signals %v: %v", flat, err))) 466 } 467 } 468 } 469 } 470 } 471 } 472 473 // allTemplatesNoop returns whether all the managed templates have change mode noop. 474 func (tm *TaskTemplateManager) allTemplatesNoop() bool { 475 for _, tmpl := range tm.config.Templates { 476 if tmpl.ChangeMode != structs.TemplateChangeModeNoop { 477 return false 478 } 479 } 480 481 return true 482 } 483 484 // templateRunner returns a consul-template runner for the given templates and a 485 // lookup by destination to the template. If no templates are in the config, a 486 // nil template runner and lookup is returned. 487 func templateRunner(config *TaskTemplateManagerConfig) ( 488 *manager.Runner, map[string][]*structs.Template, error) { 489 490 if len(config.Templates) == 0 { 491 return nil, nil, nil 492 } 493 494 // Parse the templates 495 ctmplMapping, err := parseTemplateConfigs(config) 496 if err != nil { 497 return nil, nil, err 498 } 499 500 // Create the runner configuration. 501 runnerConfig, err := newRunnerConfig(config, ctmplMapping) 502 if err != nil { 503 return nil, nil, err 504 } 505 506 runner, err := manager.NewRunner(runnerConfig, false, false) 507 if err != nil { 508 return nil, nil, err 509 } 510 511 // Set Nomad's environment variables 512 runner.Env = config.EnvBuilder.Build().All() 513 514 // Build the lookup 515 idMap := runner.TemplateConfigMapping() 516 lookup := make(map[string][]*structs.Template, len(idMap)) 517 for id, ctmpls := range idMap { 518 for _, ctmpl := range ctmpls { 519 templates := lookup[id] 520 templates = append(templates, ctmplMapping[ctmpl]) 521 lookup[id] = templates 522 } 523 } 524 525 return runner, lookup, nil 526 } 527 528 // parseTemplateConfigs converts the tasks templates in the config into 529 // consul-templates 530 func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[ctconf.TemplateConfig]*structs.Template, error) { 531 allowAbs := config.ClientConfig.ReadBoolDefault(hostSrcOption, true) 532 taskEnv := config.EnvBuilder.Build() 533 534 ctmpls := make(map[ctconf.TemplateConfig]*structs.Template, len(config.Templates)) 535 for _, tmpl := range config.Templates { 536 var src, dest string 537 if tmpl.SourcePath != "" { 538 if filepath.IsAbs(tmpl.SourcePath) { 539 if !allowAbs { 540 return nil, fmt.Errorf("Specifying absolute template paths disallowed by client config: %q", tmpl.SourcePath) 541 } 542 543 src = tmpl.SourcePath 544 } else { 545 src = filepath.Join(config.TaskDir, taskEnv.ReplaceEnv(tmpl.SourcePath)) 546 } 547 } 548 if tmpl.DestPath != "" { 549 dest = filepath.Join(config.TaskDir, taskEnv.ReplaceEnv(tmpl.DestPath)) 550 } 551 552 ct := ctconf.DefaultTemplateConfig() 553 ct.Source = &src 554 ct.Destination = &dest 555 ct.Contents = &tmpl.EmbeddedTmpl 556 ct.LeftDelim = &tmpl.LeftDelim 557 ct.RightDelim = &tmpl.RightDelim 558 559 // Set the permissions 560 if tmpl.Perms != "" { 561 v, err := strconv.ParseUint(tmpl.Perms, 8, 12) 562 if err != nil { 563 return nil, fmt.Errorf("Failed to parse %q as octal: %v", tmpl.Perms, err) 564 } 565 m := os.FileMode(v) 566 ct.Perms = &m 567 } 568 ct.Finalize() 569 570 ctmpls[*ct] = tmpl 571 } 572 573 return ctmpls, nil 574 } 575 576 // newRunnerConfig returns a consul-template runner configuration, setting the 577 // Vault and Consul configurations based on the clients configs. 578 func newRunnerConfig(config *TaskTemplateManagerConfig, 579 templateMapping map[ctconf.TemplateConfig]*structs.Template) (*ctconf.Config, error) { 580 581 cc := config.ClientConfig 582 conf := ctconf.DefaultConfig() 583 584 // Gather the consul-template templates 585 flat := ctconf.TemplateConfigs(make([]*ctconf.TemplateConfig, 0, len(templateMapping))) 586 for ctmpl := range templateMapping { 587 local := ctmpl 588 flat = append(flat, &local) 589 } 590 conf.Templates = &flat 591 592 // Go through the templates and determine the minimum Vault grace 593 vaultGrace := time.Duration(-1) 594 for _, tmpl := range templateMapping { 595 // Initial condition 596 if vaultGrace < 0 { 597 vaultGrace = tmpl.VaultGrace 598 } else if tmpl.VaultGrace < vaultGrace { 599 vaultGrace = tmpl.VaultGrace 600 } 601 } 602 603 // Force faster retries 604 if config.retryRate != 0 { 605 rate := config.retryRate 606 conf.Consul.Retry.Backoff = &rate 607 } 608 609 // Setup the Consul config 610 if cc.ConsulConfig != nil { 611 conf.Consul.Address = &cc.ConsulConfig.Addr 612 conf.Consul.Token = &cc.ConsulConfig.Token 613 614 if cc.ConsulConfig.EnableSSL != nil && *cc.ConsulConfig.EnableSSL { 615 verify := cc.ConsulConfig.VerifySSL != nil && *cc.ConsulConfig.VerifySSL 616 conf.Consul.SSL = &ctconf.SSLConfig{ 617 Enabled: helper.BoolToPtr(true), 618 Verify: &verify, 619 Cert: &cc.ConsulConfig.CertFile, 620 Key: &cc.ConsulConfig.KeyFile, 621 CaCert: &cc.ConsulConfig.CAFile, 622 } 623 } 624 625 if cc.ConsulConfig.Auth != "" { 626 parts := strings.SplitN(cc.ConsulConfig.Auth, ":", 2) 627 if len(parts) != 2 { 628 return nil, fmt.Errorf("Failed to parse Consul Auth config") 629 } 630 631 conf.Consul.Auth = &ctconf.AuthConfig{ 632 Enabled: helper.BoolToPtr(true), 633 Username: &parts[0], 634 Password: &parts[1], 635 } 636 } 637 } 638 639 // Setup the Vault config 640 // Always set these to ensure nothing is picked up from the environment 641 emptyStr := "" 642 conf.Vault.RenewToken = helper.BoolToPtr(false) 643 conf.Vault.Token = &emptyStr 644 if cc.VaultConfig != nil && cc.VaultConfig.IsEnabled() { 645 conf.Vault.Address = &cc.VaultConfig.Addr 646 conf.Vault.Token = &config.VaultToken 647 conf.Vault.Grace = helper.TimeToPtr(vaultGrace) 648 if config.ClientConfig.VaultConfig.Namespace != "" { 649 conf.Vault.Namespace = &config.ClientConfig.VaultConfig.Namespace 650 } 651 652 if strings.HasPrefix(cc.VaultConfig.Addr, "https") || cc.VaultConfig.TLSCertFile != "" { 653 skipVerify := cc.VaultConfig.TLSSkipVerify != nil && *cc.VaultConfig.TLSSkipVerify 654 verify := !skipVerify 655 conf.Vault.SSL = &ctconf.SSLConfig{ 656 Enabled: helper.BoolToPtr(true), 657 Verify: &verify, 658 Cert: &cc.VaultConfig.TLSCertFile, 659 Key: &cc.VaultConfig.TLSKeyFile, 660 CaCert: &cc.VaultConfig.TLSCaFile, 661 CaPath: &cc.VaultConfig.TLSCaPath, 662 ServerName: &cc.VaultConfig.TLSServerName, 663 } 664 } else { 665 conf.Vault.SSL = &ctconf.SSLConfig{ 666 Enabled: helper.BoolToPtr(false), 667 Verify: helper.BoolToPtr(false), 668 Cert: &emptyStr, 669 Key: &emptyStr, 670 CaCert: &emptyStr, 671 CaPath: &emptyStr, 672 ServerName: &emptyStr, 673 } 674 } 675 } 676 677 conf.Finalize() 678 return conf, nil 679 } 680 681 // loadTemplateEnv loads task environment variables from all templates. 682 func loadTemplateEnv(tmpls []*structs.Template, taskDir string, taskEnv *taskenv.TaskEnv) (map[string]string, error) { 683 all := make(map[string]string, 50) 684 for _, t := range tmpls { 685 if !t.Envvars { 686 continue 687 } 688 689 dest := filepath.Join(taskDir, taskEnv.ReplaceEnv(t.DestPath)) 690 f, err := os.Open(dest) 691 if err != nil { 692 return nil, fmt.Errorf("error opening env template: %v", err) 693 } 694 defer f.Close() 695 696 // Parse environment fil 697 vars, err := envparse.Parse(f) 698 if err != nil { 699 return nil, fmt.Errorf("error parsing env template %q: %v", dest, err) 700 } 701 for k, v := range vars { 702 all[k] = v 703 } 704 } 705 return all, nil 706 }