Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add network option #1028

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 49 additions & 1 deletion docker.go
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/go-connections/nat"
Expand Down Expand Up @@ -116,6 +117,19 @@ func (d *docker) pullImage(ctx context.Context, image string, cfg *Options) erro
return nil
}

func (d *docker) startNetwork(ctx context.Context, name string) (string, error) {
resp, err := d.client.NetworkCreate(ctx, name, types.NetworkCreate{})
if err != nil {
return "", fmt.Errorf("can't create network: %w", err)
}

return resp.ID, nil
}

func (d *docker) stopNetwork(ctx context.Context, nwID string) error {
return d.client.NetworkRemove(ctx, nwID)
}

func (d *docker) startContainer(ctx context.Context, image string, ports NamedPorts, cfg *Options) (*Container, error) {
if cfg.Reuse {
container, ok, err := d.findReusableContainer(ctx, image, ports, cfg)
Expand Down Expand Up @@ -339,7 +353,12 @@ func (d *docker) createContainer(
ExtraHosts: cfg.ExtraHosts,
}

resp, err := d.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, cfg.ContainerName)
nwConfig, err := d.findNetworkingConfig(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("can't find network: %w", err)
}

resp, err := d.client.ContainerCreate(ctx, containerConfig, hostConfig, nwConfig, nil, cfg.ContainerName)
if err == nil {
return &resp, nil
}
Expand All @@ -361,6 +380,35 @@ func (d *docker) createContainer(
return &resp, err
}

// findNetworkingConfig finds the network associated with the network ID and
// builds a NetworkingConfig with it if a network ID is provided.
func (d *docker) findNetworkingConfig(ctx context.Context, cfg *Options) (*network.NetworkingConfig, error) {
if cfg.NetworkID == "" {
return nil, nil
}

nws, err := d.client.NetworkList(ctx, types.NetworkListOptions{})
if err != nil {
return nil, fmt.Errorf("can't list networks: %w", err)
}

for _, nw := range nws {
if cfg.NetworkID == nw.ID {
nwConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
nw.Name: {
NetworkID: nw.ID,
},
},
}

return nwConfig, nil
}
}

return nil, fmt.Errorf("network '%s' does not exist", cfg.NetworkID)
}

