1536 lines
45 KiB
Go
1536 lines
45 KiB
Go
package dockerclient
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
dockertypes "github.com/docker/docker/api/types"
|
|
docker "github.com/fsouza/go-dockerclient"
|
|
"k8s.io/klog"
|
|
|
|
"github.com/openshift/imagebuilder"
|
|
"github.com/openshift/imagebuilder/dockerfile/parser"
|
|
"github.com/openshift/imagebuilder/imageprogress"
|
|
)
|
|
|
|
// NewClientFromEnv is exposed to simplify getting a client when vendoring this library.
|
|
func NewClientFromEnv() (*docker.Client, error) {
|
|
return docker.NewClientFromEnv()
|
|
}
|
|
|
|
// Mount represents a binding between the current system and the destination client
|
|
type Mount struct {
|
|
SourcePath string
|
|
DestinationPath string
|
|
}
|
|
|
|
// ClientExecutor can run Docker builds from a Docker client.
|
|
type ClientExecutor struct {
|
|
// Name is an optional name for this executor.
|
|
Name string
|
|
// Named is a map of other named executors.
|
|
Named map[string]*ClientExecutor
|
|
|
|
// TempDir is the temporary directory to use for storing file
|
|
// contents. If unset, the default temporary directory for the
|
|
// system will be used.
|
|
TempDir string
|
|
// Client is a client to a Docker daemon.
|
|
Client *docker.Client
|
|
// Directory is the context directory to build from, will use
|
|
// the current working directory if not set. Ignored if
|
|
// ContextArchive is set.
|
|
Directory string
|
|
// A compressed or uncompressed tar archive that should be used
|
|
// as the build context.
|
|
ContextArchive string
|
|
// Excludes are a list of file patterns that should be excluded
|
|
// from the context. Will be set to the contents of the
|
|
// .dockerignore file if nil.
|
|
Excludes []string
|
|
// Tag is an optional value to tag the resulting built image.
|
|
Tag string
|
|
// Additional tags is an optional array of other tags to apply
|
|
// to the image.
|
|
AdditionalTags []string
|
|
// AllowPull when set will pull images that are not present on
|
|
// the daemon.
|
|
AllowPull bool
|
|
// IgnoreUnrecognizedInstructions, if true, allows instructions
|
|
// that are not yet supported to be ignored (will be printed)
|
|
IgnoreUnrecognizedInstructions bool
|
|
// StrictVolumeOwnership if true will fail the build if a RUN
|
|
// command follows a VOLUME command, since this client cannot
|
|
// guarantee that the restored contents of the VOLUME directory
|
|
// will have the right permissions.
|
|
StrictVolumeOwnership bool
|
|
// TransientMounts are a set of mounts from outside the build
|
|
// to the inside that will not be part of the final image. Any
|
|
// content created inside the mount's destinationPath will be
|
|
// omitted from the final image.
|
|
TransientMounts []Mount
|
|
|
|
// The path within the container to perform the transient mount.
|
|
ContainerTransientMount string
|
|
|
|
// The streams used for canonical output.
|
|
Out, ErrOut io.Writer
|
|
|
|
// Container is optional and can be set to a container to use as
|
|
// the execution environment for a build.
|
|
Container *docker.Container
|
|
// Command, if set, will be used as the entrypoint for the new
|
|
// container. This is ignored if Container is set.
|
|
Command []string
|
|
// Image is optional and may be set to control which image is used
|
|
// as a base for this build. Otherwise the FROM value from the
|
|
// Dockerfile is read (will be pulled if not locally present).
|
|
Image *docker.Image
|
|
// Committed is optional and is used to track a temporary image, if one
|
|
// was created, that was based on the container as its stage ended.
|
|
Committed *docker.Image
|
|
|
|
// AuthFn will handle authenticating any docker pulls if Image
|
|
// is set to nil.
|
|
AuthFn func(name string) ([]dockertypes.AuthConfig, bool)
|
|
// HostConfig is used to start the container (if necessary).
|
|
HostConfig *docker.HostConfig
|
|
// LogFn is an optional command to log information to the end user
|
|
LogFn func(format string, args ...interface{})
|
|
|
|
// Deferred is a list of operations that must be cleaned up at
|
|
// the end of execution. Use Release() to invoke all of these.
|
|
Deferred []func() error
|
|
|
|
// Volumes handles saving and restoring volumes after RUN
|
|
// commands are executed.
|
|
Volumes *ContainerVolumeTracker
|
|
}
|
|
|
|
// NoAuthFn can be used for AuthFn when no authentication is required in Docker.
|
|
func NoAuthFn(string) ([]dockertypes.AuthConfig, bool) {
|
|
return nil, false
|
|
}
|
|
|
|
// NewClientExecutor creates a client executor.
|
|
func NewClientExecutor(client *docker.Client) *ClientExecutor {
|
|
return &ClientExecutor{
|
|
Client: client,
|
|
LogFn: func(string, ...interface{}) {},
|
|
|
|
ContainerTransientMount: "/.imagebuilder-transient-mount",
|
|
}
|
|
}
|
|
|
|
// DefaultExcludes reads the default list of excluded file patterns from the
|
|
// context directory's .containerignore file if it exists, or from the context
|
|
// directory's .dockerignore file, if it exists.
|
|
func (e *ClientExecutor) DefaultExcludes() error {
|
|
var err error
|
|
e.Excludes, err = imagebuilder.ParseDockerignore(e.Directory)
|
|
return err
|
|
}
|
|
|
|
// WithName creates a new child executor that will be used whenever a COPY statement
|
|
// uses --from=NAME or --from=POSITION.
|
|
func (e *ClientExecutor) WithName(name string, position int) *ClientExecutor {
|
|
if e.Named == nil {
|
|
e.Named = make(map[string]*ClientExecutor)
|
|
}
|
|
e.Deferred = append([]func() error{func() error {
|
|
stage, ok := e.Named[strconv.Itoa(position)]
|
|
if !ok {
|
|
return fmt.Errorf("error finding stage %d", position)
|
|
}
|
|
errs := stage.Release()
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("%v", errs)
|
|
}
|
|
return nil
|
|
}}, e.Deferred...)
|
|
|
|
copied := *e
|
|
copied.Name = name
|
|
copied.Container = nil
|
|
copied.Deferred = nil
|
|
copied.Image = nil
|
|
copied.Volumes = nil
|
|
copied.Committed = nil
|
|
|
|
child := &copied
|
|
e.Named[name] = child
|
|
e.Named[strconv.Itoa(position)] = child
|
|
return child
|
|
}
|
|
|
|
// Stages executes all of the provided stages, starting from the base image. It returns the executor of the last stage
|
|
// or an error if a stage fails.
|
|
func (e *ClientExecutor) Stages(b *imagebuilder.Builder, stages imagebuilder.Stages, from string) (*ClientExecutor, error) {
|
|
var stageExecutor *ClientExecutor
|
|
for i, stage := range stages {
|
|
stageExecutor = e.WithName(stage.Name, stage.Position)
|
|
|
|
var stageFrom string
|
|
if i == 0 {
|
|
stageFrom = from
|
|
} else {
|
|
from, err := b.From(stage.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if prereq := e.Named[from]; prereq != nil {
|
|
b, ok := stages.ByName(from)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error: Unable to find stage %s builder", from)
|
|
}
|
|
if prereq.Committed == nil {
|
|
config := b.Builder.Config()
|
|
if prereq.Container.State.Running {
|
|
klog.V(4).Infof("Stopping container %s ...", prereq.Container.ID)
|
|
if err := e.Client.StopContainer(prereq.Container.ID, 0); err != nil {
|
|
return nil, fmt.Errorf("unable to stop build container: %v", err)
|
|
}
|
|
prereq.Container.State.Running = false
|
|
// Starting the container may perform escaping of args, so to be consistent
|
|
// we also set that here
|
|
config.ArgsEscaped = true
|
|
}
|
|
image, err := e.Client.CommitContainer(docker.CommitContainerOptions{
|
|
Container: prereq.Container.ID,
|
|
Run: config,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to commit stage %s container: %v", from, err)
|
|
}
|
|
klog.V(4).Infof("Committed %s to %s as basis for image %q: %#v", prereq.Container.ID, image.ID, from, config)
|
|
// deleting this image will fail with an "image has dependent child images" error
|
|
// if it ends up being an ancestor of the final image, so don't bother returning
|
|
// errors from this specific removeImage() call
|
|
prereq.Deferred = append([]func() error{func() error { e.removeImage(image.ID); return nil }}, prereq.Deferred...)
|
|
prereq.Committed = image
|
|
}
|
|
klog.V(4).Infof("Using image %s based on previous stage %s as image", prereq.Committed.ID, from)
|
|
from = prereq.Committed.ID
|
|
}
|
|
stageFrom = from
|
|
}
|
|
|
|
if err := stageExecutor.Prepare(stage.Builder, stage.Node, stageFrom); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := stageExecutor.Execute(stage.Builder, stage.Node); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// remember the outcome of the stage execution on the container config in case
|
|
// another stage needs to access incremental state
|
|
stageExecutor.Container.Config = stage.Builder.Config()
|
|
}
|
|
return stageExecutor, nil
|
|
}
|
|
|
|
// Build is a helper method to perform a Docker build against the
|
|
// provided Docker client. It will load the image if not specified,
|
|
// create a container if one does not already exist, and start a
|
|
// container if the Dockerfile contains RUN commands. It will cleanup
|
|
// any containers it creates directly, and set the e.Committed.ID field
|
|
// to the generated image.
|
|
func (e *ClientExecutor) Build(b *imagebuilder.Builder, node *parser.Node, from string) error {
|
|
defer e.Release()
|
|
if err := e.Prepare(b, node, from); err != nil {
|
|
return err
|
|
}
|
|
if err := e.Execute(b, node); err != nil {
|
|
return err
|
|
}
|
|
return e.Commit(b)
|
|
}
|
|
|
|
func (e *ClientExecutor) Prepare(b *imagebuilder.Builder, node *parser.Node, from string) error {
|
|
var err error
|
|
|
|
// identify the base image
|
|
if len(from) == 0 {
|
|
from, err = b.From(node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// load the image
|
|
if e.Image == nil {
|
|
if from == imagebuilder.NoBaseImageSpecifier {
|
|
if runtime.GOOS == "windows" {
|
|
return fmt.Errorf("building from scratch images is not supported")
|
|
}
|
|
from, err = e.CreateScratchImage()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create a scratch image for this build: %v", err)
|
|
}
|
|
e.Deferred = append([]func() error{func() error { return e.removeImage(from) }}, e.Deferred...)
|
|
}
|
|
klog.V(4).Infof("Retrieving image %q", from)
|
|
e.Image, err = e.LoadImageWithPlatform(from, b.Platform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// update the builder with any information from the image, including ONBUILD
|
|
// statements
|
|
if err := b.FromImage(e.Image, node); err != nil {
|
|
return err
|
|
}
|
|
|
|
b.RunConfig.Image = from
|
|
if len(e.Name) > 0 {
|
|
e.LogFn("FROM %s as %s", from, e.Name)
|
|
} else {
|
|
e.LogFn("FROM %s", from)
|
|
}
|
|
klog.V(4).Infof("step: FROM %s as %s", from, e.Name)
|
|
|
|
b.Excludes = e.Excludes
|
|
|
|
var sharedMount string
|
|
|
|
defaultShell := b.RunConfig.Shell
|
|
if len(defaultShell) == 0 {
|
|
defaultShell = []string{"/bin/sh", "-c"}
|
|
}
|
|
|
|
// create a container to execute in, if necessary
|
|
mustStart := b.RequiresStart(node)
|
|
if e.Container == nil {
|
|
opts := docker.CreateContainerOptions{
|
|
Config: &docker.Config{
|
|
Image: from,
|
|
},
|
|
HostConfig: &docker.HostConfig{},
|
|
}
|
|
if e.HostConfig != nil {
|
|
opts.HostConfig = e.HostConfig
|
|
}
|
|
originalBinds := opts.HostConfig.Binds
|
|
|
|
if mustStart {
|
|
// Transient mounts only make sense on images that will be running processes
|
|
if len(e.TransientMounts) > 0 {
|
|
volumeName, err := randSeq(imageSafeCharacters, 24)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v, err := e.Client.CreateVolume(docker.CreateVolumeOptions{Name: volumeName})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create volume to mount secrets: %v", err)
|
|
}
|
|
e.Deferred = append([]func() error{func() error { return e.Client.RemoveVolume(volumeName) }}, e.Deferred...)
|
|
sharedMount = v.Mountpoint
|
|
opts.HostConfig.Binds = append(opts.HostConfig.Binds, volumeName+":"+e.ContainerTransientMount)
|
|
}
|
|
|
|
// TODO: windows support
|
|
if len(e.Command) > 0 {
|
|
opts.Config.Cmd = e.Command
|
|
opts.Config.Entrypoint = nil
|
|
} else {
|
|
// TODO; replace me with a better default command
|
|
opts.Config.Cmd = []string{"# (imagebuilder)\n/bin/sleep 86400"}
|
|
opts.Config.Entrypoint = append([]string{}, defaultShell...)
|
|
}
|
|
}
|
|
|
|
if len(opts.Config.Cmd) == 0 {
|
|
opts.Config.Entrypoint = append(append([]string{}, defaultShell...), "#(imagebuilder)")
|
|
}
|
|
|
|
// copy any source content into the temporary mount path
|
|
if mustStart && len(e.TransientMounts) > 0 {
|
|
if len(sharedMount) == 0 {
|
|
return fmt.Errorf("no mount point available for temporary mounts")
|
|
}
|
|
binds, err := e.PopulateTransientMounts(opts, e.TransientMounts, sharedMount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.HostConfig.Binds = append(originalBinds, binds...)
|
|
}
|
|
|
|
klog.V(4).Infof("Creating container with %#v %#v", opts.Config, opts.HostConfig)
|
|
container, err := e.Client.CreateContainer(opts)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create build container: %v", err)
|
|
}
|
|
e.Container = container
|
|
e.Deferred = append([]func() error{func() error { return e.removeContainer(container.ID) }}, e.Deferred...)
|
|
}
|
|
|
|
// TODO: lazy start
|
|
if mustStart && !e.Container.State.Running {
|
|
if err := e.Client.StartContainer(e.Container.ID, nil); err != nil {
|
|
return fmt.Errorf("unable to start build container: %v", err)
|
|
}
|
|
e.Container.State.Running = true
|
|
// TODO: is this racy? may have to loop wait in the actual run step
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Execute performs all of the provided steps against the initialized container. May be
|
|
// invoked multiple times for a given container.
|
|
func (e *ClientExecutor) Execute(b *imagebuilder.Builder, node *parser.Node) error {
|
|
for i, child := range node.Children {
|
|
step := b.Step()
|
|
if err := step.Resolve(child); err != nil {
|
|
return err
|
|
}
|
|
klog.V(4).Infof("step: %s", step.Original)
|
|
if e.LogFn != nil {
|
|
// original may have unescaped %, so perform fmt escaping
|
|
e.LogFn(strings.Replace(step.Original, "%", "%%", -1))
|
|
}
|
|
noRunsRemaining := !b.RequiresStart(&parser.Node{Children: node.Children[i+1:]})
|
|
|
|
if err := b.Run(step, e, noRunsRemaining); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Commit saves the completed build as an image with the provided tag. It will
|
|
// stop the container, commit the image, and then remove the container.
|
|
func (e *ClientExecutor) Commit(b *imagebuilder.Builder) error {
|
|
config := b.Config()
|
|
|
|
if e.Container.State.Running {
|
|
klog.V(4).Infof("Stopping container %s ...", e.Container.ID)
|
|
if err := e.Client.StopContainer(e.Container.ID, 0); err != nil {
|
|
return fmt.Errorf("unable to stop build container: %v", err)
|
|
}
|
|
e.Container.State.Running = false
|
|
// Starting the container may perform escaping of args, so to be consistent
|
|
// we also set that here
|
|
config.ArgsEscaped = true
|
|
}
|
|
|
|
var repository, tag string
|
|
if len(e.Tag) > 0 {
|
|
repository, tag = docker.ParseRepositoryTag(e.Tag)
|
|
klog.V(4).Infof("Committing built container %s as image %q: %#v", e.Container.ID, e.Tag, config)
|
|
if e.LogFn != nil {
|
|
e.LogFn("Committing changes to %s ...", e.Tag)
|
|
}
|
|
} else {
|
|
klog.V(4).Infof("Committing built container %s: %#v", e.Container.ID, config)
|
|
if e.LogFn != nil {
|
|
e.LogFn("Committing changes ...")
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
for _, err := range e.Release() {
|
|
e.LogFn("Unable to cleanup: %v", err)
|
|
}
|
|
}()
|
|
|
|
image, err := e.Client.CommitContainer(docker.CommitContainerOptions{
|
|
Author: b.Author,
|
|
Container: e.Container.ID,
|
|
Run: config,
|
|
Repository: repository,
|
|
Tag: tag,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to commit build container: %v", err)
|
|
}
|
|
|
|
e.Committed = image
|
|
klog.V(4).Infof("Committed %s to %s", e.Container.ID, image.ID)
|
|
|
|
if len(e.Tag) > 0 {
|
|
for _, s := range e.AdditionalTags {
|
|
repository, tag := docker.ParseRepositoryTag(s)
|
|
err := e.Client.TagImage(image.ID, docker.TagImageOptions{
|
|
Repo: repository,
|
|
Tag: tag,
|
|
})
|
|
if err != nil {
|
|
e.Deferred = append([]func() error{func() error { return e.removeImage(image.ID) }}, e.Deferred...)
|
|
return fmt.Errorf("unable to tag %q: %v", s, err)
|
|
}
|
|
e.LogFn("Tagged as %s", s)
|
|
}
|
|
}
|
|
|
|
if e.LogFn != nil {
|
|
e.LogFn("Done")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ClientExecutor) PopulateTransientMounts(opts docker.CreateContainerOptions, transientMounts []Mount, sharedMount string) ([]string, error) {
|
|
container, err := e.Client.CreateContainer(opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to create transient container: %v", err)
|
|
}
|
|
defer e.removeContainer(container.ID)
|
|
|
|
var copies []imagebuilder.Copy
|
|
for i, mount := range transientMounts {
|
|
copies = append(copies, imagebuilder.Copy{
|
|
FromFS: true,
|
|
Src: []string{mount.SourcePath},
|
|
Dest: filepath.Join(e.ContainerTransientMount, strconv.Itoa(i)),
|
|
})
|
|
}
|
|
if err := e.CopyContainer(container, nil, copies...); err != nil {
|
|
return nil, fmt.Errorf("unable to copy transient context into container: %v", err)
|
|
}
|
|
|
|
// mount individual items temporarily
|
|
var binds []string
|
|
for i, mount := range e.TransientMounts {
|
|
binds = append(binds, fmt.Sprintf("%s:%s:%s", filepath.Join(sharedMount, strconv.Itoa(i)), mount.DestinationPath, "ro"))
|
|
}
|
|
return binds, nil
|
|
}
|
|
|
|
// Release deletes any items started by this executor.
|
|
func (e *ClientExecutor) Release() []error {
|
|
errs := e.Volumes.Release()
|
|
for _, fn := range e.Deferred {
|
|
if err := fn(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
e.Deferred = nil
|
|
return errs
|
|
}
|
|
|
|
// removeContainer removes the provided container ID
|
|
func (e *ClientExecutor) removeContainer(id string) error {
|
|
e.Client.StopContainer(id, 0)
|
|
err := e.Client.RemoveContainer(docker.RemoveContainerOptions{
|
|
ID: id,
|
|
RemoveVolumes: true,
|
|
Force: true,
|
|
})
|
|
if _, ok := err.(*docker.NoSuchContainer); err != nil && !ok {
|
|
return fmt.Errorf("unable to cleanup container %s: %v", id, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// removeImage removes the provided image ID
|
|
func (e *ClientExecutor) removeImage(id string) error {
|
|
if err := e.Client.RemoveImageExtended(id, docker.RemoveImageOptions{
|
|
Force: true,
|
|
}); err != nil {
|
|
return fmt.Errorf("unable to clean up image %s: %v", id, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateScratchImage creates a new, zero byte layer that is identical to "scratch"
|
|
// except that the resulting image will have two layers.
|
|
func (e *ClientExecutor) CreateScratchImage() (string, error) {
|
|
random, err := randSeq(imageSafeCharacters, 24)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
name := fmt.Sprintf("scratch%s", random)
|
|
|
|
buf := &bytes.Buffer{}
|
|
w := tar.NewWriter(buf)
|
|
w.Close()
|
|
|
|
return name, e.Client.ImportImage(docker.ImportImageOptions{
|
|
Repository: name,
|
|
Source: "-",
|
|
InputStream: buf,
|
|
})
|
|
}
|
|
|
|
// imageSafeCharacters are characters allowed to be part of a Docker image name.
|
|
const imageSafeCharacters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
|
|
// randSeq returns a sequence of random characters drawn from source. It returns
|
|
// an error if cryptographic randomness is not available or source is more than 255
|
|
// characters.
|
|
func randSeq(source string, n int) (string, error) {
|
|
if len(source) > 255 {
|
|
return "", fmt.Errorf("source must be less than 256 bytes long")
|
|
}
|
|
random := make([]byte, n)
|
|
if _, err := io.ReadFull(rand.Reader, random); err != nil {
|
|
return "", err
|
|
}
|
|
for i := range random {
|
|
random[i] = source[random[i]%byte(len(source))]
|
|
}
|
|
return string(random), nil
|
|
}
|
|
|
|
// LoadImage checks the client for an image matching from. If not found,
|
|
// attempts to pull the image and then tries to inspect again.
|
|
func (e *ClientExecutor) LoadImage(from string) (*docker.Image, error) {
|
|
return e.LoadImageWithPlatform(from, "")
|
|
}
|
|
|
|
// LoadImage checks the client for an image matching from. If not found,
|
|
// attempts to pull the image with specified platform string.
|
|
func (e *ClientExecutor) LoadImageWithPlatform(from string, platform string) (*docker.Image, error) {
|
|
image, err := e.Client.InspectImage(from)
|
|
if err == nil {
|
|
return image, nil
|
|
}
|
|
if err != docker.ErrNoSuchImage {
|
|
return nil, err
|
|
}
|
|
|
|
if !e.AllowPull {
|
|
klog.V(4).Infof("image %s did not exist", from)
|
|
return nil, docker.ErrNoSuchImage
|
|
}
|
|
|
|
repository, tag := docker.ParseRepositoryTag(from)
|
|
if len(tag) == 0 {
|
|
tag = "latest"
|
|
}
|
|
|
|
klog.V(4).Infof("attempting to pull %s with auth from repository %s:%s", from, repository, tag)
|
|
|
|
// TODO: we may want to abstract looping over multiple credentials
|
|
auth, _ := e.AuthFn(repository)
|
|
if len(auth) == 0 {
|
|
auth = append(auth, dockertypes.AuthConfig{})
|
|
}
|
|
|
|
if e.LogFn != nil {
|
|
e.LogFn("Image %s was not found, pulling ...", from)
|
|
}
|
|
|
|
var lastErr error
|
|
outputProgress := func(s string) {
|
|
e.LogFn("%s", s)
|
|
}
|
|
for _, config := range auth {
|
|
// TODO: handle IDs?
|
|
var pullErr error
|
|
func() { // A scope for defer
|
|
pullWriter := imageprogress.NewPullWriter(outputProgress)
|
|
defer func() {
|
|
err := pullWriter.Close()
|
|
if pullErr == nil {
|
|
pullErr = err
|
|
}
|
|
}()
|
|
|
|
pullImageOptions := docker.PullImageOptions{
|
|
Repository: repository,
|
|
Tag: tag,
|
|
OutputStream: pullWriter,
|
|
Platform: platform,
|
|
RawJSONStream: true,
|
|
}
|
|
if klog.V(5) {
|
|
pullImageOptions.OutputStream = os.Stderr
|
|
pullImageOptions.RawJSONStream = false
|
|
}
|
|
authConfig := docker.AuthConfiguration{Username: config.Username, ServerAddress: config.ServerAddress, Password: config.Password}
|
|
pullErr = e.Client.PullImage(pullImageOptions, authConfig)
|
|
}()
|
|
if pullErr == nil {
|
|
break
|
|
}
|
|
lastErr = pullErr
|
|
continue
|
|
}
|
|
if lastErr != nil {
|
|
return nil, fmt.Errorf("unable to pull image (from: %s, tag: %s): %v", repository, tag, lastErr)
|
|
}
|
|
|
|
return e.Client.InspectImage(from)
|
|
}
|
|
|
|
func (e *ClientExecutor) Preserve(path string) error {
|
|
if e.Volumes == nil {
|
|
e.Volumes = NewContainerVolumeTracker()
|
|
}
|
|
|
|
if err := e.EnsureContainerPath(path); err != nil {
|
|
return err
|
|
}
|
|
|
|
e.Volumes.Add(path)
|
|
return nil
|
|
}
|
|
|
|
func (e *ClientExecutor) EnsureContainerPath(path string) error {
|
|
return e.createOrReplaceContainerPathWithOwner(path, 0, 0, nil)
|
|
}
|
|
|
|
func (e *ClientExecutor) EnsureContainerPathAs(path, user string, mode *os.FileMode) error {
|
|
uid, gid := 0, 0
|
|
|
|
u, g, err := e.getUser(user)
|
|
if err == nil {
|
|
uid = u
|
|
gid = g
|
|
}
|
|
|
|
return e.createOrReplaceContainerPathWithOwner(path, uid, gid, mode)
|
|
}
|
|
|
|
func (e *ClientExecutor) createOrReplaceContainerPathWithOwner(path string, uid, gid int, mode *os.FileMode) error {
|
|
if mode == nil {
|
|
m := os.FileMode(0755)
|
|
mode = &m
|
|
}
|
|
createPath := func(dest string) error {
|
|
var writerErr error
|
|
if !strings.HasSuffix(dest, "/") {
|
|
dest = dest + "/"
|
|
}
|
|
reader, writer := io.Pipe()
|
|
opts := docker.UploadToContainerOptions{
|
|
InputStream: reader,
|
|
Path: "/",
|
|
Context: context.TODO(),
|
|
}
|
|
go func() {
|
|
defer writer.Close()
|
|
tarball := tar.NewWriter(writer)
|
|
defer tarball.Close()
|
|
writerErr = tarball.WriteHeader(&tar.Header{
|
|
Name: dest,
|
|
Typeflag: tar.TypeDir,
|
|
Mode: int64(*mode),
|
|
Uid: uid,
|
|
Gid: gid,
|
|
})
|
|
}()
|
|
klog.V(4).Infof("Uploading empty archive to %q", dest)
|
|
err := e.Client.UploadToContainer(e.Container.ID, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to ensure existence of preserved path %s: %v", dest, err)
|
|
}
|
|
if writerErr != nil {
|
|
return fmt.Errorf("error generating tarball to ensure existence of preserved path %s: %v", dest, writerErr)
|
|
}
|
|
return nil
|
|
}
|
|
readPath := func(dest string) error {
|
|
if !strings.HasSuffix(dest, "/") {
|
|
dest = dest + "/"
|
|
}
|
|
err := e.Client.DownloadFromContainer(e.Container.ID, docker.DownloadFromContainerOptions{
|
|
Path: dest,
|
|
OutputStream: ioutil.Discard,
|
|
})
|
|
return err
|
|
}
|
|
var pathsToCreate []string
|
|
pathToCheck := path
|
|
for {
|
|
if err := readPath(pathToCheck); err != nil {
|
|
pathsToCreate = append([]string{pathToCheck}, pathsToCreate...)
|
|
}
|
|
if filepath.Dir(pathToCheck) == pathToCheck {
|
|
break
|
|
}
|
|
pathToCheck = filepath.Dir(pathToCheck)
|
|
}
|
|
for _, path := range pathsToCreate {
|
|
if err := createPath(path); err != nil {
|
|
return fmt.Errorf("error creating container directory %s: %v", path, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ClientExecutor) UnrecognizedInstruction(step *imagebuilder.Step) error {
|
|
if e.IgnoreUnrecognizedInstructions {
|
|
e.LogFn("warning: Unknown instruction: %s", strings.ToUpper(step.Command))
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Unknown instruction: %s", strings.ToUpper(step.Command))
|
|
}
|
|
|
|
// Run executes a single Run command against the current container using exec().
|
|
// Since exec does not allow ENV or WORKINGDIR to be set, we force the execution of
|
|
// the user command into a shell and perform those operations before. Since RUN
|
|
// requires /bin/sh, we can use both 'cd' and 'export'.
|
|
func (e *ClientExecutor) Run(run imagebuilder.Run, config docker.Config) error {
|
|
if len(run.Files) > 0 {
|
|
return fmt.Errorf("Heredoc syntax is not supported")
|
|
}
|
|
if len(run.Mounts) > 0 {
|
|
return fmt.Errorf("RUN --mount not supported")
|
|
}
|
|
if run.Network != "" {
|
|
return fmt.Errorf("RUN --network not supported")
|
|
}
|
|
|
|
args := make([]string, len(run.Args))
|
|
copy(args, run.Args)
|
|
|
|
defaultShell := config.Shell
|
|
if len(defaultShell) == 0 {
|
|
if runtime.GOOS == "windows" {
|
|
defaultShell = []string{"cmd", "/S", "/C"}
|
|
} else {
|
|
defaultShell = []string{"/bin/sh", "-c"}
|
|
}
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
if len(config.WorkingDir) > 0 {
|
|
args[0] = fmt.Sprintf("cd %s && %s", imagebuilder.BashQuote(config.WorkingDir), args[0])
|
|
}
|
|
// TODO: implement windows ENV
|
|
args = append(defaultShell, args...)
|
|
} else {
|
|
if run.Shell {
|
|
if len(config.WorkingDir) > 0 {
|
|
args[0] = fmt.Sprintf("cd %s && %s", imagebuilder.BashQuote(config.WorkingDir), args[0])
|
|
}
|
|
if len(config.Env) > 0 {
|
|
args[0] = imagebuilder.ExportEnv(config.Env) + args[0]
|
|
}
|
|
args = append(defaultShell, args...)
|
|
} else {
|
|
switch {
|
|
case len(config.WorkingDir) == 0 && len(config.Env) == 0:
|
|
// no change necessary
|
|
case len(args) > 0:
|
|
setup := "exec \"$@\""
|
|
if len(config.WorkingDir) > 0 {
|
|
setup = fmt.Sprintf("cd %s && %s", imagebuilder.BashQuote(config.WorkingDir), setup)
|
|
}
|
|
if len(config.Env) > 0 {
|
|
setup = imagebuilder.ExportEnv(config.Env) + setup
|
|
}
|
|
newArgs := make([]string, 0, len(args)+4)
|
|
newArgs = append(newArgs, defaultShell...)
|
|
newArgs = append(newArgs, setup, "")
|
|
newArgs = append(newArgs, args...)
|
|
args = newArgs
|
|
}
|
|
}
|
|
}
|
|
|
|
if e.StrictVolumeOwnership && !e.Volumes.Empty() {
|
|
return fmt.Errorf("a RUN command was executed after a VOLUME command, which may result in ownership information being lost")
|
|
}
|
|
if err := e.Volumes.Save(e.Container.ID, e.TempDir, e.Client); err != nil {
|
|
return err
|
|
}
|
|
|
|
config.Cmd = args
|
|
klog.V(4).Infof("Running %#v inside of %s as user %s", config.Cmd, e.Container.ID, config.User)
|
|
exec, err := e.Client.CreateExec(docker.CreateExecOptions{
|
|
Cmd: config.Cmd,
|
|
Container: e.Container.ID,
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
User: config.User,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := e.Client.StartExec(exec.ID, docker.StartExecOptions{
|
|
OutputStream: e.Out,
|
|
ErrorStream: e.ErrOut,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
status, err := e.Client.InspectExec(exec.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if status.ExitCode != 0 {
|
|
klog.V(4).Infof("Failed command (code %d): %v", status.ExitCode, args)
|
|
return fmt.Errorf("running '%s' failed with exit code %d", strings.Join(run.Args, " "), status.ExitCode)
|
|
}
|
|
|
|
if err := e.Volumes.Restore(e.Container.ID, e.Client); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Copy implements the executor copy function.
|
|
func (e *ClientExecutor) Copy(excludes []string, copies ...imagebuilder.Copy) error {
|
|
// copying content into a volume invalidates the archived state of any given directory
|
|
for _, copy := range copies {
|
|
if copy.Checksum != "" {
|
|
return fmt.Errorf("ADD --checksum not supported")
|
|
}
|
|
if len(copy.Files) > 0 {
|
|
return fmt.Errorf("Heredoc syntax is not supported")
|
|
}
|
|
e.Volumes.Invalidate(copy.Dest)
|
|
}
|
|
|
|
return e.CopyContainer(e.Container, excludes, copies...)
|
|
}
|
|
|
|
func (e *ClientExecutor) findMissingParents(container *docker.Container, dest string) (parents []string, err error) {
|
|
destParent := filepath.Clean(dest)
|
|
for filepath.Dir(destParent) != destParent {
|
|
exists, err := isContainerPathDirectory(e.Client, container.ID, destParent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
parents = append(parents, destParent)
|
|
}
|
|
destParent = filepath.Dir(destParent)
|
|
}
|
|
return parents, nil
|
|
}
|
|
|
|
func (e *ClientExecutor) getUser(userspec string) (int, int, error) {
|
|
readFile := func(path string) ([]byte, error) {
|
|
var buffer, contents bytes.Buffer
|
|
if err := e.Client.DownloadFromContainer(e.Container.ID, docker.DownloadFromContainerOptions{
|
|
OutputStream: &buffer,
|
|
Path: path,
|
|
Context: context.TODO(),
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
tr := tar.NewReader(&buffer)
|
|
hdr, err := tr.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA {
|
|
return nil, fmt.Errorf("expected %q to be a regular file, but it was of type %q", path, string(hdr.Typeflag))
|
|
}
|
|
if filepath.FromSlash(hdr.Name) != filepath.Base(path) {
|
|
return nil, fmt.Errorf("error reading contents of %q: got %q instead", path, hdr.Name)
|
|
}
|
|
n, err := io.Copy(&contents, tr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading contents of %q: %v", path, err)
|
|
}
|
|
if n != hdr.Size {
|
|
return nil, fmt.Errorf("size mismatch reading contents of %q: %v", path, err)
|
|
}
|
|
hdr, err = tr.Next()
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return nil, fmt.Errorf("error reading archive of %q: %v", path, err)
|
|
}
|
|
if err == nil {
|
|
return nil, fmt.Errorf("got unexpected extra content while reading archive of %q: %v", path, err)
|
|
}
|
|
return contents.Bytes(), nil
|
|
}
|
|
parse := func(file []byte, matchField int, key string, numFields, readField int) (string, error) {
|
|
var value *string
|
|
scanner := bufio.NewScanner(bytes.NewReader(file))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
fields := strings.SplitN(line, ":", numFields)
|
|
if len(fields) != numFields {
|
|
return "", fmt.Errorf("error parsing line %q: incorrect number of fields", line)
|
|
}
|
|
if fields[matchField] != key {
|
|
continue
|
|
}
|
|
v := fields[readField]
|
|
value = &v
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return "", fmt.Errorf("error scanning file: %v", err)
|
|
}
|
|
if value == nil {
|
|
return "", os.ErrNotExist
|
|
}
|
|
return *value, nil
|
|
}
|
|
|
|
spec := strings.SplitN(userspec, ":", 2)
|
|
if len(spec) == 2 {
|
|
parsedUid, err := strconv.ParseUint(spec[0], 10, 32)
|
|
if err != nil {
|
|
// maybe it's a user name? look up the UID
|
|
passwdFile, err := readFile("/etc/passwd")
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
uid, err := parse(passwdFile, 0, spec[0], 7, 2)
|
|
if err != nil {
|
|
return -1, -1, fmt.Errorf("error reading UID value from passwd file for --chown=%s: %v", spec[0], err)
|
|
}
|
|
parsedUid, err = strconv.ParseUint(uid, 10, 32)
|
|
if err != nil {
|
|
return -1, -1, fmt.Errorf("error parsing UID value %q from passwd file for --chown=%s", uid, userspec)
|
|
}
|
|
}
|
|
parsedGid, err := strconv.ParseUint(spec[1], 10, 32)
|
|
if err != nil {
|
|
// maybe it's a group name? look up the GID
|
|
groupFile, err := readFile("/etc/group")
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
gid, err := parse(groupFile, 0, spec[1], 4, 2)
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
parsedGid, err = strconv.ParseUint(gid, 10, 32)
|
|
if err != nil {
|
|
return -1, -1, fmt.Errorf("error parsing GID value %q from group file for --chown=%s", gid, userspec)
|
|
}
|
|
}
|
|
return int(parsedUid), int(parsedGid), nil
|
|
}
|
|
|
|
var parsedUid, parsedGid uint64
|
|
if id, err := strconv.ParseUint(spec[0], 10, 32); err == nil {
|
|
// it's an ID. use it as both the UID and the GID
|
|
parsedUid = id
|
|
parsedGid = id
|
|
} else {
|
|
// it's a user name, we'll need to look up their UID and primary GID
|
|
passwdFile, err := readFile("/etc/passwd")
|
|
if err != nil {
|
|
return -1, -1, err
|
|
}
|
|
// read the UID and primary GID
|
|
uid, err := parse(passwdFile, 0, spec[0], 7, 2)
|
|
if err != nil {
|
|
return -1, -1, fmt.Errorf("error reading UID value from /etc/passwd for --chown=%s", userspec)
|
|
}
|
|
gid, err := parse(passwdFile, 0, spec[0], 7, 3)
|
|
if err != nil {
|
|
return -1, -1, fmt.Errorf("error reading GID value from /etc/passwd for --chown=%s", userspec)
|
|
}
|
|
if parsedUid, err = strconv.ParseUint(uid, 10, 32); err != nil {
|
|
return -1, -1, fmt.Errorf("error parsing UID value %q from /etc/passwd for --chown=%s", uid, userspec)
|
|
}
|
|
if parsedGid, err = strconv.ParseUint(gid, 10, 32); err != nil {
|
|
return -1, -1, fmt.Errorf("error parsing GID value %q from /etc/passwd for --chown=%s", gid, userspec)
|
|
}
|
|
}
|
|
return int(parsedUid), int(parsedGid), nil
|
|
}
|
|
|
|
// CopyContainer copies the provided content into a destination container.
|
|
func (e *ClientExecutor) CopyContainer(container *docker.Container, excludes []string, copies ...imagebuilder.Copy) error {
|
|
chownUid, chownGid := -1, -1
|
|
chown := func(h *tar.Header, r io.Reader) (data []byte, update bool, skip bool, err error) {
|
|
if chownUid != -1 {
|
|
h.Uid = chownUid
|
|
}
|
|
if chownGid != -1 {
|
|
h.Gid = chownGid
|
|
}
|
|
if (h.Uid > 0x1fffff || h.Gid > 0x1fffff) && h.Format == tar.FormatUSTAR {
|
|
h.Format = tar.FormatPAX
|
|
}
|
|
return nil, false, false, nil
|
|
}
|
|
for _, c := range copies {
|
|
var chmod func(h *tar.Header, r io.Reader) (data []byte, update bool, skip bool, err error)
|
|
if c.Chmod != "" {
|
|
parsed, err := strconv.ParseInt(c.Chmod, 8, 16)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chmod = func(h *tar.Header, r io.Reader) (data []byte, update bool, skip bool, err error) {
|
|
mode := h.Mode &^ 0o777
|
|
mode |= parsed & 0o777
|
|
h.Mode = mode
|
|
return nil, false, false, nil
|
|
}
|
|
}
|
|
chownUid, chownGid = -1, -1
|
|
if c.Chown != "" {
|
|
var err error
|
|
chownUid, chownGid, err = e.getUser(c.Chown)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// TODO: reuse source
|
|
for _, src := range c.Src {
|
|
if src == "" {
|
|
src = "*"
|
|
}
|
|
assumeDstIsDirectory := len(c.Src) > 1
|
|
repeatThisSrc:
|
|
klog.V(4).Infof("Archiving %s download=%t fromFS=%t from=%s", src, c.Download, c.FromFS, c.From)
|
|
var r io.Reader
|
|
var closer io.Closer
|
|
var err error
|
|
if len(c.From) > 0 {
|
|
if !assumeDstIsDirectory {
|
|
var err error
|
|
if assumeDstIsDirectory, err = e.isContainerGlobMultiple(e.Client, c.From, src); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
r, closer, err = e.archiveFromContainer(c.From, src, c.Dest, assumeDstIsDirectory)
|
|
} else {
|
|
r, closer, err = e.Archive(c.FromFS, src, c.Dest, c.Download, excludes)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
asOwner := ""
|
|
if c.Chown != "" {
|
|
asOwner = fmt.Sprintf(" as %d:%d", chownUid, chownGid)
|
|
// the daemon would implicitly create missing
|
|
// directories with the wrong ownership, so
|
|
// check for any that don't exist and create
|
|
// them ourselves
|
|
missingParents, err := e.findMissingParents(container, c.Dest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(missingParents) > 0 {
|
|
sort.Strings(missingParents)
|
|
klog.V(5).Infof("Uploading directories %v to %s%s", missingParents, container.ID, asOwner)
|
|
for _, missingParent := range missingParents {
|
|
if err := e.createOrReplaceContainerPathWithOwner(missingParent, chownUid, chownGid, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
filtered, err := transformArchive(r, false, chown)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r = filtered
|
|
}
|
|
if c.Chmod != "" {
|
|
filtered, err := transformArchive(r, false, chmod)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r = filtered
|
|
}
|
|
klog.V(5).Infof("Uploading to %s%s at %s", container.ID, asOwner, c.Dest)
|
|
if klog.V(6) {
|
|
logArchiveOutput(r, "Archive file for %s")
|
|
}
|
|
// add a workaround allow us to notice if a
|
|
// dstNeedsToBeDirectoryError was returned while
|
|
// attempting to read the data we're uploading,
|
|
// indicating that we thought the content would be just
|
|
// one item, but it actually isn't
|
|
reader := &readErrorWrapper{Reader: r}
|
|
r = reader
|
|
err = e.Client.UploadToContainer(container.ID, docker.UploadToContainerOptions{
|
|
InputStream: r,
|
|
Path: "/",
|
|
})
|
|
if err := closer.Close(); err != nil {
|
|
klog.Errorf("Error while closing stream container copy stream %s: %v", container.ID, err)
|
|
}
|
|
if err != nil {
|
|
if errors.Is(reader.err, dstNeedsToBeDirectoryError) && !assumeDstIsDirectory {
|
|
assumeDstIsDirectory = true
|
|
goto repeatThisSrc
|
|
}
|
|
if apiErr, ok := err.(*docker.Error); ok && apiErr.Status == 404 {
|
|
klog.V(4).Infof("path %s did not exist in container %s: %v", src, container.ID, err)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type readErrorWrapper struct {
|
|
io.Reader
|
|
err error
|
|
}
|
|
|
|
func (r *readErrorWrapper) Read(p []byte) (n int, err error) {
|
|
n, r.err = r.Reader.Read(p)
|
|
return n, r.err
|
|
}
|
|
|
|
type closers []func() error
|
|
|
|
func (c closers) Close() error {
|
|
var lastErr error
|
|
for _, fn := range c {
|
|
if err := fn(); err != nil {
|
|
lastErr = err
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
func (e *ClientExecutor) archiveFromContainer(from string, src, dst string, multipleSources bool) (io.Reader, io.Closer, error) {
|
|
var containerID string
|
|
if other, ok := e.Named[from]; ok {
|
|
if other.Container == nil {
|
|
return nil, nil, fmt.Errorf("the stage %q has not been built yet", from)
|
|
}
|
|
klog.V(5).Infof("Using container %s as input for archive request", other.Container.ID)
|
|
containerID = other.Container.ID
|
|
} else {
|
|
klog.V(5).Infof("Creating a container temporarily for image input from %q in %s", from, src)
|
|
_, err := e.LoadImage(from)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
c, err := e.Client.CreateContainer(docker.CreateContainerOptions{
|
|
Config: &docker.Config{
|
|
Image: from,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
containerID = c.ID
|
|
e.Deferred = append([]func() error{func() error { return e.removeContainer(containerID) }}, e.Deferred...)
|
|
}
|
|
|
|
check := newDirectoryCheck(e.Client, e.Container.ID)
|
|
pr, pw := io.Pipe()
|
|
var archiveRoot string
|
|
fetch := func(pw *io.PipeWriter) {
|
|
klog.V(6).Infof("Download from container %s at path %s", containerID, archiveRoot)
|
|
err := e.Client.DownloadFromContainer(containerID, docker.DownloadFromContainerOptions{
|
|
OutputStream: pw,
|
|
Path: archiveRoot,
|
|
})
|
|
pw.CloseWithError(err)
|
|
}
|
|
ar, archiveRoot, err := archiveFromContainer(pr, src, dst, nil, check, fetch, multipleSources)
|
|
if err != nil {
|
|
pr.Close()
|
|
pw.Close()
|
|
return nil, nil, err
|
|
}
|
|
closer := newCloser(func() error {
|
|
err2 := pr.Close()
|
|
err3 := ar.Close()
|
|
if err3 != nil {
|
|
return err3
|
|
}
|
|
return err2
|
|
})
|
|
go fetch(pw)
|
|
return &readCloser{Reader: ar, Closer: closer}, pr, nil
|
|
}
|
|
|
|
func (e *ClientExecutor) isContainerGlobMultiple(client *docker.Client, from, glob string) (bool, error) {
|
|
reader, closer, err := e.archiveFromContainer(from, glob, "/ignored", true)
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
|
|
defer closer.Close()
|
|
tr := tar.NewReader(reader)
|
|
|
|
h, err := tr.Next()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
err = nil
|
|
} else {
|
|
if apiErr, ok := err.(*docker.Error); ok && apiErr.Status == 404 {
|
|
klog.V(4).Infof("path %s did not exist in container %s: %v", glob, e.Container.ID, err)
|
|
err = nil
|
|
}
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
klog.V(4).Infof("Retrieved first header from %s using glob %s: %#v", from, glob, h)
|
|
|
|
h, err = tr.Next()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
err = nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
klog.V(4).Infof("Retrieved second header from %s using glob %s: %#v", from, glob, h)
|
|
|
|
// take the remainder of the input and discard it
|
|
go func() {
|
|
n, err := io.Copy(ioutil.Discard, reader)
|
|
if n > 0 || err != nil {
|
|
klog.V(6).Infof("Discarded %d bytes from end of from glob check, and got error: %v", n, err)
|
|
}
|
|
}()
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (e *ClientExecutor) Archive(fromFS bool, src, dst string, allowDownload bool, excludes []string) (io.Reader, io.Closer, error) {
|
|
var check DirectoryCheck
|
|
if e.Container != nil {
|
|
check = newDirectoryCheck(e.Client, e.Container.ID)
|
|
}
|
|
if isURL(src) {
|
|
if !allowDownload {
|
|
return nil, nil, fmt.Errorf("source can't be a URL")
|
|
}
|
|
klog.V(5).Infof("Archiving %s -> %s from URL", src, dst)
|
|
return archiveFromURL(src, dst, e.TempDir, check)
|
|
}
|
|
// the input is from the filesystem, use the source as the input
|
|
if fromFS {
|
|
klog.V(5).Infof("Archiving %s %s -> %s from a filesystem location", src, ".", dst)
|
|
return archiveFromDisk(src, ".", dst, allowDownload, excludes, check)
|
|
}
|
|
// if the context is in archive form, read from it without decompressing
|
|
if len(e.ContextArchive) > 0 {
|
|
klog.V(5).Infof("Archiving %s %s -> %s from context archive", e.ContextArchive, src, dst)
|
|
return archiveFromFile(e.ContextArchive, src, dst, excludes, check)
|
|
}
|
|
// if the context is a directory, we only allow relative includes
|
|
klog.V(5).Infof("Archiving %q %q -> %q from disk", e.Directory, src, dst)
|
|
return archiveFromDisk(e.Directory, src, dst, allowDownload, excludes, check)
|
|
}
|
|
|
|
// ContainerVolumeTracker manages tracking archives of specific paths inside a container.
|
|
type ContainerVolumeTracker struct {
|
|
paths map[string]string
|
|
errs []error
|
|
}
|
|
|
|
func NewContainerVolumeTracker() *ContainerVolumeTracker {
|
|
return &ContainerVolumeTracker{
|
|
paths: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// Empty returns true if the tracker is not watching any paths
|
|
func (t *ContainerVolumeTracker) Empty() bool {
|
|
return t == nil || len(t.paths) == 0
|
|
}
|
|
|
|
// Add tracks path unless it already is being tracked.
|
|
func (t *ContainerVolumeTracker) Add(path string) {
|
|
if _, ok := t.paths[path]; !ok {
|
|
t.paths[path] = ""
|
|
}
|
|
}
|
|
|
|
// Release removes any stored snapshots
|
|
func (t *ContainerVolumeTracker) Release() []error {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
for path := range t.paths {
|
|
t.ReleasePath(path)
|
|
}
|
|
return t.errs
|
|
}
|
|
|
|
func (t *ContainerVolumeTracker) ReleasePath(path string) {
|
|
if t == nil {
|
|
return
|
|
}
|
|
if archivePath, ok := t.paths[path]; ok && len(archivePath) > 0 {
|
|
err := os.Remove(archivePath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
t.errs = append(t.errs, err)
|
|
}
|
|
klog.V(5).Infof("Releasing path %s (%v)", path, err)
|
|
t.paths[path] = ""
|
|
}
|
|
}
|
|
|
|
func (t *ContainerVolumeTracker) Invalidate(path string) {
|
|
if t == nil {
|
|
return
|
|
}
|
|
set := imagebuilder.VolumeSet{}
|
|
set.Add(path)
|
|
for path := range t.paths {
|
|
if set.Covers(path) {
|
|
t.ReleasePath(path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save ensures that all paths tracked underneath this container are archived or
|
|
// returns an error.
|
|
func (t *ContainerVolumeTracker) Save(containerID, tempDir string, client *docker.Client) error {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
set := imagebuilder.VolumeSet{}
|
|
for dest := range t.paths {
|
|
set.Add(dest)
|
|
}
|
|
// remove archive paths that are covered by other paths
|
|
for dest := range t.paths {
|
|
if !set.Has(dest) {
|
|
t.ReleasePath(dest)
|
|
delete(t.paths, dest)
|
|
}
|
|
}
|
|
for dest, archivePath := range t.paths {
|
|
if len(archivePath) > 0 {
|
|
continue
|
|
}
|
|
archivePath, err := snapshotPath(dest, containerID, tempDir, client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.paths[dest] = archivePath
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// filterTarPipe transforms a tar file as it is streamed, calling fn on each header in the file.
|
|
// If fn returns false, the file is skipped. If an error occurs it is returned.
|
|
func filterTarPipe(w *tar.Writer, r *tar.Reader, fn func(*tar.Header) bool) error {
|
|
for {
|
|
h, err := r.Next()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fn(h) {
|
|
if err := w.WriteHeader(h); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(w, r); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if _, err := io.Copy(ioutil.Discard, r); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// snapshotPath preserves the contents of path in container containerID as a temporary
|
|
// archive, returning either an error or the path of the archived file.
|
|
func snapshotPath(path, containerID, tempDir string, client *docker.Client) (string, error) {
|
|
f, err := ioutil.TempFile(tempDir, "archived-path")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
klog.V(4).Infof("Snapshot %s for later use under %s", path, f.Name())
|
|
|
|
r, w := io.Pipe()
|
|
tr := tar.NewReader(r)
|
|
tw := tar.NewWriter(f)
|
|
go func() {
|
|
err := filterTarPipe(tw, tr, func(h *tar.Header) bool {
|
|
if i := strings.Index(h.Name, "/"); i != -1 {
|
|
h.Name = h.Name[i+1:]
|
|
}
|
|
return len(h.Name) > 0
|
|
})
|
|
if err == nil || errors.Is(err, io.EOF) {
|
|
tw.Flush()
|
|
w.Close()
|
|
klog.V(5).Infof("Snapshot rewritten from %s", path)
|
|
return
|
|
}
|
|
klog.V(5).Infof("Snapshot of %s failed: %v", path, err)
|
|
w.CloseWithError(err)
|
|
}()
|
|
|
|
if !strings.HasSuffix(path, "/") {
|
|
path += "/"
|
|
}
|
|
err = client.DownloadFromContainer(containerID, docker.DownloadFromContainerOptions{
|
|
Path: path,
|
|
OutputStream: w,
|
|
})
|
|
f.Close()
|
|
if err != nil {
|
|
os.Remove(f.Name())
|
|
return "", err
|
|
}
|
|
return f.Name(), nil
|
|
}
|
|
|
|
// Restore ensures the paths managed by t exactly match the container. This requires running
|
|
// exec as a user that can delete contents from the container. It will return an error if
|
|
// any client operation fails.
|
|
func (t *ContainerVolumeTracker) Restore(containerID string, client *docker.Client) error {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
for dest, archivePath := range t.paths {
|
|
if len(archivePath) == 0 {
|
|
return fmt.Errorf("path %s does not have an archive and cannot be restored", dest)
|
|
}
|
|
klog.V(4).Infof("Restoring contents of %s from %s", dest, archivePath)
|
|
if !strings.HasSuffix(dest, "/") {
|
|
dest = dest + "/"
|
|
}
|
|
exec, err := client.CreateExec(docker.CreateExecOptions{
|
|
Container: containerID,
|
|
Cmd: []string{"/bin/sh", "-c", "rm -rf $@", "", dest + "*"},
|
|
User: "0",
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to setup clearing preserved path %s: %v", dest, err)
|
|
}
|
|
if err := client.StartExec(exec.ID, docker.StartExecOptions{}); err != nil {
|
|
return fmt.Errorf("unable to clear preserved path %s: %v", dest, err)
|
|
}
|
|
var status *docker.ExecInspect
|
|
for status == nil {
|
|
status, err = client.InspectExec(exec.ID)
|
|
if err != nil {
|
|
break
|
|
}
|
|
if !status.Running {
|
|
break
|
|
}
|
|
status = nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("clearing preserved path %s did not succeed: %v", dest, err)
|
|
}
|
|
if status.ExitCode != 0 {
|
|
return fmt.Errorf("clearing preserved path %s failed with exit code %d", dest, status.ExitCode)
|
|
}
|
|
err = func() error {
|
|
f, err := os.Open(archivePath)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to open archive %s for preserved path %s: %v", archivePath, dest, err)
|
|
}
|
|
defer f.Close()
|
|
if err := client.UploadToContainer(containerID, docker.UploadToContainerOptions{
|
|
InputStream: f,
|
|
Path: dest,
|
|
}); err != nil {
|
|
return fmt.Errorf("unable to upload preserved contents from %s to %s: %v", archivePath, dest, err)
|
|
}
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|