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

feat(kanikoExecute): allow building multiple images (#3443)

* feat(kanikoExecute): allow building multiple images

* enhance tests

* chore: allow running tests in parallel

* small fixes

* fix: fix destination bug

* update formatting and defaults

* fix yml formatting

* chore: change cpe parameter names

* chore: improve variable naming
This commit is contained in:
Oliver Nocon 2022-02-07 07:58:41 +01:00 committed by GitHub
parent 56726d96f1
commit 2ae1d9dac1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 535 additions and 130 deletions

View File

@ -2,7 +2,7 @@ package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/SAP/jenkins-library/pkg/buildsettings"
@ -52,9 +52,11 @@ func runKanikoExecute(config *kanikoExecuteOptions, telemetryData *telemetry.Cus
// prepare kaniko container for running with proper Docker config.json and custom certificates
// custom certificates will be downloaded and appended to ca-certificates.crt file used in container
prepCommand := strings.Split(config.ContainerPreparationCommand, " ")
if err := execRunner.RunExecutable(prepCommand[0], prepCommand[1:]...); err != nil {
return errors.Wrap(err, "failed to initialize Kaniko container")
if len(config.ContainerPreparationCommand) > 0 {
prepCommand := strings.Split(config.ContainerPreparationCommand, " ")
if err := execRunner.RunExecutable(prepCommand[0], prepCommand[1:]...); err != nil {
return errors.Wrap(err, "failed to initialize Kaniko container")
}
}
if len(config.CustomTLSCertificateLinks) > 0 {
@ -66,6 +68,39 @@ func runKanikoExecute(config *kanikoExecuteOptions, telemetryData *telemetry.Cus
log.Entry().Info("skipping updation of certificates")
}
dockerConfig := []byte(`{"auths":{}}`)
if len(config.DockerConfigJSON) > 0 {
var err error
dockerConfig, err = fileUtils.FileRead(config.DockerConfigJSON)
if err != nil {
return errors.Wrapf(err, "failed to read file '%v'", config.DockerConfigJSON)
}
}
if err := fileUtils.FileWrite("/kaniko/.docker/config.json", dockerConfig, 0644); err != nil {
return errors.Wrap(err, "failed to write file '/kaniko/.docker/config.json'")
}
log.Entry().Debugf("preparing build settings information...")
stepName := "kanikoExecute"
// ToDo: better testability required. So far retrieval of config is rather non deterministic
dockerImage, err := getDockerImageValue(stepName)
if err != nil {
return fmt.Errorf("failed to retrieve dockerImage configuration: %w", err)
}
kanikoConfig := buildsettings.BuildOptions{
DockerImage: dockerImage,
BuildSettingsInfo: config.BuildSettingsInfo,
}
log.Entry().Debugf("creating build settings information...")
buildSettingsInfo, err := buildsettings.CreateBuildSettingsInfo(&kanikoConfig, stepName)
if err != nil {
log.Entry().Warnf("failed to create build settings info: %v", err)
}
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 {
@ -74,11 +109,51 @@ func runKanikoExecute(config *kanikoExecuteOptions, telemetryData *telemetry.Cus
log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "failed to read registry url %v", config.ContainerRegistryURL)
}
containerImageTag := fmt.Sprintf("%v:%v", config.ContainerImageName, strings.ReplaceAll(config.ContainerImageTag, "+", "-"))
dest = []string{"--destination", fmt.Sprintf("%v/%v", containerRegistry, containerImageTag)}
commonPipelineEnvironment.container.registryURL = config.ContainerRegistryURL
commonPipelineEnvironment.container.imageNameTag = containerImageTag
// 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, 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, execRunner)
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
}
return nil
}
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)
@ -91,45 +166,19 @@ func runKanikoExecute(config *kanikoExecuteOptions, telemetryData *telemetry.Cus
commonPipelineEnvironment.container.imageNameTag = containerImageNameTag
}
config.BuildOptions = append(config.BuildOptions, dest...)
} else {
log.Entry().Infof("Running Kaniko build with destination defined via buildOptions: %v", config.BuildOptions)
}
dockerConfig := []byte(`{"auths":{}}`)
if len(config.DockerConfigJSON) > 0 {
var err error
dockerConfig, err = fileUtils.FileRead(config.DockerConfigJSON)
if err != nil {
return errors.Wrapf(err, "failed to read file '%v'", config.DockerConfigJSON)
}
}
// no support for building multiple containers
return runKaniko(config.DockerfilePath, config.BuildOptions, execRunner)
}
log.Entry().Debugf("creating build settings information...")
stepName := "kanikoExecute"
dockerImage, err := getDockerImageValue(stepName)
if err != nil {
return err
}
func runKaniko(dockerFilepath string, buildOptions []string, execRunner command.ExecRunner) error {
kanikoOpts := []string{"--dockerfile", dockerFilepath, "--context", filepath.Dir(dockerFilepath)}
kanikoOpts = append(kanikoOpts, buildOptions...)
kanikoConfig := buildsettings.BuildOptions{
DockerImage: dockerImage,
}
buildSettingsInfo, err := buildsettings.CreateBuildSettingsInfo(&kanikoConfig, stepName)
if err != nil {
log.Entry().Warnf("failed to create build settings info: %v", err)
}
commonPipelineEnvironment.custom.buildSettingsInfo = buildSettingsInfo
if err := fileUtils.FileWrite("/kaniko/.docker/config.json", dockerConfig, 0644); err != nil {
return errors.Wrap(err, "failed to write file '/kaniko/.docker/config.json'")
}
cwd, err := os.Getwd()
if err != nil {
return errors.Wrap(err, "failed to get current working directory")
}
kanikoOpts := []string{"--dockerfile", config.DockerfilePath, "--context", cwd}
kanikoOpts = append(kanikoOpts, config.BuildOptions...)
err = execRunner.RunExecutable("/kaniko/executor", kanikoOpts...)
err := execRunner.RunExecutable("/kaniko/executor", kanikoOpts...)
if err != nil {
log.SetErrorCategory(log.ErrorBuild)
return errors.Wrap(err, "execution of '/kaniko/executor' failed")

View File

@ -18,22 +18,27 @@ import (
)
type kanikoExecuteOptions struct {
BuildOptions []string `json:"buildOptions,omitempty"`
ContainerBuildOptions string `json:"containerBuildOptions,omitempty"`
ContainerImage string `json:"containerImage,omitempty"`
ContainerImageName string `json:"containerImageName,omitempty"`
ContainerImageTag string `json:"containerImageTag,omitempty"`
ContainerPreparationCommand string `json:"containerPreparationCommand,omitempty"`
ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"`
CustomTLSCertificateLinks []string `json:"customTlsCertificateLinks,omitempty"`
DockerConfigJSON string `json:"dockerConfigJSON,omitempty"`
DockerfilePath string `json:"dockerfilePath,omitempty"`
BuildOptions []string `json:"buildOptions,omitempty"`
BuildSettingsInfo string `json:"buildSettingsInfo,omitempty"`
ContainerMultiImageBuild bool `json:"containerMultiImageBuild,omitempty"`
ContainerMultiImageBuildExcludes []string `json:"containerMultiImageBuildExcludes,omitempty"`
ContainerBuildOptions string `json:"containerBuildOptions,omitempty"`
ContainerImage string `json:"containerImage,omitempty"`
ContainerImageName string `json:"containerImageName,omitempty"`
ContainerImageTag string `json:"containerImageTag,omitempty"`
ContainerPreparationCommand string `json:"containerPreparationCommand,omitempty"`
ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"`
CustomTLSCertificateLinks []string `json:"customTlsCertificateLinks,omitempty"`
DockerConfigJSON string `json:"dockerConfigJSON,omitempty"`
DockerfilePath string `json:"dockerfilePath,omitempty"`
}
type kanikoExecuteCommonPipelineEnvironment struct {
container struct {
registryURL string
imageNameTag string
registryURL string
imageNameTag string
imageNames []string
imageNameTags []string
}
custom struct {
buildSettingsInfo string
@ -48,6 +53,8 @@ func (p *kanikoExecuteCommonPipelineEnvironment) persist(path, resourceName stri
}{
{category: "container", name: "registryUrl", value: p.container.registryURL},
{category: "container", name: "imageNameTag", value: p.container.imageNameTag},
{category: "container", name: "imageNames", value: p.container.imageNames},
{category: "container", name: "imageNameTags", value: p.container.imageNameTags},
{category: "custom", name: "buildSettingsInfo", value: p.custom.buildSettingsInfo},
}
@ -79,7 +86,21 @@ func KanikoExecuteCommand() *cobra.Command {
var createKanikoExecuteCmd = &cobra.Command{
Use: STEP_NAME,
Short: "Executes a [Kaniko](https://github.com/GoogleContainerTools/kaniko) build for creating a Docker container.",
Long: `Executes a [Kaniko](https://github.com/GoogleContainerTools/kaniko) build for creating a Docker container.`,
Long: `Executes a [Kaniko](https://github.com/GoogleContainerTools/kaniko) build for creating a Docker container.
### Building multiple container images
The step allows you to build multiple container images with one run.
This is suitable in case you need to create multiple images for one microservice, e.g. for testing.
All images will get the same "root" name and the same versioning.<br />
**Thus, this is not suitable to be used for a monorepo approach!** For monorepos you need to use a build tool natively capable to take care for monorepos
or implement a custom logic and for example execute this ` + "`" + `kanikoExecute` + "`" + ` step multiple times in your custom pipeline.
You can activate multiple builds using the parameters
* [containerMultiImageBuild](#containermultiimagebuild) for activation
* [containerMultiImageBuildExcludes](#containermultiimagebuildexcludes) for defining excludes`,
PreRunE: func(cmd *cobra.Command, _ []string) error {
startTime = time.Now()
log.SetStepName(STEP_NAME)
@ -156,7 +177,10 @@ func KanikoExecuteCommand() *cobra.Command {
}
func addKanikoExecuteFlags(cmd *cobra.Command, stepConfig *kanikoExecuteOptions) {
cmd.Flags().StringSliceVar(&stepConfig.BuildOptions, "buildOptions", []string{`--skip-tls-verify-pull`}, "Defines a list of build options for the [kaniko](https://github.com/GoogleContainerTools/kaniko) build.")
cmd.Flags().StringSliceVar(&stepConfig.BuildOptions, "buildOptions", []string{`--skip-tls-verify-pull`, `--ignore-path`, `/busybox`}, "Defines a list of build options for the [kaniko](https://github.com/GoogleContainerTools/kaniko) build.")
cmd.Flags().StringVar(&stepConfig.BuildSettingsInfo, "buildSettingsInfo", os.Getenv("PIPER_buildSettingsInfo"), "Build settings info is typically filled by the step automatically to create information about the build settings that were used during the mta build. This information is typically used for compliance related processes.")
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.ContainerBuildOptions, "containerBuildOptions", os.Getenv("PIPER_containerBuildOptions"), "Deprected, please use buildOptions. Defines the build options for the [kaniko](https://github.com/GoogleContainerTools/kaniko) build.")
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 left empty, image will not be pushed.")
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`")
@ -190,7 +214,39 @@ func kanikoExecuteMetadata() config.StepData {
Type: "[]string",
Mandatory: false,
Aliases: []config.Alias{},
Default: []string{`--skip-tls-verify-pull`},
Default: []string{`--skip-tls-verify-pull`, `--ignore-path`, `/busybox`},
},
{
Name: "buildSettingsInfo",
ResourceRef: []config.ResourceReference{
{
Name: "commonPipelineEnvironment",
Param: "custom/buildSettingsInfo",
},
},
Scope: []string{"STEPS", "STAGES", "PARAMETERS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_buildSettingsInfo"),
},
{
Name: "containerMultiImageBuild",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "bool",
Mandatory: false,
Aliases: []config.Alias{},
Default: false,
},
{
Name: "containerMultiImageBuildExcludes",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "[]string",
Mandatory: false,
Aliases: []config.Alias{},
Default: []string{},
},
{
Name: "containerBuildOptions",
@ -312,6 +368,8 @@ func kanikoExecuteMetadata() config.StepData {
Parameters: []map[string]interface{}{
{"name": "container/registryUrl"},
{"name": "container/imageNameTag"},
{"name": "container/imageNames", "type": "[]string"},
{"name": "container/imageNameTags", "type": "[]string"},
{"name": "custom/buildSettingsInfo"},
},
},

View File

@ -6,7 +6,8 @@ import (
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
@ -36,32 +37,16 @@ func (c *kanikoMockClient) SendRequest(method, url string, body io.Reader, heade
return &http.Response{StatusCode: c.httpStatusCode, Body: ioutil.NopCloser(bytes.NewReader([]byte(c.responseBody)))}, nil
}
type kanikoFileMock struct {
*mock.FilesMock
fileReadContent map[string]string
fileReadErr map[string]error
fileWriteContent map[string]string
fileWriteErr map[string]error
}
func (f *kanikoFileMock) FileRead(path string) ([]byte, error) {
if f.fileReadErr[path] != nil {
return []byte{}, f.fileReadErr[path]
}
return []byte(f.fileReadContent[path]), nil
}
func (f *kanikoFileMock) FileWrite(path string, content []byte, perm os.FileMode) error {
if f.fileWriteErr[path] != nil {
return f.fileWriteErr[path]
}
f.fileWriteContent[path] = string(content)
return nil
}
func TestRunKanikoExecute(t *testing.T) {
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
// required due to config resolution during build settings retrieval
// ToDo: proper mocking
openFileBak := configOptions.openFile
defer func() {
configOptions.openFile = openFileBak
}()
configOptions.openFile = configOpenFileMock
t.Run("success case", func(t *testing.T) {
config := &kanikoExecuteOptions{
@ -71,17 +56,18 @@ func TestRunKanikoExecute(t *testing.T) {
CustomTLSCertificateLinks: []string{"https://test.url/cert.crt"},
DockerfilePath: "Dockerfile",
DockerConfigJSON: "path/to/docker/config.json",
BuildSettingsInfo: `{"mavenExecuteBuild":[{"dockerImage":"maven"}]}`,
}
runner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
certClient := &kanikoMockClient{
responseBody: "testCert",
}
fileUtils := &kanikoFileMock{
fileReadContent: map[string]string{"path/to/docker/config.json": `{"auths":{"custom":"test"}}`},
fileWriteContent: map[string]string{},
}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("path/to/docker/config.json", []byte(`{"auths":{"custom":"test"}}`))
fileUtils.AddFile("/kaniko/ssl/certs/ca-certificates.crt", []byte(``))
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, certClient, fileUtils)
@ -91,12 +77,15 @@ func TestRunKanikoExecute(t *testing.T) {
assert.Equal(t, []string{"-f", "/kaniko/.docker/config.json"}, runner.Calls[0].Params)
assert.Equal(t, config.CustomTLSCertificateLinks, certClient.urlsCalled)
assert.Equal(t, `{"auths":{"custom":"test"}}`, fileUtils.fileWriteContent["/kaniko/.docker/config.json"])
c, err := fileUtils.FileRead("/kaniko/.docker/config.json")
assert.NoError(t, err)
assert.Equal(t, `{"auths":{"custom":"test"}}`, string(c))
assert.Equal(t, "/kaniko/executor", runner.Calls[1].Exec)
cwd, _ := os.Getwd()
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", cwd, "--skip-tls-verify-pull", "--destination", "myImage:tag"}, runner.Calls[1].Params)
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", ".", "--skip-tls-verify-pull", "--destination", "myImage:tag"}, runner.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"}]`)
})
t.Run("success case - image params", func(t *testing.T) {
@ -112,14 +101,14 @@ func TestRunKanikoExecute(t *testing.T) {
}
runner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
certClient := &kanikoMockClient{
responseBody: "testCert",
}
fileUtils := &kanikoFileMock{
fileReadContent: map[string]string{"path/to/docker/config.json": `{"auths":{"custom":"test"}}`},
fileWriteContent: map[string]string{},
}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("path/to/docker/config.json", []byte(`{"auths":{"custom":"test"}}`))
fileUtils.AddFile("/kaniko/ssl/certs/ca-certificates.crt", []byte(``))
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, certClient, fileUtils)
@ -129,11 +118,12 @@ func TestRunKanikoExecute(t *testing.T) {
assert.Equal(t, []string{"-f", "/kaniko/.docker/config.json"}, runner.Calls[0].Params)
assert.Equal(t, config.CustomTLSCertificateLinks, certClient.urlsCalled)
assert.Equal(t, `{"auths":{"custom":"test"}}`, fileUtils.fileWriteContent["/kaniko/.docker/config.json"])
c, err := fileUtils.FileRead("/kaniko/.docker/config.json")
assert.NoError(t, err)
assert.Equal(t, `{"auths":{"custom":"test"}}`, string(c))
assert.Equal(t, "/kaniko/executor", runner.Calls[1].Exec)
cwd, _ := os.Getwd()
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", cwd, "--skip-tls-verify-pull", "--destination", "my.registry.com:50000/myImage:1.2.3-a-x"}, runner.Calls[1].Params)
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", ".", "--skip-tls-verify-pull", "--destination", "my.registry.com:50000/myImage:1.2.3-a-x"}, runner.Calls[1].Params)
})
@ -150,12 +140,12 @@ func TestRunKanikoExecute(t *testing.T) {
}
runner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
certClient := &kanikoMockClient{}
fileUtils := &kanikoFileMock{
fileWriteContent: map[string]string{},
fileReadErr: map[string]error{"/kaniko/ssl/certs/ca-certificates.crt": fmt.Errorf("read error")},
}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("path/to/docker/config.json", []byte(``))
fileUtils.FileReadErrors = map[string]error{"/kaniko/ssl/certs/ca-certificates.crt": fmt.Errorf("read error")}
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, certClient, fileUtils)
@ -171,22 +161,23 @@ func TestRunKanikoExecute(t *testing.T) {
}
runner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
certClient := &kanikoMockClient{
responseBody: "testCert",
}
fileUtils := &kanikoFileMock{
fileWriteContent: map[string]string{},
}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("/kaniko/ssl/certs/ca-certificates.crt", []byte(``))
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, certClient, fileUtils)
assert.NoError(t, err)
assert.Equal(t, `{"auths":{}}`, fileUtils.fileWriteContent["/kaniko/.docker/config.json"])
c, err := fileUtils.FileRead("/kaniko/.docker/config.json")
assert.NoError(t, err)
assert.Equal(t, `{"auths":{}}`, string(c))
cwd, _ := os.Getwd()
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", cwd, "--skip-tls-verify-pull", "--no-push"}, runner.Calls[1].Params)
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", ".", "--skip-tls-verify-pull", "--no-push"}, runner.Calls[1].Params)
})
t.Run("success case - backward compatibility", func(t *testing.T) {
@ -200,20 +191,181 @@ func TestRunKanikoExecute(t *testing.T) {
}
runner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
certClient := &kanikoMockClient{
responseBody: "testCert",
}
fileUtils := &kanikoFileMock{
fileReadContent: map[string]string{"path/to/docker/config.json": `{"auths":{"custom":"test"}}`},
fileWriteContent: map[string]string{},
}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("path/to/docker/config.json", []byte(`{"auths":{"custom":"test"}}`))
fileUtils.AddFile("/kaniko/ssl/certs/ca-certificates.crt", []byte(``))
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, certClient, fileUtils)
assert.NoError(t, err)
cwd, _ := os.Getwd()
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", cwd, "--skip-tls-verify-pull", "--destination", "myImage:tag"}, runner.Calls[1].Params)
assert.Equal(t, []string{"--dockerfile", "Dockerfile", "--context", ".", "--skip-tls-verify-pull", "--destination", "myImage:tag"}, runner.Calls[1].Params)
})
t.Run("success case - multi image build with root image", func(t *testing.T) {
config := &kanikoExecuteOptions{
ContainerImageName: "myImage",
ContainerImageTag: "myTag",
ContainerRegistryURL: "https://my.registry.com:50000",
ContainerMultiImageBuild: true,
}
runner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("Dockerfile", []byte("some content"))
fileUtils.AddFile("sub1/Dockerfile", []byte("some content"))
fileUtils.AddFile("sub2/Dockerfile", []byte("some content"))
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, nil, fileUtils)
assert.NoError(t, err)
assert.Equal(t, 3, len(runner.Calls))
assert.Equal(t, "/kaniko/executor", runner.Calls[0].Exec)
assert.Equal(t, "/kaniko/executor", runner.Calls[1].Exec)
assert.Equal(t, "/kaniko/executor", runner.Calls[2].Exec)
expectedParams := [][]string{
{"--dockerfile", "Dockerfile", "--context", ".", "--destination", "my.registry.com:50000/myImage:myTag"},
{"--dockerfile", filepath.Join("sub1", "Dockerfile"), "--context", "sub1", "--destination", "my.registry.com:50000/myImage-sub1:myTag"},
{"--dockerfile", filepath.Join("sub2", "Dockerfile"), "--context", "sub2", "--destination", "my.registry.com:50000/myImage-sub2:myTag"},
}
// need to go this way since we cannot count on the correct order
for _, call := range runner.Calls {
found := false
for _, expected := range expectedParams {
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, "myImage")
assert.Contains(t, commonPipelineEnvironment.container.imageNames, "myImage-sub1")
assert.Contains(t, commonPipelineEnvironment.container.imageNames, "myImage-sub2")
assert.Contains(t, commonPipelineEnvironment.container.imageNameTags, "myImage:myTag")
assert.Contains(t, commonPipelineEnvironment.container.imageNameTags, "myImage-sub1:myTag")
assert.Contains(t, commonPipelineEnvironment.container.imageNameTags, "myImage-sub2:myTag")
})
t.Run("success case - multi image build excluding root image", func(t *testing.T) {
config := &kanikoExecuteOptions{
ContainerImageName: "myImage",
ContainerImageTag: "myTag",
ContainerRegistryURL: "https://my.registry.com:50000",
ContainerMultiImageBuild: true,
ContainerMultiImageBuildExcludes: []string{"Dockerfile"},
}
runner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("Dockerfile", []byte("some content"))
fileUtils.AddFile("sub1/Dockerfile", []byte("some content"))
fileUtils.AddFile("sub2/Dockerfile", []byte("some content"))
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, nil, fileUtils)
assert.NoError(t, err)
assert.Equal(t, 2, len(runner.Calls))
assert.Equal(t, "/kaniko/executor", runner.Calls[0].Exec)
assert.Equal(t, "/kaniko/executor", runner.Calls[1].Exec)
expectedParams := [][]string{
{"--dockerfile", filepath.Join("sub1", "Dockerfile"), "--context", "sub1", "--destination", "my.registry.com:50000/myImage-sub1:myTag"},
{"--dockerfile", filepath.Join("sub2", "Dockerfile"), "--context", "sub2", "--destination", "my.registry.com:50000/myImage-sub2:myTag"},
}
// need to go this way since we cannot count on the correct order
for _, call := range runner.Calls {
found := false
for _, expected := range expectedParams {
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, "", commonPipelineEnvironment.container.imageNameTag)
assert.Contains(t, commonPipelineEnvironment.container.imageNames, "myImage-sub1")
assert.Contains(t, commonPipelineEnvironment.container.imageNames, "myImage-sub2")
assert.Contains(t, commonPipelineEnvironment.container.imageNameTags, "myImage-sub1:myTag")
assert.Contains(t, commonPipelineEnvironment.container.imageNameTags, "myImage-sub2:myTag")
})
t.Run("error case - multi image build: no docker files", func(t *testing.T) {
config := &kanikoExecuteOptions{
ContainerImageName: "myImage",
ContainerImageTag: "myTag",
ContainerRegistryURL: "https://my.registry.com:50000",
ContainerMultiImageBuild: true,
}
cpe := kanikoExecuteCommonPipelineEnvironment{}
runner := &mock.ExecMockRunner{}
fileUtils := &mock.FilesMock{}
err := runKanikoExecute(config, &telemetry.CustomData{}, &cpe, runner, nil, fileUtils)
assert.Error(t, err)
assert.Contains(t, fmt.Sprint(err), "failed to identify image list for multi image build")
})
t.Run("error case - multi image build: no docker files to process", func(t *testing.T) {
config := &kanikoExecuteOptions{
ContainerImageName: "myImage",
ContainerImageTag: "myTag",
ContainerRegistryURL: "https://my.registry.com:50000",
ContainerMultiImageBuild: true,
ContainerMultiImageBuildExcludes: []string{"Dockerfile"},
}
cpe := kanikoExecuteCommonPipelineEnvironment{}
runner := &mock.ExecMockRunner{}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("Dockerfile", []byte("some content"))
err := runKanikoExecute(config, &telemetry.CustomData{}, &cpe, runner, nil, fileUtils)
assert.Error(t, err)
assert.Contains(t, fmt.Sprint(err), "no docker files to process, please check exclude list")
})
t.Run("error case - multi image build: build failed", func(t *testing.T) {
config := &kanikoExecuteOptions{
ContainerImageName: "myImage",
ContainerImageTag: "myTag",
ContainerRegistryURL: "https://my.registry.com:50000",
ContainerMultiImageBuild: true,
}
cpe := kanikoExecuteCommonPipelineEnvironment{}
runner := &mock.ExecMockRunner{}
runner.ShouldFailOnCommand = map[string]error{"/kaniko/executor": fmt.Errorf("execution failed")}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("Dockerfile", []byte("some content"))
err := runKanikoExecute(config, &telemetry.CustomData{}, &cpe, runner, nil, fileUtils)
assert.Error(t, err)
assert.Contains(t, fmt.Sprint(err), "failed to build image")
})
t.Run("error case - Kaniko init failed", func(t *testing.T) {
@ -224,9 +376,10 @@ func TestRunKanikoExecute(t *testing.T) {
runner := &mock.ExecMockRunner{
ShouldFailOnCommand: map[string]error{"rm": fmt.Errorf("rm failed")},
}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
certClient := &kanikoMockClient{}
fileUtils := &kanikoFileMock{}
fileUtils := &mock.FilesMock{}
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, certClient, fileUtils)
@ -239,11 +392,10 @@ func TestRunKanikoExecute(t *testing.T) {
runner := &mock.ExecMockRunner{
ShouldFailOnCommand: map[string]error{"/kaniko/executor": fmt.Errorf("kaniko run failed")},
}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
certClient := &kanikoMockClient{}
fileUtils := &kanikoFileMock{
fileWriteContent: map[string]string{},
}
fileUtils := &mock.FilesMock{}
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, certClient, fileUtils)
@ -263,12 +415,11 @@ func TestRunKanikoExecute(t *testing.T) {
}
runner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
certClient := &kanikoMockClient{}
fileUtils := &kanikoFileMock{
fileWriteContent: map[string]string{},
fileReadErr: map[string]error{"/kaniko/ssl/certs/ca-certificates.crt": fmt.Errorf("read error")},
}
fileUtils := &mock.FilesMock{}
fileUtils.FileReadErrors = map[string]error{"/kaniko/ssl/certs/ca-certificates.crt": fmt.Errorf("read error")}
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, certClient, fileUtils)
@ -281,12 +432,11 @@ func TestRunKanikoExecute(t *testing.T) {
}
runner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
certClient := &kanikoMockClient{}
fileUtils := &kanikoFileMock{
fileWriteContent: map[string]string{},
fileReadErr: map[string]error{"path/to/docker/config.json": fmt.Errorf("read error")},
}
fileUtils := &mock.FilesMock{}
fileUtils.FileReadErrors = map[string]error{"path/to/docker/config.json": fmt.Errorf("read error")}
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, certClient, fileUtils)
@ -299,12 +449,12 @@ func TestRunKanikoExecute(t *testing.T) {
}
runner := &mock.ExecMockRunner{}
commonPipelineEnvironment := kanikoExecuteCommonPipelineEnvironment{}
certClient := &kanikoMockClient{}
fileUtils := &kanikoFileMock{
fileWriteContent: map[string]string{},
fileWriteErr: map[string]error{"/kaniko/.docker/config.json": fmt.Errorf("write error")},
}
fileUtils := &mock.FilesMock{}
fileUtils.AddFile("path/to/docker/config.json", []byte(`{"auths":{"custom":"test"}}`))
fileUtils.FileWriteErrors = map[string]error{"/kaniko/.docker/config.json": fmt.Errorf("write error")}
err := runKanikoExecute(config, &telemetry.CustomData{}, &commonPipelineEnvironment, runner, certClient, fileUtils)

