Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use an interface for container customization in modules #1042

Merged
merged 13 commits into from
Apr 14, 2023
Merged
93 changes: 59 additions & 34 deletions docs/modules/couchbase.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,42 @@ go get github.com/testcontainers/testcontainers-go/modules/couchbase

## Usage example

1. The **StartContainer** function is the main entry point to create a new CouchbaseContainer instance.
It takes a context and zero or more Option values to configure the container.
It creates a new container instance, initializes the couchbase cluster, and creates buckets.
If successful, it returns the **CouchbaseContainer** instance.

<!--codeinclude-->
[Start Couchbase](../../modules/couchbase/couchbase_test.go) inside_block:withBucket
<!--/codeinclude-->

2. The **ConnectionString** method returns the connection string to connect to the Couchbase container instance.
It returns a string with the format `couchbase://<host>:<port>`.
The **Username** method returns the username of the Couchbase administrator.
The **Password** method returns the password of the Couchbase administrator.

<!--codeinclude-->
[Connect to Couchbase](../../modules/couchbase/couchbase_test.go) inside_block:connectToCluster
<!--/codeinclude-->

## Module Reference

The Couchbase module exposes one entrypoint function to create the Couchbase container, and this function receives two parameters:

```golang
func StartContainer(ctx context.Context, opts ...Option) (*CouchbaseContainer, error)
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*CouchbaseContainer, error)
```

- `context.Context`, the Go context.
- `Option`, a variad argument for passing options.
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.

Once the container is started, it will perform the following operations, **in this particular order**:

* Wait until the node is online, waiting for the `/pools` endpoint in the management port to return a 200 HTTP status code.
* Check for Enterprise services, sending a GET request to the `/pools` endpoint in the management port. If the response contains the `isEnterprise` key set to `false`, it will check if the Analytics or the Eventing services are enabled. If so, it will raise an error.
* Rename the node, sending a POST request to the `/node/controller/rename` endpoint in the management port.
* Initialize the services, sending a POST request to the `/node/controller/setupServices` endpoint in the management port, passing as body of the request the list of enabled services.
* Set the memory quotas, sending a POST request to the `/pools/default` endpoint in the management port, passing as body of the request the memory quota for each enabled service.
* Configure the Admin user, sending a POST request to the `/settings/web` endpoint in the management port, passing as body of the request the username and password of the admin user.
* Configure the external ports, sending a POST request to the `/node/controller/setupAlternateAddresses/external` endpoint in the management port, passing as body of the request the external mapped ports for each enabled service.
* If the `Index` service is enabled, configure the indexer, sending a POST request to the `/settings/indexes` endpoint in the management port, passing as body of the request the defined storage mode. If the Community Edition is used, it will make sure the storage mode is `forestdb`. If the Enterprise Edition is used, it will make sure the storage mode is not `forestdb`, changing to `memory_optimized` in that case.
* Finally, it will wait for all nodes to be healthy. Depending of the enabled services, it will use a different wait strategy to check if the node is healthy:
- It will wait for the `/pools/default` endpoint in the management port to return a 200 HTTP status code and the response body to contain the `healthy` key set to `true`.
- If the `Query` service is enabled, it will wait for the `/admin/ping` endpoint in the query port to return a 200 HTTP status code.
- If the `Analytics` service is enabled, it will wait for the `/admin/ping` endpoint in the analytics port to return a 200 HTTP status code.
- If the `Eventing` service is enabled, it will wait for the `/api/v1/config` endpoint in the eventing port to return a 200 HTTP status code.

### Container Ports

<!--codeinclude-->
[Container Ports](../../modules/couchbase/couchbase.go) inside_block:containerPorts
<!--/codeinclude-->

### Container Options

Expand All @@ -60,19 +68,16 @@ By default, the container will use the following Docker image:

#### Credentials

If you need to change the default credentials for the admin user, you can use `WithCredentials(user, password)` with a valid username and password.
If you need to change the default credentials for the admin user, you can use `WithAdminCredentials(user, password)` with a valid username and password.
When the password has less than 6 characters, the container won't be created and the `RunContainer` function will throw an error.

!!!info
The default username is `Administrator` and the default password is `password`.

#### Bucket

When creating a new Couchbase container, you can create one or more buckets. The module provides a `NewBucket` function to create a new bucket, where
you can pass the bucket name.
#### Buckets

<!--codeinclude-->
[Adding a new bucket](../../modules/couchbase/couchbase_test.go) inside_block:withBucket
<!--/codeinclude-->
When creating a new Couchbase container, you can create one or more buckets. The module provides with a `WithBuckets` function that accepts an array of buckets to be created.
To create a new bucket, the module exposes a `NewBucket` function, where you can pass the bucket name.

It's possible to customize a newly created bucket, using the following options:

Expand All @@ -81,15 +86,9 @@ It's possible to customize a newly created bucket, using the following options:
- `WithFlushEnabled`: sets whether the bucket should be flushed when the container is stopped.
- `WithPrimaryIndex`: sets whether the primary index should be created for this bucket.

