github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/utils/container/localagent.go (about)

     1  package container
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"path"
     7  	"strings"
     8  
     9  	buildinfo "github.com/jfrog/build-info-go/entities"
    10  	"github.com/jfrog/jfrog-client-go/artifactory"
    11  	"github.com/jfrog/jfrog-client-go/artifactory/services/utils"
    12  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    13  	"github.com/jfrog/jfrog-client-go/utils/log"
    14  )
    15  
    16  // Build-info builder for local agents tools such as: Docker or Podman.
    17  type localAgentbuildInfoBuilder struct {
    18  	buildInfoBuilder *buildInfoBuilder
    19  	// Name of the container CLI tool e.g. docker
    20  	containerManager ContainerManager
    21  	commandType      CommandType
    22  }
    23  
    24  // Create new build info builder container CLI tool
    25  func NewLocalAgentBuildInfoBuilder(image *Image, repository, buildName, buildNumber, project string, serviceManager artifactory.ArtifactoryServicesManager, commandType CommandType, containerManager ContainerManager) (*localAgentbuildInfoBuilder, error) {
    26  	imageSha2, err := containerManager.Id(image)
    27  	if err != nil {
    28  		return nil, err
    29  	}
    30  	builder, err := newBuildInfoBuilder(image, repository, buildName, buildNumber, project, serviceManager)
    31  	if err != nil {
    32  		return nil, err
    33  	}
    34  	builder.setImageSha2(imageSha2)
    35  	return &localAgentbuildInfoBuilder{
    36  		buildInfoBuilder: builder,
    37  		containerManager: containerManager,
    38  		commandType:      commandType,
    39  	}, err
    40  }
    41  
    42  func (labib *localAgentbuildInfoBuilder) GetLayers() *[]utils.ResultItem {
    43  	return &labib.buildInfoBuilder.imageLayers
    44  }
    45  
    46  func (labib *localAgentbuildInfoBuilder) SetSkipTaggingLayers(skipTaggingLayers bool) {
    47  	labib.buildInfoBuilder.skipTaggingLayers = skipTaggingLayers
    48  }
    49  
    50  // Create build-info for a docker image.
    51  func (labib *localAgentbuildInfoBuilder) Build(module string) (*buildinfo.BuildInfo, error) {
    52  	// Search for image build-info.
    53  	candidateLayers, manifest, err := labib.searchImage()
    54  	if err != nil {
    55  		log.Warn("Failed to collect build-info. No layer(s) was found for image:'" + labib.buildInfoBuilder.image.name + "'. Hint, try to delete the image from the local cache and rerun the command")
    56  		log.Debug(err.Error())
    57  		return nil, nil
    58  	} else {
    59  		log.Debug("Found manifest.json with the following layers to create build-info:", candidateLayers)
    60  	}
    61  	// Create build-info from search results.
    62  	return labib.buildInfoBuilder.createBuildInfo(labib.commandType, manifest, candidateLayers, module)
    63  }
    64  
    65  // Search an image in Artifactory and validate its sha2 with local image.
    66  func (labib *localAgentbuildInfoBuilder) searchImage() (map[string]*utils.ResultItem, *manifest, error) {
    67  	longImageName, err := labib.buildInfoBuilder.image.GetImageLongNameWithTag()
    68  	if err != nil {
    69  		return nil, nil, err
    70  	}
    71  	imagePath := strings.Replace(longImageName, ":", "/", 1)
    72  	manifestPathsCandidates := getManifestPaths(imagePath, labib.buildInfoBuilder.getSearchableRepo(), labib.commandType)
    73  	log.Debug("Start searching for image manifest.json")
    74  	for _, path := range manifestPathsCandidates {
    75  		log.Debug(`Searching in:"` + path + `"`)
    76  		resultMap, err := labib.search(path)
    77  		if err != nil {
    78  			return nil, nil, err
    79  		}
    80  		manifest, err := getManifest(resultMap, labib.buildInfoBuilder.serviceManager, labib.buildInfoBuilder.repositoryDetails.key)
    81  		if err != nil {
    82  			return nil, nil, err
    83  		}
    84  		if manifest != nil && labib.isVerifiedManifest(manifest) {
    85  			return resultMap, manifest, nil
    86  		}
    87  	}
    88  	return nil, nil, errorutils.CheckErrorf(imageNotFoundErrorMessage, labib.buildInfoBuilder.image.name)
    89  }
    90  
    91  // Search image layers in artifactory by the provided image path in artifactory.
    92  // If fat-manifest is found, use it to find our image in Artifactory.
    93  func (labib *localAgentbuildInfoBuilder) search(imagePathPattern string) (resultMap map[string]*utils.ResultItem, err error) {
    94  	resultMap, err = performSearch(imagePathPattern, labib.buildInfoBuilder.serviceManager)
    95  	if err != nil {
    96  		log.Debug("Failed to search  marker layer. Error:", err.Error())
    97  		return
    98  	}
    99  	// Validate there are no .marker layers.
   100  	totalDownloaded, err := downloadMarkerLayersToRemoteCache(resultMap, labib.buildInfoBuilder)
   101  	if err != nil {
   102  		log.Debug("Failed to download marker layer. Error:", err.Error())
   103  		return nil, err
   104  	}
   105  	if totalDownloaded > 0 {
   106  		// Search again after .marker layer were downloaded.
   107  		if resultMap, err = performSearch(imagePathPattern, labib.buildInfoBuilder.serviceManager); err != nil {
   108  			log.Debug("Failed to research layers after download marker layers. Error:", err.Error())
   109  			return
   110  		}
   111  	}
   112  	// Check if search results contain multi-architecture images (fat-manifest).
   113  	if searchResult, ok := resultMap["list.manifest.json"]; labib.commandType == Pull && ok {
   114  		// In case of a fat-manifest, Artifactory will create two folders.
   115  		// One folder named as the image tag, which contains the fat manifest.
   116  		// The second folder, named as image's manifest digest, contains the image layers and the image's manifest.
   117  		log.Debug("Found list.manifest.json (fat-manifest). Searching for the image manifest digest in list.manifest.json")
   118  		var digest string
   119  		digest, err = labib.getImageDigestFromFatManifest(*searchResult)
   120  		if err == nil && digest != "" {
   121  			// Remove tag from pattern, place the manifest digest instead.
   122  			imagePathPattern = strings.Replace(imagePathPattern, "/*", "", 1)
   123  			imagePathPattern = path.Join(imagePathPattern[:strings.LastIndex(imagePathPattern, "/")], strings.Replace(digest, ":", "__", 1), "*")
   124  			// Retry search.
   125  			return labib.search(imagePathPattern)
   126  		}
   127  		log.Debug("Couldn't find matching digest in list.manifest.json")
   128  	}
   129  	return resultMap, err
   130  }
   131  
   132  // Verify manifest by comparing sha256, which references to the image digest. If there is no match, return nil.
   133  func (labib *localAgentbuildInfoBuilder) isVerifiedManifest(imageManifest *manifest) bool {
   134  	if imageManifest.Config.Digest != labib.buildInfoBuilder.imageSha2 {
   135  		log.Debug(`Found incorrect manifest.json file. Expects digest "` + labib.buildInfoBuilder.imageSha2 + `" found "` + imageManifest.Config.Digest)
   136  		return false
   137  	}
   138  	return true
   139  }
   140  
   141  func (labib *localAgentbuildInfoBuilder) getImageDigestFromFatManifest(fatManifest utils.ResultItem) (string, error) {
   142  	var fatManifestContent *FatManifest
   143  	if err := downloadLayer(fatManifest, &fatManifestContent, labib.buildInfoBuilder.serviceManager, labib.buildInfoBuilder.repositoryDetails.key); err != nil {
   144  		log.Debug(`failed to unmarshal fat-manifest`)
   145  		return "", err
   146  	}
   147  	imageOs, imageArch, err := labib.containerManager.OsCompatibility(labib.buildInfoBuilder.image)
   148  	if err != nil {
   149  		return "", err
   150  	}
   151  	return searchManifestDigest(imageOs, imageArch, fatManifestContent.Manifests), nil
   152  }
   153  
   154  // When a client tries to pull an image from a remote repository in Artifactory and the client has some the layers cached locally on the disk,
   155  // then Artifactory will not download these layers into the remote repository cache. Instead, it will mark the layer artifacts with .marker suffix files in the remote cache.
   156  // This function download all the marker layers into the remote cache repository.
   157  func downloadMarkerLayersToRemoteCache(resultMap map[string]*utils.ResultItem, builder *buildInfoBuilder) (int, error) {
   158  	if !builder.repositoryDetails.isRemote || len(resultMap) == 0 {
   159  		return 0, nil
   160  	}
   161  	totalDownloaded := 0
   162  	remoteRepo := builder.repositoryDetails.key
   163  	imageName, err := builder.image.GetImageShortName()
   164  	if err != nil {
   165  		return 0, err
   166  	}
   167  	clientDetails := builder.serviceManager.GetConfig().GetServiceDetails().CreateHttpClientDetails()
   168  	// Search for marker layers
   169  	for _, layerData := range resultMap {
   170  		if strings.HasSuffix(layerData.Name, markerLayerSuffix) {
   171  			log.Debug(fmt.Sprintf("Downloading %s layer into remote repository cache...", layerData.Name))
   172  			baseUrl := builder.serviceManager.GetConfig().GetServiceDetails().GetUrl()
   173  			endpoint := "api/docker/" + remoteRepo + "/v2/" + imageName + "/blobs/" + toNoneMarkerLayer(layerData.Name)
   174  			resp, body, err := builder.serviceManager.Client().SendHead(baseUrl+endpoint, &clientDetails)
   175  			if err != nil {
   176  				log.Debug("Failed to download marker layer. Error:", err.Error())
   177  				return totalDownloaded, err
   178  			}
   179  			if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil {
   180  				log.Debug("Failed to download marker layer. HTTP stats code:", resp.StatusCode)
   181  				return totalDownloaded, err
   182  			}
   183  			totalDownloaded++
   184  		}
   185  	}
   186  	return totalDownloaded, nil
   187  }
   188  
   189  func handleForeignLayer(layerMediaType, layerFileName string) error {
   190  	// Allow missing layer to be of a foreign type.
   191  	if layerMediaType == foreignLayerMediaType {
   192  		log.Info(fmt.Sprintf("Foreign layer: %s is missing in Artifactory and therefore will not be added to the build-info.", layerFileName))
   193  		return nil
   194  	}
   195  	return errorutils.CheckErrorf("Could not find layer: " + layerFileName + " in Artifactory")
   196  }