github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/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) 507 if err != nil { 508 return nil, nil, err 509 } 510 511 // Set Nomad's environment variables. 512 // consul-template falls back to the host process environment if a 513 // variable isn't explicitly set in the configuration, so we need 514 // to mask the environment out to ensure only the task env vars are 515 // available. 516 runner.Env = maskProcessEnv(config.EnvBuilder.Build().All()) 517 518 // Build the lookup 519 idMap := runner.TemplateConfigMapping() 520 lookup := make(map[string][]*structs.Template, len(idMap)) 521 for id, ctmpls := range idMap { 522 for _, ctmpl := range ctmpls { 523 templates := lookup[id] 524 templates = append(templates, ctmplMapping[ctmpl]) 525 lookup[id] = templates 526 } 527 } 528 529 return runner, lookup, nil 530 } 531 532 // maskProcessEnv masks away any environment variable not found in task env. 533 // It manipulates the parameter directly and returns it without copying. 534 func maskProcessEnv(env map[string]string) map[string]string { 535 procEnvs := os.Environ() 536 for _, e := range procEnvs { 537 ekv := strings.SplitN(e, "=", 2) 538 if _, ok := env[ekv[0]]; !ok { 539 env[ekv[0]] = "" 540 } 541 } 542 543 return env 544 } 545 546 // parseTemplateConfigs converts the tasks templates in the config into 547 // consul-templates 548 func parseTemplateConfigs(config *TaskTemplateManagerConfig) (map[*ctconf.TemplateConfig]*structs.Template, error) { 549 allowAbs := config.ClientConfig.ReadBoolDefault(hostSrcOption, true) 550 taskEnv := config.EnvBuilder.Build() 551 552 ctmpls := make(map[*ctconf.TemplateConfig]*structs.Template, len(config.Templates)) 553 for _, tmpl := range config.Templates { 554 var src, dest string 555 if tmpl.SourcePath != "" { 556 if filepath.IsAbs(tmpl.SourcePath) { 557 if !allowAbs { 558 return nil, fmt.Errorf("Specifying absolute template paths disallowed by client config: %q", tmpl.SourcePath) 559 } 560 561 src = tmpl.SourcePath 562 } else { 563 src = filepath.Join(config.TaskDir, taskEnv.ReplaceEnv(tmpl.SourcePath)) 564 } 565 } 566 if tmpl.DestPath != "" { 567 dest = filepath.Join(config.TaskDir, taskEnv.ReplaceEnv(tmpl.DestPath)) 568 } 569 570 ct := ctconf.DefaultTemplateConfig() 571 ct.Source = &src 572 ct.Destination = &dest 573 ct.Contents = &tmpl.EmbeddedTmpl 574 ct.LeftDelim = &tmpl.LeftDelim 575 ct.RightDelim = &tmpl.RightDelim 576 ct.FunctionBlacklist = config.ClientConfig.TemplateConfig.FunctionBlacklist 577 if !config.ClientConfig.TemplateConfig.DisableSandbox { 578 ct.SandboxPath = &config.TaskDir 579 } 580 581 // Set the permissions 582 if tmpl.Perms != "" { 583 v, err := strconv.ParseUint(tmpl.Perms, 8, 12) 584 if err != nil { 585 return nil, fmt.Errorf("Failed to parse %q as octal: %v", tmpl.Perms, err) 586 } 587 m := os.FileMode(v) 588 ct.Perms = &m 589 } 590 ct.Finalize() 591 592 ctmpls[ct] = tmpl 593 } 594 595 return ctmpls, nil 596 } 597 598 // newRunnerConfig returns a consul-template runner configuration, setting the 599 // Vault and Consul configurations based on the clients configs. 600 func newRunnerConfig(config *TaskTemplateManagerConfig, 601 templateMapping map[*ctconf.TemplateConfig]*structs.Template) (*ctconf.Config, error) { 602 603 cc := config.ClientConfig 604 conf := ctconf.DefaultConfig() 605 606 // Gather the consul-template templates 607 flat := ctconf.TemplateConfigs(make([]*ctconf.TemplateConfig, 0, len(templateMapping))) 608 for ctmpl := range templateMapping { 609 local := ctmpl 610 flat = append(flat, local) 611 } 612 conf.Templates = &flat 613 614 // Force faster retries 615 if config.retryRate != 0 { 616 rate := config.retryRate 617 conf.Consul.Retry.Backoff = &rate 618 } 619 620 // Setup the Consul config 621 if cc.ConsulConfig != nil { 622 conf.Consul.Address = &cc.ConsulConfig.Addr 623 conf.Consul.Token = &cc.ConsulConfig.Token 624 625 if cc.ConsulConfig.EnableSSL != nil && *cc.ConsulConfig.EnableSSL { 626 verify := cc.ConsulConfig.VerifySSL != nil && *cc.ConsulConfig.VerifySSL 627 conf.Consul.SSL = &ctconf.SSLConfig{ 628 Enabled: helper.BoolToPtr(true), 629 Verify: &verify, 630 Cert: &cc.ConsulConfig.CertFile, 631 Key: &cc.ConsulConfig.KeyFile, 632 CaCert: &cc.ConsulConfig.CAFile, 633 } 634 } 635 636 if cc.ConsulConfig.Auth != "" { 637 parts := strings.SplitN(cc.ConsulConfig.Auth, ":", 2) 638 if len(parts) != 2 { 639 return nil, fmt.Errorf("Failed to parse Consul Auth config") 640 } 641 642 conf.Consul.Auth = &ctconf.AuthConfig{ 643 Enabled: helper.BoolToPtr(true), 644 Username: &parts[0], 645 Password: &parts[1], 646 } 647 } 648 } 649 650 // Setup the Vault config 651 // Always set these to ensure nothing is picked up from the environment 652 emptyStr := "" 653 conf.Vault.RenewToken = helper.BoolToPtr(false) 654 conf.Vault.Token = &emptyStr 655 if cc.VaultConfig != nil && cc.VaultConfig.IsEnabled() { 656 conf.Vault.Address = &cc.VaultConfig.Addr 657 conf.Vault.Token = &config.VaultToken 658 if config.ClientConfig.VaultConfig.Namespace != "" { 659 conf.Vault.Namespace = &config.ClientConfig.VaultConfig.Namespace 660 } 661 662 if strings.HasPrefix(cc.VaultConfig.Addr, "https") || cc.VaultConfig.TLSCertFile != "" { 663 skipVerify := cc.VaultConfig.TLSSkipVerify != nil && *cc.VaultConfig.TLSSkipVerify 664 verify := !skipVerify 665 conf.Vault.SSL = &ctconf.SSLConfig{ 666 Enabled: helper.BoolToPtr(true), 667 Verify: &verify, 668 Cert: &cc.VaultConfig.TLSCertFile, 669 Key: &cc.VaultConfig.TLSKeyFile, 670 CaCert: &cc.VaultConfig.TLSCaFile, 671 CaPath: &cc.VaultConfig.TLSCaPath, 672 ServerName: &cc.VaultConfig.TLSServerName, 673 } 674 } else { 675 conf.Vault.SSL = &ctconf.SSLConfig{ 676 Enabled: helper.BoolToPtr(false), 677 Verify: helper.BoolToPtr(false), 678 Cert: &emptyStr, 679 Key: &emptyStr, 680 CaCert: &emptyStr, 681 CaPath: &emptyStr, 682 ServerName: &emptyStr, 683 } 684 } 685 } 686 687 conf.Finalize() 688 return conf, nil 689 } 690 691 // loadTemplateEnv loads task environment variables from all templates. 692 func loadTemplateEnv(tmpls []*structs.Template, taskDir string, taskEnv *taskenv.TaskEnv) (map[string]string, error) { 693 all := make(map[string]string, 50) 694 for _, t := range tmpls { 695 if !t.Envvars { 696 continue 697 } 698 699 dest := filepath.Join(taskDir, taskEnv.ReplaceEnv(t.DestPath)) 700 f, err := os.Open(dest) 701 if err != nil { 702 return nil, fmt.Errorf("error opening env template: %v", err) 703 } 704 defer f.Close() 705 706 // Parse environment fil 707 vars, err := envparse.Parse(f) 708 if err != nil { 709 return nil, fmt.Errorf("error parsing env template %q: %v", dest, err) 710 } 711 for k, v := range vars { 712 all[k] = v 713 } 714 } 715 return all, nil 716 }