```go
bucket := NewBucket(
"bucketName",
WithQuota(100),
WithReplicas(1),
WithFlushEnabled(true),
WithPrimaryIndex(true),
)
```
<!--codeinclude-->
[Adding a new bucket](../../modules/couchbase/couchbase_test.go) inside_block:withBucket
<!--/codeinclude-->

#### Index Storage

Expand All @@ -108,9 +107,35 @@ By default, the container will start with the following services: `kv`, `n1ql`,

!!!warning
When running the Enterprise Edition of Couchbase Server, the module provides two functions to enable or disable services:
`WithAnalyticsService` and `WithEventingService`. Else, it will throw an error and the container won't be created.
`WithServiceAnalytics` and `WithServiceEventing`. Else, it will throw an error and the container won't be created.

<!--codeinclude-->
[Docker images](../../modules/couchbase/couchbase_test.go) inside_block:dockerImages
<!--/codeinclude-->

### Container Methods

#### ConnectionString

The `ConnectionString` method returns the connection string to connect to the Couchbase container instance.
It returns a string with the format `couchbase://<host>:<port>`.

<!--codeinclude-->
[Connect to Couchbase](../../modules/couchbase/couchbase_test.go) inside_block:connectToCluster
<!--/codeinclude-->

#### Username

The `Username` method returns the username of the Couchbase administrator.

<!--codeinclude-->
[Connect to Couchbase using Credentials](../../modules/couchbase/couchbase_test.go) inside_block:getCredentials
<!--/codeinclude-->

#### Password

The `Password` method returns the password of the Couchbase administrator.

<!--codeinclude-->
[Connect to Couchbase using Credentials](../../modules/couchbase/couchbase_test.go) inside_block:getCredentials
<!--/codeinclude-->
54 changes: 51 additions & 3 deletions docs/modules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,59 @@ We are going to propose a set of steps to follow when adding types and methods t

- Make sure a public `Container` type exists for the module. This type have to use composition to embed the `testcontainers.Container` type, inheriting all the methods from it.
- Make sure a `RunContainer` function exists and is public. This function is the entrypoint to the module and will define the initial values for a `testcontainers.GenericContainerRequest` struct, including the image, the default exposed ports, wait strategies, etc. Therefore, the function must initialise the container request with the default values.
- Define container options for the module. We consider that a best practice for the options is to return a function that returns a modified `testcontainers.GenericContainerRequest` type, and for that, the library already provides with a `testcontainers.CustomizeRequestOption` type representing this function signature.
- Define container options for the module leveraging the `testcontainers.ContainerCustomizer` interface, that has one single method: `Customize(req *GenericContainerRequest)`.
- We consider that a best practice for the options is define a function using the `With` prefix, that returns a function returning a modified `testcontainers.GenericContainerRequest` type. For that, the library already provides with a `testcontainers.CustomizeRequestOption` type implementing the `ContainerCustomizer` interface, and we encourage you use this type for creating your own customizer functions.
- At the same time, you could need to create your own container customizers for your module. Make sure they implement the `testcontainers.ContainerCustomizer` interface. Defining your own customizer functions is useful when you need to transfer certain state that is not present at the `ContainerRequest` to the container, possibly using an intermediate Config struct.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fbiville explained here in the docs

- The options will be passed to the `RunContainer` function as variadic arguments after the Go context, and they will be processed right after defining the initial `testcontainers.GenericContainerRequest` struct using a for loop.

```golang
func RunContainer(ctx context.Context, opts ...testcontainers.CustomizeRequestOption) (*Container, error) {...}
// Config type represents an intermediate struct for transferring state from the options to the container
type Config struct {
data string
}

func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*Container, error) {
cfg := Config{}

req := testcontainers.ContainerRequest{
Image: "my-image",
...
}
genericContainerReq := testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
}
...
for _, opt := range opts {
req = opt.Customize(&genericContainerReq)

// If you need to transfer some state from the options to the container, you can do it here
if myCustomizer, ok := opt.(MyCustomizer); ok {
config.data = customizer.data
}
}
...
container, err := testcontainers.GenericContainer(ctx, genericContainerReq)
...
moduleContainer := &Container{Container: container}
moduleContainer.initializeState(ctx, cfg)
...
return moduleContainer, nil
}

// MyCustomizer type represents a container customizer for transferring state from the options to the container
type MyCustomizer struct {
data string
}
// Customize method implementation
func (c MyCustomizer) Customize(req *testcontainers.GenericContainerRequest) testcontainers.ContainerRequest {
req.ExposedPorts = append(req.ExposedPorts, "1234/tcp")
return req.ContainerRequest
}
// WithMy function option to use the customizer
func WithMy(data string) testcontainers.ContainerCustomizer {
return MyCustomizer{data: data}
}
```

- If needed, define public methods to extract information from the running container, using the `Container` type as receiver. E.g. a connection string to access a database:
Expand All @@ -73,7 +121,7 @@ func (c *Container) ConnectionString(ctx context.Context) (string, error) {...}

### ContainerRequest options

In order to simplify the creation of the container for a given module, `Testcontainers for Go` provides with a set of `testcontainers.CustomizeRequestOption` functions to customise the container request for the module. These options are:
In order to simplify the creation of the container for a given module, `Testcontainers for Go` provides with a set of `testcontainers.CustomizeRequestOption` functions to customize the container request for the module. These options are:

