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  }