Initial QSfera import
This commit is contained in:
+538
@@ -0,0 +1,538 @@
|
||||
package testcontainers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/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
|
||||
}
|
||||
}
|
||||
|
||||
// WithDockerfile allows to build a container from a Dockerfile
|
||||
func WithDockerfile(df FromDockerfile) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.FromDockerfile = df
|
||||
|
||||
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.EndpointSettingsModifier = 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{}
|
||||
}
|
||||
|
||||
maps.Copy(req.Env, envs)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// WithName will set the name of the container.
|
||||
func WithName(containerName string) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
if containerName == "" {
|
||||
return errors.New("container name must be provided")
|
||||
}
|
||||
req.Name = containerName
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoStart will prevent the container from being started after creation.
|
||||
func WithNoStart() CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.Started = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithReuseByName will mark a container to be reused if it exists or create a new one if it doesn't.
|
||||
// A container name must be provided to identify the container to be reused.
|
||||
func WithReuseByName(containerName string) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
if err := WithName(containerName)(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Reuse = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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 path.Join(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 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 path.Join(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
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogConsumerConfig sets the log consumer config for a container.
|
||||
// Beware that this option completely replaces the existing log consumer config,
|
||||
// including the log consumers and the log production options,
|
||||
// so it should be used with care.
|
||||
func WithLogConsumerConfig(config *LogConsumerConfig) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.LogConsumerCfg = config
|
||||
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, opts ...tcexec.ProcessOption) RawCommand {
|
||||
return RawCommand{
|
||||
cmds: cmds,
|
||||
ExecOptions: ExecOptions{
|
||||
opts: opts,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := make([]ContainerHook, 0, len(execs))
|
||||
|
||||
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 replaces the wait strategy for a container, using 60 seconds as deadline
|
||||
func WithWaitStrategy(strategies ...wait.Strategy) CustomizeRequestOption {
|
||||
return WithWaitStrategyAndDeadline(60*time.Second, strategies...)
|
||||
}
|
||||
|
||||
// WithAdditionalWaitStrategy appends the wait strategy for a container, using 60 seconds as deadline
|
||||
func WithAdditionalWaitStrategy(strategies ...wait.Strategy) CustomizeRequestOption {
|
||||
return WithAdditionalWaitStrategyAndDeadline(60*time.Second, strategies...)
|
||||
}
|
||||
|
||||
// WithWaitStrategyAndDeadline replaces 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
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdditionalWaitStrategyAndDeadline appends the wait strategy for a container, including deadline
|
||||
func WithAdditionalWaitStrategyAndDeadline(deadline time.Duration, strategies ...wait.Strategy) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
if req.WaitingFor == nil {
|
||||
req.WaitingFor = wait.ForAll(strategies...).WithDeadline(deadline)
|
||||
return nil
|
||||
}
|
||||
|
||||
wss := make([]wait.Strategy, 0, len(strategies)+1)
|
||||
wss = append(wss, req.WaitingFor)
|
||||
wss = append(wss, strategies...)
|
||||
|
||||
req.WaitingFor = wait.ForAll(wss...).WithDeadline(deadline)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImageMount mounts an image to a container, passing the source image name,
|
||||
// the relative subpath to mount in that image, and the mount point in the target container.
|
||||
// This option validates that the subpath is a relative path, raising an error otherwise.
|
||||
func WithImageMount(source string, subpath string, target ContainerMountTarget) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
src := NewDockerImageMountSource(source, subpath)
|
||||
|
||||
if err := src.Validate(); err != nil {
|
||||
return fmt.Errorf("validate image mount source: %w", err)
|
||||
}
|
||||
|
||||
req.Mounts = append(req.Mounts, ContainerMount{
|
||||
Source: src,
|
||||
Target: target,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAlwaysPull will pull the image before starting the container
|
||||
func WithAlwaysPull() CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.AlwaysPullImage = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImagePlatform sets the platform for a container
|
||||
func WithImagePlatform(platform string) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.ImagePlatform = platform
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEntrypoint completely replaces the entrypoint of a container
|
||||
func WithEntrypoint(entrypoint ...string) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.Entrypoint = entrypoint
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEntrypointArgs appends the entrypoint arguments to the entrypoint of a container
|
||||
func WithEntrypointArgs(entrypointArgs ...string) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.Entrypoint = append(req.Entrypoint, entrypointArgs...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithExposedPorts appends the ports to the exposed ports for a container
|
||||
func WithExposedPorts(ports ...string) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.ExposedPorts = append(req.ExposedPorts, ports...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCmd completely replaces the command for a container
|
||||
func WithCmd(cmd ...string) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.Cmd = cmd
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCmdArgs appends the command arguments to the command for a container
|
||||
func WithCmdArgs(cmdArgs ...string) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.Cmd = append(req.Cmd, cmdArgs...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithLabels appends the labels to the labels for a container
|
||||
func WithLabels(labels map[string]string) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
if req.Labels == nil {
|
||||
req.Labels = make(map[string]string)
|
||||
}
|
||||
maps.Copy(req.Labels, labels)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithLifecycleHooks completely replaces the lifecycle hooks for a container
|
||||
func WithLifecycleHooks(hooks ...ContainerLifecycleHooks) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.LifecycleHooks = hooks
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdditionalLifecycleHooks appends lifecycle hooks to the existing ones for a container
|
||||
func WithAdditionalLifecycleHooks(hooks ...ContainerLifecycleHooks) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.LifecycleHooks = append(req.LifecycleHooks, hooks...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMounts appends the mounts to the mounts for a container
|
||||
func WithMounts(mounts ...ContainerMount) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.Mounts = append(req.Mounts, mounts...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTmpfs appends the tmpfs mounts to the tmpfs mounts for a container
|
||||
func WithTmpfs(tmpfs map[string]string) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
if req.Tmpfs == nil {
|
||||
req.Tmpfs = make(map[string]string)
|
||||
}
|
||||
maps.Copy(req.Tmpfs, tmpfs)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithFiles appends the files to the files for a container
|
||||
func WithFiles(files ...ContainerFile) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.Files = append(req.Files, files...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithProvider sets the provider type for a container
|
||||
func WithProvider(provider ProviderType) CustomizeRequestOption {
|
||||
return func(req *GenericContainerRequest) error {
|
||||
req.ProviderType = provider
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user