2017-02-11 00:48:15 +08:00
|
|
|
package buildah
|
|
|
|
|
|
|
|
import (
|
2018-04-12 22:20:36 +08:00
|
|
|
"context"
|
2022-07-06 17:14:06 +08:00
|
|
|
"errors"
|
2017-02-11 00:48:15 +08:00
|
|
|
"fmt"
|
2018-10-20 02:49:51 +08:00
|
|
|
"math/rand"
|
2017-03-24 04:18:40 +08:00
|
|
|
"strings"
|
2017-02-11 00:48:15 +08:00
|
|
|
|
2021-02-07 06:49:40 +08:00
|
|
|
"github.com/containers/buildah/define"
|
2021-04-11 01:44:51 +08:00
|
|
|
"github.com/containers/common/libimage"
|
2021-04-30 15:16:03 +08:00
|
|
|
"github.com/containers/common/pkg/config"
|
2019-11-01 03:18:10 +08:00
|
|
|
"github.com/containers/image/v5/image"
|
2019-10-26 05:19:30 +08:00
|
|
|
"github.com/containers/image/v5/manifest"
|
2021-08-03 16:39:06 +08:00
|
|
|
"github.com/containers/image/v5/pkg/shortnames"
|
2019-10-26 05:19:30 +08:00
|
|
|
"github.com/containers/image/v5/transports"
|
|
|
|
"github.com/containers/image/v5/types"
|
2017-05-17 23:53:28 +08:00
|
|
|
"github.com/containers/storage"
|
2022-04-01 22:37:45 +08:00
|
|
|
"github.com/containers/storage/pkg/stringid"
|
2019-11-01 03:18:10 +08:00
|
|
|
digest "github.com/opencontainers/go-digest"
|
2021-08-03 16:39:06 +08:00
|
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
2017-03-22 04:57:07 +08:00
|
|
|
"github.com/openshift/imagebuilder"
|
2017-10-10 03:05:56 +08:00
|
|
|
"github.com/sirupsen/logrus"
|
2017-02-11 00:48:15 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2017-02-11 03:45:06 +08:00
|
|
|
// BaseImageFakeName is the "name" of a source image which we interpret
|
|
|
|
// as "no image".
|
2017-03-22 04:57:07 +08:00
|
|
|
BaseImageFakeName = imagebuilder.NoBaseImageSpecifier
|
2017-02-11 00:48:15 +08:00
|
|
|
)
|
|
|
|
|
2018-01-18 20:04:09 +08:00
|
|
|
func getImageName(name string, img *storage.Image) string {
|
|
|
|
imageName := name
|
|
|
|
if len(img.Names) > 0 {
|
|
|
|
imageName = img.Names[0]
|
|
|
|
// When the image used by the container is a tagged image
|
|
|
|
// the container name might be set to the original image instead of
|
2018-10-03 22:05:46 +08:00
|
|
|
// the image given in the "from" command line.
|
2018-01-18 20:04:09 +08:00
|
|
|
// This loop is supposed to fix this.
|
|
|
|
for _, n := range img.Names {
|
|
|
|
if strings.Contains(n, name) {
|
|
|
|
imageName = n
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return imageName
|
|
|
|
}
|
|
|
|
|
2017-06-29 05:07:58 +08:00
|
|
|
func imageNamePrefix(imageName string) string {
|
|
|
|
prefix := imageName
|
2022-04-01 22:37:45 +08:00
|
|
|
if d, err := digest.Parse(imageName); err == nil {
|
|
|
|
prefix = d.Encoded()
|
|
|
|
if len(prefix) > 12 {
|
|
|
|
prefix = prefix[:12]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if stringid.ValidateID(prefix) == nil {
|
|
|
|
prefix = stringid.TruncateID(prefix)
|
|
|
|
}
|
2020-01-03 19:01:41 +08:00
|
|
|
s := strings.Split(prefix, ":")
|
2017-06-29 05:07:58 +08:00
|
|
|
if len(s) > 0 {
|
2020-01-03 19:01:41 +08:00
|
|
|
prefix = s[0]
|
2017-06-29 05:07:58 +08:00
|
|
|
}
|
2020-01-03 19:01:41 +08:00
|
|
|
s = strings.Split(prefix, "/")
|
2017-06-29 05:07:58 +08:00
|
|
|
if len(s) > 0 {
|
2020-01-03 19:01:41 +08:00
|
|
|
prefix = s[len(s)-1]
|
2017-06-29 05:07:58 +08:00
|
|
|
}
|
|
|
|
s = strings.Split(prefix, "@")
|
|
|
|
if len(s) > 0 {
|
|
|
|
prefix = s[0]
|
|
|
|
}
|
|
|
|
return prefix
|
|
|
|
}
|
|
|
|
|
2021-02-07 06:49:40 +08:00
|
|
|
func newContainerIDMappingOptions(idmapOptions *define.IDMappingOptions) storage.IDMappingOptions {
|
2018-03-13 01:53:12 +08:00
|
|
|
var options storage.IDMappingOptions
|
|
|
|
if idmapOptions != nil {
|
2022-06-20 15:29:56 +08:00
|
|
|
if idmapOptions.AutoUserNs {
|
|
|
|
options.AutoUserNs = true
|
|
|
|
options.AutoUserNsOpts = idmapOptions.AutoUserNsOpts
|
2018-03-13 01:53:12 +08:00
|
|
|
} else {
|
2022-06-20 15:29:56 +08:00
|
|
|
options.HostUIDMapping = idmapOptions.HostUIDMapping
|
|
|
|
options.HostGIDMapping = idmapOptions.HostGIDMapping
|
|
|
|
uidmap, gidmap := convertRuntimeIDMaps(idmapOptions.UIDMap, idmapOptions.GIDMap)
|
|
|
|
if len(uidmap) > 0 && len(gidmap) > 0 {
|
|
|
|
options.UIDMap = uidmap
|
|
|
|
options.GIDMap = gidmap
|
|
|
|
} else {
|
|
|
|
options.HostUIDMapping = true
|
|
|
|
options.HostGIDMapping = true
|
|
|
|
}
|
2018-03-13 01:53:12 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return options
|
|
|
|
}
|
2018-06-02 02:52:16 +08:00
|
|
|
|
2018-10-20 02:49:51 +08:00
|
|
|
func containerNameExist(name string, containers []storage.Container) bool {
|
|
|
|
for _, container := range containers {
|
|
|
|
for _, cname := range container.Names {
|
|
|
|
if cname == name {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func findUnusedContainer(name string, containers []storage.Container) string {
|
|
|
|
suffix := 1
|
|
|
|
tmpName := name
|
|
|
|
for containerNameExist(tmpName, containers) {
|
|
|
|
tmpName = fmt.Sprintf("%s-%d", name, suffix)
|
|
|
|
suffix++
|
|
|
|
}
|
|
|
|
return tmpName
|
|
|
|
}
|
|
|
|
|
2018-06-02 02:52:16 +08:00
|
|
|
func newBuilder(ctx context.Context, store storage.Store, options BuilderOptions) (*Builder, error) {
|
2019-02-02 20:29:05 +08:00
|
|
|
var (
|
|
|
|
ref types.ImageReference
|
|
|
|
img *storage.Image
|
|
|
|
err error
|
|
|
|
)
|
2021-04-11 01:44:51 +08:00
|
|
|
|
2018-06-02 02:52:16 +08:00
|
|
|
if options.FromImage == BaseImageFakeName {
|
|
|
|
options.FromImage = ""
|
|
|
|
}
|
|
|
|
|
2022-01-06 04:36:49 +08:00
|
|
|
if options.NetworkInterface == nil {
|
|
|
|
// create the network interface
|
|
|
|
// Note: It is important to do this before we pull any images/create containers.
|
|
|
|
// The default backend detection logic needs an empty store to correctly detect
|
|
|
|
// that we can use netavark, if the store was not empty it will use CNI to not break existing installs.
|
|
|
|
options.NetworkInterface, err = getNetworkInterface(store, options.CNIConfigDir, options.CNIPluginPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-14 04:03:13 +08:00
|
|
|
systemContext := getSystemContext(store, options.SystemContext, options.SignaturePolicyPath)
|
2018-06-02 02:52:16 +08:00
|
|
|
|
2018-08-04 09:02:06 +08:00
|
|
|
if options.FromImage != "" && options.FromImage != "scratch" {
|
2021-04-11 01:44:51 +08:00
|
|
|
imageRuntime, err := libimage.RuntimeFromStore(store, &libimage.RuntimeOptions{SystemContext: systemContext})
|
2018-06-02 02:52:16 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-04-11 01:44:51 +08:00
|
|
|
|
2021-04-30 15:16:03 +08:00
|
|
|
pullPolicy, err := config.ParsePullPolicy(options.PullPolicy.String())
|
2021-04-11 01:44:51 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Note: options.Format does *not* relate to the image we're
|
|
|
|
// about to pull (see tests/digests.bats). So we're not
|
|
|
|
// forcing a MIMEType in the pullOptions below.
|
|
|
|
pullOptions := libimage.PullOptions{}
|
|
|
|
pullOptions.RetryDelay = &options.PullRetryDelay
|
|
|
|
pullOptions.OciDecryptConfig = options.OciDecryptConfig
|
|
|
|
pullOptions.SignaturePolicyPath = options.SignaturePolicyPath
|
|
|
|
pullOptions.Writer = options.ReportWriter
|
2022-02-22 21:24:48 +08:00
|
|
|
pullOptions.DestinationLookupReferenceFunc = cacheLookupReferenceFunc(options.BlobDirectory, types.PreserveOriginal)
|
2021-04-11 01:44:51 +08:00
|
|
|
|
|
|
|
maxRetries := uint(options.MaxPullRetries)
|
|
|
|
pullOptions.MaxRetries = &maxRetries
|
|
|
|
|
|
|
|
pulledImages, err := imageRuntime.Pull(ctx, options.FromImage, pullPolicy, &pullOptions)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(pulledImages) > 0 {
|
|
|
|
img = pulledImages[0].StorageImage()
|
|
|
|
ref, err = pulledImages[0].StorageReference()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2017-06-29 05:07:58 +08:00
|
|
|
}
|
2021-04-11 01:44:51 +08:00
|
|
|
|
2019-11-01 03:18:10 +08:00
|
|
|
imageSpec := options.FromImage
|
2017-06-29 05:07:58 +08:00
|
|
|
imageID := ""
|
2019-07-02 05:14:07 +08:00
|
|
|
imageDigest := ""
|
2018-06-09 00:55:46 +08:00
|
|
|
topLayer := ""
|
2017-06-29 05:07:58 +08:00
|
|
|
if img != nil {
|
2019-11-01 03:18:10 +08:00
|
|
|
imageSpec = getImageName(imageNamePrefix(imageSpec), img)
|
2017-03-16 05:19:29 +08:00
|
|
|
imageID = img.ID
|
2018-06-09 00:55:46 +08:00
|
|
|
topLayer = img.TopLayer
|
2017-06-29 05:07:58 +08:00
|
|
|
}
|
2019-11-01 03:18:10 +08:00
|
|
|
var src types.Image
|
2018-06-12 06:31:04 +08:00
|
|
|
if ref != nil {
|
2019-11-01 03:18:10 +08:00
|
|
|
srcSrc, err := ref.NewImageSource(ctx, systemContext)
|
2018-06-12 06:31:04 +08:00
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("instantiating image for %q: %w", transports.ImageName(ref), err)
|
2018-06-12 06:31:04 +08:00
|
|
|
}
|
2019-11-01 03:18:10 +08:00
|
|
|
defer srcSrc.Close()
|
|
|
|
manifestBytes, manifestType, err := srcSrc.GetManifest(ctx, nil)
|
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("loading image manifest for %q: %w", transports.ImageName(ref), err)
|
2019-11-01 03:18:10 +08:00
|
|
|
}
|
|
|
|
if manifestDigest, err := manifest.Digest(manifestBytes); err == nil {
|
|
|
|
imageDigest = manifestDigest.String()
|
|
|
|
}
|
|
|
|
var instanceDigest *digest.Digest
|
|
|
|
if manifest.MIMETypeIsMultiImage(manifestType) {
|
|
|
|
list, err := manifest.ListFromBlob(manifestBytes, manifestType)
|
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("parsing image manifest for %q as list: %w", transports.ImageName(ref), err)
|
2019-07-02 05:14:07 +08:00
|
|
|
}
|
2019-11-01 03:18:10 +08:00
|
|
|
instance, err := list.ChooseInstance(systemContext)
|
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("finding an appropriate image in manifest list %q: %w", transports.ImageName(ref), err)
|
2019-11-01 03:18:10 +08:00
|
|
|
}
|
|
|
|
instanceDigest = &instance
|
|
|
|
}
|
|
|
|
src, err = image.FromUnparsedImage(ctx, systemContext, image.UnparsedInstance(srcSrc, instanceDigest))
|
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("instantiating image for %q instance %q: %w", transports.ImageName(ref), instanceDigest, err)
|
2019-07-02 05:14:07 +08:00
|
|
|
}
|
2018-06-12 06:31:04 +08:00
|
|
|
}
|
|
|
|
|
2017-07-29 05:29:37 +08:00
|
|
|
name := "working-container"
|
2021-12-15 06:11:32 +08:00
|
|
|
if options.ContainerSuffix != "" {
|
|
|
|
name = options.ContainerSuffix
|
|
|
|
}
|
2017-07-29 05:29:37 +08:00
|
|
|
if options.Container != "" {
|
|
|
|
name = options.Container
|
|
|
|
} else {
|
2019-11-01 03:18:10 +08:00
|
|
|
if imageSpec != "" {
|
|
|
|
name = imageNamePrefix(imageSpec) + "-" + name
|
2017-07-29 05:29:37 +08:00
|
|
|
}
|
|
|
|
}
|
2018-10-20 02:49:51 +08:00
|
|
|
var container *storage.Container
|
|
|
|
tmpName := name
|
|
|
|
if options.Container == "" {
|
|
|
|
containers, err := store.Containers()
|
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("unable to check for container names: %w", err)
|
2018-10-20 02:49:51 +08:00
|
|
|
}
|
|
|
|
tmpName = findUnusedContainer(tmpName, containers)
|
|
|
|
}
|
2017-06-29 05:07:58 +08:00
|
|
|
|
2018-10-20 02:49:51 +08:00
|
|
|
conflict := 100
|
2019-02-23 10:08:09 +08:00
|
|
|
for {
|
2022-01-09 20:29:26 +08:00
|
|
|
|
|
|
|
var flags map[string]interface{}
|
|
|
|
// check if we have predefined ProcessLabel and MountLabel
|
|
|
|
// this could be true if this is another stage in a build
|
|
|
|
if options.ProcessLabel != "" && options.MountLabel != "" {
|
|
|
|
flags = map[string]interface{}{
|
|
|
|
"ProcessLabel": options.ProcessLabel,
|
|
|
|
"MountLabel": options.MountLabel,
|
|
|
|
}
|
|
|
|
}
|
2018-10-20 02:49:51 +08:00
|
|
|
coptions := storage.ContainerOptions{
|
|
|
|
LabelOpts: options.CommonBuildOpts.LabelOpts,
|
|
|
|
IDMappingOptions: newContainerIDMappingOptions(options.IDMappingOptions),
|
2022-01-09 20:29:26 +08:00
|
|
|
Flags: flags,
|
2021-02-12 17:31:29 +08:00
|
|
|
Volatile: true,
|
2018-10-20 02:49:51 +08:00
|
|
|
}
|
|
|
|
container, err = store.CreateContainer("", []string{tmpName}, imageID, "", "", &coptions)
|
|
|
|
if err == nil {
|
2018-09-13 02:34:05 +08:00
|
|
|
name = tmpName
|
2018-10-20 02:49:51 +08:00
|
|
|
break
|
2018-09-13 02:34:05 +08:00
|
|
|
}
|
2022-07-06 17:14:06 +08:00
|
|
|
if !errors.Is(err, storage.ErrDuplicateName) || options.Container != "" {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("creating container: %w", err)
|
2018-10-20 02:49:51 +08:00
|
|
|
}
|
|
|
|
tmpName = fmt.Sprintf("%s-%d", name, rand.Int()%conflict)
|
|
|
|
conflict = conflict * 10
|
2018-09-13 02:34:05 +08:00
|
|
|
}
|
2017-02-11 00:48:15 +08:00
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
2018-10-18 04:58:32 +08:00
|
|
|
if err2 := store.DeleteContainer(container.ID); err2 != nil {
|
2017-02-11 00:48:15 +08:00
|
|
|
logrus.Errorf("error deleting container %q: %v", container.ID, err2)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2018-03-08 07:11:43 +08:00
|
|
|
uidmap, gidmap := convertStorageIDMaps(container.UIDMap, container.GIDMap)
|
2018-06-27 04:35:43 +08:00
|
|
|
|
|
|
|
defaultNamespaceOptions, err := DefaultNamespaceOptions()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
namespaceOptions := defaultNamespaceOptions
|
2018-03-08 07:11:43 +08:00
|
|
|
namespaceOptions.AddOrReplace(options.NamespaceOptions...)
|
2017-10-20 05:47:15 +08:00
|
|
|
|
2021-08-03 16:39:06 +08:00
|
|
|
// Set the base-image annotations as suggested by the OCI image spec.
|
|
|
|
imageAnnotations := map[string]string{}
|
|
|
|
imageAnnotations[v1.AnnotationBaseImageDigest] = imageDigest
|
|
|
|
if !shortnames.IsShortName(imageSpec) {
|
|
|
|
// If the base image could be resolved to a fully-qualified
|
|
|
|
// image name, let's set it.
|
|
|
|
imageAnnotations[v1.AnnotationBaseImageName] = imageSpec
|
|
|
|
}
|
|
|
|
|
2017-02-11 00:48:15 +08:00
|
|
|
builder := &Builder{
|
2017-11-08 06:44:24 +08:00
|
|
|
store: store,
|
|
|
|
Type: containerType,
|
2019-11-01 03:18:10 +08:00
|
|
|
FromImage: imageSpec,
|
2017-11-08 06:44:24 +08:00
|
|
|
FromImageID: imageID,
|
2019-07-02 05:14:07 +08:00
|
|
|
FromImageDigest: imageDigest,
|
2022-12-22 03:51:59 +08:00
|
|
|
GroupAdd: options.GroupAdd,
|
2017-11-08 06:44:24 +08:00
|
|
|
Container: name,
|
|
|
|
ContainerID: container.ID,
|
2021-08-03 16:39:06 +08:00
|
|
|
ImageAnnotations: imageAnnotations,
|
2017-11-08 06:44:24 +08:00
|
|
|
ImageCreatedBy: "",
|
2018-10-20 02:49:51 +08:00
|
|
|
ProcessLabel: container.ProcessLabel(),
|
|
|
|
MountLabel: container.MountLabel(),
|
2017-11-08 06:44:24 +08:00
|
|
|
DefaultMountsFilePath: options.DefaultMountsFilePath,
|
2018-05-12 01:00:14 +08:00
|
|
|
Isolation: options.Isolation,
|
2018-03-08 07:11:43 +08:00
|
|
|
NamespaceOptions: namespaceOptions,
|
2018-04-14 06:20:25 +08:00
|
|
|
ConfigureNetwork: options.ConfigureNetwork,
|
|
|
|
CNIPluginPath: options.CNIPluginPath,
|
|
|
|
CNIConfigDir: options.CNIConfigDir,
|
2021-02-07 06:49:40 +08:00
|
|
|
IDMappingOptions: define.IDMappingOptions{
|
2018-03-08 07:11:43 +08:00
|
|
|
HostUIDMapping: len(uidmap) == 0,
|
|
|
|
HostGIDMapping: len(uidmap) == 0,
|
|
|
|
UIDMap: uidmap,
|
|
|
|
GIDMap: gidmap,
|
|
|
|
},
|
2022-01-06 04:36:49 +08:00
|
|
|
Capabilities: copyStringSlice(options.Capabilities),
|
|
|
|
CommonBuildOpts: options.CommonBuildOpts,
|
|
|
|
TopLayer: topLayer,
|
2022-05-04 01:12:01 +08:00
|
|
|
Args: copyStringStringMap(options.Args),
|
2022-01-06 04:36:49 +08:00
|
|
|
Format: options.Format,
|
|
|
|
TempVolumes: map[string]bool{},
|
|
|
|
Devices: options.Devices,
|
|
|
|
Logger: options.Logger,
|
|
|
|
NetworkInterface: options.NetworkInterface,
|
2017-02-11 00:48:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if options.Mount {
|
2018-10-20 02:49:51 +08:00
|
|
|
_, err = builder.Mount(container.MountLabel())
|
2017-02-11 00:48:15 +08:00
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("mounting build container %q: %w", builder.ContainerID, err)
|
2017-02-11 00:48:15 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
bud: teach --platform to take a list
Add a pkg/parse.PlatformsFromOptions() which understands a "variant"
value as an optional third value in an OS/ARCH[/VARIANT] argument value,
which accepts a comma-separated list of them, and which returns a list
of platforms.
Teach "from" and "pull" about the --platform option and add integration
tests for them, warning if --platform was given multiple values.
Add a define.BuildOptions.JobSemaphore which an imagebuildah executor
will use in preference to one that it might allocate for itself.
In main(), allocate a JobSemaphore if the number of jobs is not 0 (which
we treat as "unlimited", and continue to allow executors to do).
In addManifest(), take a lock on the manifest list's image ID so that we
don't overwrite changes that another thread might be making while we're
attempting to make changes to it. In main(), create an empty list if
the list doesn't already exist before we start down this path, so that
we don't get two threads trying to create that manifest list at the same
time later on. Two processes could still try to create the same list
twice, but it's an incremental improvement.
Finally, if we've been given multiple platforms to build for, run their
builds concurrently and gather up their results.
Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
2021-06-22 22:52:49 +08:00
|
|
|
if err := builder.initConfig(ctx, src, systemContext); err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("preparing image configuration: %w", err)
|
2018-06-12 05:43:21 +08:00
|
|
|
}
|
2017-02-11 00:48:15 +08:00
|
|
|
err = builder.Save()
|
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("saving builder state for container %q: %w", builder.ContainerID, err)
|
2017-02-11 00:48:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return builder, nil
|
|
|
|
}
|