diff --git a/docker.go b/docker.go index cb799dc05a..cdee43183c 100644 --- a/docker.go +++ b/docker.go @@ -922,7 +922,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque // If default network is not bridge make sure it is attached to the request // as container won't be attached to it automatically // in case of Podman the bridge network is called 'podman' as 'bridge' would conflict - if p.DefaultNetwork != p.defaultBridgeNetworkName { + if p.DefaultNetwork != p.DefaultBridgeNetworkName { isAttached := false for _, net := range req.Networks { if net == p.DefaultNetwork { @@ -1076,7 +1076,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque // default hooks include logger hook and pre-create hook defaultHooks := []ContainerLifecycleHooks{ DefaultLoggingHook(p.Logger), - defaultPreCreateHook(ctx, p, req, dockerInput, hostConfig, networkingConfig), + DefaultPreCreateHook(p, dockerInput, hostConfig, networkingConfig), defaultCopyFileToContainerHook(req.Files), defaultLogConsumersHook(req.LogConsumerCfg), defaultReadinessHook(), @@ -1479,8 +1479,8 @@ func (p *DockerProvider) getDefaultNetwork(ctx context.Context, cli client.APICl reaperNetworkExists := false for _, net := range networkResources { - if net.Name == p.defaultBridgeNetworkName { - return p.defaultBridgeNetworkName, nil + if net.Name == p.DefaultBridgeNetworkName { + return p.DefaultBridgeNetworkName, nil } if net.Name == reaperNetwork { diff --git a/docker_test.go b/docker_test.go index 45e276b1ed..5eeeeb93f1 100644 --- a/docker_test.go +++ b/docker_test.go @@ -1,4 +1,4 @@ -package testcontainers +package testcontainers_test import ( "context" @@ -23,45 +23,28 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) -const ( - mysqlImage = "docker.io/mysql:8.0.36" - nginxDelayedImage = "docker.io/menedev/delayed-nginx:1.15.2" - nginxImage = "docker.io/nginx" - nginxAlpineImage = "docker.io/nginx:alpine" - nginxDefaultPort = "80/tcp" - nginxHighPort = "8080/tcp" - daemonMaxVersion = "1.41" -) - -var providerType = ProviderDocker - -func init() { - if strings.Contains(os.Getenv("DOCKER_HOST"), "podman.sock") { - providerType = ProviderPodman - } -} - func TestContainerWithHostNetworkOptions(t *testing.T) { if os.Getenv("XDG_RUNTIME_DIR") != "" { t.Skip("Skipping test that requires host network access when running in a container") } ctx := context.Background() - SkipIfDockerDesktop(t, ctx) + testcontainers.SkipIfDockerDesktop(t, ctx) absPath, err := filepath.Abs(filepath.Join("testdata", "nginx-highport.conf")) if err != nil { t.Fatal(err) } - gcr := GenericContainerRequest{ + gcr := testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, - Files: []ContainerFile{ + Files: []testcontainers.ContainerFile{ { HostFilePath: absPath, ContainerFilePath: "/etc/nginx/conf.d/default.conf", @@ -79,7 +62,7 @@ func TestContainerWithHostNetworkOptions(t *testing.T) { Started: true, } - nginxC, err := GenericContainer(ctx, gcr) + nginxC, err := testcontainers.GenericContainer(ctx, gcr) require.NoError(t, err) terminateContainerOnEnd(t, ctx, nginxC) @@ -101,8 +84,8 @@ func TestContainerWithHostNetworkOptions(t *testing.T) { func TestContainerWithHostNetworkOptions_UseExposePortsFromImageConfigs(t *testing.T) { ctx := context.Background() - gcr := GenericContainerRequest{ - ContainerRequest: ContainerRequest{ + gcr := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "nginx", Privileged: true, WaitingFor: wait.ForExposedPort(), @@ -110,7 +93,7 @@ func TestContainerWithHostNetworkOptions_UseExposePortsFromImageConfigs(t *testi Started: true, } - nginxC, err := GenericContainer(ctx, gcr) + nginxC, err := testcontainers.GenericContainer(ctx, gcr) if err != nil { t.Fatal(err) } @@ -140,12 +123,12 @@ func TestContainerWithNetworkModeAndNetworkTogether(t *testing.T) { // skipIfDockerDesktop { ctx := context.Background() - SkipIfDockerDesktop(t, ctx) + testcontainers.SkipIfDockerDesktop(t, ctx) // } - gcr := GenericContainerRequest{ + gcr := testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxImage, Networks: []string{"new-network"}, HostConfigModifier: func(hc *container.HostConfig) { @@ -155,7 +138,7 @@ func TestContainerWithNetworkModeAndNetworkTogether(t *testing.T) { Started: true, } - nginx, err := GenericContainer(ctx, gcr) + nginx, err := testcontainers.GenericContainer(ctx, gcr) if err != nil { // Error when NetworkMode = host and Network = []string{"bridge"} t.Logf("Can't use Network and NetworkMode together, %s\n", err) @@ -169,19 +152,19 @@ func TestContainerWithHostNetwork(t *testing.T) { } ctx := context.Background() - SkipIfDockerDesktop(t, ctx) + testcontainers.SkipIfDockerDesktop(t, ctx) absPath, err := filepath.Abs(filepath.Join("testdata", "nginx-highport.conf")) if err != nil { t.Fatal(err) } - gcr := GenericContainerRequest{ + gcr := testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, WaitingFor: wait.ForListeningPort(nginxHighPort), - Files: []ContainerFile{ + Files: []testcontainers.ContainerFile{ { HostFilePath: absPath, ContainerFilePath: "/etc/nginx/conf.d/default.conf", @@ -194,7 +177,7 @@ func TestContainerWithHostNetwork(t *testing.T) { Started: true, } - nginxC, err := GenericContainer(ctx, gcr) + nginxC, err := testcontainers.GenericContainer(ctx, gcr) require.NoError(t, err) terminateContainerOnEnd(t, ctx, nginxC) @@ -223,9 +206,9 @@ func TestContainerWithHostNetwork(t *testing.T) { func TestContainerReturnItsContainerID(t *testing.T) { ctx := context.Background() - nginxA, err := GenericContainer(ctx, GenericContainerRequest{ + nginxA, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{ nginxDefaultPort, @@ -244,9 +227,9 @@ func TestContainerReturnItsContainerID(t *testing.T) { func TestContainerTerminationResetsState(t *testing.T) { ctx := context.Background() - nginxA, err := GenericContainer(ctx, GenericContainerRequest{ + nginxA, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{ nginxDefaultPort, @@ -272,10 +255,10 @@ func TestContainerTerminationResetsState(t *testing.T) { } func TestContainerStateAfterTermination(t *testing.T) { - createContainerFn := func(ctx context.Context) (Container, error) { - return GenericContainer(ctx, GenericContainerRequest{ + createContainerFn := func(ctx context.Context) (testcontainers.Container, error) { + return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{ nginxDefaultPort, @@ -332,15 +315,15 @@ func TestContainerStateAfterTermination(t *testing.T) { func TestContainerTerminationRemovesDockerImage(t *testing.T) { t.Run("if not built from Dockerfile", func(t *testing.T) { ctx := context.Background() - client, err := NewDockerClientWithOpts(ctx) + client, err := testcontainers.NewDockerClientWithOpts(ctx) if err != nil { t.Fatal(err) } defer client.Close() - container, err := GenericContainer(ctx, GenericContainerRequest{ + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{ nginxDefaultPort, @@ -363,20 +346,20 @@ func TestContainerTerminationRemovesDockerImage(t *testing.T) { t.Run("if built from Dockerfile", func(t *testing.T) { ctx := context.Background() - client, err := NewDockerClientWithOpts(ctx) + client, err := testcontainers.NewDockerClientWithOpts(ctx) if err != nil { t.Fatal(err) } defer client.Close() - req := ContainerRequest{ - FromDockerfile: FromDockerfile{ + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: filepath.Join(".", "testdata"), }, ExposedPorts: []string{"6379/tcp"}, WaitingFor: wait.ForLog("Ready to accept connections"), } - container, err := GenericContainer(ctx, GenericContainerRequest{ + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -405,9 +388,9 @@ func TestContainerTerminationRemovesDockerImage(t *testing.T) { func TestTwoContainersExposingTheSamePort(t *testing.T) { ctx := context.Background() - nginxA, err := GenericContainer(ctx, GenericContainerRequest{ + nginxA, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{ nginxDefaultPort, @@ -419,9 +402,9 @@ func TestTwoContainersExposingTheSamePort(t *testing.T) { require.NoError(t, err) terminateContainerOnEnd(t, ctx, nginxA) - nginxB, err := GenericContainer(ctx, GenericContainerRequest{ + nginxB, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{ nginxDefaultPort, @@ -466,9 +449,9 @@ func TestTwoContainersExposingTheSamePort(t *testing.T) { func TestContainerCreation(t *testing.T) { ctx := context.Background() - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{ nginxDefaultPort, @@ -520,9 +503,9 @@ func TestContainerCreationWithName(t *testing.T) { creationName := fmt.Sprintf("%s_%d", "test_container", time.Now().Unix()) expectedName := "/" + creationName // inspect adds '/' in the beginning - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{ nginxDefaultPort, @@ -553,13 +536,13 @@ func TestContainerCreationWithName(t *testing.T) { } network := networks[0] switch providerType { - case ProviderDocker: - if network != Bridge { - t.Errorf("Expected network name '%s'. Got '%s'.", Bridge, network) + case testcontainers.ProviderDocker: + if network != testcontainers.Bridge { + t.Errorf("Expected network name '%s'. Got '%s'.", testcontainers.Bridge, network) } - case ProviderPodman: - if network != Podman { - t.Errorf("Expected network name '%s'. Got '%s'.", Podman, network) + case testcontainers.ProviderPodman: + if network != testcontainers.Podman { + t.Errorf("Expected network name '%s'. Got '%s'.", testcontainers.Podman, network) } } @@ -581,9 +564,9 @@ func TestContainerCreationAndWaitForListeningPortLongEnough(t *testing.T) { ctx := context.Background() // delayed-nginx will wait 2s before opening port - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxDelayedImage, ExposedPorts: []string{ nginxDefaultPort, @@ -614,9 +597,9 @@ func TestContainerCreationAndWaitForListeningPortLongEnough(t *testing.T) { func TestContainerCreationTimesOut(t *testing.T) { ctx := context.Background() // delayed-nginx will wait 2s before opening port - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxDelayedImage, ExposedPorts: []string{ nginxDefaultPort, @@ -636,9 +619,9 @@ func TestContainerCreationTimesOut(t *testing.T) { func TestContainerRespondsWithHttp200ForIndex(t *testing.T) { ctx := context.Background() - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{ nginxDefaultPort, @@ -669,9 +652,9 @@ func TestContainerRespondsWithHttp200ForIndex(t *testing.T) { func TestContainerCreationTimesOutWithHttp(t *testing.T) { ctx := context.Background() // delayed-nginx will wait 2s before opening port - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxDelayedImage, ExposedPorts: []string{ nginxDefaultPort, @@ -689,7 +672,7 @@ func TestContainerCreationTimesOutWithHttp(t *testing.T) { func TestContainerCreationWaitsForLogContextTimeout(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: mysqlImage, ExposedPorts: []string{"3306/tcp", "33060/tcp"}, Env: map[string]string{ @@ -698,7 +681,7 @@ func TestContainerCreationWaitsForLogContextTimeout(t *testing.T) { }, WaitingFor: wait.ForLog("test context timeout").WithStartupTimeout(1 * time.Second), } - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -712,7 +695,7 @@ func TestContainerCreationWaitsForLogContextTimeout(t *testing.T) { func TestContainerCreationWaitsForLog(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: mysqlImage, ExposedPorts: []string{"3306/tcp", "33060/tcp"}, Env: map[string]string{ @@ -721,7 +704,7 @@ func TestContainerCreationWaitsForLog(t *testing.T) { }, WaitingFor: wait.ForLog("port: 3306 MySQL Community Server - GPL"), } - mysqlC, err := GenericContainer(ctx, GenericContainerRequest{ + mysqlC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -740,8 +723,8 @@ func Test_BuildContainerFromDockerfileWithBuildArgs(t *testing.T) { // fromDockerfileWithBuildArgs { ba := "build args value" - req := ContainerRequest{ - FromDockerfile: FromDockerfile{ + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: filepath.Join(".", "testdata"), Dockerfile: "args.Dockerfile", BuildArgs: map[string]*string{ @@ -753,13 +736,13 @@ func Test_BuildContainerFromDockerfileWithBuildArgs(t *testing.T) { } // } - genContainerReq := GenericContainerRequest{ + genContainerReq := testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, } - c, err := GenericContainer(ctx, genContainerReq) + c, err := testcontainers.GenericContainer(ctx, genContainerReq) require.NoError(t, err) terminateContainerOnEnd(t, ctx, c) @@ -794,8 +777,8 @@ func Test_BuildContainerFromDockerfileWithBuildLog(t *testing.T) { t.Log("got ctx, creating container request") // fromDockerfile { - req := ContainerRequest{ - FromDockerfile: FromDockerfile{ + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: filepath.Join(".", "testdata"), Dockerfile: "buildlog.Dockerfile", PrintBuildLog: true, @@ -803,13 +786,13 @@ func Test_BuildContainerFromDockerfileWithBuildLog(t *testing.T) { } // } - genContainerReq := GenericContainerRequest{ + genContainerReq := testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, } - c, err := GenericContainer(ctx, genContainerReq) + c, err := testcontainers.GenericContainer(ctx, genContainerReq) require.NoError(t, err) terminateContainerOnEnd(t, ctx, c) @@ -826,7 +809,7 @@ func Test_BuildContainerFromDockerfileWithBuildLog(t *testing.T) { func TestContainerCreationWaitsForLogAndPortContextTimeout(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: mysqlImage, ExposedPorts: []string{"3306/tcp", "33060/tcp"}, Env: map[string]string{ @@ -838,7 +821,7 @@ func TestContainerCreationWaitsForLogAndPortContextTimeout(t *testing.T) { wait.ForListeningPort("3306/tcp"), ), } - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -853,13 +836,13 @@ func TestContainerCreationWaitsForLogAndPortContextTimeout(t *testing.T) { func TestContainerCreationWaitingForHostPort(t *testing.T) { ctx := context.Background() // exposePorts { - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{nginxDefaultPort}, WaitingFor: wait.ForListeningPort(nginxDefaultPort), } // } - nginx, err := GenericContainer(ctx, GenericContainerRequest{ + nginx, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -871,12 +854,12 @@ func TestContainerCreationWaitingForHostPort(t *testing.T) { func TestContainerCreationWaitingForHostPortWithoutBashThrowsAnError(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{nginxDefaultPort}, WaitingFor: wait.ForListeningPort(nginxDefaultPort), } - nginx, err := GenericContainer(ctx, GenericContainerRequest{ + nginx, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -895,7 +878,7 @@ func TestCMD(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/alpine", WaitingFor: wait.ForAll( wait.ForLog("command override!"), @@ -903,7 +886,7 @@ func TestCMD(t *testing.T) { Cmd: []string{"echo", "command override!"}, } - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -922,7 +905,7 @@ func TestEntrypoint(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/alpine", WaitingFor: wait.ForAll( wait.ForLog("entrypoint override!"), @@ -930,7 +913,7 @@ func TestEntrypoint(t *testing.T) { Entrypoint: []string{"echo", "entrypoint override!"}, } - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -949,7 +932,7 @@ func TestWorkingDir(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/alpine", WaitingFor: wait.ForAll( wait.ForLog("/var/tmp/test"), @@ -958,7 +941,7 @@ func TestWorkingDir(t *testing.T) { WorkingDir: "/var/tmp/test", } - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -970,12 +953,12 @@ func TestWorkingDir(t *testing.T) { func ExampleDockerProvider_CreateContainer() { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), } - nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ + nginxC, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) @@ -998,12 +981,12 @@ func ExampleDockerProvider_CreateContainer() { func ExampleContainer_Host() { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), } - nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ + nginxC, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) @@ -1030,12 +1013,12 @@ func ExampleContainer_Host() { func ExampleContainer_Start() { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), } - nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ + nginxC, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, }) defer func() { @@ -1058,12 +1041,12 @@ func ExampleContainer_Start() { func ExampleContainer_Stop() { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), } - nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ + nginxC, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, }) defer func() { @@ -1087,12 +1070,12 @@ func ExampleContainer_Stop() { func ExampleContainer_MappedPort() { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/nginx:alpine", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second), } - nginxC, _ := GenericContainer(ctx, GenericContainerRequest{ + nginxC, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) @@ -1130,17 +1113,17 @@ func TestContainerCreationWithVolumeAndFileWritingToIt(t *testing.T) { volumeName := "volumeName" // Create the container that writes into the mounted volume. - bashC, err := GenericContainer(ctx, GenericContainerRequest{ + bashC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "docker.io/bash", - Files: []ContainerFile{ + Files: []testcontainers.ContainerFile{ { HostFilePath: absPath, ContainerFilePath: "/hello.sh", }, }, - Mounts: Mounts(VolumeMount(volumeName, "/data")), + Mounts: testcontainers.Mounts(testcontainers.VolumeMount(volumeName, "/data")), Cmd: []string{"bash", "/hello.sh"}, WaitingFor: wait.ForLog("done"), }, @@ -1153,13 +1136,13 @@ func TestContainerCreationWithVolumeAndFileWritingToIt(t *testing.T) { func TestContainerWithTmpFs(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/busybox", Cmd: []string{"sleep", "10"}, Tmpfs: map[string]string{"/testtmpfs": "rw"}, } - container, err := GenericContainer(ctx, GenericContainerRequest{ + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -1210,8 +1193,8 @@ func TestContainerWithTmpFs(t *testing.T) { func TestContainerNonExistentImage(t *testing.T) { t.Run("if the image not found don't propagate the error", func(t *testing.T) { - _, err := GenericContainer(context.Background(), GenericContainerRequest{ - ContainerRequest: ContainerRequest{ + _, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "postgres:nonexistent-version", }, Started: true, @@ -1226,9 +1209,9 @@ func TestContainerNonExistentImage(t *testing.T) { t.Run("the context cancellation is propagated to container creation", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "docker.io/postgres:12", WaitingFor: wait.ForLog("log"), }, @@ -1243,7 +1226,7 @@ func TestContainerNonExistentImage(t *testing.T) { } func TestContainerCustomPlatformImage(t *testing.T) { - if providerType == ProviderPodman { + if providerType == testcontainers.ProviderPodman { t.Skip("Incompatible Docker API version for Podman") } t.Run("error with a non-existent platform", func(t *testing.T) { @@ -1251,9 +1234,9 @@ func TestContainerCustomPlatformImage(t *testing.T) { nonExistentPlatform := "windows/arm12" ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "docker.io/redis:latest", ImagePlatform: nonExistentPlatform, }, @@ -1269,9 +1252,9 @@ func TestContainerCustomPlatformImage(t *testing.T) { t.Parallel() ctx := context.Background() - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "docker.io/mysql:8.0.36", ImagePlatform: "linux/amd64", }, @@ -1281,7 +1264,7 @@ func TestContainerCustomPlatformImage(t *testing.T) { require.NoError(t, err) terminateContainerOnEnd(t, ctx, c) - dockerCli, err := NewDockerClientWithOpts(ctx) + dockerCli, err := testcontainers.NewDockerClientWithOpts(ctx) require.NoError(t, err) defer dockerCli.Close() @@ -1299,12 +1282,12 @@ func TestContainerWithCustomHostname(t *testing.T) { ctx := context.Background() name := fmt.Sprintf("some-nginx-%s-%d", t.Name(), rand.Int()) hostname := fmt.Sprintf("my-nginx-%s-%d", t.Name(), rand.Int()) - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Name: name, Image: nginxImage, Hostname: hostname, } - container, err := GenericContainer(ctx, GenericContainerRequest{ + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -1319,7 +1302,7 @@ func TestContainerWithCustomHostname(t *testing.T) { } func readHostname(tb testing.TB, containerId string) string { - containerClient, err := NewDockerClientWithOpts(context.Background()) + containerClient, err := testcontainers.NewDockerClientWithOpts(context.Background()) if err != nil { tb.Fatalf("Failed to create Docker client: %v", err) } @@ -1352,9 +1335,9 @@ func TestDockerContainerCopyFileToContainer(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxImage, ExposedPorts: []string{nginxDefaultPort}, WaitingFor: wait.ForListeningPort(nginxDefaultPort), @@ -1380,9 +1363,9 @@ func TestDockerContainerCopyFileToContainer(t *testing.T) { func TestDockerContainerCopyDirToContainer(t *testing.T) { ctx := context.Background() - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxImage, ExposedPorts: []string{nginxDefaultPort}, WaitingFor: wait.ForListeningPort(nginxDefaultPort), @@ -1412,12 +1395,12 @@ func TestDockerCreateContainerWithFiles(t *testing.T) { copiedFileName := "/hello_copy.sh" tests := []struct { name string - files []ContainerFile + files []testcontainers.ContainerFile errMsg string }{ { name: "success copy", - files: []ContainerFile{ + files: []testcontainers.ContainerFile{ { HostFilePath: hostFileName, ContainerFilePath: copiedFileName, @@ -1427,7 +1410,7 @@ func TestDockerCreateContainerWithFiles(t *testing.T) { }, { name: "host file not found", - files: []ContainerFile{ + files: []testcontainers.ContainerFile{ { HostFilePath: hostFileName + "123", ContainerFilePath: copiedFileName, @@ -1441,8 +1424,8 @@ func TestDockerCreateContainerWithFiles(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ - ContainerRequest: ContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "nginx:1.17.6", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForListeningPort("80/tcp"), @@ -1483,12 +1466,12 @@ func TestDockerCreateContainerWithDirs(t *testing.T) { tests := []struct { name string - dir ContainerFile + dir testcontainers.ContainerFile hasError bool }{ { name: "success copy directory with full path", - dir: ContainerFile{ + dir: testcontainers.ContainerFile{ HostFilePath: abs, ContainerFilePath: "/tmp/" + hostDirName, // the parent dir must exist FileMode: 700, @@ -1497,7 +1480,7 @@ func TestDockerCreateContainerWithDirs(t *testing.T) { }, { name: "success copy directory", - dir: ContainerFile{ + dir: testcontainers.ContainerFile{ HostFilePath: filepath.Join(".", hostDirName), ContainerFilePath: "/tmp/" + hostDirName, // the parent dir must exist FileMode: 700, @@ -1506,7 +1489,7 @@ func TestDockerCreateContainerWithDirs(t *testing.T) { }, { name: "host dir not found", - dir: ContainerFile{ + dir: testcontainers.ContainerFile{ HostFilePath: filepath.Join(".", "testdata123"), // does not exist ContainerFilePath: "/tmp/" + hostDirName, // the parent dir must exist FileMode: 700, @@ -1515,7 +1498,7 @@ func TestDockerCreateContainerWithDirs(t *testing.T) { }, { name: "container dir not found", - dir: ContainerFile{ + dir: testcontainers.ContainerFile{ HostFilePath: filepath.Join(".", hostDirName), ContainerFilePath: "/parent-does-not-exist/testdata123", // does not exist FileMode: 700, @@ -1526,12 +1509,12 @@ func TestDockerCreateContainerWithDirs(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ - ContainerRequest: ContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "nginx:1.17.6", ExposedPorts: []string{"80/tcp"}, WaitingFor: wait.ForListeningPort("80/tcp"), - Files: []ContainerFile{tc.dir}, + Files: []testcontainers.ContainerFile{tc.dir}, }, Started: false, }) @@ -1566,9 +1549,9 @@ func TestDockerContainerCopyToContainer(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxImage, ExposedPorts: []string{nginxDefaultPort}, WaitingFor: wait.ForListeningPort(nginxDefaultPort), @@ -1605,9 +1588,9 @@ func TestDockerContainerCopyFileFromContainer(t *testing.T) { } ctx := context.Background() - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxImage, ExposedPorts: []string{nginxDefaultPort}, WaitingFor: wait.ForListeningPort(nginxDefaultPort), @@ -1644,9 +1627,9 @@ func TestDockerContainerCopyFileFromContainer(t *testing.T) { func TestDockerContainerCopyEmptyFileFromContainer(t *testing.T) { ctx := context.Background() - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxImage, ExposedPorts: []string{nginxDefaultPort}, WaitingFor: wait.ForListeningPort(nginxDefaultPort), @@ -1681,7 +1664,7 @@ func TestDockerContainerCopyEmptyFileFromContainer(t *testing.T) { } func TestDockerContainerResources(t *testing.T) { - if providerType == ProviderPodman { + if providerType == testcontainers.ProviderPodman { t.Skip("Rootless Podman does not support setting rlimit") } if os.Getenv("XDG_RUNTIME_DIR") != "" { @@ -1703,9 +1686,9 @@ func TestDockerContainerResources(t *testing.T) { }, } - nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{nginxDefaultPort}, WaitingFor: wait.ForListeningPort(nginxDefaultPort), @@ -1721,7 +1704,7 @@ func TestDockerContainerResources(t *testing.T) { require.NoError(t, err) terminateContainerOnEnd(t, ctx, nginxC) - c, err := NewDockerClientWithOpts(ctx) + c, err := testcontainers.NewDockerClientWithOpts(ctx) require.NoError(t, err) defer c.Close() @@ -1734,7 +1717,7 @@ func TestDockerContainerResources(t *testing.T) { } func TestContainerCapAdd(t *testing.T) { - if providerType == ProviderPodman { + if providerType == testcontainers.ProviderPodman { t.Skip("Rootless Podman does not support setting cap-add/cap-drop") } @@ -1742,9 +1725,9 @@ func TestContainerCapAdd(t *testing.T) { expected := "IPC_LOCK" - nginx, err := GenericContainer(ctx, GenericContainerRequest{ + nginx, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, ExposedPorts: []string{nginxDefaultPort}, WaitingFor: wait.ForListeningPort(nginxDefaultPort), @@ -1757,7 +1740,7 @@ func TestContainerCapAdd(t *testing.T) { require.NoError(t, err) terminateContainerOnEnd(t, ctx, nginx) - dockerClient, err := NewDockerClientWithOpts(ctx) + dockerClient, err := testcontainers.NewDockerClientWithOpts(ctx) require.NoError(t, err) defer dockerClient.Close() @@ -1770,7 +1753,7 @@ func TestContainerCapAdd(t *testing.T) { func TestContainerRunningCheckingStatusCode(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "influxdb:1.8.10-alpine", ExposedPorts: []string{"8086/tcp"}, ImagePlatform: "linux/amd64", // influxdb doesn't provide an alpine+arm build (https://github.com/influxdata/influxdata-docker/issues/335) @@ -1783,7 +1766,7 @@ func TestContainerRunningCheckingStatusCode(t *testing.T) { ), } - influx, err := GenericContainer(ctx, GenericContainerRequest{ + influx, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) @@ -1796,13 +1779,13 @@ func TestContainerRunningCheckingStatusCode(t *testing.T) { func TestContainerWithUserID(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/alpine:latest", User: "60125", Cmd: []string{"sh", "-c", "id -u"}, WaitingFor: wait.ForExit(), } - container, err := GenericContainer(ctx, GenericContainerRequest{ + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -1826,12 +1809,12 @@ func TestContainerWithUserID(t *testing.T) { func TestContainerWithNoUserID(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "docker.io/alpine:latest", Cmd: []string{"sh", "-c", "id -u"}, WaitingFor: wait.ForExit(), } - container, err := GenericContainer(ctx, GenericContainerRequest{ + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, ContainerRequest: req, Started: true, @@ -1856,13 +1839,13 @@ func TestContainerWithNoUserID(t *testing.T) { func TestGetGatewayIP(t *testing.T) { // When using docker-compose with DinD mode, and using host port or http wait strategy // It's need to invoke GetGatewayIP for get the host - provider, err := providerType.GetProvider(WithLogger(TestLogger(t))) + provider, err := providerType.GetProvider(testcontainers.WithLogger(testcontainers.TestLogger(t))) if err != nil { t.Fatal(err) } defer provider.Close() - ip, err := provider.(*DockerProvider).GetGatewayIP(context.Background()) + ip, err := provider.(*testcontainers.DockerProvider).GetGatewayIP(context.Background()) if err != nil { t.Fatal(err) } @@ -1872,7 +1855,7 @@ func TestGetGatewayIP(t *testing.T) { } func TestProviderHasConfig(t *testing.T) { - provider, err := NewDockerProvider(WithLogger(TestLogger(t))) + provider, err := testcontainers.NewDockerProvider(testcontainers.WithLogger(testcontainers.TestLogger(t))) if err != nil { t.Fatal(err) } @@ -1883,9 +1866,9 @@ func TestProviderHasConfig(t *testing.T) { func TestNetworkModeWithContainerReference(t *testing.T) { ctx := context.Background() - nginxA, err := GenericContainer(ctx, GenericContainerRequest{ + nginxA, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, }, Started: true, @@ -1895,9 +1878,9 @@ func TestNetworkModeWithContainerReference(t *testing.T) { terminateContainerOnEnd(t, ctx, nginxA) networkMode := fmt.Sprintf("container:%v", nginxA.GetContainerID()) - nginxB, err := GenericContainer(ctx, GenericContainerRequest{ + nginxB, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: nginxAlpineImage, HostConfigModifier: func(hc *container.HostConfig) { hc.NetworkMode = container.NetworkMode(networkMode) @@ -1911,7 +1894,7 @@ func TestNetworkModeWithContainerReference(t *testing.T) { } // creates a temporary dir in which the files will be extracted. Then it will compare the bytes of each file in the source with the bytes from the copied-from-container file -func assertExtractedFiles(t *testing.T, ctx context.Context, container Container, hostFilePath string, containerFilePath string) { +func assertExtractedFiles(t *testing.T, ctx context.Context, container testcontainers.Container, hostFilePath string, containerFilePath string) { // create all copied files into a temporary dir tmpDir := t.TempDir() @@ -1955,26 +1938,15 @@ func assertExtractedFiles(t *testing.T, ctx context.Context, container Container } } -func terminateContainerOnEnd(tb testing.TB, ctx context.Context, ctr Container) { - tb.Helper() - if ctr == nil { - return - } - tb.Cleanup(func() { - tb.Log("terminating container") - require.NoError(tb, ctr.Terminate(ctx)) - }) -} - -func TestDockerProviderFindContainerByName(t *testing.T) { +func TestDockerProviderReuseOrCreateContainer(t *testing.T) { ctx := context.Background() - provider, err := NewDockerProvider(WithLogger(TestLogger(t))) + provider, err := testcontainers.NewDockerProvider(testcontainers.WithLogger(testcontainers.TestLogger(t))) require.NoError(t, err) defer provider.Close() - c1, err := GenericContainer(ctx, GenericContainerRequest{ + c1, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Name: "test", Image: "nginx:1.17.6", WaitingFor: wait.ForExposedPort(), @@ -1986,9 +1958,9 @@ func TestDockerProviderFindContainerByName(t *testing.T) { require.NoError(t, err) terminateContainerOnEnd(t, ctx, c1) - c2, err := GenericContainer(ctx, GenericContainerRequest{ + c2, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Name: "test2", Image: "nginx:1.17.6", WaitingFor: wait.ForExposedPort(), @@ -1998,10 +1970,15 @@ func TestDockerProviderFindContainerByName(t *testing.T) { require.NoError(t, err) terminateContainerOnEnd(t, ctx, c2) - c, err := provider.findContainerByName(ctx, "test") + c, err := provider.ReuseOrCreateContainer(ctx, testcontainers.ContainerRequest{ + Name: "test", + }) + require.NoError(t, err) + + foundName, err := c.Name(ctx) require.NoError(t, err) require.NotNil(t, c) - assert.Contains(t, c.Names, c1Name) + assert.Contains(t, foundName, c1Name) } func TestImageBuiltFromDockerfile_KeepBuiltImage(t *testing.T) { @@ -2016,15 +1993,15 @@ func TestImageBuiltFromDockerfile_KeepBuiltImage(t *testing.T) { t.Run(fmt.Sprintf("Keep built image: %t", tt.keepBuiltImage), func(t *testing.T) { ctx := context.Background() // Set up CLI. - provider, err := NewDockerProvider() + provider, err := testcontainers.NewDockerProvider() require.NoError(t, err, "get docker provider should not fail") defer func() { _ = provider.Close() }() cli := provider.Client() // Create container. - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ProviderType: providerType, - ContainerRequest: ContainerRequest{ - FromDockerfile: FromDockerfile{ + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "testdata", Dockerfile: "echo.Dockerfile", KeepImage: tt.keepBuiltImage, diff --git a/lifecycle.go b/lifecycle.go index 4a10a90842..8d250ff892 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -112,8 +112,8 @@ var DefaultLoggingHook = func(logger Logging) ContainerLifecycleHooks { } } -// defaultPreCreateHook is a hook that will apply the default configuration to the container -var defaultPreCreateHook = func(ctx context.Context, p *DockerProvider, req ContainerRequest, dockerInput *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) ContainerLifecycleHooks { +// DefaultPreCreateHook is a hook that will apply the default configuration to the container +func DefaultPreCreateHook(p *DockerProvider, dockerInput *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig) ContainerLifecycleHooks { return ContainerLifecycleHooks{ PreCreates: []ContainerRequestHook{ func(ctx context.Context, req ContainerRequest) error { @@ -125,7 +125,7 @@ var defaultPreCreateHook = func(ctx context.Context, p *DockerProvider, req Cont // defaultCopyFileToContainerHook is a hook that will copy files to the container after it's created // but before it's started -var defaultCopyFileToContainerHook = func(files []ContainerFile) ContainerLifecycleHooks { +func defaultCopyFileToContainerHook(files []ContainerFile) ContainerLifecycleHooks { return ContainerLifecycleHooks{ PostCreates: []ContainerHook{ // copy files to container after it's created @@ -160,7 +160,7 @@ var defaultCopyFileToContainerHook = func(files []ContainerFile) ContainerLifecy } // defaultLogConsumersHook is a hook that will start log consumers after the container is started -var defaultLogConsumersHook = func(cfg *LogConsumerConfig) ContainerLifecycleHooks { +func defaultLogConsumersHook(cfg *LogConsumerConfig) ContainerLifecycleHooks { return ContainerLifecycleHooks{ PostStarts: []ContainerHook{ // first post-start hook is to produce logs and start log consumers @@ -197,7 +197,7 @@ var defaultLogConsumersHook = func(cfg *LogConsumerConfig) ContainerLifecycleHoo } // defaultReadinessHook is a hook that will wait for the container to be ready -var defaultReadinessHook = func() ContainerLifecycleHooks { +func defaultReadinessHook() ContainerLifecycleHooks { return ContainerLifecycleHooks{ PostStarts: []ContainerHook{ // wait for the container to be ready diff --git a/lifecycle_hooks_test.go b/lifecycle_hooks_test.go new file mode 100644 index 0000000000..a5d15ba51f --- /dev/null +++ b/lifecycle_hooks_test.go @@ -0,0 +1,247 @@ +// This test is testing very internal logic that should not be exported away from this package. We'll +// leave it in the main testcontainers package. Do not use for user facing examples. +package testcontainers + +import ( + "bufio" + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go/wait" +) + +func TestCombineLifecycleHooks(t *testing.T) { + prints := []string{} + + preCreateFunc := func(prefix string, hook string, lifecycleID int, hookID int) func(ctx context.Context, req ContainerRequest) error { + return func(ctx context.Context, _ ContainerRequest) error { + prints = append(prints, fmt.Sprintf("[%s] pre-%s hook %d.%d", prefix, hook, lifecycleID, hookID)) + return nil + } + } + hookFunc := func(prefix string, hookType string, hook string, lifecycleID int, hookID int) func(ctx context.Context, c Container) error { + return func(ctx context.Context, _ Container) error { + prints = append(prints, fmt.Sprintf("[%s] %s-%s hook %d.%d", prefix, hookType, hook, lifecycleID, hookID)) + return nil + } + } + preFunc := func(prefix string, hook string, lifecycleID int, hookID int) func(ctx context.Context, c Container) error { + return hookFunc(prefix, "pre", hook, lifecycleID, hookID) + } + postFunc := func(prefix string, hook string, lifecycleID int, hookID int) func(ctx context.Context, c Container) error { + return hookFunc(prefix, "post", hook, lifecycleID, hookID) + } + + lifecycleHookFunc := func(prefix string, lifecycleID int) ContainerLifecycleHooks { + return ContainerLifecycleHooks{ + PreCreates: []ContainerRequestHook{preCreateFunc(prefix, "create", lifecycleID, 1), preCreateFunc(prefix, "create", lifecycleID, 2)}, + PostCreates: []ContainerHook{postFunc(prefix, "create", lifecycleID, 1), postFunc(prefix, "create", lifecycleID, 2)}, + PreStarts: []ContainerHook{preFunc(prefix, "start", lifecycleID, 1), preFunc(prefix, "start", lifecycleID, 2)}, + PostStarts: []ContainerHook{postFunc(prefix, "start", lifecycleID, 1), postFunc(prefix, "start", lifecycleID, 2)}, + PostReadies: []ContainerHook{postFunc(prefix, "ready", lifecycleID, 1), postFunc(prefix, "ready", lifecycleID, 2)}, + PreStops: []ContainerHook{preFunc(prefix, "stop", lifecycleID, 1), preFunc(prefix, "stop", lifecycleID, 2)}, + PostStops: []ContainerHook{postFunc(prefix, "stop", lifecycleID, 1), postFunc(prefix, "stop", lifecycleID, 2)}, + PreTerminates: []ContainerHook{preFunc(prefix, "terminate", lifecycleID, 1), preFunc(prefix, "terminate", lifecycleID, 2)}, + PostTerminates: []ContainerHook{postFunc(prefix, "terminate", lifecycleID, 1), postFunc(prefix, "terminate", lifecycleID, 2)}, + } + } + + defaultHooks := []ContainerLifecycleHooks{lifecycleHookFunc("default", 1), lifecycleHookFunc("default", 2)} + userDefinedHooks := []ContainerLifecycleHooks{lifecycleHookFunc("user-defined", 1), lifecycleHookFunc("user-defined", 2), lifecycleHookFunc("user-defined", 3)} + + hooks := combineContainerHooks(defaultHooks, userDefinedHooks) + + // call all the hooks in the right order, honouring the lifecycle + + req := ContainerRequest{} + err := hooks.Creating(context.Background())(req) + require.NoError(t, err) + + c := &DockerContainer{} + + err = hooks.Created(context.Background())(c) + require.NoError(t, err) + err = hooks.Starting(context.Background())(c) + require.NoError(t, err) + err = hooks.Started(context.Background())(c) + require.NoError(t, err) + err = hooks.Readied(context.Background())(c) + require.NoError(t, err) + err = hooks.Stopping(context.Background())(c) + require.NoError(t, err) + err = hooks.Stopped(context.Background())(c) + require.NoError(t, err) + err = hooks.Terminating(context.Background())(c) + require.NoError(t, err) + err = hooks.Terminated(context.Background())(c) + require.NoError(t, err) + + // assertions + + // There are 2 default container lifecycle hooks and 3 user-defined container lifecycle hooks. + // Each lifecycle hook has 2 pre-create hooks and 2 post-create hooks. + // That results in 16 hooks per lifecycle (8 defaults + 12 user-defined = 20) + + // There are 5 lifecycles (create, start, ready, stop, terminate), + // but ready has only half of the hooks (it only has post), so we have 90 hooks in total. + assert.Len(t, prints, 90) + + // The order of the hooks is: + // - pre-X hooks: first default (2*2), then user-defined (3*2) + // - post-X hooks: first user-defined (3*2), then default (2*2) + + for i := 0; i < 5; i++ { + var hookType string + // this is the particular order of execution for the hooks + switch i { + case 0: + hookType = "create" + case 1: + hookType = "start" + case 2: + hookType = "ready" + case 3: + hookType = "stop" + case 4: + hookType = "terminate" + } + + initialIndex := i * 20 + if i >= 2 { + initialIndex -= 10 + } + + if hookType != "ready" { + // default pre-hooks: 4 hooks + assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 1.1", hookType), prints[initialIndex]) + assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 1.2", hookType), prints[initialIndex+1]) + assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 2.1", hookType), prints[initialIndex+2]) + assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 2.2", hookType), prints[initialIndex+3]) + + // user-defined pre-hooks: 6 hooks + assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 1.1", hookType), prints[initialIndex+4]) + assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 1.2", hookType), prints[initialIndex+5]) + assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 2.1", hookType), prints[initialIndex+6]) + assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 2.2", hookType), prints[initialIndex+7]) + assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 3.1", hookType), prints[initialIndex+8]) + assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 3.2", hookType), prints[initialIndex+9]) + } + + // user-defined post-hooks: 6 hooks + assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 1.1", hookType), prints[initialIndex+10]) + assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 1.2", hookType), prints[initialIndex+11]) + assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 2.1", hookType), prints[initialIndex+12]) + assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 2.2", hookType), prints[initialIndex+13]) + assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 3.1", hookType), prints[initialIndex+14]) + assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 3.2", hookType), prints[initialIndex+15]) + + // default post-hooks: 4 hooks + assert.Equal(t, fmt.Sprintf("[default] post-%s hook 1.1", hookType), prints[initialIndex+16]) + assert.Equal(t, fmt.Sprintf("[default] post-%s hook 1.2", hookType), prints[initialIndex+17]) + assert.Equal(t, fmt.Sprintf("[default] post-%s hook 2.1", hookType), prints[initialIndex+18]) + assert.Equal(t, fmt.Sprintf("[default] post-%s hook 2.2", hookType), prints[initialIndex+19]) + } +} + +func TestLifecycleHooks_WithMultipleHooks(t *testing.T) { + ctx := context.Background() + + dl := linesTestLogger{} + + req := ContainerRequest{ + Image: nginxAlpineImage, + LifecycleHooks: []ContainerLifecycleHooks{ + DefaultLoggingHook(&dl), + DefaultLoggingHook(&dl), + }, + } + + c, err := GenericContainer(ctx, GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + require.NoError(t, err) + require.NotNil(t, c) + + duration := 1 * time.Second + err = c.Stop(ctx, &duration) + require.NoError(t, err) + + err = c.Start(ctx) + require.NoError(t, err) + + err = c.Terminate(ctx) + require.NoError(t, err) + + require.Len(t, dl.data, 24) +} + +type linesTestLogger struct { + data []string +} + +func (l *linesTestLogger) Printf(format string, args ...interface{}) { + l.data = append(l.data, fmt.Sprintf(format, args...)) +} + +func TestPrintContainerLogsOnError(t *testing.T) { + ctx := context.Background() + + req := ContainerRequest{ + Image: "docker.io/alpine", + Cmd: []string{"echo", "-n", "I am expecting this"}, + WaitingFor: wait.ForLog("I was expecting that").WithStartupTimeout(5 * time.Second), + } + + arrayOfLinesLogger := linesTestLogger{ + data: []string{}, + } + + container, err := GenericContainer(ctx, GenericContainerRequest{ + ProviderType: providerType, + ContainerRequest: req, + Logger: &arrayOfLinesLogger, + Started: true, + }) + // it should fail because the waiting for condition is not met + if err == nil { + t.Fatal(err) + } + terminateContainerOnEnd(t, ctx, container) + + containerLogs, err := container.Logs(ctx) + if err != nil { + t.Fatal(err) + } + defer containerLogs.Close() + + // read container logs line by line, checking that each line is present in the stdout + rd := bufio.NewReader(containerLogs) + for { + line, err := rd.ReadString('\n') + if err != nil { + if err.Error() == "EOF" { + break + } + + t.Fatal("Read Error:", err) + } + + // the last line of the array should contain the line of interest, + // but we are checking all the lines to make sure that is present + found := false + for _, l := range arrayOfLinesLogger.data { + if strings.Contains(l, line) { + found = true + break + } + } + assert.True(t, found, "container log line not found in the output of the logger: %s", line) + } +} diff --git a/lifecycle_test.go b/lifecycle_test.go index 6316df739e..93794938dd 100644 --- a/lifecycle_test.go +++ b/lifecycle_test.go @@ -1,7 +1,6 @@ -package testcontainers +package testcontainers_test import ( - "bufio" "context" "fmt" "strings" @@ -16,29 +15,29 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go/wait" + "github.com/testcontainers/testcontainers-go" ) func TestPreCreateModifierHook(t *testing.T) { ctx := context.Background() - provider, err := NewDockerProvider() + provider, err := testcontainers.NewDockerProvider() require.NoError(t, err) defer provider.Close() t.Run("No exposed ports", func(t *testing.T) { // reqWithModifiers { - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: nginxAlpineImage, // alpine image does expose port 80 ConfigModifier: func(config *container.Config) { config.Env = []string{"a=b"} }, - Mounts: ContainerMounts{ + Mounts: testcontainers.ContainerMounts{ { - Source: DockerVolumeMountSource{ + Source: testcontainers.DockerVolumeMountSource{ Name: "appdata", VolumeOptions: &mount.VolumeOptions{ - Labels: GenericLabels(), + Labels: testcontainers.GenericLabels(), }, }, Target: "/data", @@ -70,7 +69,8 @@ func TestPreCreateModifierHook(t *testing.T) { inputHostConfig := &container.HostConfig{} inputNetworkingConfig := &network.NetworkingConfig{} - err = provider.preCreateContainerHook(ctx, req, inputConfig, inputHostConfig, inputNetworkingConfig) + hooks := testcontainers.DefaultPreCreateHook(provider, inputConfig, inputHostConfig, inputNetworkingConfig) + err = hooks.Creating(ctx)(req) require.NoError(t, err) // assertions @@ -94,7 +94,7 @@ func TestPreCreateModifierHook(t *testing.T) { Source: "appdata", Target: "/data", VolumeOptions: &mount.VolumeOptions{ - Labels: GenericLabels(), + Labels: testcontainers.GenericLabels(), }, }, }, @@ -128,7 +128,7 @@ func TestPreCreateModifierHook(t *testing.T) { }) t.Run("No exposed ports and network mode IsContainer", func(t *testing.T) { - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: nginxAlpineImage, // alpine image does expose port 80 HostConfigModifier: func(hostConfig *container.HostConfig) { hostConfig.PortBindings = nat.PortMap{ @@ -150,7 +150,8 @@ func TestPreCreateModifierHook(t *testing.T) { inputHostConfig := &container.HostConfig{} inputNetworkingConfig := &network.NetworkingConfig{} - err = provider.preCreateContainerHook(ctx, req, inputConfig, inputHostConfig, inputNetworkingConfig) + hooks := testcontainers.DefaultPreCreateHook(provider, inputConfig, inputHostConfig, inputNetworkingConfig) + err = hooks.Creating(ctx)(req) require.NoError(t, err) // assertions @@ -169,7 +170,7 @@ func TestPreCreateModifierHook(t *testing.T) { }) t.Run("Nil hostConfigModifier should apply default host config modifier", func(t *testing.T) { - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: nginxAlpineImage, // alpine image does expose port 80 AutoRemove: true, CapAdd: []string{"addFoo", "addBar"}, @@ -191,7 +192,8 @@ func TestPreCreateModifierHook(t *testing.T) { inputHostConfig := &container.HostConfig{} inputNetworkingConfig := &network.NetworkingConfig{} - err = provider.preCreateContainerHook(ctx, req, inputConfig, inputHostConfig, inputNetworkingConfig) + hooks := testcontainers.DefaultPreCreateHook(provider, inputConfig, inputHostConfig, inputNetworkingConfig) + err = hooks.Creating(ctx)(req) require.NoError(t, err) // assertions @@ -206,7 +208,7 @@ func TestPreCreateModifierHook(t *testing.T) { t.Run("Request contains more than one network including aliases", func(t *testing.T) { networkName := "foo" - net, err := provider.CreateNetwork(ctx, NetworkRequest{ + net, err := provider.CreateNetwork(ctx, testcontainers.NetworkRequest{ Name: networkName, }) require.NoError(t, err) @@ -217,12 +219,12 @@ func TestPreCreateModifierHook(t *testing.T) { } }() - dockerNetwork, err := provider.GetNetwork(ctx, NetworkRequest{ + dockerNetwork, err := provider.GetNetwork(ctx, testcontainers.NetworkRequest{ Name: networkName, }) require.NoError(t, err) - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: nginxAlpineImage, // alpine image does expose port 80 Networks: []string{networkName, "bar"}, NetworkAliases: map[string][]string{ @@ -237,7 +239,8 @@ func TestPreCreateModifierHook(t *testing.T) { inputHostConfig := &container.HostConfig{} inputNetworkingConfig := &network.NetworkingConfig{} - err = provider.preCreateContainerHook(ctx, req, inputConfig, inputHostConfig, inputNetworkingConfig) + hooks := testcontainers.DefaultPreCreateHook(provider, inputConfig, inputHostConfig, inputNetworkingConfig) + err = hooks.Creating(ctx)(req) require.NoError(t, err) // assertions @@ -258,7 +261,7 @@ func TestPreCreateModifierHook(t *testing.T) { t.Run("Request contains more than one network without aliases", func(t *testing.T) { networkName := "foo" - net, err := provider.CreateNetwork(ctx, NetworkRequest{ + net, err := provider.CreateNetwork(ctx, testcontainers.NetworkRequest{ Name: networkName, }) require.NoError(t, err) @@ -269,12 +272,12 @@ func TestPreCreateModifierHook(t *testing.T) { } }() - dockerNetwork, err := provider.GetNetwork(ctx, NetworkRequest{ + dockerNetwork, err := provider.GetNetwork(ctx, testcontainers.NetworkRequest{ Name: networkName, }) require.NoError(t, err) - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: nginxAlpineImage, // alpine image does expose port 80 Networks: []string{networkName, "bar"}, } @@ -286,7 +289,8 @@ func TestPreCreateModifierHook(t *testing.T) { inputHostConfig := &container.HostConfig{} inputNetworkingConfig := &network.NetworkingConfig{} - err = provider.preCreateContainerHook(ctx, req, inputConfig, inputHostConfig, inputNetworkingConfig) + hooks := testcontainers.DefaultPreCreateHook(provider, inputConfig, inputHostConfig, inputNetworkingConfig) + err = hooks.Creating(ctx)(req) require.NoError(t, err) // assertions @@ -305,7 +309,7 @@ func TestPreCreateModifierHook(t *testing.T) { }) t.Run("Request contains exposed port modifiers", func(t *testing.T) { - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: nginxAlpineImage, // alpine image does expose port 80 HostConfigModifier: func(hostConfig *container.HostConfig) { hostConfig.PortBindings = nat.PortMap{ @@ -327,7 +331,8 @@ func TestPreCreateModifierHook(t *testing.T) { inputHostConfig := &container.HostConfig{} inputNetworkingConfig := &network.NetworkingConfig{} - err = provider.preCreateContainerHook(ctx, req, inputConfig, inputHostConfig, inputNetworkingConfig) + hooks := testcontainers.DefaultPreCreateHook(provider, inputConfig, inputHostConfig, inputNetworkingConfig) + err = hooks.Creating(ctx)(req) require.NoError(t, err) // assertions @@ -336,95 +341,6 @@ func TestPreCreateModifierHook(t *testing.T) { }) } -func TestMergePortBindings(t *testing.T) { - type arg struct { - configPortMap nat.PortMap - parsedPortMap nat.PortMap - exposedPorts []string - } - cases := []struct { - name string - arg arg - expected nat.PortMap - }{ - { - name: "empty ports", - arg: arg{ - configPortMap: nil, - parsedPortMap: nil, - exposedPorts: nil, - }, - expected: map[nat.Port][]nat.PortBinding{}, - }, - { - name: "config port map but not exposed", - arg: arg{ - configPortMap: map[nat.Port][]nat.PortBinding{ - "80/tcp": {{HostIP: "1", HostPort: "2"}}, - }, - parsedPortMap: nil, - exposedPorts: nil, - }, - expected: map[nat.Port][]nat.PortBinding{}, - }, - { - name: "parsed port map without config", - arg: arg{ - configPortMap: nil, - parsedPortMap: map[nat.Port][]nat.PortBinding{ - "80/tcp": {{HostIP: "", HostPort: ""}}, - }, - exposedPorts: nil, - }, - expected: map[nat.Port][]nat.PortBinding{ - "80/tcp": {{HostIP: "", HostPort: ""}}, - }, - }, - { - name: "parsed and configured but not exposed", - arg: arg{ - configPortMap: map[nat.Port][]nat.PortBinding{ - "80/tcp": {{HostIP: "1", HostPort: "2"}}, - }, - parsedPortMap: map[nat.Port][]nat.PortBinding{ - "80/tcp": {{HostIP: "", HostPort: ""}}, - }, - exposedPorts: nil, - }, - expected: map[nat.Port][]nat.PortBinding{ - "80/tcp": {{HostIP: "", HostPort: ""}}, - }, - }, - { - name: "merge both parsed and config", - arg: arg{ - configPortMap: map[nat.Port][]nat.PortBinding{ - "60/tcp": {{HostIP: "1", HostPort: "2"}}, - "70/tcp": {{HostIP: "1", HostPort: "2"}}, - "80/tcp": {{HostIP: "1", HostPort: "2"}}, - }, - parsedPortMap: map[nat.Port][]nat.PortBinding{ - "80/tcp": {{HostIP: "", HostPort: ""}}, - "90/tcp": {{HostIP: "", HostPort: ""}}, - }, - exposedPorts: []string{"70", "80"}, - }, - expected: map[nat.Port][]nat.PortBinding{ - "70/tcp": {{HostIP: "1", HostPort: "2"}}, - "80/tcp": {{HostIP: "1", HostPort: "2"}}, - "90/tcp": {{HostIP: "", HostPort: ""}}, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := mergePortBindings(c.arg.configPortMap, c.arg.parsedPortMap, c.arg.exposedPorts) - assert.Equal(t, c.expected, res) - }) - } -} - func TestLifecycleHooks(t *testing.T) { tests := []struct { name string @@ -445,96 +361,96 @@ func TestLifecycleHooks(t *testing.T) { prints := []string{} ctx := context.Background() // reqWithLifecycleHooks { - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: nginxAlpineImage, - LifecycleHooks: []ContainerLifecycleHooks{ + LifecycleHooks: []testcontainers.ContainerLifecycleHooks{ { - PreCreates: []ContainerRequestHook{ - func(ctx context.Context, req ContainerRequest) error { + PreCreates: []testcontainers.ContainerRequestHook{ + func(ctx context.Context, req testcontainers.ContainerRequest) error { prints = append(prints, fmt.Sprintf("pre-create hook 1: %#v", req)) return nil }, - func(ctx context.Context, req ContainerRequest) error { + func(ctx context.Context, req testcontainers.ContainerRequest) error { prints = append(prints, fmt.Sprintf("pre-create hook 2: %#v", req)) return nil }, }, - PostCreates: []ContainerHook{ - func(ctx context.Context, c Container) error { + PostCreates: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("post-create hook 1: %#v", c)) return nil }, - func(ctx context.Context, c Container) error { + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("post-create hook 2: %#v", c)) return nil }, }, - PreStarts: []ContainerHook{ - func(ctx context.Context, c Container) error { + PreStarts: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("pre-start hook 1: %#v", c)) return nil }, - func(ctx context.Context, c Container) error { + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("pre-start hook 2: %#v", c)) return nil }, }, - PostStarts: []ContainerHook{ - func(ctx context.Context, c Container) error { + PostStarts: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("post-start hook 1: %#v", c)) return nil }, - func(ctx context.Context, c Container) error { + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("post-start hook 2: %#v", c)) return nil }, }, - PostReadies: []ContainerHook{ - func(ctx context.Context, c Container) error { + PostReadies: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("post-ready hook 1: %#v", c)) return nil }, - func(ctx context.Context, c Container) error { + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("post-ready hook 2: %#v", c)) return nil }, }, - PreStops: []ContainerHook{ - func(ctx context.Context, c Container) error { + PreStops: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("pre-stop hook 1: %#v", c)) return nil }, - func(ctx context.Context, c Container) error { + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("pre-stop hook 2: %#v", c)) return nil }, }, - PostStops: []ContainerHook{ - func(ctx context.Context, c Container) error { + PostStops: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("post-stop hook 1: %#v", c)) return nil }, - func(ctx context.Context, c Container) error { + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("post-stop hook 2: %#v", c)) return nil }, }, - PreTerminates: []ContainerHook{ - func(ctx context.Context, c Container) error { + PreTerminates: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("pre-terminate hook 1: %#v", c)) return nil }, - func(ctx context.Context, c Container) error { + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("pre-terminate hook 2: %#v", c)) return nil }, }, - PostTerminates: []ContainerHook{ - func(ctx context.Context, c Container) error { + PostTerminates: []testcontainers.ContainerHook{ + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("post-terminate hook 1: %#v", c)) return nil }, - func(ctx context.Context, c Container) error { + func(ctx context.Context, c testcontainers.Container) error { prints = append(prints, fmt.Sprintf("post-terminate hook 2: %#v", c)) return nil }, @@ -548,7 +464,7 @@ func TestLifecycleHooks(t *testing.T) { req.Name = "reuse-container" } - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Reuse: tt.reuse, Started: true, @@ -588,15 +504,15 @@ func TestLifecycleHooks_WithDefaultLogger(t *testing.T) { // reqWithDefaultLogginHook { dl := inMemoryLogger{} - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: nginxAlpineImage, - LifecycleHooks: []ContainerLifecycleHooks{ - DefaultLoggingHook(&dl), + LifecycleHooks: []testcontainers.ContainerLifecycleHooks{ + testcontainers.DefaultLoggingHook(&dl), }, } // } - c, err := GenericContainer(ctx, GenericContainerRequest{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) @@ -616,236 +532,6 @@ func TestLifecycleHooks_WithDefaultLogger(t *testing.T) { require.Len(t, dl.data, 12) } -func TestCombineLifecycleHooks(t *testing.T) { - prints := []string{} - - preCreateFunc := func(prefix string, hook string, lifecycleID int, hookID int) func(ctx context.Context, req ContainerRequest) error { - return func(ctx context.Context, _ ContainerRequest) error { - prints = append(prints, fmt.Sprintf("[%s] pre-%s hook %d.%d", prefix, hook, lifecycleID, hookID)) - return nil - } - } - hookFunc := func(prefix string, hookType string, hook string, lifecycleID int, hookID int) func(ctx context.Context, c Container) error { - return func(ctx context.Context, _ Container) error { - prints = append(prints, fmt.Sprintf("[%s] %s-%s hook %d.%d", prefix, hookType, hook, lifecycleID, hookID)) - return nil - } - } - preFunc := func(prefix string, hook string, lifecycleID int, hookID int) func(ctx context.Context, c Container) error { - return hookFunc(prefix, "pre", hook, lifecycleID, hookID) - } - postFunc := func(prefix string, hook string, lifecycleID int, hookID int) func(ctx context.Context, c Container) error { - return hookFunc(prefix, "post", hook, lifecycleID, hookID) - } - - lifecycleHookFunc := func(prefix string, lifecycleID int) ContainerLifecycleHooks { - return ContainerLifecycleHooks{ - PreCreates: []ContainerRequestHook{preCreateFunc(prefix, "create", lifecycleID, 1), preCreateFunc(prefix, "create", lifecycleID, 2)}, - PostCreates: []ContainerHook{postFunc(prefix, "create", lifecycleID, 1), postFunc(prefix, "create", lifecycleID, 2)}, - PreStarts: []ContainerHook{preFunc(prefix, "start", lifecycleID, 1), preFunc(prefix, "start", lifecycleID, 2)}, - PostStarts: []ContainerHook{postFunc(prefix, "start", lifecycleID, 1), postFunc(prefix, "start", lifecycleID, 2)}, - PostReadies: []ContainerHook{postFunc(prefix, "ready", lifecycleID, 1), postFunc(prefix, "ready", lifecycleID, 2)}, - PreStops: []ContainerHook{preFunc(prefix, "stop", lifecycleID, 1), preFunc(prefix, "stop", lifecycleID, 2)}, - PostStops: []ContainerHook{postFunc(prefix, "stop", lifecycleID, 1), postFunc(prefix, "stop", lifecycleID, 2)}, - PreTerminates: []ContainerHook{preFunc(prefix, "terminate", lifecycleID, 1), preFunc(prefix, "terminate", lifecycleID, 2)}, - PostTerminates: []ContainerHook{postFunc(prefix, "terminate", lifecycleID, 1), postFunc(prefix, "terminate", lifecycleID, 2)}, - } - } - - defaultHooks := []ContainerLifecycleHooks{lifecycleHookFunc("default", 1), lifecycleHookFunc("default", 2)} - userDefinedHooks := []ContainerLifecycleHooks{lifecycleHookFunc("user-defined", 1), lifecycleHookFunc("user-defined", 2), lifecycleHookFunc("user-defined", 3)} - - hooks := combineContainerHooks(defaultHooks, userDefinedHooks) - - // call all the hooks in the right order, honouring the lifecycle - - req := ContainerRequest{} - err := hooks.Creating(context.Background())(req) - require.NoError(t, err) - - c := &DockerContainer{} - - err = hooks.Created(context.Background())(c) - require.NoError(t, err) - err = hooks.Starting(context.Background())(c) - require.NoError(t, err) - err = hooks.Started(context.Background())(c) - require.NoError(t, err) - err = hooks.Readied(context.Background())(c) - require.NoError(t, err) - err = hooks.Stopping(context.Background())(c) - require.NoError(t, err) - err = hooks.Stopped(context.Background())(c) - require.NoError(t, err) - err = hooks.Terminating(context.Background())(c) - require.NoError(t, err) - err = hooks.Terminated(context.Background())(c) - require.NoError(t, err) - - // assertions - - // There are 2 default container lifecycle hooks and 3 user-defined container lifecycle hooks. - // Each lifecycle hook has 2 pre-create hooks and 2 post-create hooks. - // That results in 16 hooks per lifecycle (8 defaults + 12 user-defined = 20) - - // There are 5 lifecycles (create, start, ready, stop, terminate), - // but ready has only half of the hooks (it only has post), so we have 90 hooks in total. - assert.Len(t, prints, 90) - - // The order of the hooks is: - // - pre-X hooks: first default (2*2), then user-defined (3*2) - // - post-X hooks: first user-defined (3*2), then default (2*2) - - for i := 0; i < 5; i++ { - var hookType string - // this is the particular order of execution for the hooks - switch i { - case 0: - hookType = "create" - case 1: - hookType = "start" - case 2: - hookType = "ready" - case 3: - hookType = "stop" - case 4: - hookType = "terminate" - } - - initialIndex := i * 20 - if i >= 2 { - initialIndex -= 10 - } - - if hookType != "ready" { - // default pre-hooks: 4 hooks - assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 1.1", hookType), prints[initialIndex]) - assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 1.2", hookType), prints[initialIndex+1]) - assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 2.1", hookType), prints[initialIndex+2]) - assert.Equal(t, fmt.Sprintf("[default] pre-%s hook 2.2", hookType), prints[initialIndex+3]) - - // user-defined pre-hooks: 6 hooks - assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 1.1", hookType), prints[initialIndex+4]) - assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 1.2", hookType), prints[initialIndex+5]) - assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 2.1", hookType), prints[initialIndex+6]) - assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 2.2", hookType), prints[initialIndex+7]) - assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 3.1", hookType), prints[initialIndex+8]) - assert.Equal(t, fmt.Sprintf("[user-defined] pre-%s hook 3.2", hookType), prints[initialIndex+9]) - } - - // user-defined post-hooks: 6 hooks - assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 1.1", hookType), prints[initialIndex+10]) - assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 1.2", hookType), prints[initialIndex+11]) - assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 2.1", hookType), prints[initialIndex+12]) - assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 2.2", hookType), prints[initialIndex+13]) - assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 3.1", hookType), prints[initialIndex+14]) - assert.Equal(t, fmt.Sprintf("[user-defined] post-%s hook 3.2", hookType), prints[initialIndex+15]) - - // default post-hooks: 4 hooks - assert.Equal(t, fmt.Sprintf("[default] post-%s hook 1.1", hookType), prints[initialIndex+16]) - assert.Equal(t, fmt.Sprintf("[default] post-%s hook 1.2", hookType), prints[initialIndex+17]) - assert.Equal(t, fmt.Sprintf("[default] post-%s hook 2.1", hookType), prints[initialIndex+18]) - assert.Equal(t, fmt.Sprintf("[default] post-%s hook 2.2", hookType), prints[initialIndex+19]) - } -} - -func TestLifecycleHooks_WithMultipleHooks(t *testing.T) { - ctx := context.Background() - - dl := inMemoryLogger{} - - req := ContainerRequest{ - Image: nginxAlpineImage, - LifecycleHooks: []ContainerLifecycleHooks{ - DefaultLoggingHook(&dl), - DefaultLoggingHook(&dl), - }, - } - - c, err := GenericContainer(ctx, GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) - require.NoError(t, err) - require.NotNil(t, c) - - duration := 1 * time.Second - err = c.Stop(ctx, &duration) - require.NoError(t, err) - - err = c.Start(ctx) - require.NoError(t, err) - - err = c.Terminate(ctx) - require.NoError(t, err) - - require.Len(t, dl.data, 24) -} - -type linesTestLogger struct { - data []string -} - -func (l *linesTestLogger) Printf(format string, args ...interface{}) { - l.data = append(l.data, fmt.Sprintf(format, args...)) -} - -func TestPrintContainerLogsOnError(t *testing.T) { - ctx := context.Background() - - req := ContainerRequest{ - Image: "docker.io/alpine", - Cmd: []string{"echo", "-n", "I am expecting this"}, - WaitingFor: wait.ForLog("I was expecting that").WithStartupTimeout(5 * time.Second), - } - - arrayOfLinesLogger := linesTestLogger{ - data: []string{}, - } - - container, err := GenericContainer(ctx, GenericContainerRequest{ - ProviderType: providerType, - ContainerRequest: req, - Logger: &arrayOfLinesLogger, - Started: true, - }) - // it should fail because the waiting for condition is not met - if err == nil { - t.Fatal(err) - } - terminateContainerOnEnd(t, ctx, container) - - containerLogs, err := container.Logs(ctx) - if err != nil { - t.Fatal(err) - } - defer containerLogs.Close() - - // read container logs line by line, checking that each line is present in the stdout - rd := bufio.NewReader(containerLogs) - for { - line, err := rd.ReadString('\n') - if err != nil { - if err.Error() == "EOF" { - break - } - - t.Fatal("Read Error:", err) - } - - // the last line of the array should contain the line of interest, - // but we are checking all the lines to make sure that is present - found := false - for _, l := range arrayOfLinesLogger.data { - if strings.Contains(l, line) { - found = true - break - } - } - assert.True(t, found, "container log line not found in the output of the logger: %s", line) - } -} - func lifecycleHooksIsHonouredFn(t *testing.T, ctx context.Context, prints []string) { require.Len(t, prints, 24) diff --git a/provider.go b/provider.go index 1be1ebe20a..7e10be4808 100644 --- a/provider.go +++ b/provider.go @@ -7,6 +7,8 @@ import ( "os" "strings" + "github.com/docker/docker/client" + "github.com/testcontainers/testcontainers-go/internal/core" ) @@ -38,7 +40,7 @@ type ( // DockerProviderOptions defines options applicable to DockerProvider DockerProviderOptions struct { - defaultBridgeNetworkName string + DefaultBridgeNetworkName string *GenericProviderOptions } @@ -74,7 +76,7 @@ func Generic2DockerOptions(opts ...GenericProviderOption) []DockerProviderOption func WithDefaultBridgeNetwork(bridgeNetworkName string) DockerProviderOption { return DockerProviderOptionFunc(func(opts *DockerProviderOptions) { - opts.defaultBridgeNetworkName = bridgeNetworkName + opts.DefaultBridgeNetworkName = bridgeNetworkName }) } @@ -128,6 +130,12 @@ func (t ProviderType) GetProvider(opts ...GenericProviderOption) (GenericProvide // NewDockerProvider creates a Docker provider with the EnvClient func NewDockerProvider(provOpts ...DockerProviderOption) (*DockerProvider, error) { + return NewDockerProviderWithClientOpts([]client.Opt{}, provOpts...) +} + +// NewDockerProviderWithClientOpts creates a Docker provider with the EnvClient using the provided client opts +// for the docker client. +func NewDockerProviderWithClientOpts(clientOpts []client.Opt, provOpts ...DockerProviderOption) (*DockerProvider, error) { o := &DockerProviderOptions{ GenericProviderOptions: &GenericProviderOptions{ Logger: Logger, @@ -139,7 +147,7 @@ func NewDockerProvider(provOpts ...DockerProviderOption) (*DockerProvider, error } ctx := context.Background() - c, err := NewDockerClientWithOpts(ctx) + c, err := NewDockerClientWithOpts(ctx, clientOpts...) if err != nil { return nil, err } diff --git a/provider_test.go b/provider_test.go index 2448d84c07..39b67f38a9 100644 --- a/provider_test.go +++ b/provider_test.go @@ -1,9 +1,10 @@ -package testcontainers +package testcontainers_test import ( "context" "testing" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/internal/core" ) @@ -13,52 +14,52 @@ func TestProviderTypeGetProviderAutodetect(t *testing.T) { tests := []struct { name string - tr ProviderType + tr testcontainers.ProviderType DockerHost string want string wantErr bool }{ { name: "default provider without podman.socket", - tr: ProviderDefault, + tr: testcontainers.ProviderDefault, DockerHost: dockerHost, - want: Bridge, + want: testcontainers.Bridge, }, { name: "default provider with podman.socket", - tr: ProviderDefault, + tr: testcontainers.ProviderDefault, DockerHost: podmanSocket, - want: Podman, + want: testcontainers.Podman, }, { name: "docker provider without podman.socket", - tr: ProviderDocker, + tr: testcontainers.ProviderDocker, DockerHost: dockerHost, - want: Bridge, + want: testcontainers.Bridge, }, { // Explicitly setting Docker provider should not be overridden by auto-detect name: "docker provider with podman.socket", - tr: ProviderDocker, + tr: testcontainers.ProviderDocker, DockerHost: podmanSocket, - want: Bridge, + want: testcontainers.Bridge, }, { name: "Podman provider without podman.socket", - tr: ProviderPodman, + tr: testcontainers.ProviderPodman, DockerHost: dockerHost, - want: Podman, + want: testcontainers.Podman, }, { name: "Podman provider with podman.socket", - tr: ProviderPodman, + tr: testcontainers.ProviderPodman, DockerHost: podmanSocket, - want: Podman, + want: testcontainers.Podman, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.tr == ProviderPodman && core.IsWindows() { + if tt.tr == testcontainers.ProviderPodman && core.IsWindows() { t.Skip("Podman provider is not implemented for Windows") } @@ -69,12 +70,12 @@ func TestProviderTypeGetProviderAutodetect(t *testing.T) { t.Errorf("ProviderType.GetProvider() error = %v, wantErr %v", err, tt.wantErr) return } - provider, ok := got.(*DockerProvider) + provider, ok := got.(*testcontainers.DockerProvider) if !ok { - t.Fatalf("ProviderType.GetProvider() = %T, want %T", got, &DockerProvider{}) + t.Fatalf("ProviderType.GetProvider() = %T, want %T", got, &testcontainers.DockerProvider{}) } - if provider.defaultBridgeNetworkName != tt.want { - t.Errorf("ProviderType.GetProvider() = %v, want %v", provider.defaultBridgeNetworkName, tt.want) + if provider.DefaultBridgeNetworkName != tt.want { + t.Errorf("ProviderType.GetProvider() = %v, want %v", provider.DefaultBridgeNetworkName, tt.want) } }) } diff --git a/reaper_test.go b/reaper_test.go index d0b6f4fc7c..1385f7f65c 100644 --- a/reaper_test.go +++ b/reaper_test.go @@ -1,9 +1,12 @@ +// This test is testing very internal logic that should not be exported away from this package. We'll +// leave it in the main testcontainers package. Do not use for user facing examples. package testcontainers import ( "context" "errors" "os" + "strings" "sync" "testing" "time" @@ -19,6 +22,32 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) +const ( + nginxAlpineImage = "docker.io/nginx:alpine" + nginxDelayedImage = "docker.io/menedev/delayed-nginx:1.15.2" + nginxDefaultPort = "80/tcp" + daemonMaxVersion = "1.41" +) + +var providerType = ProviderDocker + +func init() { + if strings.Contains(os.Getenv("DOCKER_HOST"), "podman.sock") { + providerType = ProviderPodman + } +} + +func terminateContainerOnEnd(tb testing.TB, ctx context.Context, ctr Container) { + tb.Helper() + if ctr == nil { + return + } + tb.Cleanup(func() { + tb.Log("terminating container") + require.NoError(tb, ctr.Terminate(ctx)) + }) +} + // testSessionID the tests need to create a reaper in a different session, so that it does not interfere with other tests const testSessionID = "this-is-a-different-session-id" diff --git a/testhelpers_test.go b/testhelpers_test.go index 47bbcb54c3..dc18c13ee7 100644 --- a/testhelpers_test.go +++ b/testhelpers_test.go @@ -2,6 +2,8 @@ package testcontainers_test import ( "context" + "os" + "strings" "testing" "github.com/stretchr/testify/require" @@ -10,10 +12,22 @@ import ( ) const ( - nginxAlpineImage = "docker.io/nginx:alpine" - nginxDefaultPort = "80/tcp" + mysqlImage = "docker.io/mysql:8.0.36" + nginxDelayedImage = "docker.io/menedev/delayed-nginx:1.15.2" + nginxImage = "docker.io/nginx" + nginxAlpineImage = "docker.io/nginx:alpine" + nginxDefaultPort = "80/tcp" + nginxHighPort = "8080/tcp" ) +var providerType = testcontainers.ProviderDocker + +func init() { + if strings.Contains(os.Getenv("DOCKER_HOST"), "podman.sock") { + providerType = testcontainers.ProviderPodman + } +} + func terminateContainerOnEnd(tb testing.TB, ctx context.Context, ctr testcontainers.Container) { tb.Helper() if ctr == nil {