diff --git a/container.go b/container.go index 729ace9168..58c212d2a8 100644 --- a/container.go +++ b/container.go @@ -2,6 +2,7 @@ package testcontainers import ( "context" + "io" "github.com/docker/go-connections/nat" "github.com/pkg/errors" @@ -34,6 +35,7 @@ type Container interface { SessionID() string // get session id Start(context.Context) error // start the container Terminate(context.Context) error // terminate the container + Logs(context.Context) (io.ReadCloser, error) // Get logs of the container } // ContainerRequest represents the parameters used to get a running container diff --git a/docker.go b/docker.go index 6d7e99e2ad..c6ca80f217 100644 --- a/docker.go +++ b/docker.go @@ -3,6 +3,7 @@ package testcontainers import ( "context" "fmt" + "io" "io/ioutil" "net/url" "os" @@ -162,6 +163,16 @@ func (c *DockerContainer) inspectContainer(ctx context.Context) (*types.Containe return c.raw, nil } +// Logs will fetch both STDOUT and STDERR from the current container. Returns a +// ReadCloser and leaves it up to the caller to extract what it wants. +func (c *DockerContainer) Logs(ctx context.Context) (io.ReadCloser, error) { + options := types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + } + return c.provider.client.ContainerLogs(ctx, c.ID, options) +} + // DockerProvider implements the ContainerProvider interface type DockerProvider struct { client *client.Client diff --git a/docker_test.go b/docker_test.go index 83f8eaa76b..ea9f508b5b 100644 --- a/docker_test.go +++ b/docker_test.go @@ -7,6 +7,10 @@ import ( "testing" "time" + "database/sql" + // Import mysql into the scope of this package (required) + _ "github.com/go-sql-driver/mysql" + "github.com/docker/go-connections/nat" "github.com/testcontainers/testcontainers-go/wait" ) @@ -323,3 +327,48 @@ func TestContainerCreationTimesOutWithHttp(t *testing.T) { t.Error("Expected timeout") } } + +func TestContainerCreationWaitsForLog(t *testing.T) { + ctx := context.Background() + req := ContainerRequest{ + Image: "mysql:latest", + ExposedPorts: []string{"3306/tcp", "33060/tcp"}, + Env: map[string]string{ + "MYSQL_ROOT_PASSWORD": "password", + "MYSQL_DATABASE": "database", + }, + WaitingFor: wait.ForLog("port: 3306 MySQL Community Server - GPL"), + } + mysqlC, _ := GenericContainer(ctx, GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + defer func() { + t.Log("terminating container") + err := mysqlC.Terminate(ctx) + if err != nil { + t.Fatal(err) + } + }() + + host, _ := mysqlC.Host(ctx) + p, _ := mysqlC.MappedPort(ctx, "3306/tcp") + port := p.Int() + connectionString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?tls=skip-verify", + "root", "password", host, port, "database") + + db, err := sql.Open("mysql", connectionString) + defer db.Close() + + if err = db.Ping(); err != nil { + t.Errorf("error pinging db: %+v\n", err) + } + _, err = db.Exec("CREATE TABLE IF NOT EXISTS a_table ( \n" + + " `col_1` VARCHAR(128) NOT NULL, \n" + + " `col_2` VARCHAR(128) NOT NULL, \n" + + " PRIMARY KEY (`col_1`, `col_2`) \n" + + ")") + if err != nil { + t.Errorf("error creating table: %+v\n", err) + } +} diff --git a/go.mod b/go.mod index f6f6fa03b9..f6fd4d5d05 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/docker/docker v0.7.3-0.20180815000130-e05b657120a6 github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.3.3 // indirect + github.com/go-sql-driver/mysql v1.4.1 github.com/gogo/protobuf v1.2.0 // indirect github.com/google/go-cmp v0.2.0 // indirect github.com/gorilla/context v1.1.1 // indirect diff --git a/go.sum b/go.sum index 628e6eb8a1..2a8faa569d 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= diff --git a/wait/log.go b/wait/log.go new file mode 100644 index 0000000000..2409f32dca --- /dev/null +++ b/wait/log.go @@ -0,0 +1,82 @@ +package wait + +import ( + "context" + "io/ioutil" + "strings" + "time" +) + +// Implement interface +var _ Strategy = (*LogStrategy)(nil) + +// LogStrategy will wait until a given log entry shows up in the docker logs +type LogStrategy struct { + // all Strategies should have a startupTimeout to avoid waiting infinitely + startupTimeout time.Duration + + // additional properties + Log string + PollInterval time.Duration +} + +// NewLogStrategy constructs a HTTP strategy waiting on port 80 and status code 200 +func NewLogStrategy(log string) *LogStrategy { + return &LogStrategy{ + startupTimeout: defaultStartupTimeout(), + Log: log, + PollInterval: 100 * time.Millisecond, + } + +} + +// fluent builders for each property +// since go has neither covariance nor generics, the return type must be the type of the concrete implementation +// this is true for all properties, even the "shared" ones like startupTimeout + +// WithStartupTimeout can be used to change the default startup timeout +func (ws *LogStrategy) WithStartupTimeout(startupTimeout time.Duration) *LogStrategy { + ws.startupTimeout = startupTimeout + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (ws *LogStrategy) WithPollInterval(pollInterval time.Duration) *LogStrategy { + ws.PollInterval = pollInterval + return ws +} + +// ForLog is the default construction for the fluid interface. +// +// For Example: +// wait. +// ForLog("some text"). +// WithPollInterval(1 * time.Second) +func ForLog(log string) *LogStrategy { + return NewLogStrategy(log) +} + +// WaitUntilReady implements Strategy.WaitUntilReady +func (ws *LogStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { + // limit context to startupTimeout + ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout) + defer cancelContext() + + for { + reader, err := target.Logs(ctx) + if err != nil { + time.Sleep(ws.PollInterval) + continue + } + b, err := ioutil.ReadAll(reader) + logs := string(b) + if strings.Contains(logs, ws.Log) { + break + } else { + time.Sleep(ws.PollInterval) + continue + } + } + + return nil +} diff --git a/wait/wait.go b/wait/wait.go index 0756469606..c39cace074 100644 --- a/wait/wait.go +++ b/wait/wait.go @@ -2,6 +2,7 @@ package wait import ( "context" + "io" "time" "github.com/docker/go-connections/nat" @@ -14,6 +15,7 @@ type Strategy interface { type StrategyTarget interface { Host(context.Context) (string, error) MappedPort(context.Context, nat.Port) (nat.Port, error) + Logs(context.Context) (io.ReadCloser, error) } func defaultStartupTimeout() time.Duration {