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 }