func (d *docker) findReusableContainer(
ctx context.Context,
image string,
Expand Down
45 changes: 45 additions & 0 deletions gnomock.go
Expand Up @@ -341,3 +341,48 @@ func isHostDockerInternalAvailable() bool {

return err == nil
}

// StartNetwork creates a new network. The returned string is the ID of the
// created network.
func StartNetwork(ctx context.Context, name string) (string, error) {
g, err := newG(false)
if err != nil {
return "", fmt.Errorf("can't create new gnomock session: %w", err)
}

defer func() { _ = g.log.Sync() }()

cli, err := g.dockerConnect()
if err != nil {
return "", fmt.Errorf("can't create docker client: %w", err)
}

return cli.startNetwork(ctx, name)
}

// StopNetwork removes the networks associated with the provided network IDs.
func StopNetwork(ctx context.Context, nwIDs ...string) (err error) {
g, err := newG(false)
if err != nil {
return fmt.Errorf("can't create new gnomock session: %w", err)
}

defer func() { _ = g.log.Sync() }()

cli, err := g.dockerConnect()
if err != nil {
return fmt.Errorf("can't create docker client: %w", err)
}

var eg errgroup.Group

for _, n := range nwIDs {
nwID := n

eg.Go(func() error {
return cli.stopNetwork(ctx, nwID)
})
}

return eg.Wait()
}
81 changes: 81 additions & 0 deletions gnomock_test.go
Expand Up @@ -365,6 +365,87 @@ func TestGnomock_withExtraHosts(t *testing.T) {
require.NoError(t, gnomock.Stop(container))
}

func TestGnomock_withNetworkID(t *testing.T) {
t.Parallel()

ctx := context.Background()

nwID, err := gnomock.StartNetwork(ctx, "test-nw")
require.NoError(t, err)

defer func() { require.NoError(t, gnomock.StopNetwork(ctx, nwID)) }()

namedPorts := gnomock.NamedPorts{
"web80": gnomock.TCP(testutil.GoodPort80),
"web8080": gnomock.TCP(testutil.GoodPort8080),
}

c1, err := gnomock.StartCustom(
testutil.TestImage, namedPorts,
gnomock.WithContainerName("container1"),
gnomock.WithHealthCheckInterval(time.Microsecond*500),
gnomock.WithHealthCheck(testutil.Healthcheck),
gnomock.WithInit(initf),
gnomock.WithContext(context.Background()),
gnomock.WithTimeout(time.Minute),
gnomock.WithEnv("GNOMOCK_TEST_1=foo"),
gnomock.WithEnv("GNOMOCK_TEST_2=bar"),
gnomock.WithEnv("GNOMOCK_REQUEST_TARGET=http://container2:80"),
gnomock.WithNetworkID(nwID),
gnomock.WithRegistryAuth(""),
)
require.NoError(t, err)
require.NotNil(t, c1)

c2, err := gnomock.StartCustom(
testutil.TestImage, namedPorts,
gnomock.WithContainerName("container2"),
gnomock.WithHealthCheckInterval(time.Microsecond*500),
gnomock.WithHealthCheck(testutil.Healthcheck),
gnomock.WithInit(initf),
gnomock.WithContext(context.Background()),
gnomock.WithTimeout(time.Minute),
gnomock.WithEnv("GNOMOCK_TEST_1=foo"),
gnomock.WithEnv("GNOMOCK_TEST_2=bar"),
gnomock.WithEnv("GNOMOCK_REQUEST_TARGET=http://container1:80"),
gnomock.WithNetworkID(nwID),
gnomock.WithRegistryAuth(""),
)
require.NoError(t, err)
require.NotNil(t, c2)

t.Run("container1 makes request to container2", func(t *testing.T) {
// The /request endpoint only forwards the status code so we expect 200 w/ an empty body.
addr := fmt.Sprintf("http://%s/request", c1.Address("web80"))
requireResponse(t, addr, "")
})

t.Run("container2 makes request to container1", func(t *testing.T) {
// The /request endpoint only forwards the status code so we expect 200 w/ an empty body.
addr := fmt.Sprintf("http://%s/request", c2.Address("web80"))
requireResponse(t, addr, "")
})

t.Run("add non-existent network", func(t *testing.T) {
_, err = gnomock.StartCustom(
testutil.TestImage, namedPorts,
gnomock.WithContainerName("container3"),
gnomock.WithHealthCheckInterval(time.Microsecond*500),
gnomock.WithHealthCheck(testutil.Healthcheck),
gnomock.WithInit(initf),
gnomock.WithContext(context.Background()),
gnomock.WithTimeout(time.Minute),
gnomock.WithEnv("GNOMOCK_TEST_1=foo"),
gnomock.WithEnv("GNOMOCK_TEST_2=bar"),
gnomock.WithNetworkID("not-a-real-network-id"),
gnomock.WithRegistryAuth(""),
)
require.ErrorContains(t, err, "network 'not-a-real-network-id' does not exist")
})

require.NoError(t, gnomock.Stop(c1, c2))
}

func initf(context.Context, *gnomock.Container) error {
return nil
}
Expand Down
12 changes: 12 additions & 0 deletions options.go
Expand Up @@ -233,6 +233,15 @@ func nopInit(context.Context, *Container) error {
return nil
}

// WithNetworkID allows to specify a custom network to attach the container to
// by providing the network ID of a network that has already been created.
// See StartNetwork and StopNetwork for more details.
func WithNetworkID(nwID string) Option {
return func(o *Options) {
o.NetworkID = nwID
}
}

// Options includes Gnomock startup configuration. Functional options
// (WithSomething) should be used instead of directly initializing objects of
// this type whenever possible.
Expand Down Expand Up @@ -307,6 +316,9 @@ type Options struct {
// its re-use in posterior executions.
Reuse bool `json:"reuse"`

// NetworkID is the ID of a custom network the container should attach to.
NetworkID string `json:"networkID"`

ctx context.Context
init InitFunc
healthcheck HealthcheckFunc
Expand Down