diff --git a/canned/postgres/postgres.go b/canned/postgres/postgres.go new file mode 100644 index 0000000000..d9880c590d --- /dev/null +++ b/canned/postgres/postgres.go @@ -0,0 +1,115 @@ +package postgres + +import ( + "context" + "fmt" + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + image = "postgres" + tag = "9.6.15" + port nat.Port = "5432/tcp" + user = "user" + password = "password" + database = "database" + logEntry = "database system is ready to accept connections" +) + +type ContainerRequest struct { + testcontainers.GenericContainerRequest + Version string + User string + Password string + Database string +} + +// should always be created via NewContainer +type Container struct { + Container testcontainers.Container + req ContainerRequest +} + +func (c Container) ConnectURL(ctx context.Context) (string, error) { + template := "postgres://%s:%s@%s:%d/%s" + + host, err := c.Container.Host(ctx) + if err != nil { + return "", errors.Wrapf(err, "failed to get host") + } + + mappedPort, err := c.Container.MappedPort(ctx, port) + if err != nil { + return "", errors.Wrapf(err, "failed to get mapped port for %s", port.Port()) + } + + return fmt.Sprintf(template, c.req.User, c.req.Password, host, + mappedPort.Int(), c.req.Database), nil +} + +func NewContainer(ctx context.Context, req ContainerRequest) (*Container, error) { + req.ExposedPorts = []string{string(port)} + + if req.Version != "" && req.FromDockerfile.Context == "" { + req.Image = fmt.Sprintf("%s:%s", image, req.Version) + } + + // Set the default values if none were provided in the request + if req.Image == "" && req.FromDockerfile.Context == "" { + req.Image = fmt.Sprintf("%s:%s", image, tag) + } + + if req.User == "" { + req.User = user + } + + if req.Password == "" { + req.Password = password + } + + if req.Database == "" { + req.Database = database + } + + if req.Env == nil { + req.Env = map[string]string{} + } + req.Env["POSTGRES_USER"] = req.User + req.Env["POSTGRES_PASSWORD"] = req.Password + req.Env["POSTGRES_DB"] = req.Database + + if req.WaitingFor == nil { + req.WaitingFor = wait.ForAll( + wait.NewHostPortStrategy(port), + wait.ForLog(logEntry).WithOccurrence(2), + ) + } + + if req.Cmd == nil { + req.Cmd = []string{"postgres", "-c", "fsync=off"} + } + + provider, err := req.ProviderType.GetProvider() + if err != nil { + return nil, err + } + + c, err := provider.CreateContainer(ctx, req.ContainerRequest) + if err != nil { + return nil, errors.Wrap(err, "failed to create container") + } + + res := &Container{ + Container: c, + req: req, + } + + if err := c.Start(ctx); err != nil { + return res, errors.Wrap(err, "failed to start container") + } + + return res, nil +} diff --git a/canned/postgres/postgres_test.go b/canned/postgres/postgres_test.go new file mode 100644 index 0000000000..6f5428804f --- /dev/null +++ b/canned/postgres/postgres_test.go @@ -0,0 +1,81 @@ +package postgres + +import ( + "context" + "database/sql" + "testing" + + _ "github.com/lib/pq" + "github.com/testcontainers/testcontainers-go" +) + +func TestWriteIntoAPostgreSQLContainerViaDriver(t *testing.T) { + + ctx := context.Background() + + c, err := NewContainer(ctx, ContainerRequest{ + Version: "9.6.15-alpine", + Database: "hello", + }) + if err != nil { + t.Fatal(err.Error()) + } + defer c.Container.Terminate(ctx) + + connectURL, err := c.ConnectURL(ctx) + if err != nil { + t.Fatal(err.Error()) + } + + connectURL += "?sslmode=disable" + + sqlC, err := sql.Open("postgres", connectURL) + if err != nil { + t.Fatal(err.Error()) + } + defer sqlC.Close() + + _, err = sqlC.ExecContext(ctx, "CREATE TABLE example ( id integer, data varchar(32) )") + if err != nil { + t.Fatal(err.Error()) + } +} + +func ExampleContainerRequest() { + + // Optional + containerRequest := testcontainers.ContainerRequest{ + Image: "docker.io/library/postgres:11.5", + } + + genericContainerRequest := testcontainers.GenericContainerRequest{ + Started: true, + ContainerRequest: containerRequest, + } + + // Database, User, and Password are optional, + // the driver will use default ones if not provided + pgContainerRequest := ContainerRequest{ + Database: "mycustomdatabase", + User: "anyuser", + Password: "yoursecurepassword", + GenericContainerRequest: genericContainerRequest, + } + + pgContainerRequest.Validate() +} + +func ExampleNewContainer() { + ctx := context.Background() + + // Create your PostgreSQL database, + // by setting Started this function will not return + // until a test connection has been established + c, _ := NewContainer(ctx, ContainerRequest{ + Database: "hello", + GenericContainerRequest: testcontainers.GenericContainerRequest{ + Started: true, + }, + }) + defer c.Container.Terminate(ctx) +} diff --git a/canned/redis/redis.go b/canned/redis/redis.go new file mode 100644 index 0000000000..f02645deef --- /dev/null +++ b/canned/redis/redis.go @@ -0,0 +1,83 @@ +package redis + +import ( + "context" + "fmt" + "github.com/pkg/errors" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + image = "redis" + tag = "5.0.7" + port = "6379/tcp" +) + +// RedisContainerRequest completes GenericContainerRequest +type ContainerRequest struct { + testcontainers.GenericContainerRequest + Version string +} + +// Container should always be created via NewContainer +type Container struct { + Container testcontainers.Container + req ContainerRequest +} + +func (c Container) ConnectURL(ctx context.Context) (string, error) { + host, err := c.Container.Host(ctx) + if err != nil { + return "", errors.Wrapf(err, "failed to get host") + } + + mappedPort, err := c.Container.MappedPort(ctx, port) + if err != nil { + return "", errors.Wrapf(err, "failed to get mapped port for %s", port) + } + + return fmt.Sprintf("%s:%d", host, mappedPort.Int()), nil +} + +// NewContainer creates and (optionally) starts a Redis instance. +func NewContainer(ctx context.Context, req ContainerRequest) (*Container, error) { + + provider, err := req.ProviderType.GetProvider() + if err != nil { + return nil, err + } + + // With the current logic it's not really possible to allow other ports... + req.ExposedPorts = []string{port} + + if req.Env == nil { + req.Env = map[string]string{} + } + + if req.Version != "" && req.FromDockerfile.Context == "" { + req.Image = fmt.Sprintf("%s:%s", image, req.Version) + } + + if req.Image == "" && req.FromDockerfile.Context == "" { + req.Image = fmt.Sprintf("%s:%s", image, tag) + } + + req.WaitingFor = wait.NewHostPortStrategy(port) + + c, err := provider.CreateContainer(ctx, req.ContainerRequest) + if err != nil { + return nil, errors.Wrap(err, "failed to create container") + } + + res := &Container{ + Container: c, + req: req, + } + + if err := c.Start(ctx); err != nil { + return res, errors.Wrap(err, "failed to start container") + } + + return res, nil +} diff --git a/canned/redis/redis_test.go b/canned/redis/redis_test.go new file mode 100644 index 0000000000..ea52870916 --- /dev/null +++ b/canned/redis/redis_test.go @@ -0,0 +1,55 @@ +package redis + +import ( + "context" + "github.com/go-redis/redis" + "gotest.tools/assert" + "testing" + + "github.com/testcontainers/testcontainers-go" +) + +func TestSetInRedis(t *testing.T) { + ctx := context.Background() + + c, err := NewContainer(ctx, ContainerRequest{}) + if err != nil { + t.Fatal(err) + } + defer c.Container.Terminate(ctx) + + addr, err := c.ConnectURL(ctx) + if err != nil { + t.Fatal(err) + } + + client := redis.NewClient(&redis.Options{ + Addr: addr, + DB: 0, + }) + defer client.Close() + + err = client.Set("key", "value", 0).Err() + if err != nil { + t.Fatal(err) + } + + got := client.Get("key") + if got.Err() != nil { + t.Fatal(got.Err()) + } + + assert.Equal(t, got.Val(), "value") +} + +func ExampleNewContainer() { + ctx := context.Background() + + c, _ := NewContainer(ctx, ContainerRequest{ + GenericContainerRequest: testcontainers.GenericContainerRequest{ + Started: true, + }, + }) + + defer c.Container.Terminate(ctx) +} diff --git a/container.go b/container.go index 15c8cadab0..37cdf53142 100644 --- a/container.go +++ b/container.go @@ -97,7 +97,7 @@ func (t ProviderType) GetProvider() (GenericProvider, error) { return nil, errors.New("unknown provider") } -// Validate ensures that the ContainerRequest does not have invalid paramters configured to it +// Validate ensures that the ContainerRequest does not have invalid parameters configured to it // ex. make sure you are not specifying both an image as well as a context func (c *ContainerRequest) Validate() error { diff --git a/go.mod b/go.mod index e80ce251e5..5fa7b95620 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/mux v1.6.2 // indirect github.com/kr/pretty v0.1.0 // indirect + github.com/lib/pq v1.2.0 github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/gomega v1.5.0 // indirect @@ -30,7 +31,7 @@ require ( golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect google.golang.org/grpc v1.17.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gotest.tools v0.0.0-20181223230014-1083505acf35 // indirect + gotest.tools v0.0.0-20181223230014-1083505acf35 ) go 1.13 diff --git a/go.sum b/go.sum index 9a5fabe51f..1f1e38e37e 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=