github.com/bigcommerce/nomad@v0.9.3-bc/client/allocrunner/taskrunner/service_hook.go (about)

     1  package taskrunner
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	log "github.com/hashicorp/go-hclog"
    10  	"github.com/hashicorp/nomad/client/allocrunner/interfaces"
    11  	tinterfaces "github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces"
    12  	"github.com/hashicorp/nomad/client/consul"
    13  	"github.com/hashicorp/nomad/client/taskenv"
    14  	agentconsul "github.com/hashicorp/nomad/command/agent/consul"
    15  	"github.com/hashicorp/nomad/nomad/structs"
    16  	"github.com/hashicorp/nomad/plugins/drivers"
    17  )
    18  
    19  var _ interfaces.TaskPoststartHook = &serviceHook{}
    20  var _ interfaces.TaskPreKillHook = &serviceHook{}
    21  var _ interfaces.TaskExitedHook = &serviceHook{}
    22  var _ interfaces.TaskStopHook = &serviceHook{}
    23  
    24  type serviceHookConfig struct {
    25  	alloc  *structs.Allocation
    26  	task   *structs.Task
    27  	consul consul.ConsulServiceAPI
    28  
    29  	// Restarter is a subset of the TaskLifecycle interface
    30  	restarter agentconsul.TaskRestarter
    31  
    32  	logger log.Logger
    33  }
    34  
    35  type serviceHook struct {
    36  	consul    consul.ConsulServiceAPI
    37  	allocID   string
    38  	taskName  string
    39  	restarter agentconsul.TaskRestarter
    40  	logger    log.Logger
    41  
    42  	// The following fields may be updated
    43  	delay      time.Duration
    44  	driverExec tinterfaces.ScriptExecutor
    45  	driverNet  *drivers.DriverNetwork
    46  	canary     bool
    47  	services   []*structs.Service
    48  	networks   structs.Networks
    49  	taskEnv    *taskenv.TaskEnv
    50  
    51  	// Since Update() may be called concurrently with any other hook all
    52  	// hook methods must be fully serialized
    53  	mu sync.Mutex
    54  }
    55  
    56  func newServiceHook(c serviceHookConfig) *serviceHook {
    57  	h := &serviceHook{
    58  		consul:    c.consul,
    59  		allocID:   c.alloc.ID,
    60  		taskName:  c.task.Name,
    61  		services:  c.task.Services,
    62  		restarter: c.restarter,
    63  		delay:     c.task.ShutdownDelay,
    64  	}
    65  
    66  	// COMPAT(0.10): Just use the AllocatedResources
    67  	if c.alloc.AllocatedResources != nil {
    68  		if res := c.alloc.AllocatedResources.Tasks[c.task.Name]; res != nil {
    69  			h.networks = res.Networks
    70  		}
    71  	} else {
    72  		if res := c.alloc.TaskResources[c.task.Name]; res != nil {
    73  			h.networks = res.Networks
    74  		}
    75  	}
    76  
    77  	if c.alloc.DeploymentStatus != nil && c.alloc.DeploymentStatus.Canary {
    78  		h.canary = true
    79  	}
    80  
    81  	h.logger = c.logger.Named(h.Name())
    82  	return h
    83  }
    84  
    85  func (h *serviceHook) Name() string {
    86  	return "consul_services"
    87  }
    88  
    89  func (h *serviceHook) Poststart(ctx context.Context, req *interfaces.TaskPoststartRequest, _ *interfaces.TaskPoststartResponse) error {
    90  	h.mu.Lock()
    91  	defer h.mu.Unlock()
    92  
    93  	// Store the TaskEnv for interpolating now and when Updating
    94  	h.driverExec = req.DriverExec
    95  	h.driverNet = req.DriverNetwork
    96  	h.taskEnv = req.TaskEnv
    97  
    98  	// Create task services struct with request's driver metadata
    99  	taskServices := h.getTaskServices()
   100  
   101  	return h.consul.RegisterTask(taskServices)
   102  }
   103  
   104  func (h *serviceHook) Update(ctx context.Context, req *interfaces.TaskUpdateRequest, _ *interfaces.TaskUpdateResponse) error {
   105  	h.mu.Lock()
   106  	defer h.mu.Unlock()
   107  
   108  	// Create old task services struct with request's driver metadata as it
   109  	// can't change due to Updates
   110  	oldTaskServices := h.getTaskServices()
   111  
   112  	// Store new updated values out of request
   113  	canary := false
   114  	if req.Alloc.DeploymentStatus != nil {
   115  		canary = req.Alloc.DeploymentStatus.Canary
   116  	}
   117  
   118  	// COMPAT(0.10): Just use the AllocatedResources
   119  	var networks structs.Networks
   120  	if req.Alloc.AllocatedResources != nil {
   121  		if res := req.Alloc.AllocatedResources.Tasks[h.taskName]; res != nil {
   122  			networks = res.Networks
   123  		}
   124  	} else {
   125  		if res := req.Alloc.TaskResources[h.taskName]; res != nil {
   126  			networks = res.Networks
   127  		}
   128  	}
   129  
   130  	task := req.Alloc.LookupTask(h.taskName)
   131  	if task == nil {
   132  		return fmt.Errorf("task %q not found in updated alloc", h.taskName)
   133  	}
   134  
   135  	// Update service hook fields
   136  	h.delay = task.ShutdownDelay
   137  	h.taskEnv = req.TaskEnv
   138  	h.services = task.Services
   139  	h.networks = networks
   140  	h.canary = canary
   141  
   142  	// Create new task services struct with those new values
   143  	newTaskServices := h.getTaskServices()
   144  
   145  	return h.consul.UpdateTask(oldTaskServices, newTaskServices)
   146  }
   147  
   148  func (h *serviceHook) PreKilling(ctx context.Context, req *interfaces.TaskPreKillRequest, resp *interfaces.TaskPreKillResponse) error {
   149  	h.mu.Lock()
   150  	defer h.mu.Unlock()
   151  
   152  	// Deregister before killing task
   153  	h.deregister()
   154  
   155  	// If there's no shutdown delay, exit early
   156  	if h.delay == 0 {
   157  		return nil
   158  	}
   159  
   160  	h.logger.Debug("waiting before killing task", "shutdown_delay", h.delay)
   161  	select {
   162  	case <-ctx.Done():
   163  	case <-time.After(h.delay):
   164  	}
   165  	return nil
   166  }
   167  
   168  func (h *serviceHook) Exited(context.Context, *interfaces.TaskExitedRequest, *interfaces.TaskExitedResponse) error {
   169  	h.mu.Lock()
   170  	defer h.mu.Unlock()
   171  	h.deregister()
   172  	return nil
   173  }
   174  
   175  // deregister services from Consul.
   176  func (h *serviceHook) deregister() {
   177  	taskServices := h.getTaskServices()
   178  	h.consul.RemoveTask(taskServices)
   179  
   180  	// Canary flag may be getting flipped when the alloc is being
   181  	// destroyed, so remove both variations of the service
   182  	taskServices.Canary = !taskServices.Canary
   183  	h.consul.RemoveTask(taskServices)
   184  
   185  }
   186  
   187  func (h *serviceHook) Stop(ctx context.Context, req *interfaces.TaskStopRequest, resp *interfaces.TaskStopResponse) error {
   188  	h.mu.Lock()
   189  	defer h.mu.Unlock()
   190  	h.deregister()
   191  	return nil
   192  }
   193  
   194  func (h *serviceHook) getTaskServices() *agentconsul.TaskServices {
   195  	// Interpolate with the task's environment
   196  	interpolatedServices := interpolateServices(h.taskEnv, h.services)
   197  
   198  	// Create task services struct with request's driver metadata
   199  	return &agentconsul.TaskServices{
   200  		AllocID:       h.allocID,
   201  		Name:          h.taskName,
   202  		Restarter:     h.restarter,
   203  		Services:      interpolatedServices,
   204  		DriverExec:    h.driverExec,
   205  		DriverNetwork: h.driverNet,
   206  		Networks:      h.networks,
   207  		Canary:        h.canary,
   208  	}
   209  }
   210  
   211  // interpolateServices returns an interpolated copy of services and checks with
   212  // values from the task's environment.
   213  func interpolateServices(taskEnv *taskenv.TaskEnv, services []*structs.Service) []*structs.Service {
   214  	// Guard against not having a valid taskEnv. This can be the case if the
   215  	// PreKilling or Exited hook is run before Poststart.
   216  	if taskEnv == nil || len(services) == 0 {
   217  		return nil
   218  	}
   219  
   220  	interpolated := make([]*structs.Service, len(services))
   221  
   222  	for i, origService := range services {
   223  		// Create a copy as we need to reinterpolate every time the
   224  		// environment changes
   225  		service := origService.Copy()
   226  
   227  		for _, check := range service.Checks {
   228  			check.Name = taskEnv.ReplaceEnv(check.Name)
   229  			check.Type = taskEnv.ReplaceEnv(check.Type)
   230  			check.Command = taskEnv.ReplaceEnv(check.Command)
   231  			check.Args = taskEnv.ParseAndReplace(check.Args)
   232  			check.Path = taskEnv.ReplaceEnv(check.Path)
   233  			check.Protocol = taskEnv.ReplaceEnv(check.Protocol)
   234  			check.PortLabel = taskEnv.ReplaceEnv(check.PortLabel)
   235  			check.InitialStatus = taskEnv.ReplaceEnv(check.InitialStatus)
   236  			check.Method = taskEnv.ReplaceEnv(check.Method)
   237  			check.GRPCService = taskEnv.ReplaceEnv(check.GRPCService)
   238  			if len(check.Header) > 0 {
   239  				header := make(map[string][]string, len(check.Header))
   240  				for k, vs := range check.Header {
   241  					newVals := make([]string, len(vs))
   242  					for i, v := range vs {
   243  						newVals[i] = taskEnv.ReplaceEnv(v)
   244  					}
   245  					header[taskEnv.ReplaceEnv(k)] = newVals
   246  				}
   247  				check.Header = header
   248  			}
   249  		}
   250  
   251  		service.Name = taskEnv.ReplaceEnv(service.Name)
   252  		service.PortLabel = taskEnv.ReplaceEnv(service.PortLabel)
   253  		service.Tags = taskEnv.ParseAndReplace(service.Tags)
   254  		service.CanaryTags = taskEnv.ParseAndReplace(service.CanaryTags)
   255  		interpolated[i] = service
   256  	}
   257  
   258  	return interpolated
   259  }