github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/docker/executor_docker_test.go (about) 1 package docker 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "flag" 8 "fmt" 9 "io/ioutil" 10 "os" 11 "path" 12 "strings" 13 "sync" 14 "testing" 15 "time" 16 17 "github.com/docker/docker/api/types" 18 "github.com/docker/docker/api/types/container" 19 "github.com/docker/docker/api/types/network" 20 "github.com/stretchr/testify/assert" 21 "github.com/stretchr/testify/mock" 22 "github.com/stretchr/testify/require" 23 24 "gitlab.com/gitlab-org/gitlab-runner/common" 25 "gitlab.com/gitlab-org/gitlab-runner/executors" 26 "gitlab.com/gitlab-org/gitlab-runner/executors/docker/internal/volumes" 27 "gitlab.com/gitlab-org/gitlab-runner/executors/docker/internal/volumes/parser" 28 "gitlab.com/gitlab-org/gitlab-runner/helpers" 29 docker_helpers "gitlab.com/gitlab-org/gitlab-runner/helpers/docker" 30 "gitlab.com/gitlab-org/gitlab-runner/helpers/docker/helperimage" 31 "gitlab.com/gitlab-org/gitlab-runner/helpers/featureflags" 32 ) 33 34 func TestMain(m *testing.M) { 35 DockerPrebuiltImagesPaths = []string{"../../out/helper-images/"} 36 37 flag.Parse() 38 os.Exit(m.Run()) 39 } 40 41 // ImagePullOptions contains the RegistryAuth which is inferred from the docker 42 // configuration for the user, so just mock it out here. 43 func buildImagePullOptions(e executor, configName string) mock.AnythingOfTypeArgument { 44 return mock.AnythingOfType("ImagePullOptions") 45 } 46 47 func TestParseDeviceStringOne(t *testing.T) { 48 e := executor{} 49 50 device, err := e.parseDeviceString("/dev/kvm") 51 52 assert.NoError(t, err) 53 assert.Equal(t, "/dev/kvm", device.PathOnHost) 54 assert.Equal(t, "/dev/kvm", device.PathInContainer) 55 assert.Equal(t, "rwm", device.CgroupPermissions) 56 } 57 58 func TestParseDeviceStringTwo(t *testing.T) { 59 e := executor{} 60 61 device, err := e.parseDeviceString("/dev/kvm:/devices/kvm") 62 63 assert.NoError(t, err) 64 assert.Equal(t, "/dev/kvm", device.PathOnHost) 65 assert.Equal(t, "/devices/kvm", device.PathInContainer) 66 assert.Equal(t, "rwm", device.CgroupPermissions) 67 } 68 69 func TestParseDeviceStringThree(t *testing.T) { 70 e := executor{} 71 72 device, err := e.parseDeviceString("/dev/kvm:/devices/kvm:r") 73 74 assert.NoError(t, err) 75 assert.Equal(t, "/dev/kvm", device.PathOnHost) 76 assert.Equal(t, "/devices/kvm", device.PathInContainer) 77 assert.Equal(t, "r", device.CgroupPermissions) 78 } 79 80 func TestParseDeviceStringFour(t *testing.T) { 81 e := executor{} 82 83 _, err := e.parseDeviceString("/dev/kvm:/devices/kvm:r:oops") 84 85 assert.Error(t, err) 86 } 87 88 type testAllowedImageDescription struct { 89 allowed bool 90 image string 91 allowedImages []string 92 } 93 94 var testAllowedImages = []testAllowedImageDescription{ 95 {true, "ruby", []string{"*"}}, 96 {true, "ruby:2.1", []string{"*"}}, 97 {true, "ruby:latest", []string{"*"}}, 98 {true, "library/ruby", []string{"*/*"}}, 99 {true, "library/ruby:2.1", []string{"*/*"}}, 100 {true, "library/ruby:2.1", []string{"*/*:*"}}, 101 {true, "my.registry.tld/library/ruby", []string{"my.registry.tld/*/*"}}, 102 {true, "my.registry.tld/library/ruby:2.1", []string{"my.registry.tld/*/*:*"}}, 103 {true, "my.registry.tld/group/subgroup/ruby", []string{"my.registry.tld/*/*/*"}}, 104 {true, "my.registry.tld/group/subgroup/ruby:2.1", []string{"my.registry.tld/*/*/*:*"}}, 105 {true, "ruby", []string{"**/*"}}, 106 {true, "ruby:2.1", []string{"**/*"}}, 107 {true, "ruby:latest", []string{"**/*"}}, 108 {true, "library/ruby", []string{"**/*"}}, 109 {true, "library/ruby:2.1", []string{"**/*"}}, 110 {true, "library/ruby:2.1", []string{"**/*:*"}}, 111 {true, "my.registry.tld/library/ruby", []string{"my.registry.tld/**/*"}}, 112 {true, "my.registry.tld/library/ruby:2.1", []string{"my.registry.tld/**/*:*"}}, 113 {true, "my.registry.tld/group/subgroup/ruby", []string{"my.registry.tld/**/*"}}, 114 {true, "my.registry.tld/group/subgroup/ruby:2.1", []string{"my.registry.tld/**/*:*"}}, 115 {false, "library/ruby", []string{"*"}}, 116 {false, "library/ruby:2.1", []string{"*"}}, 117 {false, "my.registry.tld/ruby", []string{"*"}}, 118 {false, "my.registry.tld/ruby:2.1", []string{"*"}}, 119 {false, "my.registry.tld/library/ruby", []string{"*"}}, 120 {false, "my.registry.tld/library/ruby:2.1", []string{"*"}}, 121 {false, "my.registry.tld/group/subgroup/ruby", []string{"*"}}, 122 {false, "my.registry.tld/group/subgroup/ruby:2.1", []string{"*"}}, 123 {false, "library/ruby", []string{"*/*:*"}}, 124 {false, "my.registry.tld/group/subgroup/ruby", []string{"my.registry.tld/*/*"}}, 125 {false, "my.registry.tld/group/subgroup/ruby:2.1", []string{"my.registry.tld/*/*:*"}}, 126 {false, "library/ruby", []string{"**/*:*"}}, 127 } 128 129 func TestVerifyAllowedImage(t *testing.T) { 130 e := executor{} 131 132 for _, test := range testAllowedImages { 133 err := e.verifyAllowedImage(test.image, "", test.allowedImages, []string{}) 134 135 if err != nil && test.allowed { 136 t.Errorf("%q must be allowed by %q", test.image, test.allowedImages) 137 } else if err == nil && !test.allowed { 138 t.Errorf("%q must not be allowed by %q", test.image, test.allowedImages) 139 } 140 } 141 } 142 143 type testServiceDescription struct { 144 description string 145 image string 146 service string 147 version string 148 alias string 149 alternative string 150 } 151 152 var testServices = []testServiceDescription{ 153 {"service", "service:latest", "service", "latest", "service", ""}, 154 {"service:version", "service:version", "service", "version", "service", ""}, 155 {"namespace/service", "namespace/service:latest", "namespace/service", "latest", "namespace__service", "namespace-service"}, 156 {"namespace/service:version", "namespace/service:version", "namespace/service", "version", "namespace__service", "namespace-service"}, 157 {"domain.tld/service", "domain.tld/service:latest", "domain.tld/service", "latest", "domain.tld__service", "domain.tld-service"}, 158 {"domain.tld/service:version", "domain.tld/service:version", "domain.tld/service", "version", "domain.tld__service", "domain.tld-service"}, 159 {"domain.tld/namespace/service", "domain.tld/namespace/service:latest", "domain.tld/namespace/service", "latest", "domain.tld__namespace__service", "domain.tld-namespace-service"}, 160 {"domain.tld/namespace/service:version", "domain.tld/namespace/service:version", "domain.tld/namespace/service", "version", "domain.tld__namespace__service", "domain.tld-namespace-service"}, 161 {"domain.tld:8080/service", "domain.tld:8080/service:latest", "domain.tld/service", "latest", "domain.tld__service", "domain.tld-service"}, 162 {"domain.tld:8080/service:version", "domain.tld:8080/service:version", "domain.tld/service", "version", "domain.tld__service", "domain.tld-service"}, 163 {"domain.tld:8080/namespace/service", "domain.tld:8080/namespace/service:latest", "domain.tld/namespace/service", "latest", "domain.tld__namespace__service", "domain.tld-namespace-service"}, 164 {"domain.tld:8080/namespace/service:version", "domain.tld:8080/namespace/service:version", "domain.tld/namespace/service", "version", "domain.tld__namespace__service", "domain.tld-namespace-service"}, 165 {"subdomain.domain.tld:8080/service", "subdomain.domain.tld:8080/service:latest", "subdomain.domain.tld/service", "latest", "subdomain.domain.tld__service", "subdomain.domain.tld-service"}, 166 {"subdomain.domain.tld:8080/service:version", "subdomain.domain.tld:8080/service:version", "subdomain.domain.tld/service", "version", "subdomain.domain.tld__service", "subdomain.domain.tld-service"}, 167 {"subdomain.domain.tld:8080/namespace/service", "subdomain.domain.tld:8080/namespace/service:latest", "subdomain.domain.tld/namespace/service", "latest", "subdomain.domain.tld__namespace__service", "subdomain.domain.tld-namespace-service"}, 168 {"subdomain.domain.tld:8080/namespace/service:version", "subdomain.domain.tld:8080/namespace/service:version", "subdomain.domain.tld/namespace/service", "version", "subdomain.domain.tld__namespace__service", "subdomain.domain.tld-namespace-service"}, 169 } 170 171 func testSplitService(t *testing.T, test testServiceDescription) { 172 e := executor{} 173 service, version, imageName, linkNames := e.splitServiceAndVersion(test.description) 174 175 assert.Equal(t, test.service, service, "service for "+test.description) 176 assert.Equal(t, test.version, version, "version for "+test.description) 177 assert.Equal(t, test.image, imageName, "image for "+test.description) 178 assert.Equal(t, test.alias, linkNames[0], "alias for "+test.description) 179 if test.alternative != "" { 180 assert.Len(t, linkNames, 2, "linkNames len for "+test.description) 181 assert.Equal(t, test.alternative, linkNames[1], "alternative for "+test.description) 182 } else { 183 assert.Len(t, linkNames, 1, "linkNames len for "+test.description) 184 } 185 } 186 187 func TestSplitService(t *testing.T) { 188 for _, test := range testServices { 189 t.Run(test.description, func(t *testing.T) { 190 testSplitService(t, test) 191 }) 192 } 193 } 194 195 func testServiceFromNamedImage(t *testing.T, description, imageName, serviceName string) { 196 var c docker_helpers.MockClient 197 defer c.AssertExpectations(t) 198 199 containerName := fmt.Sprintf("runner-abcdef12-project-0-concurrent-0-%s-0", strings.Replace(serviceName, "/", "__", -1)) 200 networkID := "network-id" 201 202 e := executor{ 203 client: &c, 204 info: types.Info{ 205 OSType: helperimage.OSTypeLinux, 206 Architecture: "amd64", 207 }, 208 volumeParser: parser.NewLinuxParser(), 209 } 210 211 options := buildImagePullOptions(e, imageName) 212 e.Config = common.RunnerConfig{} 213 e.Config.Docker = &common.DockerConfig{} 214 e.Build = &common.Build{ 215 ProjectRunnerID: 0, 216 Runner: &common.RunnerConfig{}, 217 } 218 e.Build.JobInfo.ProjectID = 0 219 e.Build.Runner.Token = "abcdef1234567890" 220 e.Context = context.Background() 221 var err error 222 e.helperImageInfo, err = helperimage.Get(common.REVISION, helperimage.Config{ 223 OSType: e.info.OSType, 224 Architecture: e.info.Architecture, 225 OperatingSystem: e.info.OperatingSystem, 226 }) 227 require.NoError(t, err) 228 229 c.On("ImagePullBlocking", e.Context, imageName, options). 230 Return(nil). 231 Once() 232 233 c.On("ImageInspectWithRaw", e.Context, imageName). 234 Return(types.ImageInspect{ID: "image-id"}, nil, nil). 235 Twice() 236 237 c.On("ContainerRemove", e.Context, containerName, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}). 238 Return(nil). 239 Once() 240 241 networkContainersMap := map[string]types.EndpointResource{ 242 "1": {Name: containerName}, 243 } 244 245 c.On("NetworkList", e.Context, types.NetworkListOptions{}). 246 Return([]types.NetworkResource{{ID: networkID, Name: "network-name", Containers: networkContainersMap}}, nil). 247 Once() 248 249 c.On("NetworkDisconnect", e.Context, networkID, containerName, true). 250 Return(nil). 251 Once() 252 253 c.On("ImageInspectWithRaw", mock.Anything, "gitlab/gitlab-runner-helper:x86_64-latest"). 254 Return(types.ImageInspect{ID: "helper-image-id"}, nil, nil). 255 Once() 256 257 c.On("ContainerCreate", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). 258 Return(container.ContainerCreateCreatedBody{ID: containerName}, nil). 259 Once() 260 261 c.On("ContainerStart", e.Context, mock.Anything, mock.Anything). 262 Return(nil). 263 Once() 264 265 err = e.createVolumesManager() 266 require.NoError(t, err) 267 268 linksMap := make(map[string]*types.Container) 269 err = e.createFromServiceDefinition(0, common.Image{Name: description}, linksMap) 270 assert.NoError(t, err) 271 } 272 273 func TestServiceFromNamedImage(t *testing.T) { 274 for _, test := range testServices { 275 t.Run(test.description, func(t *testing.T) { 276 testServiceFromNamedImage(t, test.description, test.image, test.service) 277 }) 278 } 279 } 280 281 func TestDockerForNamedImage(t *testing.T) { 282 var c docker_helpers.MockClient 283 defer c.AssertExpectations(t) 284 validSHA := "real@sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c" 285 286 e := executor{client: &c} 287 e.Context = context.Background() 288 options := buildImagePullOptions(e, "test") 289 290 c.On("ImagePullBlocking", e.Context, "test:latest", options). 291 Return(os.ErrNotExist). 292 Once() 293 294 c.On("ImagePullBlocking", e.Context, "tagged:tag", options). 295 Return(os.ErrNotExist). 296 Once() 297 298 c.On("ImagePullBlocking", e.Context, validSHA, options). 299 Return(os.ErrNotExist). 300 Once() 301 302 image, err := e.pullDockerImage("test", nil) 303 assert.Error(t, err) 304 assert.Nil(t, image) 305 306 image, err = e.pullDockerImage("tagged:tag", nil) 307 assert.Error(t, err) 308 assert.Nil(t, image) 309 310 image, err = e.pullDockerImage(validSHA, nil) 311 assert.Error(t, err) 312 assert.Nil(t, image) 313 } 314 315 func TestDockerForExistingImage(t *testing.T) { 316 var c docker_helpers.MockClient 317 defer c.AssertExpectations(t) 318 319 e := executor{client: &c} 320 e.Context = context.Background() 321 options := buildImagePullOptions(e, "existing") 322 323 c.On("ImagePullBlocking", e.Context, "existing:latest", options). 324 Return(nil). 325 Once() 326 327 c.On("ImageInspectWithRaw", e.Context, "existing"). 328 Return(types.ImageInspect{ID: "image-id"}, nil, nil). 329 Once() 330 331 image, err := e.pullDockerImage("existing", nil) 332 assert.NoError(t, err) 333 assert.NotNil(t, image) 334 } 335 336 func TestHelperImageWithVariable(t *testing.T) { 337 c := new(docker_helpers.MockClient) 338 defer c.AssertExpectations(t) 339 340 c.On("ImageInspectWithRaw", mock.Anything, "gitlab/gitlab-runner:HEAD"). 341 Return(types.ImageInspect{}, nil, errors.New("not found")). 342 Once() 343 c.On("ImagePullBlocking", mock.Anything, "gitlab/gitlab-runner:HEAD", mock.Anything). 344 Return(nil). 345 Once() 346 c.On("ImageInspectWithRaw", mock.Anything, "gitlab/gitlab-runner:HEAD"). 347 Return(types.ImageInspect{ID: "helper-image"}, nil, nil). 348 Once() 349 350 e := executor{ 351 AbstractExecutor: executors.AbstractExecutor{ 352 Build: &common.Build{ 353 JobResponse: common.JobResponse{}, 354 }, 355 }, 356 client: c, 357 } 358 359 e.Config = common.RunnerConfig{} 360 e.Config.Docker = &common.DockerConfig{ 361 HelperImage: "gitlab/gitlab-runner:${CI_RUNNER_REVISION}", 362 } 363 364 img, err := e.getPrebuiltImage() 365 assert.NoError(t, err) 366 require.NotNil(t, img) 367 assert.Equal(t, "helper-image", img.ID) 368 } 369 370 func (e *executor) setPolicyMode(pullPolicy common.DockerPullPolicy) { 371 e.Config = common.RunnerConfig{ 372 RunnerSettings: common.RunnerSettings{ 373 Docker: &common.DockerConfig{ 374 PullPolicy: pullPolicy, 375 }, 376 }, 377 } 378 } 379 380 func TestDockerGetImageById(t *testing.T) { 381 var c docker_helpers.MockClient 382 defer c.AssertExpectations(t) 383 384 // Use default policy 385 e := executor{client: &c} 386 e.Context = context.Background() 387 e.setPolicyMode("") 388 389 c.On("ImageInspectWithRaw", e.Context, "ID"). 390 Return(types.ImageInspect{ID: "ID"}, nil, nil). 391 Once() 392 393 image, err := e.getDockerImage("ID") 394 assert.NoError(t, err) 395 assert.NotNil(t, image) 396 assert.Equal(t, "ID", image.ID) 397 } 398 399 func TestDockerUnknownPolicyMode(t *testing.T) { 400 var c docker_helpers.MockClient 401 defer c.AssertExpectations(t) 402 403 e := executor{client: &c} 404 e.Context = context.Background() 405 e.setPolicyMode("unknown") 406 407 _, err := e.getDockerImage("not-existing") 408 assert.Error(t, err) 409 } 410 411 func TestDockerPolicyModeNever(t *testing.T) { 412 var c docker_helpers.MockClient 413 defer c.AssertExpectations(t) 414 415 e := executor{client: &c} 416 e.Context = context.Background() 417 e.setPolicyMode(common.PullPolicyNever) 418 419 c.On("ImageInspectWithRaw", e.Context, "existing"). 420 Return(types.ImageInspect{ID: "existing"}, nil, nil). 421 Once() 422 423 c.On("ImageInspectWithRaw", e.Context, "not-existing"). 424 Return(types.ImageInspect{}, nil, os.ErrNotExist). 425 Once() 426 427 image, err := e.getDockerImage("existing") 428 assert.NoError(t, err) 429 assert.Equal(t, "existing", image.ID) 430 431 _, err = e.getDockerImage("not-existing") 432 assert.Error(t, err) 433 } 434 435 func TestDockerPolicyModeIfNotPresentForExistingImage(t *testing.T) { 436 var c docker_helpers.MockClient 437 defer c.AssertExpectations(t) 438 439 e := executor{client: &c} 440 e.Context = context.Background() 441 e.setPolicyMode(common.PullPolicyIfNotPresent) 442 443 c.On("ImageInspectWithRaw", e.Context, "existing"). 444 Return(types.ImageInspect{ID: "image-id"}, nil, nil). 445 Once() 446 447 image, err := e.getDockerImage("existing") 448 assert.NoError(t, err) 449 assert.NotNil(t, image) 450 } 451 452 func TestDockerPolicyModeIfNotPresentForNotExistingImage(t *testing.T) { 453 var c docker_helpers.MockClient 454 defer c.AssertExpectations(t) 455 456 e := executor{client: &c} 457 e.Context = context.Background() 458 e.setPolicyMode(common.PullPolicyIfNotPresent) 459 460 c.On("ImageInspectWithRaw", e.Context, "not-existing"). 461 Return(types.ImageInspect{}, nil, os.ErrNotExist). 462 Once() 463 464 options := buildImagePullOptions(e, "not-existing") 465 c.On("ImagePullBlocking", e.Context, "not-existing:latest", options). 466 Return(nil). 467 Once() 468 469 c.On("ImageInspectWithRaw", e.Context, "not-existing"). 470 Return(types.ImageInspect{ID: "image-id"}, nil, nil). 471 Once() 472 473 image, err := e.getDockerImage("not-existing") 474 assert.NoError(t, err) 475 assert.NotNil(t, image) 476 477 c.On("ImageInspectWithRaw", e.Context, "not-existing"). 478 Return(types.ImageInspect{ID: "image-id"}, nil, nil). 479 Once() 480 481 // It shouldn't execute the pull for second time 482 image, err = e.getDockerImage("not-existing") 483 assert.NoError(t, err) 484 assert.NotNil(t, image) 485 } 486 487 func TestDockerPolicyModeAlwaysForExistingImage(t *testing.T) { 488 var c docker_helpers.MockClient 489 defer c.AssertExpectations(t) 490 491 e := executor{client: &c} 492 e.Context = context.Background() 493 e.setPolicyMode(common.PullPolicyAlways) 494 495 c.On("ImageInspectWithRaw", e.Context, "existing"). 496 Return(types.ImageInspect{ID: "image-id"}, nil, nil). 497 Once() 498 499 options := buildImagePullOptions(e, "existing:latest") 500 c.On("ImagePullBlocking", e.Context, "existing:latest", options). 501 Return(nil). 502 Once() 503 504 c.On("ImageInspectWithRaw", e.Context, "existing"). 505 Return(types.ImageInspect{ID: "image-id"}, nil, nil). 506 Once() 507 508 image, err := e.getDockerImage("existing") 509 assert.NoError(t, err) 510 assert.NotNil(t, image) 511 } 512 513 func TestDockerPolicyModeAlwaysForLocalOnlyImage(t *testing.T) { 514 var c docker_helpers.MockClient 515 defer c.AssertExpectations(t) 516 517 e := executor{client: &c} 518 e.Context = context.Background() 519 e.setPolicyMode(common.PullPolicyAlways) 520 521 c.On("ImageInspectWithRaw", e.Context, "existing"). 522 Return(types.ImageInspect{ID: "image-id"}, nil, nil). 523 Once() 524 525 options := buildImagePullOptions(e, "existing:lastest") 526 c.On("ImagePullBlocking", e.Context, "existing:latest", options). 527 Return(fmt.Errorf("not found")). 528 Once() 529 530 image, err := e.getDockerImage("existing") 531 assert.Error(t, err) 532 assert.Nil(t, image) 533 } 534 535 func TestDockerGetExistingDockerImageIfPullFails(t *testing.T) { 536 var c docker_helpers.MockClient 537 defer c.AssertExpectations(t) 538 539 e := executor{client: &c} 540 e.Context = context.Background() 541 e.setPolicyMode(common.PullPolicyAlways) 542 543 c.On("ImageInspectWithRaw", e.Context, "to-pull"). 544 Return(types.ImageInspect{ID: "image-id"}, nil, nil). 545 Once() 546 547 options := buildImagePullOptions(e, "to-pull") 548 c.On("ImagePullBlocking", e.Context, "to-pull:latest", options). 549 Return(os.ErrNotExist). 550 Once() 551 552 image, err := e.getDockerImage("to-pull") 553 assert.Error(t, err) 554 assert.Nil(t, image, "Forces to authorize pulling") 555 556 c.On("ImageInspectWithRaw", e.Context, "not-existing"). 557 Return(types.ImageInspect{}, nil, os.ErrNotExist). 558 Once() 559 560 c.On("ImagePullBlocking", e.Context, "not-existing:latest", options). 561 Return(os.ErrNotExist). 562 Once() 563 564 image, err = e.getDockerImage("not-existing") 565 assert.Error(t, err) 566 assert.Nil(t, image, "No existing image") 567 } 568 569 func TestPrepareBuildsDir(t *testing.T) { 570 tests := map[string]struct { 571 parser parser.Parser 572 rootDir string 573 volumes []string 574 expectedSharedBuildsDir bool 575 expectedError string 576 }{ 577 "rootDir mounted as host based volume": { 578 parser: parser.NewLinuxParser(), 579 rootDir: "/build", 580 volumes: []string{"/build:/build"}, 581 expectedSharedBuildsDir: true, 582 }, 583 "rootDir mounted as container based volume": { 584 parser: parser.NewLinuxParser(), 585 rootDir: "/build", 586 volumes: []string{"/build"}, 587 expectedSharedBuildsDir: false, 588 }, 589 "rootDir not mounted as volume": { 590 parser: parser.NewLinuxParser(), 591 rootDir: "/build", 592 volumes: []string{"/folder:/folder"}, 593 expectedSharedBuildsDir: false, 594 }, 595 "rootDir's parent mounted as volume": { 596 parser: parser.NewLinuxParser(), 597 rootDir: "/build/other/directory", 598 volumes: []string{"/build/:/build"}, 599 expectedSharedBuildsDir: true, 600 }, 601 "rootDir is not an absolute path": { 602 parser: parser.NewLinuxParser(), 603 rootDir: "builds", 604 expectedError: "build directory needs to be an absolute path", 605 }, 606 "rootDir is /": { 607 parser: parser.NewLinuxParser(), 608 rootDir: "/", 609 expectedError: "build directory needs to be a non-root path", 610 }, 611 "error on volume parsing": { 612 parser: parser.NewLinuxParser(), 613 rootDir: "/build", 614 volumes: []string{""}, 615 expectedError: "invalid volume specification", 616 }, 617 "error on volume parser creation": { 618 expectedError: `missing volume parser`, 619 }, 620 } 621 622 for testName, test := range tests { 623 t.Run(testName, func(t *testing.T) { 624 c := common.RunnerConfig{ 625 RunnerSettings: common.RunnerSettings{ 626 BuildsDir: test.rootDir, 627 Docker: &common.DockerConfig{ 628 Volumes: test.volumes, 629 }, 630 }, 631 } 632 633 options := common.ExecutorPrepareOptions{ 634 Config: &c, 635 } 636 637 e := &executor{ 638 AbstractExecutor: executors.AbstractExecutor{ 639 Config: c, 640 }, 641 volumeParser: test.parser, 642 } 643 644 err := e.prepareBuildsDir(options) 645 if test.expectedError != "" { 646 assert.Error(t, err) 647 assert.Contains(t, err.Error(), test.expectedError) 648 return 649 } 650 651 assert.NoError(t, err) 652 assert.Equal(t, test.expectedSharedBuildsDir, e.SharedBuildsDir) 653 }) 654 } 655 } 656 657 type volumesTestCase struct { 658 volumes []string 659 buildsDir string 660 gitStrategy string 661 adjustConfiguration func(e *executor) 662 volumesManagerAssertions func(*volumes.MockManager) 663 clientAssertions func(*docker_helpers.MockClient) 664 createVolumeManager bool 665 expectedError error 666 } 667 668 var volumesTestsDefaultBuildsDir = "/default-builds-dir" 669 var volumesTestsDefaultCacheDir = "/default-cache-dir" 670 671 func getExecutorForVolumesTests(t *testing.T, test volumesTestCase) (*executor, func()) { 672 clientMock := new(docker_helpers.MockClient) 673 volumesManagerMock := new(volumes.MockManager) 674 675 oldCreateVolumesManager := createVolumesManager 676 closureFn := func() { 677 createVolumesManager = oldCreateVolumesManager 678 679 volumesManagerMock.AssertExpectations(t) 680 clientMock.AssertExpectations(t) 681 } 682 683 createVolumesManager = func(_ *executor) (volumes.Manager, error) { 684 return volumesManagerMock, nil 685 } 686 687 if test.volumesManagerAssertions != nil { 688 test.volumesManagerAssertions(volumesManagerMock) 689 } 690 691 if test.clientAssertions != nil { 692 test.clientAssertions(clientMock) 693 } 694 695 c := common.RunnerConfig{ 696 RunnerCredentials: common.RunnerCredentials{ 697 Token: "abcdef1234567890", 698 }, 699 RunnerSettings: common.RunnerSettings{ 700 BuildsDir: test.buildsDir, 701 Docker: &common.DockerConfig{ 702 Volumes: test.volumes, 703 }, 704 }, 705 } 706 707 e := &executor{ 708 AbstractExecutor: executors.AbstractExecutor{ 709 Build: &common.Build{ 710 ProjectRunnerID: 0, 711 Runner: &c, 712 JobResponse: common.JobResponse{ 713 JobInfo: common.JobInfo{ 714 ProjectID: 0, 715 }, 716 GitInfo: common.GitInfo{ 717 RepoURL: "https://gitlab.example.com/group/project.git", 718 }, 719 }, 720 }, 721 Config: c, 722 ExecutorOptions: executors.ExecutorOptions{ 723 DefaultBuildsDir: volumesTestsDefaultBuildsDir, 724 DefaultCacheDir: volumesTestsDefaultCacheDir, 725 }, 726 }, 727 client: clientMock, 728 info: types.Info{ 729 OSType: helperimage.OSTypeLinux, 730 }, 731 } 732 733 e.Build.Variables = append(e.Build.Variables, common.JobVariable{ 734 Key: "GIT_STRATEGY", 735 Value: test.gitStrategy, 736 }) 737 738 if test.adjustConfiguration != nil { 739 test.adjustConfiguration(e) 740 } 741 742 err := e.Build.StartBuild( 743 e.RootDir(), 744 e.CacheDir(), 745 e.CustomBuildEnabled(), 746 e.SharedBuildsDir, 747 ) 748 require.NoError(t, err) 749 750 if test.createVolumeManager { 751 err = e.createVolumesManager() 752 require.NoError(t, err) 753 } 754 755 return e, closureFn 756 } 757 758 func TestCreateVolumes(t *testing.T) { 759 tests := map[string]volumesTestCase{ 760 "volumes manager not created": { 761 expectedError: errVolumesManagerUndefined, 762 }, 763 "no volumes defined, empty buildsDir, clone strategy, no errors": { 764 gitStrategy: "clone", 765 createVolumeManager: true, 766 }, 767 "no volumes defined, defined buildsDir, clone strategy, no errors": { 768 buildsDir: "/builds", 769 gitStrategy: "clone", 770 createVolumeManager: true, 771 }, 772 "no volumes defined, defined buildsDir, fetch strategy, no errors": { 773 buildsDir: "/builds", 774 gitStrategy: "fetch", 775 createVolumeManager: true, 776 }, 777 "volumes defined, empty buildsDir, clone strategy, no errors on user volume": { 778 volumes: []string{"/volume"}, 779 gitStrategy: "clone", 780 volumesManagerAssertions: func(vm *volumes.MockManager) { 781 vm.On("Create", "/volume"). 782 Return(nil). 783 Once() 784 }, 785 createVolumeManager: true, 786 }, 787 "volumes defined, empty buildsDir, clone strategy, cache containers disabled error on user volume": { 788 volumes: []string{"/volume"}, 789 gitStrategy: "clone", 790 volumesManagerAssertions: func(vm *volumes.MockManager) { 791 vm.On("Create", "/volume"). 792 Return(volumes.ErrCacheVolumesDisabled). 793 Once() 794 }, 795 createVolumeManager: true, 796 }, 797 "volumes defined, empty buildsDir, clone strategy, duplicated error on user volume": { 798 volumes: []string{"/volume"}, 799 gitStrategy: "clone", 800 volumesManagerAssertions: func(vm *volumes.MockManager) { 801 vm.On("Create", "/volume"). 802 Return(volumes.NewErrVolumeAlreadyDefined("/volume")). 803 Once() 804 }, 805 createVolumeManager: true, 806 expectedError: volumes.NewErrVolumeAlreadyDefined("/volume"), 807 }, 808 "volumes defined, empty buildsDir, clone strategy, other error on user volume": { 809 volumes: []string{"/volume"}, 810 gitStrategy: "clone", 811 volumesManagerAssertions: func(vm *volumes.MockManager) { 812 vm.On("Create", "/volume"). 813 Return(errors.New("test-error")). 814 Once() 815 }, 816 createVolumeManager: true, 817 expectedError: errors.New("test-error"), 818 }, 819 } 820 821 for testName, test := range tests { 822 t.Run(testName, func(t *testing.T) { 823 e, closureFn := getExecutorForVolumesTests(t, test) 824 defer closureFn() 825 826 err := e.createVolumes() 827 assert.Equal(t, test.expectedError, err) 828 }) 829 } 830 } 831 832 func TestCreateBuildVolume(t *testing.T) { 833 tests := map[string]volumesTestCase{ 834 "volumes manager not created": { 835 expectedError: errVolumesManagerUndefined, 836 }, 837 "git strategy clone, empty buildsDir, no error": { 838 gitStrategy: "clone", 839 volumesManagerAssertions: func(vm *volumes.MockManager) { 840 vm.On("CreateTemporary", volumesTestsDefaultBuildsDir). 841 Return(nil). 842 Once() 843 }, 844 createVolumeManager: true, 845 }, 846 "git strategy clone, empty buildsDir, duplicated error": { 847 gitStrategy: "clone", 848 volumesManagerAssertions: func(vm *volumes.MockManager) { 849 vm.On("CreateTemporary", volumesTestsDefaultBuildsDir). 850 Return(volumes.NewErrVolumeAlreadyDefined(volumesTestsDefaultBuildsDir)). 851 Once() 852 }, 853 createVolumeManager: true, 854 }, 855 "git strategy clone, empty buildsDir, other error": { 856 gitStrategy: "clone", 857 volumesManagerAssertions: func(vm *volumes.MockManager) { 858 vm.On("CreateTemporary", volumesTestsDefaultBuildsDir). 859 Return(errors.New("test-error")). 860 Once() 861 }, 862 createVolumeManager: true, 863 expectedError: errors.New("test-error"), 864 }, 865 "git strategy clone, non-empty buildsDir, no error": { 866 gitStrategy: "clone", 867 buildsDir: "/builds", 868 volumesManagerAssertions: func(vm *volumes.MockManager) { 869 vm.On("CreateTemporary", "/builds"). 870 Return(nil). 871 Once() 872 }, 873 createVolumeManager: true, 874 }, 875 "git strategy clone, non-empty buildsDir, duplicated error": { 876 gitStrategy: "clone", 877 buildsDir: "/builds", 878 volumesManagerAssertions: func(vm *volumes.MockManager) { 879 vm.On("CreateTemporary", "/builds"). 880 Return(volumes.NewErrVolumeAlreadyDefined("/builds")). 881 Once() 882 }, 883 createVolumeManager: true, 884 }, 885 "git strategy clone, non-empty buildsDir, other error": { 886 gitStrategy: "clone", 887 buildsDir: "/builds", 888 volumesManagerAssertions: func(vm *volumes.MockManager) { 889 vm.On("CreateTemporary", "/builds"). 890 Return(errors.New("test-error")). 891 Once() 892 }, 893 createVolumeManager: true, 894 expectedError: errors.New("test-error"), 895 }, 896 "git strategy fetch, empty buildsDir, no error": { 897 gitStrategy: "fetch", 898 volumesManagerAssertions: func(vm *volumes.MockManager) { 899 vm.On("Create", volumesTestsDefaultBuildsDir). 900 Return(nil). 901 Once() 902 }, 903 createVolumeManager: true, 904 }, 905 "git strategy fetch, empty buildsDir, duplicated error": { 906 gitStrategy: "fetch", 907 volumesManagerAssertions: func(vm *volumes.MockManager) { 908 vm.On("Create", volumesTestsDefaultBuildsDir). 909 Return(volumes.NewErrVolumeAlreadyDefined(volumesTestsDefaultBuildsDir)). 910 Once() 911 }, 912 createVolumeManager: true, 913 }, 914 "git strategy fetch, empty buildsDir, other error": { 915 gitStrategy: "fetch", 916 volumesManagerAssertions: func(vm *volumes.MockManager) { 917 vm.On("Create", volumesTestsDefaultBuildsDir). 918 Return(errors.New("test-error")). 919 Once() 920 }, 921 createVolumeManager: true, 922 expectedError: errors.New("test-error"), 923 }, 924 "git strategy fetch, non-empty buildsDir, no error": { 925 gitStrategy: "fetch", 926 buildsDir: "/builds", 927 volumesManagerAssertions: func(vm *volumes.MockManager) { 928 vm.On("Create", "/builds"). 929 Return(nil). 930 Once() 931 }, 932 createVolumeManager: true, 933 }, 934 "git strategy fetch, non-empty buildsDir, duplicated error": { 935 gitStrategy: "fetch", 936 buildsDir: "/builds", 937 volumesManagerAssertions: func(vm *volumes.MockManager) { 938 vm.On("Create", "/builds"). 939 Return(volumes.NewErrVolumeAlreadyDefined("/builds")). 940 Once() 941 }, 942 createVolumeManager: true, 943 }, 944 "git strategy fetch, non-empty buildsDir, other error": { 945 gitStrategy: "fetch", 946 buildsDir: "/builds", 947 volumesManagerAssertions: func(vm *volumes.MockManager) { 948 vm.On("Create", "/builds"). 949 Return(errors.New("test-error")). 950 Once() 951 }, 952 createVolumeManager: true, 953 expectedError: errors.New("test-error"), 954 }, 955 "git strategy fetch, non-empty buildsDir, cache volumes disabled": { 956 gitStrategy: "fetch", 957 buildsDir: "/builds", 958 volumesManagerAssertions: func(vm *volumes.MockManager) { 959 vm.On("Create", "/builds"). 960 Return(volumes.ErrCacheVolumesDisabled). 961 Once() 962 vm.On("CreateTemporary", "/builds"). 963 Return(nil). 964 Once() 965 }, 966 createVolumeManager: true, 967 }, 968 "git strategy fetch, non-empty buildsDir, cache volumes disabled, duplicated error": { 969 gitStrategy: "fetch", 970 buildsDir: "/builds", 971 volumesManagerAssertions: func(vm *volumes.MockManager) { 972 vm.On("Create", "/builds"). 973 Return(volumes.ErrCacheVolumesDisabled). 974 Once() 975 vm.On("CreateTemporary", "/builds"). 976 Return(volumes.NewErrVolumeAlreadyDefined("/builds")). 977 Once() 978 }, 979 createVolumeManager: true, 980 }, 981 "git strategy fetch, non-empty buildsDir, no error, legacy builds dir": { 982 // TODO: Remove in 12.3 983 gitStrategy: "fetch", 984 buildsDir: "/builds", 985 adjustConfiguration: func(e *executor) { 986 e.Build.Variables = append(e.Build.Variables, common.JobVariable{ 987 Key: featureflags.UseLegacyBuildsDirForDocker, 988 Value: "true", 989 }) 990 }, 991 volumesManagerAssertions: func(vm *volumes.MockManager) { 992 vm.On("Create", "/builds/group"). 993 Return(nil). 994 Once() 995 }, 996 createVolumeManager: true, 997 }, 998 "git strategy clone, non-empty buildsDir, no error, legacy builds dir": { 999 // TODO: Remove in 12.3 1000 gitStrategy: "clone", 1001 buildsDir: "/builds", 1002 adjustConfiguration: func(e *executor) { 1003 e.Build.Variables = append(e.Build.Variables, common.JobVariable{ 1004 Key: featureflags.UseLegacyBuildsDirForDocker, 1005 Value: "true", 1006 }) 1007 }, 1008 volumesManagerAssertions: func(vm *volumes.MockManager) { 1009 vm.On("CreateTemporary", "/builds/group"). 1010 Return(nil). 1011 Once() 1012 }, 1013 createVolumeManager: true, 1014 }, 1015 } 1016 1017 for testName, test := range tests { 1018 t.Run(testName, func(t *testing.T) { 1019 e, closureFn := getExecutorForVolumesTests(t, test) 1020 defer closureFn() 1021 1022 err := e.createBuildVolume() 1023 assert.Equal(t, test.expectedError, err) 1024 }) 1025 } 1026 } 1027 1028 func TestCreateDependencies(t *testing.T) { 1029 testError := errors.New("test-error") 1030 1031 tests := map[string]struct { 1032 legacyVolumesMountingOrder string 1033 expectedServiceVolumes []string 1034 }{ 1035 "UseLegacyVolumesMountingOrder is false": { 1036 legacyVolumesMountingOrder: "false", 1037 expectedServiceVolumes: []string{"/volume", "/builds"}, 1038 }, 1039 // TODO: Remove in 12.6 1040 "UseLegacyVolumesMountingOrder is true": { 1041 legacyVolumesMountingOrder: "true", 1042 expectedServiceVolumes: []string{"/builds"}, 1043 }, 1044 } 1045 1046 for testName, test := range tests { 1047 t.Run(testName, func(t *testing.T) { 1048 testCase := volumesTestCase{ 1049 buildsDir: "/builds", 1050 volumes: []string{"/volume"}, 1051 adjustConfiguration: func(e *executor) { 1052 e.Build.Services = append(e.Build.Services, common.Image{ 1053 Name: "alpine:latest", 1054 }) 1055 1056 e.Build.Variables = append(e.Build.Variables, common.JobVariable{ 1057 Key: featureflags.UseLegacyVolumesMountingOrder, 1058 Value: test.legacyVolumesMountingOrder, 1059 }) 1060 }, 1061 volumesManagerAssertions: func(vm *volumes.MockManager) { 1062 binds := make([]string, 0) 1063 1064 vm.On("CreateTemporary", "/builds"). 1065 Return(nil). 1066 Run(func(args mock.Arguments) { 1067 binds = append(binds, args.Get(0).(string)) 1068 }). 1069 Once() 1070 vm.On("Create", "/volume"). 1071 Return(nil). 1072 Run(func(args mock.Arguments) { 1073 binds = append(binds, args.Get(0).(string)) 1074 }). 1075 Maybe() // In the FF enabled case this assertion will be not met because of error during service starts 1076 vm.On("Binds"). 1077 Return(func() []string { 1078 return binds 1079 }). 1080 Once() 1081 vm.On("ContainerIDs"). 1082 Return(nil). 1083 Once() 1084 }, 1085 clientAssertions: func(c *docker_helpers.MockClient) { 1086 hostConfigMatcher := mock.MatchedBy(func(conf *container.HostConfig) bool { 1087 return assert.Equal(t, test.expectedServiceVolumes, conf.Binds) 1088 }) 1089 1090 c.On("ImageInspectWithRaw", mock.Anything, "alpine:latest"). 1091 Return(types.ImageInspect{}, nil, nil). 1092 Once() 1093 c.On("NetworkList", mock.Anything, mock.Anything). 1094 Return(nil, nil). 1095 Once() 1096 c.On("ContainerRemove", mock.Anything, "runner-abcdef12-project-0-concurrent-0-alpine-0", mock.Anything). 1097 Return(nil). 1098 Once() 1099 c.On("ContainerCreate", mock.Anything, mock.Anything, hostConfigMatcher, mock.Anything, "runner-abcdef12-project-0-concurrent-0-alpine-0"). 1100 Return(container.ContainerCreateCreatedBody{ID: "container-ID"}, nil). 1101 Once() 1102 c.On("ContainerStart", mock.Anything, "container-ID", mock.Anything). 1103 Return(testError). 1104 Once() 1105 }, 1106 } 1107 1108 e, closureFn := getExecutorForVolumesTests(t, testCase) 1109 defer closureFn() 1110 1111 err := e.createDependencies() 1112 assert.Equal(t, testError, err) 1113 }) 1114 } 1115 } 1116 1117 var testFileAuthConfigs = `{"auths":{"https://registry.domain.tld:5005/v1/":{"auth":"aW52YWxpZF91c2VyOmludmFsaWRfcGFzc3dvcmQ="},"registry2.domain.tld:5005":{"auth":"dGVzdF91c2VyOnRlc3RfcGFzc3dvcmQ="}}}` 1118 var testVariableAuthConfigs = `{"auths":{"https://registry.domain.tld:5005/v1/":{"auth":"dGVzdF91c2VyOnRlc3RfcGFzc3dvcmQ="}}}` 1119 1120 func getAuthConfigTestExecutor(t *testing.T, precreateConfigFile bool) executor { 1121 tempHomeDir, err := ioutil.TempDir("", "docker-auth-configs-test") 1122 require.NoError(t, err) 1123 1124 if precreateConfigFile { 1125 dockerConfigFile := path.Join(tempHomeDir, ".dockercfg") 1126 err = ioutil.WriteFile(dockerConfigFile, []byte(testFileAuthConfigs), 0600) 1127 require.NoError(t, err) 1128 docker_helpers.HomeDirectory = tempHomeDir 1129 } else { 1130 docker_helpers.HomeDirectory = "" 1131 } 1132 1133 e := executor{} 1134 e.Build = &common.Build{ 1135 Runner: &common.RunnerConfig{}, 1136 } 1137 1138 e.Build.Token = "abcd123456" 1139 1140 e.Config = common.RunnerConfig{} 1141 e.Config.Docker = &common.DockerConfig{ 1142 PullPolicy: common.PullPolicyAlways, 1143 } 1144 1145 return e 1146 } 1147 1148 func addGitLabRegistryCredentials(e *executor) { 1149 e.Build.Credentials = []common.Credentials{ 1150 { 1151 Type: "registry", 1152 URL: "registry.gitlab.tld:1234", 1153 Username: "gitlab-ci-token", 1154 Password: e.Build.Token, 1155 }, 1156 } 1157 } 1158 1159 func addRemoteVariableCredentials(e *executor) { 1160 e.Build.Variables = common.JobVariables{ 1161 common.JobVariable{ 1162 Key: "DOCKER_AUTH_CONFIG", 1163 Value: testVariableAuthConfigs, 1164 }, 1165 } 1166 } 1167 1168 func addLocalVariableCredentials(e *executor) { 1169 e.Build.Runner.Environment = []string{ 1170 "DOCKER_AUTH_CONFIG=" + testVariableAuthConfigs, 1171 } 1172 } 1173 1174 func assertEmptyCredentials(t *testing.T, ac *types.AuthConfig, messageElements ...string) { 1175 if ac != nil { 1176 assert.Empty(t, ac.ServerAddress, "ServerAddress for %v", messageElements) 1177 assert.Empty(t, ac.Username, "Username for %v", messageElements) 1178 assert.Empty(t, ac.Password, "Password for %v", messageElements) 1179 } 1180 } 1181 1182 func assertCredentials(t *testing.T, serverAddress, username, password string, ac *types.AuthConfig, messageElements ...string) { 1183 assert.Equal(t, serverAddress, ac.ServerAddress, "ServerAddress for %v", messageElements) 1184 assert.Equal(t, username, ac.Username, "Username for %v", messageElements) 1185 assert.Equal(t, password, ac.Password, "Password for %v", messageElements) 1186 } 1187 1188 func getTestAuthConfig(t *testing.T, e executor, imageName string) *types.AuthConfig { 1189 ac := e.getAuthConfig(imageName) 1190 1191 return ac 1192 } 1193 1194 func testVariableAuthConfig(t *testing.T, e executor) { 1195 t.Run("withoutGitLabRegistry", func(t *testing.T) { 1196 ac := getTestAuthConfig(t, e, "registry.domain.tld:5005/image/name:version") 1197 assertCredentials(t, "https://registry.domain.tld:5005/v1/", "test_user", "test_password", ac, "registry.domain.tld:5005/image/name:version") 1198 1199 ac = getTestAuthConfig(t, e, "registry2.domain.tld:5005/image/name:version") 1200 assertCredentials(t, "registry2.domain.tld:5005", "test_user", "test_password", ac, "registry2.domain.tld:5005/image/name:version") 1201 1202 ac = getTestAuthConfig(t, e, "registry.gitlab.tld:1234/image/name:version") 1203 assertEmptyCredentials(t, ac, "registry.gitlab.tld:1234") 1204 }) 1205 1206 t.Run("withGitLabRegistry", func(t *testing.T) { 1207 addGitLabRegistryCredentials(&e) 1208 1209 ac := getTestAuthConfig(t, e, "registry.domain.tld:5005/image/name:version") 1210 assertCredentials(t, "https://registry.domain.tld:5005/v1/", "test_user", "test_password", ac, "registry.domain.tld:5005/image/name:version") 1211 1212 ac = getTestAuthConfig(t, e, "registry2.domain.tld:5005/image/name:version") 1213 assertCredentials(t, "registry2.domain.tld:5005", "test_user", "test_password", ac, "registry2.domain.tld:5005/image/name:version") 1214 1215 ac = getTestAuthConfig(t, e, "registry.gitlab.tld:1234/image/name:version") 1216 assertCredentials(t, "registry.gitlab.tld:1234", "gitlab-ci-token", "abcd123456", ac, "registry.gitlab.tld:1234") 1217 }) 1218 } 1219 1220 func TestGetRemoteVariableAuthConfig(t *testing.T) { 1221 e := getAuthConfigTestExecutor(t, true) 1222 addRemoteVariableCredentials(&e) 1223 1224 testVariableAuthConfig(t, e) 1225 } 1226 1227 func TestGetLocalVariableAuthConfig(t *testing.T) { 1228 e := getAuthConfigTestExecutor(t, true) 1229 addLocalVariableCredentials(&e) 1230 1231 testVariableAuthConfig(t, e) 1232 } 1233 1234 func TestGetDefaultAuthConfig(t *testing.T) { 1235 t.Run("withoutGitLabRegistry", func(t *testing.T) { 1236 e := getAuthConfigTestExecutor(t, false) 1237 1238 ac := getTestAuthConfig(t, e, "docker:dind") 1239 assertEmptyCredentials(t, ac, "docker:dind") 1240 1241 ac = getTestAuthConfig(t, e, "registry.gitlab.tld:1234/image/name:version") 1242 assertEmptyCredentials(t, ac, "registry.gitlab.tld:1234") 1243 1244 ac = getTestAuthConfig(t, e, "registry.domain.tld:5005/image/name:version") 1245 assertEmptyCredentials(t, ac, "registry.domain.tld:5005/image/name:version") 1246 }) 1247 1248 t.Run("withGitLabRegistry", func(t *testing.T) { 1249 e := getAuthConfigTestExecutor(t, false) 1250 addGitLabRegistryCredentials(&e) 1251 1252 ac := getTestAuthConfig(t, e, "docker:dind") 1253 assertEmptyCredentials(t, ac, "docker:dind") 1254 1255 ac = getTestAuthConfig(t, e, "registry.domain.tld:5005/image/name:version") 1256 assertEmptyCredentials(t, ac, "registry.domain.tld:5005/image/name:version") 1257 1258 ac = getTestAuthConfig(t, e, "registry.gitlab.tld:1234/image/name:version") 1259 assertCredentials(t, "registry.gitlab.tld:1234", "gitlab-ci-token", "abcd123456", ac, "registry.gitlab.tld:1234") 1260 }) 1261 } 1262 1263 func TestAuthConfigOverwritingOrder(t *testing.T) { 1264 testVariableAuthConfigs = `{"auths":{"registry.gitlab.tld:1234":{"auth":"ZnJvbV92YXJpYWJsZTpwYXNzd29yZA=="}}}` 1265 testFileAuthConfigs = `{"auths":{"registry.gitlab.tld:1234":{"auth":"ZnJvbV9maWxlOnBhc3N3b3Jk"}}}` 1266 1267 imageName := "registry.gitlab.tld:1234/image/name:latest" 1268 1269 t.Run("gitlabRegistryOnly", func(t *testing.T) { 1270 e := getAuthConfigTestExecutor(t, false) 1271 addGitLabRegistryCredentials(&e) 1272 1273 ac := getTestAuthConfig(t, e, imageName) 1274 assertCredentials(t, "registry.gitlab.tld:1234", "gitlab-ci-token", e.Build.Token, ac, imageName) 1275 }) 1276 1277 t.Run("withConfigFromRemoteVariable", func(t *testing.T) { 1278 e := getAuthConfigTestExecutor(t, false) 1279 addGitLabRegistryCredentials(&e) 1280 addRemoteVariableCredentials(&e) 1281 1282 ac := getTestAuthConfig(t, e, imageName) 1283 assertCredentials(t, "registry.gitlab.tld:1234", "from_variable", "password", ac, imageName) 1284 }) 1285 1286 t.Run("withConfigFromLocalVariable", func(t *testing.T) { 1287 e := getAuthConfigTestExecutor(t, false) 1288 addGitLabRegistryCredentials(&e) 1289 addLocalVariableCredentials(&e) 1290 1291 ac := getTestAuthConfig(t, e, imageName) 1292 assertCredentials(t, "registry.gitlab.tld:1234", "from_variable", "password", ac, imageName) 1293 }) 1294 1295 t.Run("withConfigFromFile", func(t *testing.T) { 1296 e := getAuthConfigTestExecutor(t, true) 1297 addGitLabRegistryCredentials(&e) 1298 1299 ac := getTestAuthConfig(t, e, imageName) 1300 assertCredentials(t, "registry.gitlab.tld:1234", "from_file", "password", ac, imageName) 1301 }) 1302 1303 t.Run("withConfigFromVariableAndFromFile", func(t *testing.T) { 1304 e := getAuthConfigTestExecutor(t, true) 1305 addGitLabRegistryCredentials(&e) 1306 addRemoteVariableCredentials(&e) 1307 1308 ac := getTestAuthConfig(t, e, imageName) 1309 assertCredentials(t, "registry.gitlab.tld:1234", "from_variable", "password", ac, imageName) 1310 }) 1311 1312 t.Run("withConfigFromLocalAndRemoteVariable", func(t *testing.T) { 1313 e := getAuthConfigTestExecutor(t, true) 1314 addGitLabRegistryCredentials(&e) 1315 addRemoteVariableCredentials(&e) 1316 testVariableAuthConfigs = `{"auths":{"registry.gitlab.tld:1234":{"auth":"ZnJvbV9sb2NhbF92YXJpYWJsZTpwYXNzd29yZA=="}}}` 1317 addLocalVariableCredentials(&e) 1318 1319 ac := getTestAuthConfig(t, e, imageName) 1320 assertCredentials(t, "registry.gitlab.tld:1234", "from_variable", "password", ac, imageName) 1321 }) 1322 } 1323 1324 func testGetDockerImage(t *testing.T, e executor, imageName string, setClientExpectations func(c *docker_helpers.MockClient, imageName string)) { 1325 t.Run("get:"+imageName, func(t *testing.T) { 1326 var c docker_helpers.MockClient 1327 defer c.AssertExpectations(t) 1328 1329 e.client = &c 1330 1331 setClientExpectations(&c, imageName) 1332 1333 image, err := e.getDockerImage(imageName) 1334 assert.NoError(t, err, "Should not generate error") 1335 assert.Equal(t, "this-image", image.ID, "Image ID") 1336 }) 1337 } 1338 1339 func testDeniesDockerImage(t *testing.T, e executor, imageName string, setClientExpectations func(c *docker_helpers.MockClient, imageName string)) { 1340 t.Run("deny:"+imageName, func(t *testing.T) { 1341 var c docker_helpers.MockClient 1342 defer c.AssertExpectations(t) 1343 1344 e.client = &c 1345 1346 setClientExpectations(&c, imageName) 1347 1348 _, err := e.getDockerImage(imageName) 1349 assert.Error(t, err, "Should generate error") 1350 }) 1351 } 1352 1353 func addFindsLocalImageExpectations(c *docker_helpers.MockClient, imageName string) { 1354 c.On("ImageInspectWithRaw", mock.Anything, imageName). 1355 Return(types.ImageInspect{ID: "this-image"}, nil, nil). 1356 Once() 1357 } 1358 1359 func addPullsRemoteImageExpectations(c *docker_helpers.MockClient, imageName string) { 1360 c.On("ImageInspectWithRaw", mock.Anything, imageName). 1361 Return(types.ImageInspect{ID: "not-this-image"}, nil, nil). 1362 Once() 1363 1364 c.On("ImagePullBlocking", mock.Anything, imageName, mock.AnythingOfType("types.ImagePullOptions")). 1365 Return(nil). 1366 Once() 1367 1368 c.On("ImageInspectWithRaw", mock.Anything, imageName). 1369 Return(types.ImageInspect{ID: "this-image"}, nil, nil). 1370 Once() 1371 } 1372 1373 func addDeniesPullExpectations(c *docker_helpers.MockClient, imageName string) { 1374 c.On("ImageInspectWithRaw", mock.Anything, imageName). 1375 Return(types.ImageInspect{ID: "image"}, nil, nil). 1376 Once() 1377 1378 c.On("ImagePullBlocking", mock.Anything, imageName, mock.AnythingOfType("types.ImagePullOptions")). 1379 Return(fmt.Errorf("deny pulling")). 1380 Once() 1381 } 1382 1383 func TestPullPolicyWhenAlwaysIsSet(t *testing.T) { 1384 remoteImage := "registry.domain.tld:5005/image/name:version" 1385 gitlabImage := "registry.gitlab.tld:1234/image/name:version" 1386 1387 e := getAuthConfigTestExecutor(t, false) 1388 e.Context = context.Background() 1389 e.Config.Docker.PullPolicy = common.PullPolicyAlways 1390 1391 testGetDockerImage(t, e, remoteImage, addPullsRemoteImageExpectations) 1392 testDeniesDockerImage(t, e, remoteImage, addDeniesPullExpectations) 1393 1394 testGetDockerImage(t, e, gitlabImage, addPullsRemoteImageExpectations) 1395 testDeniesDockerImage(t, e, gitlabImage, addDeniesPullExpectations) 1396 } 1397 1398 func TestPullPolicyWhenIfNotPresentIsSet(t *testing.T) { 1399 remoteImage := "registry.domain.tld:5005/image/name:version" 1400 gitlabImage := "registry.gitlab.tld:1234/image/name:version" 1401 1402 e := getAuthConfigTestExecutor(t, false) 1403 e.Context = context.Background() 1404 e.Config.Docker.PullPolicy = common.PullPolicyIfNotPresent 1405 1406 testGetDockerImage(t, e, remoteImage, addFindsLocalImageExpectations) 1407 testGetDockerImage(t, e, gitlabImage, addFindsLocalImageExpectations) 1408 } 1409 1410 func TestDockerWatchOn_1_12_4(t *testing.T) { 1411 if helpers.SkipIntegrationTests(t, "docker", "info") { 1412 return 1413 } 1414 1415 e := executor{ 1416 AbstractExecutor: executors.AbstractExecutor{ 1417 ExecutorOptions: executors.ExecutorOptions{ 1418 Metadata: map[string]string{ 1419 metadataOSType: osTypeLinux, 1420 }, 1421 }, 1422 }, 1423 volumeParser: parser.NewLinuxParser(), 1424 } 1425 e.Context = context.Background() 1426 e.Build = &common.Build{ 1427 Runner: &common.RunnerConfig{}, 1428 } 1429 e.Build.Token = "abcd123456" 1430 e.BuildShell = &common.ShellConfiguration{ 1431 Environment: []string{}, 1432 } 1433 1434 e.Config = common.RunnerConfig{} 1435 e.Config.Docker = &common.DockerConfig{ 1436 PullPolicy: common.PullPolicyIfNotPresent, 1437 } 1438 1439 output := bytes.NewBufferString("") 1440 e.Trace = &common.Trace{Writer: output} 1441 1442 err := e.connectDocker() 1443 require.NoError(t, err) 1444 1445 err = e.createVolumesManager() 1446 require.NoError(t, err) 1447 1448 container, err := e.createContainer("build", common.Image{Name: common.TestAlpineImage}, []string{"/bin/sh"}, []string{}) 1449 assert.NoError(t, err) 1450 assert.NotNil(t, container) 1451 1452 input := bytes.NewBufferString("echo 'script'") 1453 1454 finished := make(chan bool, 1) 1455 wg := &sync.WaitGroup{} 1456 wg.Add(1) // Avoid a race where assert.NoError() is called too late in the goroutine 1457 go func() { 1458 err = e.watchContainer(e.Context, container.ID, input) 1459 assert.NoError(t, err) 1460 finished <- true 1461 wg.Done() 1462 }() 1463 1464 select { 1465 case <-finished: 1466 assert.Equal(t, "script\n", output.String()) 1467 case <-time.After(15 * time.Second): 1468 t.Error("Container script not finished") 1469 } 1470 1471 err = e.removeContainer(e.Context, container.ID) 1472 assert.NoError(t, err) 1473 wg.Wait() 1474 } 1475 1476 type containerConfigExpectations func(*testing.T, *container.Config, *container.HostConfig) 1477 1478 type dockerConfigurationTestFakeDockerClient struct { 1479 docker_helpers.MockClient 1480 1481 cce containerConfigExpectations 1482 t *testing.T 1483 } 1484 1485 func (c *dockerConfigurationTestFakeDockerClient) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error) { 1486 c.cce(c.t, config, hostConfig) 1487 return container.ContainerCreateCreatedBody{ID: "abc"}, nil 1488 } 1489 1490 func prepareTestDockerConfiguration(t *testing.T, dockerConfig *common.DockerConfig, cce containerConfigExpectations) (*dockerConfigurationTestFakeDockerClient, *executor) { 1491 c := &dockerConfigurationTestFakeDockerClient{ 1492 cce: cce, 1493 t: t, 1494 } 1495 1496 e := &executor{} 1497 e.client = c 1498 e.volumeParser = parser.NewLinuxParser() 1499 e.info = types.Info{ 1500 OSType: helperimage.OSTypeLinux, 1501 Architecture: "amd64", 1502 } 1503 e.Config.Docker = dockerConfig 1504 e.Build = &common.Build{ 1505 Runner: &common.RunnerConfig{}, 1506 } 1507 e.Build.Token = "abcd123456" 1508 e.BuildShell = &common.ShellConfiguration{ 1509 Environment: []string{}, 1510 } 1511 var err error 1512 e.helperImageInfo, err = helperimage.Get(common.REVISION, helperimage.Config{ 1513 OSType: e.info.OSType, 1514 Architecture: e.info.Architecture, 1515 OperatingSystem: e.info.OperatingSystem, 1516 }) 1517 require.NoError(t, err) 1518 1519 c.On("ImageInspectWithRaw", mock.Anything, "gitlab/gitlab-runner-helper:x86_64-latest"). 1520 Return(types.ImageInspect{ID: "helper-image-id"}, nil, nil).Once() 1521 c.On("ImageInspectWithRaw", mock.Anything, "alpine"). 1522 Return(types.ImageInspect{ID: "123"}, []byte{}, nil).Twice() 1523 c.On("ImagePullBlocking", mock.Anything, "alpine:latest", mock.Anything). 1524 Return(nil).Once() 1525 c.On("NetworkList", mock.Anything, mock.Anything). 1526 Return([]types.NetworkResource{}, nil).Once() 1527 c.On("ContainerRemove", mock.Anything, mock.Anything, mock.Anything). 1528 Return(nil).Once() 1529 1530 return c, e 1531 } 1532 1533 func testDockerConfigurationWithJobContainer(t *testing.T, dockerConfig *common.DockerConfig, cce containerConfigExpectations) { 1534 c, e := prepareTestDockerConfiguration(t, dockerConfig, cce) 1535 defer c.AssertExpectations(t) 1536 1537 c.On("ContainerInspect", mock.Anything, "abc"). 1538 Return(types.ContainerJSON{}, nil).Once() 1539 1540 err := e.createVolumesManager() 1541 require.NoError(t, err) 1542 1543 _, err = e.createContainer("build", common.Image{Name: "alpine"}, []string{"/bin/sh"}, []string{}) 1544 assert.NoError(t, err, "Should create container without errors") 1545 } 1546 1547 func testDockerConfigurationWithServiceContainer(t *testing.T, dockerConfig *common.DockerConfig, cce containerConfigExpectations) { 1548 c, e := prepareTestDockerConfiguration(t, dockerConfig, cce) 1549 defer c.AssertExpectations(t) 1550 1551 c.On("ContainerStart", mock.Anything, "abc", mock.Anything). 1552 Return(nil).Once() 1553 1554 err := e.createVolumesManager() 1555 require.NoError(t, err) 1556 1557 _, err = e.createService(0, "build", "latest", "alpine", common.Image{Command: []string{"/bin/sh"}}) 1558 assert.NoError(t, err, "Should create service container without errors") 1559 } 1560 1561 func TestDockerMemorySetting(t *testing.T) { 1562 dockerConfig := &common.DockerConfig{ 1563 Memory: "42m", 1564 } 1565 1566 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1567 assert.Equal(t, int64(44040192), hostConfig.Memory) 1568 } 1569 1570 testDockerConfigurationWithJobContainer(t, dockerConfig, cce) 1571 } 1572 1573 func TestDockerMemorySwapSetting(t *testing.T) { 1574 dockerConfig := &common.DockerConfig{ 1575 MemorySwap: "2g", 1576 } 1577 1578 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1579 assert.Equal(t, int64(2147483648), hostConfig.MemorySwap) 1580 } 1581 1582 testDockerConfigurationWithJobContainer(t, dockerConfig, cce) 1583 } 1584 1585 func TestDockerMemoryReservationSetting(t *testing.T) { 1586 dockerConfig := &common.DockerConfig{ 1587 MemoryReservation: "64m", 1588 } 1589 1590 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1591 assert.Equal(t, int64(67108864), hostConfig.MemoryReservation) 1592 } 1593 1594 testDockerConfigurationWithJobContainer(t, dockerConfig, cce) 1595 } 1596 1597 func TestDockerCPUSSetting(t *testing.T) { 1598 examples := []struct { 1599 cpus string 1600 nanocpus int64 1601 }{ 1602 {"0.5", 500000000}, 1603 {"0.25", 250000000}, 1604 {"1/3", 333333333}, 1605 {"1/8", 125000000}, 1606 {"0.0001", 100000}, 1607 } 1608 1609 for _, example := range examples { 1610 t.Run(example.cpus, func(t *testing.T) { 1611 dockerConfig := &common.DockerConfig{ 1612 CPUS: example.cpus, 1613 } 1614 1615 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1616 assert.Equal(t, int64(example.nanocpus), hostConfig.NanoCPUs) 1617 } 1618 1619 testDockerConfigurationWithJobContainer(t, dockerConfig, cce) 1620 }) 1621 } 1622 } 1623 1624 func TestDockerCPUSetCPUsSetting(t *testing.T) { 1625 dockerConfig := &common.DockerConfig{ 1626 CPUSetCPUs: "1-3,5", 1627 } 1628 1629 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1630 assert.Equal(t, "1-3,5", hostConfig.CpusetCpus) 1631 } 1632 1633 testDockerConfigurationWithJobContainer(t, dockerConfig, cce) 1634 } 1635 1636 func TestDockerServicesTmpfsSetting(t *testing.T) { 1637 dockerConfig := &common.DockerConfig{ 1638 ServicesTmpfs: map[string]string{ 1639 "/tmpfs": "rw,noexec", 1640 }, 1641 } 1642 1643 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1644 require.NotEmpty(t, hostConfig.Tmpfs) 1645 } 1646 1647 testDockerConfigurationWithServiceContainer(t, dockerConfig, cce) 1648 } 1649 func TestDockerTmpfsSetting(t *testing.T) { 1650 dockerConfig := &common.DockerConfig{ 1651 Tmpfs: map[string]string{ 1652 "/tmpfs": "rw,noexec", 1653 }, 1654 } 1655 1656 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1657 require.NotEmpty(t, hostConfig.Tmpfs) 1658 } 1659 1660 testDockerConfigurationWithJobContainer(t, dockerConfig, cce) 1661 } 1662 1663 func TestDockerServicesDNSSetting(t *testing.T) { 1664 dockerConfig := &common.DockerConfig{ 1665 DNS: []string{"2001:db8::1", "192.0.2.1"}, 1666 } 1667 1668 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1669 require.Equal(t, dockerConfig.DNS, hostConfig.DNS) 1670 } 1671 1672 testDockerConfigurationWithServiceContainer(t, dockerConfig, cce) 1673 } 1674 1675 func TestDockerServicesDNSSearchSetting(t *testing.T) { 1676 dockerConfig := &common.DockerConfig{ 1677 DNSSearch: []string{"mydomain.example"}, 1678 } 1679 1680 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1681 require.Equal(t, dockerConfig.DNSSearch, hostConfig.DNSSearch) 1682 } 1683 1684 testDockerConfigurationWithServiceContainer(t, dockerConfig, cce) 1685 } 1686 1687 func TestDockerServicesExtraHostsSetting(t *testing.T) { 1688 dockerConfig := &common.DockerConfig{ 1689 ExtraHosts: []string{"foo.example:2001:db8::1", "bar.example:192.0.2.1"}, 1690 } 1691 1692 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1693 require.Equal(t, dockerConfig.ExtraHosts, hostConfig.ExtraHosts) 1694 } 1695 1696 testDockerConfigurationWithServiceContainer(t, dockerConfig, cce) 1697 } 1698 1699 func TestDockerUserNSSetting(t *testing.T) { 1700 dockerConfig := &common.DockerConfig{ 1701 UsernsMode: "host", 1702 } 1703 1704 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1705 assert.Equal(t, container.UsernsMode("host"), hostConfig.UsernsMode) 1706 } 1707 1708 testDockerConfigurationWithJobContainer(t, dockerConfig, cce) 1709 1710 } 1711 1712 func TestDockerRuntimeSetting(t *testing.T) { 1713 dockerConfig := &common.DockerConfig{ 1714 Runtime: "runc", 1715 } 1716 1717 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1718 assert.Equal(t, "runc", hostConfig.Runtime) 1719 } 1720 1721 testDockerConfigurationWithJobContainer(t, dockerConfig, cce) 1722 } 1723 1724 func TestDockerSysctlsSetting(t *testing.T) { 1725 dockerConfig := &common.DockerConfig{ 1726 SysCtls: map[string]string{ 1727 "net.ipv4.ip_forward": "1", 1728 }, 1729 } 1730 1731 cce := func(t *testing.T, config *container.Config, hostConfig *container.HostConfig) { 1732 assert.Equal(t, "1", hostConfig.Sysctls["net.ipv4.ip_forward"]) 1733 } 1734 1735 testDockerConfigurationWithJobContainer(t, dockerConfig, cce) 1736 } 1737 1738 func TestCheckOSType(t *testing.T) { 1739 cases := map[string]struct { 1740 executorMetadata map[string]string 1741 dockerInfoOSType string 1742 expectedErr string 1743 }{ 1744 "executor and docker info mismatch": { 1745 executorMetadata: map[string]string{ 1746 metadataOSType: osTypeWindows, 1747 }, 1748 dockerInfoOSType: osTypeLinux, 1749 expectedErr: "executor requires OSType=windows, but Docker Engine supports only OSType=linux", 1750 }, 1751 "executor and docker info match": { 1752 executorMetadata: map[string]string{ 1753 metadataOSType: osTypeLinux, 1754 }, 1755 dockerInfoOSType: osTypeLinux, 1756 expectedErr: "", 1757 }, 1758 "executor OSType not defined": { 1759 executorMetadata: nil, 1760 dockerInfoOSType: osTypeLinux, 1761 expectedErr: " does not have any OSType specified", 1762 }, 1763 } 1764 1765 for name, c := range cases { 1766 t.Run(name, func(t *testing.T) { 1767 executor := executor{ 1768 info: types.Info{ 1769 OSType: c.dockerInfoOSType, 1770 }, 1771 AbstractExecutor: executors.AbstractExecutor{ 1772 ExecutorOptions: executors.ExecutorOptions{ 1773 Metadata: c.executorMetadata, 1774 }, 1775 }, 1776 } 1777 1778 err := executor.validateOSType() 1779 if c.expectedErr == "" { 1780 assert.NoError(t, err) 1781 return 1782 } 1783 assert.EqualError(t, err, c.expectedErr) 1784 }) 1785 } 1786 } 1787 1788 func init() { 1789 docker_helpers.HomeDirectory = "" 1790 }