github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/client/allocrunner/taskrunner/script_check_hook_test.go (about)

     1  package taskrunner
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync/atomic"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/hashicorp/consul/api"
    11  	hclog "github.com/hashicorp/go-hclog"
    12  	"github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces"
    13  	"github.com/hashicorp/nomad/client/consul"
    14  	"github.com/hashicorp/nomad/client/taskenv"
    15  	agentconsul "github.com/hashicorp/nomad/command/agent/consul"
    16  	"github.com/hashicorp/nomad/helper/testlog"
    17  	"github.com/hashicorp/nomad/nomad/mock"
    18  	"github.com/hashicorp/nomad/nomad/structs"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  func newScriptMock(hb heartbeater, exec interfaces.ScriptExecutor, logger hclog.Logger, interval, timeout time.Duration) *scriptCheck {
    23  	script := newScriptCheck(&scriptCheckConfig{
    24  		allocID:   "allocid",
    25  		taskName:  "testtask",
    26  		serviceID: "serviceid",
    27  		check: &structs.ServiceCheck{
    28  			Interval: interval,
    29  			Timeout:  timeout,
    30  		},
    31  		agent:      hb,
    32  		driverExec: exec,
    33  		taskEnv:    &taskenv.TaskEnv{},
    34  		logger:     logger,
    35  		shutdownCh: nil,
    36  	})
    37  	script.callback = newScriptCheckCallback(script)
    38  	script.lastCheckOk = true
    39  	return script
    40  }
    41  
    42  // fakeHeartbeater implements the heartbeater interface to allow mocking out
    43  // Consul in script executor tests.
    44  type fakeHeartbeater struct {
    45  	heartbeats chan heartbeat
    46  }
    47  
    48  func (f *fakeHeartbeater) UpdateTTL(checkID, output, status string) error {
    49  	f.heartbeats <- heartbeat{checkID: checkID, output: output, status: status}
    50  	return nil
    51  }
    52  
    53  func newFakeHeartbeater() *fakeHeartbeater {
    54  	return &fakeHeartbeater{heartbeats: make(chan heartbeat)}
    55  }
    56  
    57  type heartbeat struct {
    58  	checkID string
    59  	output  string
    60  	status  string
    61  }
    62  
    63  // TestScript_Exec_Cancel asserts cancelling a script check shortcircuits
    64  // any running scripts.
    65  func TestScript_Exec_Cancel(t *testing.T) {
    66  	exec, cancel := newBlockingScriptExec()
    67  	defer cancel()
    68  
    69  	logger := testlog.HCLogger(t)
    70  	script := newScriptMock(nil, // heartbeater should never be called
    71  		exec, logger, time.Hour, time.Hour)
    72  
    73  	handle := script.run()
    74  	<-exec.running  // wait until Exec is called
    75  	handle.cancel() // cancel now that we're blocked in exec
    76  
    77  	select {
    78  	case <-handle.wait():
    79  	case <-time.After(3 * time.Second):
    80  		t.Fatalf("timed out waiting for script check to exit")
    81  	}
    82  
    83  	// The underlying ScriptExecutor (newBlockScriptExec) *cannot* be
    84  	// canceled. Only a wrapper around it obeys the context cancelation.
    85  	require.NotEqual(t, atomic.LoadInt32(&exec.exited), 1,
    86  		"expected script executor to still be running after timeout")
    87  }
    88  
    89  // TestScript_Exec_TimeoutBasic asserts a script will be killed when the
    90  // timeout is reached.
    91  func TestScript_Exec_TimeoutBasic(t *testing.T) {
    92  	t.Parallel()
    93  	exec, cancel := newBlockingScriptExec()
    94  	defer cancel()
    95  
    96  	logger := testlog.HCLogger(t)
    97  	hb := newFakeHeartbeater()
    98  	script := newScriptMock(hb, exec, logger, time.Hour, time.Second)
    99  
   100  	handle := script.run()
   101  	defer handle.cancel() // cleanup
   102  	<-exec.running        // wait until Exec is called
   103  
   104  	// Check for UpdateTTL call
   105  	select {
   106  	case update := <-hb.heartbeats:
   107  		require.Equal(t, update.output, context.DeadlineExceeded.Error())
   108  		require.Equal(t, update.status, api.HealthCritical)
   109  	case <-time.After(3 * time.Second):
   110  		t.Fatalf("timed out waiting for script check to exit")
   111  	}
   112  
   113  	// The underlying ScriptExecutor (newBlockScriptExec) *cannot* be
   114  	// canceled. Only a wrapper around it obeys the context cancelation.
   115  	require.NotEqual(t, atomic.LoadInt32(&exec.exited), 1,
   116  		"expected script executor to still be running after timeout")
   117  
   118  	// Cancel and watch for exit
   119  	handle.cancel()
   120  	select {
   121  	case <-handle.wait(): // ok!
   122  	case update := <-hb.heartbeats:
   123  		t.Errorf("unexpected UpdateTTL call on exit with status=%q", update)
   124  	case <-time.After(3 * time.Second):
   125  		t.Fatalf("timed out waiting for script check to exit")
   126  	}
   127  }
   128  
   129  // TestScript_Exec_TimeoutCritical asserts a script will be killed when
   130  // the timeout is reached and always set a critical status regardless of what
   131  // Exec returns.
   132  func TestScript_Exec_TimeoutCritical(t *testing.T) {
   133  	t.Parallel()
   134  	logger := testlog.HCLogger(t)
   135  	hb := newFakeHeartbeater()
   136  	script := newScriptMock(hb, sleeperExec{}, logger, time.Hour, time.Nanosecond)
   137  
   138  	handle := script.run()
   139  	defer handle.cancel() // cleanup
   140  
   141  	// Check for UpdateTTL call
   142  	select {
   143  	case update := <-hb.heartbeats:
   144  		require.Equal(t, update.output, context.DeadlineExceeded.Error())
   145  		require.Equal(t, update.status, api.HealthCritical)
   146  	case <-time.After(3 * time.Second):
   147  		t.Fatalf("timed out waiting for script check to timeout")
   148  	}
   149  }
   150  
   151  // TestScript_Exec_Shutdown asserts a script will be executed once more
   152  // when told to shutdown.
   153  func TestScript_Exec_Shutdown(t *testing.T) {
   154  	shutdown := make(chan struct{})
   155  	exec := newSimpleExec(0, nil)
   156  	logger := testlog.HCLogger(t)
   157  	hb := newFakeHeartbeater()
   158  	script := newScriptMock(hb, exec, logger, time.Hour, 3*time.Second)
   159  	script.shutdownCh = shutdown
   160  
   161  	handle := script.run()
   162  	defer handle.cancel() // cleanup
   163  	close(shutdown)       // tell scriptCheck to exit
   164  
   165  	select {
   166  	case update := <-hb.heartbeats:
   167  		require.Equal(t, update.output, "code=0 err=<nil>")
   168  		require.Equal(t, update.status, api.HealthPassing)
   169  	case <-time.After(3 * time.Second):
   170  		t.Fatalf("timed out waiting for script check to exit")
   171  	}
   172  
   173  	select {
   174  	case <-handle.wait(): // ok!
   175  	case <-time.After(3 * time.Second):
   176  		t.Fatalf("timed out waiting for script check to exit")
   177  	}
   178  }
   179  
   180  // TestScript_Exec_Codes asserts script exit codes are translated to their
   181  // corresponding Consul health check status.
   182  func TestScript_Exec_Codes(t *testing.T) {
   183  
   184  	exec := newScriptedExec([]execResult{
   185  		{[]byte("output"), 1, nil},
   186  		{[]byte("output"), 0, nil},
   187  		{[]byte("output"), 0, context.DeadlineExceeded},
   188  		{[]byte("output"), 0, nil},
   189  		{[]byte("<ignored output>"), 2, fmt.Errorf("some error")},
   190  		{[]byte("output"), 0, nil},
   191  		{[]byte("error9000"), 9000, nil},
   192  	})
   193  	logger := testlog.HCLogger(t)
   194  	hb := newFakeHeartbeater()
   195  	script := newScriptMock(
   196  		hb, exec, logger, time.Nanosecond, 3*time.Second)
   197  
   198  	handle := script.run()
   199  	defer handle.cancel() // cleanup
   200  	deadline := time.After(3 * time.Second)
   201  
   202  	expected := []heartbeat{
   203  		{script.id, "output", api.HealthWarning},
   204  		{script.id, "output", api.HealthPassing},
   205  		{script.id, context.DeadlineExceeded.Error(), api.HealthCritical},
   206  		{script.id, "output", api.HealthPassing},
   207  		{script.id, "some error", api.HealthCritical},
   208  		{script.id, "output", api.HealthPassing},
   209  		{script.id, "error9000", api.HealthCritical},
   210  	}
   211  
   212  	for i := 0; i <= 6; i++ {
   213  		select {
   214  		case update := <-hb.heartbeats:
   215  			require.Equal(t, update, expected[i],
   216  				"expected update %d to be '%s' but received '%s'",
   217  				i, expected[i], update)
   218  		case <-deadline:
   219  			t.Fatalf("timed out waiting for all script checks to finish")
   220  		}
   221  	}
   222  }
   223  
   224  // TestScript_TaskEnvInterpolation asserts that script check hooks are
   225  // interpolated in the same way that services are
   226  func TestScript_TaskEnvInterpolation(t *testing.T) {
   227  
   228  	logger := testlog.HCLogger(t)
   229  	consulClient := consul.NewMockConsulServiceClient(t, logger)
   230  	exec, cancel := newBlockingScriptExec()
   231  	defer cancel()
   232  
   233  	alloc := mock.ConnectAlloc()
   234  	task := alloc.Job.TaskGroups[0].Tasks[0]
   235  
   236  	task.Services[0].Name = "${NOMAD_JOB_NAME}-${TASK}-${SVC_NAME}"
   237  	task.Services[0].Checks[0].Name = "${NOMAD_JOB_NAME}-${SVC_NAME}-check"
   238  	alloc.Job.Canonicalize() // need to re-canonicalize b/c the mock already did it
   239  
   240  	env := taskenv.NewBuilder(mock.Node(), alloc, task, "global").SetHookEnv(
   241  		"script_check",
   242  		map[string]string{"SVC_NAME": "frontend"}).Build()
   243  
   244  	svcHook := newServiceHook(serviceHookConfig{
   245  		alloc:  alloc,
   246  		task:   task,
   247  		consul: consulClient,
   248  		logger: logger,
   249  	})
   250  	// emulate prestart having been fired
   251  	svcHook.taskEnv = env
   252  
   253  	scHook := newScriptCheckHook(scriptCheckHookConfig{
   254  		alloc:        alloc,
   255  		task:         task,
   256  		consul:       consulClient,
   257  		logger:       logger,
   258  		shutdownWait: time.Hour, // heartbeater will never be called
   259  	})
   260  	// emulate prestart having been fired
   261  	scHook.taskEnv = env
   262  	scHook.driverExec = exec
   263  
   264  	expectedSvc := svcHook.getWorkloadServices().Services[0]
   265  	expected := agentconsul.MakeCheckID(agentconsul.MakeAllocServiceID(
   266  		alloc.ID, task.Name, expectedSvc), expectedSvc.Checks[0])
   267  
   268  	actual := scHook.newScriptChecks()
   269  	check, ok := actual[expected]
   270  	require.True(t, ok)
   271  	require.Equal(t, "my-job-frontend-check", check.check.Name)
   272  
   273  	// emulate an update
   274  	env = taskenv.NewBuilder(mock.Node(), alloc, task, "global").SetHookEnv(
   275  		"script_check",
   276  		map[string]string{"SVC_NAME": "backend"}).Build()
   277  	scHook.taskEnv = env
   278  	svcHook.taskEnv = env
   279  
   280  	expectedSvc = svcHook.getWorkloadServices().Services[0]
   281  	expected = agentconsul.MakeCheckID(agentconsul.MakeAllocServiceID(
   282  		alloc.ID, task.Name, expectedSvc), expectedSvc.Checks[0])
   283  
   284  	actual = scHook.newScriptChecks()
   285  	check, ok = actual[expected]
   286  	require.True(t, ok)
   287  	require.Equal(t, "my-job-backend-check", check.check.Name)
   288  }