github.com/jaylevin/jenkins-library@v1.230.4/pkg/docker/docker.go (about)

     1  package docker
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  
    13  	"github.com/SAP/jenkins-library/pkg/log"
    14  	"github.com/SAP/jenkins-library/pkg/piperutils"
    15  	"github.com/pkg/errors"
    16  
    17  	cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd"
    18  	"github.com/google/go-containerregistry/pkg/authn"
    19  	"github.com/google/go-containerregistry/pkg/crane"
    20  	"github.com/google/go-containerregistry/pkg/name"
    21  	v1 "github.com/google/go-containerregistry/pkg/v1"
    22  	"github.com/google/go-containerregistry/pkg/v1/remote"
    23  )
    24  
    25  // AuthEntry defines base64 encoded username:password required inside a Docker config.json
    26  type AuthEntry struct {
    27  	Auth string `json:"auth,omitempty"`
    28  }
    29  
    30  // CreateDockerConfigJSON creates / updates a Docker config.json with registry credentials
    31  func CreateDockerConfigJSON(registryURL, username, password, targetPath, configPath string, utils piperutils.FileUtils) (string, error) {
    32  
    33  	if len(targetPath) == 0 {
    34  		targetPath = configPath
    35  	}
    36  
    37  	dockerConfig := map[string]interface{}{}
    38  	if exists, _ := utils.FileExists(configPath); exists {
    39  		dockerConfigContent, err := utils.FileRead(configPath)
    40  		if err != nil {
    41  			return "", fmt.Errorf("failed to read file '%v': %w", configPath, err)
    42  		}
    43  
    44  		err = json.Unmarshal(dockerConfigContent, &dockerConfig)
    45  		if err != nil {
    46  			return "", fmt.Errorf("failed to unmarshal json file '%v': %w", configPath, err)
    47  		}
    48  	}
    49  
    50  	credentialsBase64 := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", username, password)))
    51  	dockerAuth := AuthEntry{Auth: credentialsBase64}
    52  
    53  	if dockerConfig["auths"] == nil {
    54  		dockerConfig["auths"] = map[string]AuthEntry{registryURL: dockerAuth}
    55  	} else {
    56  		authEntries, ok := dockerConfig["auths"].(map[string]interface{})
    57  		if !ok {
    58  			return "", fmt.Errorf("failed to read authentication entries from file '%v': format invalid", configPath)
    59  		}
    60  		authEntries[registryURL] = dockerAuth
    61  		dockerConfig["auths"] = authEntries
    62  	}
    63  
    64  	jsonResult, err := json.Marshal(dockerConfig)
    65  	if err != nil {
    66  		return "", fmt.Errorf("failed to marshal Docker config.json: %w", err)
    67  	}
    68  
    69  	//always create the target path directories if any before writing
    70  	err = utils.MkdirAll(filepath.Dir(targetPath), 0777)
    71  	if err != nil {
    72  		return "", fmt.Errorf("failed to create directory path for the Docker config.json file %v:%w", targetPath, err)
    73  	}
    74  	err = utils.FileWrite(targetPath, jsonResult, 0666)
    75  	if err != nil {
    76  		return "", fmt.Errorf("failed to write Docker config.json: %w", err)
    77  	}
    78  
    79  	return targetPath, nil
    80  }
    81  
    82  // Client defines an docker client object
    83  type Client struct {
    84  	imageName     string
    85  	registryURL   string
    86  	localPath     string
    87  	includeLayers bool
    88  	imageFormat   string
    89  }
    90  
    91  // ClientOptions defines the options to be set on the client
    92  type ClientOptions struct {
    93  	ImageName   string
    94  	RegistryURL string
    95  	LocalPath   string
    96  	ImageFormat string
    97  }
    98  
    99  //Download interface for download an image to a local path
   100  type Download interface {
   101  	DownloadImage(imageSource, targetFile string) (v1.Image, error)
   102  	DownloadImageContent(imageSource, targetDir string) (v1.Image, error)
   103  	GetRemoteImageInfo(string) (v1.Image, error)
   104  }
   105  
   106  // SetOptions sets options used for the docker client
   107  func (c *Client) SetOptions(options ClientOptions) {
   108  	c.imageName = options.ImageName
   109  	c.registryURL = options.RegistryURL
   110  	c.localPath = options.LocalPath
   111  	c.imageFormat = options.ImageFormat
   112  }
   113  
   114  //DownloadImageContent downloads the image content into the given targetDir. Returns with an error if the targetDir doesnt exist
   115  func (c *Client) DownloadImageContent(imageSource, targetDir string) (v1.Image, error) {
   116  	if fileInfo, err := os.Stat(targetDir); err != nil {
   117  		return nil, err
   118  	} else if !fileInfo.IsDir() {
   119  		return nil, fmt.Errorf("specified target is not a directory: %s", targetDir)
   120  	}
   121  
   122  	noOpts := []crane.Option{}
   123  
   124  	imageRef, err := c.getImageRef(imageSource)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	img, err := crane.Pull(imageRef.Name(), noOpts...)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  
   134  	tmpFile, err := os.CreateTemp(".", ".piper-download-")
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	defer os.Remove(tmpFile.Name())
   139  
   140  	args := []string{imageRef.Name(), tmpFile.Name()}
   141  
   142  	exportCmd := cranecmd.NewCmdExport(&noOpts)
   143  	exportCmd.SetArgs(args)
   144  
   145  	if err := exportCmd.Execute(); err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	return img, piperutils.Untar(tmpFile.Name(), targetDir, 0)
   150  }
   151  
   152  // DownloadImage downloads the image and saves it as tar at the given path
   153  func (c *Client) DownloadImage(imageSource, targetFile string) (v1.Image, error) {
   154  	noOpts := []crane.Option{}
   155  
   156  	imageRef, err := c.getImageRef(imageSource)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	img, err := crane.Pull(imageRef.Name(), noOpts...)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	tmpFile, err := os.CreateTemp(".", ".piper-download-")
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	craneCmd := cranecmd.NewCmdPull(&noOpts)
   172  	craneCmd.SetOut(log.Writer())
   173  	craneCmd.SetErr(log.Writer())
   174  	craneCmd.SetArgs([]string{imageRef.Name(), tmpFile.Name(), "--format=" + c.imageFormat})
   175  
   176  	if err := craneCmd.Execute(); err != nil {
   177  		defer os.Remove(tmpFile.Name())
   178  		return nil, err
   179  	}
   180  
   181  	if err := os.Rename(tmpFile.Name(), targetFile); err != nil {
   182  		defer os.Remove(tmpFile.Name())
   183  		return nil, err
   184  	}
   185  
   186  	return img, nil
   187  }
   188  
   189  // GetRemoteImageInfo retrieves information about the image (e.g. digest) without actually downoading it
   190  func (c *Client) GetRemoteImageInfo(imageSource string) (v1.Image, error) {
   191  	ref, err := c.getImageRef(imageSource)
   192  	if err != nil {
   193  		return nil, errors.Wrap(err, "parsing image reference")
   194  	}
   195  
   196  	return remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
   197  }
   198  
   199  func (c *Client) getImageRef(image string) (name.Reference, error) {
   200  	opts := []name.Option{}
   201  	registry := ""
   202  
   203  	if len(c.registryURL) > 0 {
   204  		re := regexp.MustCompile(`(?i)^https?://`)
   205  		registry = re.ReplaceAllString(c.registryURL, "")
   206  		opts = append(opts, name.WithDefaultRegistry(registry))
   207  	}
   208  
   209  	return name.ParseReference(path.Join(registry, image), opts...)
   210  }
   211  
   212  // ImageListWithFilePath compiles container image names based on all Dockerfiles found, considering excludes
   213  // according to following search pattern: **/Dockerfile*
   214  // Return value contains a map with image names and file path
   215  // Examples for image names with imageName testImage
   216  // * Dockerfile: `imageName`
   217  // * sub1/Dockerfile: `imageName-sub1`
   218  // * sub2/Dockerfile_proxy: `imageName-sub2-proxy`
   219  func ImageListWithFilePath(imageName string, excludes []string, trimDir string, utils piperutils.FileUtils) (map[string]string, error) {
   220  
   221  	imageList := map[string]string{}
   222  
   223  	pattern := "**/Dockerfile*"
   224  
   225  	matches, err := utils.Glob(pattern)
   226  	if err != nil || len(matches) == 0 {
   227  		return imageList, fmt.Errorf("failed to retrieve Dockerfiles")
   228  	}
   229  
   230  	for _, dockerfilePath := range matches {
   231  		// make sure that the path we have is relative
   232  		// ToDo: needs rework
   233  		//dockerfilePath = strings.ReplaceAll(dockerfilePath, cwd, ".")
   234  
   235  		if piperutils.ContainsString(excludes, dockerfilePath) {
   236  			log.Entry().Infof("Discard %v since it is in the exclude list %v", dockerfilePath, excludes)
   237  			continue
   238  		}
   239  
   240  		if dockerfilePath == "Dockerfile" {
   241  			imageList[imageName] = dockerfilePath
   242  		} else {
   243  			var finalName string
   244  			if base := filepath.Base(dockerfilePath); base == "Dockerfile" {
   245  				subName := strings.ReplaceAll(filepath.Dir(dockerfilePath), string(filepath.Separator), "-")
   246  				if len(trimDir) > 0 {
   247  					// allow to remove trailing sub directories
   248  					// example .ci/app/Dockerfile
   249  					// with trimDir = .ci/ imagename would only contain app part.
   250  					subName = strings.TrimPrefix(subName, strings.ReplaceAll(trimDir, "/", "-"))
   251  					// make sure that subName does not start with a - (e.g. due not configuring trailing slash for trimDir)
   252  					subName = strings.TrimPrefix(subName, "-")
   253  				}
   254  				finalName = fmt.Sprintf("%v-%v", imageName, subName)
   255  			} else {
   256  				parts := strings.FieldsFunc(base, func(separator rune) bool {
   257  					return separator == []rune("-")[0] || separator == []rune("_")[0]
   258  				})
   259  				if len(parts) == 1 {
   260  					return imageList, fmt.Errorf("wrong format of Dockerfile, must be inside a sub-folder or contain a separator")
   261  				}
   262  				parts[0] = imageName
   263  				finalName = strings.Join(parts, "-")
   264  			}
   265  
   266  			imageList[finalName] = dockerfilePath
   267  		}
   268  	}
   269  
   270  	return imageList, nil
   271  }