github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/docker/internal/volumes/manager_test.go (about) 1 package volumes 2 3 import ( 4 "context" 5 "errors" 6 "testing" 7 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/mock" 10 "github.com/stretchr/testify/require" 11 12 "gitlab.com/gitlab-org/gitlab-runner/executors/docker/internal/volumes/parser" 13 "gitlab.com/gitlab-org/gitlab-runner/helpers/path" 14 ) 15 16 func newDebugLoggerMock() *mockDebugLogger { 17 loggerMock := new(mockDebugLogger) 18 loggerMock.On("Debugln", mock.Anything) 19 20 return loggerMock 21 } 22 23 func TestErrVolumeAlreadyDefined(t *testing.T) { 24 err := NewErrVolumeAlreadyDefined("test-path") 25 assert.EqualError(t, err, `volume for container path "test-path" is already defined`) 26 } 27 28 func TestNewDefaultManager(t *testing.T) { 29 logger := newDebugLoggerMock() 30 31 m := NewManager(logger, nil, nil, ManagerConfig{}) 32 assert.IsType(t, &manager{}, m) 33 } 34 35 func newDefaultManager(config ManagerConfig) *manager { 36 m := &manager{ 37 logger: newDebugLoggerMock(), 38 config: config, 39 managedVolumes: make(map[string]bool, 0), 40 } 41 42 return m 43 } 44 45 func addCacheContainerManager(manager *manager) *MockCacheContainersManager { 46 containerManager := new(MockCacheContainersManager) 47 48 manager.cacheContainersManager = containerManager 49 50 return containerManager 51 } 52 53 func addParser(manager *manager) *parser.MockParser { 54 parserMock := new(parser.MockParser) 55 parserMock.On("Path").Return(path.NewUnixPath()) 56 57 manager.parser = parserMock 58 return parserMock 59 } 60 61 func TestDefaultManager_CreateUserVolumes_HostVolume(t *testing.T) { 62 testCases := map[string]struct { 63 volume string 64 parsedVolume *parser.Volume 65 baseContainerPath string 66 expectedBinding []string 67 expectedError error 68 }{ 69 "no volumes specified": { 70 volume: "", 71 expectedBinding: []string{"/host:/duplicated"}, 72 }, 73 "volume with absolute path": { 74 volume: "/host:/volume", 75 parsedVolume: &parser.Volume{Source: "/host", Destination: "/volume"}, 76 expectedBinding: []string{"/host:/duplicated", "/host:/volume"}, 77 }, 78 "volume with absolute path and with baseContainerPath specified": { 79 volume: "/host:/volume", 80 parsedVolume: &parser.Volume{Source: "/host", Destination: "/volume"}, 81 baseContainerPath: "/builds", 82 expectedBinding: []string{"/host:/duplicated", "/host:/volume"}, 83 }, 84 "volume without absolute path and without baseContainerPath specified": { 85 volume: "/host:volume", 86 parsedVolume: &parser.Volume{Source: "/host", Destination: "volume"}, 87 expectedBinding: []string{"/host:/duplicated", "/host:volume"}, 88 }, 89 "volume without absolute path and with baseContainerPath specified": { 90 volume: "/host:volume", 91 parsedVolume: &parser.Volume{Source: "/host", Destination: "volume"}, 92 baseContainerPath: "/builds/project", 93 expectedBinding: []string{"/host:/duplicated", "/host:/builds/project/volume"}, 94 }, 95 "duplicated volume specification": { 96 volume: "/host/new:/duplicated", 97 parsedVolume: &parser.Volume{Source: "/host/new", Destination: "/duplicated"}, 98 expectedBinding: []string{"/host:/duplicated"}, 99 expectedError: NewErrVolumeAlreadyDefined("/duplicated"), 100 }, 101 "volume with mode specified": { 102 volume: "/host/new:/my/path:ro", 103 parsedVolume: &parser.Volume{Source: "/host/new", Destination: "/my/path", Mode: "ro"}, 104 expectedBinding: []string{"/host:/duplicated", "/host/new:/my/path:ro"}, 105 }, 106 "root volume specified": { 107 volume: "/host/new:/:ro", 108 parsedVolume: &parser.Volume{Source: "/host/new", Destination: "/", Mode: "ro"}, 109 expectedBinding: []string{"/host:/duplicated"}, 110 expectedError: errDirectoryIsRootPath, 111 }, 112 } 113 114 for testName, testCase := range testCases { 115 t.Run(testName, func(t *testing.T) { 116 config := ManagerConfig{ 117 BaseContainerPath: testCase.baseContainerPath, 118 } 119 120 m := newDefaultManager(config) 121 122 volumeParser := addParser(m) 123 defer volumeParser.AssertExpectations(t) 124 125 volumeParser.On("ParseVolume", "/host:/duplicated"). 126 Return(&parser.Volume{Source: "/host", Destination: "/duplicated"}, nil). 127 Once() 128 129 err := m.Create("/host:/duplicated") 130 require.NoError(t, err) 131 132 if len(testCase.volume) > 0 { 133 volumeParser.On("ParseVolume", testCase.volume). 134 Return(testCase.parsedVolume, nil). 135 Once() 136 } 137 138 err = m.Create(testCase.volume) 139 assert.Equal(t, testCase.expectedError, err) 140 assert.Equal(t, testCase.expectedBinding, m.volumeBindings) 141 }) 142 } 143 } 144 145 func TestDefaultManager_CreateUserVolumes_CacheVolume_Disabled(t *testing.T) { 146 expectedBinding := []string{"/host:/duplicated"} 147 148 testCases := map[string]struct { 149 volume string 150 parsedVolume *parser.Volume 151 baseContainerPath string 152 153 expectedCacheContainerIDs []string 154 expectedConfigVolume string 155 expectedError error 156 }{ 157 "no volumes specified": { 158 volume: "", 159 }, 160 "volume with absolute path, without baseContainerPath and with disableCache": { 161 volume: "/volume", 162 parsedVolume: &parser.Volume{Destination: "/volume"}, 163 baseContainerPath: "", 164 expectedError: ErrCacheVolumesDisabled, 165 }, 166 "volume with absolute path, with baseContainerPath and with disableCache": { 167 volume: "/volume", 168 parsedVolume: &parser.Volume{Destination: "/volume"}, 169 baseContainerPath: "/builds/project", 170 expectedError: ErrCacheVolumesDisabled, 171 }, 172 "volume without absolute path, without baseContainerPath and with disableCache": { 173 volume: "volume", 174 parsedVolume: &parser.Volume{Destination: "volume"}, 175 expectedError: ErrCacheVolumesDisabled, 176 }, 177 "volume without absolute path, with baseContainerPath and with disableCache": { 178 volume: "volume", 179 parsedVolume: &parser.Volume{Destination: "volume"}, 180 baseContainerPath: "/builds/project", 181 expectedError: ErrCacheVolumesDisabled, 182 }, 183 "duplicated volume definition": { 184 volume: "/duplicated", 185 parsedVolume: &parser.Volume{Destination: "/duplicated"}, 186 baseContainerPath: "", 187 expectedError: ErrCacheVolumesDisabled, 188 }, 189 } 190 191 for testName, testCase := range testCases { 192 t.Run(testName, func(t *testing.T) { 193 config := ManagerConfig{ 194 BaseContainerPath: testCase.baseContainerPath, 195 DisableCache: true, 196 } 197 198 m := newDefaultManager(config) 199 200 volumeParser := addParser(m) 201 defer volumeParser.AssertExpectations(t) 202 203 volumeParser.On("ParseVolume", "/host:/duplicated"). 204 Return(&parser.Volume{Source: "/host", Destination: "/duplicated"}, nil). 205 Once() 206 207 err := m.Create("/host:/duplicated") 208 require.NoError(t, err) 209 210 if len(testCase.volume) > 0 { 211 volumeParser.On("ParseVolume", testCase.volume). 212 Return(testCase.parsedVolume, nil). 213 Once() 214 } 215 216 err = m.Create(testCase.volume) 217 assert.Equal(t, testCase.expectedError, err) 218 assert.Equal(t, expectedBinding, m.volumeBindings) 219 }) 220 } 221 } 222 223 func TestDefaultManager_CreateUserVolumes_CacheVolume_HostBased(t *testing.T) { 224 testCases := map[string]struct { 225 volume string 226 baseContainerPath string 227 cacheDir string 228 uniqueName string 229 230 expectedBinding []string 231 expectedCacheContainerIDs []string 232 expectedConfigVolume string 233 expectedError error 234 }{ 235 "volume with absolute path, without baseContainerPath and with cacheDir": { 236 volume: "/volume", 237 cacheDir: "/cache", 238 uniqueName: "uniq", 239 expectedBinding: []string{"/host:/duplicated", "/cache/uniq/14331bf18c8e434c4b3f48a8c5cc79aa:/volume"}, 240 }, 241 "volume with absolute path, with baseContainerPath and with cacheDir": { 242 volume: "/volume", 243 baseContainerPath: "/builds/project", 244 cacheDir: "/cache", 245 uniqueName: "uniq", 246 expectedBinding: []string{"/host:/duplicated", "/cache/uniq/14331bf18c8e434c4b3f48a8c5cc79aa:/volume"}, 247 }, 248 "volume without absolute path, without baseContainerPath and with cacheDir": { 249 volume: "volume", 250 cacheDir: "/cache", 251 uniqueName: "uniq", 252 expectedBinding: []string{"/host:/duplicated", "/cache/uniq/210ab9e731c9c36c2c38db15c28a8d1c:volume"}, 253 }, 254 "volume without absolute path, with baseContainerPath and with cacheDir": { 255 volume: "volume", 256 baseContainerPath: "/builds/project", 257 cacheDir: "/cache", 258 uniqueName: "uniq", 259 expectedBinding: []string{"/host:/duplicated", "/cache/uniq/f69aef9fb01e88e6213362a04877452d:/builds/project/volume"}, 260 }, 261 "duplicated volume definition": { 262 volume: "/duplicated", 263 cacheDir: "/cache", 264 uniqueName: "uniq", 265 expectedBinding: []string{"/host:/duplicated"}, 266 expectedError: NewErrVolumeAlreadyDefined("/duplicated"), 267 }, 268 } 269 270 for testName, testCase := range testCases { 271 t.Run(testName, func(t *testing.T) { 272 config := ManagerConfig{ 273 BaseContainerPath: testCase.baseContainerPath, 274 DisableCache: false, 275 CacheDir: testCase.cacheDir, 276 UniqueName: testCase.uniqueName, 277 } 278 279 m := newDefaultManager(config) 280 281 volumeParser := addParser(m) 282 defer volumeParser.AssertExpectations(t) 283 284 volumeParser.On("ParseVolume", "/host:/duplicated"). 285 Return(&parser.Volume{Source: "/host", Destination: "/duplicated"}, nil). 286 Once() 287 288 err := m.Create("/host:/duplicated") 289 require.NoError(t, err) 290 291 volumeParser.On("ParseVolume", testCase.volume). 292 Return(&parser.Volume{Destination: testCase.volume}, nil). 293 Once() 294 295 err = m.Create(testCase.volume) 296 assert.Equal(t, testCase.expectedError, err) 297 assert.Equal(t, testCase.expectedBinding, m.volumeBindings) 298 }) 299 } 300 } 301 302 func TestDefaultManager_CreateUserVolumes_CacheVolume_ContainerBased(t *testing.T) { 303 testCases := map[string]struct { 304 volume string 305 baseContainerPath string 306 uniqueName string 307 expectedContainerName string 308 expectedContainerPath string 309 existingContainerID string 310 newContainerID string 311 expectedCacheContainerID string 312 expectedError error 313 }{ 314 "volume with absolute path, without baseContainerPath and with existing container": { 315 volume: "/volume", 316 baseContainerPath: "", 317 uniqueName: "uniq", 318 expectedContainerName: "uniq-cache-14331bf18c8e434c4b3f48a8c5cc79aa", 319 expectedContainerPath: "/volume", 320 existingContainerID: "existingContainerID", 321 expectedCacheContainerID: "existingContainerID", 322 }, 323 "volume with absolute path, without baseContainerPath and with new container": { 324 volume: "/volume", 325 baseContainerPath: "", 326 uniqueName: "uniq", 327 expectedContainerName: "uniq-cache-14331bf18c8e434c4b3f48a8c5cc79aa", 328 expectedContainerPath: "/volume", 329 existingContainerID: "", 330 newContainerID: "newContainerID", 331 expectedCacheContainerID: "newContainerID", 332 }, 333 "volume without absolute path, without baseContainerPath and with existing container": { 334 volume: "volume", 335 baseContainerPath: "", 336 uniqueName: "uniq", 337 expectedContainerName: "uniq-cache-210ab9e731c9c36c2c38db15c28a8d1c", 338 expectedContainerPath: "volume", 339 existingContainerID: "existingContainerID", 340 expectedCacheContainerID: "existingContainerID", 341 }, 342 "volume without absolute path, without baseContainerPath and with new container": { 343 volume: "volume", 344 baseContainerPath: "", 345 uniqueName: "uniq", 346 expectedContainerName: "uniq-cache-210ab9e731c9c36c2c38db15c28a8d1c", 347 expectedContainerPath: "volume", 348 existingContainerID: "", 349 newContainerID: "newContainerID", 350 expectedCacheContainerID: "newContainerID", 351 }, 352 "volume without absolute path, with baseContainerPath and with existing container": { 353 volume: "volume", 354 baseContainerPath: "/builds/project", 355 uniqueName: "uniq", 356 expectedContainerName: "uniq-cache-f69aef9fb01e88e6213362a04877452d", 357 expectedContainerPath: "/builds/project/volume", 358 existingContainerID: "existingContainerID", 359 expectedCacheContainerID: "existingContainerID", 360 }, 361 "volume without absolute path, with baseContainerPath and with new container": { 362 volume: "volume", 363 baseContainerPath: "/builds/project", 364 uniqueName: "uniq", 365 expectedContainerName: "uniq-cache-f69aef9fb01e88e6213362a04877452d", 366 expectedContainerPath: "/builds/project/volume", 367 existingContainerID: "", 368 newContainerID: "newContainerID", 369 expectedCacheContainerID: "newContainerID", 370 }, 371 "duplicated volume definition": { 372 volume: "/duplicated", 373 uniqueName: "uniq", 374 expectedError: NewErrVolumeAlreadyDefined("/duplicated"), 375 }, 376 } 377 378 for testName, testCase := range testCases { 379 t.Run(testName, func(t *testing.T) { 380 config := ManagerConfig{ 381 BaseContainerPath: testCase.baseContainerPath, 382 UniqueName: testCase.uniqueName, 383 DisableCache: false, 384 } 385 386 m := newDefaultManager(config) 387 containerManager := addCacheContainerManager(m) 388 volumeParser := addParser(m) 389 390 defer func() { 391 containerManager.AssertExpectations(t) 392 volumeParser.AssertExpectations(t) 393 }() 394 395 volumeParser.On("ParseVolume", "/host:/duplicated"). 396 Return(&parser.Volume{Source: "/host", Destination: "/duplicated"}, nil). 397 Once() 398 399 err := m.Create("/host:/duplicated") 400 require.NoError(t, err) 401 402 if testCase.volume != "/duplicated" { 403 containerManager.On("FindOrCleanExisting", testCase.expectedContainerName, testCase.expectedContainerPath). 404 Return(testCase.existingContainerID). 405 Once() 406 407 if testCase.newContainerID != "" { 408 containerManager.On("Create", testCase.expectedContainerName, testCase.expectedContainerPath). 409 Return(testCase.newContainerID, nil). 410 Once() 411 } 412 } 413 414 volumeParser.On("ParseVolume", testCase.volume). 415 Return(&parser.Volume{Destination: testCase.volume}, nil). 416 Once() 417 418 err = m.Create(testCase.volume) 419 assert.Equal(t, testCase.expectedError, err) 420 421 if testCase.expectedCacheContainerID != "" { 422 assert.Contains(t, m.cacheContainerIDs, testCase.expectedCacheContainerID) 423 } 424 }) 425 } 426 } 427 428 func TestDefaultManager_CreateUserVolumes_CacheVolume_ContainerBased_WithError(t *testing.T) { 429 config := ManagerConfig{ 430 BaseContainerPath: "/builds/project", 431 UniqueName: "unique", 432 } 433 434 m := newDefaultManager(config) 435 containerManager := addCacheContainerManager(m) 436 volumeParser := addParser(m) 437 438 defer func() { 439 containerManager.AssertExpectations(t) 440 volumeParser.AssertExpectations(t) 441 }() 442 443 containerManager.On("FindOrCleanExisting", "unique-cache-f69aef9fb01e88e6213362a04877452d", "/builds/project/volume"). 444 Return(""). 445 Once() 446 447 containerManager.On("Create", "unique-cache-f69aef9fb01e88e6213362a04877452d", "/builds/project/volume"). 448 Return("", errors.New("test error")). 449 Once() 450 451 volumeParser.On("ParseVolume", "volume"). 452 Return(&parser.Volume{Destination: "volume"}, nil). 453 Once() 454 455 err := m.Create("volume") 456 assert.Error(t, err) 457 } 458 459 func TestDefaultManager_CreateUserVolumes_ParserError(t *testing.T) { 460 m := newDefaultManager(ManagerConfig{}) 461 462 volumeParser := new(parser.MockParser) 463 defer volumeParser.AssertExpectations(t) 464 m.parser = volumeParser 465 466 volumeParser.On("ParseVolume", "volume"). 467 Return(nil, errors.New("parser-test-error")). 468 Once() 469 470 err := m.Create("volume") 471 assert.Error(t, err) 472 } 473 474 func TestDefaultManager_CreateTemporary(t *testing.T) { 475 testCases := map[string]struct { 476 volume string 477 newContainerID string 478 returnedParsedVolume *parser.Volume 479 containerCreateError error 480 expectedContainerName string 481 expectedContainerPath string 482 expectedCacheContainerID string 483 expectedTmpContainerID string 484 expectedError error 485 }{ 486 "volume created": { 487 volume: "volume", 488 returnedParsedVolume: &parser.Volume{Destination: "volume"}, 489 newContainerID: "newContainerID", 490 expectedContainerName: "uniq-cache-f69aef9fb01e88e6213362a04877452d", 491 expectedContainerPath: "/builds/project/volume", 492 expectedCacheContainerID: "newContainerID", 493 expectedTmpContainerID: "newContainerID", 494 }, 495 "cache container creation error": { 496 volume: "volume", 497 returnedParsedVolume: &parser.Volume{Destination: "volume"}, 498 newContainerID: "", 499 containerCreateError: errors.New("test-error"), 500 expectedError: errors.New("test-error"), 501 }, 502 "duplicated volume definition": { 503 volume: "/duplicated", 504 expectedError: NewErrVolumeAlreadyDefined("/duplicated"), 505 }, 506 } 507 508 for testName, testCase := range testCases { 509 t.Run(testName, func(t *testing.T) { 510 config := ManagerConfig{ 511 BaseContainerPath: "/builds/project", 512 UniqueName: "unique", 513 } 514 515 m := newDefaultManager(config) 516 containerManager := addCacheContainerManager(m) 517 volumeParser := addParser(m) 518 519 defer func() { 520 containerManager.AssertExpectations(t) 521 volumeParser.AssertExpectations(t) 522 }() 523 524 volumeParser.On("ParseVolume", "/host:/duplicated"). 525 Return(&parser.Volume{Source: "/host", Destination: "/duplicated"}, nil). 526 Once() 527 528 err := m.Create("/host:/duplicated") 529 require.NoError(t, err) 530 531 if testCase.volume != "/duplicated" { 532 containerManager.On("FindOrCleanExisting", "unique-cache-f69aef9fb01e88e6213362a04877452d", "/builds/project/volume"). 533 Return(""). 534 Once() 535 536 containerManager.On("Create", "unique-cache-f69aef9fb01e88e6213362a04877452d", "/builds/project/volume"). 537 Return(testCase.newContainerID, testCase.containerCreateError). 538 Once() 539 } 540 541 err = m.CreateTemporary(testCase.volume) 542 assert.Equal(t, testCase.expectedError, err) 543 544 if testCase.expectedCacheContainerID != "" { 545 assert.Contains(t, m.cacheContainerIDs, testCase.expectedCacheContainerID) 546 } 547 548 if testCase.expectedTmpContainerID != "" { 549 assert.Contains(t, m.tmpContainerIDs, testCase.expectedTmpContainerID) 550 } 551 }) 552 } 553 } 554 555 func TestDefaultManager_Binds(t *testing.T) { 556 expectedElements := []string{"element1", "element2"} 557 m := &manager{ 558 volumeBindings: expectedElements, 559 } 560 561 assert.Equal(t, expectedElements, m.Binds()) 562 } 563 564 func TestDefaultManager_ContainerIDs(t *testing.T) { 565 expectedElements := []string{"element1", "element2"} 566 m := &manager{ 567 cacheContainerIDs: expectedElements, 568 } 569 570 assert.Equal(t, expectedElements, m.ContainerIDs()) 571 } 572 573 func TestDefaultManager_Cleanup(t *testing.T) { 574 ccManager := new(MockCacheContainersManager) 575 defer ccManager.AssertExpectations(t) 576 577 doneCh := make(chan bool, 1) 578 579 ccManager.On("Cleanup", mock.Anything, []string{"container-1"}). 580 Run(func(_ mock.Arguments) { 581 close(doneCh) 582 }). 583 Return(doneCh). 584 Once() 585 586 m := &manager{ 587 cacheContainersManager: ccManager, 588 tmpContainerIDs: []string{"container-1"}, 589 } 590 591 done := m.Cleanup(context.Background()) 592 <-done 593 }