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 }