From 0d47b42c8555c27db4f08a97b49d03391a24dc1a Mon Sep 17 00:00:00 2001 From: ^_^void Date: Thu, 25 May 2023 01:07:44 +0800 Subject: [PATCH] #1152 execute HostConfigModifier at last (#1153) * #1152 execute HostConfigModifier at last #1152 execute HostConfigModifier at last, or some values will be overwrite * add UT: Request contains exposed port modifiers * merge nat.ParsePortSpecs and hostConfig.PortBindings * update UT * clean code * add case to UT * update PortBinding logic --- go.mod | 5 +- go.sum | 9 ++-- lifecycle.go | 23 ++++++++- lifecycle_test.go | 120 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 331b9de5d9..0b50f39009 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f github.com/opencontainers/image-spec v1.1.0-rc2 github.com/stretchr/testify v1.8.2 + golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea golang.org/x/sys v0.7.0 gotest.tools/gotestsum v1.10.0 ) @@ -45,12 +46,12 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/sirupsen/logrus v1.9.0 // indirect - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/mod v0.6.0 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/term v0.5.0 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect - golang.org/x/tools v0.1.12 // indirect + golang.org/x/tools v0.2.0 // indirect google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad // indirect google.golang.org/grpc v1.47.0 // indirect google.golang.org/protobuf v1.28.0 // indirect diff --git a/go.sum b/go.sum index 1f636b15e3..13efda0064 100644 --- a/go.sum +++ b/go.sum @@ -177,13 +177,16 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -263,8 +266,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/lifecycle.go b/lifecycle.go index 9bbecc62ca..600adbe4c7 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -2,10 +2,12 @@ package testcontainers import ( "context" + "strings" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/go-connections/nat" + "golang.org/x/exp/slices" ) // ContainerRequestHook is a hook that will be called before a container is created. @@ -318,11 +320,30 @@ func (p *DockerProvider) preCreateContainerHook(ctx context.Context, req Contain } dockerInput.ExposedPorts = exposedPortSet - hostConfig.PortBindings = exposedPortMap + + // only exposing those ports automatically if the container request exposes zero ports and the container does not run in a container network + if len(exposedPorts) == 0 && !hostConfig.NetworkMode.IsContainer() { + hostConfig.PortBindings = exposedPortMap + } else { + hostConfig.PortBindings = mergePortBindings(hostConfig.PortBindings, exposedPortMap, req.ExposedPorts) + } return nil } +func mergePortBindings(configPortMap, exposedPortMap nat.PortMap, exposedPorts []string) nat.PortMap { + if exposedPortMap == nil { + exposedPortMap = make(map[nat.Port][]nat.PortBinding) + } + + for k, v := range configPortMap { + if slices.Contains(exposedPorts, strings.Split(string(k), "/")[0]) { + exposedPortMap[k] = v + } + } + return exposedPortMap +} + // defaultHostConfigModifier provides a default modifier including the deprecated fields func defaultHostConfigModifier(req ContainerRequest) func(hostConfig *container.HostConfig) { return func(hostConfig *container.HostConfig) { diff --git a/lifecycle_test.go b/lifecycle_test.go index 092d2c9f61..2b5db5fcaa 100644 --- a/lifecycle_test.go +++ b/lifecycle_test.go @@ -300,6 +300,126 @@ func TestPreCreateModifierHook(t *testing.T) { "Networking config's network ID should be retrieved from Docker", ) }) + + t.Run("Request contains exposed port modifiers", func(t *testing.T) { + req := ContainerRequest{ + Image: nginxAlpineImage, // alpine image does expose port 80 + HostConfigModifier: func(hostConfig *container.HostConfig) { + hostConfig.PortBindings = nat.PortMap{ + "80/tcp": []nat.PortBinding{ + { + HostIP: "localhost", + HostPort: "8080", + }, + }, + } + }, + ExposedPorts: []string{"80"}, + } + + // define empty inputs to be overwritten by the pre create hook + inputConfig := &container.Config{ + Image: req.Image, + } + inputHostConfig := &container.HostConfig{} + inputNetworkingConfig := &network.NetworkingConfig{} + + err = provider.preCreateContainerHook(ctx, req, inputConfig, inputHostConfig, inputNetworkingConfig) + require.Nil(t, err) + + // assertions + assert.Equal(t, inputHostConfig.PortBindings["80/tcp"][0].HostIP, "localhost") + assert.Equal(t, inputHostConfig.PortBindings["80/tcp"][0].HostPort, "8080") + }) +} + +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) {