github.com/jfrog/jfrog-cli-core/v2@v2.52.0/artifactory/utils/container/buildinfo.go (about) 1 package container 2 3 import ( 4 "encoding/json" 5 ioutils "github.com/jfrog/gofrog/io" 6 "os" 7 "path" 8 "strings" 9 10 buildinfo "github.com/jfrog/build-info-go/entities" 11 12 artutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 13 "github.com/jfrog/jfrog-cli-core/v2/common/build" 14 "github.com/jfrog/jfrog-client-go/artifactory" 15 "github.com/jfrog/jfrog-client-go/artifactory/services" 16 "github.com/jfrog/jfrog-client-go/artifactory/services/utils" 17 "github.com/jfrog/jfrog-client-go/utils/errorutils" 18 "github.com/jfrog/jfrog-client-go/utils/io/content" 19 "github.com/jfrog/jfrog-client-go/utils/log" 20 ) 21 22 const ( 23 Pull CommandType = "pull" 24 Push CommandType = "push" 25 foreignLayerMediaType string = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" 26 imageNotFoundErrorMessage string = "Could not find docker image in Artifactory, expecting image tag: %s" 27 markerLayerSuffix string = ".marker" 28 ) 29 30 // Docker image build info builder. 31 type Builder interface { 32 Build(module string) (*buildinfo.BuildInfo, error) 33 UpdateArtifactsAndDependencies() error 34 GetLayers() *[]utils.ResultItem 35 } 36 37 type buildInfoBuilder struct { 38 image *Image 39 repositoryDetails RepositoryDetails 40 buildName string 41 buildNumber string 42 project string 43 serviceManager artifactory.ArtifactoryServicesManager 44 imageSha2 string 45 // If true, don't set layers props in Artifactory. 46 skipTaggingLayers bool 47 imageLayers []utils.ResultItem 48 } 49 50 // Create instance of docker build info builder. 51 func newBuildInfoBuilder(image *Image, repository, buildName, buildNumber, project string, serviceManager artifactory.ArtifactoryServicesManager) (*buildInfoBuilder, error) { 52 var err error 53 builder := &buildInfoBuilder{} 54 builder.repositoryDetails.key = repository 55 builder.repositoryDetails.isRemote, err = artutils.IsRemoteRepo(repository, serviceManager) 56 if err != nil { 57 return nil, err 58 } 59 builder.image = image 60 builder.buildName = buildName 61 builder.buildNumber = buildNumber 62 builder.project = project 63 builder.serviceManager = serviceManager 64 return builder, nil 65 } 66 67 type RepositoryDetails struct { 68 key string 69 isRemote bool 70 } 71 72 func (builder *buildInfoBuilder) setImageSha2(imageSha2 string) { 73 builder.imageSha2 = imageSha2 74 } 75 76 func (builder *buildInfoBuilder) GetLayers() *[]utils.ResultItem { 77 return &builder.imageLayers 78 } 79 80 func (builder *buildInfoBuilder) getSearchableRepo() string { 81 if builder.repositoryDetails.isRemote { 82 return builder.repositoryDetails.key + "-cache" 83 } 84 return builder.repositoryDetails.key 85 } 86 87 // Set build properties on image layers in Artifactory. 88 func setBuildProperties(buildName, buildNumber, project string, imageLayers []utils.ResultItem, serviceManager artifactory.ArtifactoryServicesManager) (err error) { 89 props, err := build.CreateBuildProperties(buildName, buildNumber, project) 90 if err != nil { 91 return 92 } 93 pathToFile, err := writeLayersToFile(imageLayers) 94 if err != nil { 95 return 96 } 97 reader := content.NewContentReader(pathToFile, content.DefaultKey) 98 defer ioutils.Close(reader, &err) 99 _, err = serviceManager.SetProps(services.PropsParams{Reader: reader, Props: props}) 100 return 101 } 102 103 // Download the content of layer search result. 104 func downloadLayer(searchResult utils.ResultItem, result interface{}, serviceManager artifactory.ArtifactoryServicesManager, repo string) error { 105 // Search results may include artifacts from the remote-cache repository. 106 // When artifact is expired, it cannot be downloaded from the remote-cache. 107 // To solve this, change back the search results' repository, to its origin remote/virtual. 108 searchResult.Repo = repo 109 return artutils.RemoteUnmarshal(serviceManager, searchResult.GetItemRelativePath(), result) 110 } 111 112 func writeLayersToFile(layers []utils.ResultItem) (filePath string, err error) { 113 writer, err := content.NewContentWriter("results", true, false) 114 if err != nil { 115 return 116 } 117 defer ioutils.Close(writer, &err) 118 for _, layer := range layers { 119 writer.Write(layer) 120 } 121 filePath = writer.GetFilePath() 122 return 123 } 124 125 // Return - manifest artifacts as buildinfo.Artifact struct. 126 func getManifestArtifact(manifest *utils.ResultItem) (artifact buildinfo.Artifact) { 127 return buildinfo.Artifact{Name: "manifest.json", Type: "json", Checksum: buildinfo.Checksum{Sha1: manifest.Actual_Sha1, Md5: manifest.Actual_Md5}, Path: path.Join(manifest.Path, manifest.Name)} 128 } 129 130 // Return - fat manifest artifacts as buildinfo.Artifact struct. 131 func getFatManifestArtifact(fatManifest *utils.ResultItem) (artifact buildinfo.Artifact) { 132 return buildinfo.Artifact{Name: "list.manifest.json", Type: "json", Checksum: buildinfo.Checksum{Sha1: fatManifest.Actual_Sha1, Md5: fatManifest.Actual_Md5}, Path: path.Join(fatManifest.Path, fatManifest.Name)} 133 } 134 135 // Return - manifest dependency as buildinfo.Dependency struct. 136 func getManifestDependency(searchResults *utils.ResultItem) (dependency buildinfo.Dependency) { 137 return buildinfo.Dependency{Id: "manifest.json", Type: "json", Checksum: buildinfo.Checksum{Sha1: searchResults.Actual_Sha1, Md5: searchResults.Actual_Md5}} 138 } 139 140 // Read the file which contains the following format: 'IMAGE-TAG-IN-ARTIFACTORY'@sha256'SHA256-OF-THE-IMAGE-MANIFEST'. 141 func GetImageTagWithDigest(filePath string) (*Image, string, error) { 142 var buildxMetaData buildxMetaData 143 data, err := os.ReadFile(filePath) 144 if errorutils.CheckError(err) != nil { 145 log.Debug("os.ReadFile failed with '%s'\n", err) 146 return nil, "", err 147 } 148 err = json.Unmarshal(data, &buildxMetaData) 149 if err != nil { 150 log.Debug("failed unmarshalling buildxMetaData file with error: " + err.Error() + ". falling back to Kanico/OC file format...") 151 } 152 // Try to read buildx metadata file. 153 if buildxMetaData.ImageName != "" && buildxMetaData.ImageSha256 != "" { 154 return NewImage(buildxMetaData.ImageName), buildxMetaData.ImageSha256, nil 155 } 156 // Try read Kaniko/oc file. 157 splittedData := strings.Split(string(data), `@`) 158 if len(splittedData) != 2 { 159 return nil, "", errorutils.CheckErrorf(`unexpected file format "` + filePath + `". The file should include one line in the following format: image-tag@sha256`) 160 } 161 tag, sha256 := splittedData[0], strings.Trim(splittedData[1], "\n") 162 if tag == "" || sha256 == "" { 163 err = errorutils.CheckErrorf(`missing image-tag/sha256 in file: "` + filePath + `"`) 164 if err != nil { 165 return nil, "", err 166 } 167 } 168 return NewImage(tag), sha256, nil 169 } 170 171 type buildxMetaData struct { 172 ImageName string `json:"image.name"` 173 ImageSha256 string `json:"containerimage.digest"` 174 } 175 176 // Search for manifest digest in fat manifest, which contains specific platforms. 177 func searchManifestDigest(imageOs, imageArch string, manifestList []ManifestDetails) (digest string) { 178 for _, manifest := range manifestList { 179 if manifest.Platform.Os == imageOs && manifest.Platform.Architecture == imageArch { 180 digest = manifest.Digest 181 break 182 } 183 } 184 return 185 } 186 187 // Returns a map of: layer-digest -> layer-search-result 188 func performSearch(imagePathPattern string, serviceManager artifactory.ArtifactoryServicesManager) (resultMap map[string]*utils.ResultItem, err error) { 189 searchParams := services.NewSearchParams() 190 searchParams.CommonParams = &utils.CommonParams{} 191 searchParams.Pattern = imagePathPattern 192 var reader *content.ContentReader 193 reader, err = serviceManager.SearchFiles(searchParams) 194 if err != nil { 195 return nil, err 196 } 197 defer ioutils.Close(reader, &err) 198 resultMap = make(map[string]*utils.ResultItem) 199 for resultItem := new(utils.ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(utils.ResultItem) { 200 resultMap[resultItem.Name] = resultItem 201 } 202 err = reader.GetError() 203 return 204 } 205 206 // Returns a map of: image-sha2 -> image-layers 207 func performMultiPlatformImageSearch(imagePathPattern string, serviceManager artifactory.ArtifactoryServicesManager) (resultMap map[string][]*utils.ResultItem, err error) { 208 searchParams := services.NewSearchParams() 209 searchParams.CommonParams = &utils.CommonParams{} 210 searchParams.Pattern = imagePathPattern 211 searchParams.Recursive = true 212 var reader *content.ContentReader 213 reader, err = serviceManager.SearchFiles(searchParams) 214 if err != nil { 215 return nil, err 216 } 217 defer ioutils.Close(reader, &err) 218 pathToSha2 := make(map[string]string) 219 pathToImageLayers := make(map[string][]*utils.ResultItem) 220 resultMap = make(map[string][]*utils.ResultItem) 221 for resultItem := new(utils.ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(utils.ResultItem) { 222 pathToImageLayers[resultItem.Path] = append(pathToImageLayers[resultItem.Path], resultItem) 223 if resultItem.Name == "manifest.json" { 224 pathToSha2[resultItem.Path] = "sha256:" + resultItem.Sha256 225 } 226 } 227 for k, v := range pathToSha2 { 228 resultMap[v] = append(resultMap[v], pathToImageLayers[k]...) 229 } 230 err = reader.GetError() 231 return 232 } 233 234 // Digest of type sha256:30daa5c11544632449b01f450bebfef6b89644e9e683258ed05797abe7c32a6e to 235 // sha256__30daa5c11544632449b01f450bebfef6b89644e9e683258ed05797abe7c32a6e 236 func digestToLayer(digest string) string { 237 return strings.Replace(digest, ":", "__", 1) 238 } 239 240 // Get the number of dependencies layers from the config. 241 func (configLayer *configLayer) getNumberOfDependentLayers() int { 242 layersNum := len(configLayer.History) 243 newImageLayers := true 244 for i := len(configLayer.History) - 1; i >= 0; i-- { 245 if newImageLayers { 246 layersNum-- 247 } 248 if !newImageLayers && configLayer.History[i].EmptyLayer { 249 layersNum-- 250 } 251 createdBy := configLayer.History[i].CreatedBy 252 if strings.Contains(createdBy, "ENTRYPOINT") || strings.Contains(createdBy, "MAINTAINER") { 253 newImageLayers = false 254 } 255 } 256 return layersNum 257 } 258 259 func removeDuplicateLayers(imageMLayers []layer) []layer { 260 res := imageMLayers[:0] 261 // Use map to record duplicates as we find them. 262 encountered := map[string]bool{} 263 for _, v := range imageMLayers { 264 if !encountered[v.Digest] { 265 res = append(res, v) 266 encountered[v.Digest] = true 267 } 268 } 269 return res 270 } 271 272 func toNoneMarkerLayer(layer string) string { 273 imageId := strings.Replace(layer, "__", ":", 1) 274 return strings.Replace(imageId, ".marker", "", 1) 275 } 276 277 type CommandType string 278 279 // Create an image's build info from manifest.json. 280 func (builder *buildInfoBuilder) createBuildInfo(commandType CommandType, manifest *manifest, candidateLayers map[string]*utils.ResultItem, module string) (*buildinfo.BuildInfo, error) { 281 if manifest == nil { 282 return nil, nil 283 } 284 imageProperties := map[string]string{ 285 "docker.image.id": builder.imageSha2, 286 "docker.image.tag": builder.image.Name(), 287 } 288 if module == "" { 289 var err error 290 if module, err = builder.image.GetImageShortNameWithTag(); err != nil { 291 return nil, err 292 } 293 } 294 // Manifest may hold 'empty layers'. As a result, promotion will fail to promote the same layer more than once. 295 manifest.Layers = removeDuplicateLayers(manifest.Layers) 296 var artifacts []buildinfo.Artifact 297 var dependencies []buildinfo.Dependency 298 var err error 299 switch commandType { 300 case Pull: 301 dependencies = builder.createPullBuildProperties(manifest, candidateLayers) 302 case Push: 303 artifacts, dependencies, builder.imageLayers, err = builder.createPushBuildProperties(manifest, candidateLayers) 304 if err != nil { 305 return nil, err 306 } 307 if !builder.skipTaggingLayers { 308 if err := setBuildProperties(builder.buildName, builder.buildNumber, builder.project, builder.imageLayers, builder.serviceManager); err != nil { 309 return nil, err 310 } 311 } 312 } 313 buildInfo := &buildinfo.BuildInfo{Modules: []buildinfo.Module{{ 314 Id: module, 315 Type: buildinfo.Docker, 316 Properties: imageProperties, 317 Artifacts: artifacts, 318 Dependencies: dependencies, 319 }}} 320 return buildInfo, nil 321 } 322 323 // Create the image's build info from list.manifest.json. 324 func (builder *buildInfoBuilder) createMultiPlatformBuildInfo(fatManifest *FatManifest, searchRultFatManifest *utils.ResultItem, candidateimages map[string][]*utils.ResultItem, module string) (*buildinfo.BuildInfo, error) { 325 imageProperties := map[string]string{ 326 "docker.image.tag": builder.image.Name(), 327 } 328 if module == "" { 329 imageName, err := builder.image.GetImageShortNameWithTag() 330 if err != nil { 331 return nil, err 332 } 333 module = imageName 334 } 335 // Add layers. 336 builder.imageLayers = append(builder.imageLayers, *searchRultFatManifest) 337 // Create fat-manifest module 338 buildInfo := &buildinfo.BuildInfo{Modules: []buildinfo.Module{{ 339 Id: module, 340 Type: buildinfo.Docker, 341 Properties: imageProperties, 342 Artifacts: []buildinfo.Artifact{getFatManifestArtifact(searchRultFatManifest)}, 343 }}} 344 // Create all image arch modules 345 for _, manifest := range fatManifest.Manifests { 346 image := candidateimages[manifest.Digest] 347 var artifacts []buildinfo.Artifact 348 for _, layer := range image { 349 builder.imageLayers = append(builder.imageLayers, *layer) 350 if layer.Name == "manifest.json" { 351 artifacts = append(artifacts, getManifestArtifact(layer)) 352 } else { 353 artifacts = append(artifacts, layer.ToArtifact()) 354 } 355 } 356 buildInfo.Modules = append(buildInfo.Modules, buildinfo.Module{ 357 Id: manifest.Platform.Os + "/" + manifest.Platform.Architecture + "/" + module, 358 Type: buildinfo.Docker, 359 Artifacts: artifacts, 360 }) 361 } 362 return buildInfo, setBuildProperties(builder.buildName, builder.buildNumber, builder.project, builder.imageLayers, builder.serviceManager) 363 } 364 365 func (builder *buildInfoBuilder) createPushBuildProperties(imageManifest *manifest, candidateLayers map[string]*utils.ResultItem) (artifacts []buildinfo.Artifact, dependencies []buildinfo.Dependency, imageLayers []utils.ResultItem, err error) { 366 // Add artifacts. 367 artifacts = append(artifacts, getManifestArtifact(candidateLayers["manifest.json"])) 368 artifacts = append(artifacts, candidateLayers[digestToLayer(builder.imageSha2)].ToArtifact()) 369 370 // Add layers. 371 imageLayers = append(imageLayers, *candidateLayers["manifest.json"]) 372 imageLayers = append(imageLayers, *candidateLayers[digestToLayer(builder.imageSha2)]) 373 374 totalLayers := len(imageManifest.Layers) 375 totalDependencies, err := builder.totalDependencies(candidateLayers[digestToLayer(builder.imageSha2)]) 376 if err != nil { 377 return nil, nil, nil, err 378 } 379 380 // Add image layers as artifacts and dependencies. 381 for i := 0; i < totalLayers; i++ { 382 layerFileName := digestToLayer(imageManifest.Layers[i].Digest) 383 item, layerExists := candidateLayers[layerFileName] 384 if !layerExists { 385 err := handleForeignLayer(imageManifest.Layers[i].MediaType, layerFileName) 386 if err != nil { 387 return nil, nil, nil, err 388 } 389 continue 390 } 391 392 // Decide if the layer is also a dependency. 393 if i < totalDependencies { 394 dependencies = append(dependencies, item.ToDependency()) 395 } 396 artifacts = append(artifacts, item.ToArtifact()) 397 imageLayers = append(imageLayers, *item) 398 } 399 return 400 } 401 402 func (builder *buildInfoBuilder) createPullBuildProperties(imageManifest *manifest, imageLayers map[string]*utils.ResultItem) []buildinfo.Dependency { 403 configDependencies, err := getDependenciesFromManifestConfig(imageLayers, builder.imageSha2) 404 if err != nil { 405 log.Debug(err.Error()) 406 return nil 407 } 408 409 layerDependencies, err := getDependenciesFromManifestLayer(imageLayers, imageManifest) 410 if err != nil { 411 log.Debug(err.Error()) 412 return nil 413 } 414 415 return append(configDependencies, layerDependencies...) 416 } 417 418 func getDependenciesFromManifestConfig(candidateLayers map[string]*utils.ResultItem, imageSha2 string) ([]buildinfo.Dependency, error) { 419 var dependencies []buildinfo.Dependency 420 manifestSearchResults, found := candidateLayers["manifest.json"] 421 if !found { 422 return nil, errorutils.CheckErrorf("failed to collect build-info. The manifest.json was not found in Artifactory") 423 } 424 425 dependencies = append(dependencies, getManifestDependency(manifestSearchResults)) 426 imageDetails, found := candidateLayers[digestToLayer(imageSha2)] 427 if !found { 428 return nil, errorutils.CheckErrorf("failed to collect build-info. Image '" + imageSha2 + "' was not found in Artifactory") 429 } 430 431 return append(dependencies, imageDetails.ToDependency()), nil 432 } 433 434 func getDependenciesFromManifestLayer(layers map[string]*utils.ResultItem, imageManifest *manifest) ([]buildinfo.Dependency, error) { 435 var dependencies []buildinfo.Dependency 436 for i := 0; i < len(imageManifest.Layers); i++ { 437 layerFileName := digestToLayer(imageManifest.Layers[i].Digest) 438 item, layerExists := layers[layerFileName] 439 if !layerExists { 440 if err := handleForeignLayer(imageManifest.Layers[i].MediaType, layerFileName); err != nil { 441 return nil, err 442 } 443 continue 444 } 445 dependencies = append(dependencies, item.ToDependency()) 446 } 447 return dependencies, nil 448 } 449 450 func (builder *buildInfoBuilder) totalDependencies(image *utils.ResultItem) (int, error) { 451 configurationLayer := new(configLayer) 452 if err := downloadLayer(*image, &configurationLayer, builder.serviceManager, builder.repositoryDetails.key); err != nil { 453 return 0, err 454 } 455 return configurationLayer.getNumberOfDependentLayers(), nil 456 }