- `testcontainers.CustomizeRequest`: a function that merges the default options with the ones provided by the user. Recommended for completely customising the container request.
- `testcontainers.WithImage`: a function that sets the image for the container request.
Expand Down
39 changes: 28 additions & 11 deletions docs/modules/localstack.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,16 @@ For further reference on the SDK v1, please check out the AWS docs [here](https:

For further reference on the SDK v2, please check out the AWS docs [here](https://aws.github.io/aws-sdk-go-v2/docs/getting-started)

## `HOSTNAME_EXTERNAL` and hostname-sensitive services
## Accessing hostname-sensitive services

Some Localstack APIs, such as SQS, require the container to be aware of the hostname that it is accessible on - for example, for construction of queue URLs in responses.

Testcontainers will inform Localstack of the best hostname automatically, using the `HOSTNAME_EXTERNAL` environment variable:
Testcontainers will inform Localstack of the best hostname automatically, using the an environment variable for that:

* for Localstack versions 0.10.0 and above, the `HOSTNAME_EXTERNAL` environment variable will be set to hostname in the container request.
* for Localstack versions 2.0.0 and above, the `LOCALSTACK_HOST` environment variable will be set to the hostname in the container request.

Once the variable is set:

* when running the Localstack container directly without a custom network defined, it is expected that all calls to the container will be from the test host. As such, the container address will be used (typically localhost or the address where the Docker daemon is running).

Expand All @@ -64,32 +69,44 @@ Testcontainers will inform Localstack of the best hostname automatically, using
The LocalStack module exposes one single function to create the LocalStack container, and this function receives two parameters:

```golang
func StartContainer(ctx context.Context, overrideReq OverrideContainerRequestOption) (*LocalStackContainer, error)
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*LocalStackContainer, error)
```

- `context.Context`
- `OverrideContainerRequestOption`
- `context.Context`, the Go context.
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.

### Container Options

When starting the Localstack container, you can pass options in a variadic way to configure it.

#### Set Image

By default, the image used is `localstack:1.4.0`. If you need to use a different image, you can use `testcontainers.WithImage` option.

<!--codeinclude-->
[Custom Image](../../modules/localstack/localstack_test.go) inside_block:withImage
<!--/codeinclude-->

### OverrideContainerRequestOption
#### Customize the container request

The `OverrideContainerRequestOption` functional option represents a way to override the default LocalStack container request:
It's possible to entirely override the default LocalStack container request:

<!--codeinclude-->
[Default container request](../../modules/localstack/localstack.go) inside_block:defaultContainerRequest
[Customize container request](../../modules/localstack/localstack_test.go) inside_block:withCustomContainerRequest
<!--/codeinclude-->

With simply passing your own instance of an `OverrideContainerRequestOption` type to the `StartContainer` function, you'll be able to configure the LocalStack container with your own needs, as this new container request will be merged with the original one.
With simply passing the `testcontainers.CustomizeRequest` functional option to the `RunContainer` function, you'll be able to configure the LocalStack container with your own needs, as this new container request will be merged with the original one.

In the following example you check how it's possible to set certain environment variables that are needed by the tests, the most important of them the AWS services you want to use. Besides, the container runs in a separate Docker network with an alias:

<!--codeinclude-->
[Overriding the default container request](../../modules/localstack/localstack_test.go) inside_block:withNetwork
<!--/codeinclude-->

If you do not need to override the container request, you can pass `nil` or the `NoopOverrideContainerRequest` instance, which is exposed as a helper for this reason.
If you do not need to override the container request, you can simply pass the Go context to the `RunContainer` function.

<!--codeinclude-->
[Skip overriding the default container request](../../modules/localstack/localstack_test.go) inside_block:noopOverrideContainerRequest
[Skip overriding the default container request](../../modules/localstack/localstack_test.go) inside_block:noOverrideContainerRequest
<!--/codeinclude-->

## Testing the module
Expand Down
4 changes: 2 additions & 2 deletions docs/modules/mysql.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ go get github.com/testcontainers/testcontainers-go/modules/mysql
The MySQL module exposes one entrypoint function to create the container, and this function receives two parameters:

```golang
func RunContainer(ctx context.Context, opts ...testcontainers.CustomizeRequestOption) (*MySQLContainer, error) {
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*MySQLContainer, error) {
```

- `context.Context`, the Go context.
- `testcontainers.CustomizeRequestOption`, a variad argument for passing options.
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.

## Container Options

Expand Down
4 changes: 2 additions & 2 deletions docs/modules/postgres.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ go get github.com/testcontainers/testcontainers-go/modules/postgres
The Postgres module exposes one entrypoint function to create the Postgres container, and this function receives two parameters:

```golang
func RunContainer(ctx context.Context, opts ...testcontainers.CustomizeRequestOption) (*PostgresContainer, error)
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*PostgresContainer, error)
```

- `context.Context`, the Go context.
- `testcontainers.CustomizeRequestOption`, a variadic argument for passing options.
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.

### Container Options

Expand Down