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 }