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"
|
2022-07-06 17:14:06 +08:00
|
|
|
"errors"
|
2020-05-19 20:20:14 +08:00
|
|
|
"fmt"
|
2017-03-28 15:06:13 +08:00
|
|
|
"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"
|
2021-09-28 05:26:01 +08:00
|
|
|
"strconv"
|
2017-03-28 15:06:13 +08:00
|
|
|
"strings"
|
2021-08-11 06:11:15 +08:00
|
|
|
"sync"
|
2017-03-28 15:06:13 +08:00
|
|
|
|
2021-09-28 05:26:01 +08:00
|
|
|
"github.com/containerd/containerd/platforms"
|
2021-02-07 06:49:40 +08:00
|
|
|
"github.com/containers/buildah/define"
|
2022-09-26 14:39:06 +08:00
|
|
|
internalUtil "github.com/containers/buildah/internal/util"
|
2021-05-12 03:05:48 +08:00
|
|
|
"github.com/containers/buildah/util"
|
2021-08-11 06:11:15 +08:00
|
|
|
"github.com/containers/common/libimage"
|
2020-02-08 01:54:18 +08:00
|
|
|
"github.com/containers/common/pkg/config"
|
2021-09-28 05:26:01 +08:00
|
|
|
"github.com/containers/image/v5/docker"
|
2019-10-26 05:19:30 +08:00
|
|
|
"github.com/containers/image/v5/docker/reference"
|
2021-08-11 06:11:15 +08:00
|
|
|
"github.com/containers/image/v5/manifest"
|
2021-09-28 05:26:01 +08:00
|
|
|
"github.com/containers/image/v5/pkg/shortnames"
|
2021-08-11 06:11:15 +08:00
|
|
|
istorage "github.com/containers/image/v5/storage"
|
2021-08-11 06:11:15 +08:00
|
|
|
"github.com/containers/image/v5/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"
|
2021-08-11 06:11:15 +08:00
|
|
|
"github.com/hashicorp/go-multierror"
|
2022-05-14 18:58:34 +08:00
|
|
|
"github.com/mattn/go-shellwords"
|
2021-08-11 06:11:15 +08:00
|
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
2019-10-02 04:03:57 +08:00
|
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
2017-03-28 15:06:13 +08:00
|
|
|
"github.com/openshift/imagebuilder"
|
2017-10-10 03:05:56 +08:00
|
|
|
"github.com/sirupsen/logrus"
|
2021-08-11 06:11:15 +08:00
|
|
|
"golang.org/x/sync/semaphore"
|
2017-03-28 15:06:13 +08:00
|
|
|
)
|
|
|
|
|
2017-04-11 02:15:30 +08:00
|
|
|
const (
|
2021-02-07 06:49:40 +08:00
|
|
|
PullIfMissing = define.PullIfMissing
|
|
|
|
PullAlways = define.PullAlways
|
|
|
|
PullIfNewer = define.PullIfNewer
|
|
|
|
PullNever = define.PullNever
|
2017-04-11 22:27:05 +08:00
|
|
|
|
|
|
|
Gzip = archive.Gzip
|
|
|
|
Bzip2 = archive.Bzip2
|
|
|
|
Xz = archive.Xz
|
2019-07-09 05:50:33 +08:00
|
|
|
Zstd = archive.Zstd
|
2017-04-11 22:27:05 +08:00
|
|
|
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.
|
2021-07-21 04:23:25 +08:00
|
|
|
type Mount = specs.Mount
|
2017-04-11 22:27:05 +08:00
|
|
|
|
2021-03-02 02:07:58 +08:00
|
|
|
type BuildOptions = define.BuildOptions
|
2017-03-28 15:06:13 +08:00
|
|
|
|
|
|
|
// BuildDockerfiles parses a set of one or more Dockerfiles (which may be
|
2021-08-11 06:11:15 +08:00
|
|
|
// URLs), creates one or more new Executors, and then runs
|
|
|
|
// Prepare/Execute/Commit/Delete over the entire set of instructions.
|
|
|
|
// If the Manifest option is set, returns the ID of the manifest list, else it
|
|
|
|
// returns the ID of the built image, and if a name was assigned to it, a
|
|
|
|
// canonical reference for that image.
|
|
|
|
func BuildDockerfiles(ctx context.Context, store storage.Store, options define.BuildOptions, paths ...string) (id string, ref reference.Canonical, err error) {
|
2021-10-14 22:58:24 +08:00
|
|
|
if options.CommonBuildOpts == nil {
|
|
|
|
options.CommonBuildOpts = &define.CommonBuildOptions{}
|
|
|
|
}
|
|
|
|
|
2018-04-11 01:35:03 +08:00
|
|
|
if len(paths) == 0 {
|
2022-09-18 18:36:08 +08:00
|
|
|
return "", nil, errors.New("building: no dockerfiles specified")
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
if len(options.Platforms) > 1 && options.IIDFile != "" {
|
2022-07-06 17:14:06 +08:00
|
|
|
return "", nil, fmt.Errorf("building multiple images, but iidfile %q can only be used to store one image ID", options.IIDFile)
|
2021-08-11 06:11:15 +08:00
|
|
|
}
|
|
|
|
|
2021-05-08 01:38:44 +08:00
|
|
|
logger := logrus.New()
|
|
|
|
if options.Err != nil {
|
|
|
|
logger.SetOutput(options.Err)
|
|
|
|
} else {
|
|
|
|
logger.SetOutput(os.Stderr)
|
|
|
|
}
|
|
|
|
logger.SetLevel(logrus.GetLevel())
|
|
|
|
|
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
|
|
|
|
2021-05-12 03:05:48 +08:00
|
|
|
for _, tag := range append([]string{options.Output}, options.AdditionalTags...) {
|
|
|
|
if tag == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if _, err := util.VerifyTagName(tag); err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return "", nil, fmt.Errorf("tag %s: %w", tag, err)
|
2021-05-12 03:05:48 +08:00
|
|
|
}
|
|
|
|
}
|
2021-08-11 06:11:15 +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://") {
|
2021-08-11 06:11:15 +08:00
|
|
|
logger.Debugf("reading remote Dockerfile %q", dfile)
|
2017-03-28 15:06:13 +08:00
|
|
|
resp, err := http.Get(dfile)
|
|
|
|
if err != nil {
|
2020-10-15 17:16:50 +08:00
|
|
|
return "", nil, err
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
|
|
|
if resp.ContentLength == 0 {
|
|
|
|
resp.Body.Close()
|
2022-07-06 17:14:06 +08:00
|
|
|
return "", nil, fmt.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-10-28 03:47:03 +08:00
|
|
|
dinfo, err := os.Stat(dfile)
|
2020-11-09 11:14:56 +08:00
|
|
|
if err != nil {
|
|
|
|
// If the Dockerfile isn't available, try again with
|
|
|
|
// context directory prepended (if not prepended yet).
|
2020-10-13 09:30:01 +08:00
|
|
|
if !strings.HasPrefix(dfile, options.ContextDirectory) {
|
|
|
|
dfile = filepath.Join(options.ContextDirectory, dfile)
|
2020-11-09 11:14:56 +08:00
|
|
|
dinfo, err = os.Stat(dfile)
|
2020-10-13 09:30:01 +08:00
|
|
|
}
|
2018-10-28 03:47:03 +08:00
|
|
|
}
|
2020-11-09 11:14:56 +08:00
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
|
2021-03-15 19:52:22 +08:00
|
|
|
var contents *os.File
|
2022-07-07 13:33:04 +08:00
|
|
|
// If given a directory error out since `-f` does not supports path to directory
|
2018-10-28 03:47:03 +08:00
|
|
|
if dinfo.Mode().IsDir() {
|
2022-07-07 13:33:04 +08:00
|
|
|
return "", nil, fmt.Errorf("containerfile: %q cannot be path to a directory", dfile)
|
2018-10-28 03:47:03 +08:00
|
|
|
}
|
2022-07-07 13:33:04 +08:00
|
|
|
contents, err = os.Open(dfile)
|
2018-02-24 18:09:25 +08:00
|
|
|
if err != nil {
|
2020-10-15 17:16:50 +08:00
|
|
|
return "", nil, err
|
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()
|
2022-09-18 18:36:08 +08:00
|
|
|
return "", nil, fmt.Errorf("reading info about %q: %w", dfile, err)
|
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()
|
2022-07-06 17:14:06 +08:00
|
|
|
return "", nil, fmt.Errorf("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") {
|
2022-05-14 18:58:34 +08:00
|
|
|
pData, err := preprocessContainerfileContents(logger, dfile, data, options.ContextDirectory, options.CPPFlags)
|
2018-08-01 18:31:02 +08:00
|
|
|
if err != nil {
|
2018-10-12 04:58:04 +08:00
|
|
|
return "", nil, err
|
2018-08-01 18:31:02 +08:00
|
|
|
}
|
2021-06-10 01:23:12 +08:00
|
|
|
data = ioutil.NopCloser(pData)
|
2018-08-01 18:31:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
dockerfiles = append(dockerfiles, data)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2018-11-17 06:43:48 +08:00
|
|
|
|
2021-08-11 06:11:15 +08:00
|
|
|
var files [][]byte
|
|
|
|
for _, dockerfile := range dockerfiles {
|
|
|
|
var b bytes.Buffer
|
|
|
|
if _, err := b.ReadFrom(dockerfile); err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
files = append(files, b.Bytes())
|
|
|
|
}
|
|
|
|
|
2022-01-28 07:08:53 +08:00
|
|
|
if options.JobSemaphore == nil {
|
|
|
|
if options.Jobs != nil {
|
|
|
|
if *options.Jobs < 0 {
|
2022-09-18 18:36:08 +08:00
|
|
|
return "", nil, errors.New("building: invalid value for jobs. It must be a positive integer")
|
2022-01-28 07:08:53 +08:00
|
|
|
}
|
|
|
|
if *options.Jobs > 0 {
|
|
|
|
options.JobSemaphore = semaphore.NewWeighted(int64(*options.Jobs))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
options.JobSemaphore = semaphore.NewWeighted(1)
|
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
}
|
|
|
|
|
2021-08-11 06:11:15 +08:00
|
|
|
manifestList := options.Manifest
|
|
|
|
options.Manifest = ""
|
|
|
|
type instance struct {
|
|
|
|
v1.Platform
|
2022-07-18 19:12:57 +08:00
|
|
|
ID string
|
|
|
|
Ref reference.Canonical
|
2021-08-11 06:11:15 +08:00
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
var instances []instance
|
|
|
|
var instancesLock sync.Mutex
|
2021-08-11 06:11:15 +08:00
|
|
|
|
|
|
|
var builds multierror.Group
|
|
|
|
if options.SystemContext == nil {
|
|
|
|
options.SystemContext = &types.SystemContext{}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(options.Platforms) == 0 {
|
|
|
|
options.Platforms = append(options.Platforms, struct{ OS, Arch, Variant string }{
|
|
|
|
OS: options.SystemContext.OSChoice,
|
|
|
|
Arch: options.SystemContext.ArchitectureChoice,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-09-28 05:26:01 +08:00
|
|
|
if options.AllPlatforms {
|
2022-05-10 18:11:37 +08:00
|
|
|
if options.AdditionalBuildContexts == nil {
|
|
|
|
options.AdditionalBuildContexts = make(map[string]*define.AdditionalBuildContext)
|
|
|
|
}
|
|
|
|
options.Platforms, err = platformsForBaseImages(ctx, logger, paths, files, options.From, options.Args, options.AdditionalBuildContexts, options.SystemContext)
|
2021-09-28 05:26:01 +08:00
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-11 06:11:15 +08:00
|
|
|
systemContext := options.SystemContext
|
|
|
|
for _, platform := range options.Platforms {
|
|
|
|
platformContext := *systemContext
|
2022-09-26 14:39:06 +08:00
|
|
|
platformSpec := internalUtil.NormalizePlatform(v1.Platform{
|
2021-09-28 05:26:01 +08:00
|
|
|
OS: platform.OS,
|
|
|
|
Architecture: platform.Arch,
|
|
|
|
Variant: platform.Variant,
|
|
|
|
})
|
2022-09-26 14:39:06 +08:00
|
|
|
// internalUtil.NormalizePlatform converts an empty os value to GOOS
|
2022-01-10 22:27:26 +08:00
|
|
|
// so we have to check the original value here to not overwrite the default for no reason
|
|
|
|
if platform.OS != "" {
|
2021-09-28 05:26:01 +08:00
|
|
|
platformContext.OSChoice = platformSpec.OS
|
2022-01-10 22:27:26 +08:00
|
|
|
}
|
|
|
|
if platform.Arch != "" {
|
2021-09-28 05:26:01 +08:00
|
|
|
platformContext.ArchitectureChoice = platformSpec.Architecture
|
|
|
|
platformContext.VariantChoice = platformSpec.Variant
|
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
platformOptions := options
|
|
|
|
platformOptions.SystemContext = &platformContext
|
2021-09-28 05:26:01 +08:00
|
|
|
platformOptions.OS = platformContext.OSChoice
|
|
|
|
platformOptions.Architecture = platformContext.ArchitectureChoice
|
2021-08-11 06:11:15 +08:00
|
|
|
logPrefix := ""
|
|
|
|
if len(options.Platforms) > 1 {
|
2021-09-28 05:26:01 +08:00
|
|
|
logPrefix = "[" + platforms.Format(platformSpec) + "] "
|
2021-08-11 06:11:15 +08:00
|
|
|
}
|
2022-05-04 12:46:07 +08:00
|
|
|
// Deep copy args to prevent concurrent read/writes over Args.
|
|
|
|
argsCopy := make(map[string]string)
|
|
|
|
for key, value := range options.Args {
|
|
|
|
argsCopy[key] = value
|
|
|
|
}
|
|
|
|
platformOptions.Args = argsCopy
|
2021-08-11 06:11:15 +08:00
|
|
|
builds.Go(func() error {
|
2022-06-06 14:33:15 +08:00
|
|
|
loggerPerPlatform := logger
|
|
|
|
if platformOptions.LogFile != "" && platformOptions.LogSplitByPlatform {
|
|
|
|
logFile := platformOptions.LogFile + "_" + platformOptions.OS + "_" + platformOptions.Architecture
|
2022-09-27 10:31:20 +08:00
|
|
|
f, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
|
2022-06-06 14:33:15 +08:00
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return fmt.Errorf("opening logfile: %q: %w", logFile, err)
|
2022-06-06 14:33:15 +08:00
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
loggerPerPlatform = logrus.New()
|
|
|
|
loggerPerPlatform.SetOutput(f)
|
|
|
|
loggerPerPlatform.SetLevel(logrus.GetLevel())
|
|
|
|
stdout := f
|
|
|
|
stderr := f
|
|
|
|
reporter := f
|
|
|
|
platformOptions.Out = stdout
|
|
|
|
platformOptions.ReportWriter = reporter
|
|
|
|
platformOptions.Err = stderr
|
|
|
|
}
|
|
|
|
thisID, thisRef, err := buildDockerfilesOnce(ctx, store, loggerPerPlatform, logPrefix, platformOptions, paths, files)
|
2021-08-11 06:11:15 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
instancesLock.Lock()
|
|
|
|
instances = append(instances, instance{
|
2021-09-28 05:26:01 +08:00
|
|
|
ID: thisID,
|
2022-07-18 19:12:57 +08:00
|
|
|
Ref: thisRef,
|
2021-09-28 05:26:01 +08:00
|
|
|
Platform: platformSpec,
|
2021-08-11 06:11:15 +08:00
|
|
|
})
|
|
|
|
instancesLock.Unlock()
|
2021-08-11 06:11:15 +08:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if merr := builds.Wait(); merr != nil {
|
|
|
|
if merr.Len() == 1 {
|
|
|
|
return "", nil, merr.Errors[0]
|
|
|
|
}
|
|
|
|
return "", nil, merr.ErrorOrNil()
|
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
|
2022-09-27 18:15:48 +08:00
|
|
|
// Reasons for this id, ref assignment w.r.t to use-case:
|
2022-07-18 19:12:57 +08:00
|
|
|
//
|
|
|
|
// * Single-platform build: On single platform build we only
|
|
|
|
// have one built instance i.e on indice 0 of built instances,
|
|
|
|
// so assign that.
|
|
|
|
//
|
|
|
|
// * Multi-platform build with manifestList: If this is a build for
|
|
|
|
// multiple platforms ( more than one platform ) and --manifest
|
|
|
|
// option then this assignment is insignificant since it will be
|
2022-09-27 18:15:48 +08:00
|
|
|
// overridden anyways with the id and ref of manifest list later in
|
2022-07-18 19:12:57 +08:00
|
|
|
// in this code.
|
|
|
|
//
|
|
|
|
// * Multi-platform build without manifest list: If this is a build for
|
|
|
|
// multiple platforms without --manifest then we are free to return
|
|
|
|
// id and ref of any one of the image in the instance list so always
|
|
|
|
// return indice 0 for predictable output instead returning the id and
|
|
|
|
// ref of the go routine which completed at last.
|
|
|
|
id, ref = instances[0].ID, instances[0].Ref
|
|
|
|
|
2021-08-11 06:11:15 +08:00
|
|
|
if manifestList != "" {
|
2021-08-11 06:11:15 +08:00
|
|
|
rt, err := libimage.RuntimeFromStore(store, nil)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
// Create the manifest list ourselves, so that it's not in a
|
|
|
|
// partially-populated state at any point if we're creating it
|
|
|
|
// fresh.
|
|
|
|
list, err := rt.LookupManifestList(manifestList)
|
2022-07-06 17:14:06 +08:00
|
|
|
if err != nil && errors.Is(err, storage.ErrImageUnknown) {
|
2021-08-11 06:11:15 +08:00
|
|
|
list, err = rt.CreateManifestList(manifestList)
|
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
// Add each instance to the list in turn.
|
|
|
|
storeTransportName := istorage.Transport.Name()
|
|
|
|
for _, instance := range instances {
|
|
|
|
instanceDigest, err := list.Add(ctx, storeTransportName+":"+instance.ID, nil)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
err = list.AnnotateInstance(instanceDigest, &libimage.ManifestListAnnotateOptions{
|
|
|
|
Architecture: instance.Architecture,
|
|
|
|
OS: instance.OS,
|
|
|
|
Variant: instance.Variant,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
id, ref = list.ID(), nil
|
2021-08-11 06:11:15 +08:00
|
|
|
// Put together a canonical reference
|
|
|
|
storeRef, err := istorage.Transport.NewStoreReference(store, nil, list.ID())
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
imgSource, err := storeRef.NewImageSource(ctx, nil)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
defer imgSource.Close()
|
|
|
|
manifestBytes, _, err := imgSource.GetManifest(ctx, nil)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
manifestDigest, err := manifest.Digest(manifestBytes)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
img, err := store.Image(id)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
for _, name := range img.Names {
|
|
|
|
if named, err := reference.ParseNamed(name); err == nil {
|
|
|
|
if r, err := reference.WithDigest(reference.TrimNamed(named), manifestDigest); err == nil {
|
|
|
|
ref = r
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
}
|
2021-08-11 06:11:15 +08:00
|
|
|
|
2021-08-11 06:11:15 +08:00
|
|
|
return id, ref, nil
|
|
|
|
}
|
|
|
|
|
2022-09-13 14:35:34 +08:00
|
|
|
func buildDockerfilesOnce(ctx context.Context, store storage.Store, logger *logrus.Logger, logPrefix string, options define.BuildOptions, containerFiles []string, dockerfilecontents [][]byte) (string, reference.Canonical, error) {
|
2021-08-11 06:11:15 +08:00
|
|
|
mainNode, err := imagebuilder.ParseDockerfile(bytes.NewReader(dockerfilecontents[0]))
|
2018-02-25 06:40:44 +08:00
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return "", nil, fmt.Errorf("parsing main Dockerfile: %s: %w", containerFiles[0], err)
|
2017-03-28 15:06:13 +08:00
|
|
|
}
|
2020-05-19 20:20:14 +08:00
|
|
|
|
2022-05-03 02:32:36 +08:00
|
|
|
// --platform was explicitly selected for this build
|
|
|
|
// so set correct TARGETPLATFORM in args if it is not
|
|
|
|
// already selected by the user.
|
|
|
|
if options.SystemContext.OSChoice != "" && options.SystemContext.ArchitectureChoice != "" {
|
|
|
|
// os component from --platform string populates TARGETOS
|
|
|
|
// buildkit parity: give priority to user's `--build-arg`
|
|
|
|
if _, ok := options.Args["TARGETOS"]; !ok {
|
|
|
|
options.Args["TARGETOS"] = options.SystemContext.OSChoice
|
|
|
|
}
|
|
|
|
// arch component from --platform string populates TARGETARCH
|
|
|
|
// buildkit parity: give priority to user's `--build-arg`
|
|
|
|
if _, ok := options.Args["TARGETARCH"]; !ok {
|
|
|
|
options.Args["TARGETARCH"] = options.SystemContext.ArchitectureChoice
|
|
|
|
}
|
|
|
|
// variant component from --platform string populates TARGETVARIANT
|
|
|
|
// buildkit parity: give priority to user's `--build-arg`
|
|
|
|
if _, ok := options.Args["TARGETVARIANT"]; !ok {
|
|
|
|
if options.SystemContext.VariantChoice != "" {
|
|
|
|
options.Args["TARGETVARIANT"] = options.SystemContext.VariantChoice
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// buildkit parity: give priority to user's `--build-arg`
|
|
|
|
if _, ok := options.Args["TARGETPLATFORM"]; !ok {
|
|
|
|
// buildkit parity: TARGETPLATFORM should be always created
|
|
|
|
// from SystemContext and not `TARGETOS` and `TARGETARCH` because
|
|
|
|
// users can always override values of `TARGETOS` and `TARGETARCH`
|
|
|
|
// but `TARGETPLATFORM` should be set independent of those values.
|
|
|
|
options.Args["TARGETPLATFORM"] = options.SystemContext.OSChoice + "/" + options.SystemContext.ArchitectureChoice
|
|
|
|
if options.SystemContext.VariantChoice != "" {
|
|
|
|
options.Args["TARGETPLATFORM"] = options.Args["TARGETPLATFORM"] + "/" + options.SystemContext.VariantChoice
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-11 06:11:15 +08:00
|
|
|
for i, d := range dockerfilecontents[1:] {
|
|
|
|
additionalNode, err := imagebuilder.ParseDockerfile(bytes.NewReader(d))
|
2018-02-25 06:40:44 +08:00
|
|
|
if err != nil {
|
2022-09-13 14:35:34 +08:00
|
|
|
containerFiles := containerFiles[1:]
|
2022-09-18 18:36:08 +08:00
|
|
|
return "", nil, fmt.Errorf("parsing additional Dockerfile %s: %w", containerFiles[i], err)
|
2018-02-25 06:40:44 +08:00
|
|
|
}
|
|
|
|
mainNode.Children = append(mainNode.Children, additionalNode.Children...)
|
|
|
|
}
|
2021-09-14 18:44:27 +08:00
|
|
|
|
|
|
|
// Check if any modifications done to labels
|
|
|
|
// add them to node-layer so it becomes regular
|
|
|
|
// layer.
|
|
|
|
// Reason: Docker adds label modification as
|
|
|
|
// last step which can be processed as regular
|
|
|
|
// steps and if no modification is done to layers
|
|
|
|
// its easier to re-use cached layers.
|
|
|
|
if len(options.Labels) > 0 {
|
|
|
|
for _, labelSpec := range options.Labels {
|
|
|
|
label := strings.SplitN(labelSpec, "=", 2)
|
|
|
|
labelLine := ""
|
|
|
|
key := label[0]
|
|
|
|
value := ""
|
|
|
|
if len(label) > 1 {
|
|
|
|
value = label[1]
|
|
|
|
}
|
|
|
|
// check from only empty key since docker supports empty value
|
|
|
|
if key != "" {
|
|
|
|
labelLine = fmt.Sprintf("LABEL %q=%q\n", key, value)
|
|
|
|
additionalNode, err := imagebuilder.ParseDockerfile(strings.NewReader(labelLine))
|
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return "", nil, fmt.Errorf("while adding additional LABEL steps: %w", err)
|
2021-09-14 18:44:27 +08:00
|
|
|
}
|
|
|
|
mainNode.Children = append(mainNode.Children, additionalNode.Children...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-13 14:35:34 +08:00
|
|
|
exec, err := newExecutor(logger, logPrefix, store, options, mainNode, containerFiles)
|
2018-02-25 06:40:44 +08:00
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return "", nil, fmt.Errorf("creating build executor: %w", err)
|
2018-02-25 06:40:44 +08:00
|
|
|
}
|
|
|
|
b := imagebuilder.NewBuilder(options.Args)
|
2020-02-08 01:54:18 +08:00
|
|
|
defaultContainerConfig, err := config.Default()
|
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return "", nil, fmt.Errorf("failed to get container config: %w", err)
|
2020-02-08 01:54:18 +08:00
|
|
|
}
|
|
|
|
b.Env = append(defaultContainerConfig.GetDefaultEnv(), b.Env...)
|
2018-11-08 18:31:14 +08:00
|
|
|
stages, err := imagebuilder.NewStages(mainNode, b)
|
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return "", nil, fmt.Errorf("reading multiple stages: %w", err)
|
2018-11-08 18:31:14 +08:00
|
|
|
}
|
2019-02-03 07:31:44 +08:00
|
|
|
if options.Target != "" {
|
|
|
|
stagesTargeted, ok := stages.ThroughTarget(options.Target)
|
|
|
|
if !ok {
|
2022-07-06 17:14:06 +08:00
|
|
|
return "", nil, fmt.Errorf("The target %q was not found in the provided Dockerfile", options.Target)
|
2019-02-03 07:31:44 +08:00
|
|
|
}
|
|
|
|
stages = stagesTargeted
|
|
|
|
}
|
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
|
|
|
|
2021-05-19 21:05:47 +08:00
|
|
|
// preprocessContainerfileContents runs CPP(1) in preprocess-only mode on the input
|
2018-08-01 18:31:02 +08:00
|
|
|
// dockerfile content and will use ctxDir as the base include path.
|
2022-05-14 18:58:34 +08:00
|
|
|
func preprocessContainerfileContents(logger *logrus.Logger, containerfile string, r io.Reader, ctxDir string, cppFlags []string) (stdout io.Reader, err error) {
|
2021-06-10 01:23:12 +08:00
|
|
|
cppCommand := "cpp"
|
|
|
|
cppPath, err := exec.LookPath(cppCommand)
|
|
|
|
if err != nil {
|
2022-03-17 16:44:31 +08:00
|
|
|
if errors.Is(err, exec.ErrNotFound) {
|
2022-09-18 18:36:08 +08:00
|
|
|
err = fmt.Errorf("%v: .in support requires %s to be installed", err, cppCommand)
|
2018-08-01 18:31:02 +08:00
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-06-10 01:23:12 +08:00
|
|
|
stdoutBuffer := bytes.Buffer{}
|
|
|
|
stderrBuffer := bytes.Buffer{}
|
2018-08-01 18:31:02 +08:00
|
|
|
|
2022-05-14 18:58:34 +08:00
|
|
|
cppArgs := []string{"-E", "-iquote", ctxDir, "-traditional", "-undef", "-"}
|
|
|
|
if flags, ok := os.LookupEnv("BUILDAH_CPPFLAGS"); ok {
|
|
|
|
args, err := shellwords.Parse(flags)
|
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("parsing BUILDAH_CPPFLAGS %q: %v", flags, err)
|
2022-05-14 18:58:34 +08:00
|
|
|
}
|
|
|
|
cppArgs = append(cppArgs, args...)
|
|
|
|
}
|
|
|
|
cppArgs = append(cppArgs, cppFlags...)
|
|
|
|
cmd := exec.Command(cppPath, cppArgs...)
|
2021-06-10 01:23:12 +08:00
|
|
|
cmd.Stdin = r
|
|
|
|
cmd.Stdout = &stdoutBuffer
|
|
|
|
cmd.Stderr = &stderrBuffer
|
2018-08-01 18:31:02 +08:00
|
|
|
|
|
|
|
if err = cmd.Start(); err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("preprocessing %s: %w", containerfile, err)
|
2018-08-01 18:31:02 +08:00
|
|
|
}
|
|
|
|
if err = cmd.Wait(); err != nil {
|
2021-06-10 01:23:12 +08:00
|
|
|
if stderrBuffer.Len() != 0 {
|
|
|
|
logger.Warnf("Ignoring %s\n", stderrBuffer.String())
|
|
|
|
}
|
|
|
|
if stdoutBuffer.Len() == 0 {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("preprocessing %s: preprocessor produced no output: %w", containerfile, err)
|
2018-08-01 18:31:02 +08:00
|
|
|
}
|
|
|
|
}
|
2021-06-10 01:23:12 +08:00
|
|
|
return &stdoutBuffer, nil
|
2018-08-01 18:31:02 +08:00
|
|
|
}
|
2021-09-28 05:26:01 +08:00
|
|
|
|
|
|
|
// platformsForBaseImages resolves the names of base images from the
|
|
|
|
// dockerfiles, and if they are all valid references to manifest lists, returns
|
|
|
|
// the list of platforms that are supported by all of the base images.
|
2022-05-10 18:11:37 +08:00
|
|
|
func platformsForBaseImages(ctx context.Context, logger *logrus.Logger, dockerfilepaths []string, dockerfiles [][]byte, from string, args map[string]string, additionalBuildContext map[string]*define.AdditionalBuildContext, systemContext *types.SystemContext) ([]struct{ OS, Arch, Variant string }, error) {
|
|
|
|
baseImages, err := baseImages(dockerfilepaths, dockerfiles, from, args, additionalBuildContext)
|
2021-09-28 05:26:01 +08:00
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("determining list of base images: %w", err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
logrus.Debugf("unresolved base images: %v", baseImages)
|
|
|
|
if len(baseImages) == 0 {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("build uses no non-scratch base images: %w", err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
targetPlatforms := make(map[string]struct{})
|
|
|
|
var platformList []struct{ OS, Arch, Variant string }
|
|
|
|
for baseImageIndex, baseImage := range baseImages {
|
|
|
|
resolved, err := shortnames.Resolve(systemContext, baseImage)
|
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("resolving image name %q: %w", baseImage, err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
var manifestBytes []byte
|
|
|
|
var manifestType string
|
|
|
|
for _, candidate := range resolved.PullCandidates {
|
|
|
|
ref, err := docker.NewReference(candidate.Value)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Debugf("parsing image reference %q: %v", candidate.Value.String(), err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
src, err := ref.NewImageSource(ctx, systemContext)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Debugf("preparing to read image manifest for %q: %v", baseImage, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
candidateBytes, candidateType, err := src.GetManifest(ctx, nil)
|
|
|
|
_ = src.Close()
|
|
|
|
if err != nil {
|
|
|
|
logrus.Debugf("reading image manifest for %q: %v", baseImage, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if !manifest.MIMETypeIsMultiImage(candidateType) {
|
|
|
|
logrus.Debugf("base image %q is not a reference to a manifest list: %v", baseImage, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err := candidate.Record(); err != nil {
|
|
|
|
logrus.Debugf("error recording name %q for base image %q: %v", candidate.Value.String(), baseImage, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
baseImage = candidate.Value.String()
|
|
|
|
manifestBytes, manifestType = candidateBytes, candidateType
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if len(manifestBytes) == 0 {
|
|
|
|
if len(resolved.PullCandidates) > 0 {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("base image name %q didn't resolve to a manifest list", baseImage)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("base image name %q didn't resolve to anything", baseImage)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
if manifestType != v1.MediaTypeImageIndex {
|
|
|
|
list, err := manifest.ListFromBlob(manifestBytes, manifestType)
|
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("parsing manifest list from base image %q: %w", baseImage, err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
list, err = list.ConvertToMIMEType(v1.MediaTypeImageIndex)
|
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("converting manifest list from base image %q to v2s2 list: %w", baseImage, err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
manifestBytes, err = list.Serialize()
|
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("encoding converted v2s2 manifest list for base image %q: %w", baseImage, err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
index, err := manifest.OCI1IndexFromManifest(manifestBytes)
|
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("decoding manifest list for base image %q: %w", baseImage, err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
if baseImageIndex == 0 {
|
|
|
|
// populate the list with the first image's normalized platforms
|
|
|
|
for _, instance := range index.Manifests {
|
|
|
|
if instance.Platform == nil {
|
|
|
|
continue
|
|
|
|
}
|
2022-09-26 14:39:06 +08:00
|
|
|
platform := internalUtil.NormalizePlatform(*instance.Platform)
|
2021-09-28 05:26:01 +08:00
|
|
|
targetPlatforms[platforms.Format(platform)] = struct{}{}
|
|
|
|
logger.Debugf("image %q supports %q", baseImage, platforms.Format(platform))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// prune the list of any normalized platforms this base image doesn't support
|
|
|
|
imagePlatforms := make(map[string]struct{})
|
|
|
|
for _, instance := range index.Manifests {
|
|
|
|
if instance.Platform == nil {
|
|
|
|
continue
|
|
|
|
}
|
2022-09-26 14:39:06 +08:00
|
|
|
platform := internalUtil.NormalizePlatform(*instance.Platform)
|
2021-09-28 05:26:01 +08:00
|
|
|
imagePlatforms[platforms.Format(platform)] = struct{}{}
|
|
|
|
logger.Debugf("image %q supports %q", baseImage, platforms.Format(platform))
|
|
|
|
}
|
|
|
|
var removed []string
|
|
|
|
for platform := range targetPlatforms {
|
|
|
|
if _, present := imagePlatforms[platform]; !present {
|
|
|
|
removed = append(removed, platform)
|
|
|
|
logger.Debugf("image %q does not support %q", baseImage, platform)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, remove := range removed {
|
|
|
|
delete(targetPlatforms, remove)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if baseImageIndex == len(baseImages)-1 && len(targetPlatforms) > 0 {
|
|
|
|
// extract the list
|
|
|
|
for platform := range targetPlatforms {
|
|
|
|
platform, err := platforms.Parse(platform)
|
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("parsing platform double/triple %q: %w", platform, err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
platformList = append(platformList, struct{ OS, Arch, Variant string }{
|
|
|
|
OS: platform.OS,
|
|
|
|
Arch: platform.Architecture,
|
|
|
|
Variant: platform.Variant,
|
|
|
|
})
|
|
|
|
logger.Debugf("base images all support %q", platform)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(platformList) == 0 {
|
|
|
|
return nil, errors.New("base images have no platforms in common")
|
|
|
|
}
|
|
|
|
return platformList, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// baseImages parses the dockerfilecontents, possibly replacing the first
|
|
|
|
// stage's base image with FROM, and returns the list of base images as
|
|
|
|
// provided. Each entry in the dockerfilenames slice corresponds to a slice in
|
|
|
|
// dockerfilecontents.
|
2022-05-10 18:11:37 +08:00
|
|
|
func baseImages(dockerfilenames []string, dockerfilecontents [][]byte, from string, args map[string]string, additionalBuildContext map[string]*define.AdditionalBuildContext) ([]string, error) {
|
2021-09-28 05:26:01 +08:00
|
|
|
mainNode, err := imagebuilder.ParseDockerfile(bytes.NewReader(dockerfilecontents[0]))
|
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("parsing main Dockerfile: %s: %w", dockerfilenames[0], err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
for i, d := range dockerfilecontents[1:] {
|
|
|
|
additionalNode, err := imagebuilder.ParseDockerfile(bytes.NewReader(d))
|
|
|
|
if err != nil {
|
2022-07-26 22:19:11 +08:00
|
|
|
dockerfilenames := dockerfilenames[1:]
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("parsing additional Dockerfile %s: %w", dockerfilenames[i], err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
mainNode.Children = append(mainNode.Children, additionalNode.Children...)
|
|
|
|
}
|
|
|
|
|
|
|
|
b := imagebuilder.NewBuilder(args)
|
|
|
|
defaultContainerConfig, err := config.Default()
|
|
|
|
if err != nil {
|
2022-07-06 17:14:06 +08:00
|
|
|
return nil, fmt.Errorf("failed to get container config: %w", err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
b.Env = defaultContainerConfig.GetDefaultEnv()
|
|
|
|
stages, err := imagebuilder.NewStages(mainNode, b)
|
|
|
|
if err != nil {
|
2022-09-18 18:36:08 +08:00
|
|
|
return nil, fmt.Errorf("reading multiple stages: %w", err)
|
2021-09-28 05:26:01 +08:00
|
|
|
}
|
|
|
|
var baseImages []string
|
|
|
|
nicknames := make(map[string]bool)
|
|
|
|
for stageIndex, stage := range stages {
|
|
|
|
node := stage.Node // first line
|
|
|
|
for node != nil { // each line
|
|
|
|
for _, child := range node.Children { // tokens on this line, though we only care about the first
|
|
|
|
switch strings.ToUpper(child.Value) { // first token - instruction
|
|
|
|
case "FROM":
|
|
|
|
if child.Next != nil { // second token on this line
|
|
|
|
// If we have a fromOverride, replace the value of
|
|
|
|
// image name for the first FROM in the Containerfile.
|
|
|
|
if from != "" {
|
|
|
|
child.Next.Value = from
|
|
|
|
from = ""
|
|
|
|
}
|
2022-05-10 18:11:37 +08:00
|
|
|
if replaceBuildContext, ok := additionalBuildContext[child.Next.Value]; ok {
|
|
|
|
if replaceBuildContext.IsImage {
|
|
|
|
child.Next.Value = replaceBuildContext.Value
|
|
|
|
} else {
|
|
|
|
return nil, fmt.Errorf("build context %q is not an image, can not be used for FROM %q", child.Next.Value, child.Next.Value)
|
|
|
|
}
|
|
|
|
}
|
2021-09-28 05:26:01 +08:00
|
|
|
base := child.Next.Value
|
|
|
|
if base != "scratch" && !nicknames[base] {
|
|
|
|
// TODO: this didn't undergo variable and arg
|
|
|
|
// expansion, so if the AS clause in another
|
|
|
|
// FROM instruction uses argument values,
|
|
|
|
// we might not record the right value here.
|
|
|
|
baseImages = append(baseImages, base)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
node = node.Next // next line
|
|
|
|
}
|
|
|
|
if stage.Name != strconv.Itoa(stageIndex) {
|
|
|
|
nicknames[stage.Name] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return baseImages, nil
|
|
|
|
}
|