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