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

     1  // +build !windows
     2  // todo(shoenig): Once Connect is supported on Windows, we'll need to make this
     3  //  set of tests work there too.
     4  
     5  package taskrunner
     6  
     7  import (
     8  	"context"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/hashicorp/nomad/client/allocrunner/interfaces"
    16  	"github.com/hashicorp/nomad/client/consul"
    17  	consulapi "github.com/hashicorp/nomad/client/consul"
    18  	"github.com/hashicorp/nomad/helper"
    19  	"github.com/hashicorp/nomad/helper/testlog"
    20  	"github.com/hashicorp/nomad/helper/uuid"
    21  	"github.com/hashicorp/nomad/nomad/mock"
    22  	"github.com/hashicorp/nomad/nomad/structs"
    23  	"github.com/hashicorp/nomad/testutil"
    24  	"github.com/stretchr/testify/require"
    25  	"golang.org/x/sys/unix"
    26  )
    27  
    28  var _ interfaces.TaskPrestartHook = (*sidsHook)(nil)
    29  
    30  func tmpDir(t *testing.T) string {
    31  	dir, err := ioutil.TempDir("", "sids-")
    32  	require.NoError(t, err)
    33  	return dir
    34  }
    35  
    36  func cleanupDir(t *testing.T, dir string) {
    37  	err := os.RemoveAll(dir)
    38  	require.NoError(t, err)
    39  }
    40  
    41  func sidecar(task string) (string, structs.TaskKind) {
    42  	name := structs.ConnectProxyPrefix + "-" + task
    43  	kind := structs.TaskKind(structs.ConnectProxyPrefix + ":" + task)
    44  	return name, kind
    45  }
    46  
    47  func TestSIDSHook_recoverToken(t *testing.T) {
    48  	t.Parallel()
    49  	r := require.New(t)
    50  
    51  	secrets := tmpDir(t)
    52  	defer cleanupDir(t, secrets)
    53  
    54  	taskName, taskKind := sidecar("foo")
    55  	h := newSIDSHook(sidsHookConfig{
    56  		task: &structs.Task{
    57  			Name: taskName,
    58  			Kind: taskKind,
    59  		},
    60  		logger: testlog.HCLogger(t),
    61  	})
    62  
    63  	expected := uuid.Generate()
    64  	err := h.writeToken(secrets, expected)
    65  	r.NoError(err)
    66  
    67  	token, err := h.recoverToken(secrets)
    68  	r.NoError(err)
    69  	r.Equal(expected, token)
    70  }
    71  
    72  func TestSIDSHook_recoverToken_empty(t *testing.T) {
    73  	t.Parallel()
    74  	r := require.New(t)
    75  
    76  	secrets := tmpDir(t)
    77  	defer cleanupDir(t, secrets)
    78  
    79  	taskName, taskKind := sidecar("foo")
    80  	h := newSIDSHook(sidsHookConfig{
    81  		task: &structs.Task{
    82  			Name: taskName,
    83  			Kind: taskKind,
    84  		},
    85  		logger: testlog.HCLogger(t),
    86  	})
    87  
    88  	token, err := h.recoverToken(secrets)
    89  	r.NoError(err)
    90  	r.Empty(token)
    91  }
    92  
    93  func TestSIDSHook_recoverToken_unReadable(t *testing.T) {
    94  	// This test fails when running as root because the test case for checking
    95  	// the error condition when the file is unreadable fails (root can read the
    96  	// file even though the permissions are set to 0200).
    97  	if unix.Geteuid() == 0 {
    98  		t.Skip("test only works as non-root")
    99  	}
   100  
   101  	t.Parallel()
   102  	r := require.New(t)
   103  
   104  	secrets := tmpDir(t)
   105  	defer cleanupDir(t, secrets)
   106  
   107  	err := os.Chmod(secrets, 0000)
   108  	r.NoError(err)
   109  
   110  	taskName, taskKind := sidecar("foo")
   111  	h := newSIDSHook(sidsHookConfig{
   112  		task: &structs.Task{
   113  			Name: taskName,
   114  			Kind: taskKind,
   115  		},
   116  		logger: testlog.HCLogger(t),
   117  	})
   118  
   119  	_, err = h.recoverToken(secrets)
   120  	r.Error(err)
   121  }
   122  
   123  func TestSIDSHook_writeToken(t *testing.T) {
   124  	t.Parallel()
   125  	r := require.New(t)
   126  
   127  	secrets := tmpDir(t)
   128  	defer cleanupDir(t, secrets)
   129  
   130  	id := uuid.Generate()
   131  	h := new(sidsHook)
   132  	err := h.writeToken(secrets, id)
   133  	r.NoError(err)
   134  
   135  	content, err := ioutil.ReadFile(filepath.Join(secrets, sidsTokenFile))
   136  	r.NoError(err)
   137  	r.Equal(id, string(content))
   138  }
   139  
   140  func TestSIDSHook_writeToken_unWritable(t *testing.T) {
   141  	// This test fails when running as root because the test case for checking
   142  	// the error condition when the file is unreadable fails (root can read the
   143  	// file even though the permissions are set to 0200).
   144  	if unix.Geteuid() == 0 {
   145  		t.Skip("test only works as non-root")
   146  	}
   147  
   148  	t.Parallel()
   149  	r := require.New(t)
   150  
   151  	secrets := tmpDir(t)
   152  	defer cleanupDir(t, secrets)
   153  
   154  	err := os.Chmod(secrets, 0000)
   155  	r.NoError(err)
   156  
   157  	id := uuid.Generate()
   158  	h := new(sidsHook)
   159  	err = h.writeToken(secrets, id)
   160  	r.Error(err)
   161  }
   162  
   163  func Test_SIDSHook_writeToken_nonExistent(t *testing.T) {
   164  	t.Parallel()
   165  	r := require.New(t)
   166  
   167  	base := tmpDir(t)
   168  	defer cleanupDir(t, base)
   169  	secrets := filepath.Join(base, "does/not/exist")
   170  
   171  	id := uuid.Generate()
   172  	h := new(sidsHook)
   173  	err := h.writeToken(secrets, id)
   174  	r.Error(err)
   175  }
   176  
   177  func TestSIDSHook_deriveSIToken(t *testing.T) {
   178  	t.Parallel()
   179  	r := require.New(t)
   180  
   181  	taskName, taskKind := sidecar("task1")
   182  	h := newSIDSHook(sidsHookConfig{
   183  		alloc: &structs.Allocation{ID: "a1"},
   184  		task: &structs.Task{
   185  			Name: taskName,
   186  			Kind: taskKind,
   187  		},
   188  		logger:     testlog.HCLogger(t),
   189  		sidsClient: consul.NewMockServiceIdentitiesClient(),
   190  	})
   191  
   192  	ctx := context.Background()
   193  	token, err := h.deriveSIToken(ctx)
   194  	r.NoError(err)
   195  	r.True(helper.IsUUID(token), "token: %q", token)
   196  }
   197  
   198  func TestSIDSHook_deriveSIToken_timeout(t *testing.T) {
   199  	t.Parallel()
   200  	r := require.New(t)
   201  
   202  	siClient := consul.NewMockServiceIdentitiesClient()
   203  	siClient.DeriveTokenFn = func(allocation *structs.Allocation, strings []string) (m map[string]string, err error) {
   204  		select {
   205  		// block forever, hopefully triggering a timeout in the caller
   206  		}
   207  	}
   208  
   209  	taskName, taskKind := sidecar("task1")
   210  	h := newSIDSHook(sidsHookConfig{
   211  		alloc: &structs.Allocation{ID: "a1"},
   212  		task: &structs.Task{
   213  			Name: taskName,
   214  			Kind: taskKind,
   215  		},
   216  		logger:     testlog.HCLogger(t),
   217  		sidsClient: siClient,
   218  	})
   219  
   220  	// set the timeout to a really small value for testing
   221  	h.derivationTimeout = time.Duration(1 * time.Millisecond)
   222  
   223  	ctx := context.Background()
   224  	_, err := h.deriveSIToken(ctx)
   225  	r.EqualError(err, "context deadline exceeded")
   226  }
   227  
   228  func TestSIDSHook_computeBackoff(t *testing.T) {
   229  	t.Parallel()
   230  
   231  	try := func(i int, exp time.Duration) {
   232  		result := computeBackoff(i)
   233  		require.Equal(t, exp, result)
   234  	}
   235  
   236  	try(0, time.Duration(0))
   237  	try(1, 100*time.Millisecond)
   238  	try(2, 10*time.Second)
   239  	try(3, 15*time.Second)
   240  	try(4, 20*time.Second)
   241  	try(5, 25*time.Second)
   242  }
   243  
   244  func TestSIDSHook_backoff(t *testing.T) {
   245  	t.Parallel()
   246  	r := require.New(t)
   247  
   248  	ctx := context.Background()
   249  	stop := !backoff(ctx, 0)
   250  	r.False(stop)
   251  }
   252  
   253  func TestSIDSHook_backoffKilled(t *testing.T) {
   254  	t.Parallel()
   255  	r := require.New(t)
   256  
   257  	ctx, cancel := context.WithTimeout(context.Background(), 1)
   258  	defer cancel()
   259  
   260  	stop := !backoff(ctx, 1000)
   261  	r.True(stop)
   262  }
   263  
   264  func TestTaskRunner_DeriveSIToken_UnWritableTokenFile(t *testing.T) {
   265  	// Normally this test would live in test_runner_test.go, but since it requires
   266  	// root and the check for root doesn't like Windows, we put this file in here
   267  	// for now.
   268  
   269  	// This test fails when running as root because the test case for checking
   270  	// the error condition when the file is unreadable fails (root can read the
   271  	// file even though the permissions are set to 0200).
   272  	if unix.Geteuid() == 0 {
   273  		t.Skip("test only works as non-root")
   274  	}
   275  
   276  	t.Parallel()
   277  	r := require.New(t)
   278  
   279  	alloc := mock.BatchConnectAlloc()
   280  	task := alloc.Job.TaskGroups[0].Tasks[0]
   281  	task.Config = map[string]interface{}{
   282  		"run_for": "0s",
   283  	}
   284  
   285  	trConfig, cleanup := testTaskRunnerConfig(t, alloc, task.Name)
   286  	defer cleanup()
   287  
   288  	// make the si_token file un-writable, triggering a failure after a
   289  	// successful token derivation
   290  	secrets := tmpDir(t)
   291  	defer cleanupDir(t, secrets)
   292  	trConfig.TaskDir.SecretsDir = secrets
   293  	err := ioutil.WriteFile(filepath.Join(secrets, sidsTokenFile), nil, 0400)
   294  	r.NoError(err)
   295  
   296  	// set a consul token for the nomad client, which is what triggers the
   297  	// SIDS hook to be applied
   298  	trConfig.ClientConfig.ConsulConfig.Token = uuid.Generate()
   299  
   300  	// derive token works just fine
   301  	deriveFn := func(*structs.Allocation, []string) (map[string]string, error) {
   302  		return map[string]string{task.Name: uuid.Generate()}, nil
   303  	}
   304  	siClient := trConfig.ConsulSI.(*consulapi.MockServiceIdentitiesClient)
   305  	siClient.DeriveTokenFn = deriveFn
   306  
   307  	// start the task runner
   308  	tr, err := NewTaskRunner(trConfig)
   309  	r.NoError(err)
   310  	defer tr.Kill(context.Background(), structs.NewTaskEvent("cleanup"))
   311  	useMockEnvoyBootstrapHook(tr) // mock the envoy bootstrap
   312  
   313  	go tr.Run()
   314  
   315  	// wait for task runner to finish running
   316  	select {
   317  	case <-tr.WaitCh():
   318  	case <-time.After(time.Duration(testutil.TestMultiplier()*15) * time.Second):
   319  		r.Fail("timed out waiting for task runner")
   320  	}
   321  
   322  	// assert task exited un-successfully
   323  	finalState := tr.TaskState()
   324  	r.Equal(structs.TaskStateDead, finalState.State)
   325  	r.True(finalState.Failed) // should have failed to write SI token
   326  	r.Contains(finalState.Events[2].DisplayMessage, "failed to write SI token")
   327  
   328  	// assert the token is *not* on disk, as secrets dir was un-writable
   329  	tokenPath := filepath.Join(trConfig.TaskDir.SecretsDir, sidsTokenFile)
   330  	token, err := ioutil.ReadFile(tokenPath)
   331  	r.NoError(err)
   332  	r.Empty(token)
   333  }