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 }