diff --git a/cmd/imagePushToRegistry.go b/cmd/imagePushToRegistry.go index 55a8a2934d..fc70ee8f40 100644 --- a/cmd/imagePushToRegistry.go +++ b/cmd/imagePushToRegistry.go @@ -81,7 +81,7 @@ func imagePushToRegistry(config imagePushToRegistryOptions, telemetryData *telem } func runImagePushToRegistry(config *imagePushToRegistryOptions, telemetryData *telemetry.CustomData, utils imagePushToRegistryUtils) error { - if !config.PushLocalDockerImage { + if !config.PushLocalDockerImage && !config.UseImageNameTags { if len(config.TargetImages) == 0 { config.TargetImages = mapSourceTargetImages(config.SourceImages) } @@ -91,6 +91,13 @@ func runImagePushToRegistry(config *imagePushToRegistryOptions, telemetryData *t } } + if config.UseImageNameTags { + if len(config.TargetImageNameTags) > 0 && len(config.TargetImageNameTags) != len(config.SourceImageNameTags) { + log.SetErrorCategory(log.ErrorConfiguration) + return errors.New("configuration error: please configure targetImageNameTags and sourceImageNameTags properly") + } + } + // Docker image tags don't allow plus signs in tags, thus replacing with dash config.SourceImageTag = strings.ReplaceAll(config.SourceImageTag, "+", "-") config.TargetImageTag = strings.ReplaceAll(config.TargetImageTag, "+", "-") @@ -115,6 +122,13 @@ func runImagePushToRegistry(config *imagePushToRegistryOptions, telemetryData *t return errors.Wrap(err, "failed to handle credentials for source registry") } + if config.UseImageNameTags { + if err := pushImageNameTagsToTargetRegistry(config, utils); err != nil { + return errors.Wrapf(err, "failed to push imageNameTags to target registry") + } + return nil + } + if err := copyImages(config, utils); err != nil { return errors.Wrap(err, "failed to copy images") } @@ -242,6 +256,37 @@ func pushLocalImageToTargetRegistry(config *imagePushToRegistryOptions, utils im return nil } +func pushImageNameTagsToTargetRegistry(config *imagePushToRegistryOptions, utils imagePushToRegistryUtils) error { + g, ctx := errgroup.WithContext(context.Background()) + g.SetLimit(10) + + for i, sourceImageNameTag := range config.SourceImageNameTags { + src := fmt.Sprintf("%s/%s", config.SourceRegistryURL, sourceImageNameTag) + + dst := "" + if len(config.TargetImageNameTags) == 0 { + dst = fmt.Sprintf("%s/%s", config.TargetRegistryURL, sourceImageNameTag) + } else { + dst = fmt.Sprintf("%s/%s", config.TargetRegistryURL, config.TargetImageNameTags[i]) + } + + g.Go(func() error { + log.Entry().Infof("Copying %s to %s...", src, dst) + if err := utils.CopyImage(ctx, src, dst, ""); err != nil { + return err + } + log.Entry().Infof("Copying %s to %s... Done", src, dst) + return nil + }) + } + + if err := g.Wait(); err != nil { + return err + } + + return nil +} + func mapSourceTargetImages(sourceImages []string) map[string]any { targetImages := make(map[string]any, len(sourceImages)) for _, sourceImage := range sourceImages { diff --git a/cmd/imagePushToRegistry_generated.go b/cmd/imagePushToRegistry_generated.go index 6791a6d3ec..c98e50df4d 100644 --- a/cmd/imagePushToRegistry_generated.go +++ b/cmd/imagePushToRegistry_generated.go @@ -26,6 +26,9 @@ type imagePushToRegistryOptions struct { TargetRegistryUser string `json:"targetRegistryUser,omitempty"` TargetRegistryPassword string `json:"targetRegistryPassword,omitempty"` TargetImageTag string `json:"targetImageTag,omitempty" validate:"required_if=TagLatest false"` + UseImageNameTags bool `json:"useImageNameTags,omitempty"` + SourceImageNameTags []string `json:"sourceImageNameTags,omitempty"` + TargetImageNameTags []string `json:"targetImageNameTags,omitempty"` TagLatest bool `json:"tagLatest,omitempty"` DockerConfigJSON string `json:"dockerConfigJSON,omitempty"` PushLocalDockerImage bool `json:"pushLocalDockerImage,omitempty"` @@ -151,6 +154,9 @@ func addImagePushToRegistryFlags(cmd *cobra.Command, stepConfig *imagePushToRegi cmd.Flags().StringVar(&stepConfig.TargetRegistryUser, "targetRegistryUser", os.Getenv("PIPER_targetRegistryUser"), "Username of the target registry where the image should be pushed to.") cmd.Flags().StringVar(&stepConfig.TargetRegistryPassword, "targetRegistryPassword", os.Getenv("PIPER_targetRegistryPassword"), "Password of the target registry where the image should be pushed to.") cmd.Flags().StringVar(&stepConfig.TargetImageTag, "targetImageTag", os.Getenv("PIPER_targetImageTag"), "Tag of the targetImages") + cmd.Flags().BoolVar(&stepConfig.UseImageNameTags, "useImageNameTags", false, "Will use the sourceImageNameTags and targetImageNameTags parameters, instead of sourceImages and targetImages.\nsourceImageNameTags can be set by a build step, e.g. kanikoExecute, and is then available in the pipeline environment.\n") + cmd.Flags().StringSliceVar(&stepConfig.SourceImageNameTags, "sourceImageNameTags", []string{}, "List of full names (registry and tag) of the images to be copied. Works in combination with useImageNameTags.") + cmd.Flags().StringSliceVar(&stepConfig.TargetImageNameTags, "targetImageNameTags", []string{}, "List of full names (registry and tag) of the images to be deployed. Works in combination with useImageNameTags.\nIf not set, the value will be the sourceImageNameTags with the targetRegistryUrl incorporated.\n") cmd.Flags().BoolVar(&stepConfig.TagLatest, "tagLatest", false, "Defines if the image should be tagged as `latest`. The parameter is true if targetImageTag is not specified.") 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().BoolVar(&stepConfig.PushLocalDockerImage, "pushLocalDockerImage", false, "Defines if the local image should be pushed to registry") @@ -319,6 +325,38 @@ func imagePushToRegistryMetadata() config.StepData { Aliases: []config.Alias{{Name: "artifactVersion"}, {Name: "containerImageTag"}}, Default: os.Getenv("PIPER_targetImageTag"), }, + { + Name: "useImageNameTags", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + Aliases: []config.Alias{}, + Default: false, + }, + { + Name: "sourceImageNameTags", + ResourceRef: []config.ResourceReference{ + { + Name: "commonPipelineEnvironment", + Param: "container/imageNameTags", + }, + }, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "[]string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: []string{}, + }, + { + Name: "targetImageNameTags", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "[]string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: []string{}, + }, { Name: "tagLatest", ResourceRef: []config.ResourceReference{}, diff --git a/cmd/imagePushToRegistry_test.go b/cmd/imagePushToRegistry_test.go index 29a2fc02dd..4fe93de4d9 100644 --- a/cmd/imagePushToRegistry_test.go +++ b/cmd/imagePushToRegistry_test.go @@ -71,6 +71,32 @@ func TestRunImagePushToRegistry(t *testing.T) { assert.Equal(t, "1.0.0-123-456", config.TargetImageTag) }) + t.Run("multiple imageNameTags", func(t *testing.T) { + t.Parallel() + + config := imagePushToRegistryOptions{ + SourceRegistryURL: "https://source.registry", + SourceImages: []string{"source-image"}, + SourceImageNameTags: []string{"com.sap.docker/ppiper:240104-20240227184612", + "com.sap.docker/ppiper:240104-20240227184612-amd64", + "com.sap.docker/ppiper:240104-20240227184612-aarch64", + }, + SourceRegistryUser: "sourceuser", + SourceRegistryPassword: "sourcepassword", + TargetRegistryURL: "https://target.registry", + TargetImageTag: "1.0.0-123+456", + TargetRegistryUser: "targetuser", + TargetRegistryPassword: "targetpassword", + UseImageNameTags: true, + } + craneMockUtils := &dockermock.CraneMockUtils{} + utils := newImagePushToRegistryMockUtils(craneMockUtils) + err := runImagePushToRegistry(&config, nil, utils) + assert.NoError(t, err) + createdConfig, err := utils.FileRead(targetDockerConfigPath) + assert.Equal(t, customDockerConfig, string(createdConfig)) + }) + t.Run("failed to copy image", func(t *testing.T) { t.Parallel() diff --git a/resources/metadata/imagePushToRegistry.yaml b/resources/metadata/imagePushToRegistry.yaml index dd944afcf3..3dafe2c4be 100644 --- a/resources/metadata/imagePushToRegistry.yaml +++ b/resources/metadata/imagePushToRegistry.yaml @@ -171,6 +171,34 @@ spec: resourceRef: - name: commonPipelineEnvironment param: artifactVersion + - name: useImageNameTags + description: | + Will use the sourceImageNameTags and targetImageNameTags parameters, instead of sourceImages and targetImages. + sourceImageNameTags can be set by a build step, e.g. kanikoExecute, and is then available in the pipeline environment. + type: bool + scope: + - PARAMETERS + - STAGES + - STEPS + - name: sourceImageNameTags + type: "[]string" + description: "List of full names (registry and tag) of the images to be copied. Works in combination with useImageNameTags." + resourceRef: + - name: commonPipelineEnvironment + param: container/imageNameTags + scope: + - PARAMETERS + - STAGES + - STEPS + - name: targetImageNameTags + type: "[]string" + description: | + List of full names (registry and tag) of the images to be deployed. Works in combination with useImageNameTags. + If not set, the value will be the sourceImageNameTags with the targetRegistryUrl incorporated. + scope: + - PARAMETERS + - STAGES + - STEPS - name: tagLatest description: "Defines if the image should be tagged as `latest`. The parameter is true if targetImageTag is not specified." type: bool