diff --git a/go.mod b/go.mod index 47c010e..17385f1 100644 --- a/go.mod +++ b/go.mod @@ -8,3 +8,9 @@ require ( github.com/mattn/go-sqlite3 v1.14.18 golang.org/x/sync v0.5.0 ) + +require ( + github.com/go-sql-driver/mysql v1.7.1 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum index 5beacd1..bca01a4 100644 --- a/go.sum +++ b/go.sum @@ -63,8 +63,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= @@ -342,8 +342,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -404,8 +404,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/testcontainers/minio/examples/container_test.go b/testcontainers/minio/examples/container_test.go new file mode 100644 index 0000000..857c0fa --- /dev/null +++ b/testcontainers/minio/examples/container_test.go @@ -0,0 +1,114 @@ +// https://github.com/minio/minio/blob/master/docs/orchestration/docker-compose/docker-compose.yaml +// https://github.com/mmadfox/testcontainers/blob/master/minio/minio_test.go +// https://github.com/mmadfox/testcontainers/blob/master/minio/minio.go +// https://github.com/testcontainers/testcontainers-go/blob/main/examples/cockroachdb/cockroachdb.go +// https://dev.to/minhblues/easy-file-uploads-in-go-fiber-with-minio-393c +package examples + +import ( + "context" + "fmt" + "io" + "testing" + "time" + + "github.com/adoublef/sdk/bytest" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +func TestMinio(t *testing.T) { + ctx := context.Background() + + minioC, err := setupMinio(ctx) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + if err := minioC.Terminate(ctx); err != nil { + t.Fatalf("failed to terminate container: %s", err) + } + }) + + c, err := minio.New(minioC.URI, &minio.Options{ + Creds: credentials.NewStaticV4("minioadmin", "minioadmin", ""), // seems to play no affect + Secure: false, + }) + if err != nil { + t.Fatal(err) + } + + bucketName := "testcontainers" + location := "eu-west-2" + + // create bucket + err = c.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: location}) + if err != nil { + t.Fatal(err) + } + + objectName := "testdata" + contentType := "applcation/octet-stream" + + uploadInfo, err := c.PutObject(ctx, bucketName, objectName, bytest.NewReader(bytest.MB*16), (bytest.MB * 16), minio.PutObjectOptions{ContentType: contentType}) + if err != nil { + t.Fatal(err) + } + + // object is a readSeekCloser + object, err := c.GetObject(ctx, uploadInfo.Bucket, uploadInfo.Key, minio.GetObjectOptions{}) + if err != nil { + t.Fatal(err) + } + defer object.Close() + + n, err := io.Copy(io.Discard, object) + if err != nil { + t.Fatal(err) + } + + if n != bytest.MB*16 { + t.Fatalf("expected %d; got %d", bytest.MB*16, n) + } +} + +type minioContainer struct { + testcontainers.Container + URI string +} + +func setupMinio(ctx context.Context) (*minioContainer, error) { + req := testcontainers.ContainerRequest{ + Image: "minio/minio:RELEASE.2024-01-16T16-07-38Z", + ExposedPorts: []string{"9000/tcp", "9001/tcp"}, + Env: map[string]string{ + "MINIO_ROOT_USER": "minioadmin", + "MINIO_ROOT_PASSWORD": "minioadmin", + }, + Cmd: []string{"server", "/data"}, + WaitingFor: wait.ForListeningPort("9000").WithStartupTimeout(time.Minute * 2), + } + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return nil, err + } + + mappedPort, err := container.MappedPort(ctx, "9000") + if err != nil { + return nil, err + } + + hostIP, err := container.Host(ctx) + if err != nil { + return nil, err + } + + uri := fmt.Sprintf("%s:%s", hostIP, mappedPort.Port()) + return &minioContainer{Container: container, URI: uri}, nil +} diff --git a/testcontainers/minio/minio.go b/testcontainers/minio/minio.go new file mode 100644 index 0000000..7b02ca1 --- /dev/null +++ b/testcontainers/minio/minio.go @@ -0,0 +1,89 @@ +package minio + +import ( + "context" + "fmt" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +const ( + defaultUser = "minioadmin" + defaultPassword = "minioadmin" + defaultImage = "minio/minio:RELEASE.2024-01-16T16-07-38Z" +) + +// MinioContainer +type MinioContainer struct { + testcontainers.Container + Username string + Password string +} + +func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*MinioContainer, error) { + req := testcontainers.ContainerRequest{ + Image: defaultImage, + ExposedPorts: []string{"9000/tcp"}, + WaitingFor: wait.ForListeningPort("9000").WithStartupTimeout(time.Minute * 2), + Env: map[string]string{ + "MINIO_ROOT_USER": defaultUser, + "MINIO_ROOT_PASSWORD": defaultPassword, + }, + Cmd: []string{"server", "/data"}, + } + + genericContainerReq := testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + for _, opt := range opts { + opt.Customize(&genericContainerReq) + } + username := req.Env["MINIO_ROOT_USER"] + password := req.Env["MINIO_ROOT_PASSWORD"] + if username == "" || password == "" { + return nil, fmt.Errorf("username or password has not been set") + } + + container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + if err != nil { + return nil, err + } + return &MinioContainer{Container: container, Username: username, Password: password}, nil +} + +// WithUsername +func WithUsername(username string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) { + if username == "" { + username = defaultUser + } + req.Env["MINIO_ROOT_USER"] = username + } +} + +// WithPassword +func WithPassword(password string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) { + if password == "" { + password = defaultPassword + } + req.Env["MINIO_ROOT_PASSWORD"] = password + } +} + +// ConnectionString +func (c *MinioContainer) ConnectionString(ctx context.Context) (string, error) { + host, err := c.Host(ctx) + if err != nil { + return "", err + } + port, err := c.MappedPort(ctx, "9000/tcp") + if err != nil { + return "", err + } + return fmt.Sprintf("%s:%s", host, port.Port()), nil +} diff --git a/testcontainers/minio/minio_test.go b/testcontainers/minio/minio_test.go new file mode 100644 index 0000000..d07fc20 --- /dev/null +++ b/testcontainers/minio/minio_test.go @@ -0,0 +1,73 @@ +package minio_test + +import ( + "context" + "io" + "testing" + + "github.com/adoublef/sdk/bytest" + . "github.com/adoublef/sdk/testcontainers/minio" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +func TestContainer(t *testing.T) { + ctx := context.Background() + + minioContainer, err := RunContainer(ctx, WithUsername("username"), WithPassword("password")) + if err != nil { + t.Fatal(err) + } + + defer func() { + if err := minioContainer.Terminate(ctx); err != nil { + panic(err) + } + }() + + url, err := minioContainer.ConnectionString(ctx) + if err != nil { + t.Fatal(err) + } + + minioClient, err := minio.New(url, &minio.Options{ + Creds: credentials.NewStaticV4(minioContainer.Username, minioContainer.Password, ""), // seems to play no affect + Secure: false, + }) + if err != nil { + t.Fatal(err) + } + + bucketName := "testcontainers" + location := "eu-west-2" + + // create bucket + err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: location}) + if err != nil { + t.Fatal(err) + } + + objectName := "testdata" + contentType := "applcation/octet-stream" + + uploadInfo, err := minioClient.PutObject(ctx, bucketName, objectName, bytest.NewReader(bytest.MB*16), (bytest.MB * 16), minio.PutObjectOptions{ContentType: contentType}) + if err != nil { + t.Fatal(err) + } + + // object is a readSeekCloser + object, err := minioClient.GetObject(ctx, uploadInfo.Bucket, uploadInfo.Key, minio.GetObjectOptions{}) + if err != nil { + t.Fatal(err) + } + defer object.Close() + + n, err := io.Copy(io.Discard, object) + if err != nil { + t.Fatal(err) + } + + if n != bytest.MB*16 { + t.Fatalf("expected %d; got %d", bytest.MB*16, n) + } +}