github.com/diptanu/nomad@v0.5.7-0.20170516172507-d72e86cbe3d9/command/agent/consul/script_test.go (about)

     1  package consul
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/hashicorp/consul/api"
    12  	"github.com/hashicorp/nomad/helper/testtask"
    13  	"github.com/hashicorp/nomad/nomad/structs"
    14  )
    15  
    16  func TestMain(m *testing.M) {
    17  	if !testtask.Run() {
    18  		os.Exit(m.Run())
    19  	}
    20  }
    21  
    22  // blockingScriptExec implements ScriptExec by running a subcommand that never
    23  // exits.
    24  type blockingScriptExec struct {
    25  	// running is ticked before blocking to allow synchronizing operations
    26  	running chan struct{}
    27  
    28  	// set to true if Exec is called and has exited
    29  	exited bool
    30  }
    31  
    32  func newBlockingScriptExec() *blockingScriptExec {
    33  	return &blockingScriptExec{running: make(chan struct{})}
    34  }
    35  
    36  func (b *blockingScriptExec) Exec(ctx context.Context, _ string, _ []string) ([]byte, int, error) {
    37  	b.running <- struct{}{}
    38  	cmd := exec.CommandContext(ctx, testtask.Path(), "sleep", "9000h")
    39  	err := cmd.Run()
    40  	code := 0
    41  	if exitErr, ok := err.(*exec.ExitError); ok {
    42  		if !exitErr.Success() {
    43  			code = 1
    44  		}
    45  	}
    46  	b.exited = true
    47  	return []byte{}, code, err
    48  }
    49  
    50  // TestConsulScript_Exec_Cancel asserts cancelling a script check shortcircuits
    51  // any running scripts.
    52  func TestConsulScript_Exec_Cancel(t *testing.T) {
    53  	serviceCheck := structs.ServiceCheck{
    54  		Name:     "sleeper",
    55  		Interval: time.Hour,
    56  		Timeout:  time.Hour,
    57  	}
    58  	exec := newBlockingScriptExec()
    59  
    60  	// pass nil for heartbeater as it shouldn't be called
    61  	check := newScriptCheck("allocid", "testtask", "checkid", &serviceCheck, exec, nil, testLogger(), nil)
    62  	handle := check.run()
    63  
    64  	// wait until Exec is called
    65  	<-exec.running
    66  
    67  	// cancel now that we're blocked in exec
    68  	handle.cancel()
    69  
    70  	select {
    71  	case <-handle.wait():
    72  	case <-time.After(3 * time.Second):
    73  		t.Fatalf("timed out waiting for script check to exit")
    74  	}
    75  	if !exec.exited {
    76  		t.Errorf("expected script executor to run and exit but it has not")
    77  	}
    78  }
    79  
    80  type execStatus struct {
    81  	checkID string
    82  	output  string
    83  	status  string
    84  }
    85  
    86  // fakeHeartbeater implements the heartbeater interface to allow mocking out
    87  // Consul in script executor tests.
    88  type fakeHeartbeater struct {
    89  	updates chan execStatus
    90  }
    91  
    92  func (f *fakeHeartbeater) UpdateTTL(checkID, output, status string) error {
    93  	f.updates <- execStatus{checkID: checkID, output: output, status: status}
    94  	return nil
    95  }
    96  
    97  func newFakeHeartbeater() *fakeHeartbeater {
    98  	return &fakeHeartbeater{updates: make(chan execStatus)}
    99  }
   100  
   101  // TestConsulScript_Exec_Timeout asserts a script will be killed when the
   102  // timeout is reached.
   103  func TestConsulScript_Exec_Timeout(t *testing.T) {
   104  	t.Parallel() // run the slow tests in parallel
   105  	serviceCheck := structs.ServiceCheck{
   106  		Name:     "sleeper",
   107  		Interval: time.Hour,
   108  		Timeout:  time.Second,
   109  	}
   110  	exec := newBlockingScriptExec()
   111  
   112  	hb := newFakeHeartbeater()
   113  	check := newScriptCheck("allocid", "testtask", "checkid", &serviceCheck, exec, hb, testLogger(), nil)
   114  	handle := check.run()
   115  	defer handle.cancel() // just-in-case cleanup
   116  	<-exec.running
   117  
   118  	// Check for UpdateTTL call
   119  	select {
   120  	case update := <-hb.updates:
   121  		if update.status != api.HealthCritical {
   122  			t.Errorf("expected %q due to timeout but received %q", api.HealthCritical, update)
   123  		}
   124  	case <-time.After(3 * time.Second):
   125  		t.Fatalf("timed out waiting for script check to exit")
   126  	}
   127  	if !exec.exited {
   128  		t.Errorf("expected script executor to run and exit but it has not")
   129  	}
   130  
   131  	// Cancel and watch for exit
   132  	handle.cancel()
   133  	select {
   134  	case <-handle.wait():
   135  		// ok!
   136  	case update := <-hb.updates:
   137  		t.Errorf("unexpected UpdateTTL call on exit with status=%q", update)
   138  	case <-time.After(3 * time.Second):
   139  		t.Fatalf("timed out waiting for script check to exit")
   140  	}
   141  }
   142  
   143  // sleeperExec sleeps for 100ms but returns successfully to allow testing timeout conditions
   144  type sleeperExec struct{}
   145  
   146  func (sleeperExec) Exec(context.Context, string, []string) ([]byte, int, error) {
   147  	time.Sleep(100 * time.Millisecond)
   148  	return []byte{}, 0, nil
   149  }
   150  
   151  // TestConsulScript_Exec_TimeoutCritical asserts a script will be killed when
   152  // the timeout is reached and always set a critical status regardless of what
   153  // Exec returns.
   154  func TestConsulScript_Exec_TimeoutCritical(t *testing.T) {
   155  	t.Parallel() // run the slow tests in parallel
   156  	serviceCheck := structs.ServiceCheck{
   157  		Name:     "sleeper",
   158  		Interval: time.Hour,
   159  		Timeout:  time.Nanosecond,
   160  	}
   161  	hb := newFakeHeartbeater()
   162  	check := newScriptCheck("allocid", "testtask", "checkid", &serviceCheck, sleeperExec{}, hb, testLogger(), nil)
   163  	handle := check.run()
   164  	defer handle.cancel() // just-in-case cleanup
   165  
   166  	// Check for UpdateTTL call
   167  	select {
   168  	case update := <-hb.updates:
   169  		if update.status != api.HealthCritical {
   170  			t.Errorf("expected %q due to timeout but received %q", api.HealthCritical, update)
   171  		}
   172  		if update.output != context.DeadlineExceeded.Error() {
   173  			t.Errorf("expected output=%q but found: %q", context.DeadlineExceeded.Error(), update.output)
   174  		}
   175  	case <-time.After(3 * time.Second):
   176  		t.Fatalf("timed out waiting for script check to timeout")
   177  	}
   178  }
   179  
   180  // simpleExec is a fake ScriptExecutor that returns whatever is specified.
   181  type simpleExec struct {
   182  	code int
   183  	err  error
   184  }
   185  
   186  func (s simpleExec) Exec(context.Context, string, []string) ([]byte, int, error) {
   187  	return []byte(fmt.Sprintf("code=%d err=%v", s.code, s.err)), s.code, s.err
   188  }
   189  
   190  // newSimpleExec creates a new ScriptExecutor that returns the given code and err.
   191  func newSimpleExec(code int, err error) simpleExec {
   192  	return simpleExec{code: code, err: err}
   193  }
   194  
   195  // TestConsulScript_Exec_Shutdown asserts a script will be executed once more
   196  // when told to shutdown.
   197  func TestConsulScript_Exec_Shutdown(t *testing.T) {
   198  	serviceCheck := structs.ServiceCheck{
   199  		Name:     "sleeper",
   200  		Interval: time.Hour,
   201  		Timeout:  3 * time.Second,
   202  	}
   203  
   204  	hb := newFakeHeartbeater()
   205  	shutdown := make(chan struct{})
   206  	exec := newSimpleExec(0, nil)
   207  	check := newScriptCheck("allocid", "testtask", "checkid", &serviceCheck, exec, hb, testLogger(), shutdown)
   208  	handle := check.run()
   209  	defer handle.cancel() // just-in-case cleanup
   210  
   211  	// Tell scriptCheck to exit
   212  	close(shutdown)
   213  
   214  	select {
   215  	case update := <-hb.updates:
   216  		if update.status != api.HealthPassing {
   217  			t.Errorf("expected %q due to timeout but received %q", api.HealthCritical, update)
   218  		}
   219  	case <-time.After(3 * time.Second):
   220  		t.Fatalf("timed out waiting for script check to exit")
   221  	}
   222  
   223  	select {
   224  	case <-handle.wait():
   225  		// ok!
   226  	case <-time.After(3 * time.Second):
   227  		t.Fatalf("timed out waiting for script check to exit")
   228  	}
   229  }
   230  
   231  func TestConsulScript_Exec_Codes(t *testing.T) {
   232  	run := func(code int, err error, expected string) func(t *testing.T) {
   233  		return func(t *testing.T) {
   234  			t.Parallel()
   235  			serviceCheck := structs.ServiceCheck{
   236  				Name:     "test",
   237  				Interval: time.Hour,
   238  				Timeout:  3 * time.Second,
   239  			}
   240  
   241  			hb := newFakeHeartbeater()
   242  			shutdown := make(chan struct{})
   243  			exec := newSimpleExec(code, err)
   244  			check := newScriptCheck("allocid", "testtask", "checkid", &serviceCheck, exec, hb, testLogger(), shutdown)
   245  			handle := check.run()
   246  			defer handle.cancel()
   247  
   248  			select {
   249  			case update := <-hb.updates:
   250  				if update.status != expected {
   251  					t.Errorf("expected %q but received %q", expected, update)
   252  				}
   253  				// assert output is being reported
   254  				expectedOutput := fmt.Sprintf("code=%d err=%v", code, err)
   255  				if err != nil {
   256  					expectedOutput = err.Error()
   257  				}
   258  				if update.output != expectedOutput {
   259  					t.Errorf("expected output=%q but found: %q", expectedOutput, update.output)
   260  				}
   261  			case <-time.After(3 * time.Second):
   262  				t.Fatalf("timed out waiting for script check to exec")
   263  			}
   264  		}
   265  	}
   266  
   267  	// Test exit codes with errors
   268  	t.Run("Passing", run(0, nil, api.HealthPassing))
   269  	t.Run("Warning", run(1, nil, api.HealthWarning))
   270  	t.Run("Critical-2", run(2, nil, api.HealthCritical))
   271  	t.Run("Critical-9000", run(9000, nil, api.HealthCritical))
   272  
   273  	// Errors should always cause Critical status
   274  	err := fmt.Errorf("test error")
   275  	t.Run("Error-0", run(0, err, api.HealthCritical))
   276  	t.Run("Error-1", run(1, err, api.HealthCritical))
   277  	t.Run("Error-2", run(2, err, api.HealthCritical))
   278  	t.Run("Error-9000", run(9000, err, api.HealthCritical))
   279  }