315 lines
7.7 KiB
Go
315 lines
7.7 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
//go:build !windows
|
|
// +build !windows
|
|
|
|
// todo(shoenig): Once Connect is supported on Windows, we'll need to make this
|
|
// set of tests work there too.
|
|
|
|
package taskrunner
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
|
|
consulapi "github.com/hashicorp/nomad/client/consul"
|
|
"github.com/hashicorp/nomad/helper"
|
|
"github.com/hashicorp/nomad/helper/testlog"
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
|
"github.com/hashicorp/nomad/nomad/mock"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
var _ interfaces.TaskPrestartHook = (*sidsHook)(nil)
|
|
|
|
func sidecar(task string) (string, structs.TaskKind) {
|
|
name := structs.ConnectProxyPrefix + "-" + task
|
|
kind := structs.TaskKind(structs.ConnectProxyPrefix + ":" + task)
|
|
return name, kind
|
|
}
|
|
|
|
func TestSIDSHook_recoverToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
r := require.New(t)
|
|
|
|
secrets := t.TempDir()
|
|
|
|
taskName, taskKind := sidecar("foo")
|
|
h := newSIDSHook(sidsHookConfig{
|
|
task: &structs.Task{
|
|
Name: taskName,
|
|
Kind: taskKind,
|
|
},
|
|
logger: testlog.HCLogger(t),
|
|
})
|
|
|
|
expected := uuid.Generate()
|
|
err := h.writeToken(secrets, expected)
|
|
r.NoError(err)
|
|
|
|
token, err := h.recoverToken(secrets)
|
|
r.NoError(err)
|
|
r.Equal(expected, token)
|
|
}
|
|
|
|
func TestSIDSHook_recoverToken_empty(t *testing.T) {
|
|
ci.Parallel(t)
|
|
r := require.New(t)
|
|
|
|
secrets := t.TempDir()
|
|
|
|
taskName, taskKind := sidecar("foo")
|
|
h := newSIDSHook(sidsHookConfig{
|
|
task: &structs.Task{
|
|
Name: taskName,
|
|
Kind: taskKind,
|
|
},
|
|
logger: testlog.HCLogger(t),
|
|
})
|
|
|
|
token, err := h.recoverToken(secrets)
|
|
r.NoError(err)
|
|
r.Empty(token)
|
|
}
|
|
|
|
func TestSIDSHook_recoverToken_unReadable(t *testing.T) {
|
|
ci.Parallel(t)
|
|
// This test fails when running as root because the test case for checking
|
|
// the error condition when the file is unreadable fails (root can read the
|
|
// file even though the permissions are set to 0200).
|
|
if unix.Geteuid() == 0 {
|
|
t.Skip("test only works as non-root")
|
|
}
|
|
|
|
r := require.New(t)
|
|
|
|
secrets := t.TempDir()
|
|
|
|
err := os.Chmod(secrets, 0000)
|
|
r.NoError(err)
|
|
|
|
taskName, taskKind := sidecar("foo")
|
|
h := newSIDSHook(sidsHookConfig{
|
|
task: &structs.Task{
|
|
Name: taskName,
|
|
Kind: taskKind,
|
|
},
|
|
logger: testlog.HCLogger(t),
|
|
})
|
|
|
|
_, err = h.recoverToken(secrets)
|
|
r.Error(err)
|
|
}
|
|
|
|
func TestSIDSHook_writeToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
r := require.New(t)
|
|
|
|
secrets := t.TempDir()
|
|
|
|
id := uuid.Generate()
|
|
h := new(sidsHook)
|
|
err := h.writeToken(secrets, id)
|
|
r.NoError(err)
|
|
|
|
content, err := os.ReadFile(filepath.Join(secrets, sidsTokenFile))
|
|
r.NoError(err)
|
|
r.Equal(id, string(content))
|
|
}
|
|
|
|
func TestSIDSHook_writeToken_unWritable(t *testing.T) {
|
|
ci.Parallel(t)
|
|
// This test fails when running as root because the test case for checking
|
|
// the error condition when the file is unreadable fails (root can read the
|
|
// file even though the permissions are set to 0200).
|
|
if unix.Geteuid() == 0 {
|
|
t.Skip("test only works as non-root")
|
|
}
|
|
|
|
r := require.New(t)
|
|
|
|
secrets := t.TempDir()
|
|
|
|
err := os.Chmod(secrets, 0000)
|
|
r.NoError(err)
|
|
|
|
id := uuid.Generate()
|
|
h := new(sidsHook)
|
|
err = h.writeToken(secrets, id)
|
|
r.Error(err)
|
|
}
|
|
|
|
func Test_SIDSHook_writeToken_nonExistent(t *testing.T) {
|
|
ci.Parallel(t)
|
|
r := require.New(t)
|
|
|
|
base := t.TempDir()
|
|
secrets := filepath.Join(base, "does/not/exist")
|
|
|
|
id := uuid.Generate()
|
|
h := new(sidsHook)
|
|
err := h.writeToken(secrets, id)
|
|
r.Error(err)
|
|
}
|
|
|
|
func TestSIDSHook_deriveSIToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
r := require.New(t)
|
|
|
|
taskName, taskKind := sidecar("task1")
|
|
h := newSIDSHook(sidsHookConfig{
|
|
alloc: &structs.Allocation{ID: "a1"},
|
|
task: &structs.Task{
|
|
Name: taskName,
|
|
Kind: taskKind,
|
|
},
|
|
logger: testlog.HCLogger(t),
|
|
sidsClient: consulapi.NewMockServiceIdentitiesClient(),
|
|
})
|
|
|
|
ctx := context.Background()
|
|
token, err := h.deriveSIToken(ctx)
|
|
r.NoError(err)
|
|
r.True(helper.IsUUID(token), "token: %q", token)
|
|
}
|
|
|
|
func TestSIDSHook_deriveSIToken_timeout(t *testing.T) {
|
|
ci.Parallel(t)
|
|
r := require.New(t)
|
|
|
|
siClient := consulapi.NewMockServiceIdentitiesClient()
|
|
siClient.DeriveTokenFn = func(allocation *structs.Allocation, strings []string) (m map[string]string, err error) {
|
|
select {
|
|
// block forever, hopefully triggering a timeout in the caller
|
|
}
|
|
}
|
|
|
|
taskName, taskKind := sidecar("task1")
|
|
h := newSIDSHook(sidsHookConfig{
|
|
alloc: &structs.Allocation{ID: "a1"},
|
|
task: &structs.Task{
|
|
Name: taskName,
|
|
Kind: taskKind,
|
|
},
|
|
logger: testlog.HCLogger(t),
|
|
sidsClient: siClient,
|
|
})
|
|
|
|
// set the timeout to a really small value for testing
|
|
h.derivationTimeout = time.Duration(1 * time.Millisecond)
|
|
|
|
ctx := context.Background()
|
|
_, err := h.deriveSIToken(ctx)
|
|
r.EqualError(err, "context deadline exceeded")
|
|
}
|
|
|
|
func TestSIDSHook_computeBackoff(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
try := func(i int, exp time.Duration) {
|
|
result := computeBackoff(i)
|
|
require.Equal(t, exp, result)
|
|
}
|
|
|
|
try(0, time.Duration(0))
|
|
try(1, 100*time.Millisecond)
|
|
try(2, 10*time.Second)
|
|
try(3, 15*time.Second)
|
|
try(4, 20*time.Second)
|
|
try(5, 25*time.Second)
|
|
}
|
|
|
|
func TestSIDSHook_backoff(t *testing.T) {
|
|
ci.Parallel(t)
|
|
r := require.New(t)
|
|
|
|
ctx := context.Background()
|
|
stop := !backoff(ctx, 0)
|
|
r.False(stop)
|
|
}
|
|
|
|
func TestSIDSHook_backoffKilled(t *testing.T) {
|
|
ci.Parallel(t)
|
|
r := require.New(t)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1)
|
|
defer cancel()
|
|
|
|
stop := !backoff(ctx, 1000)
|
|
r.True(stop)
|
|
}
|
|
|
|
func TestTaskRunner_DeriveSIToken_UnWritableTokenFile(t *testing.T) {
|
|
ci.Parallel(t)
|
|
// Normally this test would live in test_runner_test.go, but since it requires
|
|
// root and the check for root doesn't like Windows, we put this file in here
|
|
// for now.
|
|
|
|
// This test fails when running as root because the test case for checking
|
|
// the error condition when the file is unreadable fails (root can read the
|
|
// file even though the permissions are set to 0200).
|
|
if unix.Geteuid() == 0 {
|
|
t.Skip("test only works as non-root")
|
|
}
|
|
|
|
r := require.New(t)
|
|
|
|
alloc := mock.BatchConnectAlloc()
|
|
task := alloc.Job.TaskGroups[0].Tasks[0]
|
|
task.Config = map[string]interface{}{
|
|
"run_for": "0s",
|
|
}
|
|
|
|
trConfig, cleanup := testTaskRunnerConfig(t, alloc, task.Name)
|
|
defer cleanup()
|
|
|
|
// make the si_token file un-writable, triggering a failure after a
|
|
// successful token derivation
|
|
secrets := t.TempDir()
|
|
trConfig.TaskDir.SecretsDir = secrets
|
|
err := os.WriteFile(filepath.Join(secrets, sidsTokenFile), nil, 0400)
|
|
r.NoError(err)
|
|
|
|
// set a consul token for the nomad client, which is what triggers the
|
|
// SIDS hook to be applied
|
|
trConfig.ClientConfig.ConsulConfig.Token = uuid.Generate()
|
|
|
|
// derive token works just fine
|
|
deriveFn := func(*structs.Allocation, []string) (map[string]string, error) {
|
|
return map[string]string{task.Name: uuid.Generate()}, nil
|
|
}
|
|
siClient := trConfig.ConsulSI.(*consulapi.MockServiceIdentitiesClient)
|
|
siClient.DeriveTokenFn = deriveFn
|
|
|
|
// start the task runner
|
|
tr, err := NewTaskRunner(trConfig)
|
|
r.NoError(err)
|
|
defer tr.Kill(context.Background(), structs.NewTaskEvent("cleanup"))
|
|
useMockEnvoyBootstrapHook(tr) // mock the envoy bootstrap
|
|
|
|
go tr.Run()
|
|
|
|
// wait for task runner to finish running
|
|
testWaitForTaskToDie(t, tr)
|
|
|
|
// assert task exited un-successfully
|
|
finalState := tr.TaskState()
|
|
r.Equal(structs.TaskStateDead, finalState.State)
|
|
r.True(finalState.Failed) // should have failed to write SI token
|
|
r.Contains(finalState.Events[2].DisplayMessage, "failed to write SI token")
|
|
|
|
// assert the token is *not* on disk, as secrets dir was un-writable
|
|
tokenPath := filepath.Join(trConfig.TaskDir.SecretsDir, sidsTokenFile)
|
|
token, err := os.ReadFile(tokenPath)
|
|
r.NoError(err)
|
|
r.Empty(token)
|
|
}
|