diff --git a/container.go b/container.go index e7b96787e7..71e8dc0b72 100644 --- a/container.go +++ b/container.go @@ -37,20 +37,24 @@ type Container interface { Terminate(context.Context) error // terminate the container Logs(context.Context) (io.ReadCloser, error) // Get logs of the container Name(context.Context) (string, error) // get container name + Networks(context.Context) ([]string, error) // get container networks + NetworkAliases(context.Context) (map[string][]string, error) // get container network aliases for a network } // ContainerRequest represents the parameters used to get a running container type ContainerRequest struct { - Image string - Env map[string]string - ExposedPorts []string // allow specifying protocol info - Cmd string - Labels map[string]string - BindMounts map[string]string - RegistryCred string - WaitingFor wait.Strategy - Name string // for specifying container name - Privileged bool // for starting privileged container + Image string + Env map[string]string + ExposedPorts []string // allow specifying protocol info + Cmd string + Labels map[string]string + BindMounts map[string]string + RegistryCred string + WaitingFor wait.Strategy + Name string // for specifying container name + Privileged bool // for starting privileged container + Networks []string // for specifying network names + NetworkAliases map[string][]string // for specifying network aliases SkipReaper bool // indicates whether we skip setting up a reaper for this } @@ -64,7 +68,7 @@ const ( ) // GetProvider provides the provider implementation for a certain type -func (t ProviderType) GetProvider() (ContainerProvider, error) { +func (t ProviderType) GetProvider() (GenericProvider, error) { switch t { case ProviderDocker: provider, err := NewDockerProvider() diff --git a/docker.go b/docker.go index f8e1c2dc87..c05ed1540f 100644 --- a/docker.go +++ b/docker.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" @@ -185,6 +186,59 @@ func (c *DockerContainer) Name(ctx context.Context) (string, error) { return inspect.Name, nil } +// Networks gets the names of the networks the container is attached to. +func (c *DockerContainer) Networks(ctx context.Context) ([]string, error) { + inspect, err := c.inspectContainer(ctx) + if err != nil { + return []string{}, err + } + + networks := inspect.NetworkSettings.Networks + + n := []string{} + + for k := range networks { + n = append(n, k) + } + + return n, nil +} + +// NetworkAliases gets the aliases of the container for the networks it is attached to. +func (c *DockerContainer) NetworkAliases(ctx context.Context) (map[string][]string, error) { + inspect, err := c.inspectContainer(ctx) + if err != nil { + return map[string][]string{}, err + } + + networks := inspect.NetworkSettings.Networks + + a := map[string][]string{} + + for k := range networks { + a[k] = networks[k].Aliases + } + + return a, nil +} + +// DockerNetwork represents a network started using Docker +type DockerNetwork struct { + ID string // Network ID from Docker + Driver string + Name string + provider *DockerProvider + terminationSignal chan bool +} + +// Remove is used to remove the network. It is usually triggered by as defer function. +func (n *DockerNetwork) Remove(_ context.Context) error { + if n.terminationSignal != nil { + n.terminationSignal <- true + } + return nil +} + // DockerProvider implements the ContainerProvider interface type DockerProvider struct { client *client.Client @@ -297,7 +351,24 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque Privileged: req.Privileged, } - resp, err := p.client.ContainerCreate(ctx, dockerInput, hostConfig, nil, req.Name) + endpointConfigs := map[string]*network.EndpointSettings{} + for _, n := range req.Networks { + nw, err := p.GetNetwork(ctx, NetworkRequest{ + Name: n, + }) + if err == nil { + endpointSetting := network.EndpointSettings{ + Aliases: req.NetworkAliases[n], + NetworkID: nw.ID, + } + endpointConfigs[n] = &endpointSetting + } + } + networkingConfig := network.NetworkingConfig{ + EndpointsConfig: endpointConfigs, + } + + resp, err := p.client.ContainerCreate(ctx, dockerInput, hostConfig, &networkingConfig, req.Name) if err != nil { return nil, err } @@ -368,6 +439,67 @@ func (p *DockerProvider) daemonHost() (string, error) { return p.hostCache, nil } +// CreateNetwork returns the object representing a new network identified by its name +func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) (Network, error) { + if req.Labels == nil { + req.Labels = make(map[string]string) + } + + nc := types.NetworkCreate{ + Driver: req.Driver, + CheckDuplicate: req.CheckDuplicate, + Internal: req.Internal, + EnableIPv6: req.EnableIPv6, + Attachable: req.Attachable, + Labels: req.Labels, + } + + sessionID := uuid.NewV4() + + var termSignal chan bool + if !req.SkipReaper { + r, err := NewReaper(ctx, sessionID.String(), p) + if err != nil { + return nil, errors.Wrap(err, "creating network reaper failed") + } + termSignal, err = r.Connect() + if err != nil { + return nil, errors.Wrap(err, "connecting to network reaper failed") + } + for k, v := range r.Labels() { + if _, ok := req.Labels[k]; !ok { + req.Labels[k] = v + } + } + } + + response, err := p.client.NetworkCreate(ctx, req.Name, nc) + if err != nil { + return &DockerNetwork{}, err + } + + n := &DockerNetwork{ + ID: response.ID, + Driver: req.Driver, + Name: req.Name, + terminationSignal: termSignal, + } + + return n, nil +} + +// GetNetwork returns the object representing the network identified by its name +func (p *DockerProvider) GetNetwork(ctx context.Context, req NetworkRequest) (types.NetworkResource, error) { + networkResource, err := p.client.NetworkInspect(ctx, req.Name, types.NetworkInspectOptions{ + Verbose: true, + }) + if err != nil { + return types.NetworkResource{}, err + } + + return networkResource, err +} + func inAContainer() bool { // see https://github.com/testcontainers/testcontainers-java/blob/3ad8d80e2484864e554744a4800a81f6b7982168/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L15 if _, err := os.Stat("/.dockerenv"); err == nil { diff --git a/docker_test.go b/docker_test.go index 8245937f70..702bc9a3a8 100644 --- a/docker_test.go +++ b/docker_test.go @@ -18,6 +18,74 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) +func TestContainerAttachedToNewNetwork(t *testing.T) { + networkName := "new-network" + + ctx := context.Background() + gcr := GenericContainerRequest{ + ContainerRequest: ContainerRequest{ + Image: "nginx", + ExposedPorts: []string{ + "80/tcp", + }, + Networks: []string{ + networkName, + }, + NetworkAliases: map[string][]string{ + networkName: []string{ + "alias1", "alias2", "alias3", + }, + }, + }, + } + + provider, err := gcr.ProviderType.GetProvider() + + newNetwork, err := provider.CreateNetwork(ctx, NetworkRequest{ + Name: networkName, + CheckDuplicate: true, + }) + if err != nil { + t.Fatal(err) + } + defer newNetwork.Remove(ctx) + + nginx, err := GenericContainer(ctx, gcr) + if err != nil { + t.Fatal(err) + } + defer nginx.Terminate(ctx) + + networks, err := nginx.Networks(ctx) + if err != nil { + t.Fatal(err) + } + if len(networks) != 1 { + t.Errorf("Expected networks 1. Got '%d'.", len(networks)) + } + network := networks[0] + if network != networkName { + t.Errorf("Expected network name '%s'. Got '%s'.", networkName, network) + } + + networkAliases, err := nginx.NetworkAliases(ctx) + if err != nil { + t.Fatal(err) + } + if len(networkAliases) != 1 { + t.Errorf("Expected network aliases for 1 network. Got '%d'.", len(networkAliases)) + } + networkAlias := networkAliases[networkName] + if len(networkAlias) != 3 { + t.Errorf("Expected network aliases %d. Got '%d'.", 3, len(networkAlias)) + } + if networkAlias[0] != "alias1" || networkAlias[1] != "alias2" || networkAlias[2] != "alias3" { + t.Errorf( + "Expected network aliases '%s', '%s' and '%s'. Got '%s', '%s' and '%s'.", + "alias1", "alias2", "alias3", networkAlias[0], networkAlias[1], networkAlias[2]) + } +} + func TestContainerReturnItsContainerID(t *testing.T) { ctx := context.Background() nginxA, err := GenericContainer(ctx, GenericContainerRequest{ @@ -295,6 +363,17 @@ func TestContainerCreation(t *testing.T) { if resp.StatusCode != http.StatusOK { t.Errorf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode) } + networkAliases, err := nginxC.NetworkAliases(ctx) + if err != nil { + t.Fatal(err) + } + if len(networkAliases) != 1 { + fmt.Printf("%v", networkAliases) + t.Errorf("Expected number of connected networks %d. Got %d.", 0, len(networkAliases)) + } + if len(networkAliases["bridge"]) != 0 { + t.Errorf("Expected number of aliases for 'bridge' network %d. Got %d.", 0, len(networkAliases["bridge"])) + } } func TestContainerCreationWithName(t *testing.T) { @@ -309,7 +388,8 @@ func TestContainerCreationWithName(t *testing.T) { ExposedPorts: []string{ nginxPort, }, - Name: creationName, + Name: creationName, + Networks: []string{"bridge"}, }, Started: true, }) @@ -329,6 +409,17 @@ func TestContainerCreationWithName(t *testing.T) { if name != expectedName { t.Errorf("Expected container name '%s'. Got '%s'.", expectedName, name) } + networks, err := nginxC.Networks(ctx) + if err != nil { + t.Fatal(err) + } + if len(networks) != 1 { + t.Errorf("Expected networks 1. Got '%d'.", len(networks)) + } + network := networks[0] + if network != "bridge" { + t.Errorf("Expected network name '%s'. Got '%s'.", "bridge", network) + } ip, err := nginxC.Host(ctx) if err != nil { t.Fatal(err) diff --git a/generic.go b/generic.go index c21b387e20..cf39b8c1a0 100644 --- a/generic.go +++ b/generic.go @@ -33,3 +33,9 @@ func GenericContainer(ctx context.Context, req GenericContainerRequest) (Contain return c, nil } + +// GenericProvider represents an abstraction for container and network providers +type GenericProvider interface { + ContainerProvider + NetworkProvider +} diff --git a/network.go b/network.go new file mode 100644 index 0000000000..01956a2186 --- /dev/null +++ b/network.go @@ -0,0 +1,31 @@ +package testcontainers + +import ( + "context" + + "github.com/docker/docker/api/types" +) + +// NetworkProvider allows the creation of networks on an arbitrary system +type NetworkProvider interface { + CreateNetwork(context.Context, NetworkRequest) (Network, error) // create a network + GetNetwork(context.Context, NetworkRequest) (types.NetworkResource, error) // get a network +} + +// Network allows getting info about a single network instance +type Network interface { + Remove(context.Context) error // removes the network +} + +// NetworkRequest represents the parameters used to get a network +type NetworkRequest struct { + Driver string + CheckDuplicate bool + Internal bool + EnableIPv6 bool + Name string + Labels map[string]string + Attachable bool + + SkipReaper bool // indicates whether we skip setting up a reaper for this +}