1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

fix(cnbBuild): read dockerConfigJSON from CPE and merge it with user-provided (#4444)

Co-authored-by: Ralf Pannemans <ralf.pannemans@sap.com>
This commit is contained in:
Pavel Busko 2023-07-04 14:19:02 +02:00 committed by GitHub
parent 1befaa80a2
commit 13f1e94ade
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 172 additions and 123 deletions

View File

@ -7,7 +7,6 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"github.com/SAP/jenkins-library/pkg/buildsettings" "github.com/SAP/jenkins-library/pkg/buildsettings"
"github.com/SAP/jenkins-library/pkg/certutils" "github.com/SAP/jenkins-library/pkg/certutils"
@ -212,28 +211,26 @@ func extractZip(source, target string) error {
return nil return nil
} }
func renameDockerConfig(config *cnbBuildOptions, utils cnbutils.BuildUtils) error { func ensureDockerConfig(config *cnbBuildOptions, utils cnbutils.BuildUtils) error {
if filepath.Base(config.DockerConfigJSON) != "config.json" { newFile := "/tmp/config.json"
log.Entry().Debugf("Renaming docker config file from '%s' to 'config.json'", filepath.Base(config.DockerConfigJSON)) if config.DockerConfigJSON == "" {
config.DockerConfigJSON = newFile
newPath := filepath.Join(filepath.Dir(config.DockerConfigJSON), "config.json") return utils.FileWrite(config.DockerConfigJSON, []byte("{}"), os.ModePerm)
alreadyExists, err := utils.FileExists(newPath) }
log.Entry().Debugf("Copying docker config file from '%s' to '%s'", config.DockerConfigJSON, newFile)
_, err := utils.Copy(config.DockerConfigJSON, newFile)
if err != nil { if err != nil {
return err return err
} }
if alreadyExists { err = utils.Chmod(newFile, 0644)
return nil
}
err = utils.FileRename(config.DockerConfigJSON, newPath)
if err != nil { if err != nil {
return err return err
} }
config.DockerConfigJSON = newPath config.DockerConfigJSON = newFile
}
return nil return nil
} }
@ -382,38 +379,19 @@ func callCnbBuild(config *cnbBuildOptions, telemetryData *telemetry.CustomData,
} }
commonPipelineEnvironment.custom.buildSettingsInfo = buildSettingsInfo commonPipelineEnvironment.custom.buildSettingsInfo = buildSettingsInfo
if len(config.DockerConfigJSON) > 0 { err = ensureDockerConfig(config, utils)
err = renameDockerConfig(config, utils)
if err != nil { if err != nil {
log.SetErrorCategory(log.ErrorConfiguration) log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "failed to rename DockerConfigJSON file '%s'", config.DockerConfigJSON) return errors.Wrapf(err, "failed to create/rename DockerConfigJSON file")
}
} }
if config.ContainerRegistryUser != "" && config.ContainerRegistryPassword != "" { if config.DockerConfigJSONCPE != "" {
log.Entry().Debug("enhancing docker config with the provided credentials") log.Entry().Debugf("merging docker config file '%s' into '%s'", config.DockerConfigJSONCPE, config.DockerConfigJSON)
if config.DockerConfigJSON == "" { err = docker.MergeDockerConfigJSON(config.DockerConfigJSONCPE, config.DockerConfigJSON, utils)
config.DockerConfigJSON = "/tmp/config.json"
}
log.Entry().Debugf("using docker config file %q", config.DockerConfigJSON)
if matched, _ := regexp.MatchString("^(http|https)://.*", config.ContainerRegistryURL); !matched {
config.ContainerRegistryURL = fmt.Sprintf("https://%s", config.ContainerRegistryURL)
}
containerRegistry, err := docker.ContainerRegistryFromURL(config.ContainerRegistryURL)
if err != nil { if err != nil {
log.SetErrorCategory(log.ErrorConfiguration) log.SetErrorCategory(log.ErrorConfiguration)
return errors.Wrapf(err, "failed to read registry url %q", config.ContainerRegistryURL) return errors.Wrapf(err, "failed to merge DockerConfigJSON files")
} }
_, err = docker.CreateDockerConfigJSON(containerRegistry, config.ContainerRegistryUser, config.ContainerRegistryPassword, "", config.DockerConfigJSON, utils)
if err != nil {
log.SetErrorCategory(log.ErrorBuild)
return errors.Wrapf(err, "failed to update DockerConfigJSON file %q", config.DockerConfigJSON)
}
log.Entry().Debugf("docker config %q has been updated", config.DockerConfigJSON)
} }
mergedConfigs, err := processConfigs(*config, config.MultipleImages) mergedConfigs, err := processConfigs(*config, config.MultipleImages)

