// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package allocrunner import ( "context" "testing" "time" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/client/allocrunner/interfaces" regMock "github.com/hashicorp/nomad/client/serviceregistration/mock" "github.com/hashicorp/nomad/client/serviceregistration/wrapper" cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/client/taskenv" agentconsul "github.com/hashicorp/nomad/command/agent/consul" "github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/helper/testlog" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/shoenig/test/must" ) // TestGroupServiceHook_NoGroupServices asserts calling group service hooks // without group services does not error. func TestGroupServiceHook_NoGroupServices(t *testing.T) { ci.Parallel(t) alloc := mock.Alloc() alloc.Job.TaskGroups[0].Services = []*structs.Service{{ Name: "foo", Provider: "consul", PortLabel: "9999", }} logger := testlog.HCLogger(t) consulMockClient := regMock.NewServiceRegistrationHandler(logger) env := taskenv.NewBuilder(mock.Node(), alloc, nil, alloc.Job.Region).Build() regWrapper := wrapper.NewHandlerWrapper( logger, consulMockClient, regMock.NewServiceRegistrationHandler(logger)) h := newGroupServiceHook(groupServiceHookConfig{ alloc: alloc, serviceRegWrapper: regWrapper, restarter: agentconsul.NoopRestarter(), logger: logger, hookResources: cstructs.NewAllocHookResources(), }) must.NoError(t, h.Prerun(env)) req := &interfaces.RunnerUpdateRequest{Alloc: alloc, AllocEnv: env} must.NoError(t, h.Update(req)) must.NoError(t, h.Postrun()) must.NoError(t, h.PreTaskRestart()) ops := consulMockClient.GetOps() must.Len(t, 4, ops) must.Eq(t, "add", ops[0].Op) // Prerun must.Eq(t, "update", ops[1].Op) // Update must.Eq(t, "remove", ops[2].Op) // Postrun must.Eq(t, "add", ops[3].Op) // Restart -> preRun } // TestGroupServiceHook_ShutdownDelayUpdate asserts calling group service hooks // update updates the hooks delay value. func TestGroupServiceHook_ShutdownDelayUpdate(t *testing.T) { ci.Parallel(t) alloc := mock.Alloc() alloc.Job.TaskGroups[0].ShutdownDelay = pointer.Of(10 * time.Second) logger := testlog.HCLogger(t) consulMockClient := regMock.NewServiceRegistrationHandler(logger) env := taskenv.NewBuilder(mock.Node(), alloc, nil, alloc.Job.Region).Build() regWrapper := wrapper.NewHandlerWrapper( logger, consulMockClient, regMock.NewServiceRegistrationHandler(logger), ) h := newGroupServiceHook(groupServiceHookConfig{ alloc: alloc, serviceRegWrapper: regWrapper, restarter: agentconsul.NoopRestarter(), logger: logger, hookResources: cstructs.NewAllocHookResources(), }) must.NoError(t, h.Prerun(env)) // Incease shutdown Delay alloc.Job.TaskGroups[0].ShutdownDelay = pointer.Of(15 * time.Second) req := &interfaces.RunnerUpdateRequest{Alloc: alloc, AllocEnv: env} must.NoError(t, h.Update(req)) // Assert that update updated the delay value must.Eq(t, h.delay, 15*time.Second) // Remove shutdown delay alloc.Job.TaskGroups[0].ShutdownDelay = nil req = &interfaces.RunnerUpdateRequest{Alloc: alloc} must.NoError(t, h.Update(req)) // Assert that update updated the delay value must.Eq(t, h.delay, 0*time.Second) } // TestGroupServiceHook_GroupServices asserts group service hooks with group // services does not error. func TestGroupServiceHook_GroupServices(t *testing.T) { ci.Parallel(t) alloc := mock.ConnectAlloc() alloc.Job.Canonicalize() logger := testlog.HCLogger(t) consulMockClient := regMock.NewServiceRegistrationHandler(logger) env := taskenv.NewBuilder(mock.Node(), alloc, nil, alloc.Job.Region).Build() regWrapper := wrapper.NewHandlerWrapper( logger, consulMockClient, regMock.NewServiceRegistrationHandler(logger)) h := newGroupServiceHook(groupServiceHookConfig{ alloc: alloc, serviceRegWrapper: regWrapper, restarter: agentconsul.NoopRestarter(), logger: logger, hookResources: cstructs.NewAllocHookResources(), }) must.NoError(t, h.Prerun(env)) req := &interfaces.RunnerUpdateRequest{Alloc: alloc, AllocEnv: env} must.NoError(t, h.Update(req)) must.NoError(t, h.Postrun()) must.NoError(t, h.PreTaskRestart()) ops := consulMockClient.GetOps() must.Len(t, 4, ops) must.Eq(t, "add", ops[0].Op) // Prerun must.Eq(t, "update", ops[1].Op) // Update must.Eq(t, "remove", ops[2].Op) // Postrun must.Eq(t, "add", ops[3].Op) // Restart -> preRun } // TestGroupServiceHook_GroupServices_Nomad asserts group service hooks with // group services does not error when using the Nomad provider. func TestGroupServiceHook_GroupServices_Nomad(t *testing.T) { ci.Parallel(t) // Create a mock alloc, and add a group service using provider Nomad. alloc := mock.Alloc() alloc.Job.TaskGroups[0].Services = []*structs.Service{ { Name: "nomad-provider-service", Provider: structs.ServiceProviderNomad, }, } // Create our base objects and our subsequent wrapper. logger := testlog.HCLogger(t) consulMockClient := regMock.NewServiceRegistrationHandler(logger) nomadMockClient := regMock.NewServiceRegistrationHandler(logger) env := taskenv.NewBuilder(mock.Node(), alloc, nil, alloc.Job.Region).Build() regWrapper := wrapper.NewHandlerWrapper(logger, consulMockClient, nomadMockClient) h := newGroupServiceHook(groupServiceHookConfig{ alloc: alloc, serviceRegWrapper: regWrapper, restarter: agentconsul.NoopRestarter(), logger: logger, hookResources: cstructs.NewAllocHookResources(), }) must.NoError(t, h.Prerun(env)) // Trigger our hook requests. req := &interfaces.RunnerUpdateRequest{Alloc: alloc, AllocEnv: env} must.NoError(t, h.Update(req)) must.NoError(t, h.Postrun()) must.NoError(t, h.PreTaskRestart()) // Ensure the Nomad mock provider has the expected operations. ops := nomadMockClient.GetOps() must.Len(t, 4, ops) must.Eq(t, "add", ops[0].Op) // Prerun must.Eq(t, "update", ops[1].Op) // Update must.Eq(t, "remove", ops[2].Op) // Postrun must.Eq(t, "add", ops[3].Op) // Restart -> preRun // Ensure the Consul mock provider has zero operations. must.SliceEmpty(t, consulMockClient.GetOps()) } // TestGroupServiceHook_Error asserts group service hooks with group // services but no group network is handled gracefully. func TestGroupServiceHook_NoNetwork(t *testing.T) { ci.Parallel(t) alloc := mock.Alloc() alloc.Job.TaskGroups[0].Networks = []*structs.NetworkResource{} tg := alloc.Job.LookupTaskGroup(alloc.TaskGroup) tg.Services = []*structs.Service{ { Name: "testconnect", Provider: "consul", PortLabel: "9999", Connect: &structs.ConsulConnect{ SidecarService: &structs.ConsulSidecarService{}, }, }, } logger := testlog.HCLogger(t) consulMockClient := regMock.NewServiceRegistrationHandler(logger) env := taskenv.NewBuilder(mock.Node(), alloc, nil, alloc.Job.Region).Build() regWrapper := wrapper.NewHandlerWrapper( logger, consulMockClient, regMock.NewServiceRegistrationHandler(logger)) h := newGroupServiceHook(groupServiceHookConfig{ alloc: alloc, serviceRegWrapper: regWrapper, restarter: agentconsul.NoopRestarter(), logger: logger, hookResources: cstructs.NewAllocHookResources(), }) must.NoError(t, h.Prerun(env)) req := &interfaces.RunnerUpdateRequest{Alloc: alloc, AllocEnv: env} must.NoError(t, h.Update(req)) must.NoError(t, h.Postrun()) must.NoError(t, h.PreTaskRestart()) ops := consulMockClient.GetOps() must.Len(t, 4, ops) must.Eq(t, "add", ops[0].Op) // Prerun must.Eq(t, "update", ops[1].Op) // Update must.Eq(t, "remove", ops[2].Op) // Postrun must.Eq(t, "add", ops[3].Op) // Restart -> preRun } func TestGroupServiceHook_getWorkloadServices(t *testing.T) { ci.Parallel(t) alloc := mock.Alloc() alloc.Job.TaskGroups[0].Networks = []*structs.NetworkResource{} tg := alloc.Job.LookupTaskGroup(alloc.TaskGroup) tg.Services = []*structs.Service{ { Name: "testconnect", PortLabel: "9999", Connect: &structs.ConsulConnect{ SidecarService: &structs.ConsulSidecarService{}, }, }, } logger := testlog.HCLogger(t) consulMockClient := regMock.NewServiceRegistrationHandler(logger) regWrapper := wrapper.NewHandlerWrapper( logger, consulMockClient, regMock.NewServiceRegistrationHandler(logger)) h := newGroupServiceHook(groupServiceHookConfig{ alloc: alloc, serviceRegWrapper: regWrapper, restarter: agentconsul.NoopRestarter(), logger: logger, hookResources: cstructs.NewAllocHookResources(), }) services := h.getWorkloadServicesLocked() must.Len(t, 1, services.Services) } func TestGroupServiceHook_PreKill(t *testing.T) { ci.Parallel(t) logger := testlog.HCLogger(t) t.Run("waits for shutdown delay", func(t *testing.T) { alloc := mock.Alloc() alloc.Job.TaskGroups[0].Networks = []*structs.NetworkResource{} tg := alloc.Job.LookupTaskGroup(alloc.TaskGroup) tg.Services = []*structs.Service{ { Name: "testconnect", PortLabel: "9999", Connect: &structs.ConsulConnect{ SidecarService: &structs.ConsulSidecarService{}, }, }, } delay := 200 * time.Millisecond tg.ShutdownDelay = &delay consulMockClient := regMock.NewServiceRegistrationHandler(logger) regWrapper := wrapper.NewHandlerWrapper( logger, consulMockClient, regMock.NewServiceRegistrationHandler(logger)) shutDownCtx, cancel := context.WithTimeout(context.Background(), delay*2) defer cancel() h := newGroupServiceHook(groupServiceHookConfig{ alloc: alloc, serviceRegWrapper: regWrapper, shutdownDelayCtx: shutDownCtx, restarter: agentconsul.NoopRestarter(), logger: logger, hookResources: cstructs.NewAllocHookResources(), }) successChan := make(chan struct{}, 1) go func() { before := time.Now() h.PreKill() // we did not respect the shutdown_delay and returned immediately if time.Since(before) < delay { t.Fail() } successChan <- struct{}{} }() select { case <-shutDownCtx.Done(): // we didn't wait longer than shutdown delay and get killed by the shutdownCtx t.Fail() case <-successChan: } }) t.Run("returns immediately when no shutdown delay", func(t *testing.T) { alloc := mock.Alloc() alloc.Job.TaskGroups[0].Networks = []*structs.NetworkResource{} tg := alloc.Job.LookupTaskGroup(alloc.TaskGroup) tg.Services = []*structs.Service{ { Name: "testconnect", PortLabel: "9999", Connect: &structs.ConsulConnect{ SidecarService: &structs.ConsulSidecarService{}, }, }, } consulMockClient := regMock.NewServiceRegistrationHandler(logger) regWrapper := wrapper.NewHandlerWrapper( logger, consulMockClient, regMock.NewServiceRegistrationHandler(logger)) h := newGroupServiceHook(groupServiceHookConfig{ alloc: alloc, serviceRegWrapper: regWrapper, restarter: agentconsul.NoopRestarter(), logger: logger, hookResources: cstructs.NewAllocHookResources(), }) successChan := make(chan struct{}, 1) go func() { h.PreKill() successChan <- struct{}{} }() shutDownCtx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() select { case <-shutDownCtx.Done(): t.Fail() case <-successChan: } }) t.Run("returns immediately when already deregistered", func(t *testing.T) { alloc := mock.Alloc() alloc.Job.TaskGroups[0].Networks = []*structs.NetworkResource{} tg := alloc.Job.LookupTaskGroup(alloc.TaskGroup) tg.Services = []*structs.Service{ { Name: "testconnect", PortLabel: "9999", Connect: &structs.ConsulConnect{ SidecarService: &structs.ConsulSidecarService{}, }, }, } delay := 500 * time.Millisecond tg.ShutdownDelay = &delay consulMockClient := regMock.NewServiceRegistrationHandler(logger) regWrapper := wrapper.NewHandlerWrapper( logger, consulMockClient, regMock.NewServiceRegistrationHandler(logger)) // wait a shorter amount of time than shutdown_delay. If this triggers, the shutdown delay // is being waited on, so we did not skip it. shutDownCtx, cancel := context.WithTimeout(context.Background(), delay-300*time.Millisecond) defer cancel() h := newGroupServiceHook(groupServiceHookConfig{ alloc: alloc, serviceRegWrapper: regWrapper, restarter: agentconsul.NoopRestarter(), logger: logger, hookResources: cstructs.NewAllocHookResources(), }) h.deregistered = true successChan := make(chan struct{}, 1) go func() { h.PreKill() successChan <- struct{}{} }() select { case <-shutDownCtx.Done(): t.Fail() case <-successChan: } }) }