2017-03-28 15:06:13 +08:00
|
|
|
package imagebuildah
|
|
|
|
|
|
|
|
import (
|
2018-04-12 22:20:36 +08:00
|
|
|
"context"
|
2017-03-28 15:06:13 +08:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"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
|
|
|
|
|
|
|
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-03-28 15:06:13 +08:00
|
|
|
"github.com/projectatomic/buildah"
|
2018-05-02 03:37:13 +08:00
|
|
|
"github.com/projectatomic/buildah/util"
|
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 (
|
2017-05-18 05:02:40 +08:00
|
|
|
PullIfMissing = buildah.PullIfMissing
|
|
|
|
PullAlways = buildah.PullAlways
|
|
|
|
PullNever = buildah.PullNever
|
|
|
|
OCIv1ImageFormat = buildah.OCIv1ImageManifest
|
|
|
|
Dockerv2ImageFormat = buildah.Dockerv2ImageManifest
|
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
|
|
|
|
// Runtime is the name of the command to run for RUN instructions. It
|
|
|
|
// should accept the same arguments and flags that runc does.
|
|
|
|
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{})
|
|
|
|
// 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.
|
|
|
|
// Accepted values are OCIv1ImageFormat and Dockerv2ImageFormat.
|
|
|
|
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
|
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{})
|
|
|
|
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-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
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
if symLink, err := resolveSymLink(b.mountPoint, path); err == nil {
|
|
|
|
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 {
|
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 {
|
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))
|
|
|
|
}
|
|
|
|
}
|
2017-11-30 22:34:02 +08:00
|
|
|
if err := b.builder.Add(copy.Dest, copy.Download, buildah.AddAndCopyOptions{}, 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-06-06 01:53:39 +08:00
|
|
|
devNull, err := os.Open(os.DevNull)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Errorf("error opening %q for reading: %v", os.DevNull, err)
|
|
|
|
}
|
|
|
|
defer devNull.Close()
|
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-06-06 01:53:39 +08:00
|
|
|
Stdin: devNull,
|
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-06-06 01:53:39 +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-06-14 02:09:45 +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,
|
|
|
|
out: options.Out,
|
|
|
|
err: options.Err,
|
|
|
|
reportWriter: options.ReportWriter,
|
|
|
|
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,
|
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-04-12 22:20:36 +08:00
|
|
|
func (b *Executor) Prepare(ctx context.Context, ib *imagebuilder.Builder, node *parser.Node, from string) error {
|
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
|
|
|
}
|
|
|
|
logrus.Debugf("FROM %#v", from)
|
|
|
|
if !b.quiet {
|
2017-04-11 02:17:15 +08:00
|
|
|
b.log("FROM %s", from)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
builderOptions := buildah.BuilderOptions{
|
2018-02-24 01:38:39 +08:00
|
|
|
FromImage: from,
|
|
|
|
PullPolicy: b.pullPolicy,
|
|
|
|
Registry: b.registry,
|
|
|
|
Transport: b.transport,
|
|
|
|
SignaturePolicyPath: b.signaturePolicyPath,
|
|
|
|
ReportWriter: b.reportWriter,
|
|
|
|
SystemContext: b.systemContext,
|
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,
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-04-12 22:20:36 +08:00
|
|
|
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
|
|
|
}
|
2017-05-18 23:38:38 +08:00
|
|
|
volumes := map[string]struct{}{}
|
|
|
|
for _, v := range builder.Volumes() {
|
|
|
|
volumes[v] = 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{
|
2017-05-18 23:38:38 +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(),
|
2018-03-16 19:57:36 +08:00
|
|
|
Shell: builder.Shell(),
|
2018-04-11 16:10:45 +08:00
|
|
|
StopSignal: builder.StopSignal(),
|
2018-05-25 15:53:30 +08:00
|
|
|
OnBuild: builder.OnBuild(),
|
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-09 00:55:46 +08:00
|
|
|
// Add the top layer of this image to b.topLayers so we can keep track of them
|
|
|
|
// when building with cached images.
|
|
|
|
b.topLayers = append(b.topLayers, builder.TopLayer)
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Execute runs each of the steps in the parsed tree, in turn.
|
2018-06-09 00:55:46 +08:00
|
|
|
func (b *Executor) Execute(ctx context.Context, ib *imagebuilder.Builder, node *parser.Node) error {
|
|
|
|
checkForLayers := true
|
|
|
|
children := node.Children
|
2018-06-14 02:09:45 +08:00
|
|
|
commitName := b.output
|
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)
|
|
|
|
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
|
|
|
|
)
|
|
|
|
// 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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if cacheID == "" || !checkForLayers {
|
|
|
|
checkForLayers = false
|
|
|
|
err := ib.Run(step, b, requiresStart)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "error building at step %+v", *step)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
b.log("Using cache %s", cacheID)
|
|
|
|
}
|
|
|
|
// Commit if at the last step of the Dockerfile and a cached image is found.
|
|
|
|
// Also commit steps if no cache is found.
|
|
|
|
if (cacheID != "" && i == len(children)-1) || cacheID == "" {
|
|
|
|
imgID, err = b.Commit(ctx, ib, getCreatedBy(node))
|
|
|
|
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-06-14 02:09:45 +08:00
|
|
|
// Delete the intermediate container if b.removeIntermediateCtrs is true.
|
|
|
|
if b.removeIntermediateCtrs {
|
|
|
|
if err := b.Delete(); err != nil {
|
|
|
|
return errors.Wrap(err, "error deleting intermediate container")
|
|
|
|
}
|
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 {
|
|
|
|
if err := b.Prepare(ctx, ib, node, imgID); err != nil {
|
|
|
|
return errors.Wrap(err, "error preparing container for next step")
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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.
|
|
|
|
// It verifies tihis by checking the parent of the top layer of the image and the history.
|
|
|
|
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)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
// For local files, walk the file tree and check the time stamps.
|
|
|
|
timeIsGreater := false
|
|
|
|
err := filepath.Walk(item, func(path string, info os.FileInfo, err error) error {
|
|
|
|
if info.ModTime().After(*historyTime) {
|
|
|
|
timeIsGreater = true
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return false, errors.Wrapf(err, "error walking file tree %q", item)
|
|
|
|
}
|
|
|
|
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-06-09 00:55:46 +08:00
|
|
|
func (b *Executor) Commit(ctx context.Context, ib *imagebuilder.Builder, createdBy string) (string, error) {
|
|
|
|
var (
|
|
|
|
imageRef types.ImageReference
|
|
|
|
err error
|
|
|
|
)
|
2018-05-02 03:37:13 +08:00
|
|
|
|
2017-03-28 15:06:13 +08:00
|
|
|
if b.output != "" {
|
|
|
|
imageRef, err = alltransports.ParseImageName(b.output)
|
|
|
|
if err != nil {
|
2018-05-02 03:37:13 +08:00
|
|
|
candidates := util.ResolveName(b.output, "", b.systemContext, b.store)
|
|
|
|
if len(candidates) == 0 {
|
2018-06-09 00:55:46 +08:00
|
|
|
return "", errors.Errorf("error parsing target image name %q", b.output)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-05-02 03:37:13 +08:00
|
|
|
imageRef2, err2 := is.Transport.ParseStoreReference(b.store, candidates[0])
|
|
|
|
if err2 != nil {
|
2018-06-09 00:55:46 +08:00
|
|
|
return "", errors.Wrapf(err, "error parsing target image name %q", b.output)
|
2018-05-02 03:37:13 +08:00
|
|
|
}
|
|
|
|
imageRef = imageRef2
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
imageRef, err = is.Transport.ParseStoreReference(b.store, "@"+stringid.GenerateRandomID())
|
2018-05-02 03:37:13 +08:00
|
|
|
if err != nil {
|
2018-06-09 00:55:46 +08:00
|
|
|
return "", errors.Wrapf(err, "error parsing reference for image to be written")
|
2018-05-02 03:37:13 +08:00
|
|
|
}
|
2017-05-18 23:36:39 +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)
|
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-04-25 22:00:46 +08:00
|
|
|
IIDFile: b.iidfile,
|
2018-05-22 05:02:50 +08:00
|
|
|
Squash: b.squash,
|
2018-06-09 00:55:46 +08:00
|
|
|
Parent: b.builder.FromImageID,
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-05-02 01:20:16 +08:00
|
|
|
imgID, err := b.builder.Commit(ctx, imageRef, options)
|
|
|
|
if err != nil {
|
2018-06-09 00:55:46 +08:00
|
|
|
return "", err
|
2018-05-02 01:20:16 +08:00
|
|
|
}
|
|
|
|
if options.IIDFile == "" && imgID != "" {
|
2018-06-06 01:53:39 +08:00
|
|
|
fmt.Fprintf(b.out, "%s\n", imgID)
|
2018-05-02 01:20:16 +08:00
|
|
|
}
|
2018-06-09 00:55:46 +08:00
|
|
|
return imgID, 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-04-12 22:20:36 +08:00
|
|
|
func (b *Executor) Build(ctx context.Context, stages imagebuilder.Stages) error {
|
2018-02-24 18:09:25 +08:00
|
|
|
if len(stages) == 0 {
|
|
|
|
errors.New("error building: no stages to build")
|
|
|
|
}
|
|
|
|
var stageExecutor *Executor
|
|
|
|
for _, stage := range stages {
|
2018-04-11 01:35:03 +08:00
|
|
|
stageExecutor = b.withName(stage.Name, stage.Position)
|
2018-04-12 22:20:36 +08:00
|
|
|
if err := stageExecutor.Prepare(ctx, stage.Builder, stage.Node, ""); err != nil {
|
2018-02-24 18:09:25 +08:00
|
|
|
return err
|
|
|
|
}
|
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-06-09 00:55:46 +08:00
|
|
|
if err := stageExecutor.Execute(ctx, stage.Builder, stage.Node); err != nil {
|
2018-02-24 18:09:25 +08:00
|
|
|
return err
|
|
|
|
}
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-06-09 00:55:46 +08:00
|
|
|
if b.layers || b.noCache {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
_, err := stageExecutor.Commit(ctx, stages[len(stages)-1].Builder, "")
|
2018-06-14 02:09:45 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// 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) {
|
|
|
|
return stageExecutor.Delete()
|
|
|
|
}
|
|
|
|
return 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-04-12 22:20:36 +08:00
|
|
|
func BuildDockerfiles(ctx context.Context, store storage.Store, options BuildOptions, paths ...string) error {
|
2018-04-11 01:35:03 +08:00
|
|
|
if len(paths) == 0 {
|
2017-06-03 00:17:27 +08:00
|
|
|
return 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-04-11 01:35:03 +08:00
|
|
|
for _, dfile := range paths {
|
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 {
|
2017-06-02 03:23:02 +08:00
|
|
|
return errors.Wrapf(err, "error getting %q", dfile)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
if resp.ContentLength == 0 {
|
|
|
|
resp.Body.Close()
|
2017-06-03 00:17:27 +08:00
|
|
|
return errors.Errorf("no contents in %q", dfile)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-02-25 06:40:44 +08:00
|
|
|
dockerfiles = append(dockerfiles, resp.Body)
|
2017-03-28 15:06:13 +08:00
|
|
|
} else {
|
|
|
|
if !filepath.IsAbs(dfile) {
|
|
|
|
logrus.Debugf("resolving local Dockerfile %q", dfile)
|
|
|
|
dfile = filepath.Join(options.ContextDirectory, dfile)
|
|
|
|
}
|
|
|
|
logrus.Debugf("reading local Dockerfile %q", dfile)
|
2018-02-24 18:09:25 +08:00
|
|
|
contents, err := os.Open(dfile)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "error reading %q", dfile)
|
|
|
|
}
|
|
|
|
dinfo, err := contents.Stat()
|
|
|
|
if err != nil {
|
|
|
|
contents.Close()
|
|
|
|
return errors.Wrapf(err, "error reading info about %q", dfile)
|
|
|
|
}
|
|
|
|
if dinfo.Size() == 0 {
|
|
|
|
contents.Close()
|
|
|
|
return errors.Wrapf(err, "no contents in %q", dfile)
|
|
|
|
}
|
2018-02-25 06:40:44 +08:00
|
|
|
dockerfiles = append(dockerfiles, contents)
|
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 {
|
|
|
|
return 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 {
|
|
|
|
return errors.Wrapf(err, "error parsing additional Dockerfile")
|
|
|
|
}
|
|
|
|
mainNode.Children = append(mainNode.Children, additionalNode.Children...)
|
|
|
|
}
|
|
|
|
exec, err := NewExecutor(store, options)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "error creating build executor")
|
|
|
|
}
|
|
|
|
b := imagebuilder.NewBuilder(options.Args)
|
|
|
|
stages := imagebuilder.NewStages(mainNode, b)
|
2018-04-12 22:20:36 +08:00
|
|
|
return exec.Build(ctx, stages)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|