Skip to content

Commit

Permalink
Merge pull request #76 from mdelapenya/support-for-networking
Browse files Browse the repository at this point in the history
(#73) Support attaching containers to different networks than bridge
  • Loading branch information
gianarb authored Sep 3, 2019
2 parents a059006 + a49bb84 commit 8c8ed2b
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 13 deletions.
26 changes: 15 additions & 11 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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()
Expand Down
134 changes: 133 additions & 1 deletion docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down
93 changes: 92 additions & 1 deletion docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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) {
Expand All @@ -309,7 +388,8 @@ func TestContainerCreationWithName(t *testing.T) {
ExposedPorts: []string{
nginxPort,
},
Name: creationName,
Name: creationName,
Networks: []string{"bridge"},
},
Started: true,
})
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
31 changes: 31 additions & 0 deletions network.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 8c8ed2b

Please sign in to comment.