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
Anil Keshav 8dc2a1bfb4
feat: Add imagePushToRegistry step (#4609)
* imagePushToRegistry new step

* adding copy and push functionality

* including only copy correctly

* groovy step for imagePushToRegistry

* create .docker folder

* imagePushToRegistry new step

* adding copy and push functionality

* including only copy correctly

* groovy step for imagePushToRegistry

* create .docker folder

* fix CopyImage

* test

* test

* Correct docker config path

* Update

* Update

* Update

* Update

* Update

* Use creds from Vault

* Use creds from Vault

* Use creds from Vault

* Use creds from Vault

* Test

* Comment some logic

* Test: move regexp logic

* Test

* Update

* Update

* Clean up

* Update

* Update

* Update interface

* Rename function

* imagePushToRegistry: small refactoring (#4688)

* imagePushToRegistry new step

* adding copy and push functionality

* including only copy correctly

* groovy step for imagePushToRegistry

* create .docker folder

* Correct docker config path

* Update

* Update

* Update

* Update

* Update

* Use creds from Vault

* Use creds from Vault

* Use creds from Vault

* Use creds from Vault

* Test

* Comment some logic

* Test: move regexp logic

* Test

* Update

* Update

* Clean up

* Update

* Update

---------

Co-authored-by: Keshav <anil.keshav@sap.com>
Co-authored-by: Muhammadali Nazarov <muhammadalinazarov@gmail.com>

* Update step yaml file

* Update interface

* Rename func

* Update tests

* Update interface, create mock methods, update tests

* Update mock

* Add md file

* Fix groovy doc, unit test, go unit test

* Update

* Add unit tests

* Support tagLatest param

* Fetch source creds from Vault

* Update yaml file

* Support multiple images

* Update test

* Support copy images in parallel

* Update yaml

* Clean up

* Return err if no creds provided

* Fix tests

* Add err msg

* Add debug log

* Do not use CPE for targetImages

* Support platform

* Delete Jenkins specific creds

* Update groovy: do not handle Jenkins creds

* Delete unused code

* Fix: Support platform

* Fix: Support platform

* Apply suggestion from code review

Co-authored-by: Egor Balakin <14162703+m1ron0xFF@users.noreply.github.com>

* Apply suggestion from code review

Co-authored-by: Egor Balakin <14162703+m1ron0xFF@users.noreply.github.com>

* Add tests for parseDockerImageName

* Add comment that tagArtifactVersion is not supported yet

* Set limit of running goroutines

* Fix: Set limit of running goroutines

* The tagArtifactVersion is not supported yet

---------

Co-authored-by: Muhammadali Nazarov <muhammadalinazarov@gmail.com>
Co-authored-by: Egor Balakin <egor.balakin@sap.com>
Co-authored-by: Vyacheslav Starostin <vyacheslav.starostin@sap.com>
Co-authored-by: Vyacheslav Starostin <32613074+vstarostin@users.noreply.github.com>
Co-authored-by: Egor Balakin <14162703+m1ron0xFF@users.noreply.github.com>
2023-11-30 15:06:31 +06:00

236 lines
7.7 KiB
Go

package cmd
import (
"context"
"fmt"
"regexp"
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 len(config.TargetImages) == 0 {
config.TargetImages = config.SourceImages
}
if len(config.TargetImages) != len(config.SourceImages) {
log.SetErrorCategory(log.ErrorConfiguration)
return errors.New("configuration error: please configure targetImage and sourceImage properly")
}
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 len(config.LocalDockerImagePath) > 0 {
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 i := 0; i < len(config.SourceImages); i++ {
src := fmt.Sprintf("%s/%s", config.SourceRegistryURL, config.SourceImages[i])
dst := fmt.Sprintf("%s/%s", config.TargetRegistryURL, config.TargetImages[i])
g.Go(func() error {
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 {
// imageName is repository + image, e.g test.registry/testImage
imageName := parseDockerImageName(dst)
log.Entry().Infof("Copying %s to %s...", src, imageName)
if err := utils.CopyImage(ctx, src, imageName, platform); err != nil {
return err
}
log.Entry().Infof("Copying %s to %s... Done", src, imageName)
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 i := 0; i < len(config.TargetImages); i++ {
dst := fmt.Sprintf("%s/%s", config.TargetRegistryURL, config.TargetImages[i])
g.Go(func() error {
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 {
// imageName is repository + image, e.g test.registry/testImage
imageName := parseDockerImageName(dst)
log.Entry().Infof("Pushing %s...", imageName)
if err := utils.PushImage(ctx, img, imageName, platform); err != nil {
return err
}
log.Entry().Infof("Pushing %s... Done", imageName)
return nil
})
}
}
if err := g.Wait(); err != nil {
return err
}
return nil
}
func parseDockerImageName(image string) string {
re := regexp.MustCompile(`^(.*?)(?::([^:/]+))?$`)
matches := re.FindStringSubmatch(image)
if len(matches) > 1 {
return matches[1]
}
return image
}