blob: 7902750d6dc52966e910c4ba4744d92f90a00a4a [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package docker
import (
"archive/tar"
"bufio"
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/cenkalti/backoff/v3"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
"github.com/hashicorp/go-uuid"
)
const DockerAPIVersion = "1.40"
type Runner struct {
DockerAPI *client.Client
RunOptions RunOptions
}
type RunOptions struct {
ImageRepo string
ImageTag string
ContainerName string
Cmd []string
Entrypoint []string
Env []string
NetworkName string
NetworkID string
CopyFromTo map[string]string
Ports []string
DoNotAutoRemove bool
AuthUsername string
AuthPassword string
OmitLogTimestamps bool
LogConsumer func(string)
Capabilities []string
PreDelete bool
PostStart func(string, string) error
LogStderr io.Writer
LogStdout io.Writer
VolumeNameToMountPoint map[string]string
}
func NewDockerAPI() (*client.Client, error) {
return client.NewClientWithOpts(client.FromEnv, client.WithVersion(DockerAPIVersion))
}
func NewServiceRunner(opts RunOptions) (*Runner, error) {
dapi, err := NewDockerAPI()
if err != nil {
return nil, err
}
if opts.NetworkName == "" {
opts.NetworkName = os.Getenv("TEST_DOCKER_NETWORK_NAME")
}
if opts.NetworkName != "" {
nets, err := dapi.NetworkList(context.TODO(), types.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("name", opts.NetworkName)),
})
if err != nil {
return nil, err
}
if len(nets) != 1 {
return nil, fmt.Errorf("expected exactly one docker network named %q, got %d", opts.NetworkName, len(nets))
}
opts.NetworkID = nets[0].ID
}
if opts.NetworkID == "" {
opts.NetworkID = os.Getenv("TEST_DOCKER_NETWORK_ID")
}
if opts.ContainerName == "" {
if strings.Contains(opts.ImageRepo, "/") {
return nil, fmt.Errorf("ContainerName is required for non-library images")
}
// If there's no slash in the repo it's almost certainly going to be
// a good container name.
opts.ContainerName = opts.ImageRepo
}
return &Runner{
DockerAPI: dapi,
RunOptions: opts,
}, nil
}
type ServiceConfig interface {
Address() string
URL() *url.URL
}
func NewServiceHostPort(host string, port int) *ServiceHostPort {
return &ServiceHostPort{address: fmt.Sprintf("%s:%d", host, port)}
}
func NewServiceHostPortParse(s string) (*ServiceHostPort, error) {
pieces := strings.Split(s, ":")
if len(pieces) != 2 {
return nil, fmt.Errorf("address must be of the form host:port, got: %v", s)
}
port, err := strconv.Atoi(pieces[1])
if err != nil || port < 1 {
return nil, fmt.Errorf("address must be of the form host:port, got: %v", s)
}
return &ServiceHostPort{s}, nil
}
type ServiceHostPort struct {
address string
}
func (s ServiceHostPort) Address() string {
return s.address
}
func (s ServiceHostPort) URL() *url.URL {
return &url.URL{Host: s.address}
}
func NewServiceURLParse(s string) (*ServiceURL, error) {
u, err := url.Parse(s)
if err != nil {
return nil, err
}
return &ServiceURL{u: *u}, nil
}
func NewServiceURL(u url.URL) *ServiceURL {
return &ServiceURL{u: u}
}
type ServiceURL struct {
u url.URL
}
func (s ServiceURL) Address() string {
return s.u.Host
}
func (s ServiceURL) URL() *url.URL {
return &s.u
}
// ServiceAdapter verifies connectivity to the service, then returns either the
// connection string (typically a URL) and nil, or empty string and an error.
type ServiceAdapter func(ctx context.Context, host string, port int) (ServiceConfig, error)
// StartService will start the runner's configured docker container with a
// random UUID suffix appended to the name to make it unique and will return
// either a hostname or local address depending on if a Docker network was given.
//
// Most tests can default to using this.
func (d *Runner) StartService(ctx context.Context, connect ServiceAdapter) (*Service, error) {
serv, _, err := d.StartNewService(ctx, true, false, connect)
return serv, err
}
type LogConsumerWriter struct {
consumer func(string)
}
func (l LogConsumerWriter) Write(p []byte) (n int, err error) {
// TODO this assumes that we're never passed partial log lines, which
// seems a safe assumption for now based on how docker looks to implement
// logging, but might change in the future.
scanner := bufio.NewScanner(bytes.NewReader(p))
scanner.Buffer(make([]byte, 64*1024), bufio.MaxScanTokenSize)
for scanner.Scan() {
l.consumer(scanner.Text())
}
return len(p), nil
}
var _ io.Writer = &LogConsumerWriter{}
// StartNewService will start the runner's configured docker container but with the
// ability to control adding a name suffix or forcing a local address to be returned.
// 'addSuffix' will add a random UUID to the end of the container name.
// 'forceLocalAddr' will force the container address returned to be in the
// form of '127.0.0.1:1234' where 1234 is the mapped container port.
func (d *Runner) StartNewService(ctx context.Context, addSuffix, forceLocalAddr bool, connect ServiceAdapter) (*Service, string, error) {
if d.RunOptions.PreDelete {
name := d.RunOptions.ContainerName
matches, err := d.DockerAPI.ContainerList(ctx, types.ContainerListOptions{
All: true,
// TODO use labels to ensure we don't delete anything we shouldn't
Filters: filters.NewArgs(
filters.Arg("name", name),
),
})
if err != nil {
return nil, "", fmt.Errorf("failed to list containers named %q", name)
}
for _, cont := range matches {
err = d.DockerAPI.ContainerRemove(ctx, cont.ID, types.ContainerRemoveOptions{Force: true})
if err != nil {
return nil, "", fmt.Errorf("failed to pre-delete container named %q", name)
}
}
}
result, err := d.Start(context.Background(), addSuffix, forceLocalAddr)
if err != nil {
return nil, "", err
}
var wg sync.WaitGroup
consumeLogs := false
var logStdout, logStderr io.Writer
if d.RunOptions.LogStdout != nil && d.RunOptions.LogStderr != nil {
consumeLogs = true
logStdout = d.RunOptions.LogStdout
logStderr = d.RunOptions.LogStderr
} else if d.RunOptions.LogConsumer != nil {
consumeLogs = true
logStdout = &LogConsumerWriter{d.RunOptions.LogConsumer}
logStderr = &LogConsumerWriter{d.RunOptions.LogConsumer}
}
// The waitgroup wg is used here to support some stuff in NewDockerCluster.
// We can't generate the PKI cert for the https listener until we know the
// container's address, meaning we must first start the container, then
// generate the cert, then copy it into the container, then signal Vault
// to reload its config/certs. However, if we SIGHUP Vault before Vault
// has installed its signal handler, that will kill Vault, since the default
// behaviour for HUP is termination. So the PostStart that NewDockerCluster
// passes in (which does all that PKI cert stuff) waits to see output from
// Vault on stdout/stderr before it sends the signal, and we don't want to
// run the PostStart until we've hooked into the docker logs.
if consumeLogs {
wg.Add(1)
go func() {
// We must run inside a goroutine because we're using Follow:true,
// and StdCopy will block until the log stream is closed.
stream, err := d.DockerAPI.ContainerLogs(context.Background(), result.Container.ID, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Timestamps: !d.RunOptions.OmitLogTimestamps,
Details: true,
Follow: true,
})
wg.Done()
if err != nil {
d.RunOptions.LogConsumer(fmt.Sprintf("error reading container logs: %v", err))
} else {
_, err := stdcopy.StdCopy(logStdout, logStderr, stream)
if err != nil {
d.RunOptions.LogConsumer(fmt.Sprintf("error demultiplexing docker logs: %v", err))
}
}
}()
}
wg.Wait()
if d.RunOptions.PostStart != nil {
if err := d.RunOptions.PostStart(result.Container.ID, result.RealIP); err != nil {
return nil, "", fmt.Errorf("poststart failed: %w", err)
}
}
cleanup := func() {
for i := 0; i < 10; i++ {
err := d.DockerAPI.ContainerRemove(ctx, result.Container.ID, types.ContainerRemoveOptions{Force: true})
if err == nil || client.IsErrNotFound(err) {
return
}
time.Sleep(1 * time.Second)
}
}
bo := backoff.NewExponentialBackOff()
bo.MaxInterval = time.Second * 5
bo.MaxElapsedTime = 2 * time.Minute
pieces := strings.Split(result.Addrs[0], ":")
portInt, err := strconv.Atoi(pieces[1])
if err != nil {
return nil, "", err
}
var config ServiceConfig
err = backoff.Retry(func() error {
container, err := d.DockerAPI.ContainerInspect(ctx, result.Container.ID)
if err != nil || !container.State.Running {
return backoff.Permanent(fmt.Errorf("failed inspect or container %q not running: %w", result.Container.ID, err))
}
c, err := connect(ctx, pieces[0], portInt)
if err != nil {
return err
}
if c == nil {
return fmt.Errorf("service adapter returned nil error and config")
}
config = c
return nil
}, bo)
if err != nil {
if !d.RunOptions.DoNotAutoRemove {
cleanup()
}
return nil, "", err
}
return &Service{
Config: config,
Cleanup: cleanup,
Container: result.Container,
StartResult: result,
}, result.Container.ID, nil
}
type Service struct {
Config ServiceConfig
Cleanup func()
Container *types.ContainerJSON
StartResult *StartResult
}
type StartResult struct {
Container *types.ContainerJSON
Addrs []string
RealIP string
}
func (d *Runner) Start(ctx context.Context, addSuffix, forceLocalAddr bool) (*StartResult, error) {
name := d.RunOptions.ContainerName
if addSuffix {
suffix, err := uuid.GenerateUUID()
if err != nil {
return nil, err
}
name += "-" + suffix
}
cfg := &container.Config{
Hostname: name,
Image: fmt.Sprintf("%s:%s", d.RunOptions.ImageRepo, d.RunOptions.ImageTag),
Env: d.RunOptions.Env,
Cmd: d.RunOptions.Cmd,
}
if len(d.RunOptions.Ports) > 0 {
cfg.ExposedPorts = make(map[nat.Port]struct{})
for _, p := range d.RunOptions.Ports {
cfg.ExposedPorts[nat.Port(p)] = struct{}{}
}
}
if len(d.RunOptions.Entrypoint) > 0 {
cfg.Entrypoint = strslice.StrSlice(d.RunOptions.Entrypoint)
}
hostConfig := &container.HostConfig{
AutoRemove: !d.RunOptions.DoNotAutoRemove,
PublishAllPorts: true,
}
if len(d.RunOptions.Capabilities) > 0 {
hostConfig.CapAdd = d.RunOptions.Capabilities
}
netConfig := &network.NetworkingConfig{}
if d.RunOptions.NetworkID != "" {
netConfig.EndpointsConfig = map[string]*network.EndpointSettings{
d.RunOptions.NetworkID: {},
}
}
// best-effort pull
var opts types.ImageCreateOptions
if d.RunOptions.AuthUsername != "" && d.RunOptions.AuthPassword != "" {
var buf bytes.Buffer
auth := map[string]string{
"username": d.RunOptions.AuthUsername,
"password": d.RunOptions.AuthPassword,
}
if err := json.NewEncoder(&buf).Encode(auth); err != nil {
return nil, err
}
opts.RegistryAuth = base64.URLEncoding.EncodeToString(buf.Bytes())
}
resp, _ := d.DockerAPI.ImageCreate(ctx, cfg.Image, opts)
if resp != nil {
_, _ = ioutil.ReadAll(resp)
}
for vol, mtpt := range d.RunOptions.VolumeNameToMountPoint {
hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{
Type: "volume",
Source: vol,
Target: mtpt,
ReadOnly: false,
})
}
c, err := d.DockerAPI.ContainerCreate(ctx, cfg, hostConfig, netConfig, nil, cfg.Hostname)
if err != nil {
return nil, fmt.Errorf("container create failed: %v", err)
}
for from, to := range d.RunOptions.CopyFromTo {
if err := copyToContainer(ctx, d.DockerAPI, c.ID, from, to); err != nil {
_ = d.DockerAPI.ContainerRemove(ctx, c.ID, types.ContainerRemoveOptions{})
return nil, err
}
}
err = d.DockerAPI.ContainerStart(ctx, c.ID, types.ContainerStartOptions{})
if err != nil {
_ = d.DockerAPI.ContainerRemove(ctx, c.ID, types.ContainerRemoveOptions{})
return nil, fmt.Errorf("container start failed: %v", err)
}
inspect, err := d.DockerAPI.ContainerInspect(ctx, c.ID)
if err != nil {
_ = d.DockerAPI.ContainerRemove(ctx, c.ID, types.ContainerRemoveOptions{})
return nil, err
}
var addrs []string
for _, port := range d.RunOptions.Ports {
pieces := strings.Split(port, "/")
if len(pieces) < 2 {
return nil, fmt.Errorf("expected port of the form 1234/tcp, got: %s", port)
}
if d.RunOptions.NetworkID != "" && !forceLocalAddr {
addrs = append(addrs, fmt.Sprintf("%s:%s", cfg.Hostname, pieces[0]))
} else {
mapped, ok := inspect.NetworkSettings.Ports[nat.Port(port)]
if !ok || len(mapped) == 0 {
return nil, fmt.Errorf("no port mapping found for %s", port)
}
addrs = append(addrs, fmt.Sprintf("127.0.0.1:%s", mapped[0].HostPort))
}
}
var realIP string
if d.RunOptions.NetworkID == "" {
if len(inspect.NetworkSettings.Networks) > 1 {
return nil, fmt.Errorf("Set d.RunOptions.NetworkName instead for container with multiple networks: %v", inspect.NetworkSettings.Networks)
}
for _, network := range inspect.NetworkSettings.Networks {
realIP = network.IPAddress
break
}
} else {
realIP = inspect.NetworkSettings.Networks[d.RunOptions.NetworkName].IPAddress
}
return &StartResult{
Container: &inspect,
Addrs: addrs,
RealIP: realIP,
}, nil
}
func (d *Runner) RefreshFiles(ctx context.Context, containerID string) error {
for from, to := range d.RunOptions.CopyFromTo {
if err := copyToContainer(ctx, d.DockerAPI, containerID, from, to); err != nil {
// TODO too drastic?
_ = d.DockerAPI.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{})
return err
}
}
return d.DockerAPI.ContainerKill(ctx, containerID, "SIGHUP")
}
func (d *Runner) Stop(ctx context.Context, containerID string) error {
if d.RunOptions.NetworkID != "" {
if err := d.DockerAPI.NetworkDisconnect(ctx, d.RunOptions.NetworkID, containerID, true); err != nil {
return fmt.Errorf("error disconnecting network (%v): %v", d.RunOptions.NetworkID, err)
}
}
// timeout in seconds
timeout := 5
options := container.StopOptions{
Timeout: &timeout,
}
if err := d.DockerAPI.ContainerStop(ctx, containerID, options); err != nil {
return fmt.Errorf("error stopping container: %v", err)
}
return nil
}
func (d *Runner) Restart(ctx context.Context, containerID string) error {
if err := d.DockerAPI.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil {
return err
}
ends := &network.EndpointSettings{
NetworkID: d.RunOptions.NetworkID,
}
return d.DockerAPI.NetworkConnect(ctx, d.RunOptions.NetworkID, containerID, ends)
}
func copyToContainer(ctx context.Context, dapi *client.Client, containerID, from, to string) error {
srcInfo, err := archive.CopyInfoSourcePath(from, false)
if err != nil {
return fmt.Errorf("error copying from source %q: %v", from, err)
}
srcArchive, err := archive.TarResource(srcInfo)
if err != nil {
return fmt.Errorf("error creating tar from source %q: %v", from, err)
}
defer srcArchive.Close()
dstInfo := archive.CopyInfo{Path: to}
dstDir, content, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
if err != nil {
return fmt.Errorf("error preparing copy from %q -> %q: %v", from, to, err)
}
defer content.Close()
err = dapi.CopyToContainer(ctx, containerID, dstDir, content, types.CopyToContainerOptions{})
if err != nil {
return fmt.Errorf("error copying from %q -> %q: %v", from, to, err)
}
return nil
}
type RunCmdOpt interface {
Apply(cfg *types.ExecConfig) error
}
type RunCmdUser string
var _ RunCmdOpt = (*RunCmdUser)(nil)
func (u RunCmdUser) Apply(cfg *types.ExecConfig) error {
cfg.User = string(u)
return nil
}
func (d *Runner) RunCmdWithOutput(ctx context.Context, container string, cmd []string, opts ...RunCmdOpt) ([]byte, []byte, int, error) {
return RunCmdWithOutput(d.DockerAPI, ctx, container, cmd, opts...)
}
func RunCmdWithOutput(api *client.Client, ctx context.Context, container string, cmd []string, opts ...RunCmdOpt) ([]byte, []byte, int, error) {
runCfg := types.ExecConfig{
AttachStdout: true,
AttachStderr: true,
Cmd: cmd,
}
for index, opt := range opts {
if err := opt.Apply(&runCfg); err != nil {
return nil, nil, -1, fmt.Errorf("error applying option (%d / %v): %w", index, opt, err)
}
}
ret, err := api.ContainerExecCreate(ctx, container, runCfg)
if err != nil {
return nil, nil, -1, fmt.Errorf("error creating execution environment: %v\ncfg: %v\n", err, runCfg)
}
resp, err := api.ContainerExecAttach(ctx, ret.ID, types.ExecStartCheck{})
if err != nil {
return nil, nil, -1, fmt.Errorf("error attaching to command execution: %v\ncfg: %v\nret: %v\n", err, runCfg, ret)
}
defer resp.Close()
var stdoutB bytes.Buffer
var stderrB bytes.Buffer
if _, err := stdcopy.StdCopy(&stdoutB, &stderrB, resp.Reader); err != nil {
return nil, nil, -1, fmt.Errorf("error reading command output: %v", err)
}
stdout := stdoutB.Bytes()
stderr := stderrB.Bytes()
// Fetch return code.
info, err := api.ContainerExecInspect(ctx, ret.ID)
if err != nil {
return stdout, stderr, -1, fmt.Errorf("error reading command exit code: %v", err)
}
return stdout, stderr, info.ExitCode, nil
}
func (d *Runner) RunCmdInBackground(ctx context.Context, container string, cmd []string, opts ...RunCmdOpt) (string, error) {
return RunCmdInBackground(d.DockerAPI, ctx, container, cmd, opts...)
}
func RunCmdInBackground(api *client.Client, ctx context.Context, container string, cmd []string, opts ...RunCmdOpt) (string, error) {
runCfg := types.ExecConfig{
AttachStdout: true,
AttachStderr: true,
Cmd: cmd,
}
for index, opt := range opts {
if err := opt.Apply(&runCfg); err != nil {
return "", fmt.Errorf("error applying option (%d / %v): %w", index, opt, err)
}
}
ret, err := api.ContainerExecCreate(ctx, container, runCfg)
if err != nil {
return "", fmt.Errorf("error creating execution environment: %w\ncfg: %v\n", err, runCfg)
}
err = api.ContainerExecStart(ctx, ret.ID, types.ExecStartCheck{})
if err != nil {
return "", fmt.Errorf("error starting command execution: %w\ncfg: %v\nret: %v\n", err, runCfg, ret)
}
return ret.ID, nil
}
// Mapping of path->contents
type PathContents interface {
UpdateHeader(header *tar.Header) error
Get() ([]byte, error)
SetMode(mode int64)
SetOwners(uid int, gid int)
}
type FileContents struct {
Data []byte
Mode int64
UID int
GID int
}
func (b FileContents) UpdateHeader(header *tar.Header) error {
header.Mode = b.Mode
header.Uid = b.UID
header.Gid = b.GID
return nil
}
func (b FileContents) Get() ([]byte, error) {
return b.Data, nil
}
func (b *FileContents) SetMode(mode int64) {
b.Mode = mode
}
func (b *FileContents) SetOwners(uid int, gid int) {
b.UID = uid
b.GID = gid
}
func PathContentsFromBytes(data []byte) PathContents {
return &FileContents{
Data: data,
Mode: 0o644,
}
}
func PathContentsFromString(data string) PathContents {
return PathContentsFromBytes([]byte(data))
}
type BuildContext map[string]PathContents
func NewBuildContext() BuildContext {
return BuildContext{}
}
func BuildContextFromTarball(reader io.Reader) (BuildContext, error) {
archive := tar.NewReader(reader)
bCtx := NewBuildContext()
for true {
header, err := archive.Next()
if err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("failed to parse provided tarball: %v", err)
}
data := make([]byte, int(header.Size))
read, err := archive.Read(data)
if err != nil {
return nil, fmt.Errorf("failed to parse read from provided tarball: %v", err)
}
if read != int(header.Size) {
return nil, fmt.Errorf("unexpectedly short read on tarball: %v of %v", read, header.Size)
}
bCtx[header.Name] = &FileContents{
Data: data,
Mode: header.Mode,
UID: header.Uid,
GID: header.Gid,
}
}
return bCtx, nil
}
func (bCtx *BuildContext) ToTarball() (io.Reader, error) {
var err error
buffer := new(bytes.Buffer)
tarBuilder := tar.NewWriter(buffer)
defer tarBuilder.Close()
now := time.Now()
for filepath, contents := range *bCtx {
fileHeader := &tar.Header{
Name: filepath,
ModTime: now,
AccessTime: now,
ChangeTime: now,
}
if contents == nil && !strings.HasSuffix(filepath, "/") {
return nil, fmt.Errorf("expected file path (%v) to have trailing / due to nil contents, indicating directory", filepath)
}
if err := contents.UpdateHeader(fileHeader); err != nil {
return nil, fmt.Errorf("failed to update tar header entry for %v: %w", filepath, err)
}
var rawContents []byte
if contents != nil {
rawContents, err = contents.Get()
if err != nil {
return nil, fmt.Errorf("failed to get file contents for %v: %w", filepath, err)
}
fileHeader.Size = int64(len(rawContents))
}
if err := tarBuilder.WriteHeader(fileHeader); err != nil {
return nil, fmt.Errorf("failed to write tar header entry for %v: %w", filepath, err)
}
if contents != nil {
if _, err := tarBuilder.Write(rawContents); err != nil {
return nil, fmt.Errorf("failed to write tar file entry for %v: %w", filepath, err)
}
}
}
return bytes.NewReader(buffer.Bytes()), nil
}
type BuildOpt interface {
Apply(cfg *types.ImageBuildOptions) error
}
type BuildRemove bool
var _ BuildOpt = (*BuildRemove)(nil)
func (u BuildRemove) Apply(cfg *types.ImageBuildOptions) error {
cfg.Remove = bool(u)
return nil
}
type BuildForceRemove bool
var _ BuildOpt = (*BuildForceRemove)(nil)
func (u BuildForceRemove) Apply(cfg *types.ImageBuildOptions) error {
cfg.ForceRemove = bool(u)
return nil
}
type BuildPullParent bool
var _ BuildOpt = (*BuildPullParent)(nil)
func (u BuildPullParent) Apply(cfg *types.ImageBuildOptions) error {
cfg.PullParent = bool(u)
return nil
}
type BuildArgs map[string]*string
var _ BuildOpt = (*BuildArgs)(nil)
func (u BuildArgs) Apply(cfg *types.ImageBuildOptions) error {
cfg.BuildArgs = u
return nil
}
type BuildTags []string
var _ BuildOpt = (*BuildTags)(nil)
func (u BuildTags) Apply(cfg *types.ImageBuildOptions) error {
cfg.Tags = u
return nil
}
const containerfilePath = "_containerfile"
func (d *Runner) BuildImage(ctx context.Context, containerfile string, containerContext BuildContext, opts ...BuildOpt) ([]byte, error) {
return BuildImage(ctx, d.DockerAPI, containerfile, containerContext, opts...)
}
func BuildImage(ctx context.Context, api *client.Client, containerfile string, containerContext BuildContext, opts ...BuildOpt) ([]byte, error) {
var cfg types.ImageBuildOptions
// Build container context tarball, provisioning containerfile in.
containerContext[containerfilePath] = PathContentsFromBytes([]byte(containerfile))
tar, err := containerContext.ToTarball()
if err != nil {
return nil, fmt.Errorf("failed to create build image context tarball: %w", err)
}
cfg.Dockerfile = "/" + containerfilePath
// Apply all given options
for index, opt := range opts {
if err := opt.Apply(&cfg); err != nil {
return nil, fmt.Errorf("failed to apply option (%d / %v): %w", index, opt, err)
}
}
resp, err := api.ImageBuild(ctx, tar, cfg)
if err != nil {
return nil, fmt.Errorf("failed to build image: %v", err)
}
output, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read image build output: %w", err)
}
return output, nil
}
func (d *Runner) CopyTo(container string, destination string, contents BuildContext) error {
// XXX: currently we use the default options but we might want to allow
// modifying cfg.CopyUIDGID in the future.
var cfg types.CopyToContainerOptions
// Convert our provided contents to a tarball to ship up.
tar, err := contents.ToTarball()
if err != nil {
return fmt.Errorf("failed to build contents into tarball: %v", err)
}
return d.DockerAPI.CopyToContainer(context.Background(), container, destination, tar, cfg)
}
func (d *Runner) CopyFrom(container string, source string) (BuildContext, *types.ContainerPathStat, error) {
reader, stat, err := d.DockerAPI.CopyFromContainer(context.Background(), container, source)
if err != nil {
return nil, nil, fmt.Errorf("failed to read %v from container: %v", source, err)
}
result, err := BuildContextFromTarball(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to build archive from result: %v", err)
}
return result, &stat, nil
}
func (d *Runner) GetNetworkAndAddresses(container string) (map[string]string, error) {
response, err := d.DockerAPI.ContainerInspect(context.Background(), container)
if err != nil {
return nil, fmt.Errorf("failed to fetch container inspection data: %v", err)
}
if response.NetworkSettings == nil || len(response.NetworkSettings.Networks) == 0 {
return nil, fmt.Errorf("container (%v) had no associated network settings: %v", container, response)
}
ret := make(map[string]string)
ns := response.NetworkSettings.Networks
for network, data := range ns {
if data == nil {
continue
}
ret[network] = data.IPAddress
}
if len(ret) == 0 {
return nil, fmt.Errorf("no valid network data for container (%v): %v", container, response)
}
return ret, nil
}