github.com/adevinta/lava@v0.7.2/internal/containers/containers_test.go (about) 1 // Copyright 2023 Adevinta 2 3 package containers 4 5 import ( 6 "bytes" 7 "context" 8 "crypto/tls" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "net" 14 "net/http" 15 "net/http/httptest" 16 "os" 17 "regexp" 18 "strings" 19 "testing" 20 21 "github.com/docker/docker/api/types/container" 22 "github.com/docker/docker/api/types/filters" 23 "github.com/docker/docker/api/types/image" 24 "github.com/docker/docker/client" 25 "github.com/docker/docker/pkg/stdcopy" 26 "github.com/google/go-cmp/cmp" 27 ) 28 29 var testRuntime Runtime 30 31 func TestMain(m *testing.M) { 32 rt, err := GetenvRuntime() 33 if err != nil { 34 fmt.Fprintf(os.Stderr, "error: get env runtime: %v", err) 35 os.Exit(2) 36 } 37 testRuntime = rt 38 39 os.Exit(m.Run()) 40 } 41 42 func TestParseRuntime(t *testing.T) { 43 tests := []struct { 44 name string 45 rtName string 46 want Runtime 47 wantNilErr bool 48 }{ 49 { 50 name: "valid runtime", 51 rtName: "DockerdDockerDesktop", 52 want: RuntimeDockerdDockerDesktop, 53 wantNilErr: true, 54 }, 55 { 56 name: "invalid runtime", 57 rtName: "Invalid", 58 want: Runtime(0), 59 wantNilErr: false, 60 }, 61 } 62 63 for _, tt := range tests { 64 t.Run(tt.name, func(t *testing.T) { 65 got, err := ParseRuntime(tt.rtName) 66 67 if (err == nil) != tt.wantNilErr { 68 t.Errorf("unexpected error: %v", err) 69 } 70 71 if got != tt.want { 72 t.Errorf("unexpected runtime: got: %v, want: %v", tt.want, got) 73 } 74 }) 75 } 76 } 77 78 func TestGetenvRuntime(t *testing.T) { 79 tests := []struct { 80 name string 81 env string 82 want Runtime 83 wantNilErr bool 84 }{ 85 { 86 name: "empty env var", 87 env: "", 88 want: RuntimeDockerd, 89 wantNilErr: true, 90 }, 91 { 92 name: "dockerd podman desktop", 93 env: "DockerdPodmanDesktop", 94 want: RuntimeDockerdPodmanDesktop, 95 wantNilErr: true, 96 }, 97 { 98 name: "invalid runtime", 99 env: "Invalid", 100 want: Runtime(0), 101 wantNilErr: false, 102 }, 103 } 104 105 for _, tt := range tests { 106 t.Run(tt.name, func(t *testing.T) { 107 t.Setenv("LAVA_RUNTIME", tt.env) 108 109 got, err := GetenvRuntime() 110 111 if (err == nil) != tt.wantNilErr { 112 t.Errorf("unexpected error: %v", err) 113 } 114 115 if got != tt.want { 116 t.Errorf("unexpected runtime: got: %v, want: %v", got, tt.want) 117 } 118 }) 119 } 120 } 121 122 func TestRuntime_UnmarshalText(t *testing.T) { 123 type JSONData struct { 124 Runtime Runtime `json:"runtime"` 125 } 126 127 tests := []struct { 128 name string 129 data string 130 want JSONData 131 wantNilErr bool 132 }{ 133 { 134 name: "valid runtime", 135 data: `{"runtime": "DockerdRancherDesktop"}`, 136 want: JSONData{Runtime: RuntimeDockerdRancherDesktop}, 137 wantNilErr: true, 138 }, 139 { 140 name: "invalid runtime", 141 data: `{"runtime": "Invalid"}`, 142 want: JSONData{}, 143 wantNilErr: false, 144 }, 145 } 146 147 for _, tt := range tests { 148 t.Run(tt.name, func(t *testing.T) { 149 var got JSONData 150 151 err := json.Unmarshal([]byte(tt.data), &got) 152 153 if (err == nil) != tt.wantNilErr { 154 t.Errorf("unexpected error: %v", err) 155 } 156 157 if got != tt.want { 158 t.Errorf("unexpected runtime: got: %v, want: %v", tt.want, got) 159 } 160 }) 161 } 162 } 163 164 var ( 165 bridgeCfgs = []mockDockerdIPAMConfig{{Subnet: "172.17.0.0/16", Gateway: "172.17.0.1"}} 166 bridgeAddr = &net.IPNet{IP: net.ParseIP("172.17.0.1"), Mask: net.CIDRMask(16, 32)} 167 168 defaultAPITestdata = mockDockerdTestdata{ 169 networks: map[string]mockDockerdNetworkTestdata{ 170 defaultDockerBridgeNetwork: { 171 cfgs: bridgeCfgs, 172 gateways: []*net.IPNet{bridgeAddr}, 173 bridgeGateway: bridgeAddr, 174 }, 175 "multi": { 176 cfgs: []mockDockerdIPAMConfig{ 177 {Subnet: "172.18.0.0/16", Gateway: "172.18.0.1"}, 178 {Subnet: "172.19.0.0/16", Gateway: "172.19.0.10"}, 179 }, 180 gateways: []*net.IPNet{ 181 {IP: net.ParseIP("172.18.0.1"), Mask: net.CIDRMask(16, 32)}, 182 {IP: net.ParseIP("172.19.0.10"), Mask: net.CIDRMask(16, 32)}, 183 }, 184 }, 185 "empty": {}, 186 "mismatch": { 187 cfgs: []mockDockerdIPAMConfig{ 188 {Subnet: "172.17.0.0/16", Gateway: "172.18.0.1"}, 189 }, 190 }, 191 "badgateway": { 192 cfgs: []mockDockerdIPAMConfig{ 193 {Subnet: "172.18.0.0/16", Gateway: "172.18.0.555"}, 194 }, 195 }, 196 "badsubnet": { 197 cfgs: []mockDockerdIPAMConfig{ 198 {Subnet: "172.18.555.0/16", Gateway: "172.18.0.1"}, 199 }, 200 }, 201 }, 202 system: mockDockerdSystemTestdata{ 203 id: "dockerutil", 204 }, 205 } 206 ) 207 208 func TestNewDockerdClient_tls(t *testing.T) { 209 tests := []struct { 210 name string 211 host string 212 wantID string 213 wantNilErr bool 214 }{ 215 { 216 name: "success", 217 host: "127.0.0.1", 218 wantID: defaultAPITestdata.system.id, 219 wantNilErr: true, 220 }, 221 { 222 name: "error", 223 host: "localhost", 224 wantID: "", 225 wantNilErr: false, 226 }, 227 } 228 229 for _, tt := range tests { 230 t.Run(tt.name, func(t *testing.T) { 231 srv := httptest.NewUnstartedServer(mockDockerd{testdata: defaultAPITestdata}) 232 233 cert, err := tls.LoadX509KeyPair("testdata/certs/server-cert.pem", "testdata/certs/server-key.pem") 234 if err != nil { 235 panic(fmt.Sprintf("httptest: NewTLSServer: %v", err)) 236 } 237 srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} 238 239 srv.StartTLS() 240 defer srv.Close() 241 242 addr := srv.Listener.Addr().(*net.TCPAddr) 243 dockerHost := fmt.Sprintf("tcp://%v:%v", tt.host, addr.Port) 244 245 t.Setenv("DOCKER_CONFIG", "testdata") 246 t.Setenv("DOCKER_CERT_PATH", "testdata/certs") 247 t.Setenv("DOCKER_HOST", dockerHost) 248 t.Setenv("DOCKER_TLS_VERIFY", "1") 249 250 cli, err := NewDockerdClient(RuntimeDockerd) 251 if err != nil { 252 t.Fatalf("could not create API client: %v", err) 253 } 254 defer cli.Close() 255 256 if dh := cli.DaemonHost(); dh != dockerHost { 257 t.Errorf("unexpected daemon host: got: %v, want: %v", dh, dockerHost) 258 } 259 260 info, err := cli.Info(context.Background()) 261 262 if err == nil != tt.wantNilErr { 263 t.Errorf("unexpected error: %v", err) 264 } 265 266 if err != nil { 267 var tlsErr *tls.CertificateVerificationError 268 if !errors.As(err, &tlsErr) { 269 t.Errorf("error is not a TLS error: %v", err) 270 } 271 } 272 273 if info.ID != tt.wantID { 274 t.Errorf("unexpected system ID: got: %v, want: %v", info.ID, defaultAPITestdata.system.id) 275 } 276 }) 277 } 278 } 279 280 func TestDockerdClient_DaemonHost(t *testing.T) { 281 const dockerHost = "tcp://example.com:1234" 282 283 t.Setenv("DOCKER_CONFIG", "testdata/certs") 284 t.Setenv("DOCKER_HOST", dockerHost) 285 286 cli, err := NewDockerdClient(RuntimeDockerd) 287 if err != nil { 288 t.Fatalf("could not create API client: %v", err) 289 } 290 defer cli.Close() 291 292 if dh := cli.DaemonHost(); dh != dockerHost { 293 t.Errorf("unexpected daemon host: got: %v, want: %v", dh, dockerHost) 294 } 295 } 296 297 func TestDockerdClient_HostGatewayHostname(t *testing.T) { 298 tests := []struct { 299 name string 300 rt Runtime 301 want string 302 }{ 303 { 304 name: "dockerd", 305 rt: RuntimeDockerd, 306 want: "host.docker.internal", 307 }, 308 { 309 name: "dockerd podman desktop", 310 rt: RuntimeDockerdPodmanDesktop, 311 want: "host.containers.internal", 312 }, 313 { 314 name: "invalid runtime", 315 rt: Runtime(255), 316 want: "host.docker.internal", 317 }, 318 } 319 320 for _, tt := range tests { 321 t.Run(tt.name, func(t *testing.T) { 322 cli, err := NewDockerdClient(tt.rt) 323 if err != nil { 324 t.Fatalf("could not create API client: %v", err) 325 } 326 defer cli.Close() 327 328 got := cli.HostGatewayHostname() 329 if got != tt.want { 330 t.Errorf("unexpected hostname: got: %v, want: %v", got, tt.want) 331 } 332 }) 333 } 334 } 335 336 func TestDockerdClient_HostGatewayMapping(t *testing.T) { 337 tests := []struct { 338 name string 339 rt Runtime 340 want string 341 }{ 342 { 343 name: "dockerd", 344 rt: RuntimeDockerd, 345 want: "host.docker.internal:host-gateway", 346 }, 347 { 348 name: "dockerd podman desktop", 349 rt: RuntimeDockerdPodmanDesktop, 350 want: "", 351 }, 352 { 353 name: "invalid runtime", 354 rt: Runtime(255), 355 want: "", 356 }, 357 } 358 359 for _, tt := range tests { 360 t.Run(tt.name, func(t *testing.T) { 361 cli, err := NewDockerdClient(tt.rt) 362 if err != nil { 363 t.Fatalf("could not create API client: %v", err) 364 } 365 defer cli.Close() 366 367 got := cli.HostGatewayMapping() 368 if got != tt.want { 369 t.Errorf("unexpected hostname: got: %v, want: %v", got, tt.want) 370 } 371 }) 372 } 373 } 374 375 func TestDockerdClient_gateways(t *testing.T) { 376 tests := []struct { 377 name string 378 net string 379 wantNilErr bool 380 }{ 381 { 382 name: "default bridge network", 383 net: defaultDockerBridgeNetwork, 384 wantNilErr: true, 385 }, 386 { 387 name: "multiple gateways", 388 net: "multi", 389 wantNilErr: true, 390 }, 391 { 392 name: "no gateways", 393 net: "empty", 394 wantNilErr: true, 395 }, 396 { 397 name: "subnet mismatch", 398 net: "mismatch", 399 wantNilErr: false, 400 }, 401 { 402 name: "malformed subnet", 403 net: "badsubnet", 404 wantNilErr: false, 405 }, 406 { 407 name: "malformed gateway", 408 net: "badgateway", 409 wantNilErr: false, 410 }, 411 { 412 name: "api error", 413 net: "notfound", 414 wantNilErr: false, 415 }, 416 } 417 418 for _, tt := range tests { 419 t.Run(tt.name, func(t *testing.T) { 420 cli, err := newMockDockerdClient(t, RuntimeDockerd, defaultAPITestdata) 421 if err != nil { 422 t.Fatalf("could not create test client: %v", err) 423 } 424 defer cli.Close() 425 426 got, err := cli.gateways(context.Background(), tt.net) 427 428 if (err == nil) != tt.wantNilErr { 429 t.Errorf("unexpected error: %v", err) 430 } 431 432 td := defaultAPITestdata.networks[tt.net] 433 if diff := cmp.Diff(td.gateways, got); diff != "" { 434 t.Errorf("gateways mismatch (-want +got):\n%s", diff) 435 } 436 }) 437 } 438 } 439 440 func TestDockerdClient_bridgeGateway(t *testing.T) { 441 tests := []struct { 442 name string 443 td mockDockerdTestdata 444 wantNilErr bool 445 }{ 446 { 447 name: "default bridge network", 448 td: defaultAPITestdata, 449 wantNilErr: true, 450 }, 451 { 452 name: "multiple gateways", 453 td: mockDockerdTestdata{ 454 networks: map[string]mockDockerdNetworkTestdata{ 455 defaultDockerBridgeNetwork: { 456 cfgs: []mockDockerdIPAMConfig{ 457 {Subnet: "172.18.0.0/16", Gateway: "172.18.0.1"}, 458 {Subnet: "172.19.0.0/16", Gateway: "172.19.0.10"}, 459 }, 460 }, 461 }, 462 }, 463 wantNilErr: false, 464 }, 465 { 466 name: "no gateways", 467 td: mockDockerdTestdata{ 468 networks: map[string]mockDockerdNetworkTestdata{ 469 defaultDockerBridgeNetwork: {}, 470 }, 471 }, 472 wantNilErr: false, 473 }, 474 } 475 476 for _, tt := range tests { 477 t.Run(tt.name, func(t *testing.T) { 478 cli, err := newMockDockerdClient(t, RuntimeDockerd, tt.td) 479 if err != nil { 480 t.Fatalf("could not create test client: %v", err) 481 } 482 defer cli.Close() 483 484 got, err := cli.bridgeGateway() 485 486 if (err == nil) != tt.wantNilErr { 487 t.Errorf("unexpected error: %v", err) 488 } 489 490 want := tt.td.networks[defaultDockerBridgeNetwork].bridgeGateway 491 if !cmp.Equal(got, want) { 492 t.Errorf("unexpected value: got: %v, want: %v", got, want) 493 } 494 }) 495 } 496 } 497 498 func TestDockerdClient_HostGatewayInterfaceAddr(t *testing.T) { 499 tests := []struct { 500 name string 501 rt Runtime 502 want string 503 }{ 504 { 505 name: "docker desktop", 506 rt: RuntimeDockerdDockerDesktop, 507 want: "127.0.0.1", 508 }, 509 { 510 name: "docker engine", 511 rt: RuntimeDockerd, 512 want: bridgeAddr.IP.String(), 513 }, 514 } 515 516 for _, tt := range tests { 517 t.Run(tt.name, func(t *testing.T) { 518 cli, err := newMockDockerdClient(t, tt.rt, defaultAPITestdata) 519 if err != nil { 520 t.Fatalf("could not create test client: %v", err) 521 } 522 defer cli.Close() 523 524 got, err := cli.HostGatewayInterfaceAddr() 525 if err != nil { 526 t.Errorf("unexpected error: %v", err) 527 } 528 529 if got != tt.want { 530 t.Errorf("unexpected value: got: %v, want: %v", got, tt.want) 531 } 532 }) 533 } 534 } 535 536 func TestDockerdClient_ImageBuild(t *testing.T) { 537 cli, err := NewDockerdClient(testRuntime) 538 if err != nil { 539 t.Fatalf("could not create API client: %v", err) 540 } 541 defer cli.Close() 542 543 const imgRef = "lava-internal-containers-test:go-test" 544 545 imgID, err := cli.ImageBuild(context.Background(), "testdata/image", "Dockerfile", imgRef) 546 if err != nil { 547 t.Fatalf("image build error: %v", err) 548 } 549 defer func() { 550 rmOpts := image.RemoveOptions{Force: true, PruneChildren: true} 551 if _, err := cli.ImageRemove(context.Background(), imgRef, rmOpts); err != nil { 552 t.Logf("could not delete test Docker image %q: %v", imgRef, err) 553 } 554 }() 555 556 summ, err := cli.ImageList(context.Background(), image.ListOptions{ 557 Filters: filters.NewArgs(filters.Arg("reference", imgRef)), 558 }) 559 if err != nil { 560 t.Fatalf("image list error: %v", err) 561 } 562 563 if len(summ) != 1 { 564 t.Errorf("unexpected number of images: %v", len(summ)) 565 } 566 567 const want = "image build test" 568 569 got, err := dockerRun(t, cli.APIClient, imgID, want) 570 if err != nil { 571 t.Fatalf("docker run error: %v", err) 572 } 573 574 if got != want { 575 t.Errorf("unexpected output: got: %q, want: %q", got, want) 576 } 577 } 578 579 func dockerRun(t *testing.T, cli client.APIClient, ref string, cmd ...string) (stdout string, err error) { 580 contCfg := &container.Config{ 581 Image: ref, 582 Cmd: cmd, 583 Tty: false, 584 } 585 resp, err := cli.ContainerCreate(context.Background(), contCfg, nil, nil, nil, "") 586 if err != nil { 587 return "", fmt.Errorf("container create: %w", err) 588 } 589 defer func() { 590 rmOpts := container.RemoveOptions{Force: true} 591 if err := cli.ContainerRemove(context.Background(), resp.ID, rmOpts); err != nil { 592 t.Logf("could not delete test Docker container %q: %v", resp.ID, err) 593 } 594 }() 595 596 if err := cli.ContainerStart(context.Background(), resp.ID, container.StartOptions{}); err != nil { 597 return "", fmt.Errorf("container start: %w", err) 598 } 599 600 statusCh, errCh := cli.ContainerWait(context.Background(), resp.ID, container.WaitConditionNotRunning) 601 select { 602 case err := <-errCh: 603 if err != nil { 604 return "", fmt.Errorf("container wait: %w", err) 605 } 606 case <-statusCh: 607 } 608 609 logs, err := cli.ContainerLogs(context.Background(), resp.ID, container.LogsOptions{ShowStdout: true}) 610 if err != nil { 611 return "", fmt.Errorf("container logs: %w", err) 612 } 613 614 var out bytes.Buffer 615 if _, err := stdcopy.StdCopy(&out, io.Discard, logs); err != nil { 616 return "", fmt.Errorf("std copy: %w", err) 617 } 618 619 return out.String(), nil 620 } 621 622 type mockDockerdClient struct { 623 DockerdClient 624 srv *httptest.Server 625 } 626 627 func newMockDockerdClient(t *testing.T, rt Runtime, td mockDockerdTestdata) (mockDockerdClient, error) { 628 srv := httptest.NewServer(mockDockerd{testdata: td}) 629 630 t.Setenv("DOCKER_HOST", "tcp://"+srv.Listener.Addr().String()) 631 632 cli, err := NewDockerdClient(rt) 633 if err != nil { 634 srv.Close() 635 return mockDockerdClient{}, fmt.Errorf("new client: %w", err) 636 } 637 638 mockcli := mockDockerdClient{ 639 DockerdClient: cli, 640 srv: srv, 641 } 642 return mockcli, nil 643 } 644 645 func (mockcli mockDockerdClient) Close() error { 646 mockcli.srv.Close() 647 return mockcli.DockerdClient.Close() 648 } 649 650 type mockDockerd struct { 651 testdata mockDockerdTestdata 652 } 653 654 type mockDockerdTestdata struct { 655 networks map[string]mockDockerdNetworkTestdata 656 system mockDockerdSystemTestdata 657 } 658 659 type mockDockerdNetworkTestdata struct { 660 cfgs []mockDockerdIPAMConfig 661 gateways []*net.IPNet 662 bridgeGateway *net.IPNet 663 } 664 665 type mockDockerdSystemTestdata struct { 666 id string 667 } 668 669 var routeRegexp = regexp.MustCompile(`^/v\d+\.\d+(/.*)$`) 670 671 func (api mockDockerd) ServeHTTP(w http.ResponseWriter, r *http.Request) { 672 m := routeRegexp.FindStringSubmatch(r.URL.Path) 673 if m == nil { 674 http.Error(w, "bad request", http.StatusBadRequest) 675 return 676 } 677 endpoint := m[1] 678 679 if r.Method != "GET" { 680 http.Error(w, "not implemented", http.StatusNotImplemented) 681 return 682 } 683 684 switch { 685 case strings.HasPrefix(endpoint, "/networks/"): 686 api.handleNetworks(w, r, strings.TrimPrefix(endpoint, "/networks/")) 687 case endpoint == "/info": 688 api.handleInfo(w, r) 689 default: 690 http.Error(w, "not found", http.StatusNotFound) 691 } 692 } 693 694 type mockDockerdNetwork struct { 695 IPAM mockDockerdIPAM `json:"IPAM"` 696 } 697 698 type mockDockerdIPAM struct { 699 Config []mockDockerdIPAMConfig `json:"Config"` 700 } 701 702 type mockDockerdIPAMConfig struct { 703 Subnet string `json:"Subnet"` 704 Gateway string `json:"Gateway"` 705 } 706 707 func (api mockDockerd) handleNetworks(w http.ResponseWriter, _ *http.Request, name string) { 708 td, ok := api.testdata.networks[name] 709 if !ok { 710 http.Error(w, "not found", http.StatusNotFound) 711 return 712 } 713 714 net := mockDockerdNetwork{IPAM: mockDockerdIPAM{Config: td.cfgs}} 715 if err := json.NewEncoder(w).Encode(net); err != nil { 716 http.Error(w, fmt.Sprintf("marshal: %v", err), http.StatusInternalServerError) 717 } 718 } 719 720 type mockDockerdInfo struct { 721 ID string `json:"ID"` 722 } 723 724 func (api mockDockerd) handleInfo(w http.ResponseWriter, _ *http.Request) { 725 net := mockDockerdInfo{ID: api.testdata.system.id} 726 if err := json.NewEncoder(w).Encode(net); err != nil { 727 http.Error(w, fmt.Sprintf("marshal: %v", err), http.StatusInternalServerError) 728 } 729 }