Documentation
¶
Overview ¶
Package morbyd is a thin layer on top of the standard Docker Go client to easily build and run ephemeral test Docker images and containers, and run commands inside containers. It especially hides the gory details of how to stream the output, and optionally input, of containers and commands. Just io.Writer and io.Reader.
morbyd makes heavy use of option functions in order to help test writers to get a grip on Docker's (slightly) excessive knobs-for-everything API design. It neatly groups the many With...() options in packages, such as run for “run container” and exec for “container execute”. This design avoids stuttering option names that would otherwise clash across different API operations for common configuration elements, such as names, labels, and options.
Features of morbyd ¶
At a glance:
- testable examples for common tasks to get you quickly up and running.
- option function design with extensive Go Doc comments that IDEs show upon option completion. No more pseudo option function “callbacks” that are none the better than passing the original Docker config type verbatim.
- uses the official Docker Go client in order to benefit from its security fixes, functional upgrades, and all the other nice things to to get directly from upstream.
- “auto-cleaning” that runs when creating a new test session and again at its end, removing all containers and networks especially tagged using session.WithAutoCleaning for the test.
- uses context.Context throughout the whole module, especially integrating well with testing frameworks (such as Ginkgo) that support automatic unit test context creation.
- extensive unit tests with large coverage.
Trivia ¶
The module name “morby” is an amalgation of “Moby (Dock)” and “morbid” – ephemeral – test containers.
Index ¶
- Constants
- func Sleep(ctx context.Context, d time.Duration) error
- type Addr
- type Addrs
- type Container
- func (c *Container) AbbreviatedID() string
- func (c *Container) Exec(ctx context.Context, cmd exec.Cmd, opts ...exec.Opt) (es *ExecSession, err error)
- func (c *Container) IP(ctx context.Context) net.IP
- func (c *Container) Kill(ctx context.Context)
- func (c *Container) PID(ctx context.Context) (int, error)
- func (c *Container) PublishedPort(portproto string) Addrs
- func (c *Container) Refresh(ctx context.Context) error
- func (c *Container) Stop(ctx context.Context)
- func (c *Container) Wait(ctx context.Context) error
- type ExecSession
- type Network
- type PullImageOpt
- type Session
- func (s *Session) AutoClean(ctx context.Context)
- func (s *Session) BuildImage(ctx context.Context, buildctxpath string, opts ...build.Opt) (id string, err error)
- func (s *Session) Client() moby.Client
- func (s *Session) Close(ctx context.Context)
- func (s *Session) Container(ctx context.Context, nameID string) (*Container, error)
- func (s *Session) CreateNetwork(ctx context.Context, name string, opts ...net.Opt) (*Network, error)
- func (s *Session) HasImage(ctx context.Context, imageref string) (bool, error)
- func (s *Session) IsDockerDesktop(ctx context.Context) bool
- func (s *Session) Network(ctx context.Context, nameID string) (*Network, error)
- func (s *Session) PullImage(ctx context.Context, imgref string, opts ...PullImageOpt) error
- func (s *Session) Run(ctx context.Context, imageref string, opts ...run.Opt) (cntr *Container, err error)
Examples ¶
Constants ¶
const AbbreviatedIDLength = 10
AbbreviatedIDLength defines the number of hex digits of a container ID to show in error and log messages.
const DefaultSleep = 10 * time.Millisecond
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Addr ¶ added in v0.11.1
type Addr struct {
// contains filtered or unexported fields
}
Addr represents a non-zero IP address together with a transport-layer port and the particular transport-layer protocol.
func (Addr) Network ¶ added in v0.11.1
Network returns the “name” of the “network” the service is on, either “tcp” or “udp”.
func (Addr) String ¶ added in v0.11.1
String returns the address with port, such as “127.0.0.1:123” or “[::1]:123”. It returns "" for a zero Addr.
func (Addr) UnspecifiedAsLoopback ¶ added in v0.11.1
UnspecifiedAsLoopback returns the IPv4 or IPv6 loopback address if this Addr is unspecified, otherwise Addr unmodified. Use UnspecifiedAsLoopback to ensure to always get a non-unspecified destination address of a published container port.
type Addrs ¶ added in v0.11.1
type Addrs []Addr
Addrs is a list of Addr elements, providing helper functions on top.
type Container ¶
type Container struct { Name string ID string Session *Session Details types.ContainerJSON // inspection information after start. }
Container represents a Docker container, providing notable operations specific to it:
- Container.IP returns an host-internal IP address where the container can be reached.
- Container.Exec to execute a command inside the container.
- Container.PID to retrieve the PID of the container's initial process.
- Container.Stop to stop the container by sending it the configured signal (defaults to SIGTERM).
- Container.Kill to forcefully kill the container using SIGKILL.
func (*Container) AbbreviatedID ¶
AbbreviatedID returns an abbreviated container ID for use in error reporting in order to not report unwieldy long IDs.
func (*Container) Exec ¶
func (c *Container) Exec(ctx context.Context, cmd exec.Cmd, opts ...exec.Opt) (es *ExecSession, err error)
Exec a command inside a container, using the specified command using exec.Command(cmd, args...) and optional configuration information. It returns an *ExecSession object if successful, otherwise an error.
Important: executing the command is a fully asynchronous process to the extend that the session returned might still in its startup phase with the command not yet being executed. ExecSession.PID can be re-appropriated to wait for the command have been started.
Note: when using exec.WithInput make sure to close the input reader in order to not leak go routines handling the executed input and output streams in the background.
Note: morbyd does not support executing detached commands, so we will always be attached to the executing command's input/output streams.
Example ¶
Execute a command inside a running container using Container.Exec.
In this example, we start by creating a session using NewSession. Because unit tests may crash and leave test containers and networks behind, we enable “auto-cleaning” using the session.WithAutoCleaning option, passing it a unique label. This label can be either a unique key (“KEY=”) or a unique key-value pair (“KEY=VALUE”); either form is allowed, depending on how you like to structure and label your test containers and networks. Auto-cleaning runs automatically directly after session creation (to remove any left-overs from a previous test run) and then again when calling Session.Close.
Next, we start a container using Session.Run where this container simply sits idle in a sleep loop (so that the idling shell process reacts more quickly to SIGTERMs).
Then, we run a new command inside this container and pick up its output. Finally, we wind everything down.
Note: [safe.Buffer] is a bytes.Buffer that is safe for concurrent use.
package main import ( "context" "fmt" "time" "github.com/thediveo/morbyd" "github.com/thediveo/morbyd/exec" "github.com/thediveo/morbyd/run" "github.com/thediveo/morbyd/safe" "github.com/thediveo/morbyd/session" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() sess, err := morbyd.NewSession(ctx, session.WithAutoCleaning("test.morbyd=example.container.exec")) if err != nil { panic(err) } defer sess.Close(ctx) container, err := sess.Run(ctx, "busybox", run.WithCommand("/bin/sh", "-c", "trap 'exit 1' TERM; while true; do sleep 1; done"), run.WithAutoRemove()) if err != nil { panic(err) } defer container.Kill(context.Background()) // just to be sure var out safe.Buffer exec, err := container.Exec(ctx, exec.Command("/bin/echo", "Hellorld! from exec"), exec.WithCombinedOutput(&out)) if err != nil { panic(err) } exitcode, _ := exec.Wait(ctx) fmt.Printf("command exited with code %d\n", exitcode) container.Stop(ctx) fmt.Println(out.String()) }
Output: command exited with code 0 Hellorld! from exec
func (*Container) IP ¶
IP returns an IP address (net.IP) of this container that can be used to reach the container from the host. If no suitable IP address can be found, IP return nil. IP ignores addresses on a MACVLAN network, as IP addresses on a MACVLAN network cannot reached from the host.
NOTE: the container's IP address is usable without the need to (publicly) expose container ports on the host – which often is less than desirable in tests. However, with Docker Desktop the container IPs aren't directly reachable anymore as on plain Docker hosts, so in these cases you'll need to expose a container's exposable ports on (preferably) loopback.
func (*Container) PID ¶
PID of the initial container process, as seen by the container engine. In case the container is restarting, it waits for the next Doctor, erm, container incarnation to come online.
Note to Docker Desktop users: the PID is only valid in the context of the Docker engine that in case of macOS runs in its own VM, and in case of WSL2 in its own PID namespace in the same HyperV Linux VM.
func (*Container) PublishedPort ¶ added in v0.11.1
PublishedPort returns the host IP address(es) and port(s) that forward to the transport-layer port and protocol of this container, such as “1234” or “1234/tcp”. If the transport-layer protocol is left unspecified, “tcp” is assumed by default.
If there is no such port (with protocol) published, PublishedPort returns an empty Addrs list.
In order to easily connect to a published container service, a suitable IP address string including port number can be determined as follows:
// for instance, returns "127.0.0.1:32890" svcAddrPort := cntr.PublishedPort("1234").Any().UnspecifiedAsLoopback().String()
Example ¶
Run a container that publishes its http service on loopback and then query this service for the “magic” phrase.
We start by creating a session using NewSession. Because unit tests may crash and leave test containers and networks behind, we enable “auto-cleaning” using the session.WithAutoCleaning option, passing it a unique label. This label can be either a unique key (“KEY=”) or a unique key-value pair (“KEY=VALUE”); either form is allowed, depending on how you like to structure and label your test containers and networks. Auto-cleaning runs automatically directly after session creation (to remove any left-overs from a previous test run) and then again when calling Session.Close.
Next, Session.Run creates the container and then runs (in our example) the http server command that we supplied as part of the run configuration. We additionally publish this http service on the host's (IPv4) loopback interface, on an ephemeral port.
We then pick up the ephemeral port number and send an HTTP GET request to it, checking for the proper successful answer.
package main import ( "context" "fmt" io "io" "net/http" "time" "github.com/thediveo/morbyd" "github.com/thediveo/morbyd/run" "github.com/thediveo/morbyd/session" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() sess, err := morbyd.NewSession(ctx, session.WithAutoCleaning("test.morbyd=example.container.port")) if err != nil { panic(err) } defer sess.Close(ctx) container, err := sess.Run(ctx, "busybox", run.WithCommand("/bin/sh", "-c", `echo "DOH!" > index.html && httpd -f -p 1234`), run.WithAutoRemove(), run.WithPublishedPort("127.0.0.1:1234")) if err != nil { panic(err) } svcAddrPort := container. PublishedPort("1234"). Any().UnspecifiedAsLoopback().String() req, err := http.NewRequest( http.MethodGet, "http://"+svcAddrPort+"/", nil) if err != nil { panic(err) } resp, err := http.DefaultClient.Do(req.WithContext(ctx)) if err != nil { panic(err) } if resp.StatusCode != http.StatusOK { panic(resp.StatusCode) } defer resp.Body.Close() phrase, err := io.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Printf("%s\n", phrase) }
Output: DOH!
func (*Container) Refresh ¶
Refresh the details about this container, or return an error in case refreshing fails.
func (*Container) Stop ¶
Stop the container by sending it a termination signal. Default is SIGTERM, unless changed using run.WithStopSignal.
type ExecSession ¶
type ExecSession struct { ID string // command execution ID. Container *Container // container this command runs inside. // contains filtered or unexported fields }
ExecSession represents a command running inside a container.
Nota bene: the Docker API doesn't have an API endpoint for deleting executions when they're finished and we've picked up the results.
func (*ExecSession) Done ¶
func (e *ExecSession) Done() chan struct{}
Done returns a channel that gets closed when the command has finished executing inside its container.
func (*ExecSession) PID ¶
func (e *ExecSession) PID(ctx context.Context) (int, error)
PID returns the executing command's PID, or an error if the command has already terminated.
Note: [Container.Run] can already return while the underlying Docker session for executing the command inside the container is still starting up. In this case, PID will wait until the executing command's PID becomes available or the passed context gets cancelled.
func (*ExecSession) Wait ¶
func (e *ExecSession) Wait(ctx context.Context) (exitcode int, err error)
Wait for the command executed inside its container to finish, and then return the command's exit code. If the passed context gets cancelled or there is a problem picking up the command's exit code, Wait returns an error instead.
type PullImageOpt ¶
type PullImageOpt func(*pullImageOptions)
func WithPullImageOutput ¶
func WithPullImageOutput(w io.Writer) PullImageOpt
WithImageBuildOutput set the writer to send the output of the image pull process to.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session represents a Docker API client connection, together with additional configuration options that are inherited to newly created images, containers, and networks.
func NewSession ¶
NewSession creates a new Docker client and test session, returning a Session object on success, or an error otherwise.
When [sess.WithAutoCleaning] has been specified, then NewSession will then forcefully remove all containers and then networks matching the specified auto-cleaning label. In this case, Session.Close will then run a post-session cleaning.
Note: the Docker client is created using the options client.FromEnv and client.WithAPIVersionNegotiation.
func (*Session) AutoClean ¶
AutoClean forcefully removes all left-over containers and networks that are labelled with the auto-cleaning label specified when creating this session. If no auto-cleaning label was specified, AutoClean simply returns, doing nothing. (Well, it does something: it returns ... but that is now too meta).
func (*Session) BuildImage ¶
func (s *Session) BuildImage(ctx context.Context, buildctxpath string, opts ...build.Opt) (id string, err error)
BuildImage builds a container image using the specified build context and further build options. These build options are applied in the order they are provided, which allows modifying (or even nuking) the defaults when building an image.
BuildImage returns the ID of the built image, or an error in case of build errors.
Unless overridden using a build option, the following defaults apply:
- Dockerfile: "Dockerfile"
- Remove: true
- ForceRemove: true
If no build process output writer has been specified using build.WithOutput any output (such as build steps, et cetera) will simply be discarded.
func (*Session) Close ¶
Close removes left-over containers and networks if auto-cleaning has been enabled, and then closes idle HTTP connections to the Docker daemon.
func (*Session) Container ¶
Container returns a *Container object for the specified name or ID if it exists, otherwise it returns an error. Please note that multiple calls for the same name or ID will return different *Container objects, as there is no caching.
func (*Session) CreateNetwork ¶
func (s *Session) CreateNetwork(ctx context.Context, name string, opts ...net.Opt) (*Network, error)
CreateNetwork creates a new “custom” Docker network using the specified configuration options.
Notable configuration options:
- net.WithInternal marks the new network as “[internal]”.
- net.WithDriver allows setting a different Docker network driver, such as “macvlan” or “ipvlan”.
- see github.com/thediveo/morbyd/net/bridge, github.com/thediveo/morbyd/net/macvlan, and github.com/thediveo/morbyd/net/ipvlan for further, drive-specific configuration options.
See also: docker network create
Example ¶
Create a new “custom” Docker network, then run an example container attached to this (purely internal) network.
We start by creating a session using NewSession. Because unit tests may crash and leave test containers and networks behind, we enable “auto-cleaning” using the session.WithAutoCleaning option, passing it a unique label. This label can be either a unique key (“KEY=”) or a unique key-value pair (“KEY=VALUE”); either form is allowed, depending on how you like to structure and label your test containers and networks. Auto-cleaning runs automatically directly after session creation (to remove any left-overs from a previous test run) and then again when calling Session.Close.
Please note that for this testable example we need a deterministic container IP address assignment. In your tests, you most probably just need a working IP address, but not a particular one, so you won't need net.WithIPAM in most circumstance.
In our special case, we create a custom Docker network with an IP address management (IPAM) pool of “0.0.1.0/24” that is a small part of the so-called “this” network defined in RFC5735 section 3 and RFC8190.
The default “bridge” driver will automatically allocate the first available pool IP address “0.0.1.1” to the Linux kernel bridge, so the first container IP address will be “0.0.1.2”. Please note that the IP address “0.0.1.0” is the “subnet address” of this subnet and usually is reserved, so Docker's standard IPAM driver never assigns it.
When the example container attached to this custom network (using run.WithNetwork) starts, it executes the following shell command that grabs information about the specified network interface “eth0” and then cuts out only the IPv4 address:
ip a sh dev eth0 | awk '/inet / { print $2 }'
package main import ( "context" "fmt" "time" "github.com/thediveo/morbyd" "github.com/thediveo/morbyd/ipam" "github.com/thediveo/morbyd/net" "github.com/thediveo/morbyd/run" "github.com/thediveo/morbyd/safe" "github.com/thediveo/morbyd/session" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() sess, err := morbyd.NewSession(ctx, session.WithAutoCleaning("test.morbyd=example.session.network")) if err != nil { panic(err) } defer sess.Close(ctx) _ = sess.Client().NetworkRemove(ctx, "my-notwork") netw, err := sess.CreateNetwork(ctx, "my-notwork", net.WithInternal(), net.WithIPAM(ipam.WithPool("0.0.42.0/24"))) if err != nil { panic(err) } defer func() { _ = netw.Remove(ctx) }() var out safe.Buffer container, err := sess.Run(ctx, "busybox", run.WithCommand("/bin/sh", "-c", "ip a sh dev eth0 | awk '/inet / { print $2 }'"), run.WithNetwork(netw.ID), run.WithAutoRemove(), run.WithCombinedOutput(&out)) if err != nil { panic(err) } _ = container.Wait(ctx) fmt.Print(out.String()) }
Output: 0.0.42.2/24
func (*Session) HasImage ¶
HasImage returns true if the image referenced by imageref is locally available, otherwise false.
func (*Session) IsDockerDesktop ¶ added in v0.10.0
IsDockerDesktop returns true if the Docker engine is the Docker Desktop engine, as opposed to a “plain” Docker engine. This differentiation is important in some situation, as containers managed by Docker Desktop cannot be directly reached from the host, but always require ports to be published.
func (*Session) Network ¶
Network returns a *Network object for the specified name or ID if it exists, otherwise it returns an error. Please note that multiple calls for the same name or ID will return different *Network objects, as there is no caching.
func (*Session) PullImage ¶
PullImage pulls a container image specified by the image reference, if not already locally available. The additional pull options are applied in the order they are provided.
If no pull process output writer has been specified using WithPullImageOutput any output (such as pull progress, et cetera) will simply be discarded.
Any pull process errors will be reported.
func (*Session) Run ¶
func (s *Session) Run(ctx context.Context, imageref string, opts ...run.Opt) (cntr *Container, err error)
Run (create and start) a new container, using the referenced image and optional configuration information, returning a *Container object if successful. Otherwise, it returns an error without leaving behind any container.
Additionally, Run attaches to the container's input and output streams which can be accessed using run.WithInput, and either run.WithCombinedOutput or run.WithDemuxedOutput.
If the session has configured with labels, the new container inherits them. Use run.ClearLabels before run.WithLabel or run.WithLabels in order to remove any inherited labels first.
Example ¶
Run a container and gather its output.
We start by creating a session using NewSession. Because unit tests may crash and leave test containers and networks behind, we enable “auto-cleaning” using the session.WithAutoCleaning option, passing it a unique label. This label can be either a unique key (“KEY=”) or a unique key-value pair (“KEY=VALUE”); either form is allowed, depending on how you like to structure and label your test containers and networks. Auto-cleaning runs automatically directly after session creation (to remove any left-overs from a previous test run) and then again when calling Session.Close.
Next, Session.Run creates the container and then runs (in our example) a command that we supplied as part of the run configuration.
Because running the container and gathering its output are asynchronous operations, we Container.Wait for the container to have terminated before we pick up its output.
Note: [safe.Buffer] is a bytes.Buffer that is safe for concurrent use.
package main import ( "context" "fmt" "time" "github.com/thediveo/morbyd" "github.com/thediveo/morbyd/run" "github.com/thediveo/morbyd/safe" "github.com/thediveo/morbyd/session" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() sess, err := morbyd.NewSession(ctx, session.WithAutoCleaning("test.morbyd=example.session.run")) if err != nil { panic(err) } defer sess.Close(ctx) var out safe.Buffer container, err := sess.Run(ctx, "busybox", run.WithCommand("/bin/sh", "-c", "echo \"Hellorld!\""), run.WithAutoRemove(), run.WithCombinedOutput(&out)) if err != nil { panic(err) } _ = container.Wait(ctx) fmt.Print(out.String()) }
Output: Hellorld!
Source Files
¶
Directories
¶
Path | Synopsis |
---|---|
Package build provides configuring options for building images and handling build arguments.
|
Package build provides configuring options for building images and handling build arguments. |
Package exec provides configuration options for executing programs inside containers.
|
Package exec provides configuration options for executing programs inside containers. |
Package internal: keep your dirty paws off.
|
Package internal: keep your dirty paws off. |
ipamint
Package ipamint provides package “ipam”-internal APIs for sole use within the morbyd module, not littering the public API.
|
Package ipamint provides package “ipam”-internal APIs for sole use within the morbyd module, not littering the public API. |
netint
Package netint provides package “net”-internal APIs for sole use within the morbyd module, not littering the public API.
|
Package netint provides package “net”-internal APIs for sole use within the morbyd module, not littering the public API. |
Package ipam provides options for IP address management (“IPAM”), making Docker's IPAM-related API data structures more accessible.
|
Package ipam provides options for IP address management (“IPAM”), making Docker's IPAM-related API data structures more accessible. |
Package labels provides parsing of labels into a key-value map.
|
Package labels provides parsing of labels into a key-value map. |
Package net provides options to configure new Docker (custom) networks.
|
Package net provides options to configure new Docker (custom) networks. |
bridge
Package bridge provides Docker “bridge”-driver specific network configuration options.
|
Package bridge provides Docker “bridge”-driver specific network configuration options. |
ipvlan
Package ipvlan provides Docker “ipvlan”-driver specific network configuration options.
|
Package ipvlan provides Docker “ipvlan”-driver specific network configuration options. |
macvlan
Package macvlan provides Docker “macvlan”-driver specific network configuration options.
|
Package macvlan provides Docker “macvlan”-driver specific network configuration options. |
Package run provides configuration options for running containers.
|
Package run provides configuration options for running containers. |
dockercli
Source: https://github.com/docker/cli/blob/v25.0.1/opts/mount.go
|
Source: https://github.com/docker/cli/blob/v25.0.1/opts/mount.go |
Package safe provides a concurrency-safe buffer io.Writer.
|
Package safe provides a concurrency-safe buffer io.Writer. |
Package sess provides options for creating (test) sessions.
|
Package sess provides options for creating (test) sessions. |
Package strukt provides parsing strings with delimiter-separated fields into structs consisting of sufficient exported string fields.
|
Package strukt provides parsing strings with delimiter-separated fields into structs consisting of sufficient exported string fields. |
Package timestamper provides an io.Writer that adds timestamps at the beginning of each line of output.
|
Package timestamper provides an io.Writer that adds timestamps at the beginning of each line of output. |