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  }