github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/allocrunner/taskrunner/artifact_hook_test.go (about) 1 package taskrunner 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "path/filepath" 11 "sort" 12 "testing" 13 14 "github.com/hashicorp/nomad/ci" 15 "github.com/hashicorp/nomad/client/allocdir" 16 "github.com/hashicorp/nomad/client/allocrunner/interfaces" 17 "github.com/hashicorp/nomad/client/allocrunner/taskrunner/getter" 18 "github.com/hashicorp/nomad/client/taskenv" 19 "github.com/hashicorp/nomad/client/testutil" 20 "github.com/hashicorp/nomad/helper/testlog" 21 "github.com/hashicorp/nomad/nomad/structs" 22 "github.com/stretchr/testify/require" 23 "golang.org/x/exp/maps" 24 ) 25 26 // Statically assert the artifact hook implements the expected interface 27 var _ interfaces.TaskPrestartHook = (*artifactHook)(nil) 28 29 type mockEmitter struct { 30 events []*structs.TaskEvent 31 } 32 33 func (m *mockEmitter) EmitEvent(ev *structs.TaskEvent) { 34 m.events = append(m.events, ev) 35 } 36 37 // TestTaskRunner_ArtifactHook_Recoverable asserts that failures to download 38 // artifacts are a recoverable error. 39 func TestTaskRunner_ArtifactHook_Recoverable(t *testing.T) { 40 ci.Parallel(t) 41 42 me := &mockEmitter{} 43 sbox := getter.TestSandbox(t) 44 artifactHook := newArtifactHook(me, sbox, testlog.HCLogger(t)) 45 46 req := &interfaces.TaskPrestartRequest{ 47 TaskEnv: taskenv.NewEmptyTaskEnv(), 48 TaskDir: &allocdir.TaskDir{Dir: os.TempDir()}, 49 Task: &structs.Task{ 50 Artifacts: []*structs.TaskArtifact{ 51 { 52 GetterSource: "http://127.0.0.1:0", 53 GetterMode: structs.GetterModeAny, 54 }, 55 }, 56 }, 57 } 58 59 resp := interfaces.TaskPrestartResponse{} 60 61 err := artifactHook.Prestart(context.Background(), req, &resp) 62 63 require.False(t, resp.Done) 64 require.NotNil(t, err) 65 require.True(t, structs.IsRecoverable(err)) 66 require.Len(t, me.events, 1) 67 require.Equal(t, structs.TaskDownloadingArtifacts, me.events[0].Type) 68 } 69 70 // TestTaskRunnerArtifactHook_PartialDone asserts that the artifact hook skips 71 // already downloaded artifacts when subsequent artifacts fail and cause a 72 // restart. 73 func TestTaskRunner_ArtifactHook_PartialDone(t *testing.T) { 74 testutil.RequireRoot(t) 75 ci.Parallel(t) 76 77 me := &mockEmitter{} 78 sbox := getter.TestSandbox(t) 79 artifactHook := newArtifactHook(me, sbox, testlog.HCLogger(t)) 80 81 // Create a source directory with 1 of the 2 artifacts 82 srcdir := t.TempDir() 83 84 // Only create one of the 2 artifacts to cause an error on first run. 85 file1 := filepath.Join(srcdir, "foo.txt") 86 require.NoError(t, ioutil.WriteFile(file1, []byte{'1'}, 0644)) 87 88 // Test server to serve the artifacts 89 ts := httptest.NewServer(http.FileServer(http.Dir(srcdir))) 90 defer ts.Close() 91 92 // Create the target directory. 93 _, destdir := getter.SetupDir(t) 94 95 req := &interfaces.TaskPrestartRequest{ 96 TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, destdir, ""), 97 TaskDir: &allocdir.TaskDir{Dir: destdir}, 98 Task: &structs.Task{ 99 Artifacts: []*structs.TaskArtifact{ 100 { 101 GetterSource: ts.URL + "/foo.txt", 102 GetterMode: structs.GetterModeAny, 103 }, 104 { 105 GetterSource: ts.URL + "/bar.txt", 106 GetterMode: structs.GetterModeAny, 107 }, 108 }, 109 }, 110 } 111 112 resp := interfaces.TaskPrestartResponse{} 113 114 // On first run file1 (foo) should download but file2 (bar) should 115 // fail. 116 err := artifactHook.Prestart(context.Background(), req, &resp) 117 118 require.NotNil(t, err) 119 require.True(t, structs.IsRecoverable(err)) 120 require.Len(t, resp.State, 1) 121 require.False(t, resp.Done) 122 require.Len(t, me.events, 1) 123 require.Equal(t, structs.TaskDownloadingArtifacts, me.events[0].Type) 124 125 // Remove file1 from the server so it errors if its downloaded again. 126 require.NoError(t, os.Remove(file1)) 127 128 // Write file2 so artifacts can download successfully 129 file2 := filepath.Join(srcdir, "bar.txt") 130 require.NoError(t, ioutil.WriteFile(file2, []byte{'1'}, 0644)) 131 132 // Mock TaskRunner by copying state from resp to req and reset resp. 133 req.PreviousState = maps.Clone(resp.State) 134 135 resp = interfaces.TaskPrestartResponse{} 136 137 // Retry the download and assert it succeeds 138 err = artifactHook.Prestart(context.Background(), req, &resp) 139 140 require.NoError(t, err) 141 require.True(t, resp.Done) 142 require.Len(t, resp.State, 2) 143 144 // Assert both files downloaded properly 145 files, err := filepath.Glob(filepath.Join(destdir, "*.txt")) 146 require.NoError(t, err) 147 sort.Strings(files) 148 require.Contains(t, files[0], "bar.txt") 149 require.Contains(t, files[1], "foo.txt") 150 151 // Stop the test server entirely and assert that re-running works 152 ts.Close() 153 req.PreviousState = maps.Clone(resp.State) 154 resp = interfaces.TaskPrestartResponse{} 155 err = artifactHook.Prestart(context.Background(), req, &resp) 156 require.NoError(t, err) 157 require.True(t, resp.Done) 158 require.Len(t, resp.State, 2) 159 } 160 161 // TestTaskRunner_ArtifactHook_ConcurrentDownloadSuccess asserts that the artifact hook 162 // download multiple files concurrently. this is a successful test without any errors. 163 func TestTaskRunner_ArtifactHook_ConcurrentDownloadSuccess(t *testing.T) { 164 t.Parallel() 165 166 me := &mockEmitter{} 167 sbox := getter.TestSandbox(t) 168 artifactHook := newArtifactHook(me, sbox, testlog.HCLogger(t)) 169 170 // Create a source directory all 7 artifacts 171 srcdir := t.TempDir() 172 173 numOfFiles := 7 174 for i := 0; i < numOfFiles; i++ { 175 file := filepath.Join(srcdir, fmt.Sprintf("file%d.txt", i)) 176 require.NoError(t, ioutil.WriteFile(file, []byte{byte(i)}, 0644)) 177 } 178 179 // Test server to serve the artifacts 180 ts := httptest.NewServer(http.FileServer(http.Dir(srcdir))) 181 defer ts.Close() 182 183 // Create the target directory. 184 _, destdir := getter.SetupDir(t) 185 186 req := &interfaces.TaskPrestartRequest{ 187 TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, destdir, ""), 188 TaskDir: &allocdir.TaskDir{Dir: destdir}, 189 Task: &structs.Task{ 190 Artifacts: []*structs.TaskArtifact{ 191 { 192 GetterSource: ts.URL + "/file0.txt", 193 GetterMode: structs.GetterModeAny, 194 }, 195 { 196 GetterSource: ts.URL + "/file1.txt", 197 GetterMode: structs.GetterModeAny, 198 }, 199 { 200 GetterSource: ts.URL + "/file2.txt", 201 GetterMode: structs.GetterModeAny, 202 }, 203 { 204 GetterSource: ts.URL + "/file3.txt", 205 GetterMode: structs.GetterModeAny, 206 }, 207 { 208 GetterSource: ts.URL + "/file4.txt", 209 GetterMode: structs.GetterModeAny, 210 }, 211 { 212 GetterSource: ts.URL + "/file5.txt", 213 GetterMode: structs.GetterModeAny, 214 }, 215 { 216 GetterSource: ts.URL + "/file6.txt", 217 GetterMode: structs.GetterModeAny, 218 }, 219 }, 220 }, 221 } 222 223 resp := interfaces.TaskPrestartResponse{} 224 225 // start the hook 226 err := artifactHook.Prestart(context.Background(), req, &resp) 227 228 require.NoError(t, err) 229 require.True(t, resp.Done) 230 require.Len(t, resp.State, 7) 231 require.Len(t, me.events, 1) 232 require.Equal(t, structs.TaskDownloadingArtifacts, me.events[0].Type) 233 234 // Assert all files downloaded properly 235 files, err := filepath.Glob(filepath.Join(destdir, "*.txt")) 236 require.NoError(t, err) 237 require.Len(t, files, 7) 238 sort.Strings(files) 239 require.Contains(t, files[0], "file0.txt") 240 require.Contains(t, files[1], "file1.txt") 241 require.Contains(t, files[2], "file2.txt") 242 require.Contains(t, files[3], "file3.txt") 243 require.Contains(t, files[4], "file4.txt") 244 require.Contains(t, files[5], "file5.txt") 245 require.Contains(t, files[6], "file6.txt") 246 } 247 248 // TestTaskRunner_ArtifactHook_ConcurrentDownloadFailure asserts that the artifact hook 249 // download multiple files concurrently. first iteration will result in failure and 250 // second iteration should succeed without downloading already downloaded files. 251 func TestTaskRunner_ArtifactHook_ConcurrentDownloadFailure(t *testing.T) { 252 t.Parallel() 253 254 me := &mockEmitter{} 255 sbox := getter.TestSandbox(t) 256 artifactHook := newArtifactHook(me, sbox, testlog.HCLogger(t)) 257 258 // Create a source directory with 3 of the 4 artifacts 259 srcdir := t.TempDir() 260 261 file1 := filepath.Join(srcdir, "file1.txt") 262 require.NoError(t, ioutil.WriteFile(file1, []byte{'1'}, 0644)) 263 264 file2 := filepath.Join(srcdir, "file2.txt") 265 require.NoError(t, ioutil.WriteFile(file2, []byte{'2'}, 0644)) 266 267 file3 := filepath.Join(srcdir, "file3.txt") 268 require.NoError(t, ioutil.WriteFile(file3, []byte{'3'}, 0644)) 269 270 // Test server to serve the artifacts 271 ts := httptest.NewServer(http.FileServer(http.Dir(srcdir))) 272 defer ts.Close() 273 274 // Create the target directory. 275 _, destdir := getter.SetupDir(t) 276 277 req := &interfaces.TaskPrestartRequest{ 278 TaskEnv: taskenv.NewTaskEnv(nil, nil, nil, nil, destdir, ""), 279 TaskDir: &allocdir.TaskDir{Dir: destdir}, 280 Task: &structs.Task{ 281 Artifacts: []*structs.TaskArtifact{ 282 { 283 GetterSource: ts.URL + "/file0.txt", // this request will fail 284 GetterMode: structs.GetterModeAny, 285 }, 286 { 287 GetterSource: ts.URL + "/file1.txt", 288 GetterMode: structs.GetterModeAny, 289 }, 290 { 291 GetterSource: ts.URL + "/file2.txt", 292 GetterMode: structs.GetterModeAny, 293 }, 294 { 295 GetterSource: ts.URL + "/file3.txt", 296 GetterMode: structs.GetterModeAny, 297 }, 298 }, 299 }, 300 } 301 302 resp := interfaces.TaskPrestartResponse{} 303 304 // On first run all files will be downloaded except file0.txt 305 err := artifactHook.Prestart(context.Background(), req, &resp) 306 307 require.Error(t, err) 308 require.True(t, structs.IsRecoverable(err)) 309 require.Len(t, resp.State, 3) 310 require.False(t, resp.Done) 311 require.Len(t, me.events, 1) 312 require.Equal(t, structs.TaskDownloadingArtifacts, me.events[0].Type) 313 314 // delete the downloaded files so that it'll error if it's downloaded again 315 require.NoError(t, os.Remove(file1)) 316 require.NoError(t, os.Remove(file2)) 317 require.NoError(t, os.Remove(file3)) 318 319 // create the missing file 320 file0 := filepath.Join(srcdir, "file0.txt") 321 require.NoError(t, ioutil.WriteFile(file0, []byte{'0'}, 0644)) 322 323 // Mock TaskRunner by copying state from resp to req and reset resp. 324 req.PreviousState = maps.Clone(resp.State) 325 326 resp = interfaces.TaskPrestartResponse{} 327 328 // Retry the download and assert it succeeds 329 err = artifactHook.Prestart(context.Background(), req, &resp) 330 require.NoError(t, err) 331 require.True(t, resp.Done) 332 require.Len(t, resp.State, 4) 333 334 // Assert all files downloaded properly 335 files, err := filepath.Glob(filepath.Join(destdir, "*.txt")) 336 require.NoError(t, err) 337 sort.Strings(files) 338 require.Contains(t, files[0], "file0.txt") 339 require.Contains(t, files[1], "file1.txt") 340 require.Contains(t, files[2], "file2.txt") 341 require.Contains(t, files[3], "file3.txt") 342 343 // verify the file contents too, since files will also be created for failed downloads 344 data0, err := ioutil.ReadFile(files[0]) 345 require.NoError(t, err) 346 require.Equal(t, data0, []byte{'0'}) 347 348 data1, err := ioutil.ReadFile(files[1]) 349 require.NoError(t, err) 350 require.Equal(t, data1, []byte{'1'}) 351 352 data2, err := ioutil.ReadFile(files[2]) 353 require.NoError(t, err) 354 require.Equal(t, data2, []byte{'2'}) 355 356 data3, err := ioutil.ReadFile(files[3]) 357 require.NoError(t, err) 358 require.Equal(t, data3, []byte{'3'}) 359 360 require.True(t, resp.Done) 361 require.Len(t, resp.State, 4) 362 }