Skip to content

Commit 3bf015b

Browse files
authored
cmd/atlas/docker: Revert let Docker Engine allocate a free port (#3500)
This reverts commit 25065bc.
1 parent 5733756 commit 3bf015b

File tree

2 files changed

+81
-70
lines changed

2 files changed

+81
-70
lines changed

cmd/atlas/internal/docker/docker.go

Lines changed: 78 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import (
1111
"fmt"
1212
"io"
1313
"log"
14+
"net"
1415
"net/url"
1516
"os"
1617
"os/exec"
1718
"path"
19+
"strconv"
1820
"strings"
1921
"time"
2022

@@ -50,9 +52,11 @@ type (
5052
}
5153
// A Container is an instance of a created container.
5254
Container struct {
53-
Config // Config used to create this container
54-
ID string
55-
HostPort string // Host port to connect to
55+
Config // Config used to create this container
56+
// ID of the container.
57+
ID string
58+
// Port on the host this containers service is bound to.
59+
Port string
5660
}
5761
// ConnOptions allows configuring the underlying connection pool.
5862
ConnOptions struct {
@@ -384,54 +388,40 @@ func Conn(s *ConnOptions) ConfigOption {
384388

385389
// Run pulls and starts a new docker container from the Config.
386390
func (c *Config) Run(ctx context.Context) (*Container, error) {
387-
if c == nil || c.Image == "" || c.Port == "" || c.Out == nil {
388-
return nil, fmt.Errorf("docker: invalid configuration %#v", c)
389-
}
390-
args := []string{"run", "--rm", "--detach", "--publish", c.Port}
391-
for _, e := range c.Env {
392-
args = append(args, "--env", e)
393-
}
394-
args = append(args, c.Image)
395-
id, err := c.docker(ctx, args...)
396-
if err != nil {
397-
return nil, fmt.Errorf("docker: run container: %w", err)
391+
// Make sure the configuration is not missing critical values.
392+
if err := c.validate(); err != nil {
393+
return nil, err
398394
}
399-
hostPort, err := c.docker(ctx, "port", id, c.Port)
395+
// Get a free host TCP port the container can bind its exposed service port on.
396+
p, err := freePort()
400397
if err != nil {
401-
return nil, fmt.Errorf("docker: get container port: %w", err)
398+
return nil, fmt.Errorf("getting open port: %w", err)
402399
}
403-
return &Container{
404-
Config: *c,
405-
ID: id,
406-
HostPort: strings.SplitN(hostPort, "\n", 2)[0],
407-
}, nil
408-
}
409-
410-
// StopAndRemove stops the container with the given id.
411-
func (c *Config) StopAndRemove(ctx context.Context, id string) error {
412-
_, err := c.docker(ctx, "kill", id)
413-
if err != nil {
414-
return fmt.Errorf("docker: kill container: %w", err)
400+
// Run the container.
401+
args := []string{"docker", "run", "--rm", "--detach"}
402+
for _, e := range c.Env {
403+
args = append(args, "-e", e)
415404
}
416-
return nil
417-
}
418-
419-
func (c *Config) docker(ctx context.Context, args ...string) (string, error) {
405+
args = append(args, "-p", fmt.Sprintf("%s:%s", p, c.Port), c.Image)
406+
cmd := exec.CommandContext(ctx, args[0], args[1:]...) //nolint:gosec
420407
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
421-
cmd := exec.CommandContext(ctx, "docker", args...) //nolint:gosec
422408
cmd.Stdout, cmd.Stderr = io.MultiWriter(c.Out, stdout), stderr
423409
if err := cmd.Run(); err != nil {
424410
if stderr.Len() > 0 {
425411
err = errors.New(strings.TrimSpace(stderr.String()))
426412
}
427-
return "", err
413+
return nil, err
428414
}
429-
return strings.TrimSpace(stdout.String()), nil
415+
return &Container{
416+
Config: *c,
417+
ID: strings.TrimSpace(stdout.String()),
418+
Port: p,
419+
}, nil
430420
}
431421

432422
// Close stops and removes this container.
433423
func (c *Container) Close() error {
434-
return c.Config.StopAndRemove(context.Background(), c.ID)
424+
return exec.Command("docker", "kill", c.ID).Run() //nolint:gosec
435425
}
436426

437427
// Wait waits for this container to be ready.
@@ -442,16 +432,20 @@ func (c *Container) Wait(ctx context.Context, timeout time.Duration) error {
442432
if timeout > time.Minute {
443433
timeout = time.Minute
444434
}
445-
u, err := c.PingURL()
435+
var (
436+
done = time.After(timeout)
437+
u, err = c.URL()
438+
)
446439
if err != nil {
447440
return err
448441
}
449-
for done := time.After(timeout); ; {
442+
pingURL := c.PingURL(*u)
443+
for {
450444
select {
451445
case <-time.After(100 * time.Millisecond):
452446
var client *sqlclient.Client
453447
// Ping against the root connection.
454-
client, err = sqlclient.Open(ctx, u)
448+
client, err = sqlclient.Open(ctx, pingURL)
455449
if err != nil {
456450
continue
457451
}
@@ -479,39 +473,22 @@ func (c *Container) Wait(ctx context.Context, timeout time.Duration) error {
479473
}
480474
}
481475

482-
// PingURL returns a URL to ping the Container.
483-
func (c *Container) PingURL() (string, error) {
484-
u, err := c.URL()
485-
if err != nil {
486-
return "", err
487-
}
488-
switch c.driver {
489-
case DriverSQLServer:
490-
q := u.Query()
491-
q.Del("database")
492-
u.RawQuery = q.Encode()
493-
return u.String(), nil
494-
default:
495-
u.Path = "/"
496-
return u.String(), nil
497-
}
498-
}
499-
500476
// URL returns a URL to connect to the Container.
501477
func (c *Container) URL() (*url.URL, error) {
502-
u := &url.URL{
503-
Scheme: c.driver,
504-
User: c.User,
505-
Host: c.HostPort,
506-
}
478+
host := "localhost"
507479
// Check if the DOCKER_HOST env var is set.
508480
// If it is, use the host from the URL.
509481
if h := os.Getenv("DOCKER_HOST"); h != "" {
510-
dh, err := url.Parse(h)
482+
u, err := url.Parse(h)
511483
if err != nil {
512484
return nil, err
513485
}
514-
u.Host = fmt.Sprintf("%s:%s", dh.Hostname(), u.Port())
486+
host = u.Hostname()
487+
}
488+
u := &url.URL{
489+
Scheme: c.driver,
490+
User: c.User,
491+
Host: fmt.Sprintf("%s:%s", host, c.Port),
515492
}
516493
switch c.driver {
517494
case DriverSQLServer:
@@ -533,6 +510,43 @@ func (c *Container) URL() (*url.URL, error) {
533510
return u, nil
534511
}
535512

513+
// PingURL returns a URL to ping the Container.
514+
func (c *Container) PingURL(u url.URL) string {
515+
switch c.driver {
516+
case DriverSQLServer:
517+
q := u.Query()
518+
q.Del("database")
519+
u.RawQuery = q.Encode()
520+
return u.String()
521+
default:
522+
u.Path = "/"
523+
return u.String()
524+
}
525+
}
526+
527+
// validate that no empty values are given.
528+
func (c *Config) validate() error {
529+
if c == nil || c.Image == "" || c.Port == "" || c.Out == nil {
530+
return fmt.Errorf("invalid configuration %#v", c)
531+
}
532+
return nil
533+
}
534+
535+
func freePort() (string, error) {
536+
a, err := net.ResolveTCPAddr("tcp", ":0")
537+
if err != nil {
538+
return "", err
539+
}
540+
l, err := net.ListenTCP("tcp", a)
541+
if err != nil {
542+
return "", err
543+
}
544+
if err := l.Close(); err != nil {
545+
return "", err
546+
}
547+
return strconv.Itoa(l.Addr().(*net.TCPAddr).Port), nil
548+
}
549+
536550
func init() {
537551
sqlclient.Register(
538552
"docker",

cmd/atlas/internal/docker/docker_test.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package docker
66

77
import (
88
"context"
9-
"fmt"
109
"io"
1110
"net/url"
1211
"testing"
@@ -19,7 +18,7 @@ func TestDockerConfig(t *testing.T) {
1918

2019
// invalid config
2120
_, err := (&Config{}).Run(ctx)
22-
require.ErrorContains(t, err, fmt.Sprintf("invalid configuration %#v", &Config{}))
21+
require.Error(t, err)
2322

2423
// MySQL
2524
cfg, err := MySQL("latest", Out(io.Discard))
@@ -469,13 +468,11 @@ func TestContainerURL(t *testing.T) {
469468
driver: "postgres",
470469
User: url.UserPassword("postgres", "pass"),
471470
},
472-
HostPort: "0.0.0.0:5432",
471+
Port: "5432",
473472
}
474-
// Without DOCKER_HOST
475-
t.Setenv("DOCKER_HOST", "")
476473
u, err := c.URL()
477474
require.NoError(t, err)
478-
require.Equal(t, "postgres://postgres:pass@0.0.0.0:5432/?sslmode=disable", u.String())
475+
require.Equal(t, "postgres://postgres:pass@localhost:5432/?sslmode=disable", u.String())
479476

480477
// With DOCKER_HOST
481478
t.Setenv("DOCKER_HOST", "tcp://host.docker.internal:2375")

0 commit comments

Comments
 (0)