github.com/hhrutter/nomad@v0.6.0-rc2.0.20170723054333-80c4b03f0705/client/consul_template.go (about) 1 package client 2 3 import ( 4 "fmt" 5 "math/rand" 6 "os" 7 "path/filepath" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 ctconf "github.com/hashicorp/consul-template/config" 14 "github.com/hashicorp/consul-template/manager" 15 "github.com/hashicorp/consul-template/signals" 16 envparse "github.com/hashicorp/go-envparse" 17 multierror "github.com/hashicorp/go-multierror" 18 "github.com/hashicorp/nomad/client/config" 19 "github.com/hashicorp/nomad/client/driver/env" 20 "github.com/hashicorp/nomad/nomad/structs" 21 ) 22 23 const ( 24 // hostSrcOption is the Client option that determines whether the template 25 // source may be from the host 26 hostSrcOption = "template.allow_host_source" 27 ) 28 29 var ( 30 // testRetryRate is used to speed up tests by setting consul-templates retry 31 // rate to something low 32 testRetryRate time.Duration = 0 33 ) 34 35 // TaskHooks is an interface which provides hooks into the tasks life-cycle 36 type TaskHooks interface { 37 // Restart is used to restart the task 38 Restart(source, reason string) 39 40 // Signal is used to signal the task 41 Signal(source, reason string, s os.Signal) error 42 43 // UnblockStart is used to unblock the starting of the task. This should be 44 // called after prestart work is completed 45 UnblockStart(source string) 46 47 // Kill is used to kill the task because of the passed error. If fail is set 48 // to true, the task is marked as failed 49 Kill(source, reason string, fail bool) 50 } 51 52 // TaskTemplateManager is used to run a set of templates for a given task 53 type TaskTemplateManager struct { 54 // templates is the set of templates we are managing 55 templates []*structs.Template 56 57 // lookup allows looking up the set of Nomad templates by their consul-template ID 58 lookup map[string][]*structs.Template 59 60 // hooks is used to signal/restart the task as templates are rendered 61 hook TaskHooks 62 63 // runner is the consul-template runner 64 runner *manager.Runner 65 66 // signals is a lookup map from the string representation of a signal to its 67 // actual signal 68 signals map[string]os.Signal 69 70 // shutdownCh is used to signal and started goroutine to shutdown 71 shutdownCh chan struct{} 72 73 // shutdown marks whether the manager has been shutdown 74 shutdown bool 75 shutdownLock sync.Mutex 76 } 77 78 func NewTaskTemplateManager(hook TaskHooks, tmpls []*structs.Template, 79 config *config.Config, vaultToken, taskDir string, 80 envBuilder *env.Builder) (*TaskTemplateManager, error) { 81 82 // Check pre-conditions 83 if hook == nil { 84 return nil, fmt.Errorf("Invalid task hook given") 85 } else if config == nil { 86 return nil, fmt.Errorf("Invalid config given") 87 } else if taskDir == "" { 88 return nil, fmt.Errorf("Invalid task directory given") 89 } else if envBuilder == nil { 90 return nil, fmt.Errorf("Invalid task environment given") 91 } 92 93 tm := &TaskTemplateManager{ 94 templates: tmpls, 95 hook: hook, 96 shutdownCh: make(chan struct{}), 97 } 98 99 // Parse the signals that we need 100 for _, tmpl := range tmpls { 101 if tmpl.ChangeSignal == "" { 102 continue 103 } 104 105 sig, err := signals.Parse(tmpl.ChangeSignal) 106 if err != nil { 107 return nil, fmt.Errorf("Failed to parse signal %q", tmpl.ChangeSignal) 108 } 109 110 if tm.signals == nil { 111 tm.signals = make(map[string]os.Signal) 112 } 113 114 tm.signals[tmpl.ChangeSignal] = sig 115 } 116 117 // Build the consul-template runner 118 runner, lookup, err := templateRunner(tmpls, config, vaultToken, taskDir, envBuilder.Build()) 119 if err != nil { 120 return nil, err 121 } 122 tm.runner = runner 123 tm.lookup = lookup 124 125 go tm.run(envBuilder, taskDir) 126 return tm, nil 127 } 128 129 // Stop is used to stop the consul-template runner 130 func (tm *TaskTemplateManager) Stop() { 131 tm.shutdownLock.Lock() 132 defer tm.shutdownLock.Unlock() 133 134 if tm.shutdown { 135 return 136 } 137 138 close(tm.shutdownCh) 139 tm.shutdown = true 140 141 // Stop the consul-template runner 142 if tm.runner != nil { 143 tm.runner.Stop() 144 } 145 } 146 147 // run is the long lived loop that handles errors and templates being rendered 148 func (tm *TaskTemplateManager) run(envBuilder *env.Builder, taskDir string) { 149 // Runner is nil if there is no templates 150 if tm.runner == nil { 151 // Unblock the start if there is nothing to do 152 tm.hook.UnblockStart("consul-template") 153 return 154 } 155 156 // Start the runner 157 go tm.runner.Start() 158 159 // Track when they have all been rendered so we don't signal the task for 160 // any render event before hand 161 var allRenderedTime time.Time 162 163 // Handle the first rendering 164 // Wait till all the templates have been rendered 165 WAIT: 166 for { 167 select { 168 case <-tm.shutdownCh: 169 return 170 case err, ok := <-tm.runner.ErrCh: 171 if !ok { 172 continue 173 } 174 175 tm.hook.Kill("consul-template", err.Error(), true) 176 case <-tm.runner.TemplateRenderedCh(): 177 // A template has been rendered, figure out what to do 178 events := tm.runner.RenderEvents() 179 180 // Not all templates have been rendered yet 181 if len(events) < len(tm.lookup) { 182 continue 183 } 184 185 for _, event := range events { 186 // This template hasn't been rendered 187 if event.LastWouldRender.IsZero() { 188 continue WAIT 189 } 190 } 191 192 break WAIT 193 } 194 } 195 196 // Read environment variables from env templates 197 envMap, err := loadTemplateEnv(tm.templates, taskDir) 198 if err != nil { 199 tm.hook.Kill("consul-template", err.Error(), true) 200 return 201 } 202 envBuilder.SetTemplateEnv(envMap) 203 204 allRenderedTime = time.Now() 205 tm.hook.UnblockStart("consul-template") 206 207 // If all our templates are change mode no-op, then we can exit here 208 if tm.allTemplatesNoop() { 209 return 210 } 211 212 // A lookup for the last time the template was handled 213 numTemplates := len(tm.templates) 214 handledRenders := make(map[string]time.Time, numTemplates) 215 216 for { 217 select { 218 case <-tm.shutdownCh: 219 return 220 case err, ok := <-tm.runner.ErrCh: 221 if !ok { 222 continue 223 } 224 225 tm.hook.Kill("consul-template", err.Error(), true) 226 case <-tm.runner.TemplateRenderedCh(): 227 // A template has been rendered, figure out what to do 228 var handling []string 229 signals := make(map[string]struct{}) 230 restart := false 231 var splay time.Duration 232 233 events := tm.runner.RenderEvents() 234 for id, event := range events { 235 236 // First time through 237 if allRenderedTime.After(event.LastDidRender) || allRenderedTime.Equal(event.LastDidRender) { 238 handledRenders[id] = allRenderedTime 239 continue 240 } 241 242 // We have already handled this one 243 if htime := handledRenders[id]; htime.After(event.LastDidRender) || htime.Equal(event.LastDidRender) { 244 continue 245 } 246 247 // Lookup the template and determine what to do 248 tmpls, ok := tm.lookup[id] 249 if !ok { 250 tm.hook.Kill("consul-template", fmt.Sprintf("consul-template runner returned unknown template id %q", id), true) 251 return 252 } 253 254 // Read environment variables from templates 255 envMap, err := loadTemplateEnv(tmpls, taskDir) 256 if err != nil { 257 tm.hook.Kill("consul-template", err.Error(), true) 258 return 259 } 260 envBuilder.SetTemplateEnv(envMap) 261 262 for _, tmpl := range tmpls { 263 switch tmpl.ChangeMode { 264 case structs.TemplateChangeModeSignal: 265 signals[tmpl.ChangeSignal] = struct{}{} 266 case structs.TemplateChangeModeRestart: 267 restart = true 268 case structs.TemplateChangeModeNoop: 269 continue 270 } 271 272 if tmpl.Splay > splay { 273 splay = tmpl.Splay 274 } 275 } 276 277 handling = append(handling, id) 278 } 279 280 if restart || len(signals) != 0 { 281 if splay != 0 { 282 ns := splay.Nanoseconds() 283 offset := rand.Int63n(ns) 284 t := time.Duration(offset) 285 286 select { 287 case <-time.After(t): 288 case <-tm.shutdownCh: 289 return 290 } 291 } 292 293 // Update handle time 294 for _, id := range handling { 295 handledRenders[id] = events[id].LastDidRender 296 } 297 298 if restart { 299 tm.hook.Restart("consul-template", "template with change_mode restart re-rendered") 300 } else if len(signals) != 0 { 301 var mErr multierror.Error 302 for signal := range signals { 303 err := tm.hook.Signal("consul-template", "template re-rendered", tm.signals[signal]) 304 if err != nil { 305 multierror.Append(&mErr, err) 306 } 307 } 308 309 if err := mErr.ErrorOrNil(); err != nil { 310 flat := make([]os.Signal, 0, len(signals)) 311 for signal := range signals { 312 flat = append(flat, tm.signals[signal]) 313 } 314 tm.hook.Kill("consul-template", fmt.Sprintf("Sending signals %v failed: %v", flat, err), true) 315 } 316 } 317 } 318 } 319 } 320 } 321 322 // allTemplatesNoop returns whether all the managed templates have change mode noop. 323 func (tm *TaskTemplateManager) allTemplatesNoop() bool { 324 for _, tmpl := range tm.templates { 325 if tmpl.ChangeMode != structs.TemplateChangeModeNoop { 326 return false 327 } 328 } 329 330 return true 331 } 332 333 // templateRunner returns a consul-template runner for the given templates and a 334 // lookup by destination to the template. If no templates are given, a nil 335 // template runner and lookup is returned. 336 func templateRunner(tmpls []*structs.Template, config *config.Config, 337 vaultToken, taskDir string, taskEnv *env.TaskEnv) ( 338 *manager.Runner, map[string][]*structs.Template, error) { 339 340 if len(tmpls) == 0 { 341 return nil, nil, nil 342 } 343 344 runnerConfig, err := runnerConfig(config, vaultToken) 345 if err != nil { 346 return nil, nil, err 347 } 348 349 // Parse the templates 350 allowAbs := config.ReadBoolDefault(hostSrcOption, true) 351 ctmplMapping, err := parseTemplateConfigs(tmpls, taskDir, taskEnv, allowAbs) 352 if err != nil { 353 return nil, nil, err 354 } 355 356 // Set the config 357 flat := ctconf.TemplateConfigs(make([]*ctconf.TemplateConfig, 0, len(ctmplMapping))) 358 for ctmpl := range ctmplMapping { 359 local := ctmpl 360 flat = append(flat, &local) 361 } 362 runnerConfig.Templates = &flat 363 364 runner, err := manager.NewRunner(runnerConfig, false, false) 365 if err != nil { 366 return nil, nil, err 367 } 368 369 // Set Nomad's environment variables 370 runner.Env = taskEnv.All() 371 372 // Build the lookup 373 idMap := runner.TemplateConfigMapping() 374 lookup := make(map[string][]*structs.Template, len(idMap)) 375 for id, ctmpls := range idMap { 376 for _, ctmpl := range ctmpls { 377 templates := lookup[id] 378 templates = append(templates, ctmplMapping[ctmpl]) 379 lookup[id] = templates 380 } 381 } 382 383 return runner, lookup, nil 384 } 385 386 // parseTemplateConfigs converts the tasks templates into consul-templates 387 func parseTemplateConfigs(tmpls []*structs.Template, taskDir string, 388 taskEnv *env.TaskEnv, allowAbs bool) (map[ctconf.TemplateConfig]*structs.Template, error) { 389 390 ctmpls := make(map[ctconf.TemplateConfig]*structs.Template, len(tmpls)) 391 for _, tmpl := range tmpls { 392 var src, dest string 393 if tmpl.SourcePath != "" { 394 if filepath.IsAbs(tmpl.SourcePath) { 395 if !allowAbs { 396 return nil, fmt.Errorf("Specifying absolute template paths disallowed by client config: %q", tmpl.SourcePath) 397 } 398 399 src = tmpl.SourcePath 400 } else { 401 src = filepath.Join(taskDir, taskEnv.ReplaceEnv(tmpl.SourcePath)) 402 } 403 } 404 if tmpl.DestPath != "" { 405 dest = filepath.Join(taskDir, taskEnv.ReplaceEnv(tmpl.DestPath)) 406 } 407 408 ct := ctconf.DefaultTemplateConfig() 409 ct.Source = &src 410 ct.Destination = &dest 411 ct.Contents = &tmpl.EmbeddedTmpl 412 ct.LeftDelim = &tmpl.LeftDelim 413 ct.RightDelim = &tmpl.RightDelim 414 415 // Set the permissions 416 if tmpl.Perms != "" { 417 v, err := strconv.ParseUint(tmpl.Perms, 8, 12) 418 if err != nil { 419 return nil, fmt.Errorf("Failed to parse %q as octal: %v", tmpl.Perms, err) 420 } 421 m := os.FileMode(v) 422 ct.Perms = &m 423 } 424 ct.Finalize() 425 426 ctmpls[*ct] = tmpl 427 } 428 429 return ctmpls, nil 430 } 431 432 // runnerConfig returns a consul-template runner configuration, setting the 433 // Vault and Consul configurations based on the clients configs. 434 func runnerConfig(config *config.Config, vaultToken string) (*ctconf.Config, error) { 435 conf := ctconf.DefaultConfig() 436 437 t, f := true, false 438 439 // Force faster retries 440 if testRetryRate != 0 { 441 rate := testRetryRate 442 conf.Consul.Retry.Backoff = &rate 443 } 444 445 // Setup the Consul config 446 if config.ConsulConfig != nil { 447 conf.Consul.Address = &config.ConsulConfig.Addr 448 conf.Consul.Token = &config.ConsulConfig.Token 449 450 if config.ConsulConfig.EnableSSL != nil && *config.ConsulConfig.EnableSSL { 451 verify := config.ConsulConfig.VerifySSL != nil && *config.ConsulConfig.VerifySSL 452 conf.Consul.SSL = &ctconf.SSLConfig{ 453 Enabled: &t, 454 Verify: &verify, 455 Cert: &config.ConsulConfig.CertFile, 456 Key: &config.ConsulConfig.KeyFile, 457 CaCert: &config.ConsulConfig.CAFile, 458 } 459 } 460 461 if config.ConsulConfig.Auth != "" { 462 parts := strings.SplitN(config.ConsulConfig.Auth, ":", 2) 463 if len(parts) != 2 { 464 return nil, fmt.Errorf("Failed to parse Consul Auth config") 465 } 466 467 conf.Consul.Auth = &ctconf.AuthConfig{ 468 Enabled: &t, 469 Username: &parts[0], 470 Password: &parts[1], 471 } 472 } 473 } 474 475 // Setup the Vault config 476 // Always set these to ensure nothing is picked up from the environment 477 emptyStr := "" 478 conf.Vault.RenewToken = &f 479 conf.Vault.Token = &emptyStr 480 if config.VaultConfig != nil && config.VaultConfig.IsEnabled() { 481 conf.Vault.Address = &config.VaultConfig.Addr 482 conf.Vault.Token = &vaultToken 483 484 if strings.HasPrefix(config.VaultConfig.Addr, "https") || config.VaultConfig.TLSCertFile != "" { 485 skipVerify := config.VaultConfig.TLSSkipVerify != nil && *config.VaultConfig.TLSSkipVerify 486 verify := !skipVerify 487 conf.Vault.SSL = &ctconf.SSLConfig{ 488 Enabled: &t, 489 Verify: &verify, 490 Cert: &config.VaultConfig.TLSCertFile, 491 Key: &config.VaultConfig.TLSKeyFile, 492 CaCert: &config.VaultConfig.TLSCaFile, 493 CaPath: &config.VaultConfig.TLSCaPath, 494 ServerName: &config.VaultConfig.TLSServerName, 495 } 496 } else { 497 conf.Vault.SSL = &ctconf.SSLConfig{ 498 Enabled: &f, 499 Verify: &f, 500 Cert: &emptyStr, 501 Key: &emptyStr, 502 CaCert: &emptyStr, 503 CaPath: &emptyStr, 504 ServerName: &emptyStr, 505 } 506 } 507 } 508 509 conf.Finalize() 510 return conf, nil 511 } 512 513 // loadTemplateEnv loads task environment variables from all templates. 514 func loadTemplateEnv(tmpls []*structs.Template, taskDir string) (map[string]string, error) { 515 all := make(map[string]string, 50) 516 for _, t := range tmpls { 517 if !t.Envvars { 518 continue 519 } 520 f, err := os.Open(filepath.Join(taskDir, t.DestPath)) 521 if err != nil { 522 return nil, fmt.Errorf("error opening env template: %v", err) 523 } 524 defer f.Close() 525 526 // Parse environment fil 527 vars, err := envparse.Parse(f) 528 if err != nil { 529 return nil, fmt.Errorf("error parsing env template %q: %v", t.DestPath, err) 530 } 531 for k, v := range vars { 532 all[k] = v 533 } 534 } 535 return all, nil 536 }