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