From 07d955f277853627aa7c3f60b3824877cab163b9 Mon Sep 17 00:00:00 2001 From: Aditya R Date: Mon, 9 Oct 2023 13:59:52 +0530 Subject: [PATCH] imagebuildah,multi-stage: do not remove base images When building a multi-stage image ( without `--layers` ) and a stage contains only a base-image buildah removes the base-image itself as part of cleanup process. This is a bug and following commit fixes that. Reproducer ```Dockerfile FROM parent FROM another-base COPY --from=0 somefile . ``` `buildah build -t multi-stage .` Closes: https://github.com/containers/podman/issues/20291 Signed-off-by: Aditya R --- imagebuildah/executor.go | 45 +++++----- imagebuildah/stage_executor.go | 88 ++++++++++--------- tests/bud.bats | 8 ++ .../bud/multi-stage-only-base/Containerfile1 | 2 + .../bud/multi-stage-only-base/Containerfile2 | 2 + .../bud/multi-stage-only-base/Containerfile3 | 3 + 6 files changed, 85 insertions(+), 63 deletions(-) create mode 100644 tests/bud/multi-stage-only-base/Containerfile1 create mode 100644 tests/bud/multi-stage-only-base/Containerfile2 create mode 100644 tests/bud/multi-stage-only-base/Containerfile3 diff --git a/imagebuildah/executor.go b/imagebuildah/executor.go index 6878babe3dd..a5ee4293486 100644 --- a/imagebuildah/executor.go +++ b/imagebuildah/executor.go @@ -470,14 +470,14 @@ func (b *Executor) getImageTypeAndHistoryAndDiffIDs(ctx context.Context, imageID return manifestFormat, oci.History, oci.RootFS.DiffIDs, nil } -func (b *Executor) buildStage(ctx context.Context, cleanupStages map[int]*StageExecutor, stages imagebuilder.Stages, stageIndex int) (imageID string, ref reference.Canonical, err error) { +func (b *Executor) buildStage(ctx context.Context, cleanupStages map[int]*StageExecutor, stages imagebuilder.Stages, stageIndex int) (imageID string, ref reference.Canonical, onlyBaseImage bool, err error) { stage := stages[stageIndex] ib := stage.Builder node := stage.Node base, err := ib.From(node) if err != nil { logrus.Debugf("buildStage(node.Children=%#v)", node.Children) - return "", nil, err + return "", nil, false, err } // If this is the last stage, then the image that we produce at @@ -508,7 +508,7 @@ func (b *Executor) buildStage(ctx context.Context, cleanupStages map[int]*StageE if len(labelLine) > 0 { additionalNode, err := imagebuilder.ParseDockerfile(strings.NewReader("LABEL" + labelLine + "\n")) if err != nil { - return "", nil, fmt.Errorf("while adding additional LABEL step: %w", err) + return "", nil, false, fmt.Errorf("while adding additional LABEL step: %w", err) } stage.Node.Children = append(stage.Node.Children, additionalNode.Children...) } @@ -527,13 +527,13 @@ func (b *Executor) buildStage(ctx context.Context, cleanupStages map[int]*StageE value := env[1] envLine += fmt.Sprintf(" %q=%q", key, value) } else { - return "", nil, fmt.Errorf("BUG: unresolved environment variable: %q", key) + return "", nil, false, fmt.Errorf("BUG: unresolved environment variable: %q", key) } } if len(envLine) > 0 { additionalNode, err := imagebuilder.ParseDockerfile(strings.NewReader("ENV" + envLine + "\n")) if err != nil { - return "", nil, fmt.Errorf("while adding additional ENV step: %w", err) + return "", nil, false, fmt.Errorf("while adding additional ENV step: %w", err) } // make this the first instruction in the stage after its FROM instruction stage.Node.Children = append(additionalNode.Children, stage.Node.Children...) @@ -574,8 +574,8 @@ func (b *Executor) buildStage(ctx context.Context, cleanupStages map[int]*StageE } // Build this stage. - if imageID, ref, err = stageExecutor.Execute(ctx, base); err != nil { - return "", nil, err + if imageID, ref, onlyBaseImage, err = stageExecutor.Execute(ctx, base); err != nil { + return "", nil, onlyBaseImage, err } // The stage succeeded, so remove its build container if we're @@ -588,7 +588,7 @@ func (b *Executor) buildStage(ctx context.Context, cleanupStages map[int]*StageE b.stagesLock.Unlock() } - return imageID, ref, nil + return imageID, ref, onlyBaseImage, nil } type stageDependencyInfo struct { @@ -880,10 +880,11 @@ func (b *Executor) Build(ctx context.Context, stages imagebuilder.Stages) (image b.warnOnUnsetBuildArgs(stages, dependencyMap, b.args) type Result struct { - Index int - ImageID string - Ref reference.Canonical - Error error + Index int + ImageID string + OnlyBaseImage bool + Ref reference.Canonical + Error error } ch := make(chan Result, len(stages)) @@ -943,21 +944,23 @@ func (b *Executor) Build(ctx context.Context, stages imagebuilder.Stages) (image return } } - stageID, stageRef, stageErr := b.buildStage(ctx, cleanupStages, stages, index) + stageID, stageRef, stageOnlyBaseImage, stageErr := b.buildStage(ctx, cleanupStages, stages, index) if stageErr != nil { cancel = true ch <- Result{ - Index: index, - Error: stageErr, + Index: index, + Error: stageErr, + OnlyBaseImage: stageOnlyBaseImage, } return } ch <- Result{ - Index: index, - ImageID: stageID, - Ref: stageRef, - Error: nil, + Index: index, + ImageID: stageID, + Ref: stageRef, + OnlyBaseImage: stageOnlyBaseImage, + Error: nil, } }() } @@ -987,7 +990,9 @@ func (b *Executor) Build(ctx context.Context, stages imagebuilder.Stages) (image // We're not populating the cache with intermediate // images, so add this one to the list of images that // we'll remove later. - if !b.layers { + // Only remove intermediate image is `--layers` is not provided + // or following stage was not only a base image ( i.e a different image ). + if !b.layers && !r.OnlyBaseImage { cleanupImages = append(cleanupImages, r.ImageID) } } diff --git a/imagebuildah/stage_executor.go b/imagebuildah/stage_executor.go index 19eea2a76e7..c5325444718 100644 --- a/imagebuildah/stage_executor.go +++ b/imagebuildah/stage_executor.go @@ -915,13 +915,14 @@ func (s *StageExecutor) getContentSummaryAfterAddingContent() string { } // Execute runs each of the steps in the stage's parsed tree, in turn. -func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, ref reference.Canonical, err error) { +func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, ref reference.Canonical, onlyBaseImg bool, err error) { var resourceUsage rusage.Rusage stage := s.stage ib := stage.Builder checkForLayers := s.executor.layers && s.executor.useCache moreStages := s.index < len(s.stages)-1 lastStage := !moreStages + onlyBaseImage := false imageIsUsedLater := moreStages && (s.executor.baseMap[stage.Name] || s.executor.baseMap[strconv.Itoa(stage.Position)]) rootfsIsUsedLater := moreStages && (s.executor.rootfsMap[stage.Name] || s.executor.rootfsMap[strconv.Itoa(stage.Position)]) @@ -934,7 +935,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // either in local storage, or one that we have to pull from a // registry, subject to the passed-in pull policy. if isStage, err := s.executor.waitForStage(ctx, base, s.stages[:s.index]); isStage && err != nil { - return "", nil, err + return "", nil, false, err } pullPolicy := s.executor.pullPolicy s.executor.stagesLock.Lock() @@ -964,7 +965,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // Start counting resource usage before we potentially pull a base image. if rusage.Supported() { if resourceUsage, err = rusage.Get(); err != nil { - return "", nil, err + return "", nil, false, err } // Log the final incremental resource usage counter before we return. defer logRusage() @@ -974,7 +975,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // the imagebuilder configuration may alter the list of steps we have, // so take a snapshot of them *after* that. if _, err := s.prepare(ctx, base, true, true, preserveBaseImageAnnotationsAtStageStart, pullPolicy); err != nil { - return "", nil, err + return "", nil, false, err } children := stage.Node.Children @@ -1032,7 +1033,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, logrus.Debugf("Generating custom build output with options %q", s.executor.buildOutput) buildOutputOption, err = parse.GetBuildOutput(s.executor.buildOutput) if err != nil { - return "", nil, fmt.Errorf("failed to parse build output: %w", err) + return "", nil, false, fmt.Errorf("failed to parse build output: %w", err) } } @@ -1050,12 +1051,12 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, emptyLayer = true } if imgID, ref, err = s.commit(ctx, s.getCreatedBy(nil, ""), emptyLayer, s.output, s.executor.squash, lastStage); err != nil { - return "", nil, fmt.Errorf("committing base container: %w", err) + return "", nil, false, fmt.Errorf("committing base container: %w", err) } // Generate build output if needed. if canGenerateBuildOutput { if err := s.generateBuildOutput(buildOutputOption); err != nil { - return "", nil, err + return "", nil, false, err } } } else if len(s.executor.labels) > 0 || len(s.executor.annotations) > 0 { @@ -1063,12 +1064,12 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // via the command line, so we need to commit. logCommit(s.output, -1) if imgID, ref, err = s.commit(ctx, s.getCreatedBy(stage.Node, ""), true, s.output, s.executor.squash, lastStage); err != nil { - return "", nil, err + return "", nil, false, err } // Generate build output if needed. if canGenerateBuildOutput { if err := s.generateBuildOutput(buildOutputOption); err != nil { - return "", nil, err + return "", nil, false, err } } } else { @@ -1077,8 +1078,9 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // options, so just reuse the base image. logCommit(s.output, -1) if imgID, ref, err = s.tagExistingImage(ctx, s.builder.FromImageID, s.output); err != nil { - return "", nil, err + return "", nil, onlyBaseImage, err } + onlyBaseImage = true // If we have reached this point then our build is just performing a tag // and it contains no steps or instructions (i.e Containerfile only contains // `FROM and nothing else so we will never end up committing this @@ -1086,7 +1088,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // specified honor that and export the contents of the current build anyways. if canGenerateBuildOutput { if err := s.generateBuildOutput(buildOutputOption); err != nil { - return "", nil, err + return "", nil, onlyBaseImage, err } } } @@ -1100,7 +1102,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // Resolve any arguments in this instruction. step := ib.Step() if err := step.Resolve(node); err != nil { - return "", nil, fmt.Errorf("resolving step %+v: %w", *node, err) + return "", nil, false, fmt.Errorf("resolving step %+v: %w", *node, err) } logrus.Debugf("Parsed Step: %+v", *step) if !s.executor.quiet { @@ -1113,21 +1115,21 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, command := strings.ToUpper(step.Command) // chmod, chown and from flags should have an '=' sign, '--chmod=', '--chown=' or '--from=' if command == "COPY" && (flag == "--chmod" || flag == "--chown" || flag == "--from") { - return "", nil, fmt.Errorf("COPY only supports the --chmod= --chown= and the --from= flags") + return "", nil, false, fmt.Errorf("COPY only supports the --chmod= --chown= and the --from= flags") } if command == "ADD" && (flag == "--chmod" || flag == "--chown") { - return "", nil, fmt.Errorf("ADD only supports the --chmod= and the --chown= flags") + return "", nil, false, fmt.Errorf("ADD only supports the --chmod= and the --chown= flags") } if strings.Contains(flag, "--from") && command == "COPY" { arr := strings.Split(flag, "=") if len(arr) != 2 { - return "", nil, fmt.Errorf("%s: invalid --from flag, should be --from=", command) + return "", nil, false, fmt.Errorf("%s: invalid --from flag, should be --from=", command) } // If arr[1] has an argument within it, resolve it to its // value. Otherwise just return the value found. from, fromErr := imagebuilder.ProcessWord(arr[1], s.stage.Builder.Arguments()) if fromErr != nil { - return "", nil, fmt.Errorf("unable to resolve argument %q: %w", arr[1], fromErr) + return "", nil, false, fmt.Errorf("unable to resolve argument %q: %w", arr[1], fromErr) } // Before looking into additional context @@ -1150,7 +1152,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // replace with image set in build context from = additionalBuildContext.Value if _, err := s.getImageRootfs(ctx, from); err != nil { - return "", nil, fmt.Errorf("%s --from=%s: no stage or image found with that name", command, from) + return "", nil, false, fmt.Errorf("%s --from=%s: no stage or image found with that name", command, from) } break } @@ -1160,12 +1162,12 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // result of an earlier stage, wait for that // stage to finish being built. if isStage, err := s.executor.waitForStage(ctx, from, s.stages[:s.index]); isStage && err != nil { - return "", nil, err + return "", nil, false, err } if otherStage, ok := s.executor.stages[from]; ok && otherStage.index < s.index { break } else if _, err = s.getImageRootfs(ctx, from); err != nil { - return "", nil, fmt.Errorf("%s --from=%s: no stage or image found with that name", command, from) + return "", nil, false, fmt.Errorf("%s --from=%s: no stage or image found with that name", command, from) } break } @@ -1187,7 +1189,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, err := ib.Run(step, s, noRunsRemaining) if err != nil { logrus.Debugf("Error building at step %+v: %v", *step, err) - return "", nil, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) + return "", nil, false, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) } // In case we added content, retrieve its digest. addedContentSummary := s.getContentSummaryAfterAddingContent() @@ -1212,13 +1214,13 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, logCommit(s.output, i) imgID, ref, err = s.commit(ctx, s.getCreatedBy(node, addedContentSummary), false, s.output, s.executor.squash, lastStage && lastInstruction) if err != nil { - return "", nil, fmt.Errorf("committing container for step %+v: %w", *step, err) + return "", nil, false, fmt.Errorf("committing container for step %+v: %w", *step, err) } logImageID(imgID) // Generate build output if needed. if canGenerateBuildOutput { if err := s.generateBuildOutput(buildOutputOption); err != nil { - return "", nil, err + return "", nil, false, err } } } else { @@ -1250,7 +1252,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, for _, a := range node.Flags { arg, err := imagebuilder.ProcessWord(a, s.stage.Builder.Arguments()) if err != nil { - return "", nil, err + return "", nil, false, err } switch { case strings.HasPrefix(arg, "--mount="): @@ -1262,7 +1264,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, } stageMountPoints, err := s.runStageMountPoints(mounts) if err != nil { - return "", nil, err + return "", nil, false, err } for _, mountPoint := range stageMountPoints { if mountPoint.DidExecute { @@ -1284,7 +1286,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, if needsCacheKey { cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) if err != nil { - return "", nil, fmt.Errorf("failed while generating cache key: %w", err) + return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err) } } // Check if there's already an image based on our parent that @@ -1304,7 +1306,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, s.didExecute = true if err = ib.Run(step, s, noRunsRemaining); err != nil { logrus.Debugf("Error building at step %+v: %v", *step, err) - return "", nil, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) + return "", nil, false, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) } // Retrieve the digest info for the content that we just copied // into the rootfs. @@ -1313,13 +1315,13 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, if needsCacheKey { cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) if err != nil { - return "", nil, fmt.Errorf("failed while generating cache key: %w", err) + return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err) } } } cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) if err != nil { - return "", nil, fmt.Errorf("checking if cached image exists from a previous build: %w", err) + return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) } // All the best effort to find cache on localstorage have failed try pulling // cache from remote repo if `--cache-from` was configured. @@ -1331,7 +1333,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, logCachePulled(cacheKey, ref) cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) if err != nil { - return "", nil, fmt.Errorf("checking if cached image exists from a previous build: %w", err) + return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) } if cacheID != "" { pulledAndUsedCacheImage = true @@ -1351,7 +1353,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, s.didExecute = true if err = ib.Run(step, s, noRunsRemaining); err != nil { logrus.Debugf("Error building at step %+v: %v", *step, err) - return "", nil, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) + return "", nil, false, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) } // In case we added content, retrieve its digest. @@ -1360,7 +1362,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, if needsCacheKey { cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) if err != nil { - return "", nil, fmt.Errorf("failed while generating cache key: %w", err) + return "", nil, false, fmt.Errorf("failed while generating cache key: %w", err) } } @@ -1369,7 +1371,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, if checkForLayers && !avoidLookingCache { cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) if err != nil { - return "", nil, fmt.Errorf("checking if cached image exists from a previous build: %w", err) + return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) } // All the best effort to find cache on localstorage have failed try pulling // cache from remote repo if `--cache-from` was configured and cacheKey was @@ -1382,7 +1384,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, logCachePulled(cacheKey, ref) cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step)) if err != nil { - return "", nil, fmt.Errorf("checking if cached image exists from a previous build: %w", err) + return "", nil, false, fmt.Errorf("checking if cached image exists from a previous build: %w", err) } if cacheID != "" { pulledAndUsedCacheImage = true @@ -1406,7 +1408,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, err := ib.Run(step, s, noRunsRemaining) if err != nil { logrus.Debugf("Error building at step %+v: %v", *step, err) - return "", nil, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) + return "", nil, false, fmt.Errorf("building at STEP \"%s\": %w", step.Message, err) } } } @@ -1423,7 +1425,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, if commitName != "" { logCommit(commitName, i) if imgID, ref, err = s.tagExistingImage(ctx, cacheID, commitName); err != nil { - return "", nil, err + return "", nil, false, err } } } else { @@ -1439,12 +1441,12 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // can be part of build-cache. imgID, ref, err = s.commit(ctx, s.getCreatedBy(node, addedContentSummary), !s.stepRequiresLayer(step), commitName, false, lastStage && lastInstruction) if err != nil { - return "", nil, fmt.Errorf("committing container for step %+v: %w", *step, err) + return "", nil, false, fmt.Errorf("committing container for step %+v: %w", *step, err) } // Generate build output if needed. if canGenerateBuildOutput { if err := s.generateBuildOutput(buildOutputOption); err != nil { - return "", nil, err + return "", nil, false, err } } } @@ -1462,7 +1464,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, if len(s.executor.cacheTo) != 0 && (!pulledAndUsedCacheImage || cacheID == "") && needsCacheKey { logCachePush(cacheKey) if err = s.pushCache(ctx, imgID, cacheKey); err != nil { - return "", nil, err + return "", nil, false, err } } @@ -1473,12 +1475,12 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // is the last instruction of the last stage. imgID, ref, err = s.commit(ctx, s.getCreatedBy(node, addedContentSummary), !s.stepRequiresLayer(step), commitName, true, lastStage && lastInstruction) if err != nil { - return "", nil, fmt.Errorf("committing final squash step %+v: %w", *step, err) + return "", nil, false, fmt.Errorf("committing final squash step %+v: %w", *step, err) } // Generate build output if needed. if canGenerateBuildOutput { if err := s.generateBuildOutput(buildOutputOption); err != nil { - return "", nil, err + return "", nil, false, err } } } else if cacheID != "" { @@ -1493,7 +1495,7 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // Generate build output if needed. if canGenerateBuildOutput { if err := s.generateBuildOutput(buildOutputOption); err != nil { - return "", nil, err + return "", nil, false, err } } } @@ -1524,11 +1526,11 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string, // ID that we really should not be pulling anymore (see // containers/podman/issues/10307). if _, err := s.prepare(ctx, imgID, false, true, true, define.PullNever); err != nil { - return "", nil, fmt.Errorf("preparing container for next step: %w", err) + return "", nil, false, fmt.Errorf("preparing container for next step: %w", err) } } } - return imgID, ref, nil + return imgID, ref, onlyBaseImage, nil } func historyEntriesEqual(base, derived v1.History) bool { diff --git a/tests/bud.bats b/tests/bud.bats index 329069548d6..488e60a79b7 100644 --- a/tests/bud.bats +++ b/tests/bud.bats @@ -89,6 +89,14 @@ _EOF validate_instance_compression "3" "$list" "arm64" "zstd" } +@test "Multi-stage should not remove used base-image without --layers" { + run_buildah build -t parent-one -f $BUDFILES/multi-stage-only-base/Containerfile1 + run_buildah build -t parent-two -f $BUDFILES/multi-stage-only-base/Containerfile2 + run_buildah build -t multi-stage -f $BUDFILES/multi-stage-only-base/Containerfile3 + run_buildah images -a + expect_output --substring "parent-one" "parent one must not be removed" +} + @test "no layer should be created on scratch" { run_buildah build --layers --label "label1=value1" -t test -f $BUDFILES/from-scratch/Containerfile run_buildah inspect -f '{{len .Docker.RootFS.DiffIDs}}' test diff --git a/tests/bud/multi-stage-only-base/Containerfile1 b/tests/bud/multi-stage-only-base/Containerfile1 new file mode 100644 index 00000000000..1d61b0b07ae --- /dev/null +++ b/tests/bud/multi-stage-only-base/Containerfile1 @@ -0,0 +1,2 @@ +FROM alpine +RUN echo "parent-one" > parent-one diff --git a/tests/bud/multi-stage-only-base/Containerfile2 b/tests/bud/multi-stage-only-base/Containerfile2 new file mode 100644 index 00000000000..74a3966deae --- /dev/null +++ b/tests/bud/multi-stage-only-base/Containerfile2 @@ -0,0 +1,2 @@ +FROM alpine +RUN echo "parent-two" > parent-two diff --git a/tests/bud/multi-stage-only-base/Containerfile3 b/tests/bud/multi-stage-only-base/Containerfile3 new file mode 100644 index 00000000000..38c84d0df64 --- /dev/null +++ b/tests/bud/multi-stage-only-base/Containerfile3 @@ -0,0 +1,3 @@ +FROM localhost/parent-one as p1 +FROM localhost/parent-two +COPY --from=p1 parent-one .