2017-03-28 15:06:13 +08:00
|
|
|
package imagebuildah
|
|
|
|
|
|
|
|
import (
|
2018-08-01 18:31:02 +08:00
|
|
|
"bytes"
|
2018-04-12 22:20:36 +08:00
|
|
|
"context"
|
2017-03-28 15:06:13 +08:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2018-08-01 18:31:02 +08:00
|
|
|
"io/ioutil"
|
2017-03-28 15:06:13 +08:00
|
|
|
"net/http"
|
|
|
|
"os"
|
2018-08-01 18:31:02 +08:00
|
|
|
"os/exec"
|
2017-03-28 15:06:13 +08:00
|
|
|
"path/filepath"
|
2018-04-11 01:35:03 +08:00
|
|
|
"strconv"
|
2017-03-28 15:06:13 +08:00
|
|
|
"strings"
|
2018-06-09 00:55:46 +08:00
|
|
|
"time"
|
2017-03-28 15:06:13 +08:00
|
|
|
|
2018-09-18 03:20:16 +08:00
|
|
|
"github.com/containers/buildah"
|
2018-08-21 03:25:10 +08:00
|
|
|
buildahdocker "github.com/containers/buildah/docker"
|
2018-09-18 03:20:16 +08:00
|
|
|
"github.com/containers/buildah/util"
|
2018-06-29 01:59:42 +08:00
|
|
|
cp "github.com/containers/image/copy"
|
2018-10-12 04:58:04 +08:00
|
|
|
"github.com/containers/image/docker/reference"
|
2017-03-28 15:06:13 +08:00
|
|
|
is "github.com/containers/image/storage"
|
|
|
|
"github.com/containers/image/transports"
|
|
|
|
"github.com/containers/image/transports/alltransports"
|
|
|
|
"github.com/containers/image/types"
|
2017-05-17 23:53:28 +08:00
|
|
|
"github.com/containers/storage"
|
2017-03-28 15:06:13 +08:00
|
|
|
"github.com/containers/storage/pkg/archive"
|
|
|
|
"github.com/containers/storage/pkg/stringid"
|
|
|
|
"github.com/docker/docker/builder/dockerfile/parser"
|
|
|
|
docker "github.com/fsouza/go-dockerclient"
|
2018-06-09 00:55:46 +08:00
|
|
|
"github.com/opencontainers/image-spec/specs-go/v1"
|
2017-03-28 15:06:13 +08:00
|
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
|
|
|
"github.com/openshift/imagebuilder"
|
2017-06-02 03:23:02 +08:00
|
|
|
"github.com/pkg/errors"
|
2017-10-10 03:05:56 +08:00
|
|
|
"github.com/sirupsen/logrus"
|
2017-03-28 15:06:13 +08:00
|
|
|
)
|
|
|
|
|
2017-04-11 02:15:30 +08:00
|
|
|
const (
|
2018-08-24 00:55:16 +08:00
|
|
|
PullIfMissing = buildah.PullIfMissing
|
|
|
|
PullAlways = buildah.PullAlways
|
|
|
|
PullNever = buildah.PullNever
|
2017-04-11 22:27:05 +08:00
|
|
|
|
|
|
|
Gzip = archive.Gzip
|
|
|
|
Bzip2 = archive.Bzip2
|
|
|
|
Xz = archive.Xz
|
|
|
|
Uncompressed = archive.Uncompressed
|
2017-04-11 02:15:30 +08:00
|
|
|
)
|
|
|
|
|
2017-04-11 22:27:05 +08:00
|
|
|
// Mount is a mountpoint for the build container.
|
|
|
|
type Mount specs.Mount
|
|
|
|
|
2017-03-28 15:06:13 +08:00
|
|
|
// BuildOptions can be used to alter how an image is built.
|
|
|
|
type BuildOptions struct {
|
|
|
|
// ContextDirectory is the default source location for COPY and ADD
|
|
|
|
// commands.
|
|
|
|
ContextDirectory string
|
2017-04-11 02:15:30 +08:00
|
|
|
// PullPolicy controls whether or not we pull images. It should be one
|
|
|
|
// of PullIfMissing, PullAlways, or PullNever.
|
2018-03-29 02:14:57 +08:00
|
|
|
PullPolicy buildah.PullPolicy
|
2017-03-28 15:06:13 +08:00
|
|
|
// Registry is a value which is prepended to the image's name, if it
|
|
|
|
// needs to be pulled and the image name alone can not be resolved to a
|
2017-07-31 22:44:21 +08:00
|
|
|
// reference to a source image. No separator is implicitly added.
|
2017-03-28 15:06:13 +08:00
|
|
|
Registry string
|
2017-07-31 22:44:21 +08:00
|
|
|
// Transport is a value which is prepended to the image's name, if it
|
|
|
|
// needs to be pulled and the image name alone, or the image name and
|
|
|
|
// the registry together, can not be resolved to a reference to a
|
|
|
|
// source image. No separator is implicitly added.
|
|
|
|
Transport string
|
2017-03-28 15:06:13 +08:00
|
|
|
// IgnoreUnrecognizedInstructions tells us to just log instructions we
|
|
|
|
// don't recognize, and try to keep going.
|
|
|
|
IgnoreUnrecognizedInstructions bool
|
|
|
|
// Quiet tells us whether or not to announce steps as we go through them.
|
|
|
|
Quiet bool
|
2018-05-12 01:00:14 +08:00
|
|
|
// Isolation controls how Run() runs things.
|
|
|
|
Isolation buildah.Isolation
|
|
|
|
// Runtime is the name of the command to run for RUN instructions when
|
|
|
|
// Isolation is either IsolationDefault or IsolationOCI. It should
|
|
|
|
// accept the same arguments and flags that runc does.
|
2017-03-28 15:06:13 +08:00
|
|
|
Runtime string
|
|
|
|
// RuntimeArgs adds global arguments for the runtime.
|
|
|
|
RuntimeArgs []string
|
|
|
|
// TransientMounts is a list of mounts that won't be kept in the image.
|
2017-04-11 22:27:05 +08:00
|
|
|
TransientMounts []Mount
|
2017-03-28 15:06:13 +08:00
|
|
|
// Compression specifies the type of compression which is applied to
|
|
|
|
// layer blobs. The default is to not use compression, but
|
|
|
|
// archive.Gzip is recommended.
|
|
|
|
Compression archive.Compression
|
|
|
|
// Arguments which can be interpolated into Dockerfiles
|
|
|
|
Args map[string]string
|
|
|
|
// Name of the image to write to.
|
|
|
|
Output string
|
2017-04-11 02:25:07 +08:00
|
|
|
// Additional tags to add to the image that we write, if we know of a
|
|
|
|
// way to add them.
|
|
|
|
AdditionalTags []string
|
2017-04-11 02:17:15 +08:00
|
|
|
// Log is a callback that will print a progress message. If no value
|
|
|
|
// is supplied, the message will be sent to Err (or os.Stderr, if Err
|
|
|
|
// is nil) by default.
|
|
|
|
Log func(format string, args ...interface{})
|
2018-08-10 00:44:30 +08:00
|
|
|
// In is connected to stdin for RUN instructions.
|
|
|
|
In io.Reader
|
2017-04-11 02:17:15 +08:00
|
|
|
// Out is a place where non-error log messages are sent.
|
|
|
|
Out io.Writer
|
|
|
|
// Err is a place where error log messages should be sent.
|
|
|
|
Err io.Writer
|
2017-03-28 15:06:13 +08:00
|
|
|
// SignaturePolicyPath specifies an override location for the signature
|
|
|
|
// policy which should be used for verifying the new image as it is
|
|
|
|
// being written. Except in specific circumstances, no value should be
|
|
|
|
// specified, indicating that the shared, system-wide default policy
|
|
|
|
// should be used.
|
|
|
|
SignaturePolicyPath string
|
2017-05-09 23:56:44 +08:00
|
|
|
// ReportWriter is an io.Writer which will be used to report the
|
|
|
|
// progress of the (possible) pulling of the source image and the
|
|
|
|
// writing of the new image.
|
|
|
|
ReportWriter io.Writer
|
2017-05-18 05:02:40 +08:00
|
|
|
// OutputFormat is the format of the output image's manifest and
|
|
|
|
// configuration data.
|
2018-08-24 00:55:16 +08:00
|
|
|
// Accepted values are buildah.OCIv1ImageManifest and buildah.Dockerv2ImageManifest.
|
2017-05-18 05:02:40 +08:00
|
|
|
OutputFormat string
|
2018-01-28 07:17:05 +08:00
|
|
|
// SystemContext holds parameters used for authentication.
|
2018-03-13 01:53:12 +08:00
|
|
|
SystemContext *types.SystemContext
|
|
|
|
// NamespaceOptions controls how we set up namespaces processes that we
|
|
|
|
// might need when handling RUN instructions.
|
|
|
|
NamespaceOptions []buildah.NamespaceOption
|
2018-04-14 06:20:25 +08:00
|
|
|
// ConfigureNetwork controls whether or not network interfaces and
|
|
|
|
// routing are configured for a new network namespace (i.e., when not
|
|
|
|
// joining another's namespace and not just using the host's
|
|
|
|
// namespace), effectively deciding whether or not the process has a
|
|
|
|
// usable network.
|
|
|
|
ConfigureNetwork buildah.NetworkConfigurationPolicy
|
|
|
|
// CNIPluginPath is the location of CNI plugin helpers, if they should be
|
|
|
|
// run from a location other than the default location.
|
|
|
|
CNIPluginPath string
|
|
|
|
// CNIConfigDir is the location of CNI configuration files, if the files in
|
|
|
|
// the default configuration directory shouldn't be used.
|
|
|
|
CNIConfigDir string
|
2018-03-13 01:53:12 +08:00
|
|
|
// ID mapping options to use if we're setting up our own user namespace
|
|
|
|
// when handling RUN instructions.
|
|
|
|
IDMappingOptions *buildah.IDMappingOptions
|
2018-06-05 05:36:26 +08:00
|
|
|
// AddCapabilities is a list of capabilities to add to the default set when
|
|
|
|
// handling RUN instructions.
|
|
|
|
AddCapabilities []string
|
|
|
|
// DropCapabilities is a list of capabilities to remove from the default set
|
|
|
|
// when handling RUN instructions. If a capability appears in both lists, it
|
|
|
|
// will be dropped.
|
|
|
|
DropCapabilities []string
|
2018-03-13 01:53:12 +08:00
|
|
|
CommonBuildOpts *buildah.CommonBuildOptions
|
2018-02-24 01:38:39 +08:00
|
|
|
// DefaultMountsFilePath is the file path holding the mounts to be mounted in "host-path:container-path" format
|
|
|
|
DefaultMountsFilePath string
|
2018-04-25 22:00:46 +08:00
|
|
|
// IIDFile tells the builder to write the image ID to the specified file
|
|
|
|
IIDFile string
|
2018-05-22 05:02:50 +08:00
|
|
|
// Squash tells the builder to produce an image with a single layer
|
|
|
|
// instead of with possibly more than one layer.
|
|
|
|
Squash bool
|
2018-05-17 02:13:12 +08:00
|
|
|
// Labels metadata for an image
|
|
|
|
Labels []string
|
2018-05-23 00:05:18 +08:00
|
|
|
// Annotation metadata for an image
|
|
|
|
Annotations []string
|
2018-05-25 15:53:30 +08:00
|
|
|
// OnBuild commands to be run by images based on this image
|
|
|
|
OnBuild []string
|
2018-06-09 00:55:46 +08:00
|
|
|
// Layers tells the builder to create a cache of images for each step in the Dockerfile
|
|
|
|
Layers bool
|
|
|
|
// NoCache tells the builder to build the image from scratch without checking for a cache.
|
|
|
|
// It creates a new set of cached images for the build.
|
|
|
|
NoCache bool
|
2018-06-14 02:09:45 +08:00
|
|
|
// RemoveIntermediateCtrs tells the builder whether to remove intermediate containers used
|
|
|
|
// during the build process. Default is true.
|
|
|
|
RemoveIntermediateCtrs bool
|
|
|
|
// ForceRmIntermediateCtrs tells the builder to remove all intermediate containers even if
|
|
|
|
// the build was unsuccessful.
|
|
|
|
ForceRmIntermediateCtrs bool
|
2018-10-18 06:06:16 +08:00
|
|
|
// BlobDirectory is a directory which we'll use for caching layer blobs.
|
|
|
|
BlobDirectory string
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Executor is a buildah-based implementation of the imagebuilder.Executor
|
|
|
|
// interface.
|
|
|
|
type Executor struct {
|
2018-04-11 01:35:03 +08:00
|
|
|
index int
|
2018-02-24 18:09:25 +08:00
|
|
|
name string
|
|
|
|
named map[string]*Executor
|
2017-03-28 15:06:13 +08:00
|
|
|
store storage.Store
|
|
|
|
contextDir string
|
|
|
|
builder *buildah.Builder
|
2018-03-29 02:14:57 +08:00
|
|
|
pullPolicy buildah.PullPolicy
|
2017-03-28 15:06:13 +08:00
|
|
|
registry string
|
2017-07-31 22:44:21 +08:00
|
|
|
transport string
|
2017-03-28 15:06:13 +08:00
|
|
|
ignoreUnrecognizedInstructions bool
|
|
|
|
quiet bool
|
|
|
|
runtime string
|
|
|
|
runtimeArgs []string
|
2017-04-11 22:27:05 +08:00
|
|
|
transientMounts []Mount
|
2017-03-28 15:06:13 +08:00
|
|
|
compression archive.Compression
|
|
|
|
output string
|
2017-05-30 23:51:40 +08:00
|
|
|
outputFormat string
|
2017-04-11 02:25:07 +08:00
|
|
|
additionalTags []string
|
2017-04-11 02:17:15 +08:00
|
|
|
log func(format string, args ...interface{})
|
2018-08-10 00:44:30 +08:00
|
|
|
in io.Reader
|
2017-04-11 02:17:15 +08:00
|
|
|
out io.Writer
|
|
|
|
err io.Writer
|
2017-03-28 15:06:13 +08:00
|
|
|
signaturePolicyPath string
|
|
|
|
systemContext *types.SystemContext
|
|
|
|
mountPoint string
|
|
|
|
preserved int
|
|
|
|
volumes imagebuilder.VolumeSet
|
|
|
|
volumeCache map[string]string
|
|
|
|
volumeCacheInfo map[string]os.FileInfo
|
2017-05-09 23:56:44 +08:00
|
|
|
reportWriter io.Writer
|
2018-05-12 01:00:14 +08:00
|
|
|
isolation buildah.Isolation
|
2018-03-13 01:53:12 +08:00
|
|
|
namespaceOptions []buildah.NamespaceOption
|
2018-04-14 06:20:25 +08:00
|
|
|
configureNetwork buildah.NetworkConfigurationPolicy
|
|
|
|
cniPluginPath string
|
|
|
|
cniConfigDir string
|
2018-03-13 01:53:12 +08:00
|
|
|
idmappingOptions *buildah.IDMappingOptions
|
2018-02-14 03:58:56 +08:00
|
|
|
commonBuildOptions *buildah.CommonBuildOptions
|
2018-02-24 01:38:39 +08:00
|
|
|
defaultMountsFilePath string
|
2018-04-25 22:00:46 +08:00
|
|
|
iidfile string
|
2018-05-22 05:02:50 +08:00
|
|
|
squash bool
|
2018-05-17 02:13:12 +08:00
|
|
|
labels []string
|
2018-05-23 00:05:18 +08:00
|
|
|
annotations []string
|
2018-05-25 15:53:30 +08:00
|
|
|
onbuild []string
|
2018-06-09 00:55:46 +08:00
|
|
|
layers bool
|
|
|
|
topLayers []string
|
|
|
|
noCache bool
|
2018-06-14 02:09:45 +08:00
|
|
|
removeIntermediateCtrs bool
|
|
|
|
forceRmIntermediateCtrs bool
|
2018-09-22 07:37:02 +08:00
|
|
|
containerIDs []string // Stores the IDs of the successful intermediate containers used during layer build
|
|
|
|
imageMap map[string]string // Used to map images that we create to handle the AS construct.
|
2018-11-19 04:10:26 +08:00
|
|
|
copyFrom string // Used to keep track of the --from flag from COPY and ADD
|
2018-10-18 06:06:16 +08:00
|
|
|
blobDirectory string
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
|
2018-11-29 14:31:20 +08:00
|
|
|
// builtinAllowedBuildArgs is list of built-in allowed build args
|
|
|
|
var builtinAllowedBuildArgs = map[string]bool{
|
|
|
|
"HTTP_PROXY": true,
|
|
|
|
"http_proxy": true,
|
|
|
|
"HTTPS_PROXY": true,
|
|
|
|
"https_proxy": true,
|
|
|
|
"FTP_PROXY": true,
|
|
|
|
"ftp_proxy": true,
|
|
|
|
"NO_PROXY": true,
|
|
|
|
"no_proxy": true,
|
|
|
|
}
|
|
|
|
|
2018-02-24 18:09:25 +08:00
|
|
|
// withName creates a new child executor that will be used whenever a COPY statement uses --from=NAME.
|
2018-04-11 01:35:03 +08:00
|
|
|
func (b *Executor) withName(name string, index int) *Executor {
|
2018-02-24 18:09:25 +08:00
|
|
|
if b.named == nil {
|
|
|
|
b.named = make(map[string]*Executor)
|
|
|
|
}
|
|
|
|
copied := *b
|
2018-04-11 01:35:03 +08:00
|
|
|
copied.index = index
|
2018-02-24 18:09:25 +08:00
|
|
|
copied.name = name
|
|
|
|
child := &copied
|
|
|
|
b.named[name] = child
|
2018-04-11 01:35:03 +08:00
|
|
|
if idx := strconv.Itoa(index); idx != name {
|
|
|
|
b.named[idx] = child
|
|
|
|
}
|
2018-02-24 18:09:25 +08:00
|
|
|
return child
|
|
|
|
}
|
|
|
|
|
2017-03-28 15:06:13 +08:00
|
|
|
// Preserve informs the executor that from this point on, it needs to ensure
|
|
|
|
// that only COPY and ADD instructions can modify the contents of this
|
|
|
|
// directory or anything below it.
|
|
|
|
// The Executor handles this by caching the contents of directories which have
|
|
|
|
// been marked this way before executing a RUN instruction, invalidating that
|
|
|
|
// cache when an ADD or COPY instruction sets any location under the directory
|
|
|
|
// as the destination, and using the cache to reset the contents of the
|
|
|
|
// directory tree after processing each RUN instruction.
|
|
|
|
// It would be simpler if we could just mark the directory as a read-only bind
|
|
|
|
// mount of itself during Run(), but the directory is expected to be remain
|
|
|
|
// writeable, even if any changes within it are ultimately discarded.
|
|
|
|
func (b *Executor) Preserve(path string) error {
|
|
|
|
logrus.Debugf("PRESERVE %q", path)
|
|
|
|
if b.volumes.Covers(path) {
|
|
|
|
// This path is already a subdirectory of a volume path that
|
2017-06-21 05:37:50 +08:00
|
|
|
// we're already preserving, so there's nothing new to be done
|
|
|
|
// except ensure that it exists.
|
|
|
|
archivedPath := filepath.Join(b.mountPoint, path)
|
|
|
|
if err := os.MkdirAll(archivedPath, 0755); err != nil {
|
|
|
|
return errors.Wrapf(err, "error ensuring volume path %q exists", archivedPath)
|
|
|
|
}
|
|
|
|
if err := b.volumeCacheInvalidate(path); err != nil {
|
|
|
|
return errors.Wrapf(err, "error ensuring volume path %q is preserved", archivedPath)
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// Figure out where the cache for this volume would be stored.
|
|
|
|
b.preserved++
|
2017-05-17 23:53:28 +08:00
|
|
|
cacheDir, err := b.store.ContainerDirectory(b.builder.ContainerID)
|
2017-03-28 15:06:13 +08:00
|
|
|
if err != nil {
|
2017-06-03 00:17:27 +08:00
|
|
|
return errors.Errorf("unable to locate temporary directory for container")
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
cacheFile := filepath.Join(cacheDir, fmt.Sprintf("volume%d.tar", b.preserved))
|
|
|
|
// Save info about the top level of the location that we'll be archiving.
|
|
|
|
archivedPath := filepath.Join(b.mountPoint, path)
|
2018-03-28 02:06:00 +08:00
|
|
|
|
|
|
|
// Try and resolve the symlink (if one exists)
|
|
|
|
// Set archivedPath and path based on whether a symlink is found or not
|
2018-11-07 16:48:30 +08:00
|
|
|
if symLink, err := ResolveSymLink(b.mountPoint, path); err == nil {
|
2018-03-28 02:06:00 +08:00
|
|
|
archivedPath = filepath.Join(b.mountPoint, symLink)
|
|
|
|
path = symLink
|
|
|
|
} else {
|
|
|
|
return errors.Wrapf(err, "error reading symbolic link to %q", path)
|
|
|
|
}
|
|
|
|
|
2017-03-28 15:06:13 +08:00
|
|
|
st, err := os.Stat(archivedPath)
|
2017-06-21 05:37:50 +08:00
|
|
|
if os.IsNotExist(err) {
|
|
|
|
if err = os.MkdirAll(archivedPath, 0755); err != nil {
|
|
|
|
return errors.Wrapf(err, "error ensuring volume path %q exists", archivedPath)
|
|
|
|
}
|
|
|
|
st, err = os.Stat(archivedPath)
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
if err != nil {
|
|
|
|
logrus.Debugf("error reading info about %q: %v", archivedPath, err)
|
2017-06-21 05:37:50 +08:00
|
|
|
return errors.Wrapf(err, "error reading info about volume path %q", archivedPath)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
b.volumeCacheInfo[path] = st
|
|
|
|
if !b.volumes.Add(path) {
|
|
|
|
// This path is not a subdirectory of a volume path that we're
|
|
|
|
// already preserving, so adding it to the list should work.
|
2017-06-03 00:17:27 +08:00
|
|
|
return errors.Errorf("error adding %q to the volume cache", path)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
b.volumeCache[path] = cacheFile
|
|
|
|
// Now prune cache files for volumes that are now supplanted by this one.
|
|
|
|
removed := []string{}
|
|
|
|
for cachedPath := range b.volumeCache {
|
|
|
|
// Walk our list of cached volumes, and check that they're
|
|
|
|
// still in the list of locations that we need to cache.
|
|
|
|
found := false
|
|
|
|
for _, volume := range b.volumes {
|
|
|
|
if volume == cachedPath {
|
|
|
|
// We need to keep this volume's cache.
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !found {
|
|
|
|
// We don't need to keep this volume's cache. Make a
|
|
|
|
// note to remove it.
|
|
|
|
removed = append(removed, cachedPath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Actually remove the caches that we decided to remove.
|
|
|
|
for _, cachedPath := range removed {
|
|
|
|
archivedPath := filepath.Join(b.mountPoint, cachedPath)
|
|
|
|
logrus.Debugf("no longer need cache of %q in %q", archivedPath, b.volumeCache[cachedPath])
|
|
|
|
if err := os.Remove(b.volumeCache[cachedPath]); err != nil {
|
2018-09-28 11:53:00 +08:00
|
|
|
if os.IsNotExist(err) {
|
|
|
|
continue
|
|
|
|
}
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error removing %q", b.volumeCache[cachedPath])
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
delete(b.volumeCache, cachedPath)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove any volume cache item which will need to be re-saved because we're
|
|
|
|
// writing to part of it.
|
|
|
|
func (b *Executor) volumeCacheInvalidate(path string) error {
|
|
|
|
invalidated := []string{}
|
|
|
|
for cachedPath := range b.volumeCache {
|
|
|
|
if strings.HasPrefix(path, cachedPath+string(os.PathSeparator)) {
|
|
|
|
invalidated = append(invalidated, cachedPath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, cachedPath := range invalidated {
|
|
|
|
if err := os.Remove(b.volumeCache[cachedPath]); err != nil {
|
2018-09-28 11:53:00 +08:00
|
|
|
if os.IsNotExist(err) {
|
|
|
|
continue
|
|
|
|
}
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error removing volume cache %q", b.volumeCache[cachedPath])
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
archivedPath := filepath.Join(b.mountPoint, cachedPath)
|
|
|
|
logrus.Debugf("invalidated volume cache for %q from %q", archivedPath, b.volumeCache[cachedPath])
|
|
|
|
delete(b.volumeCache, cachedPath)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save the contents of each of the executor's list of volumes for which we
|
|
|
|
// don't already have a cache file.
|
|
|
|
func (b *Executor) volumeCacheSave() error {
|
|
|
|
for cachedPath, cacheFile := range b.volumeCache {
|
|
|
|
archivedPath := filepath.Join(b.mountPoint, cachedPath)
|
|
|
|
_, err := os.Stat(cacheFile)
|
|
|
|
if err == nil {
|
|
|
|
logrus.Debugf("contents of volume %q are already cached in %q", archivedPath, cacheFile)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if !os.IsNotExist(err) {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error checking for cache of %q in %q", archivedPath, cacheFile)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2017-06-21 05:37:50 +08:00
|
|
|
if err := os.MkdirAll(archivedPath, 0755); err != nil {
|
|
|
|
return errors.Wrapf(err, "error ensuring volume path %q exists", archivedPath)
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
logrus.Debugf("caching contents of volume %q in %q", archivedPath, cacheFile)
|
|
|
|
cache, err := os.Create(cacheFile)
|
|
|
|
if err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error creating archive at %q", cacheFile)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
defer cache.Close()
|
|
|
|
rc, err := archive.Tar(archivedPath, archive.Uncompressed)
|
|
|
|
if err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error archiving %q", archivedPath)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
defer rc.Close()
|
|
|
|
_, err = io.Copy(cache, rc)
|
|
|
|
if err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error archiving %q to %q", archivedPath, cacheFile)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Restore the contents of each of the executor's list of volumes.
|
|
|
|
func (b *Executor) volumeCacheRestore() error {
|
|
|
|
for cachedPath, cacheFile := range b.volumeCache {
|
|
|
|
archivedPath := filepath.Join(b.mountPoint, cachedPath)
|
|
|
|
logrus.Debugf("restoring contents of volume %q from %q", archivedPath, cacheFile)
|
|
|
|
cache, err := os.Open(cacheFile)
|
|
|
|
if err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error opening archive at %q", cacheFile)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
defer cache.Close()
|
|
|
|
if err := os.RemoveAll(archivedPath); err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error clearing volume path %q", archivedPath)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2017-06-21 05:37:50 +08:00
|
|
|
if err := os.MkdirAll(archivedPath, 0755); err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error recreating volume path %q", archivedPath)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
err = archive.Untar(cache, archivedPath, nil)
|
|
|
|
if err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error extracting archive at %q", archivedPath)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
if st, ok := b.volumeCacheInfo[cachedPath]; ok {
|
|
|
|
if err := os.Chmod(archivedPath, st.Mode()); err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error restoring permissions on %q", archivedPath)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
if err := os.Chown(archivedPath, 0, 0); err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error setting ownership on %q", archivedPath)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
if err := os.Chtimes(archivedPath, st.ModTime(), st.ModTime()); err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error restoring datestamps on %q", archivedPath)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy copies data into the working tree. The "Download" field is how
|
|
|
|
// imagebuilder tells us the instruction was "ADD" and not "COPY".
|
|
|
|
func (b *Executor) Copy(excludes []string, copies ...imagebuilder.Copy) error {
|
|
|
|
for _, copy := range copies {
|
|
|
|
logrus.Debugf("COPY %#v, %#v", excludes, copy)
|
|
|
|
if err := b.volumeCacheInvalidate(copy.Dest); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
sources := []string{}
|
|
|
|
for _, src := range copy.Src {
|
|
|
|
if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
|
|
|
|
sources = append(sources, src)
|
2018-02-24 18:09:25 +08:00
|
|
|
} else if len(copy.From) > 0 {
|
2018-04-11 01:35:03 +08:00
|
|
|
if other, ok := b.named[copy.From]; ok && other.index < b.index {
|
2018-02-24 18:09:25 +08:00
|
|
|
sources = append(sources, filepath.Join(other.mountPoint, src))
|
|
|
|
} else {
|
|
|
|
return errors.Errorf("the stage %q has not been built", copy.From)
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
} else {
|
|
|
|
sources = append(sources, filepath.Join(b.contextDir, src))
|
|
|
|
}
|
|
|
|
}
|
2018-08-13 22:18:04 +08:00
|
|
|
|
|
|
|
options := buildah.AddAndCopyOptions{
|
|
|
|
Chown: copy.Chown,
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := b.builder.Add(copy.Dest, copy.Download, options, sources...); err != nil {
|
2017-03-28 15:06:13 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-04-11 22:27:05 +08:00
|
|
|
func convertMounts(mounts []Mount) []specs.Mount {
|
|
|
|
specmounts := []specs.Mount{}
|
|
|
|
for _, m := range mounts {
|
|
|
|
s := specs.Mount{
|
|
|
|
Destination: m.Destination,
|
|
|
|
Type: m.Type,
|
|
|
|
Source: m.Source,
|
|
|
|
Options: m.Options,
|
|
|
|
}
|
|
|
|
specmounts = append(specmounts, s)
|
|
|
|
}
|
|
|
|
return specmounts
|
|
|
|
}
|
|
|
|
|
2017-03-28 15:06:13 +08:00
|
|
|
// Run executes a RUN instruction using the working container as a root
|
|
|
|
// directory.
|
|
|
|
func (b *Executor) Run(run imagebuilder.Run, config docker.Config) error {
|
|
|
|
logrus.Debugf("RUN %#v, %#v", run, config)
|
|
|
|
if b.builder == nil {
|
2017-06-03 00:17:27 +08:00
|
|
|
return errors.Errorf("no build container available")
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-08-10 00:44:30 +08:00
|
|
|
stdin := b.in
|
|
|
|
if stdin == nil {
|
|
|
|
devNull, err := os.Open(os.DevNull)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Errorf("error opening %q for reading: %v", os.DevNull, err)
|
|
|
|
}
|
|
|
|
defer devNull.Close()
|
|
|
|
stdin = devNull
|
2018-06-06 01:53:39 +08:00
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
options := buildah.RunOptions{
|
2018-04-14 06:20:25 +08:00
|
|
|
Hostname: config.Hostname,
|
|
|
|
Runtime: b.runtime,
|
|
|
|
Args: b.runtimeArgs,
|
|
|
|
Mounts: convertMounts(b.transientMounts),
|
|
|
|
Env: config.Env,
|
|
|
|
User: config.User,
|
|
|
|
WorkingDir: config.WorkingDir,
|
|
|
|
Entrypoint: config.Entrypoint,
|
|
|
|
Cmd: config.Cmd,
|
2018-08-10 00:44:30 +08:00
|
|
|
Stdin: stdin,
|
2018-06-08 02:41:54 +08:00
|
|
|
Stdout: b.out,
|
|
|
|
Stderr: b.err,
|
2018-04-14 06:20:25 +08:00
|
|
|
Quiet: b.quiet,
|
|
|
|
}
|
|
|
|
if config.NetworkDisabled {
|
|
|
|
options.ConfigureNetwork = buildah.NetworkDisabled
|
|
|
|
} else {
|
|
|
|
options.ConfigureNetwork = buildah.NetworkEnabled
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
args := run.Args
|
|
|
|
if run.Shell {
|
|
|
|
args = append([]string{"/bin/sh", "-c"}, args...)
|
|
|
|
}
|
|
|
|
if err := b.volumeCacheSave(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-08-10 00:44:30 +08:00
|
|
|
err := b.builder.Run(args, options)
|
2017-03-28 15:06:13 +08:00
|
|
|
if err2 := b.volumeCacheRestore(); err2 != nil {
|
|
|
|
if err == nil {
|
|
|
|
return err2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnrecognizedInstruction is called when we encounter an instruction that the
|
|
|
|
// imagebuilder parser didn't understand.
|
|
|
|
func (b *Executor) UnrecognizedInstruction(step *imagebuilder.Step) error {
|
2018-04-12 22:20:36 +08:00
|
|
|
errStr := fmt.Sprintf("Build error: Unknown instruction: %q ", step.Command)
|
|
|
|
err := fmt.Sprintf(errStr+"%#v", step)
|
2018-02-07 01:02:21 +08:00
|
|
|
if b.ignoreUnrecognizedInstructions {
|
2018-02-09 05:10:18 +08:00
|
|
|
logrus.Debugf(err)
|
2017-03-28 15:06:13 +08:00
|
|
|
return nil
|
|
|
|
}
|
2018-02-09 05:10:18 +08:00
|
|
|
|
|
|
|
switch logrus.GetLevel() {
|
|
|
|
case logrus.ErrorLevel:
|
2018-04-12 22:20:36 +08:00
|
|
|
logrus.Errorf(errStr)
|
2018-02-09 05:10:18 +08:00
|
|
|
case logrus.DebugLevel:
|
|
|
|
logrus.Debugf(err)
|
|
|
|
default:
|
|
|
|
logrus.Errorf("+(UNHANDLED LOGLEVEL) %#v", step)
|
|
|
|
}
|
|
|
|
|
|
|
|
return errors.Errorf(err)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewExecutor creates a new instance of the imagebuilder.Executor interface.
|
|
|
|
func NewExecutor(store storage.Store, options BuildOptions) (*Executor, error) {
|
|
|
|
exec := Executor{
|
|
|
|
store: store,
|
|
|
|
contextDir: options.ContextDirectory,
|
2017-04-11 02:15:30 +08:00
|
|
|
pullPolicy: options.PullPolicy,
|
2017-03-28 15:06:13 +08:00
|
|
|
registry: options.Registry,
|
2017-07-31 22:44:21 +08:00
|
|
|
transport: options.Transport,
|
2017-03-28 15:06:13 +08:00
|
|
|
ignoreUnrecognizedInstructions: options.IgnoreUnrecognizedInstructions,
|
2018-10-18 07:20:37 +08:00
|
|
|
quiet: options.Quiet,
|
|
|
|
runtime: options.Runtime,
|
|
|
|
runtimeArgs: options.RuntimeArgs,
|
|
|
|
transientMounts: options.TransientMounts,
|
|
|
|
compression: options.Compression,
|
|
|
|
output: options.Output,
|
|
|
|
outputFormat: options.OutputFormat,
|
|
|
|
additionalTags: options.AdditionalTags,
|
|
|
|
signaturePolicyPath: options.SignaturePolicyPath,
|
|
|
|
systemContext: options.SystemContext,
|
|
|
|
volumeCache: make(map[string]string),
|
|
|
|
volumeCacheInfo: make(map[string]os.FileInfo),
|
|
|
|
log: options.Log,
|
|
|
|
in: options.In,
|
|
|
|
out: options.Out,
|
|
|
|
err: options.Err,
|
|
|
|
reportWriter: options.ReportWriter,
|
|
|
|
isolation: options.Isolation,
|
|
|
|
namespaceOptions: options.NamespaceOptions,
|
|
|
|
configureNetwork: options.ConfigureNetwork,
|
|
|
|
cniPluginPath: options.CNIPluginPath,
|
|
|
|
cniConfigDir: options.CNIConfigDir,
|
|
|
|
idmappingOptions: options.IDMappingOptions,
|
|
|
|
commonBuildOptions: options.CommonBuildOpts,
|
|
|
|
defaultMountsFilePath: options.DefaultMountsFilePath,
|
|
|
|
iidfile: options.IIDFile,
|
|
|
|
squash: options.Squash,
|
|
|
|
labels: append([]string{}, options.Labels...),
|
|
|
|
annotations: append([]string{}, options.Annotations...),
|
|
|
|
layers: options.Layers,
|
|
|
|
noCache: options.NoCache,
|
|
|
|
removeIntermediateCtrs: options.RemoveIntermediateCtrs,
|
|
|
|
forceRmIntermediateCtrs: options.ForceRmIntermediateCtrs,
|
2018-10-18 06:06:16 +08:00
|
|
|
blobDirectory: options.BlobDirectory,
|
2017-04-11 02:17:15 +08:00
|
|
|
}
|
|
|
|
if exec.err == nil {
|
|
|
|
exec.err = os.Stderr
|
|
|
|
}
|
|
|
|
if exec.out == nil {
|
|
|
|
exec.out = os.Stdout
|
|
|
|
}
|
|
|
|
if exec.log == nil {
|
|
|
|
stepCounter := 0
|
|
|
|
exec.log = func(format string, args ...interface{}) {
|
|
|
|
stepCounter++
|
|
|
|
prefix := fmt.Sprintf("STEP %d: ", stepCounter)
|
|
|
|
suffix := "\n"
|
|
|
|
fmt.Fprintf(exec.err, prefix+format+suffix, args...)
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
return &exec, nil
|
|
|
|
}
|
|
|
|
|
2017-04-12 06:18:35 +08:00
|
|
|
// Prepare creates a working container based on specified image, or if one
|
|
|
|
// isn't specified, the first FROM instruction we can find in the parsed tree.
|
2018-09-22 07:37:02 +08:00
|
|
|
func (b *Executor) Prepare(ctx context.Context, stage imagebuilder.Stage, from string) error {
|
|
|
|
ib := stage.Builder
|
|
|
|
node := stage.Node
|
|
|
|
|
2017-04-12 06:18:35 +08:00
|
|
|
if from == "" {
|
|
|
|
base, err := ib.From(node)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Debugf("Prepare(node.Children=%#v)", node.Children)
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error determining starting point for build")
|
2017-04-12 06:18:35 +08:00
|
|
|
}
|
|
|
|
from = base
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-09-22 07:37:02 +08:00
|
|
|
displayFrom := from
|
|
|
|
// stage.Name will be a string of integers for all stages without an "AS" clause
|
|
|
|
asImageName := stage.Name
|
|
|
|
if asImageName != "" {
|
|
|
|
if _, err := strconv.Atoi(asImageName); err != nil {
|
|
|
|
displayFrom = from + " AS " + asImageName
|
|
|
|
} else {
|
|
|
|
asImageName = ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
logrus.Debugf("FROM %#v", displayFrom)
|
2017-03-28 15:06:13 +08:00
|
|
|
if !b.quiet {
|
2018-09-22 07:37:02 +08:00
|
|
|
b.log("FROM %s", displayFrom)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-09-22 07:37:02 +08:00
|
|
|
|
2017-03-28 15:06:13 +08:00
|
|
|
builderOptions := buildah.BuilderOptions{
|
2018-06-25 20:53:47 +08:00
|
|
|
Args: ib.Args,
|
2018-02-24 01:38:39 +08:00
|
|
|
FromImage: from,
|
|
|
|
PullPolicy: b.pullPolicy,
|
|
|
|
Registry: b.registry,
|
|
|
|
Transport: b.transport,
|
2018-10-18 06:06:16 +08:00
|
|
|
PullBlobDirectory: b.blobDirectory,
|
2018-02-24 01:38:39 +08:00
|
|
|
SignaturePolicyPath: b.signaturePolicyPath,
|
|
|
|
ReportWriter: b.reportWriter,
|
|
|
|
SystemContext: b.systemContext,
|
2018-05-12 01:00:14 +08:00
|
|
|
Isolation: b.isolation,
|
2018-03-13 01:53:12 +08:00
|
|
|
NamespaceOptions: b.namespaceOptions,
|
2018-04-14 06:20:25 +08:00
|
|
|
ConfigureNetwork: b.configureNetwork,
|
|
|
|
CNIPluginPath: b.cniPluginPath,
|
|
|
|
CNIConfigDir: b.cniConfigDir,
|
2018-03-13 01:53:12 +08:00
|
|
|
IDMappingOptions: b.idmappingOptions,
|
2018-02-24 01:38:39 +08:00
|
|
|
CommonBuildOpts: b.commonBuildOptions,
|
|
|
|
DefaultMountsFilePath: b.defaultMountsFilePath,
|
2018-08-24 00:55:16 +08:00
|
|
|
Format: b.outputFormat,
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-09-22 07:37:02 +08:00
|
|
|
|
|
|
|
var builder *buildah.Builder
|
|
|
|
var err error
|
|
|
|
// Check and see if the image was declared previously with
|
|
|
|
// an AS clause in the Dockerfile.
|
|
|
|
if asImageFound, ok := b.imageMap[from]; ok {
|
|
|
|
builderOptions.FromImage = asImageFound
|
|
|
|
}
|
|
|
|
builder, err = buildah.NewBuilder(ctx, b.store, builderOptions)
|
2017-03-28 15:06:13 +08:00
|
|
|
if err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error creating build container")
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-09-22 07:37:02 +08:00
|
|
|
|
2017-05-18 23:38:38 +08:00
|
|
|
volumes := map[string]struct{}{}
|
|
|
|
for _, v := range builder.Volumes() {
|
|
|
|
volumes[v] = struct{}{}
|
|
|
|
}
|
2018-09-27 02:34:34 +08:00
|
|
|
ports := map[docker.Port]struct{}{}
|
|
|
|
for _, p := range builder.Ports() {
|
|
|
|
ports[docker.Port(p)] = struct{}{}
|
|
|
|
}
|
Maintain multiple working container configs
Maintain the container configuration in multiple formats in the Buildah
object, initializing one based on the other, depending on which format
the source image used for its configuration.
Replace directly manipulated fields in the Buildah object (Annotations,
CreatedBy, OS, Architecture, Maintainer, User, Workdir, Env, Cmd,
Entrypoint, Expose, Labels, and Volumes) with accessor functions which
update both configurations and which read from whichever one we consider
to be authoritative. Drop Args because we weren't using them.
Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
Closes: #102
Approved by: rhatdan
2017-05-16 23:08:52 +08:00
|
|
|
dConfig := docker.Config{
|
2018-09-27 02:34:34 +08:00
|
|
|
Hostname: builder.Hostname(),
|
|
|
|
Domainname: builder.Domainname(),
|
|
|
|
User: builder.User(),
|
|
|
|
Env: builder.Env(),
|
|
|
|
Cmd: builder.Cmd(),
|
|
|
|
Image: from,
|
|
|
|
Volumes: volumes,
|
|
|
|
WorkingDir: builder.WorkDir(),
|
|
|
|
Entrypoint: builder.Entrypoint(),
|
|
|
|
Labels: builder.Labels(),
|
|
|
|
Shell: builder.Shell(),
|
|
|
|
StopSignal: builder.StopSignal(),
|
|
|
|
OnBuild: builder.OnBuild(),
|
|
|
|
ExposedPorts: ports,
|
2017-05-18 23:38:38 +08:00
|
|
|
}
|
|
|
|
var rootfs *docker.RootFS
|
|
|
|
if builder.Docker.RootFS != nil {
|
|
|
|
rootfs = &docker.RootFS{
|
|
|
|
Type: builder.Docker.RootFS.Type,
|
|
|
|
}
|
|
|
|
for _, id := range builder.Docker.RootFS.DiffIDs {
|
|
|
|
rootfs.Layers = append(rootfs.Layers, id.String())
|
|
|
|
}
|
Maintain multiple working container configs
Maintain the container configuration in multiple formats in the Buildah
object, initializing one based on the other, depending on which format
the source image used for its configuration.
Replace directly manipulated fields in the Buildah object (Annotations,
CreatedBy, OS, Architecture, Maintainer, User, Workdir, Env, Cmd,
Entrypoint, Expose, Labels, and Volumes) with accessor functions which
update both configurations and which read from whichever one we consider
to be authoritative. Drop Args because we weren't using them.
Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
Closes: #102
Approved by: rhatdan
2017-05-16 23:08:52 +08:00
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
dImage := docker.Image{
|
2017-05-18 23:38:38 +08:00
|
|
|
Parent: builder.FromImage,
|
Maintain multiple working container configs
Maintain the container configuration in multiple formats in the Buildah
object, initializing one based on the other, depending on which format
the source image used for its configuration.
Replace directly manipulated fields in the Buildah object (Annotations,
CreatedBy, OS, Architecture, Maintainer, User, Workdir, Env, Cmd,
Entrypoint, Expose, Labels, and Volumes) with accessor functions which
update both configurations and which read from whichever one we consider
to be authoritative. Drop Args because we weren't using them.
Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
Closes: #102
Approved by: rhatdan
2017-05-16 23:08:52 +08:00
|
|
|
ContainerConfig: dConfig,
|
2017-05-18 23:38:38 +08:00
|
|
|
Container: builder.Container,
|
|
|
|
Author: builder.Maintainer(),
|
|
|
|
Architecture: builder.Architecture(),
|
|
|
|
RootFS: rootfs,
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2017-05-18 23:38:38 +08:00
|
|
|
dImage.Config = &dImage.ContainerConfig
|
2017-03-28 15:06:13 +08:00
|
|
|
err = ib.FromImage(&dImage, node)
|
|
|
|
if err != nil {
|
|
|
|
if err2 := builder.Delete(); err2 != nil {
|
|
|
|
logrus.Debugf("error deleting container which we failed to update: %v", err2)
|
|
|
|
}
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error updating build context")
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2017-11-22 21:57:31 +08:00
|
|
|
mountPoint, err := builder.Mount(builder.MountLabel)
|
2017-03-28 15:06:13 +08:00
|
|
|
if err != nil {
|
|
|
|
if err2 := builder.Delete(); err2 != nil {
|
|
|
|
logrus.Debugf("error deleting container which we failed to mount: %v", err2)
|
|
|
|
}
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error mounting new container")
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
b.mountPoint = mountPoint
|
|
|
|
b.builder = builder
|
2018-06-20 03:51:19 +08:00
|
|
|
// Add the top layer of this image to b.topLayers so we can keep track of them
|
2018-06-09 00:55:46 +08:00
|
|
|
// when building with cached images.
|
|
|
|
b.topLayers = append(b.topLayers, builder.TopLayer)
|
2018-08-01 00:02:06 +08:00
|
|
|
logrus.Debugln("Container ID:", builder.ContainerID)
|
2017-03-28 15:06:13 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete deletes the working container, if we have one. The Executor object
|
|
|
|
// should not be used to build another image, as the name of the output image
|
|
|
|
// isn't resettable.
|
|
|
|
func (b *Executor) Delete() (err error) {
|
|
|
|
if b.builder != nil {
|
|
|
|
err = b.builder.Delete()
|
|
|
|
b.builder = nil
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-06-29 01:59:42 +08:00
|
|
|
// resolveNameToImageRef creates a types.ImageReference from b.output
|
|
|
|
func (b *Executor) resolveNameToImageRef() (types.ImageReference, error) {
|
|
|
|
var (
|
|
|
|
imageRef types.ImageReference
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
if b.output != "" {
|
|
|
|
imageRef, err = alltransports.ParseImageName(b.output)
|
|
|
|
if err != nil {
|
2018-08-04 09:21:57 +08:00
|
|
|
candidates, _, err := util.ResolveName(b.output, "", b.systemContext, b.store)
|
2018-08-22 04:33:36 +08:00
|
|
|
if err != nil {
|
2018-10-17 07:57:59 +08:00
|
|
|
return nil, errors.Wrapf(err, "error parsing target image name %q", b.output)
|
2018-08-22 04:33:36 +08:00
|
|
|
}
|
2018-06-29 01:59:42 +08:00
|
|
|
if len(candidates) == 0 {
|
2018-07-10 23:47:26 +08:00
|
|
|
return nil, errors.Errorf("error parsing target image name %q", b.output)
|
2018-06-29 01:59:42 +08:00
|
|
|
}
|
|
|
|
imageRef2, err2 := is.Transport.ParseStoreReference(b.store, candidates[0])
|
|
|
|
if err2 != nil {
|
2018-07-10 23:47:26 +08:00
|
|
|
return nil, errors.Wrapf(err, "error parsing target image name %q", b.output)
|
2018-06-29 01:59:42 +08:00
|
|
|
}
|
|
|
|
return imageRef2, nil
|
|
|
|
}
|
2018-07-10 23:47:26 +08:00
|
|
|
return imageRef, nil
|
2018-06-29 01:59:42 +08:00
|
|
|
}
|
|
|
|
imageRef, err = is.Transport.ParseStoreReference(b.store, "@"+stringid.GenerateRandomID())
|
|
|
|
if err != nil {
|
2018-07-10 23:47:26 +08:00
|
|
|
return nil, errors.Wrapf(err, "error parsing reference for image to be written")
|
2018-06-29 01:59:42 +08:00
|
|
|
}
|
|
|
|
return imageRef, nil
|
|
|
|
}
|
|
|
|
|
2017-03-28 15:06:13 +08:00
|
|
|
// Execute runs each of the steps in the parsed tree, in turn.
|
2018-09-22 07:37:02 +08:00
|
|
|
func (b *Executor) Execute(ctx context.Context, stage imagebuilder.Stage) error {
|
|
|
|
ib := stage.Builder
|
|
|
|
node := stage.Node
|
2018-06-09 00:55:46 +08:00
|
|
|
checkForLayers := true
|
|
|
|
children := node.Children
|
2018-06-14 02:09:45 +08:00
|
|
|
commitName := b.output
|
2018-09-26 05:39:22 +08:00
|
|
|
b.containerIDs = nil
|
2018-09-22 07:37:02 +08:00
|
|
|
|
2018-11-29 14:31:20 +08:00
|
|
|
var leftoverArgs []string
|
|
|
|
for arg := range b.builder.Args {
|
|
|
|
if !builtinAllowedBuildArgs[arg] {
|
|
|
|
leftoverArgs = append(leftoverArgs, arg)
|
|
|
|
}
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
for i, node := range node.Children {
|
|
|
|
step := ib.Step()
|
|
|
|
if err := step.Resolve(node); err != nil {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error resolving step %+v", *node)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
logrus.Debugf("Parsed Step: %+v", *step)
|
2018-11-29 14:31:20 +08:00
|
|
|
if step.Command == "arg" {
|
|
|
|
for index, arg := range leftoverArgs {
|
|
|
|
for _, Arg := range step.Args {
|
|
|
|
list := strings.SplitN(Arg, "=", 2)
|
|
|
|
if arg == list[0] {
|
|
|
|
leftoverArgs = append(leftoverArgs[:index], leftoverArgs[index+1:]...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
if !b.quiet {
|
2017-04-11 02:17:15 +08:00
|
|
|
b.log("%s", step.Original)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
requiresStart := false
|
|
|
|
if i < len(node.Children)-1 {
|
|
|
|
requiresStart = ib.RequiresStart(&parser.Node{Children: node.Children[i+1:]})
|
|
|
|
}
|
2018-06-09 00:55:46 +08:00
|
|
|
|
|
|
|
if !b.layers && !b.noCache {
|
|
|
|
err := ib.Run(step, b, requiresStart)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "error building at step %+v", *step)
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-06-14 02:09:45 +08:00
|
|
|
if i < len(children)-1 {
|
|
|
|
b.output = ""
|
|
|
|
} else {
|
|
|
|
b.output = commitName
|
|
|
|
}
|
|
|
|
|
2018-06-09 00:55:46 +08:00
|
|
|
var (
|
|
|
|
cacheID string
|
|
|
|
err error
|
|
|
|
imgID string
|
|
|
|
)
|
2018-11-19 04:10:26 +08:00
|
|
|
|
|
|
|
b.copyFrom = ""
|
|
|
|
// Check if --from exists in the step command of COPY or ADD
|
|
|
|
// If it exists, set b.copyfrom to that value
|
|
|
|
for _, n := range step.Flags {
|
|
|
|
if strings.Contains(n, "--from") && (step.Command == "copy" || step.Command == "add") {
|
|
|
|
arr := strings.Split(n, "=")
|
|
|
|
b.copyFrom = b.named[arr[1]].mountPoint
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-09 00:55:46 +08:00
|
|
|
// checkForLayers will be true if b.layers is true and a cached intermediate image is found.
|
|
|
|
// checkForLayers is set to false when either there is no cached image or a break occurs where
|
|
|
|
// the instructions in the Dockerfile change from a previous build.
|
|
|
|
// Don't check for cache if b.noCache is set to true.
|
|
|
|
if checkForLayers && !b.noCache {
|
|
|
|
cacheID, err = b.layerExists(ctx, node, children[:i])
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "error checking if cached image exists from a previous build")
|
|
|
|
}
|
|
|
|
}
|
2018-06-29 01:59:42 +08:00
|
|
|
|
|
|
|
if cacheID != "" {
|
|
|
|
fmt.Fprintf(b.out, "--> Using cache %s\n", cacheID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If a cache is found for the last step, that means nothing in the
|
|
|
|
// Dockerfile changed. Just create a copy of the existing image and
|
|
|
|
// save it with the new name passed in by the user.
|
|
|
|
if cacheID != "" && i == len(children)-1 {
|
|
|
|
if err := b.copyExistingImage(ctx, cacheID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-11-19 04:10:26 +08:00
|
|
|
b.containerIDs = append(b.containerIDs, b.builder.ContainerID)
|
2018-06-29 01:59:42 +08:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2018-06-09 00:55:46 +08:00
|
|
|
if cacheID == "" || !checkForLayers {
|
|
|
|
checkForLayers = false
|
|
|
|
err := ib.Run(step, b, requiresStart)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "error building at step %+v", *step)
|
|
|
|
}
|
|
|
|
}
|
2018-06-29 01:59:42 +08:00
|
|
|
|
|
|
|
// Commit if no cache is found
|
|
|
|
if cacheID == "" {
|
2018-10-12 04:58:04 +08:00
|
|
|
imgID, _, err = b.Commit(ctx, ib, getCreatedBy(node))
|
2018-06-09 00:55:46 +08:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "error committing container for step %+v", *step)
|
|
|
|
}
|
|
|
|
if i == len(children)-1 {
|
|
|
|
b.log("COMMIT %s", b.output)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Cache is found, assign imgID the id of the cached image so
|
|
|
|
// it is used to create the container for the next step.
|
|
|
|
imgID = cacheID
|
|
|
|
}
|
2018-08-01 00:02:06 +08:00
|
|
|
// Add container ID of successful intermediate container to b.containerIDs
|
|
|
|
b.containerIDs = append(b.containerIDs, b.builder.ContainerID)
|
2018-06-09 00:55:46 +08:00
|
|
|
// Prepare for the next step with imgID as the new base image.
|
|
|
|
if i != len(children)-1 {
|
2018-09-22 07:37:02 +08:00
|
|
|
if err := b.Prepare(ctx, stage, imgID); err != nil {
|
2018-06-09 00:55:46 +08:00
|
|
|
return errors.Wrap(err, "error preparing container for next step")
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
}
|
2018-11-29 14:31:20 +08:00
|
|
|
if len(leftoverArgs) > 0 {
|
|
|
|
fmt.Fprintf(b.out, "[Warning] One or more build-args %v were not consumed\n", leftoverArgs)
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-29 01:59:42 +08:00
|
|
|
// copyExistingImage creates a copy of an image already in store
|
|
|
|
func (b *Executor) copyExistingImage(ctx context.Context, cacheID string) error {
|
|
|
|
// Get the destination Image Reference
|
|
|
|
dest, err := b.resolveNameToImageRef()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
policyContext, err := util.GetPolicyContext(b.systemContext)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer policyContext.Destroy()
|
|
|
|
|
|
|
|
// Look up the source image, expecting it to be in local storage
|
|
|
|
src, err := is.Transport.ParseStoreReference(b.store, cacheID)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "error getting source imageReference for %q", cacheID)
|
|
|
|
}
|
2018-10-12 04:34:13 +08:00
|
|
|
if _, err := cp.Image(ctx, policyContext, dest, src, nil); err != nil {
|
2018-06-29 01:59:42 +08:00
|
|
|
return errors.Wrapf(err, "error copying image %q", cacheID)
|
|
|
|
}
|
|
|
|
b.log("COMMIT %s", b.output)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-09 00:55:46 +08:00
|
|
|
// layerExists returns true if an intermediate image of currNode exists in the image store from a previous build.
|
2018-09-22 07:37:02 +08:00
|
|
|
// It verifies this by checking the parent of the top layer of the image and the history.
|
2018-06-09 00:55:46 +08:00
|
|
|
func (b *Executor) layerExists(ctx context.Context, currNode *parser.Node, children []*parser.Node) (string, error) {
|
|
|
|
// Get the list of images available in the image store
|
|
|
|
images, err := b.store.Images()
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrap(err, "error getting image list from store")
|
|
|
|
}
|
|
|
|
for _, image := range images {
|
|
|
|
layer, err := b.store.Layer(image.TopLayer)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrapf(err, "error getting top layer info")
|
|
|
|
}
|
|
|
|
// If the parent of the top layer of an image is equal to the last entry in b.topLayers
|
|
|
|
// it means that this image is potentially a cached intermediate image from a previous
|
|
|
|
// build. Next we double check that the history of this image is equivalent to the previous
|
|
|
|
// lines in the Dockerfile up till the point we are at in the build.
|
|
|
|
if layer.Parent == b.topLayers[len(b.topLayers)-1] {
|
|
|
|
history, err := b.getImageHistory(ctx, image.ID)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrapf(err, "error getting history of %q", image.ID)
|
|
|
|
}
|
|
|
|
// children + currNode is the point of the Dockerfile we are currently at.
|
|
|
|
if historyMatches(append(children, currNode), history) {
|
|
|
|
// This checks if the files copied during build have been changed if the node is
|
|
|
|
// a COPY or ADD command.
|
|
|
|
filesMatch, err := b.copiedFilesMatch(currNode, history[len(history)-1].Created)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrapf(err, "error checking if copied files match")
|
|
|
|
}
|
|
|
|
if filesMatch {
|
|
|
|
return image.ID, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getImageHistory returns the history of imageID.
|
|
|
|
func (b *Executor) getImageHistory(ctx context.Context, imageID string) ([]v1.History, error) {
|
|
|
|
imageRef, err := is.Transport.ParseStoreReference(b.store, "@"+imageID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "error getting image reference %q", imageID)
|
|
|
|
}
|
|
|
|
ref, err := imageRef.NewImage(ctx, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "error creating new image from reference")
|
|
|
|
}
|
|
|
|
oci, err := ref.OCIConfig(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "error getting oci config of image %q", imageID)
|
|
|
|
}
|
|
|
|
return oci.History, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getCreatedBy returns the command the image at node will be created by.
|
|
|
|
func getCreatedBy(node *parser.Node) string {
|
|
|
|
if node.Value == "run" {
|
|
|
|
return "/bin/sh -c " + node.Original[4:]
|
|
|
|
}
|
|
|
|
return "/bin/sh -c #(nop) " + node.Original
|
|
|
|
}
|
|
|
|
|
|
|
|
// historyMatches returns true if the history of the image matches the lines
|
|
|
|
// in the Dockerfile till the point of build we are at.
|
|
|
|
// Used to verify whether a cache of the intermediate image exists and whether
|
|
|
|
// to run the build again.
|
|
|
|
func historyMatches(children []*parser.Node, history []v1.History) bool {
|
|
|
|
i := len(history) - 1
|
|
|
|
for j := len(children) - 1; j >= 0; j-- {
|
|
|
|
instruction := children[j].Original
|
|
|
|
if children[j].Value == "run" {
|
|
|
|
instruction = instruction[4:]
|
|
|
|
}
|
|
|
|
if !strings.Contains(history[i].CreatedBy, instruction) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
i--
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// getFilesToCopy goes through node to get all the src files that are copied, added or downloaded.
|
|
|
|
// It is possible for the Dockerfile to have src as hom*, which means all files that have hom as a prefix.
|
|
|
|
// Another format is hom?.txt, which means all files that have that name format with the ? replaced by another character.
|
|
|
|
func (b *Executor) getFilesToCopy(node *parser.Node) ([]string, error) {
|
|
|
|
currNode := node.Next
|
|
|
|
var src []string
|
|
|
|
for currNode.Next != nil {
|
|
|
|
if currNode.Next == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if strings.HasPrefix(currNode.Value, "http://") || strings.HasPrefix(currNode.Value, "https://") {
|
|
|
|
src = append(src, currNode.Value)
|
2018-08-21 23:28:19 +08:00
|
|
|
currNode = currNode.Next
|
2018-06-09 00:55:46 +08:00
|
|
|
continue
|
|
|
|
}
|
2018-11-19 04:10:26 +08:00
|
|
|
if b.copyFrom != "" {
|
|
|
|
src = append(src, filepath.Join(b.copyFrom, currNode.Value))
|
|
|
|
currNode = currNode.Next
|
|
|
|
continue
|
|
|
|
}
|
2018-06-09 00:55:46 +08:00
|
|
|
matches, err := filepath.Glob(filepath.Join(b.contextDir, currNode.Value))
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "error finding match for pattern %q", currNode.Value)
|
|
|
|
}
|
|
|
|
src = append(src, matches...)
|
|
|
|
currNode = currNode.Next
|
|
|
|
}
|
|
|
|
return src, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// copiedFilesMatch checks to see if the node instruction is a COPY or ADD.
|
|
|
|
// If it is either of those two it checks the timestamps on all the files copied/added
|
|
|
|
// by the dockerfile. If the host version has a time stamp greater than the time stamp
|
|
|
|
// of the build, the build will not use the cached version and will rebuild.
|
|
|
|
func (b *Executor) copiedFilesMatch(node *parser.Node, historyTime *time.Time) (bool, error) {
|
|
|
|
if node.Value != "add" && node.Value != "copy" {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
src, err := b.getFilesToCopy(node)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
for _, item := range src {
|
|
|
|
// for urls, check the Last-Modified field in the header.
|
|
|
|
if strings.HasPrefix(item, "http://") || strings.HasPrefix(item, "https://") {
|
|
|
|
urlContentNew, err := urlContentModified(item, historyTime)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
if urlContentNew {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
2018-10-09 02:17:31 +08:00
|
|
|
// Walks the file tree for local files and uses chroot to ensure we don't escape out of the allowed path
|
|
|
|
// when resolving any symlinks.
|
|
|
|
// Change the time format to ensure we don't run into a parsing error when converting again from string
|
|
|
|
// to time.Time. It is a known Go issue that the conversions cause errors sometimes, so specifying a particular
|
|
|
|
// time format here when converting to a string.
|
2018-11-19 04:10:26 +08:00
|
|
|
// If the COPY has --from in the command, change the rootdir to mountpoint of the container it is copying from
|
|
|
|
rootdir := b.contextDir
|
|
|
|
if b.copyFrom != "" {
|
|
|
|
rootdir = b.copyFrom
|
|
|
|
}
|
|
|
|
timeIsGreater, err := resolveModifiedTime(rootdir, item, historyTime.Format(time.RFC3339Nano))
|
2018-06-09 00:55:46 +08:00
|
|
|
if err != nil {
|
2018-10-09 02:17:31 +08:00
|
|
|
return false, errors.Wrapf(err, "error resolving symlinks and comparing modified times: %q", item)
|
2018-06-09 00:55:46 +08:00
|
|
|
}
|
|
|
|
if timeIsGreater {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// urlContentModified sends a get request to the url and checks if the header has a value in
|
|
|
|
// Last-Modified, and if it does compares the time stamp to that of the history of the cached image.
|
|
|
|
// returns true if there is no Last-Modified value in the header.
|
|
|
|
func urlContentModified(url string, historyTime *time.Time) (bool, error) {
|
|
|
|
resp, err := http.Get(url)
|
|
|
|
if err != nil {
|
|
|
|
return false, errors.Wrapf(err, "error getting %q", url)
|
|
|
|
}
|
|
|
|
if lastModified := resp.Header.Get("Last-Modified"); lastModified != "" {
|
|
|
|
lastModifiedTime, err := time.Parse(time.RFC1123, lastModified)
|
|
|
|
if err != nil {
|
|
|
|
return false, errors.Wrapf(err, "error parsing time for %q", url)
|
|
|
|
}
|
|
|
|
return lastModifiedTime.After(*historyTime), nil
|
|
|
|
}
|
|
|
|
logrus.Debugf("Response header did not have Last-Modified %q, will rebuild.", url)
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2017-03-28 15:06:13 +08:00
|
|
|
// Commit writes the container's contents to an image, using a passed-in tag as
|
|
|
|
// the name if there is one, generating a unique ID-based one otherwise.
|
2018-10-12 04:58:04 +08:00
|
|
|
func (b *Executor) Commit(ctx context.Context, ib *imagebuilder.Builder, createdBy string) (string, reference.Canonical, error) {
|
2018-06-29 01:59:42 +08:00
|
|
|
imageRef, err := b.resolveNameToImageRef()
|
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, err
|
2017-05-18 23:36:39 +08:00
|
|
|
}
|
2018-06-29 01:59:42 +08:00
|
|
|
|
2018-02-06 05:26:21 +08:00
|
|
|
if ib.Author != "" {
|
|
|
|
b.builder.SetMaintainer(ib.Author)
|
|
|
|
}
|
2017-05-18 23:38:38 +08:00
|
|
|
config := ib.Config()
|
2018-06-09 00:55:46 +08:00
|
|
|
b.builder.SetCreatedBy(createdBy)
|
2017-05-18 23:38:38 +08:00
|
|
|
b.builder.SetHostname(config.Hostname)
|
|
|
|
b.builder.SetDomainname(config.Domainname)
|
|
|
|
b.builder.SetUser(config.User)
|
|
|
|
b.builder.ClearPorts()
|
|
|
|
for p := range config.ExposedPorts {
|
|
|
|
b.builder.SetPort(string(p))
|
|
|
|
}
|
|
|
|
for _, envSpec := range config.Env {
|
|
|
|
spec := strings.SplitN(envSpec, "=", 2)
|
|
|
|
b.builder.SetEnv(spec[0], spec[1])
|
|
|
|
}
|
|
|
|
b.builder.SetCmd(config.Cmd)
|
|
|
|
b.builder.ClearVolumes()
|
|
|
|
for v := range config.Volumes {
|
|
|
|
b.builder.AddVolume(v)
|
|
|
|
}
|
2018-05-25 15:53:30 +08:00
|
|
|
b.builder.ClearOnBuild()
|
|
|
|
for _, onBuildSpec := range config.OnBuild {
|
|
|
|
b.builder.SetOnBuild(onBuildSpec)
|
|
|
|
}
|
2017-05-18 23:38:38 +08:00
|
|
|
b.builder.SetWorkDir(config.WorkingDir)
|
|
|
|
b.builder.SetEntrypoint(config.Entrypoint)
|
2018-03-16 19:57:36 +08:00
|
|
|
b.builder.SetShell(config.Shell)
|
2018-04-11 16:10:45 +08:00
|
|
|
b.builder.SetStopSignal(config.StopSignal)
|
2018-08-21 03:25:10 +08:00
|
|
|
if config.Healthcheck != nil {
|
|
|
|
b.builder.SetHealthcheck(&buildahdocker.HealthConfig{
|
|
|
|
Test: append([]string{}, config.Healthcheck.Test...),
|
|
|
|
Interval: config.Healthcheck.Interval,
|
|
|
|
Timeout: config.Healthcheck.Timeout,
|
|
|
|
StartPeriod: config.Healthcheck.StartPeriod,
|
|
|
|
Retries: config.Healthcheck.Retries,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
b.builder.SetHealthcheck(nil)
|
|
|
|
}
|
2017-05-18 23:38:38 +08:00
|
|
|
b.builder.ClearLabels()
|
|
|
|
for k, v := range config.Labels {
|
|
|
|
b.builder.SetLabel(k, v)
|
|
|
|
}
|
2018-05-23 00:05:18 +08:00
|
|
|
for _, labelSpec := range b.labels {
|
|
|
|
label := strings.SplitN(labelSpec, "=", 2)
|
|
|
|
if len(label) > 1 {
|
|
|
|
b.builder.SetLabel(label[0], label[1])
|
|
|
|
} else {
|
|
|
|
b.builder.SetLabel(label[0], "")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, annotationSpec := range b.annotations {
|
|
|
|
annotation := strings.SplitN(annotationSpec, "=", 2)
|
|
|
|
if len(annotation) > 1 {
|
|
|
|
b.builder.SetAnnotation(annotation[0], annotation[1])
|
|
|
|
} else {
|
|
|
|
b.builder.SetAnnotation(annotation[0], "")
|
|
|
|
}
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
if imageRef != nil {
|
|
|
|
logName := transports.ImageName(imageRef)
|
|
|
|
logrus.Debugf("COMMIT %q", logName)
|
2018-06-09 00:55:46 +08:00
|
|
|
if !b.quiet && !b.layers && !b.noCache {
|
2017-04-11 02:17:15 +08:00
|
|
|
b.log("COMMIT %s", logName)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logrus.Debugf("COMMIT")
|
2018-06-09 00:55:46 +08:00
|
|
|
if !b.quiet && !b.layers && !b.noCache {
|
2017-04-11 02:17:15 +08:00
|
|
|
b.log("COMMIT")
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
}
|
2018-06-09 00:55:46 +08:00
|
|
|
writer := b.reportWriter
|
|
|
|
if b.layers || b.noCache {
|
|
|
|
writer = nil
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
options := buildah.CommitOptions{
|
2017-05-30 23:51:40 +08:00
|
|
|
Compression: b.compression,
|
|
|
|
SignaturePolicyPath: b.signaturePolicyPath,
|
|
|
|
AdditionalTags: b.additionalTags,
|
2018-06-09 00:55:46 +08:00
|
|
|
ReportWriter: writer,
|
2017-05-30 23:51:40 +08:00
|
|
|
PreferredManifestType: b.outputFormat,
|
2018-10-03 01:48:45 +08:00
|
|
|
SystemContext: b.systemContext,
|
2018-04-25 22:00:46 +08:00
|
|
|
IIDFile: b.iidfile,
|
2018-05-22 05:02:50 +08:00
|
|
|
Squash: b.squash,
|
2018-10-18 06:06:16 +08:00
|
|
|
BlobDirectory: b.blobDirectory,
|
2018-06-09 00:55:46 +08:00
|
|
|
Parent: b.builder.FromImageID,
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-10-12 04:58:04 +08:00
|
|
|
imgID, ref, _, err := b.builder.Commit(ctx, imageRef, options)
|
2018-05-02 01:20:16 +08:00
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, err
|
2018-05-02 01:20:16 +08:00
|
|
|
}
|
|
|
|
if options.IIDFile == "" && imgID != "" {
|
2018-06-29 01:59:42 +08:00
|
|
|
fmt.Fprintf(b.out, "--> %s\n", imgID)
|
2018-05-02 01:20:16 +08:00
|
|
|
}
|
2018-10-12 04:58:04 +08:00
|
|
|
return imgID, ref, nil
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Build takes care of the details of running Prepare/Execute/Commit/Delete
|
2018-02-24 18:09:25 +08:00
|
|
|
// over each of the one or more parsed Dockerfiles and stages.
|
2018-10-12 04:58:04 +08:00
|
|
|
func (b *Executor) Build(ctx context.Context, stages imagebuilder.Stages) (string, reference.Canonical, error) {
|
2018-02-24 18:09:25 +08:00
|
|
|
if len(stages) == 0 {
|
|
|
|
errors.New("error building: no stages to build")
|
|
|
|
}
|
2018-08-01 00:02:06 +08:00
|
|
|
var (
|
|
|
|
stageExecutor *Executor
|
|
|
|
lastErr error
|
|
|
|
)
|
2018-09-22 07:37:02 +08:00
|
|
|
b.imageMap = make(map[string]string)
|
|
|
|
stageCount := 0
|
2018-02-24 18:09:25 +08:00
|
|
|
for _, stage := range stages {
|
2018-04-11 01:35:03 +08:00
|
|
|
stageExecutor = b.withName(stage.Name, stage.Position)
|
2018-09-22 07:37:02 +08:00
|
|
|
if err := stageExecutor.Prepare(ctx, stage, ""); err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, err
|
2018-02-24 18:09:25 +08:00
|
|
|
}
|
2018-06-14 02:09:45 +08:00
|
|
|
// Always remove the intermediate/build containers, even if the build was unsuccessful.
|
|
|
|
// If building with layers, remove all intermediate/build containers if b.forceRmIntermediateCtrs
|
|
|
|
// is true.
|
|
|
|
if b.forceRmIntermediateCtrs || (!b.layers && !b.noCache) {
|
|
|
|
defer stageExecutor.Delete()
|
|
|
|
}
|
2018-09-22 07:37:02 +08:00
|
|
|
if err := stageExecutor.Execute(ctx, stage); err != nil {
|
2018-08-01 00:02:06 +08:00
|
|
|
lastErr = err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete the successful intermediate containers if an error in the build
|
|
|
|
// process occurs and b.removeIntermediateCtrs is true.
|
|
|
|
if lastErr != nil {
|
|
|
|
if b.removeIntermediateCtrs {
|
|
|
|
stageExecutor.deleteSuccessfulIntermediateCtrs()
|
|
|
|
}
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, lastErr
|
2018-02-24 18:09:25 +08:00
|
|
|
}
|
2018-08-01 00:02:06 +08:00
|
|
|
b.containerIDs = append(b.containerIDs, stageExecutor.containerIDs...)
|
2018-09-22 07:37:02 +08:00
|
|
|
// If we've a stage.Name with alpha and not numeric, we've an
|
|
|
|
// AS clause in play. Create an intermediate image for this
|
|
|
|
// stage to be used by other FROM statements that will want
|
|
|
|
// to use it later in the Dockerfile. Note the id in our map.
|
|
|
|
if _, err := strconv.Atoi(stage.Name); err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
imgID, _, err := stageExecutor.Commit(ctx, stages[stageCount].Builder, "")
|
2018-09-22 07:37:02 +08:00
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, err
|
2018-09-22 07:37:02 +08:00
|
|
|
}
|
|
|
|
b.imageMap[stage.Name] = imgID
|
|
|
|
}
|
|
|
|
stageCount++
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-06-29 01:59:42 +08:00
|
|
|
|
2018-10-12 04:58:04 +08:00
|
|
|
var imageRef reference.Canonical
|
|
|
|
imageID := ""
|
2018-06-29 01:59:42 +08:00
|
|
|
if !b.layers && !b.noCache {
|
2018-10-12 04:58:04 +08:00
|
|
|
imgID, ref, err := stageExecutor.Commit(ctx, stages[len(stages)-1].Builder, "")
|
2018-06-29 01:59:42 +08:00
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, err
|
2018-06-29 01:59:42 +08:00
|
|
|
}
|
2018-10-12 04:58:04 +08:00
|
|
|
imageID = imgID
|
|
|
|
imageRef = ref
|
2018-06-14 02:09:45 +08:00
|
|
|
}
|
|
|
|
// If building with layers and b.removeIntermediateCtrs is true
|
|
|
|
// only remove intermediate container for each step if an error
|
|
|
|
// during the build process doesn't occur.
|
|
|
|
// If the build is unsuccessful, the container created at the step
|
|
|
|
// the failure happened will persist in the container store.
|
|
|
|
// This if condition will be false if not building with layers and
|
|
|
|
// the removal of intermediate/build containers will be handled by the
|
|
|
|
// defer statement above.
|
|
|
|
if b.removeIntermediateCtrs && (b.layers || b.noCache) {
|
2018-08-01 00:02:06 +08:00
|
|
|
if err := b.deleteSuccessfulIntermediateCtrs(); err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, errors.Errorf("Failed to cleanup intermediate containers")
|
2018-08-01 00:02:06 +08:00
|
|
|
}
|
2018-06-14 02:09:45 +08:00
|
|
|
}
|
2018-09-22 07:37:02 +08:00
|
|
|
// Remove intermediate images that we created for AS clause handling
|
|
|
|
for _, value := range b.imageMap {
|
|
|
|
if _, err := b.store.DeleteImage(value, true); err != nil {
|
|
|
|
logrus.Debugf("unable to remove intermediate image %q: %v", value, err)
|
|
|
|
}
|
|
|
|
}
|
2018-10-12 04:58:04 +08:00
|
|
|
return imageID, imageRef, nil
|
2018-02-24 18:09:25 +08:00
|
|
|
}
|
|
|
|
|
2017-03-28 15:06:13 +08:00
|
|
|
// BuildDockerfiles parses a set of one or more Dockerfiles (which may be
|
|
|
|
// URLs), creates a new Executor, and then runs Prepare/Execute/Commit/Delete
|
|
|
|
// over the entire set of instructions.
|
2018-10-12 04:58:04 +08:00
|
|
|
func BuildDockerfiles(ctx context.Context, store storage.Store, options BuildOptions, paths ...string) (string, reference.Canonical, error) {
|
2018-04-11 01:35:03 +08:00
|
|
|
if len(paths) == 0 {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, errors.Errorf("error building: no dockerfiles specified")
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-02-25 06:40:44 +08:00
|
|
|
var dockerfiles []io.ReadCloser
|
|
|
|
defer func(dockerfiles ...io.ReadCloser) {
|
|
|
|
for _, d := range dockerfiles {
|
|
|
|
d.Close()
|
|
|
|
}
|
|
|
|
}(dockerfiles...)
|
2018-09-22 07:37:02 +08:00
|
|
|
|
2018-04-11 01:35:03 +08:00
|
|
|
for _, dfile := range paths {
|
2018-08-01 18:31:02 +08:00
|
|
|
var data io.ReadCloser
|
|
|
|
|
2017-03-28 15:06:13 +08:00
|
|
|
if strings.HasPrefix(dfile, "http://") || strings.HasPrefix(dfile, "https://") {
|
|
|
|
logrus.Debugf("reading remote Dockerfile %q", dfile)
|
|
|
|
resp, err := http.Get(dfile)
|
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, errors.Wrapf(err, "error getting %q", dfile)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
if resp.ContentLength == 0 {
|
|
|
|
resp.Body.Close()
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, errors.Errorf("no contents in %q", dfile)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-08-01 18:31:02 +08:00
|
|
|
data = resp.Body
|
2017-03-28 15:06:13 +08:00
|
|
|
} else {
|
2018-08-03 07:48:44 +08:00
|
|
|
// If the Dockerfile isn't found try prepending the
|
|
|
|
// context directory to it.
|
2018-10-28 03:47:03 +08:00
|
|
|
dinfo, err := os.Stat(dfile)
|
|
|
|
if os.IsNotExist(err) {
|
2017-03-28 15:06:13 +08:00
|
|
|
dfile = filepath.Join(options.ContextDirectory, dfile)
|
|
|
|
}
|
2018-10-28 03:47:03 +08:00
|
|
|
dinfo, err = os.Stat(dfile)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, errors.Wrapf(err, "error reading info about %q", dfile)
|
|
|
|
}
|
|
|
|
// If given a directory, add '/Dockerfile' to it.
|
|
|
|
if dinfo.Mode().IsDir() {
|
|
|
|
dfile = filepath.Join(dfile, "Dockerfile")
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
logrus.Debugf("reading local Dockerfile %q", dfile)
|
2018-02-24 18:09:25 +08:00
|
|
|
contents, err := os.Open(dfile)
|
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, errors.Wrapf(err, "error reading %q", dfile)
|
2018-02-24 18:09:25 +08:00
|
|
|
}
|
2018-10-28 03:47:03 +08:00
|
|
|
dinfo, err = contents.Stat()
|
2018-02-24 18:09:25 +08:00
|
|
|
if err != nil {
|
|
|
|
contents.Close()
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, errors.Wrapf(err, "error reading info about %q", dfile)
|
2018-02-24 18:09:25 +08:00
|
|
|
}
|
2018-07-27 22:48:16 +08:00
|
|
|
if dinfo.Mode().IsRegular() && dinfo.Size() == 0 {
|
2018-02-24 18:09:25 +08:00
|
|
|
contents.Close()
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, errors.Wrapf(err, "no contents in %q", dfile)
|
2018-02-24 18:09:25 +08:00
|
|
|
}
|
2018-08-01 18:31:02 +08:00
|
|
|
data = contents
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-08-01 18:31:02 +08:00
|
|
|
|
|
|
|
// pre-process Dockerfiles with ".in" suffix
|
|
|
|
if strings.HasSuffix(dfile, ".in") {
|
|
|
|
pData, err := preprocessDockerfileContents(data, options.ContextDirectory)
|
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, err
|
2018-08-01 18:31:02 +08:00
|
|
|
}
|
|
|
|
data = *pData
|
|
|
|
}
|
|
|
|
|
|
|
|
dockerfiles = append(dockerfiles, data)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-02-25 06:40:44 +08:00
|
|
|
mainNode, err := imagebuilder.ParseDockerfile(dockerfiles[0])
|
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, errors.Wrapf(err, "error parsing main Dockerfile")
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-02-25 06:40:44 +08:00
|
|
|
for _, d := range dockerfiles[1:] {
|
|
|
|
additionalNode, err := imagebuilder.ParseDockerfile(d)
|
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, errors.Wrapf(err, "error parsing additional Dockerfile")
|
2018-02-25 06:40:44 +08:00
|
|
|
}
|
|
|
|
mainNode.Children = append(mainNode.Children, additionalNode.Children...)
|
|
|
|
}
|
|
|
|
exec, err := NewExecutor(store, options)
|
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, errors.Wrapf(err, "error creating build executor")
|
2018-02-25 06:40:44 +08:00
|
|
|
}
|
|
|
|
b := imagebuilder.NewBuilder(options.Args)
|
2018-11-08 18:31:14 +08:00
|
|
|
stages, err := imagebuilder.NewStages(mainNode, b)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, errors.Wrap(err, "error reading multiple stages")
|
|
|
|
}
|
2018-04-12 22:20:36 +08:00
|
|
|
return exec.Build(ctx, stages)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-08-01 00:02:06 +08:00
|
|
|
|
|
|
|
// deleteSuccessfulIntermediateCtrs goes through the container IDs in b.containerIDs
|
|
|
|
// and deletes the containers associated with that ID.
|
|
|
|
func (b *Executor) deleteSuccessfulIntermediateCtrs() error {
|
|
|
|
var lastErr error
|
|
|
|
for _, ctr := range b.containerIDs {
|
|
|
|
if err := b.store.DeleteContainer(ctr); err != nil {
|
|
|
|
logrus.Errorf("error deleting build container %q: %v\n", ctr, err)
|
|
|
|
lastErr = err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return lastErr
|
|
|
|
}
|
2018-08-01 18:31:02 +08:00
|
|
|
|
|
|
|
// preprocessDockerfileContents runs CPP(1) in preprocess-only mode on the input
|
|
|
|
// dockerfile content and will use ctxDir as the base include path.
|
|
|
|
//
|
|
|
|
// Note: we cannot use cmd.StdoutPipe() as cmd.Wait() closes it.
|
|
|
|
func preprocessDockerfileContents(r io.ReadCloser, ctxDir string) (rdrCloser *io.ReadCloser, err error) {
|
|
|
|
cppPath := "/usr/bin/cpp"
|
|
|
|
if _, err = os.Stat(cppPath); err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
err = errors.Errorf("error: Dockerfile.in support requires %s to be installed", cppPath)
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
stdout := bytes.Buffer{}
|
|
|
|
stderr := bytes.Buffer{}
|
|
|
|
|
|
|
|
cmd := exec.Command(cppPath, "-E", "-iquote", ctxDir, "-")
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
cmd.Stderr = &stderr
|
|
|
|
|
|
|
|
pipe, err := cmd.StdinPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
|
|
|
pipe.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if err = cmd.Start(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err = io.Copy(pipe, r); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
pipe.Close()
|
|
|
|
if err = cmd.Wait(); err != nil {
|
|
|
|
if stderr.Len() > 0 {
|
|
|
|
err = fmt.Errorf("%v: %s", err, strings.TrimSpace(stderr.String()))
|
|
|
|
}
|
|
|
|
return nil, errors.Wrapf(err, "error pre-processing Dockerfile")
|
|
|
|
}
|
|
|
|
|
|
|
|
rc := ioutil.NopCloser(bytes.NewReader(stdout.Bytes()))
|
|
|
|
return &rc, nil
|
|
|
|
}
|