dockerutils/builder.go

244 lines
7.0 KiB
Go

package dockerutils
import (
"context"
"fmt"
"runtime"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// Builder is a wrapper around the official docker API to start a container
// image.
type Builder struct {
client *Client
containerConfig *container.Config
containerName string
hostConfig *container.HostConfig
networkConfig *network.NetworkingConfig
networks map[string][]string
platform *v1.Platform
ports []string
pull bool
waitForHealthy bool
}
// AddEnv to the container
func (builder *Builder) AddEnv(key string, value string) *Builder {
builder.containerConfig.Env = append(builder.containerConfig.Env, fmt.Sprintf("%v=%v", key, value))
return builder
}
// AddLabel to the container
func (builder *Builder) AddLabel(key string, value string) *Builder {
builder.containerConfig.Labels[key] = value
return builder
}
// Env set environment variables to the container
func (builder *Builder) Env(env map[string]string) *Builder {
builder.containerConfig.Labels = make(map[string]string)
for key, value := range env {
builder.containerConfig.Env = append(builder.containerConfig.Env, fmt.Sprintf("%v=%v", key, value))
}
return builder
}
// Labels set labels to the container
func (builder *Builder) Labels(labels map[string]string) *Builder {
builder.containerConfig.Labels = labels
return builder
}
// Memory defines a memory limit for the container
func (builder *Builder) Memory(limit int64) *Builder {
builder.hostConfig.Memory = limit
return builder
}
// Mount a source volume or hostpath into the filesystem of the container
func (builder *Builder) Mount(source string, dest string) *Builder {
builder.hostConfig.Binds = append(builder.hostConfig.Binds, fmt.Sprintf("%v:%v", source, dest))
return builder
}
// Mounts a set of source volumes or hostpath into the filesystem of the
// container
func (builder *Builder) Mounts(mounts map[string]string) *Builder {
for source, dest := range mounts {
builder.Mount(source, dest)
}
return builder
}
// Network add the container with aliasses to a specific network
func (builder *Builder) Network(networkName string, aliasses ...string) *Builder {
builder.networks[networkName] = aliasses
return builder
}
// Port defines a port forwarding from the host machine to the container
// Examples:
// - 8080:8080
// - 10.6.231.10:8080:8080
// - 10.6.231.10:8080:8080/tcp
func (builder *Builder) Port(port string) *Builder {
builder.ports = append(builder.ports, port)
return builder
}
// Ports defines a set port forwarding rules from the host machine to the
// container
// Examples:
// - 8080:8080
// - 10.6.231.10:8080:8080
// - 10.6.231.10:8080:8080/tcp
func (builder *Builder) Ports(ports []string) *Builder {
builder.ports = ports
return builder
}
// Pull the image if absent
func (builder *Builder) Pull() *Builder {
builder.pull = true
return builder
}
// Start the container
func (builder *Builder) Start(ctx context.Context) (string, error) {
// Pull container image if absent
if builder.pull {
err := builder.client.PullQuiet(ctx, builder.containerConfig.Image)
if err != nil {
return "", err
}
}
// Network: portbinding Host->Container
exposedPorts, portBindings, err := nat.ParsePortSpecs(builder.ports)
if err != nil {
return "", fmt.Errorf("unabel to parse ports: %w", err)
}
if len(portBindings) > 0 {
time.Sleep(1 * time.Second)
builder.containerConfig.ExposedPorts = exposedPorts
builder.hostConfig.PortBindings = portBindings
}
// Network: Add container to container networks
// Add the container to the first defined container network, if any one is
// defined. If no one is defined, the docker API will add the container to
// their default bridge docker0. The other networks will be added to the
// container after the container start.
var (
networkNames = make([]string, 0)
networks = make([]types.NetworkResource, 0)
)
if len(builder.networks) > 0 {
for networkName := range builder.networks {
networkNames = append(networkNames, networkName)
}
var err error
networks, err = builder.client.NetworkListByNames(ctx, networkNames...)
if err != nil {
return "", err
}
endpointSetting := &network.EndpointSettings{
NetworkID: networks[0].ID,
Aliases: builder.networks[networkNames[0]],
}
builder.networkConfig.EndpointsConfig[networkNames[0]] = endpointSetting
networkNames = networkNames[1:]
networks = networks[1:]
}
// Container: Create
resp, err := builder.client.ContainerCreate(
ctx,
builder.containerConfig,
builder.hostConfig,
builder.networkConfig,
&v1.Platform{
Architecture: runtime.GOARCH,
OS: runtime.GOOS,
},
builder.containerName,
)
if err != nil {
return "", fmt.Errorf("Unable to create container: %w", err)
}
err = builder.client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil {
shutdownErr := builder.client.ContainerStopByIDs(ctx, 1*time.Second, resp.ID)
if shutdownErr != nil {
return "", fmt.Errorf("Unable to start container: %w\nUnable to remove container %s: %v\nManual cleanup necessary", err, resp.ID, shutdownErr)
}
return "", fmt.Errorf("Unable to start container: %w", err)
}
// Network: Add more container networks
for i, networkName := range networkNames {
endpointSetting := &network.EndpointSettings{
NetworkID: networks[i].ID,
Aliases: builder.networks[networkName],
}
err := builder.client.NetworkConnect(ctx, networks[i].ID, resp.ID, endpointSetting)
if err != nil {
return "", fmt.Errorf("Unable to append container endpoint to network %v", networkName)
}
}
if builder.waitForHealthy {
watcher := builder.client.GetWatcher()
errors := make(chan error, 1)
done := make(chan struct{})
err = watcher.AddListener(resp.ID, errors, done)
if err != nil {
containerRemoveError := builder.client.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true})
if containerRemoveError != nil {
return "", fmt.Errorf("error while watching for container status: %w - unable to remove container: %v", err, containerRemoveError)
}
return "", fmt.Errorf("error while watching for container status: %w", err)
}
select {
case err := <-errors:
if err != nil {
containerRemoveError := builder.client.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true})
if containerRemoveError != nil {
return "", fmt.Errorf("%w - unable to remove container: %v", err, containerRemoveError)
}
return "", err
}
case <-done:
}
}
return resp.ID, err
}
// WaitForHealthy set the option to wait during the start process until the
// container is healthy.
func (builder *Builder) WaitForHealthy() *Builder {
builder.waitForHealthy = true
return builder
}
// WithName set the name of the container
func (builder *Builder) WithName(containerName string) *Builder {
builder.containerName = containerName
return builder
}