View File

@ -26,13 +26,12 @@ type cnbBuildOptions struct {
ContainerImageAlias string `json:"containerImageAlias,omitempty"` ContainerImageAlias string `json:"containerImageAlias,omitempty"`
ContainerImageTag string `json:"containerImageTag,omitempty"` ContainerImageTag string `json:"containerImageTag,omitempty"`
ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"` ContainerRegistryURL string `json:"containerRegistryUrl,omitempty"`
ContainerRegistryUser string `json:"containerRegistryUser,omitempty"`
ContainerRegistryPassword string `json:"containerRegistryPassword,omitempty"`
Buildpacks []string `json:"buildpacks,omitempty"` Buildpacks []string `json:"buildpacks,omitempty"`
BuildEnvVars map[string]interface{} `json:"buildEnvVars,omitempty"` BuildEnvVars map[string]interface{} `json:"buildEnvVars,omitempty"`
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
ProjectDescriptor string `json:"projectDescriptor,omitempty"` ProjectDescriptor string `json:"projectDescriptor,omitempty"`
DockerConfigJSON string `json:"dockerConfigJSON,omitempty"` DockerConfigJSON string `json:"dockerConfigJSON,omitempty"`
DockerConfigJSONCPE string `json:"dockerConfigJSONCPE,omitempty"`
CustomTLSCertificateLinks []string `json:"customTlsCertificateLinks,omitempty"` CustomTLSCertificateLinks []string `json:"customTlsCertificateLinks,omitempty"`
AdditionalTags []string `json:"additionalTags,omitempty"` AdditionalTags []string `json:"additionalTags,omitempty"`
Bindings map[string]interface{} `json:"bindings,omitempty"` Bindings map[string]interface{} `json:"bindings,omitempty"`
@ -158,6 +157,7 @@ func CnbBuildCommand() *cobra.Command {
return err return err
} }
log.RegisterSecret(stepConfig.DockerConfigJSON) log.RegisterSecret(stepConfig.DockerConfigJSON)
log.RegisterSecret(stepConfig.DockerConfigJSONCPE)
if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID)
@ -226,13 +226,12 @@ func addCnbBuildFlags(cmd *cobra.Command, stepConfig *cnbBuildOptions) {
cmd.Flags().StringVar(&stepConfig.ContainerImageAlias, "containerImageAlias", os.Getenv("PIPER_containerImageAlias"), "Logical name used for this image.\n") cmd.Flags().StringVar(&stepConfig.ContainerImageAlias, "containerImageAlias", os.Getenv("PIPER_containerImageAlias"), "Logical name used for this image.\n")
cmd.Flags().StringVar(&stepConfig.ContainerImageTag, "containerImageTag", os.Getenv("PIPER_containerImageTag"), "Tag of the container which will be built") cmd.Flags().StringVar(&stepConfig.ContainerImageTag, "containerImageTag", os.Getenv("PIPER_containerImageTag"), "Tag of the container which will be built")
cmd.Flags().StringVar(&stepConfig.ContainerRegistryURL, "containerRegistryUrl", os.Getenv("PIPER_containerRegistryUrl"), "Container registry where the image should be pushed to.\n\n**Note**: `containerRegistryUrl` should include only the domain. If you want to publish an image under `docker.io/example/my-image`, you must set `containerRegistryUrl: \"docker.io\"` and `containerImageName: \"example/my-image\"`.\n") cmd.Flags().StringVar(&stepConfig.ContainerRegistryURL, "containerRegistryUrl", os.Getenv("PIPER_containerRegistryUrl"), "Container registry where the image should be pushed to.\n\n**Note**: `containerRegistryUrl` should include only the domain. If you want to publish an image under `docker.io/example/my-image`, you must set `containerRegistryUrl: \"docker.io\"` and `containerImageName: \"example/my-image\"`.\n")
cmd.Flags().StringVar(&stepConfig.ContainerRegistryUser, "containerRegistryUser", os.Getenv("PIPER_containerRegistryUser"), "Username of the container registry where the image should be pushed to - which will updated in a docker config json file. If a docker config json file is provided via parameter `dockerConfigJSON`, then the existing file will be enhanced")
cmd.Flags().StringVar(&stepConfig.ContainerRegistryPassword, "containerRegistryPassword", os.Getenv("PIPER_containerRegistryPassword"), "Password of the container registry where the image should be pushed to - which will updated in a docker config json file. If a docker config json file is provided via parameter `dockerConfigJSON`, then the existing file will be enhanced")
cmd.Flags().StringSliceVar(&stepConfig.Buildpacks, "buildpacks", []string{}, "List of custom buildpacks to use in the form of `$HOSTNAME/$REPO[:$TAG]`.") cmd.Flags().StringSliceVar(&stepConfig.Buildpacks, "buildpacks", []string{}, "List of custom buildpacks to use in the form of `$HOSTNAME/$REPO[:$TAG]`.")
cmd.Flags().StringVar(&stepConfig.Path, "path", os.Getenv("PIPER_path"), "Glob that should either point to a directory with your sources or one artifact in zip format.\nThis property determines the input to the buildpack.\n") cmd.Flags().StringVar(&stepConfig.Path, "path", os.Getenv("PIPER_path"), "Glob that should either point to a directory with your sources or one artifact in zip format.\nThis property determines the input to the buildpack.\n")
cmd.Flags().StringVar(&stepConfig.ProjectDescriptor, "projectDescriptor", `project.toml`, "Relative path to the project.toml file.\nSee [buildpacks.io](https://buildpacks.io/docs/reference/config/project-descriptor/) for the reference.\nParameters passed to the cnbBuild step will take precedence over the parameters set in the project.toml file, except the `env` block.\nEnvironment variables declared in a project descriptor file, will be merged with the `buildEnvVars` property, with the `buildEnvVars` having a precedence.\n\n*Note*: The project descriptor path should be relative to what is set in the [path](#path) property. If the `path` property is pointing to a zip archive (e.g. jar file), project descriptor path will be relative to the root of the workspace.\n\n*Note*: Inline buildpacks (see [specification](https://buildpacks.io/docs/reference/config/project-descriptor/#build-_table-optional_)) are not supported yet.\n") cmd.Flags().StringVar(&stepConfig.ProjectDescriptor, "projectDescriptor", `project.toml`, "Relative path to the project.toml file.\nSee [buildpacks.io](https://buildpacks.io/docs/reference/config/project-descriptor/) for the reference.\nParameters passed to the cnbBuild step will take precedence over the parameters set in the project.toml file, except the `env` block.\nEnvironment variables declared in a project descriptor file, will be merged with the `buildEnvVars` property, with the `buildEnvVars` having a precedence.\n\n*Note*: The project descriptor path should be relative to what is set in the [path](#path) property. If the `path` property is pointing to a zip archive (e.g. jar file), project descriptor path will be relative to the root of the workspace.\n\n*Note*: Inline buildpacks (see [specification](https://buildpacks.io/docs/reference/config/project-descriptor/#build-_table-optional_)) are not supported yet.\n")
cmd.Flags().StringVar(&stepConfig.DockerConfigJSON, "dockerConfigJSON", os.Getenv("PIPER_dockerConfigJSON"), "Path to the file `.docker/config.json` - this is typically provided by your CI/CD system. You can find more details about the Docker credentials in the [Docker documentation](https://docs.docker.com/engine/reference/commandline/login/).") cmd.Flags().StringVar(&stepConfig.DockerConfigJSON, "dockerConfigJSON", os.Getenv("PIPER_dockerConfigJSON"), "Path to the file `.docker/config.json` - this is typically provided by your CI/CD system. You can find more details about the Docker credentials in the [Docker documentation](https://docs.docker.com/engine/reference/commandline/login/).")
cmd.Flags().StringVar(&stepConfig.DockerConfigJSONCPE, "dockerConfigJSONCPE", os.Getenv("PIPER_dockerConfigJSONCPE"), "This property is intended only for reading the `dockerConfigJSON` from the Common Pipeline Environment. If you want to provide your own credentials, please refer to the [dockerConfigJSON](#dockerConfigJSON) property. If both properties are set, the config files will be merged, with the [dockerConfigJSON](#dockerConfigJSON) having higher priority.")
cmd.Flags().StringSliceVar(&stepConfig.CustomTLSCertificateLinks, "customTlsCertificateLinks", []string{}, "List containing download links of custom TLS certificates. This is required to ensure trusted connections to registries with custom certificates.") cmd.Flags().StringSliceVar(&stepConfig.CustomTLSCertificateLinks, "customTlsCertificateLinks", []string{}, "List containing download links of custom TLS certificates. This is required to ensure trusted connections to registries with custom certificates.")
cmd.Flags().StringSliceVar(&stepConfig.AdditionalTags, "additionalTags", []string{}, "List of tags which will be pushed to the registry (additionally to the provided `containerImageTag`), e.g. \"latest\".") cmd.Flags().StringSliceVar(&stepConfig.AdditionalTags, "additionalTags", []string{}, "List of tags which will be pushed to the registry (additionally to the provided `containerImageTag`), e.g. \"latest\".")
@ -312,34 +311,6 @@ func cnbBuildMetadata() config.StepData {
Aliases: []config.Alias{{Name: "dockerRegistryUrl"}}, Aliases: []config.Alias{{Name: "dockerRegistryUrl"}},
Default: os.Getenv("PIPER_containerRegistryUrl"), Default: os.Getenv("PIPER_containerRegistryUrl"),
}, },
{
Name: "containerRegistryUser",
ResourceRef: []config.ResourceReference{
{
Name: "commonPipelineEnvironment",
Param: "container/repositoryUsername",
},
},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{{Name: "dockerRegistryUser"}},
Default: os.Getenv("PIPER_containerRegistryUser"),
},
{
Name: "containerRegistryPassword",
ResourceRef: []config.ResourceReference{
{
Name: "commonPipelineEnvironment",
Param: "container/repositoryPassword",
},
},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{{Name: "dockerRegistryPassword"}},
Default: os.Getenv("PIPER_containerRegistryPassword"),
},
{ {
Name: "buildpacks", Name: "buildpacks",
ResourceRef: []config.ResourceReference{ ResourceRef: []config.ResourceReference{
@ -400,6 +371,20 @@ func cnbBuildMetadata() config.StepData {
Aliases: []config.Alias{}, Aliases: []config.Alias{},
Default: os.Getenv("PIPER_dockerConfigJSON"), Default: os.Getenv("PIPER_dockerConfigJSON"),
}, },
{
Name: "dockerConfigJSONCPE",
ResourceRef: []config.ResourceReference{
{
Name: "commonPipelineEnvironment",
Param: "custom/dockerConfigJSON",
},
},
Scope: []string{},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_dockerConfigJSONCPE"),
},
{ {
Name: "customTlsCertificateLinks", Name: "customTlsCertificateLinks",
ResourceRef: []config.ResourceReference{}, ResourceRef: []config.ResourceReference{},

View File

@ -230,11 +230,8 @@ func TestRunCnbBuild(t *testing.T) {
assert.Contains(t, runner.Calls[0].Params, fmt.Sprintf("%s/%s:%s", config.ContainerRegistryURL, config.ContainerImageName, config.ContainerImageTag)) assert.Contains(t, runner.Calls[0].Params, fmt.Sprintf("%s/%s:%s", config.ContainerRegistryURL, config.ContainerImageName, config.ContainerImageTag))
assert.Contains(t, runner.Calls[0].Params, fmt.Sprintf("%s/%s:latest", config.ContainerRegistryURL, config.ContainerImageName)) assert.Contains(t, runner.Calls[0].Params, fmt.Sprintf("%s/%s:latest", config.ContainerRegistryURL, config.ContainerImageName))
initialFileExists, _ := utils.FileExists("/path/to/test.json") copiedFileExists, _ := utils.FileExists("/tmp/config.json")
renamedFileExists, _ := utils.FileExists("/path/to/config.json") assert.True(t, copiedFileExists)
assert.False(t, initialFileExists)
assert.True(t, renamedFileExists)
}) })
t.Run("success case (customTlsCertificates)", func(t *testing.T) { t.Run("success case (customTlsCertificates)", func(t *testing.T) {
@ -420,7 +417,7 @@ func TestRunCnbBuild(t *testing.T) {
addBuilderFiles(&utils) addBuilderFiles(&utils)
err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{}) err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{})
assert.EqualError(t, err, "failed to generate CNB_REGISTRY_AUTH: could not read 'not-there/config.json'") assert.EqualError(t, err, "failed to create/rename DockerConfigJSON file: cannot copy 'not-there/config.json': file does not exist")
}) })
t.Run("error case: DockerConfigJSON file not there (not config.json)", func(t *testing.T) { t.Run("error case: DockerConfigJSON file not there (not config.json)", func(t *testing.T) {
@ -436,7 +433,7 @@ func TestRunCnbBuild(t *testing.T) {
addBuilderFiles(&utils) addBuilderFiles(&utils)
err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{}) err := callCnbBuild(&config, &telemetry.CustomData{}, &utils, &cnbBuildCommonPipelineEnvironment{}, &piperhttp.Client{})
assert.EqualError(t, err, "failed to rename DockerConfigJSON file 'not-there': renaming file 'not-there' is not supported, since it does not exist, or is not a leaf-entry") assert.EqualError(t, err, "failed to create/rename DockerConfigJSON file: cannot copy 'not-there': file does not exist")
}) })
t.Run("error case: dockerImage is not a valid builder", func(t *testing.T) { t.Run("error case: dockerImage is not a valid builder", func(t *testing.T) {

View File

@ -44,7 +44,7 @@ func TestCNBIntegrationNPMProject(t *testing.T) {
container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{
Image: baseBuilder, Image: baseBuilder,
User: "cnb", User: "cnb",
TestDir: []string{"testdata"}, TestDir: []string{"testdata", "TestCnbIntegration"},
Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()),
Environment: map[string]string{ Environment: map[string]string{
"PIPER_VAULTCREDENTIAL_DYNATRACE_API_KEY": "api-key-content", "PIPER_VAULTCREDENTIAL_DYNATRACE_API_KEY": "api-key-content",
@ -54,14 +54,14 @@ func TestCNBIntegrationNPMProject(t *testing.T) {
container2 := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ container2 := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{
Image: baseBuilder, Image: baseBuilder,
User: "cnb", User: "cnb",
TestDir: []string{"testdata"}, TestDir: []string{"testdata", "TestCnbIntegration"},
Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()),
Environment: map[string]string{ Environment: map[string]string{
"PIPER_VAULTCREDENTIAL_DYNATRACE_API_KEY": "api-key-content", "PIPER_VAULTCREDENTIAL_DYNATRACE_API_KEY": "api-key-content",
}, },
}) })
err := container.whenRunningPiperCommand("cnbBuild", "--noTelemetry", "--verbose", "--path", "TestCnbIntegration/project", "--customConfig", "TestCnbIntegration/config.yml", "--containerImageName", "node", "--containerImageTag", "0.0.1", "--dockerConfigJSON", "TestCnbIntegration/config.json", "--containerRegistryUrl", fmt.Sprintf("http://%s", registryURL), "--containerRegistryUser", "foo", "--containerRegistryPassword", "bar", "--defaultProcess", "greeter") err := container.whenRunningPiperCommand("cnbBuild", "--noTelemetry", "--verbose", "--path", "project", "--customConfig", "config.yml", "--containerImageName", "node", "--containerImageTag", "0.0.1", "--dockerConfigJSON", "config.json", "--containerRegistryUrl", fmt.Sprintf("http://%s", registryURL), "--defaultProcess", "greeter")
assert.NoError(t, err) assert.NoError(t, err)
container.assertHasOutput(t, "running command: /cnb/lifecycle/creator") container.assertHasOutput(t, "running command: /cnb/lifecycle/creator")
container.assertHasOutput(t, "Selected Node Engine version (using BP_NODE_VERSION): 16") container.assertHasOutput(t, "Selected Node Engine version (using BP_NODE_VERSION): 16")
@ -70,10 +70,10 @@ func TestCNBIntegrationNPMProject(t *testing.T) {
container.assertHasOutput(t, "Setting default process type 'greeter'") container.assertHasOutput(t, "Setting default process type 'greeter'")
container.assertHasOutput(t, "*** Images (sha256:") container.assertHasOutput(t, "*** Images (sha256:")
container.assertHasOutput(t, "SUCCESS") container.assertHasOutput(t, "SUCCESS")
container.assertFileContentEquals(t, "/project/TestCnbIntegration/config.json", "{\"auths\":{\"localhost:5000\":{\"auth\":\"Zm9vOmJhcg==\"},\"test.registry.io\":{}}}") container.assertFileContentEquals(t, "/tmp/config.json", "{\n\t\"auths\": {\n\t\t\"test.registry.io\": {},\n\t\t\"test2.registry.io\": {}\n\t}\n}")
container.terminate(t) container.terminate(t)
err = container2.whenRunningPiperCommand("cnbBuild", "--noTelemetry", "--verbose", "--path", "TestCnbIntegration/project", "--customConfig", "TestCnbIntegration/config.yml", "--containerImageName", "node", "--containerImageTag", "0.0.1", "--containerRegistryUrl", registryURL, "--containerRegistryUser", "foo", "--containerRegistryPassword", "bar", "--projectDescriptor", "project-with-id.toml") err = container2.whenRunningPiperCommand("cnbBuild", "--noTelemetry", "--verbose", "--path", "project", "--customConfig", "config.yml", "--containerImageName", "node", "--containerImageTag", "0.0.1", "--containerRegistryUrl", registryURL, "--projectDescriptor", "project-with-id.toml")
assert.NoError(t, err) assert.NoError(t, err)
container2.assertHasOutput(t, "running command: /cnb/lifecycle/creator") container2.assertHasOutput(t, "running command: /cnb/lifecycle/creator")
container2.assertHasOutput(t, "Selected Node Engine version (using BP_NODE_VERSION): 16") container2.assertHasOutput(t, "Selected Node Engine version (using BP_NODE_VERSION): 16")
@ -81,7 +81,7 @@ func TestCNBIntegrationNPMProject(t *testing.T) {
container2.assertHasOutput(t, fmt.Sprintf("Saving %s/node:0.0.1", registryURL)) container2.assertHasOutput(t, fmt.Sprintf("Saving %s/node:0.0.1", registryURL))
container2.assertHasOutput(t, "*** Images (sha256:") container2.assertHasOutput(t, "*** Images (sha256:")
container2.assertHasOutput(t, "SUCCESS") container2.assertHasOutput(t, "SUCCESS")
container2.assertFileContentEquals(t, "/tmp/config.json", "{\"auths\":{\"localhost:5000\":{\"auth\":\"Zm9vOmJhcg==\"}}}") container2.assertFileContentEquals(t, "/tmp/config.json", "{\n\t\"auths\": {\n\t\t\"test2.registry.io\": {}\n\t}\n}")
container2.terminate(t) container2.terminate(t)
} }
@ -243,17 +243,16 @@ func TestCNBIntegrationBindings(t *testing.T) {
container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{ container := givenThisContainer(t, IntegrationTestDockerExecRunnerBundle{
Image: baseBuilder, Image: baseBuilder,
User: "cnb", User: "cnb",
TestDir: []string{"testdata"}, TestDir: []string{"testdata", "TestCnbIntegration"},
Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()), Network: fmt.Sprintf("container:%s", registryContainer.GetContainerID()),
Environment: map[string]string{ Environment: map[string]string{
"PIPER_VAULTCREDENTIAL_DYNATRACE_API_KEY": "api-key-content", "PIPER_VAULTCREDENTIAL_DYNATRACE_API_KEY": "api-key-content",
}, },
}) })
err := container.whenRunningPiperCommand("cnbBuild", "--noTelemetry", "--verbose", "--customConfig", "TestCnbIntegration/config.yml", "--containerImageName", "not-found", "--containerImageTag", "0.0.1", "--containerRegistryUrl", registryURL, "--path", "TestMtaIntegration/maven") err := container.whenRunningPiperCommand("cnbBuild", "--noTelemetry", "--verbose", "--customConfig", "config.yml", "--containerImageName", "not-found", "--containerImageTag", "0.0.1", "--containerRegistryUrl", registryURL, "--path", "project")
assert.Error(t, err) assert.NoError(t, err)
container.assertHasOutput(t, "bindings/maven-settings/settings.xml: only whitespace content allowed before start tag")
container.assertHasFiles(t, container.assertHasFiles(t,
"/tmp/platform/bindings/dummy-binding/type", "/tmp/platform/bindings/dummy-binding/type",
"/tmp/platform/bindings/dummy-binding/dummy.yml", "/tmp/platform/bindings/dummy-binding/dummy.yml",

View File

@ -0,0 +1 @@
.pipeline/config.json

View File

@ -0,0 +1,5 @@
{
"auths": {
"test2.registry.io": {}
}
}

View File

@ -12,7 +12,7 @@ steps:
type: dummy type: dummy
data: data:
- key: dummy.yml - key: dummy.yml
file: TestCnbIntegration/config.yml file: config.yml
dynatrace: dynatrace:
type: Dynatrace type: Dynatrace
data: data:

View File

@ -1,6 +1,7 @@
package docker package docker
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -14,6 +15,9 @@ import (
"github.com/SAP/jenkins-library/pkg/piperutils" "github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd" cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd"
"github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/crane"
@ -27,6 +31,61 @@ type AuthEntry struct {
Auth string `json:"auth,omitempty"` Auth string `json:"auth,omitempty"`
} }
// MergeDockerConfigJSON merges two docker config.json files.
func MergeDockerConfigJSON(sourcePath, targetPath string, utils piperutils.FileUtils) error {
if exists, _ := utils.FileExists(sourcePath); !exists {
return fmt.Errorf("source dockerConfigJSON file %q does not exist", sourcePath)
}
sourceReader, err := utils.Open(sourcePath)
if err != nil {
return errors.Wrapf(err, "failed to open file %q", sourcePath)
}
defer sourceReader.Close()
sourceConfig, err := config.LoadFromReader(sourceReader)
if err != nil {
return errors.Wrapf(err, "failed to read file %q", sourcePath)
}
var targetConfig *configfile.ConfigFile
if exists, _ := utils.FileExists(targetPath); !exists {
log.Entry().Warnf("target dockerConfigJSON file %q does not exist, creating a new one", sourcePath)
targetConfig = configfile.New(targetPath)
} else {
targetReader, err := utils.Open(targetPath)
if err != nil {
return errors.Wrapf(err, "failed to open file %q", targetReader)
}
defer targetReader.Close()
targetConfig, err = config.LoadFromReader(targetReader)
if err != nil {
return errors.Wrapf(err, "failed to read file %q", targetPath)
}
}
for registry, auth := range sourceConfig.GetAuthConfigs() {
targetConfig.AuthConfigs[registry] = auth
}
buf := bytes.NewBuffer(nil)
err = targetConfig.SaveToWriter(buf)
if err != nil {
return errors.Wrapf(err, "failed to save file %q", targetPath)
}
err = utils.MkdirAll(filepath.Dir(targetPath), 0777)
if err != nil {
return fmt.Errorf("failed to create directory path for the file %q: %w", targetPath, err)
}
err = utils.FileWrite(targetPath, buf.Bytes(), 0666)
if err != nil {
return fmt.Errorf("failed to write %q: %w", targetPath, err)
}
return nil
}
// CreateDockerConfigJSON creates / updates a Docker config.json with registry credentials // CreateDockerConfigJSON creates / updates a Docker config.json with registry credentials
func CreateDockerConfigJSON(registryURL, username, password, targetPath, configPath string, utils piperutils.FileUtils) (string, error) { func CreateDockerConfigJSON(registryURL, username, password, targetPath, configPath string, utils piperutils.FileUtils) (string, error) {

View File

@ -148,3 +148,47 @@ func TestImageListWithFilePath(t *testing.T) {
}) })
} }
} }
func TestMergeDockerConfigJSON(t *testing.T) {
t.Parallel()
t.Run("success - both files present", func(t *testing.T) {
sourceFile := "/tmp/source.json"
targetFile := "/tmp/target.json"
expectedContent := "{\n\t\"auths\": {\n\t\t\"bar\": {},\n\t\t\"foo\": {\n\t\t\t\"auth\": \"Zm9vOmJhcg==\"\n\t\t}\n\t}\n}"
utilsMock := mock.FilesMock{}
utilsMock.AddFile(targetFile, []byte("{\"auths\": {\"foo\": {\"auth\": \"dGVzdDp0ZXN0\"}}}"))
utilsMock.AddFile(sourceFile, []byte("{\"auths\": {\"bar\": {}, \"foo\": {\"auth\": \"Zm9vOmJhcg==\"}}}"))
err := MergeDockerConfigJSON(sourceFile, targetFile, &utilsMock)
assert.NoError(t, err)
content, err := utilsMock.FileRead(targetFile)
assert.NoError(t, err)
assert.Equal(t, expectedContent, string(content))
})
t.Run("success - target file is missing", func(t *testing.T) {
sourceFile := "/tmp/source.json"
targetFile := "/tmp/target.json"
expectedContent := "{\n\t\"auths\": {\n\t\t\"bar\": {},\n\t\t\"foo\": {\n\t\t\t\"auth\": \"Zm9vOmJhcg==\"\n\t\t}\n\t}\n}"
utilsMock := mock.FilesMock{}
utilsMock.AddFile(sourceFile, []byte("{\"auths\": {\"bar\": {}, \"foo\": {\"auth\": \"Zm9vOmJhcg==\"}}}"))
err := MergeDockerConfigJSON(sourceFile, targetFile, &utilsMock)
assert.NoError(t, err)
content, err := utilsMock.FileRead(targetFile)
assert.NoError(t, err)
assert.Equal(t, expectedContent, string(content))
})
t.Run("error - source file is missing", func(t *testing.T) {
utilsMock := mock.FilesMock{}
err := MergeDockerConfigJSON("missing-file", "also-missing-file", &utilsMock)
assert.Error(t, err)
assert.Equal(t, "source dockerConfigJSON file \"missing-file\" does not exist", err.Error())
})
}

View File

@ -95,32 +95,6 @@ spec:
resourceRef: resourceRef:
- name: commonPipelineEnvironment - name: commonPipelineEnvironment
param: container/registryUrl param: container/registryUrl
- name: containerRegistryUser
aliases:
- name: dockerRegistryUser
type: string
description: Username of the container registry where the image should be pushed to - which will updated in a docker config json file. If a docker config json file is provided via parameter `dockerConfigJSON`, then the existing file will be enhanced
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
resourceRef:
- name: commonPipelineEnvironment
param: container/repositoryUsername
- name: containerRegistryPassword
aliases:
- name: dockerRegistryPassword
type: string
description: Password of the container registry where the image should be pushed to - which will updated in a docker config json file. If a docker config json file is provided via parameter `dockerConfigJSON`, then the existing file will be enhanced
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
resourceRef:
- name: commonPipelineEnvironment
param: container/repositoryPassword
- name: buildpacks - name: buildpacks
type: "[]string" type: "[]string"
description: List of custom buildpacks to use in the form of `$HOSTNAME/$REPO[:$TAG]`. description: List of custom buildpacks to use in the form of `$HOSTNAME/$REPO[:$TAG]`.
@ -181,6 +155,13 @@ spec:
- type: vaultSecretFile - type: vaultSecretFile
name: dockerConfigFileVaultSecretName name: dockerConfigFileVaultSecretName
default: docker-config default: docker-config
- name: dockerConfigJSONCPE
type: string
description: This property is intended only for reading the `dockerConfigJSON` from the Common Pipeline Environment. If you want to provide your own credentials, please refer to the [dockerConfigJSON](#dockerConfigJSON) property. If both properties are set, the config files will be merged, with the [dockerConfigJSON](#dockerConfigJSON) having higher priority.
secret: true
resourceRef:
- name: commonPipelineEnvironment
param: custom/dockerConfigJSON
- name: customTlsCertificateLinks - name: customTlsCertificateLinks
type: "[]string" type: "[]string"
description: List containing download links of custom TLS certificates. This is required to ensure trusted connections to registries with custom certificates. description: List containing download links of custom TLS certificates. This is required to ensure trusted connections to registries with custom certificates.