diff --git a/go.mod b/go.mod index 8334ef3..69ce78a 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,13 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/containerd/containerd v1.5.5 // indirect github.com/docker/docker v20.10.8+incompatible - github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-connections v0.4.0 github.com/go-redis/redis/v8 v8.11.3 github.com/go-sql-driver/mysql v1.6.0 github.com/gorilla/mux v1.8.0 // indirect github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.7.0 go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.7.0 // indirect diff --git a/internal/services/icinga2/docker.go b/internal/services/icinga2/docker.go index 761c792..118b2bc 100644 --- a/internal/services/icinga2/docker.go +++ b/internal/services/icinga2/docker.go @@ -16,6 +16,8 @@ import ( "time" ) +const PORT = "5665" + type dockerCreator struct { logger *zap.Logger dockerClient *client.Client @@ -59,11 +61,13 @@ func (i *dockerCreator) CreateIcinga2(name string) services.Icinga2Base { panic(err) } + port := utils.NewPortDecision(i.dockerClient, PORT) + cont, err := i.dockerClient.ContainerCreate(context.Background(), &container.Config{ Image: dockerImage, Hostname: name, Env: []string{"ICINGA_MASTER=1"}, - }, nil, &network.NetworkingConfig{ + }, &container.HostConfig{PublishAllPorts: port.Remote()}, &network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ networkName: { NetworkID: i.dockerNetworkId, @@ -90,10 +94,15 @@ func (i *dockerCreator) CreateIcinga2(name string) services.Icinga2Base { } logger.Debug("started container") + binding, err := port.Binding(context.Background(), i.dockerClient, cont.ID) + if err != nil { + logger.Fatal("can't create port binding", zap.Error(err)) + } + n := &dockerInstance{ info: info{ - host: utils.MustString(utils.DockerContainerAddress(context.Background(), i.dockerClient, cont.ID)), - port: "5665", + host: binding.Host, + port: binding.Port, }, icinga2Docker: i, logger: logger, diff --git a/internal/services/icingadb/docker_binary.go b/internal/services/icingadb/docker_binary.go index 09aba6a..b3f0d57 100644 --- a/internal/services/icingadb/docker_binary.go +++ b/internal/services/icingadb/docker_binary.go @@ -118,7 +118,7 @@ func (i *dockerBinaryCreator) CreateIcingaDb( }, }, nil, containerName) if err != nil { - inst.logger.Fatal("failed to create icingadb container") + inst.logger.Fatal("failed to create icingadb container", zap.Error(err)) } inst.containerId = cont.ID inst.logger = inst.logger.With(zap.String("container-id", cont.ID)) @@ -136,7 +136,7 @@ func (i *dockerBinaryCreator) CreateIcingaDb( err = i.dockerClient.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) if err != nil { - inst.logger.Fatal("failed to start container") + inst.logger.Fatal("failed to start container", zap.Error(err)) } inst.logger.Debug("started container") diff --git a/internal/services/mysql/docker.go b/internal/services/mysql/docker.go index e49f3fc..5b1db12 100644 --- a/internal/services/mysql/docker.go +++ b/internal/services/mysql/docker.go @@ -11,6 +11,8 @@ import ( "time" ) +const PORT = "3306" + type dockerCreator struct { *rootConnection logger *zap.Logger @@ -38,13 +40,14 @@ func NewDockerCreator(logger *zap.Logger, dockerClient *client.Client, container panic(err) } + port := utils.NewPortDecision(dockerClient, PORT) + rootPassword := utils.RandomString(16) cont, err := dockerClient.ContainerCreate(context.Background(), &container.Config{ - ExposedPorts: nil, - Env: []string{"MYSQL_ROOT_PASSWORD=" + rootPassword}, - Cmd: nil, - Image: dockerImage, - }, nil, &network.NetworkingConfig{ + Env: []string{"MYSQL_ROOT_PASSWORD=" + rootPassword}, + Cmd: nil, + Image: dockerImage, + }, &container.HostConfig{PortBindings: port.Map()}, &network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ networkName: { Aliases: []string{"mysql"}, @@ -73,14 +76,14 @@ func NewDockerCreator(logger *zap.Logger, dockerClient *client.Client, container } logger.Debug("started mysql container") - containerAddress := utils.MustString(utils.DockerContainerAddress(context.Background(), dockerClient, cont.ID)) - d := &dockerCreator{ - rootConnection: newRootConnection(containerAddress, "3306", "root", rootPassword), - logger: logger, - client: dockerClient, - containerId: cont.ID, - containerName: containerName, + rootConnection: newRootConnection( + port.Address(context.Background(), dockerClient, cont.ID), port.Port(), "root", rootPassword, + ), + logger: logger, + client: dockerClient, + containerId: cont.ID, + containerName: containerName, } for attempt := 1; ; attempt++ { diff --git a/internal/services/redis/docker.go b/internal/services/redis/docker.go index 1382dda..ec6f248 100644 --- a/internal/services/redis/docker.go +++ b/internal/services/redis/docker.go @@ -15,6 +15,8 @@ import ( "time" ) +const PORT = "6379" + type dockerCreator struct { logger *zap.Logger dockerClient *client.Client @@ -55,9 +57,11 @@ func (r *dockerCreator) CreateRedisServer() services.RedisServerBase { panic(err) } + port := utils.NewPortDecision(r.dockerClient, PORT) + cont, err := r.dockerClient.ContainerCreate(context.Background(), &container.Config{ Image: dockerImage, - }, nil, &network.NetworkingConfig{ + }, &container.HostConfig{PortBindings: port.Map()}, &network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ networkName: { NetworkID: r.dockerNetworkId, @@ -82,14 +86,14 @@ func (r *dockerCreator) CreateRedisServer() services.RedisServerBase { err = r.dockerClient.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) if err != nil { - logger.Fatal("failed to start container") + logger.Fatal("failed to start container", zap.Error(err)) } logger.Debug("started container") s := &dockerServer{ info: info{ - host: utils.MustString(utils.DockerContainerAddress(context.Background(), r.dockerClient, cont.ID)), - port: "6379", + host: port.Address(context.Background(), r.dockerClient, cont.ID), + port: port.Port(), }, redisDocker: r, logger: logger, diff --git a/it.go b/it.go index 81a6b0b..bb5fae0 100644 --- a/it.go +++ b/it.go @@ -70,7 +70,7 @@ func NewIT() *IT { }) } - if n, err := it.dockerClient.NetworkCreate(context.Background(), it.prefix, types.NetworkCreate{}); err != nil { + if n, err := it.dockerClient.NetworkCreate(context.Background(), it.prefix, types.NetworkCreate{Labels: map[string]string{"icinga": "testing"}}); err != nil { it.logger.Fatal("failed to create docker network", zap.String("network-name", it.prefix), zap.Error(err)) } else { it.logger.Debug("created docker network", zap.String("network-name", it.prefix), zap.String("network-id", n.ID)) diff --git a/utils/port.go b/utils/port.go new file mode 100644 index 0000000..c38e04f --- /dev/null +++ b/utils/port.go @@ -0,0 +1,119 @@ +package utils + +import ( + "context" + "fmt" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" + "net" + "strconv" +) + +type PortDecision struct { + dockerHost string + exposed string + port string + remote bool +} + +func NewPortDecision(c *client.Client, port string) *PortDecision { + url, err := client.ParseHostURL(c.DaemonHost()) + if err != nil { + panic(err) + } + p := &PortDecision{port: port} + if url.Scheme != "unix" { + portNumber, err := GetFreePort() + if err != nil { + panic(err) + } + p.dockerHost = url.Hostname() + p.exposed = strconv.Itoa(portNumber) + p.remote = true + } else { + p.exposed = port + } + + return p +} + +func (p *PortDecision) Address(ctx context.Context, c *client.Client, id string) string { + if p.remote { + return p.dockerHost + } + + return MustString(DockerContainerAddress(ctx, c, id)) +} + +func (p *PortDecision) Binding(ctx context.Context, c *client.Client, id string) (*PortBinding, error) { + var port string + host := p.Address(ctx, c, id) + if p.remote { + r, err := c.ContainerInspect(context.Background(), id) + if err != nil { + return nil, errors.Wrap(err, "failed to inspect container") + } + + defaultPort, err := nat.NewPort(nat.SplitProtoPort(p.port)) + if err != nil { + return nil, errors.Wrap(err, "can't parse default port") + } + + p, ok := r.NetworkSettings.Ports[defaultPort] + if !ok { + return nil, errors.New(fmt.Sprintf("default port %s not exposed", defaultPort)) + } + port = p[0].HostPort + } else { + port = p.port + } + + return &PortBinding{ + Host: host, + Port: port, + }, nil +} + +func (p *PortDecision) Map() nat.PortMap { + if p.remote { + return nat.PortMap{ + nat.Port(fmt.Sprintf("%s/tcp", p.port)): []nat.PortBinding{ + { + HostIP: "0.0.0.0", + HostPort: p.exposed, + }, + }, + } + } + + return nil +} + +func (p *PortDecision) Port() string { + return p.exposed +} + +func (p *PortDecision) Remote() bool { + return p.remote +} + +func GetFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", ":0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + + return l.Addr().(*net.TCPAddr).Port, nil +} + +type PortBinding struct { + Host string + Port string +}