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  }