1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-02-21 19:48:53 +02:00

kanikoExecute: add multiple build (#4461)

* kanikoExecute: add MultipleImages option

---------

Co-authored-by: Egor Balakin <egor.balakin@sap.com>
This commit is contained in:
Egor Balakin 2023-08-07 16:58:59 +04:00 committed by GitHub
parent b474eb2de7
commit e2bf31872b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 392 additions and 122 deletions

View File

@ -2,6 +2,7 @@ package cmd
import (
"fmt"
"github.com/mitchellh/mapstructure"
"strings"
"github.com/SAP/jenkins-library/pkg/buildsettings"
@ -134,117 +135,222 @@ func runKanikoExecute(config *kanikoExecuteOptions, telemetryData *telemetry.Cus
}
commonPipelineEnvironment.custom.buildSettingsInfo = buildSettingsInfo
if !piperutils.ContainsString(config.BuildOptions, "--destination") {
dest := []string{"--no-push"}
if len(config.ContainerRegistryURL) > 0 && len(config.ContainerImageName) > 0 && len(config.ContainerImageTag) > 0 {
containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "failed to read registry url %v", config.ContainerRegistryURL)
}
switch {
case config.ContainerMultiImageBuild:
log.Entry().Debugf("Multi-image build activated for image name '%v'", config.ContainerImageName)
commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
// Docker image tags don't allow plus signs in tags, thus replacing with dash
containerImageTag := strings.ReplaceAll(config.ContainerImageTag, "+", "-")
if config.ContainerMultiImageBuild {
log.Entry().Debugf("Multi-image build activated for image name '%v'", config.ContainerImageName)
imageListWithFilePath, err := docker.ImageListWithFilePath(config.ContainerImageName, config.ContainerMultiImageBuildExcludes, config.ContainerMultiImageBuildTrimDir, fileUtils)
if err != nil {
return fmt.Errorf("failed to identify image list for multi image build: %w", err)
}
if len(imageListWithFilePath) == 0 {
return fmt.Errorf("no docker files to process, please check exclude list")
}
for image, file := range imageListWithFilePath {
log.Entry().Debugf("Building image '%v' using file '%v'", image, file)
containerImageNameAndTag := fmt.Sprintf("%v:%v", image, containerImageTag)
dest = []string{"--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag)}
buildOpts := append(config.BuildOptions, dest...)
err = runKaniko(file, buildOpts, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment)
if err != nil {
return fmt.Errorf("failed to build image '%v' using '%v': %w", image, file, err)
}
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, image)
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
}
// for compatibility reasons also fill single imageNameTag field with "root" image in commonPipelineEnvironment
// only consider if it has been built
// ToDo: reconsider and possibly remove at a later point
if len(imageListWithFilePath[config.ContainerImageName]) > 0 {
containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
}
if config.CreateBOM {
//Syft for multi image, generates bom-docker-(1/2/3).xml
return syft.GenerateSBOM(config.SyftDownloadURL, "/kaniko/.docker", execRunner, fileUtils, httpClient, commonPipelineEnvironment.container.registryURL, commonPipelineEnvironment.container.imageNameTags)
}
return nil
} else {
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, config.ContainerImageName)
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag))
}
log.Entry().Debugf("Single image build for image name '%v'", config.ContainerImageName)
containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
dest = []string{"--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag)}
commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
} else if len(config.ContainerImage) > 0 {
log.Entry().Debugf("Single image build for image '%v'", config.ContainerImage)
containerRegistry, err := docker.ContainerRegistryFromImage(config.ContainerImage)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "invalid registry part in image %v", config.ContainerImage)
}
// errors are already caught with previous call to docker.ContainerRegistryFromImage
containerImageName, _ := docker.ContainerImageNameFromImage(config.ContainerImage)
containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(config.ContainerImage)
dest = []string{"--destination", config.ContainerImage}
commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%v", containerRegistry)
commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
if config.ContainerRegistryURL == "" {
return fmt.Errorf("empty ContainerRegistryURL")
}
if config.ContainerImageName == "" {
return fmt.Errorf("empty ContainerImageName")
}
if config.ContainerImageTag == "" {
return fmt.Errorf("empty ContainerImageTag")
}
config.BuildOptions = append(config.BuildOptions, dest...)
} else {
log.Entry().Infof("Running Kaniko build with destination defined via buildOptions: %v", config.BuildOptions)
destination := ""
containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "failed to read registry url %v", config.ContainerRegistryURL)
}
commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
// Docker image tags don't allow plus signs in tags, thus replacing with dash
containerImageTag := strings.ReplaceAll(config.ContainerImageTag, "+", "-")
imageListWithFilePath, err := docker.ImageListWithFilePath(config.ContainerImageName, config.ContainerMultiImageBuildExcludes, config.ContainerMultiImageBuildTrimDir, fileUtils)
if err != nil {
return fmt.Errorf("failed to identify image list for multi image build: %w", err)
}
if len(imageListWithFilePath) == 0 {
return fmt.Errorf("no docker files to process, please check exclude list")
}
for image, file := range imageListWithFilePath {
log.Entry().Debugf("Building image '%v' using file '%v'", image, file)
containerImageNameAndTag := fmt.Sprintf("%v:%v", image, containerImageTag)
buildOpts := append(config.BuildOptions, "--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag))
if err = runKaniko(file, buildOpts, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
return fmt.Errorf("failed to build image '%v' using '%v': %w", image, file, err)
}
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, image)
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
}
// for compatibility reasons also fill single imageNameTag field with "root" image in commonPipelineEnvironment
// only consider if it has been built
// ToDo: reconsider and possibly remove at a later point
if len(imageListWithFilePath[config.ContainerImageName]) > 0 {
containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
}
if config.CreateBOM {
// Syft for multi image, generates bom-docker-(1/2/3).xml
return syft.GenerateSBOM(config.SyftDownloadURL, "/kaniko/.docker", execRunner, fileUtils, httpClient, commonPipelineEnvironment.container.registryURL, commonPipelineEnvironment.container.imageNameTags)
}
return nil
case config.MultipleImages != nil:
log.Entry().Debugf("multipleImages build activated")
parsedMultipleImages, err := parseMultipleImages(config.MultipleImages)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrap(err, "failed to parse multipleImages param")
}
for _, entry := range parsedMultipleImages {
switch {
case entry.ContextSubPath == "":
return fmt.Errorf("multipleImages: empty contextSubPath")
case entry.ContainerImageName != "":
containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "multipleImages: failed to read registry url %v", config.ContainerRegistryURL)
}
if entry.ContainerImageTag == "" {
if config.ContainerImageTag == "" {
return fmt.Errorf("both multipleImages containerImageTag and config.containerImageTag are empty")
}
entry.ContainerImageTag = config.ContainerImageTag
}
// Docker image tags don't allow plus signs in tags, thus replacing with dash
containerImageTag := strings.ReplaceAll(entry.ContainerImageTag, "+", "-")
containerImageNameAndTag := fmt.Sprintf("%v:%v", entry.ContainerImageName, containerImageTag)
log.Entry().Debugf("multipleImages: image build '%v'", entry.ContainerImageName)
buildOptions := append(config.BuildOptions,
"--context-sub-path", entry.ContextSubPath,
"--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag),
)
if err = runKaniko(config.DockerfilePath, buildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
return fmt.Errorf("multipleImages: failed to build image '%v' using '%v': %w", entry.ContainerImageName, config.DockerfilePath, err)
}
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, entry.ContainerImageName)
case entry.ContainerImage != "":
containerImageName, err := docker.ContainerImageNameFromImage(entry.ContainerImage)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "invalid name part in image %v", entry.ContainerImage)
}
containerImageNameTag, err := docker.ContainerImageNameTagFromImage(entry.ContainerImage)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "invalid tag part in image %v", entry.ContainerImage)
}
log.Entry().Debugf("multipleImages: image build '%v'", containerImageName)
buildOptions := append(config.BuildOptions,
"--context-sub-path", entry.ContextSubPath,
"--destination", entry.ContainerImage,
)
if err = runKaniko(config.DockerfilePath, buildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
return fmt.Errorf("multipleImages: failed to build image '%v' using '%v': %w", containerImageName, config.DockerfilePath, err)
}
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
default:
return fmt.Errorf("multipleImages: either containerImageName or containerImage must be filled")
}
}
// for compatibility reasons also fill single imageNameTag field with "root" image in commonPipelineEnvironment
containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, config.ContainerImageTag)
commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
if config.CreateBOM {
// Syft for multi image, generates bom-docker-(1/2/3).xml
return syft.GenerateSBOM(config.SyftDownloadURL, "/kaniko/.docker", execRunner, fileUtils, httpClient, commonPipelineEnvironment.container.registryURL, commonPipelineEnvironment.container.imageNameTags)
}
return nil
case piperutils.ContainsString(config.BuildOptions, "--destination"):
log.Entry().Infof("Running Kaniko build with destination defined via buildOptions: %v", config.BuildOptions)
for i, o := range config.BuildOptions {
if o == "--destination" && i+1 < len(config.BuildOptions) {
destination = config.BuildOptions[i+1]
break
destination := config.BuildOptions[i+1]
containerRegistry, err := docker.ContainerRegistryFromImage(destination)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "invalid registry part in image %v", destination)
}
if commonPipelineEnvironment.container.registryURL == "" {
commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%v", containerRegistry)
}
// errors are already caught with previous call to docker.ContainerRegistryFromImage
containerImageName, _ := docker.ContainerImageNameFromImage(destination)
containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(destination)
if commonPipelineEnvironment.container.imageNameTag == "" {
commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
}
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
}
}
containerRegistry, err := docker.ContainerRegistryFromImage(destination)
case config.ContainerRegistryURL != "" && config.ContainerImageName != "" && config.ContainerImageTag != "":
log.Entry().Debugf("Single image build for image name '%v'", config.ContainerImageName)
containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "invalid registry part in image %v", destination)
return errors.Wrapf(err, "failed to read registry url %v", config.ContainerRegistryURL)
}
containerImageName, _ := docker.ContainerImageNameFromImage(destination)
containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(destination)
// Docker image tags don't allow plus signs in tags, thus replacing with dash
containerImageTag := strings.ReplaceAll(config.ContainerImageTag, "+", "-")
containerImageNameAndTag := fmt.Sprintf("%v:%v", config.ContainerImageName, containerImageTag)
commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
commonPipelineEnvironment.container.imageNameTag = containerImageNameAndTag
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameAndTag)
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, config.ContainerImageName)
config.BuildOptions = append(config.BuildOptions, "--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageNameAndTag))
case config.ContainerImage != "":
log.Entry().Debugf("Single image build for image '%v'", config.ContainerImage)
containerRegistry, err := docker.ContainerRegistryFromImage(config.ContainerImage)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "invalid registry part in image %v", config.ContainerImage)
}
// errors are already caught with previous call to docker.ContainerRegistryFromImage
containerImageName, _ := docker.ContainerImageNameFromImage(config.ContainerImage)
containerImageNameTag, _ := docker.ContainerImageNameTagFromImage(config.ContainerImage)
commonPipelineEnvironment.container.registryURL = fmt.Sprintf("https://%v", containerRegistry)
commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
commonPipelineEnvironment.container.imageNameTags = append(commonPipelineEnvironment.container.imageNameTags, containerImageNameTag)
commonPipelineEnvironment.container.imageNames = append(commonPipelineEnvironment.container.imageNames, containerImageName)
config.BuildOptions = append(config.BuildOptions, "--destination", config.ContainerImage)
default:
config.BuildOptions = append(config.BuildOptions, "--no-push")
}
// no support for building multiple containers
kanikoErr := runKaniko(config.DockerfilePath, config.BuildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment)
if kanikoErr != nil {
return kanikoErr
if err = runKaniko(config.DockerfilePath, config.BuildOptions, config.ReadImageDigest, execRunner, fileUtils, commonPipelineEnvironment); err != nil {
return err
}
if config.CreateBOM {
// Syft for single image, generates bom-docker-0.xml
return syft.GenerateSBOM(config.SyftDownloadURL, "/kaniko/.docker", execRunner, fileUtils, httpClient, commonPipelineEnvironment.container.registryURL, commonPipelineEnvironment.container.imageNameTags)
}
return nil
}
@ -254,7 +360,9 @@ func runKaniko(dockerFilepath string, buildOptions []string, readDigest bool, ex
return fmt.Errorf("failed to get current working directory: %w", err)
}
kanikoOpts := []string{"--dockerfile", dockerFilepath, "--context", cwd}
// kaniko build context needs a proper prefix, for local directory it is 'dir://'
// for more details see https://github.com/GoogleContainerTools/kaniko#kaniko-build-contexts
kanikoOpts := []string{"--dockerfile", dockerFilepath, "--context", "dir://" + cwd}
kanikoOpts = append(kanikoOpts, buildOptions...)
tmpDir, err := fileUtils.TempDir("", "*-kanikoExecute")
@ -280,7 +388,6 @@ func runKaniko(dockerFilepath string, buildOptions []string, readDigest bool, ex
if b, err := fileUtils.FileExists(digestFilePath); err == nil && b {
digest, err := fileUtils.FileRead(digestFilePath)
if err != nil {
return errors.Wrap(err, "error while reading image digest")
}
@ -289,9 +396,31 @@ func runKaniko(dockerFilepath string, buildOptions []string, readDigest bool, ex
log.Entry().Debugf("image digest: %s", digestStr)
commonPipelineEnvironment.container.imageDigest = string(digestStr)
commonPipelineEnvironment.container.imageDigest = digestStr
commonPipelineEnvironment.container.imageDigests = append(commonPipelineEnvironment.container.imageDigests, digestStr)
}
return nil
}
type multipleImageConf struct {
ContextSubPath string `json:"contextSubPath,omitempty"`
ContainerImageName string `json:"containerImageName,omitempty"`
ContainerImageTag string `json:"containerImageTag,omitempty"`
ContainerImage string `json:"containerImage,omitempty"`
}
func parseMultipleImages(src []map[string]interface{}) ([]multipleImageConf, error) {
var result []multipleImageConf
for _, conf := range src {
var structuredConf multipleImageConf
if err := mapstructure.Decode(conf, &structuredConf); err != nil {
return nil, err
}
result = append(result, structuredConf)
}
return result, nil
}

View File

@ -22,26 +22,27 @@ import (
)
type kanikoExecuteOptions struct {
BuildOptions []string `json:"buildOptions,omitempty"`
BuildSettingsInfo string `json:"buildSettingsInfo,omitempty"`
ContainerBuildOptions string `json:"containerBuildOptions,omitempty"`
ContainerImage string `json:"containerImage,omitempty"`
ContainerImageName string `json:"containerImageName,omitempty" validate:"required_if=ContainerMultiImageBuild true"`
ContainerImageTag string `json:"containerImageTag,omitempty"`
ContainerMultiImageBuild bool `json:"containerMultiImageBuild,omitempty"`
ContainerMultiImageBuildExcludes []string `json:"containerMultiImageBuildExcludes,omitempty"`
ContainerMultiImageBuildTrimDir string `json:"containerMultiImageBuildTrimDir,omitempty"`
ContainerPreparationCommand string `json:"containerPreparationCommand,omitempty"`
ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"`
ContainerRegistryUser string `json:"containerRegistryUser,omitempty"`
ContainerRegistryPassword string `json:"containerRegistryPassword,omitempty"`
CustomTLSCertificateLinks []string `json:"customTlsCertificateLinks,omitempty"`
DockerConfigJSON string `json:"dockerConfigJSON,omitempty"`
DockerfilePath string `json:"dockerfilePath,omitempty"`
TargetArchitectures []string `json:"targetArchitectures,omitempty"`
ReadImageDigest bool `json:"readImageDigest,omitempty"`
CreateBOM bool `json:"createBOM,omitempty"`
SyftDownloadURL string `json:"syftDownloadUrl,omitempty"`
BuildOptions []string `json:"buildOptions,omitempty"`
BuildSettingsInfo string `json:"buildSettingsInfo,omitempty"`
ContainerBuildOptions string `json:"containerBuildOptions,omitempty"`
ContainerImage string `json:"containerImage,omitempty"`
ContainerImageName string `json:"containerImageName,omitempty" validate:"required_if=ContainerMultiImageBuild true"`
ContainerImageTag string `json:"containerImageTag,omitempty"`
MultipleImages []map[string]interface{} `json:"multipleImages,omitempty"`
ContainerMultiImageBuild bool `json:"containerMultiImageBuild,omitempty"`
ContainerMultiImageBuildExcludes []string `json:"containerMultiImageBuildExcludes,omitempty"`
ContainerMultiImageBuildTrimDir string `json:"containerMultiImageBuildTrimDir,omitempty"`
ContainerPreparationCommand string `json:"containerPreparationCommand,omitempty"`
ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"`
ContainerRegistryUser string `json:"containerRegistryUser,omitempty"`
ContainerRegistryPassword string `json:"containerRegistryPassword,omitempty"`
CustomTLSCertificateLinks []string `json:"customTlsCertificateLinks,omitempty"`
DockerConfigJSON string `json:"dockerConfigJSON,omitempty"`
DockerfilePath string `json:"dockerfilePath,omitempty"`
TargetArchitectures []string `json:"targetArchitectures,omitempty"`
ReadImageDigest bool `json:"readImageDigest,omitempty"`
CreateBOM bool `json:"createBOM,omitempty"`
SyftDownloadURL string `json:"syftDownloadUrl,omitempty"`
}
type kanikoExecuteCommonPipelineEnvironment struct {
@ -299,6 +300,7 @@ func addKanikoExecuteFlags(cmd *cobra.Command, stepConfig *kanikoExecuteOptions)
cmd.Flags().StringVar(&stepConfig.ContainerImage, "containerImage", os.Getenv("PIPER_containerImage"), "Defines the full name of the Docker image to be created including registry, image name and tag like `my.docker.registry/path/myImageName:myTag`. If `containerImage` is not provided, then `containerImageName` or `--destination` (via buildOptions) should be provided.")
cmd.Flags().StringVar(&stepConfig.ContainerImageName, "containerImageName", os.Getenv("PIPER_containerImageName"), "Name of the container which will be built - will be used instead of parameter `containerImage`. If `containerImageName` is not provided, then `containerImage` or `--destination` (via buildOptions) should be provided.")
cmd.Flags().StringVar(&stepConfig.ContainerImageTag, "containerImageTag", os.Getenv("PIPER_containerImageTag"), "Tag of the container which will be built - will be used instead of parameter `containerImage`")
cmd.Flags().BoolVar(&stepConfig.ContainerMultiImageBuild, "containerMultiImageBuild", false, "Defines if multiple containers should be build. Dockerfiles are used using the pattern **/Dockerfile*. Excludes can be defined via [`containerMultiImageBuildExcludes`](#containermultiimagebuildexscludes).")
cmd.Flags().StringSliceVar(&stepConfig.ContainerMultiImageBuildExcludes, "containerMultiImageBuildExcludes", []string{}, "Defines a list of Dockerfile paths to exclude from the build when using [`containerMultiImageBuild`](#containermultiimagebuild).")
cmd.Flags().StringVar(&stepConfig.ContainerMultiImageBuildTrimDir, "containerMultiImageBuildTrimDir", os.Getenv("PIPER_containerMultiImageBuildTrimDir"), "Defines a trailing directory part which should not be considered in the final image name.")
@ -394,6 +396,14 @@ func kanikoExecuteMetadata() config.StepData {
Aliases: []config.Alias{{Name: "artifactVersion"}},
Default: os.Getenv("PIPER_containerImageTag"),
},
{
Name: "multipleImages",
ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "[]map[string]interface{}",
Mandatory: false,
Aliases: []config.Alias{{Name: "images"}},
},
{
Name: "containerMultiImageBuild",
ResourceRef: []config.ResourceReference{},

View File

@ -93,7 +93,7 @@ func TestRunKanikoExecute(t *testing.T) {
assert.Equal(t, "/kaniko/executor", execRunner.Calls[1].Exec)
cwd, _ := fileUtils.Getwd()
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", cwd, "--skip-tls-verify-pull", "--destination", "myImage:tag"}, execRunner.Calls[1].Params)
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", "dir://" + cwd, "--skip-tls-verify-pull", "--destination", "myImage:tag"}, execRunner.Calls[1].Params)
assert.Contains(t, commonPipelineEnvironment.custom.buildSettingsInfo, `"mavenExecuteBuild":[{"dockerImage":"maven"}]`)
assert.Contains(t, commonPipelineEnvironment.custom.buildSettingsInfo, `"kanikoExecute":[{"dockerImage":"gcr.io/kaniko-project/executor:debug"}]`)
@ -144,7 +144,7 @@ func TestRunKanikoExecute(t *testing.T) {
assert.Equal(t, "/kaniko/executor", execRunner.Calls[1].Exec)
cwd, _ := fileUtils.Getwd()
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", cwd, "--skip-tls-verify-pull", "--destination", "myImage:tag", "--digest-file", "/tmp/*-kanikoExecutetest/digest.txt"}, execRunner.Calls[1].Params)
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", "dir://" + cwd, "--skip-tls-verify-pull", "--destination", "myImage:tag", "--digest-file", "/tmp/*-kanikoExecutetest/digest.txt"}, execRunner.Calls[1].Params)
assert.Contains(t, commonPipelineEnvironment.custom.buildSettingsInfo, `"mavenExecuteBuild":[{"dockerImage":"maven"}]`)
assert.Contains(t, commonPipelineEnvironment.custom.buildSettingsInfo, `"kanikoExecute":[{"dockerImage":"gcr.io/kaniko-project/executor:debug"}]`)
@ -194,7 +194,7 @@ func TestRunKanikoExecute(t *testing.T) {
assert.Equal(t, "/kaniko/executor", execRunner.Calls[1].Exec)
cwd, _ := fileUtils.Getwd()
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", cwd, "--skip-tls-verify-pull", "--destination", "my.registry.com:50000/myImage:1.2.3-a-x"}, execRunner.Calls[1].Params)
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", "dir://" + cwd, "--skip-tls-verify-pull", "--destination", "my.registry.com:50000/myImage:1.2.3-a-x"}, execRunner.Calls[1].Params)
assert.Equal(t, "myImage:1.2.3-a-x", commonPipelineEnvironment.container.imageNameTag)
assert.Equal(t, "https://my.registry.com:50000", commonPipelineEnvironment.container.registryURL)
@ -238,7 +238,7 @@ func TestRunKanikoExecute(t *testing.T) {
assert.Equal(t, "/kaniko/executor", execRunner.Calls[1].Exec)
cwd, _ := fileUtils.Getwd()
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", cwd, "--skip-tls-verify-pull", "--destination", "my.other.registry.com:50000/myImage:3.2.1-a-x"}, execRunner.Calls[1].Params)
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", "dir://" + cwd, "--skip-tls-verify-pull", "--destination", "my.other.registry.com:50000/myImage:3.2.1-a-x"}, execRunner.Calls[1].Params)
assert.Equal(t, "myImage:3.2.1-a-x", commonPipelineEnvironment.container.imageNameTag)
assert.Equal(t, "https://my.other.registry.com:50000", commonPipelineEnvironment.container.registryURL)
@ -300,7 +300,7 @@ func TestRunKanikoExecute(t *testing.T) {
assert.Equal(t, `{"auths":{}}`, string(c))
cwd, _ := fileUtils.Getwd()
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", cwd, "--skip-tls-verify-pull", "--no-push"}, execRunner.Calls[1].Params)
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", "dir://" + cwd, "--skip-tls-verify-pull", "--no-push"}, execRunner.Calls[1].Params)
})
t.Run("success case - backward compatibility", func(t *testing.T) {
@ -327,7 +327,7 @@ func TestRunKanikoExecute(t *testing.T) {
assert.NoError(t, err)
cwd, _ := fileUtils.Getwd()
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", cwd, "--skip-tls-verify-pull", "--destination", "myImage:tag"}, execRunner.Calls[1].Params)
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", "dir://" + cwd, "--skip-tls-verify-pull", "--destination", "myImage:tag"}, execRunner.Calls[1].Params)
})
t.Run("success case - createBOM", func(t *testing.T) {
@ -391,9 +391,9 @@ func TestRunKanikoExecute(t *testing.T) {
cwd, _ := fileUtils.Getwd()
expectedParams := [][]string{
{"--dockerfile", "Dockerfile", "--context", cwd, "--destination", "my.registry.com:50000/myImage:myTag"},
{"--dockerfile", filepath.Join("sub1", "Dockerfile"), "--context", cwd, "--destination", "my.registry.com:50000/myImage-sub1:myTag"},
{"--dockerfile", filepath.Join("sub2", "Dockerfile"), "--context", cwd, "--destination", "my.registry.com:50000/myImage-sub2:myTag"},
{"--dockerfile", "Dockerfile", "--context", "dir://" + cwd, "--destination", "my.registry.com:50000/myImage:myTag"},
{"--dockerfile", filepath.Join("sub1", "Dockerfile"), "--context", "dir://" + cwd, "--destination", "my.registry.com:50000/myImage-sub1:myTag"},
{"--dockerfile", filepath.Join("sub2", "Dockerfile"), "--context", "dir://" + cwd, "--destination", "my.registry.com:50000/myImage-sub2:myTag"},
}
// need to go this way since we cannot count on the correct order
for _, call := range execRunner.Calls {
@ -447,8 +447,8 @@ func TestRunKanikoExecute(t *testing.T) {
cwd, _ := fileUtils.Getwd()
expectedParams := [][]string{
{"--dockerfile", filepath.Join("sub1", "Dockerfile"), "--context", cwd, "--destination", "my.registry.com:50000/myImage-sub1:myTag"},
{"--dockerfile", filepath.Join("sub2", "Dockerfile"), "--context", cwd, "--destination", "my.registry.com:50000/myImage-sub2:myTag"},
{"--dockerfile", filepath.Join("sub1", "Dockerfile"), "--context", "dir://" + cwd, "--destination", "my.registry.com:50000/myImage-sub1:myTag"},
{"--dockerfile", filepath.Join("sub2", "Dockerfile"), "--context", "dir://" + cwd, "--destination", "my.registry.com:50000/myImage-sub2:myTag"},
}
// need to go this way since we cannot count on the correct order
for _, call := range execRunner.Calls {
@ -507,9 +507,9 @@ func TestRunKanikoExecute(t *testing.T) {
cwd, _ := fileUtils.Getwd()
expectedParams := [][]string{
{"--dockerfile", "Dockerfile", "--context", cwd, "--destination", "my.registry.com:50000/myImage:myTag"},
{"--dockerfile", filepath.Join("sub1", "Dockerfile"), "--context", cwd, "--destination", "my.registry.com:50000/myImage-sub1:myTag"},
{"--dockerfile", filepath.Join("sub2", "Dockerfile"), "--context", cwd, "--destination", "my.registry.com:50000/myImage-sub2:myTag"},
{"--dockerfile", "Dockerfile", "--context", "dir://" + cwd, "--destination", "my.registry.com:50000/myImage:myTag"},
{"--dockerfile", filepath.Join("sub1", "Dockerfile"), "--context", "dir://" + cwd, "--destination", "my.registry.com:50000/myImage-sub1:myTag"},
{"--dockerfile", filepath.Join("sub2", "Dockerfile"), "--context", "dir://" + cwd, "--destination", "my.registry.com:50000/myImage-sub2:myTag"},
{"packages", "registry:my.registry.com:50000/myImage:myTag", "-o", "cyclonedx-xml", "--file"},
{"packages", "registry:my.registry.com:50000/myImage-sub1:myTag", "-o", "cyclonedx-xml", "--file"},
{"packages", "registry:my.registry.com:50000/myImage-sub2:myTag", "-o", "cyclonedx-xml", "--file"},
@ -614,6 +614,84 @@ func TestRunKanikoExecute(t *testing.T) {
assert.Equal(t, `{"auths":{"https://my.registry.com:50000":{"auth":"ZHVtbXlVc2VyOmR1bW15UGFzc3dvcmQ="}}}`, string(c))
})
t.Run("success case - multi context build with CreateBOM", func(t *testing.T) {
config := &kanikoExecuteOptions{
ContainerImageName: "myImage",
ContainerImageTag: "myTag",
ContainerRegistryURL: "https://my.registry.com:50000",
DockerConfigJSON: "path/to/docker/config.json",
DockerfilePath: "Dockerfile",
CreateBOM: true,
SyftDownloadURL: "http://test-syft-url.io",
MultipleImages: []map[string]interface{}{
{
"contextSubPath": "/test1",
"containerImageName": "myImageOne",
},
{
"contextSubPath": "/test2",
"containerImageName": "myImageTwo",
"containerImageTag": "myTagTwo",
},
},
}
execRunner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("path/to/docker/config.json", []byte(`{"auths":{"custom":"test"}}`))
fileUtils.AddFile("Dockerfile", []byte("some content"))
fileUtils.AddFile("test1/test", []byte("some content test1"))
fileUtils.AddFile("test2/test", []byte("some content test2"))
httpmock.Activate()
defer httpmock.DeactivateAndReset()
fakeArchive, err := fileUtils.CreateArchive(map[string][]byte{"syft": []byte("test")})
assert.NoError(t, err)
httpmock.RegisterResponder(http.MethodGet, "http://test-syft-url.io", httpmock.NewBytesResponder(http.StatusOK, fakeArchive))
client := &piperhttp.Client{}
client.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})
err = runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, execRunner, client, fileUtils)
assert.NoError(t, err)
assert.Equal(t, 4, len(execRunner.Calls))
assert.Equal(t, "/kaniko/executor", execRunner.Calls[0].Exec)
assert.Equal(t, "/kaniko/executor", execRunner.Calls[1].Exec)
cwd, _ := fileUtils.Getwd()
expectedParams := [][]string{
{"--dockerfile", "Dockerfile", "--context", "dir://" + cwd, "--context-sub-path", "/test1", "--destination", "my.registry.com:50000/myImageOne:myTag"},
{"--dockerfile", "Dockerfile", "--context", "dir://" + cwd, "--context-sub-path", "/test2", "--destination", "my.registry.com:50000/myImageTwo:myTagTwo"},
{"packages", "registry:my.registry.com:50000/myImageOne:myTag", "-o", "cyclonedx-xml", "--file"},
{"packages", "registry:my.registry.com:50000/myImageTwo:myTagTwo", "-o", "cyclonedx-xml", "--file"},
}
// need to go this way since we cannot count on the correct order
for index, call := range execRunner.Calls {
found := false
for _, expected := range expectedParams {
if expected[0] == "packages" {
expected = append(expected, fmt.Sprintf("bom-docker-%d.xml", index-2), "-q")
}
if strings.Join(call.Params, " ") == strings.Join(expected, " ") {
found = true
break
}
}
assert.True(t, found, fmt.Sprintf("%v not found", call.Params))
}
assert.Equal(t, "https://my.registry.com:50000", commonPipelineEnvironment.container.registryURL)
assert.Equal(t, "myImage:myTag", commonPipelineEnvironment.container.imageNameTag)
assert.Contains(t, commonPipelineEnvironment.container.imageNames, "myImageOne")
assert.Contains(t, commonPipelineEnvironment.container.imageNames, "myImageTwo")
assert.Contains(t, commonPipelineEnvironment.container.imageNameTags, "myImageOne:myTag")
assert.Contains(t, commonPipelineEnvironment.container.imageNameTags, "myImageTwo:myTagTwo")
assert.Equal(t, "", commonPipelineEnvironment.container.imageDigest)
assert.Empty(t, commonPipelineEnvironment.container.imageDigests)
})
t.Run("error case - multi image build: no docker files", func(t *testing.T) {
config := &kanikoExecuteOptions{
ContainerImageName: "myImage",
@ -762,4 +840,27 @@ func TestRunKanikoExecute(t *testing.T) {
assert.EqualError(t, err, "failed to write file '/kaniko/.docker/config.json': write error")
})
t.Run("error case - multi context build: no subcontext provided", func(t *testing.T) {
config := &kanikoExecuteOptions{
ContainerImageName: "myImage",
ContainerImageTag: "myTag",
ContainerRegistryURL: "https://my.registry.com:50000",
MultipleImages: []map[string]interface{}{
{"containerImageName": "myImageOne"},
{"containerImageName": "myImageTwo"},
},
}
cpe := kanikoExecuteCommonPipelineEnvironment{}
execRunner := &mock.ExecMockRunner{}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("Dockerfile", []byte("some content"))
err := runKanikoExecute(config, &telemetry.CustomData{}, &cpe, execRunner, nil, fileUtils)
assert.Error(t, err)
assert.Contains(t, fmt.Sprint(err), "multipleImages: empty contextSubPath")
})
}

View File

@ -144,6 +144,36 @@ spec:
resourceRef:
- name: commonPipelineEnvironment
param: artifactVersion
- name: multipleImages
aliases:
- name: images
type: "[]map[string]interface{}"
description: |
This parameter is only needed if `kanikoExecute` should create multiple images using the same root Dockerfile, but with different sub-contexts.
Otherwise it can be ignored!!!
In case of multiple images, this array contains one entry for each image.
Either containerImageName OR containerImage MUST be provided for each entry.
contextSubPath MUST be provided for each entry.
Array keys:
contextSubPath - Set a context subpath.
containerImageName - Name of the container which will be built.
containerImageTag - Tag of the container which will be built. If empty - root containerImageTag will be used.
containerImage - Defines the full name of the Docker image to be created including registry.
```yaml
containerRegistryUrl: docker.io
containerImageTag: latest
multipleImages:
- containerImageName: myImage1
containerImageTag: v1.0.0
contextSubPath: path/to/folder
```
scope:
- PARAMETERS
- STAGES
- STEPS
- name: containerMultiImageBuild
type: bool
description: Defines if multiple containers should be build. Dockerfiles are used using the pattern **/Dockerfile*. Excludes can be defined via [`containerMultiImageBuildExcludes`](#containermultiimagebuildexscludes).