Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1025,8 +1025,8 @@ want to save the image as tarball only you also need to set `--no-push`.

#### Flag `--target`

Set this flag to indicate which build stage is the target build stage.
If not set we implicitly target the last stage.
Set this flag to indicate which stages to build. If multiple targets are configured the first in the list is pushed.
If not set we implicitly target the last stage of the Dockerfile.

#### Flag `--use-new-run`

Expand Down
2 changes: 1 addition & 1 deletion cmd/executor/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ func AddKanikoOptionsFlags(cmd *cobra.Command, opts *config.KanikoOptions) {
cmd.Flags().StringVarP(&opts.TarPath, "tar-path", "", "", "Path to save the image in as a tarball instead of pushing")
cmd.Flags().BoolVarP(&opts.SingleSnapshot, "single-snapshot", "", false, "Take a single snapshot at the end of the build.")
cmd.Flags().BoolVarP(&opts.Reproducible, "reproducible", "", false, "Strip timestamps out of the image to make it reproducible")
cmd.Flags().StringVarP(&opts.Target, "target", "", "", "Set the target build stage to build")
cmd.Flags().StringSliceVarP(&opts.Target, "target", "", []string{}, "Set the target stages to build, the first in the list denotes the stage to be pushed")
cmd.Flags().BoolVarP(&opts.NoPush, "no-push", "", false, "Do not push the image to the registry")
cmd.Flags().BoolVarP(&opts.NoPushCache, "no-push-cache", "", false, "Do not push the cache layers to the registry")
cmd.Flags().StringVarP(&opts.CacheRepo, "cache-repo", "", "", "Specify a repository to use as a cache, otherwise one will be inferred from the destination provided; when prefixed with 'oci:' the repository will be written in OCI image layout format at the path provided")
Expand Down
2 changes: 2 additions & 0 deletions golden/golden_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
testissuemz195 "github.com/osscontainertools/kaniko/golden/testdata/test_issue_mz195"
testissuemz333 "github.com/osscontainertools/kaniko/golden/testdata/test_issue_mz333"
testissuemz338 "github.com/osscontainertools/kaniko/golden/testdata/test_issue_mz338"
testissuemz480 "github.com/osscontainertools/kaniko/golden/testdata/test_issue_mz480"
testissuemz487 "github.com/osscontainertools/kaniko/golden/testdata/test_issue_mz487"
testunittests "github.com/osscontainertools/kaniko/golden/testdata/test_unittests"
"github.com/osscontainertools/kaniko/golden/types"
Expand Down Expand Up @@ -64,6 +65,7 @@ var allTests = map[string][]types.GoldenTests{
"test_issue_mz333": {testissuemz333.Tests},
"test_issue_mz338": {testissuemz338.Tests},
"test_issue_mz487": {testissuemz487.Tests},
"test_issue_mz480": {testissuemz480.Tests},
"test_unittests": testunittests.Tests,
}
var update bool
Expand Down
11 changes: 11 additions & 0 deletions golden/testdata/test_issue_mz480/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM debian:12.10 AS base
RUN install

FROM base AS build
RUN compile

FROM base AS final
COPY --from=build output output

FROM final AS test
RUN test
16 changes: 16 additions & 0 deletions golden/testdata/test_issue_mz480/plans/final
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM debian:12.10 AS base
UNPACK debian:12.10
RUN install
SAVE STAGE /kaniko/stages/0
CLEAN

FROM base AS build
UNPACK /kaniko/stages/0
RUN compile
SAVE FILES [output] /kaniko/deps/1
CLEAN

FROM base AS final
UNPACK /kaniko/stages/0
COPY --from=build output output
PUSH [registry]
22 changes: 22 additions & 0 deletions golden/testdata/test_issue_mz480/plans/final_test
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM debian:12.10 AS base
UNPACK debian:12.10
RUN install
SAVE STAGE /kaniko/stages/0
CLEAN

FROM base AS build
UNPACK /kaniko/stages/0
RUN compile
SAVE FILES [output] /kaniko/deps/1
CLEAN

FROM base AS final
UNPACK /kaniko/stages/0
COPY --from=build output output
PUSH [registry]
SAVE STAGE /kaniko/stages/2
CLEAN

FROM final AS test
UNPACK /kaniko/stages/2
RUN test
27 changes: 27 additions & 0 deletions golden/testdata/test_issue_mz480/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package testissuemz480

import "github.com/osscontainertools/kaniko/golden/types"

var Tests = types.GoldenTests{
Name: "test_issue_mz480",
Dockerfile: "Dockerfile",
Tests: []types.GoldenTest{
{
Args: []string{"--target=final", "--destination=registry"},
// TODO: clean after "base" stage is unnecesary
Plan: "final",
},
{
Args: []string{"--target=final", "--target=build", "--destination=registry"},
// TODO: clean after "base" stage is unnecesary
Plan: "final",
},
{
Args: []string{"--target=final", "--target=test", "--destination=registry"},
// TODO: clean after "base" stage is unnecesary
// TODO: saving the "final" stage is unnecessary
// TODO: clean after "final" stage is unnecesary
Plan: "final_test",
},
},
}
2 changes: 1 addition & 1 deletion pkg/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ type KanikoOptions struct {
TarPath string
TarPathDeprecated string
KanikoDir string
Target string
Target []string
CacheRepo string
DigestFile string
ImageNameDigestFile string
Expand Down
1 change: 1 addition & 0 deletions pkg/config/stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type KanikoStage struct {
BaseName string
Commands []instructions.Command
BaseImageIndex int
Push bool
Final bool
BaseImageStoredLocally bool
SaveStage bool
Expand Down
58 changes: 41 additions & 17 deletions pkg/dockerfile/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"net/http"
"os"
"regexp"
"slices"
"strconv"
"strings"

Expand Down Expand Up @@ -197,17 +198,26 @@ func extractValFromQuotes(val string) (string, error) {
return val[1 : len(val)-1], nil
}

// targetStage returns the index of the target stage kaniko is trying to build
func targetStage(stages []instructions.Stage, target string) (int, error) {
if target == "" {
return len(stages) - 1, nil
}
for i, stage := range stages {
if strings.EqualFold(stage.Name, target) {
return i, nil
// targetStage returns the indexes of the target stages kaniko is trying to build
func targetStages(stages []instructions.Stage, targets []string) ([]int, error) {
if len(targets) == 0 {
return []int{len(stages) - 1}, nil
}
var result []int
for _, target := range targets {
found := false
for i, stage := range stages {
if strings.EqualFold(stage.Name, target) {
result = append(result, i)
found = true
break
}
}
if !found {
return nil, fmt.Errorf("%q is not a valid target build stage", target)
}
}
return -1, fmt.Errorf("%s is not a valid target build stage", target)
return result, nil
}

// ParseCommands parses an array of commands into an array of instructions.Command; used for onbuild
Expand Down Expand Up @@ -262,15 +272,20 @@ func resolveStagesArgs(stages []instructions.Stage, args []string) error {
}

func MakeKanikoStages(opts *config.KanikoOptions, stages []instructions.Stage, metaArgs []instructions.ArgCommand) ([]config.KanikoStage, error) {
targetStage, err := targetStage(stages, opts.Target)
targetStages, err := targetStages(stages, opts.Target)
if err != nil {
return nil, fmt.Errorf("error finding target stage: %w", err)
}
pushStage := targetStages[0]
slices.Sort(targetStages)
targetStages = slices.Compact(targetStages)
finalStage := targetStages[len(targetStages)-1]

args := unifyArgs(metaArgs, opts.BuildArgs)
if err := resolveStagesArgs(stages, args); err != nil {
return nil, fmt.Errorf("resolving args: %w", err)
}
stages = stages[:targetStage+1]
stages = stages[:finalStage+1]

stageByName := make(map[string]int)
for idx, s := range stages {
Expand All @@ -282,11 +297,18 @@ func MakeKanikoStages(opts *config.KanikoOptions, stages []instructions.Stage, m
kanikoStages := make([]config.KanikoStage, len(stages))
// We now "count" references, it is only safe to squash
// stages if the references are exactly 1 and there are no COPY references
buildTargets := make([]bool, len(stages))
stagesDependencies := make([]int, len(stages))
copyDependencies := make([]int, len(stages))
stagesDependencies[targetStage] = 1
for i := targetStage; i >= 0; i-- {
if stagesDependencies[i] == 0 && copyDependencies[i] == 0 && opts.SkipUnusedStages {
for _, x := range targetStages {
// buildTargets we just need to visit, but they
// can be squashed together, we don't care.
buildTargets[x] = true
}
// push stage cannot be squashed
stagesDependencies[pushStage] = 1
for i := finalStage; i >= 0; i-- {
if !buildTargets[i] && stagesDependencies[i] == 0 && copyDependencies[i] == 0 && opts.SkipUnusedStages {
continue
}
stage := stages[i]
Expand Down Expand Up @@ -331,14 +353,15 @@ func MakeKanikoStages(opts *config.KanikoOptions, stages []instructions.Stage, m
BaseImageIndex: baseImageIndex,
BaseImageStoredLocally: baseImageStoredLocally,
SaveStage: stagesDependencies[i] > 0,
Final: i == targetStage,
Push: i == pushStage,
Final: i == finalStage,
MetaArgs: metaArgs,
Index: i,
}
}
if opts.SkipUnusedStages && config.EnvBoolDefault("FF_KANIKO_SQUASH_STAGES", true) {
for i, s := range kanikoStages {
if stagesDependencies[i] > 0 || copyDependencies[i] > 0 {
if buildTargets[i] || stagesDependencies[i] > 0 || copyDependencies[i] > 0 {
if s.BaseImageStoredLocally && stagesDependencies[s.BaseImageIndex] == 1 && copyDependencies[s.BaseImageIndex] == 0 {
sb := kanikoStages[s.BaseImageIndex]
// squash stages[i] into stages[i].BaseName
Expand All @@ -355,7 +378,7 @@ func MakeKanikoStages(opts *config.KanikoOptions, stages []instructions.Stage, m
if opts.SkipUnusedStages {
var onlyUsedStages []config.KanikoStage
for i, s := range kanikoStages {
if stagesDependencies[i] > 0 || copyDependencies[i] > 0 {
if buildTargets[i] || stagesDependencies[i] > 0 || copyDependencies[i] > 0 {
s.SaveStage = stagesDependencies[i] > 0
onlyUsedStages = append(onlyUsedStages, s)
}
Expand Down Expand Up @@ -433,6 +456,7 @@ func squash(a, b config.KanikoStage) config.KanikoStage {
BaseName: a.BaseName,
Commands: append(acmds, b.Commands...),
BaseImageIndex: a.BaseImageIndex,
Push: b.Push,
Final: b.Final,
BaseImageStoredLocally: a.BaseImageStoredLocally,
SaveStage: b.SaveStage,
Expand Down
10 changes: 9 additions & 1 deletion pkg/dockerfile/dockerfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,17 @@ func Test_targetStage(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
target, err := targetStage(stages, test.target)
var targs []string
if test.target != "" {
targs = []string{test.target}
}
targets, err := targetStages(stages, targs)
testutil.CheckError(t, test.shouldErr, err)
if !test.shouldErr {
if len(targets) != 1 {
t.Errorf("Expected one target, received %d", len(targets))
}
target := targets[0]
if target != test.targetIndex {
t.Errorf("got incorrect target, expected %d got %d", test.targetIndex, target)
}
Expand Down
22 changes: 15 additions & 7 deletions pkg/executor/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func newStageBuilder(args *dockerfile.BuildArgs, opts *config.KanikoOptions, sta
}

_opts := *opts
if !stage.Final {
if !stage.Push {
_opts.Labels = []string{}
}
imageConfig, err := initializeConfig(sourceImage, &_opts)
Expand Down Expand Up @@ -741,10 +741,10 @@ func RenderStages(stages []config.KanikoStage, opts *config.KanikoOptions, fileC
}
printf("%s\n", command)
}
if s.Push && !opts.NoPush {
printf("PUSH %v\n", opts.Destinations)
}
if s.Final {
if !opts.NoPush {
printf("PUSH %v\n", opts.Destinations)
}
if opts.Cleanup {
printf("CLEAN\n")
}
Expand All @@ -762,7 +762,7 @@ func RenderStages(stages []config.KanikoStage, opts *config.KanikoOptions, fileC
printf("RESTORE CONTEXT\n\n")
}
}
logrus.Panic("unreachable - we should always have a final stage")
logrus.Panic("Unreachable Code: we should always have a final stage")
return retErr
}

Expand Down Expand Up @@ -860,6 +860,7 @@ func DoBuild(opts *config.KanikoOptions) (image v1.Image, retErr error) {
})
}

var pushImage v1.Image
for _, stage := range kanikoStages {
sb, err := newStageBuilder(
args, opts, stage,
Expand Down Expand Up @@ -923,7 +924,7 @@ func DoBuild(opts *config.KanikoOptions) (image v1.Image, retErr error) {
digestToCacheKey[d.String()] = finalCacheKey
logrus.Debugf("Mapping digest %v to cachekey %v", d.String(), finalCacheKey)

if stage.Final {
if stage.Push {
sourceImage, err = mutate.CreatedAt(sourceImage, v1.Time{Time: time.Now()})
if err != nil {
return nil, err
Expand All @@ -937,8 +938,15 @@ func DoBuild(opts *config.KanikoOptions) (image v1.Image, retErr error) {
if len(opts.Annotations) > 0 {
sourceImage = mutate.Annotations(sourceImage, opts.Annotations).(v1.Image)
}
pushImage = sourceImage
}
if stage.Final {
timing.DefaultRun.Stop(t)
return sourceImage, nil
if pushImage == nil {
// Final stage must be last, so by definition after Push stage
logrus.Panic("pushImage is nil")
}
return pushImage, nil
}
if stage.SaveStage {
if err := saveStageAsTarball(strconv.Itoa(stage.Index), sourceImage); err != nil {
Expand Down
Loading