interpolate environment for services in script checks (#6916)

In 0.10.2 (specifically 387b016) we added interpolation to group
service blocks and centralized the logic for task environment
interpolation. This wasn't also added to script checks, which caused a
regression where the IDs for script checks for services w/
interpolated fields (ex. the service name) didn't match the service ID
that was registered with Consul.

This changeset calls the same taskenv interpolation logic during
`script_check` configuration, and adds tests to reduce the risk of
future regressions by comparing the IDs of service hook and the check hook.
This commit is contained in:
Tim Gross 2020-01-09 08:12:54 -05:00 committed by GitHub
parent d1b12781ea
commit fa4da93578
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 74 additions and 30 deletions

View file

@ -174,7 +174,8 @@ func (h *scriptCheckHook) Stop(ctx context.Context, req *interfaces.TaskStopRequ
func (h *scriptCheckHook) newScriptChecks() map[string]*scriptCheck {
scriptChecks := make(map[string]*scriptCheck)
for _, service := range h.task.Services {
interpolatedTaskServices := taskenv.InterpolateServices(h.taskEnv, h.task.Services)
for _, service := range interpolatedTaskServices {
for _, check := range service.Checks {
if check.Type != structs.ServiceCheckScript {
continue
@ -204,7 +205,8 @@ func (h *scriptCheckHook) newScriptChecks() map[string]*scriptCheck {
// needs are entirely encapsulated within the group service hook which
// watches Consul for status changes.
tg := h.alloc.Job.LookupTaskGroup(h.alloc.TaskGroup)
for _, service := range tg.Services {
interpolatedGroupServices := taskenv.InterpolateServices(h.taskEnv, tg.Services)
for _, service := range interpolatedGroupServices {
for _, check := range service.Checks {
if check.Type != structs.ServiceCheckScript {
continue
@ -293,37 +295,10 @@ func newScriptCheck(config *scriptCheckConfig) *scriptCheck {
sc.callback = newScriptCheckCallback(sc)
sc.logger = config.logger
sc.shutdownCh = config.shutdownCh
// the hash of the interior structs.ServiceCheck is used by the
// Consul client to get the ID to register for the check. So we
// update it here so that we have the same ID for UpdateTTL.
// TODO(tgross): this block is similar to one in service_hook
// and we can pull that out to a function so we know we're
// interpolating the same everywhere
sc.check.Name = config.taskEnv.ReplaceEnv(orig.Name)
sc.check.Type = config.taskEnv.ReplaceEnv(orig.Type)
sc.check.Command = sc.Command
sc.check.Args = sc.Args
sc.check.Path = config.taskEnv.ReplaceEnv(orig.Path)
sc.check.Protocol = config.taskEnv.ReplaceEnv(orig.Protocol)
sc.check.PortLabel = config.taskEnv.ReplaceEnv(orig.PortLabel)
sc.check.InitialStatus = config.taskEnv.ReplaceEnv(orig.InitialStatus)
sc.check.Method = config.taskEnv.ReplaceEnv(orig.Method)
sc.check.GRPCService = config.taskEnv.ReplaceEnv(orig.GRPCService)
if len(orig.Header) > 0 {
header := make(map[string][]string, len(orig.Header))
for k, vs := range orig.Header {
newVals := make([]string, len(vs))
for i, v := range vs {
newVals[i] = config.taskEnv.ReplaceEnv(v)
}
header[config.taskEnv.ReplaceEnv(k)] = newVals
}
sc.check.Header = header
}
if config.isGroup {
// TODO(tgross):
// group services don't have access to a task environment
// at creation, so their checks get registered before the
// check can be interpolated here. if we don't use the

View file

@ -10,8 +10,11 @@ import (
"github.com/hashicorp/consul/api"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces"
"github.com/hashicorp/nomad/client/consul"
"github.com/hashicorp/nomad/client/taskenv"
agentconsul "github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper/testlog"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/stretchr/testify/require"
)
@ -217,3 +220,69 @@ func TestScript_Exec_Codes(t *testing.T) {
}
}
}
// TestScript_TaskEnvInterpolation asserts that script check hooks are
// interpolated in the same way that services are
func TestScript_TaskEnvInterpolation(t *testing.T) {
logger := testlog.HCLogger(t)
consulClient := consul.NewMockConsulServiceClient(t, logger)
exec, cancel := newBlockingScriptExec()
defer cancel()
alloc := mock.ConnectAlloc()
task := alloc.Job.TaskGroups[0].Tasks[0]
task.Services[0].Name = "${NOMAD_JOB_NAME}-${TASK}-${SVC_NAME}"
task.Services[0].Checks[0].Name = "${NOMAD_JOB_NAME}-${SVC_NAME}-check"
alloc.Job.Canonicalize() // need to re-canonicalize b/c the mock already did it
env := taskenv.NewBuilder(mock.Node(), alloc, task, "global").SetHookEnv(
"script_check",
map[string]string{"SVC_NAME": "frontend"}).Build()
svcHook := newServiceHook(serviceHookConfig{
alloc: alloc,
task: task,
consul: consulClient,
logger: logger,
})
// emulate prestart having been fired
svcHook.taskEnv = env
scHook := newScriptCheckHook(scriptCheckHookConfig{
alloc: alloc,
task: task,
consul: consulClient,
logger: logger,
shutdownWait: time.Hour, // heartbeater will never be called
})
// emulate prestart having been fired
scHook.taskEnv = env
scHook.driverExec = exec
expectedSvc := svcHook.getWorkloadServices().Services[0]
expected := agentconsul.MakeCheckID(agentconsul.MakeAllocServiceID(
alloc.ID, task.Name, expectedSvc), expectedSvc.Checks[0])
actual := scHook.newScriptChecks()
check, ok := actual[expected]
require.True(t, ok)
require.Equal(t, "my-job-frontend-check", check.check.Name)
// emulate an update
env = taskenv.NewBuilder(mock.Node(), alloc, task, "global").SetHookEnv(
"script_check",
map[string]string{"SVC_NAME": "backend"}).Build()
scHook.taskEnv = env
svcHook.taskEnv = env
expectedSvc = svcHook.getWorkloadServices().Services[0]
expected = agentconsul.MakeCheckID(agentconsul.MakeAllocServiceID(
alloc.ID, task.Name, expectedSvc), expectedSvc.Checks[0])
actual = scHook.newScriptChecks()
check, ok = actual[expected]
require.True(t, ok)
require.Equal(t, "my-job-backend-check", check.check.Name)
}