1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-14 11:03:09 +02:00
sap-jenkins-library/cmd/imagePushToRegistry.go

253 lines
8.4 KiB
Go

package cmd
import (
"context"
"fmt"
"regexp"
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
"github.com/SAP/jenkins-library/pkg/command"
"github.com/SAP/jenkins-library/pkg/docker"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/telemetry"
)
const (
targetDockerConfigPath = "/root/.docker/config.json"
)
type dockerImageUtils interface {
LoadImage(ctx context.Context, src string) (v1.Image, error)
PushImage(ctx context.Context, im v1.Image, dest, platform string) error
CopyImage(ctx context.Context, src, dest, platform string) error
}
type imagePushToRegistryUtils interface {
command.ExecRunner
piperutils.FileUtils
dockerImageUtils
// Add more methods here, or embed additional interfaces, or remove/replace as required.
// The imagePushToRegistryUtils interface should be descriptive of your runtime dependencies,
// i.e. include everything you need to be able to mock in tests.
// Unit tests shall be executable in parallel (not depend on global state), and don't (re-)test dependencies.
}
type imagePushToRegistryUtilsBundle struct {
*command.Command
*piperutils.Files
dockerImageUtils
// Embed more structs as necessary to implement methods or interfaces you add to imagePushToRegistryUtils.
// Structs embedded in this way must each have a unique set of methods attached.
// If there is no struct which implements the method you need, attach the method to
// imagePushToRegistryUtilsBundle and forward to the implementation of the dependency.
}
func newImagePushToRegistryUtils() imagePushToRegistryUtils {
utils := imagePushToRegistryUtilsBundle{
Command: &command.Command{
StepName: "imagePushToRegistry",
},
Files: &piperutils.Files{},
dockerImageUtils: &docker.CraneUtilsBundle{},
}
// Reroute command output to logging framework
utils.Stdout(log.Writer())
utils.Stderr(log.Writer())
return &utils
}
func imagePushToRegistry(config imagePushToRegistryOptions, telemetryData *telemetry.CustomData) {
// Utils can be used wherever the command.ExecRunner interface is expected.
// It can also be used for example as a mavenExecRunner.
utils := newImagePushToRegistryUtils()
// For HTTP calls import piperhttp "github.com/SAP/jenkins-library/pkg/http"
// and use a &piperhttp.Client{} in a custom system
// Example: step checkmarxExecuteScan.go
// Error situations should be bubbled up until they reach the line below which will then stop execution
// through the log.Entry().Fatal() call leading to an os.Exit(1) in the end.
err := runImagePushToRegistry(&config, telemetryData, utils)
if err != nil {
log.Entry().WithError(err).Fatal("step execution failed")
}
}
func runImagePushToRegistry(config *imagePushToRegistryOptions, telemetryData *telemetry.CustomData, utils imagePushToRegistryUtils) error {
if !config.PushLocalDockerImage {
if len(config.TargetImages) == 0 {
config.TargetImages = mapSourceTargetImages(config.SourceImages)
}
if len(config.TargetImages) != len(config.SourceImages) {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.New("configuration error: please configure targetImage and sourceImage 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, "+", "-")
re := regexp.MustCompile(`^https?://`)
config.SourceRegistryURL = re.ReplaceAllString(config.SourceRegistryURL, "")
config.TargetRegistryURL = re.ReplaceAllString(config.TargetRegistryURL, "")
log.Entry().Debug("Handling destination registry credentials")
if err := handleCredentialsForPrivateRegistry(config.DockerConfigJSON, config.TargetRegistryURL, config.TargetRegistryUser, config.TargetRegistryPassword, utils); err != nil {
return errors.Wrap(err, "failed to handle credentials for target registry")
}
if config.PushLocalDockerImage {
if err := pushLocalImageToTargetRegistry(config, utils); err != nil {
return errors.Wrapf(err, "failed to push local image to %q", config.TargetRegistryURL)
}
return nil
}
log.Entry().Debug("Handling source registry credentials")
if err := handleCredentialsForPrivateRegistry(config.DockerConfigJSON, config.SourceRegistryURL, config.SourceRegistryUser, config.SourceRegistryPassword, utils); err != nil {
return errors.Wrap(err, "failed to handle credentials for source registry")
}
if err := copyImages(config, utils); err != nil {
return errors.Wrap(err, "failed to copy images")
}
return nil
}
func handleCredentialsForPrivateRegistry(dockerConfigJsonPath, registry, username, password string, utils imagePushToRegistryUtils) error {
if len(dockerConfigJsonPath) == 0 {
if len(registry) == 0 || len(username) == 0 || len(password) == 0 {
return errors.New("docker credentials not provided")
}
if _, err := docker.CreateDockerConfigJSON(registry, username, password, "", targetDockerConfigPath, utils); err != nil {
return errors.Wrap(err, "failed to create new docker config")
}
return nil
}
if _, err := docker.CreateDockerConfigJSON(registry, username, password, targetDockerConfigPath, dockerConfigJsonPath, utils); err != nil {
return errors.Wrapf(err, "failed to update docker config %q", dockerConfigJsonPath)
}
if err := docker.MergeDockerConfigJSON(targetDockerConfigPath, dockerConfigJsonPath, utils); err != nil {
return errors.Wrapf(err, "failed to merge docker config files")
}
return nil
}
func copyImages(config *imagePushToRegistryOptions, utils imagePushToRegistryUtils) error {
g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(10)
platform := config.TargetArchitecture
for _, sourceImage := range config.SourceImages {
sourceImage := sourceImage
src := fmt.Sprintf("%s/%s:%s", config.SourceRegistryURL, sourceImage, config.SourceImageTag)
targetImage, ok := config.TargetImages[sourceImage].(string)
if !ok {
return fmt.Errorf("incorrect name of target image: %v", config.TargetImages[sourceImage])
}
if config.TargetImageTag != "" {
g.Go(func() error {
dst := fmt.Sprintf("%s/%s:%s", config.TargetRegistryURL, targetImage, config.TargetImageTag)
log.Entry().Infof("Copying %s to %s...", src, dst)
if err := utils.CopyImage(ctx, src, dst, platform); err != nil {
return err
}
log.Entry().Infof("Copying %s to %s... Done", src, dst)
return nil
})
}
if config.TagLatest {
g.Go(func() error {
dst := fmt.Sprintf("%s/%s", config.TargetRegistryURL, config.TargetImages[sourceImage])
log.Entry().Infof("Copying %s to %s...", src, dst)
if err := utils.CopyImage(ctx, src, dst, platform); 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 pushLocalImageToTargetRegistry(config *imagePushToRegistryOptions, utils imagePushToRegistryUtils) error {
g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(10)
platform := config.TargetArchitecture
log.Entry().Infof("Loading local image...")
img, err := utils.LoadImage(ctx, config.LocalDockerImagePath)
if err != nil {
return err
}
log.Entry().Infof("Loading local image... Done")
for _, trgImage := range config.TargetImages {
trgImage := trgImage
targetImage, ok := trgImage.(string)
if !ok {
return fmt.Errorf("incorrect name of target image: %v", trgImage)
}
if config.TargetImageTag != "" {
g.Go(func() error {
dst := fmt.Sprintf("%s/%s:%s", config.TargetRegistryURL, targetImage, config.TargetImageTag)
log.Entry().Infof("Pushing %s...", dst)
if err := utils.PushImage(ctx, img, dst, platform); err != nil {
return err
}
log.Entry().Infof("Pushing %s... Done", dst)
return nil
})
}
if config.TagLatest {
g.Go(func() error {
dst := fmt.Sprintf("%s/%s", config.TargetRegistryURL, targetImage)
log.Entry().Infof("Pushing %s...", dst)
if err := utils.PushImage(ctx, img, dst, platform); err != nil {
return err
}
log.Entry().Infof("Pushing %s... Done", 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 {
targetImages[sourceImage] = sourceImage
}
return targetImages
}