326 lines
10 KiB
Go
326 lines
10 KiB
Go
package testcontainers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"dario.cat/mergo"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/network"
|
|
|
|
tcexec "github.com/testcontainers/testcontainers-go/exec"
|
|
"github.com/testcontainers/testcontainers-go/internal/core"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
)
|
|
|
|
// ContainerCustomizer is an interface that can be used to configure the Testcontainers container
|
|
// request. The passed request will be merged with the default one.
|
|
type ContainerCustomizer interface {
|
|
Customize(req *GenericContainerRequest) error
|
|
}
|
|
|
|
// CustomizeRequestOption is a type that can be used to configure the Testcontainers container request.
|
|
// The passed request will be merged with the default one.
|
|
type CustomizeRequestOption func(req *GenericContainerRequest) error
|
|
|
|
func (opt CustomizeRequestOption) Customize(req *GenericContainerRequest) error {
|
|
return opt(req)
|
|
}
|
|
|
|
// CustomizeRequest returns a function that can be used to merge the passed container request with the one that is used by the container.
|
|
// Slices and Maps will be appended.
|
|
func CustomizeRequest(src GenericContainerRequest) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
if err := mergo.Merge(req, &src, mergo.WithOverride, mergo.WithAppendSlice); err != nil {
|
|
return fmt.Errorf("error merging container request, keeping the original one: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithConfigModifier allows to override the default container config
|
|
func WithConfigModifier(modifier func(config *container.Config)) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
req.ConfigModifier = modifier
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithEndpointSettingsModifier allows to override the default endpoint settings
|
|
func WithEndpointSettingsModifier(modifier func(settings map[string]*network.EndpointSettings)) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
req.EnpointSettingsModifier = modifier
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithEnv sets the environment variables for a container.
|
|
// If the environment variable already exists, it will be overridden.
|
|
func WithEnv(envs map[string]string) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
if req.Env == nil {
|
|
req.Env = map[string]string{}
|
|
}
|
|
|
|
for key, val := range envs {
|
|
req.Env[key] = val
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithHostConfigModifier allows to override the default host config
|
|
func WithHostConfigModifier(modifier func(hostConfig *container.HostConfig)) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
req.HostConfigModifier = modifier
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithHostPortAccess allows to expose the host ports to the container
|
|
func WithHostPortAccess(ports ...int) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
if req.HostAccessPorts == nil {
|
|
req.HostAccessPorts = []int{}
|
|
}
|
|
|
|
req.HostAccessPorts = append(req.HostAccessPorts, ports...)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Deprecated: the modules API forces passing the image as part of the signature of the Run function.
|
|
// WithImage sets the image for a container
|
|
func WithImage(image string) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
req.Image = image
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// imageSubstitutor {
|
|
|
|
// ImageSubstitutor represents a way to substitute container image names
|
|
type ImageSubstitutor interface {
|
|
// Description returns the name of the type and a short description of how it modifies the image.
|
|
// Useful to be printed in logs
|
|
Description() string
|
|
Substitute(image string) (string, error)
|
|
}
|
|
|
|
// }
|
|
|
|
// CustomHubSubstitutor represents a way to substitute the hub of an image with a custom one,
|
|
// using provided value with respect to the HubImageNamePrefix configuration value.
|
|
type CustomHubSubstitutor struct {
|
|
hub string
|
|
}
|
|
|
|
// NewCustomHubSubstitutor creates a new CustomHubSubstitutor
|
|
func NewCustomHubSubstitutor(hub string) CustomHubSubstitutor {
|
|
return CustomHubSubstitutor{
|
|
hub: hub,
|
|
}
|
|
}
|
|
|
|
// Description returns the name of the type and a short description of how it modifies the image.
|
|
func (c CustomHubSubstitutor) Description() string {
|
|
return fmt.Sprintf("CustomHubSubstitutor (replaces hub with %s)", c.hub)
|
|
}
|
|
|
|
// Substitute replaces the hub of the image with the provided one, with certain conditions:
|
|
// - if the hub is empty, the image is returned as is.
|
|
// - if the image already contains a registry, the image is returned as is.
|
|
// - if the HubImageNamePrefix configuration value is set, the image is returned as is.
|
|
func (c CustomHubSubstitutor) Substitute(image string) (string, error) {
|
|
registry := core.ExtractRegistry(image, "")
|
|
cfg := ReadConfig()
|
|
|
|
exclusions := []func() bool{
|
|
func() bool { return c.hub == "" },
|
|
func() bool { return registry != "" },
|
|
func() bool { return cfg.Config.HubImageNamePrefix != "" },
|
|
}
|
|
|
|
for _, exclusion := range exclusions {
|
|
if exclusion() {
|
|
return image, nil
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s", c.hub, image), nil
|
|
}
|
|
|
|
// prependHubRegistry represents a way to prepend a custom Hub registry to the image name,
|
|
// using the HubImageNamePrefix configuration value
|
|
type prependHubRegistry struct {
|
|
prefix string
|
|
}
|
|
|
|
// newPrependHubRegistry creates a new prependHubRegistry
|
|
func newPrependHubRegistry(hubPrefix string) prependHubRegistry {
|
|
return prependHubRegistry{
|
|
prefix: hubPrefix,
|
|
}
|
|
}
|
|
|
|
// Description returns the name of the type and a short description of how it modifies the image.
|
|
func (p prependHubRegistry) Description() string {
|
|
return fmt.Sprintf("HubImageSubstitutor (prepends %s)", p.prefix)
|
|
}
|
|
|
|
// Substitute prepends the Hub prefix to the image name, with certain conditions:
|
|
// - if the prefix is empty, the image is returned as is.
|
|
// - if the image is a non-hub image (e.g. where another registry is set), the image is returned as is.
|
|
// - if the image is a Docker Hub image where the hub registry is explicitly part of the name
|
|
// (i.e. anything with a docker.io or registry.hub.docker.com host part), the image is returned as is.
|
|
func (p prependHubRegistry) Substitute(image string) (string, error) {
|
|
registry := core.ExtractRegistry(image, "")
|
|
|
|
// add the exclusions in the right order
|
|
exclusions := []func() bool{
|
|
func() bool { return p.prefix == "" }, // no prefix set at the configuration level
|
|
func() bool { return registry != "" }, // non-hub image
|
|
func() bool { return registry == "docker.io" }, // explicitly including docker.io
|
|
func() bool { return registry == "registry.hub.docker.com" }, // explicitly including registry.hub.docker.com
|
|
}
|
|
|
|
for _, exclusion := range exclusions {
|
|
if exclusion() {
|
|
return image, nil
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s", p.prefix, image), nil
|
|
}
|
|
|
|
// WithImageSubstitutors sets the image substitutors for a container
|
|
func WithImageSubstitutors(fn ...ImageSubstitutor) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
req.ImageSubstitutors = fn
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithLogConsumers sets the log consumers for a container
|
|
func WithLogConsumers(consumer ...LogConsumer) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
if req.LogConsumerCfg == nil {
|
|
req.LogConsumerCfg = &LogConsumerConfig{}
|
|
}
|
|
|
|
req.LogConsumerCfg.Consumers = consumer
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Executable represents an executable command to be sent to a container, including options,
|
|
// as part of the different lifecycle hooks.
|
|
type Executable interface {
|
|
AsCommand() []string
|
|
// Options can container two different types of options:
|
|
// - Docker's ExecConfigs (WithUser, WithWorkingDir, WithEnv, etc.)
|
|
// - testcontainers' ProcessOptions (i.e. Multiplexed response)
|
|
Options() []tcexec.ProcessOption
|
|
}
|
|
|
|
// ExecOptions is a struct that provides a default implementation for the Options method
|
|
// of the Executable interface.
|
|
type ExecOptions struct {
|
|
opts []tcexec.ProcessOption
|
|
}
|
|
|
|
func (ce ExecOptions) Options() []tcexec.ProcessOption {
|
|
return ce.opts
|
|
}
|
|
|
|
// RawCommand is a type that implements Executable and represents a command to be sent to a container
|
|
type RawCommand struct {
|
|
ExecOptions
|
|
cmds []string
|
|
}
|
|
|
|
func NewRawCommand(cmds []string) RawCommand {
|
|
return RawCommand{
|
|
cmds: cmds,
|
|
ExecOptions: ExecOptions{
|
|
opts: []tcexec.ProcessOption{},
|
|
},
|
|
}
|
|
}
|
|
|
|
// AsCommand returns the command as a slice of strings
|
|
func (r RawCommand) AsCommand() []string {
|
|
return r.cmds
|
|
}
|
|
|
|
// WithStartupCommand will execute the command representation of each Executable into the container.
|
|
// It will leverage the container lifecycle hooks to call the command right after the container
|
|
// is started.
|
|
func WithStartupCommand(execs ...Executable) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
startupCommandsHook := ContainerLifecycleHooks{
|
|
PostStarts: []ContainerHook{},
|
|
}
|
|
|
|
for _, exec := range execs {
|
|
execFn := func(ctx context.Context, c Container) error {
|
|
_, _, err := c.Exec(ctx, exec.AsCommand(), exec.Options()...)
|
|
return err
|
|
}
|
|
|
|
startupCommandsHook.PostStarts = append(startupCommandsHook.PostStarts, execFn)
|
|
}
|
|
|
|
req.LifecycleHooks = append(req.LifecycleHooks, startupCommandsHook)
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithAfterReadyCommand will execute the command representation of each Executable into the container.
|
|
// It will leverage the container lifecycle hooks to call the command right after the container
|
|
// is ready.
|
|
func WithAfterReadyCommand(execs ...Executable) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
postReadiesHook := []ContainerHook{}
|
|
|
|
for _, exec := range execs {
|
|
execFn := func(ctx context.Context, c Container) error {
|
|
_, _, err := c.Exec(ctx, exec.AsCommand(), exec.Options()...)
|
|
return err
|
|
}
|
|
|
|
postReadiesHook = append(postReadiesHook, execFn)
|
|
}
|
|
|
|
req.LifecycleHooks = append(req.LifecycleHooks, ContainerLifecycleHooks{
|
|
PostReadies: postReadiesHook,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithWaitStrategy sets the wait strategy for a container, using 60 seconds as deadline
|
|
func WithWaitStrategy(strategies ...wait.Strategy) CustomizeRequestOption {
|
|
return WithWaitStrategyAndDeadline(60*time.Second, strategies...)
|
|
}
|
|
|
|
// WithWaitStrategyAndDeadline sets the wait strategy for a container, including deadline
|
|
func WithWaitStrategyAndDeadline(deadline time.Duration, strategies ...wait.Strategy) CustomizeRequestOption {
|
|
return func(req *GenericContainerRequest) error {
|
|
req.WaitingFor = wait.ForAll(strategies...).WithDeadline(deadline)
|
|
|
|
return nil
|
|
}
|
|
}
|