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