View File

@ -89,7 +89,7 @@ func CreateBuildSettingsInfo(config *BuildOptions, buildTool string) (string, er
}
}
log.Entry().Infof("build settings infomration successfully created with '%v", string(jsonResult))
log.Entry().Infof("build settings information successfully created with '%v", string(jsonResult))
return string(jsonResult), nil

View File

@ -358,6 +358,9 @@ func GetYAML(data interface{}) (string, error) {
// OpenPiperFile provides functionality to retrieve configuration via file or http
func OpenPiperFile(name string, accessTokens map[string]string) (io.ReadCloser, error) {
if len(name) == 0 {
return nil, fmt.Errorf("no filename provided")
}
if !strings.HasPrefix(name, "http://") && !strings.HasPrefix(name, "https://") {
return os.Open(name)
}

View File

@ -9,6 +9,7 @@ import (
"path/filepath"
"strings"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util"
@ -165,3 +166,55 @@ func (c *Client) TarImage(writer io.Writer, image pkgutil.Image) error {
}
return nil
}
// ImageListWithFilePath compiles container image names based on all Dockerfiles found, considering excludes
// according to following search pattern: **/Dockerfile*
// Return value contains a map with image names and file path
// Examples for image names with imageName testImage
// * Dockerfile: `imageName`
// * sub1/Dockerfile: `imageName-sub1`
// * sub2/Dockerfile_proxy: `imageName-sub2-proxy`
func ImageListWithFilePath(imageName string, excludes []string, utils piperutils.FileUtils) (map[string]string, error) {
imageList := map[string]string{}
pattern := "**/Dockerfile*"
matches, err := utils.Glob(pattern)
if err != nil || len(matches) == 0 {
return imageList, fmt.Errorf("failed to retrieve Dockerfiles")
}
for _, dockerfilePath := range matches {
// make sure that the path we have is relative
// ToDo: needs rework
//dockerfilePath = strings.ReplaceAll(dockerfilePath, cwd, ".")
if piperutils.ContainsString(excludes, dockerfilePath) {
log.Entry().Infof("Discard %v since it is in the exclude list %v", dockerfilePath, excludes)
continue
}
if dockerfilePath == "Dockerfile" {
imageList[imageName] = dockerfilePath
} else {
var finalName string
if base := filepath.Base(dockerfilePath); base == "Dockerfile" {
finalName = fmt.Sprintf("%v-%v", imageName, strings.ReplaceAll(filepath.Dir(dockerfilePath), string(filepath.Separator), "-"))
} else {
parts := strings.FieldsFunc(base, func(separator rune) bool {
return separator == []rune("-")[0] || separator == []rune("_")[0]
})
if len(parts) == 1 {
return imageList, fmt.Errorf("wrong format of Dockerfile, must be inside a sub-folder or contain a separator")
}
parts[0] = imageName
finalName = strings.Join(parts, "-")
}
imageList[finalName] = dockerfilePath
}
}
return imageList, nil
}

View File

@ -2,6 +2,7 @@ package docker
import (
"fmt"
"path/filepath"
"testing"
"github.com/SAP/jenkins-library/pkg/mock"
@ -130,3 +131,44 @@ func TestGetImageSource(t *testing.T) {
assert.Equal(t, c.want, got)
}
}
func TestImageListWithFilePath(t *testing.T) {
t.Parallel()
imageName := "testImage"
tt := []struct {
name string
excludes []string
fileList []string
expected map[string]string
expectedError error
}{
{name: "Dockerfile only", fileList: []string{"Dockerfile"}, expected: map[string]string{imageName: "Dockerfile"}},
{name: "Dockerfile in subdir", fileList: []string{"sub/Dockerfile"}, expected: map[string]string{fmt.Sprintf("%v-sub", imageName): filepath.FromSlash("sub/Dockerfile")}},
{name: "Dockerfiles in multiple subdirs & parent", fileList: []string{"Dockerfile", "sub1/Dockerfile", "sub2/Dockerfile"}, expected: map[string]string{fmt.Sprintf("%v", imageName): filepath.FromSlash("Dockerfile"), fmt.Sprintf("%v-sub1", imageName): filepath.FromSlash("sub1/Dockerfile"), fmt.Sprintf("%v-sub2", imageName): filepath.FromSlash("sub2/Dockerfile")}},
{name: "Dockerfiles in multiple subdirs & parent - with excludes", excludes: []string{"Dockerfile"}, fileList: []string{"Dockerfile", "sub1/Dockerfile", "sub2/Dockerfile"}, expected: map[string]string{fmt.Sprintf("%v-sub1", imageName): filepath.FromSlash("sub1/Dockerfile"), fmt.Sprintf("%v-sub2", imageName): filepath.FromSlash("sub2/Dockerfile")}},
{name: "Dockerfiles with extensions", fileList: []string{"Dockerfile_main", "Dockerfile_sub1", "Dockerfile_sub2"}, expected: map[string]string{fmt.Sprintf("%v-main", imageName): filepath.FromSlash("Dockerfile_main"), fmt.Sprintf("%v-sub1", imageName): filepath.FromSlash("Dockerfile_sub1"), fmt.Sprintf("%v-sub2", imageName): filepath.FromSlash("Dockerfile_sub2")}},
{name: "Dockerfiles with extensions", fileList: []string{"Dockerfile_main", "Dockerfile_sub1", "Dockerfile_sub2"}, expected: map[string]string{fmt.Sprintf("%v-main", imageName): filepath.FromSlash("Dockerfile_main"), fmt.Sprintf("%v-sub1", imageName): filepath.FromSlash("Dockerfile_sub1"), fmt.Sprintf("%v-sub2", imageName): filepath.FromSlash("Dockerfile_sub2")}},
{name: "No Dockerfile", fileList: []string{"NoDockerFile"}, expectedError: fmt.Errorf("failed to retrieve Dockerfiles")},
{name: "Incorrect Dockerfile", fileList: []string{"DockerfileNotSupported"}, expectedError: fmt.Errorf("wrong format of Dockerfile, must be inside a sub-folder or contain a separator")},
}
for _, test := range tt {
t.Run(test.name, func(t *testing.T) {
fileMock := mock.FilesMock{}
for _, file := range test.fileList {
fileMock.AddFile(file, []byte("someContent"))
}
imageList, err := ImageListWithFilePath(imageName, test.excludes, &fileMock)
if test.expectedError != nil {
assert.EqualError(t, err, fmt.Sprint(test.expectedError))
} else {
assert.NoError(t, err)
assert.Equal(t, test.expected, imageList)
}
})
}
}

View File

@ -1,7 +1,23 @@
metadata:
name: kanikoExecute
description: Executes a [Kaniko](https://github.com/GoogleContainerTools/kaniko) build for creating a Docker container.
longDescription: Executes a [Kaniko](https://github.com/GoogleContainerTools/kaniko) build for creating a Docker container.
longDescription: |
Executes a [Kaniko](https://github.com/GoogleContainerTools/kaniko) build for creating a Docker container.
### Building multiple container images
The step allows you to build multiple container images with one run.
This is suitable in case you need to create multiple images for one microservice, e.g. for testing.
All images will get the same "root" name and the same versioning.<br />
**Thus, this is not suitable to be used for a monorepo approach!** For monorepos you need to use a build tool natively capable to take care for monorepos
or implement a custom logic and for example execute this `kanikoExecute` step multiple times in your custom pipeline.
You can activate multiple builds using the parameters
* [containerMultiImageBuild](#containermultiimagebuild) for activation
* [containerMultiImageBuildExcludes](#containermultiimagebuildexcludes) for defining excludes
spec:
inputs:
secrets:
@ -18,6 +34,36 @@ spec:
- STEPS
default:
- --skip-tls-verify-pull
# fixing Kaniko issue https://github.com/GoogleContainerTools/kaniko/issues/1586
# as per comment https://github.com/GoogleContainerTools/kaniko/issues/1586#issuecomment-945718536
- --ignore-path
- /busybox
- name: buildSettingsInfo
type: string
description: Build settings info is typically filled by the step automatically to create information about the build settings that were used during the mta build. This information is typically used for compliance related processes.
scope:
- STEPS
- STAGES
- PARAMETERS
resourceRef:
- name: commonPipelineEnvironment
param: custom/buildSettingsInfo
- 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).
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
- name: containerMultiImageBuildExcludes
type: '[]string'
description: Defines a list of Dockerfile paths to exclude from the build when using [`containerMultiImageBuild`](#containermultiimagebuild).
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
- name: containerBuildOptions
type: string
description: Deprected, please use buildOptions. Defines the build options for the [kaniko](https://github.com/GoogleContainerTools/kaniko) build.
@ -119,6 +165,10 @@ spec:
params:
- name: container/registryUrl
- name: container/imageNameTag
- name: container/imageNames
type: "[]string"
- name: container/imageNameTags
type: "[]string"
- name: custom/buildSettingsInfo
containers:
- image: gcr.io/kaniko-project/executor:debug