Skip to content

Commit

Permalink
Merge pull request #55 from alde/master
Browse files Browse the repository at this point in the history
Implement a Log wait strategy
  • Loading branch information
gianarb authored Apr 8, 2019
2 parents c193456 + bd4e169 commit e99af5d
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 0 deletions.
2 changes: 2 additions & 0 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package testcontainers

import (
"context"
"io"

"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package testcontainers
import (
"context"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
82 changes: 82 additions & 0 deletions wait/log.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions wait/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package wait

import (
"context"
"io"
"time"

"github.com/docker/go-connections/nat"
Expand All @@ -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 {
Expand Down

0 comments on commit e99af5d

Please sign in to comment.