tcontainer

package module
v0.0.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2024 License: MIT Imports: 18 Imported by: 0

README

tcontainer

PkgGoDev Go Report Card codecov

Wrapper over github.com/ory/dockertest

Provides additional conveniences for creating docker containers in tests:

  • More convenient syntax for creating containers using options
  • Ability to reuse a container if it already exists (RunOptions).Reuse
  • Ability to remove old container when creating a new one instead of getting ErrContainerAlreadyExists error
  • All containers are created with the label "tcontainer=tcontainer" You can quickly delete all test containers using the docker ps -aq --filter "label=tcontainer=tcontainer" | xargs docker rm -f command
  • Ability to fast remove old containers before / after test by (Pool).Prune()
  • Custom options like WithContainerName(t.Name())

Usage example

package tcontainer_test

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/ory/dockertest/v3"
	"github.com/ory/dockertest/v3/docker"

	"github.com/kiteggrad/tcontainer"
)

func ExamplePool_Run() {
	const containerAPIPort = "80"
	const serverHelloMesage = "Hello, World!"
	startServerCMD := fmt.Sprintf(`echo '%s' > /index.html && httpd -p %s -h / && tail -f /dev/null`, serverHelloMesage, containerAPIPort)

	// define function to check the server is ready
	url := ""
	pingServerRetry := func(container *dockertest.Resource) (err error) {
		url = "http://" + tcontainer.GetAPIEndpoints(container)[containerAPIPort].NetJoinHostPort()

		resp, err := http.Get(url)
		if err != nil {
			return fmt.Errorf("failed to http.Get: %w", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			return fmt.Errorf("unexpected response status `%s`", resp.Status)
		}

		return nil
	}

	pool := tcontainer.MustNewPool("")

	// you can remove all containers and images created by this package (from previous tests run)
	// in order to avoid errors like ErrContainerAlreadyExists
	err := pool.Prune(context.Background())
	if err != nil {
		panic(err)
	}

	// run container with the server
	container, err := pool.Run(
		context.Background(),
		"busybox",
		tcontainer.WithContainerName("my-test-server"),
		func(options *tcontainer.RunOptions) (err error) {
			// set by one field instead of *options = tcontainer.RunOptions{...}
			// in order to not owerride default values (like options.Retry.Timeout)

			options.Tag = "latest"
			options.Env = []string{"SOME_ENV=value"}
			options.Cmd = []string{"sh", "-c", startServerCMD}
			options.ExposedPorts = []string{containerAPIPort}
			options.HostConfig.AutoRemove = false
			options.HostConfig.RestartPolicy = docker.RestartPolicy{Name: "no", MaximumRetryCount: 0}
			options.Retry.Operation = pingServerRetry
			options.Reuse.Reuse = true
			options.Reuse.RecreateOnErr = true
			options.ContainerExpiry = time.Minute * 10
			options.HostConfig.PortBindings = map[docker.Port][]docker.PortBinding{
				"80": {{HostIP: "", HostPort: "8080"}},
			}
			options.RemoveOnExists = false // not compatible with Reuse option

			return nil
		},
	)
	if err != nil {
		panic(err)
	}
	defer container.Close() // not necessary if you want to WithReuseContainer

	// make request to the server
	resp, err := http.Get(url)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	responseBody, _ := io.ReadAll(resp.Body)

	fmt.Println(string(responseBody))

	// Output:
	// Hello, World!
}

Documentation

Index

Examples

Constants

View Source
const (
	DefaultLabelKeyValue = "tcontainer"
)
View Source
const (
	ImageLabelUUID = DefaultLabelKeyValue + ".uuid"
)

Variables

View Source
var (
	// ErrInvalidOptions - occurs when invalid value was passed to TestContainerOption.
	ErrInvalidOptions = errors.New("invalid option")
	// ErrOptionConflict - occurs when incompatible TestContainerOption have been passed.
	ErrOptionConflict = errors.New("conflicted options")
)
View Source
var (
	// ErrContainerAlreadyExists - occurs when the container already exists.
	ErrContainerAlreadyExists = docker.ErrContainerAlreadyExists
	// ErrUnreusableState - occurs when it's impossible to reuse container (see WithReuseContainer()).
	ErrUnreusableState = errors.New("imposible to reuse container with it's current state")
	// ErrReuseContainerConflict - occurs when existed container have different options (e.q. image tag).
	ErrReuseContainerConflict = errors.New("imposible to reuse container, it has differnent options")
)

Functions

func GetAPIEndpoints added in v0.0.4

func GetAPIEndpoints(container *dockertest.Resource) (endpointByPrivatePort map[PrivatePort]APIEndpoint)

GetAPIEndpoints - provides you APIEndpoint by each privatePort (port inside the container).

Types

type APIEndpoint added in v0.0.3

type APIEndpoint struct {
	IP   string // localhost/dockerGateway or container IP
	Port string // publicPort or private port
}

Endpoint that you can use to connect to the container.

Note: macOS users may encounter issues accessing the container through APIEndpoint from inside the container. This is because macOS users cannot use the container's IP directly, potentially leading to connectivity problems.

func (APIEndpoint) NetJoinHostPort added in v0.0.3

func (e APIEndpoint) NetJoinHostPort() string

NetJoinHostPort - combines ip and port into a network address of the form "host:port".

type BuildOption added in v0.0.5

type BuildOption func(options *BuildOptions) (err error)

BuildOption - option for (Pool).Build / (Pool).BuildAndGet functions. See ApplyBuildOptions.

func WithImageName added in v0.0.5

func WithImageName(nameParts ...string) BuildOption

WithImageName - use custom image name instead of random (generated by docker).

  • All invalid characters will be repaced to "/".
  • Not empty nameParts will be joined with "/" separator, empty parts will be removed.
  • Snake case will be applied.

Example:

WithImageName(t.Name(), "redis") // "Test/withInvalid|chars", "redis" -> "Test/with_invalid/chars/redis"

type BuildOptions added in v0.0.5

type BuildOptions struct {
	ImageName           string
	Dockerfile          string
	ContextDir          string
	BuildArgs           []docker.BuildArg
	Platform            string
	NoCache             bool
	CacheFrom           []string
	SuppressOutput      bool
	Pull                bool
	RmTmpContainer      bool
	ForceRmTmpContainer bool
	RawJSONStream       bool
	Memory              int64
	Memswap             int64
	CPUShares           int64
	CPUQuota            int64
	CPUPeriod           int64
	CPUSetCPUs          string
	Labels              map[string]string
	InputStream         io.Reader
	OutputStream        io.Writer
	ErrorStream         io.Writer
	Remote              string
	Auth                docker.AuthConfiguration
	AuthConfigs         docker.AuthConfigurations
	Ulimits             []docker.ULimit
	NetworkMode         string
	InactivityTimeout   time.Duration
	CgroupParent        string
	SecurityOpt         []string
	Target              string
}

BuildOptions for (Pool).Build / (Pool).BuildAndGet functions.

func ApplyBuildOptions added in v0.0.5

func ApplyBuildOptions(uuid string, customOpts ...BuildOption) (
	options BuildOptions, err error,
)

ApplyBuildOptions sets defaults and apply custom options. Options aplies in order they passed.

Each option rewrites previous value

ApplyBuildOptions(WithImageName("first"), WithImageName("second")) // "second"

type ContainerConfigCheck added in v0.0.5

type ContainerConfigCheck func(container *docker.Container, expectedOptions RunOptions) (err error)

Function for check that container suits for reuse.

type Pool added in v0.0.5

type Pool struct {
	Pool *dockertest.Pool
}

Pool with docker client.

func MustNewPool added in v0.0.5

func MustNewPool(endpoint string) Pool

func NewPool added in v0.0.5

func NewPool(endpoint string) (Pool, error)

func (Pool) Build added in v0.0.5

func (p Pool) Build(ctx context.Context, buildOptions ...BuildOption) (err error)

Build a new image.

  • Rewrites old image with new one if they have the same name.
  • Old image with the same name won't be removed, but it will lose it's name.

func (Pool) BuildAndGet added in v0.0.5

func (p Pool) BuildAndGet(ctx context.Context, buildOptions ...BuildOption) (image *docker.Image, err error)

BuildAndGet a new image.

  • Rewrites old image with new one if they have the same name.
  • Old image with the same name won't be removed, but it will lose it's name.
  • Returns information about the created image.

func (Pool) Prune added in v0.0.5

func (p Pool) Prune(ctx context.Context, customOptions ...PruneOption) (err error)

Prune - remove containers and images created by this package.

func (Pool) Run added in v0.0.5

func (p Pool) Run(
	ctx context.Context, repository string, customOpts ...RunOption,
) (container *dockertest.Resource, err error)

Run - creates and runs new test container.

Example
const containerAPIPort = "80"
const serverHelloMesage = "Hello, World!"
startServerCMD := fmt.Sprintf(`echo '%s' > /index.html && httpd -p %s -h / && tail -f /dev/null`, serverHelloMesage, containerAPIPort)

// define function to check the server is ready
url := ""
pingServerRetry := func(container *dockertest.Resource) (err error) {
	url = "http://" + tcontainer.GetAPIEndpoints(container)[containerAPIPort].NetJoinHostPort()

	resp, err := http.Get(url)
	if err != nil {
		return fmt.Errorf("failed to http.Get: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected response status `%s`", resp.Status)
	}

	return nil
}

pool := tcontainer.MustNewPool("")

// you can remove all containers and images created by this package (from previous tests run)
// in order to avoid errors like ErrContainerAlreadyExists
err := pool.Prune(context.Background())
if err != nil {
	panic(err)
}

// run container with the server
container, err := pool.Run(
	context.Background(),
	"busybox",
	tcontainer.WithContainerName("my-test-server"),
	func(options *tcontainer.RunOptions) (err error) {
		// set by one field instead of *options = tcontainer.RunOptions{...}
		// in order to not owerride default values (like options.Retry.Timeout)

		options.Tag = "latest"
		options.Env = []string{"SOME_ENV=value"}
		options.Cmd = []string{"sh", "-c", startServerCMD}
		options.ExposedPorts = []string{containerAPIPort}
		options.HostConfig.AutoRemove = false
		options.HostConfig.RestartPolicy = docker.RestartPolicy{Name: "no", MaximumRetryCount: 0}
		options.Retry.Operation = pingServerRetry
		options.Reuse.Reuse = true
		options.Reuse.RecreateOnErr = true
		options.ContainerExpiry = time.Minute * 10
		options.HostConfig.PortBindings = map[docker.Port][]docker.PortBinding{
			"80": {{HostIP: "", HostPort: "8080"}},
		}
		options.RemoveOnExists = false // not compatible with Reuse option

		return nil
	},
)
if err != nil {
	panic(err)
}
defer container.Close() // not necessary if you want to WithReuseContainer

// make request to the server
resp, err := http.Get(url)
if err != nil {
	panic(err)
}
defer resp.Body.Close()
responseBody, _ := io.ReadAll(resp.Body)

fmt.Println(string(responseBody))
Output:

Hello, World!

type PrivatePort added in v0.0.5

type PrivatePort = string

PrivatePort - port inside the container.

type PruneContainersOption added in v0.0.5

type PruneContainersOption struct {
	Filters map[string][]string
}

PruneContainersOption for (Pool).Prune function.

type PruneImagesOption added in v0.0.5

type PruneImagesOption struct {
	Filters map[string][]string
}

PruneImagesOption for (Pool).Prune function.

type PruneOption added in v0.0.5

type PruneOption func(options *PruneOptions) (err error)

PruneOption - option for (Pool).Prune function. See ApplyPruneOptions.

type PruneOptions added in v0.0.5

type PruneOptions struct {
	PruneContainersOption PruneContainersOption
	PruneImagesOption     PruneImagesOption
}

PruneOptions for (Pool).Prune function.

func ApplyPruneOptions added in v0.0.5

func ApplyPruneOptions(customOpts ...PruneOption) (
	options PruneOptions, err error,
)

ApplyPruneOptions sets defaults and apply custom options. Options aplies in order they passed.

Each option rewrites previous value.

type RetryOperation

type RetryOperation func(container *dockertest.Resource) (err error)

RetryOperation is an exponential backoff retry operation. You can use it to wait for e.g. mysql to boot up.

type RetryOptions added in v0.0.5

type RetryOptions struct {
	Operation RetryOperation
	Backoff   backoff.BackOff
}

Allows you to specify a command that checks that the container is successfully started and ready to work.

  • `Run` function will periodically run and wait for the successful completion of `Retry.Operation` or issue an error upon reaching `backoff.Stop` / `backoff.Permanent`.
  • Use `GetAPIEndpoints(container)` to get the externally accessible ip and port to connect to a specific internal port of the container.

Default:

  • if `Retry.Operation` is not performed, `Run` function complete immediately after container creation
  • `Retry.Backoff.MaxElapsedTime` - `time.Second * 20`

Example:

func(options *RunOptions) (err error) {
    options.Retry.Operation = func(container *dockertest.Resource) (err error) {
        fmt.Println("ping")
        return nil
    }
    retryBackoff := backoff.NewExponentialBackOff()
    retryBackoff.MaxInterval = time.Second
    retryBackoff.MaxElapsedTime = time.Second * 20
    retryBackoff.Reset()
    options.Retry.Backoff = retryBackoff
    return nil
}

type ReuseContainerOptions added in v0.0.5

type ReuseContainerOptions struct {
	Reuse         bool
	Backoff       backoff.BackOff
	RecreateOnErr bool
	ConfigChecks  []ContainerConfigCheck
}

Allows you to reuse a container instead of getting an error that the container already exists.

  • Should not be used together with `RemoveOnExists` - will return `ErrOptionsConflict` error.
  • You may get an error if the existing container has different settings (different port mapping or image name). This error can be ignored with `RecreateOnErr`
  • You can specify `Backoff` to change the timeout waiting for the old container to be unpaused or started.
  • You can specify `RecreateOnErr` to recreate the container instead of getting an error when trying to reuse it. (When the old container has different settings or could not be revived)
  • Use `ConfigChecks` to check that old container suits for reuse

Default:

  • `Reuse` - `false`
  • `Backoff.MaxElapsedTime` - `time.Second * 20`
  • `RecreateOnErr` - `false`
  • `ConfigChecks` - checks that old container have the same image, exposed ports and port bindings

Example

func(options *RunOptions) (err error) {
	options.Reuse.Reuse = true
	reuseBackoff := backoff.NewExponentialBackOff()
	reuseBackoff.MaxInterval = time.Second
	reuseBackoff.MaxElapsedTime = time.Second * 20
	reuseBackoff.Reset()
	options.Reuse.Backoff = reuseBackoff
	options.Reuse.ConfigChecks = append(options.Reuse.ConfigChecks,
		func(container *docker.Container, expectedOptions RunOptions) (err error) {
			if container.Config.Image != expectedOptions.Repository + ":" + expectedOptions.Tag {
				return errors.New("old container have other image")
			}
			return nil
		},
	)
	return nil
}

type RunOption added in v0.0.5

type RunOption func(options *RunOptions) (err error)

RunOption - option for (Pool).Run function. See ApplyRunOptions.

func WithContainerName

func WithContainerName(nameParts ...string) RunOption

WithContainerName - use custom container name instead of random (generated by docker). All invalid characters will be repaced to "-". Not empty containerNameParts will be joined with "-" separator, empty parts will be removed.

Example usage:

WithContainerName(t.Name(), "redis") // "Test/with/invalid/chars", "redis" -> "Test-with-invalid-chars-redis"

type RunOptions added in v0.0.5

type RunOptions struct {
	Hostname     string
	Name         string
	Repository   string
	Tag          string
	Env          []string
	Entrypoint   []string
	Cmd          []string
	ExposedPorts []string
	WorkingDir   string
	Networks     []*dockertest.Network // optional networks to join
	Labels       map[string]string
	Auth         docker.AuthConfiguration
	User         string
	Tty          bool
	Platform     string
	HostConfig   docker.HostConfig

	// Allows you to reuse a container instead of getting an error that the container already exists.
	// See [RetryOptions] struct description
	Retry           RetryOptions
	ContainerExpiry time.Duration

	// Try to reuse container if it already exists.
	// See [ReuseContainerOptions] struct description.
	Reuse ReuseContainerOptions

	// Allows you to remove an existing container instead of getting an error that the container already exists.
	//	- Should not be used together with `Reuse` - will return `ErrOptionsConflict` error.
	//
	// Default: `false`
	RemoveOnExists bool
}

RunOptions for (Pool).Run function.

func ApplyRunOptions added in v0.0.5

func ApplyRunOptions(repository string, customOpts ...RunOption) (
	options RunOptions, err error,
)

ApplyRunOptions sets defaults and apply custom options. Options aplies in order they passed.

Each option rewrites previous value

ApplyRunOptions(WithContainerName("first"), WithContainerName("second")) // "second"

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL