如何防止 github.com/ory/dockertest 将容器分配到随机端口?

How to prevent github.com/ory/dockertest from assigning containers to random ports?

我正在尝试编写 运行 都在本地使用 github.com/ory/dockertest and in a CircleCI environment (in which the "CI" environment variable is set) using a Docker executor type. In the container, I'd like to run the Google Pub/Sub emulator using the google/cloud-sdk 图片的单元测试。

作为一个简化的例子,我写了这个 Go 程序:

package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "net"
    "os"
    "time"

    "cloud.google.com/go/pubsub"
    "github.com/ory/dockertest"
    "github.com/ory/dockertest/docker"
    "google.golang.org/api/iterator"
)

var pubsubEmulatorHost string

func main() {
    flag.StringVar(&pubsubEmulatorHost, "pubsubEmulatorHost", "localhost:8085", "Google Pub/Sub emulator host")
    flag.Parse()

    if os.Getenv("CI") == "" {
        pool, err := dockertest.NewPool("")
        if err != nil {
            log.Fatalf("Could not connect to Docker: %v", err)
        }

        opts := &dockertest.RunOptions{
            Hostname:     "localhost",
            Repository:   "google/cloud-sdk",
            Cmd:          []string{"gcloud", "beta", "emulators", "pubsub", "start", "--host-port", "127.0.0.1:8085"},
            ExposedPorts: []string{"8085"},
            PortBindings: map[docker.Port][]docker.PortBinding{
                "8085/tcp": {{HostIP: "127.0.0.1", HostPort: "8085/tcp"}},
            },
        }
        resource, err := pool.RunWithOptions(opts)
        if err != nil {
            log.Fatalf("Could not start resource: %v", err)
        }

        pool.MaxWait = 10 * time.Second
        if err := pool.Retry(func() error {
            _, err := net.Dial("tcp", "localhost:8085")
            return err
        }); err != nil {
            log.Fatalf("Could not dial the Pub/Sub emulator: %v", err)
        }

        defer func() {
            if err := pool.Purge(resource); err != nil {
                log.Fatalf("Could not purge resource: %v", err)
            }
        }()
    }

    os.Setenv("PUBSUB_EMULATOR_HOST", pubsubEmulatorHost)
    defer os.Unsetenv("PUBSUB_EMULATOR_HOST")

    client, err := pubsub.NewClient(context.Background(), "my-project")
    if err != nil {
        log.Fatalf("NewClient: %v", err)
    }

    topic, err := client.CreateTopic(context.Background(), "my-topic")
    if err != nil {
        log.Fatalf("CreateTopic: %v", err)
    }
    log.Println("Created topic:", topic)

    topicIterator := client.Topics(context.Background())
    for {
        topic, err := topicIterator.Next()
        if err == iterator.Done {
            break
        }
        if err != nil {
            log.Fatalf("Next: %v", err)
        }
        fmt.Printf("%s\n", topic)
    }
}

首先,我已经验证 运行 在从命令行 运行 将容器设置为非空值后 运行 将其与 CI 环境变量设置为非空值会产生预期结果:

> 
docker run -p "8085:8085" google/cloud-sdk gcloud beta emulators pubsub start --host-port=0.0.0.0:8085
Executing: /usr/lib/google-cloud-sdk/platform/pubsub-emulator/bin/cloud-pubsub-emulator --host=0.0.0.0 --port=8085
[pubsub] This is the Google Pub/Sub fake.
[pubsub] Implementation may be incomplete or differ from the real system.
[pubsub] Jul 16, 2020 9:21:33 PM com.google.cloud.pubsub.testing.v1.Main main
[pubsub] INFO: IAM integration is disabled. IAM policy methods and ACL checks are not supported
[pubsub] Jul 16, 2020 9:21:34 PM io.gapi.emulators.netty.NettyUtil applyJava7LongHostnameWorkaround
[pubsub] INFO: Applied Java 7 long hostname workaround.
[pubsub] Jul 16, 2020 9:21:34 PM com.google.cloud.pubsub.testing.v1.Main main
[pubsub] INFO: Server started, listening on 8085

其次是

> env CI=true go run main.go
2020/07/16 14:22:01 Created topic: projects/my-project/topics/my-topic
projects/my-project/topics/my-topic

请注意,此时容器上的 8085 端口已按预期映射到主机上的 8085 端口:

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
76724696f9d9        google/cloud-sdk    "gcloud beta emulato…"   55 seconds ago      Up 54 seconds       0.0.0.0:8085->8085/tcp   epic_ganguly

我不想在不设置 CI 环境变量的情况下停止容器和 运行 程序,应该自动启动容器。然而,我观察到的是尝试建立连接超时:

> go run main.go
2020/07/16 14:23:56 Could not dial the Pub/Sub emulator: dial tcp [::1]:8085: connect: connection refused
exit status 1

检查容器后,它似乎映射到本地端口 32778 而不是 8085:

> docker ps
CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS              PORTS                     NAMES
0df07ac232d5        google/cloud-sdk:latest   "gcloud beta emulato…"   34 seconds ago      Up 33 seconds       0.0.0.0:32778->8085/tcp   wizardly_ptolemy

我认为像上面那样在 RunOptions 中指定 PortBindings 应该将容器上的端口 8085 映射到主机上的端口 8085,但似乎不是案件。有谁知道使该程序运行的正确 运行 选项?

Dockertest 允许您使用 resource.GetPort() 检索容器的映射端口,您可以使用它来将 pubsubEmulatorHost 设置为正确的值:

port := "8085"

if os.Getenv("CI") == "" {
  // ...
  pubsubEmulatorHost = opts.Hostname + resource.GetPort("8085/tcp")
  // pubsubEmulatorHost = "localhost:32778"
  // ...
}