github.com/jfrog/jfrog-cli-go@v1.22.1-0.20200318093948-4826ef344ffd/artifactory/utils/docker/buildinfo.go (about) 1 package docker 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "path" 9 "strings" 10 11 buildutils "github.com/jfrog/jfrog-cli-go/artifactory/utils" 12 "github.com/jfrog/jfrog-client-go/artifactory" 13 "github.com/jfrog/jfrog-client-go/artifactory/buildinfo" 14 "github.com/jfrog/jfrog-client-go/artifactory/services" 15 "github.com/jfrog/jfrog-client-go/artifactory/services/utils" 16 "github.com/jfrog/jfrog-client-go/utils/errorutils" 17 "github.com/jfrog/jfrog-client-go/utils/log" 18 ) 19 20 const ( 21 Pull CommandType = "pull" 22 Push CommandType = "push" 23 ForeignLayerMediaType string = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" 24 ImageNotFoundErrorMessage string = "Could not find docker image in Artifactory, expecting image ID: %s" 25 ) 26 27 // Docker image build info builder. 28 type Builder interface { 29 Build(module string) (*buildinfo.BuildInfo, error) 30 } 31 32 // Create instance of docker build info builder. 33 func BuildInfoBuilder(image Image, repository, buildName, buildNumber string, serviceManager *artifactory.ArtifactoryServicesManager, commandType CommandType) Builder { 34 builder := &buildInfoBuilder{} 35 builder.image = image 36 builder.repository = repository 37 builder.buildName = buildName 38 builder.buildNumber = buildNumber 39 builder.serviceManager = serviceManager 40 builder.commandType = commandType 41 return builder 42 } 43 44 type buildInfoBuilder struct { 45 image Image 46 repository string 47 buildName string 48 buildNumber string 49 serviceManager *artifactory.ArtifactoryServicesManager 50 51 // internal fields 52 imageId string 53 layers []utils.ResultItem 54 artifacts []buildinfo.Artifact 55 dependencies []buildinfo.Dependency 56 commandType CommandType 57 } 58 59 // Create build info for docker image. 60 func (builder *buildInfoBuilder) Build(module string) (*buildinfo.BuildInfo, error) { 61 var err error 62 builder.imageId, err = builder.image.Id() 63 if err != nil { 64 return nil, err 65 } 66 67 err = builder.updateArtifactsAndDependencies() 68 if err != nil { 69 return nil, err 70 } 71 72 // Set build properties only when pushing image. 73 if builder.commandType == Push { 74 _, err = builder.setBuildProperties() 75 if err != nil { 76 return nil, err 77 } 78 } 79 80 return builder.createBuildInfo(module) 81 } 82 83 // Search, validate and create artifacts and dependencies of docker image. 84 func (builder *buildInfoBuilder) updateArtifactsAndDependencies() error { 85 // Search for all the image layer to get the local path inside Artifactory (supporting virtual repos). 86 searchResults, err := builder.getImageLayersFromArtifactory() 87 if err != nil { 88 return err 89 } 90 91 manifest, manifestArtifact, manifestDependency, err := getManifest(builder.imageId, searchResults, builder.serviceManager) 92 if err != nil { 93 return err 94 } 95 96 configLayer, configLayerArtifact, configLayerDependency, err := getConfigLayer(builder.imageId, searchResults, builder.serviceManager) 97 if err != nil { 98 return err 99 } 100 101 if builder.commandType == Push { 102 return builder.handlePush(manifestArtifact, configLayerArtifact, manifest, configLayer, searchResults) 103 } 104 105 return builder.handlePull(manifestDependency, configLayerDependency, manifest, searchResults) 106 } 107 108 // First we will try to get assuming using a reverse proxy (sub domain or port methods). 109 // If fails, we will try the repository path (proxy-less). 110 func (builder *buildInfoBuilder) getImageLayersFromArtifactory() (map[string]utils.ResultItem, error) { 111 var searchResults map[string]utils.ResultItem 112 imagePath := builder.image.Path() 113 114 // Search layers - assuming reverse proxy. 115 searchResults, err := searchImageLayers(builder, path.Join(builder.repository, imagePath, "*"), builder.serviceManager) 116 if err != nil || searchResults != nil { 117 return searchResults, err 118 } 119 120 // Search layers - assuming proxy-less (repository path). 121 // Need to remove the "/" from the image path. 122 searchResults, err = searchImageLayers(builder, path.Join(imagePath[1:], "*"), builder.serviceManager) 123 if err != nil || searchResults != nil { 124 return searchResults, err 125 } 126 127 if builder.commandType == Push { 128 return nil, errorutils.CheckError(errors.New(fmt.Sprintf(ImageNotFoundErrorMessage, builder.imageId))) 129 } 130 131 // If image path includes more than 3 slashes, Artifactory doesn't store this image under 'library', 132 // thus we should not look further. 133 if strings.Count(imagePath, "/") > 3 { 134 return nil, errorutils.CheckError(errors.New(fmt.Sprintf(ImageNotFoundErrorMessage, builder.imageId))) 135 } 136 137 // Assume reverse proxy - this time with 'library' as part of the path. 138 searchResults, err = searchImageLayers(builder, path.Join(builder.repository, "library", imagePath, "*"), builder.serviceManager) 139 if err != nil || searchResults != nil { 140 return searchResults, err 141 } 142 143 // Assume proxy-less - this time with 'library' as part of the path. 144 searchResults, err = searchImageLayers(builder, path.Join(builder.buildReverseProxyPathWithLibrary(), "*"), builder.serviceManager) 145 if err != nil || searchResults != nil { 146 return searchResults, err 147 } 148 149 // Image layers not found. 150 return nil, errorutils.CheckError(errors.New(fmt.Sprintf(ImageNotFoundErrorMessage, builder.imageId))) 151 } 152 153 func (builder *buildInfoBuilder) buildReverseProxyPathWithLibrary() string { 154 endOfRepoNameIndex := strings.Index(builder.image.Path()[1:], "/") 155 return path.Join(builder.repository, "library", builder.image.Path()[endOfRepoNameIndex+1:]) 156 } 157 158 func (builder *buildInfoBuilder) handlePull(manifestDependency, configLayerDependency buildinfo.Dependency, imageManifest *manifest, searchResults map[string]utils.ResultItem) error { 159 // Add dependencies. 160 builder.dependencies = append(builder.dependencies, manifestDependency) 161 builder.dependencies = append(builder.dependencies, configLayerDependency) 162 163 // Add image layers as dependencies. 164 for i := 0; i < len(imageManifest.Layers); i++ { 165 layerFileName := digestToLayer(imageManifest.Layers[i].Digest) 166 item, layerExists := searchResults[layerFileName] 167 if !layerExists { 168 // Check if layer marker exists in Artifactory. 169 item, layerExists = searchResults[layerFileName+".marker"] 170 if !layerExists { 171 err := builder.handleMissingLayer(imageManifest.Layers[i].MediaType, layerFileName) 172 if err != nil { 173 return err 174 } 175 continue 176 } 177 } 178 builder.dependencies = append(builder.dependencies, item.ToDependency()) 179 } 180 return nil 181 } 182 183 func (builder *buildInfoBuilder) handlePush(manifestArtifact, configLayerArtifact buildinfo.Artifact, imageManifest *manifest, configurationLayer *configLayer, searchResults map[string]utils.ResultItem) error { 184 // Add artifacts 185 builder.artifacts = append(builder.artifacts, manifestArtifact) 186 builder.artifacts = append(builder.artifacts, configLayerArtifact) 187 // Add layers 188 builder.layers = append(builder.layers, searchResults["manifest.json"]) 189 builder.layers = append(builder.layers, searchResults[digestToLayer(builder.imageId)]) 190 191 // Add image layers as artifacts and dependencies. 192 for i := 0; i < configurationLayer.getNumberLayers(); i++ { 193 layerFileName := digestToLayer(imageManifest.Layers[i].Digest) 194 item, layerExists := searchResults[layerFileName] 195 if !layerExists { 196 err := builder.handleMissingLayer(imageManifest.Layers[i].MediaType, layerFileName) 197 if err != nil { 198 return err 199 } 200 continue 201 } 202 // Decide if the layer is also a dependency. 203 if i < configurationLayer.getNumberOfDependentLayers() { 204 builder.dependencies = append(builder.dependencies, item.ToDependency()) 205 } 206 207 builder.artifacts = append(builder.artifacts, item.ToArtifact()) 208 builder.layers = append(builder.layers, item) 209 } 210 return nil 211 } 212 213 func (builder *buildInfoBuilder) handleMissingLayer(layerMediaType, layerFileName string) error { 214 // Allow missing layer to be of a foreign type. 215 if layerMediaType == ForeignLayerMediaType { 216 log.Info(fmt.Sprintf("Foreign layer: %s is missing in Artifactory and therefore will not be added to the build-info.", layerFileName)) 217 return nil 218 } 219 220 return errorutils.CheckError(errors.New("Could not find layer: " + layerFileName + " in Artifactory")) 221 } 222 223 // Set build properties on docker image layers in Artifactory. 224 func (builder *buildInfoBuilder) setBuildProperties() (int, error) { 225 props, err := buildutils.CreateBuildProperties(builder.buildName, builder.buildNumber) 226 if err != nil { 227 return 0, err 228 } 229 return builder.serviceManager.SetProps(services.PropsParams{Items: builder.layers, Props: props}) 230 } 231 232 // Create docker build info 233 func (builder *buildInfoBuilder) createBuildInfo(module string) (*buildinfo.BuildInfo, error) { 234 imageProperties := map[string]string{} 235 imageProperties["docker.image.id"] = builder.imageId 236 imageProperties["docker.image.tag"] = builder.image.Tag() 237 238 parentId, err := builder.image.ParentId() 239 if err != nil { 240 return nil, err 241 } 242 if parentId != "" { 243 imageProperties["docker.image.parent"] = parentId 244 } 245 246 if module == "" { 247 module = builder.image.Name() 248 } 249 buildInfo := &buildinfo.BuildInfo{Modules: []buildinfo.Module{{ 250 Id: module, 251 Properties: imageProperties, 252 Artifacts: builder.artifacts, 253 Dependencies: builder.dependencies, 254 }}} 255 return buildInfo, nil 256 } 257 258 // Download and read the manifest from Artifactory. 259 // Returned values: 260 // imageManifest - pointer to the manifest struct, retrieved from Artifactory. 261 // artifact - manifest as buildinfo.Artifact object. 262 // dependency - manifest as buildinfo.Dependency object. 263 func getManifest(imageId string, searchResults map[string]utils.ResultItem, serviceManager *artifactory.ArtifactoryServicesManager) (imageManifest *manifest, artifact buildinfo.Artifact, dependency buildinfo.Dependency, err error) { 264 item := searchResults["manifest.json"] 265 ioReaderCloser, err := serviceManager.ReadRemoteFile(item.GetItemRelativePath()) 266 if err != nil { 267 return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err 268 } 269 defer ioReaderCloser.Close() 270 content, err := ioutil.ReadAll(ioReaderCloser) 271 if err != nil { 272 return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err 273 } 274 275 imageManifest = new(manifest) 276 err = json.Unmarshal(content, &imageManifest) 277 if errorutils.CheckError(err) != nil { 278 return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err 279 } 280 281 // Check that the manifest ID is the right one. 282 if imageManifest.Config.Digest != imageId { 283 return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, errorutils.CheckError(errors.New("Found incorrect manifest.json file, expecting image ID: " + imageId)) 284 } 285 286 artifact = buildinfo.Artifact{Name: "manifest.json", Type: "json", Checksum: &buildinfo.Checksum{Sha1: item.Actual_Sha1, Md5: item.Actual_Md5}} 287 dependency = buildinfo.Dependency{Id: "manifest.json", Type: "json", Checksum: &buildinfo.Checksum{Sha1: item.Actual_Sha1, Md5: item.Actual_Md5}} 288 return 289 } 290 291 // Download and read the config layer from Artifactory. 292 // Returned values: 293 // configurationLayer - pointer to the configuration layer struct, retrieved from Artifactory. 294 // artifact - configuration layer as buildinfo.Artifact object. 295 // dependency - configuration layer as buildinfo.Dependency object. 296 func getConfigLayer(imageId string, searchResults map[string]utils.ResultItem, serviceManager *artifactory.ArtifactoryServicesManager) (configurationLayer *configLayer, artifact buildinfo.Artifact, dependency buildinfo.Dependency, err error) { 297 item := searchResults[digestToLayer(imageId)] 298 ioReaderCloser, err := serviceManager.ReadRemoteFile(item.GetItemRelativePath()) 299 if err != nil { 300 return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err 301 } 302 defer ioReaderCloser.Close() 303 content, err := ioutil.ReadAll(ioReaderCloser) 304 if err != nil { 305 return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err 306 } 307 308 configurationLayer = new(configLayer) 309 err = json.Unmarshal(content, &configurationLayer) 310 if err != nil { 311 return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err 312 } 313 314 artifact = buildinfo.Artifact{Name: digestToLayer(imageId), Checksum: &buildinfo.Checksum{Sha1: item.Actual_Sha1, Md5: item.Actual_Md5}} 315 dependency = buildinfo.Dependency{Id: digestToLayer(imageId), Checksum: &buildinfo.Checksum{Sha1: item.Actual_Sha1, Md5: item.Actual_Md5}} 316 return 317 } 318 319 // Search for image layers in Artifactory. 320 func searchImageLayers(builder *buildInfoBuilder, imagePathPattern string, serviceManager *artifactory.ArtifactoryServicesManager) (map[string]utils.ResultItem, error) { 321 resultMap, err := searchImageHandler(builder, imagePathPattern, serviceManager) 322 if err != nil { 323 return nil, err 324 } 325 326 // Validate image ID layer exists. 327 if _, ok := resultMap[digestToLayer(builder.imageId)]; !ok { 328 // In case of a fat-manifest, Artifactory will create two folders. 329 // One folder named as the image tag, which contains the fat manifest. 330 // The second folder, named as image's manifest digest, contains the image layers and the image's manifest. 331 if _, ok := resultMap["list.manifest.json"]; ok { 332 v, err := builder.image.Manifest() 333 if err != nil { 334 log.Error("Fail to run docker inspect " + builder.image.Tag() + " \nError: " + err.Error()) 335 return nil, err 336 } 337 var listManifest []Manifest 338 err = json.Unmarshal([]byte(v), &listManifest) 339 if err != nil { 340 log.Error("json.Unmarshal failed with " + err.Error() + "\n" + v) 341 return nil, err 342 } 343 result := "" 344 for _, manifest := range listManifest { 345 if *manifest.SchemaV2Manifest.Config.Digest == builder.imageId { 346 result = *manifest.Descriptor.Digest 347 break 348 } 349 } 350 if result != "" { 351 // Remove the tag from the pattern, and place the manifest digest instead. 352 imagePathPattern = strings.Replace(imagePathPattern, "/*", "", 1) 353 imagePathPattern = path.Join(imagePathPattern[:strings.LastIndex(imagePathPattern, "/")], strings.Replace(result, ":", "__", 1), "*") 354 return searchImageHandler(builder, imagePathPattern, serviceManager) 355 } 356 } 357 return nil, nil 358 } 359 return resultMap, nil 360 } 361 362 func searchImageHandler(builder *buildInfoBuilder, imagePathPattern string, serviceManager *artifactory.ArtifactoryServicesManager) (map[string]utils.ResultItem, error) { 363 searchParams := services.NewSearchParams() 364 searchParams.ArtifactoryCommonParams = &utils.ArtifactoryCommonParams{} 365 searchParams.Pattern = imagePathPattern 366 results, err := serviceManager.SearchFiles(searchParams) 367 if err != nil { 368 return nil, err 369 } 370 resultMap := map[string]utils.ResultItem{} 371 for _, v := range results { 372 resultMap[v.Name] = v 373 } 374 return resultMap, nil 375 } 376 377 // Digest of type sha256:30daa5c11544632449b01f450bebfef6b89644e9e683258ed05797abe7c32a6e to 378 // sha256__30daa5c11544632449b01f450bebfef6b89644e9e683258ed05797abe7c32a6e 379 func digestToLayer(digest string) string { 380 return strings.Replace(digest, ":", "__", 1) 381 } 382 383 // Get the total number of layers from the config. 384 func (configLayer *configLayer) getNumberLayers() int { 385 layersNum := len(configLayer.History) 386 for i := len(configLayer.History) - 1; i >= 0; i-- { 387 if configLayer.History[i].EmptyLayer { 388 layersNum-- 389 } 390 } 391 return layersNum 392 } 393 394 // Get the number of dependencies layers from the config. 395 func (configLayer *configLayer) getNumberOfDependentLayers() int { 396 layersNum := len(configLayer.History) 397 newImageLayers := true 398 for i := len(configLayer.History) - 1; i >= 0; i-- { 399 if newImageLayers { 400 layersNum-- 401 } 402 403 if !newImageLayers && configLayer.History[i].EmptyLayer { 404 layersNum-- 405 } 406 407 createdBy := configLayer.History[i].CreatedBy 408 if strings.Contains(createdBy, "ENTRYPOINT") || strings.Contains(createdBy, "MAINTAINER") { 409 newImageLayers = false 410 } 411 } 412 return layersNum 413 } 414 415 // To unmarshal config layer file 416 type configLayer struct { 417 History []history `json:"history,omitempty"` 418 } 419 420 type history struct { 421 Created string `json:"created,omitempty"` 422 CreatedBy string `json:"created_by,omitempty"` 423 EmptyLayer bool `json:"empty_layer,omitempty"` 424 } 425 426 // To unmarshal manifest.json file 427 type manifest struct { 428 Config manifestConfig `json:"config,omitempty"` 429 Layers []layer `json:"layers,omitempty"` 430 } 431 432 type manifestConfig struct { 433 Digest string `json:"digest,omitempty"` 434 } 435 436 type layer struct { 437 Digest string `json:"digest,omitempty"` 438 MediaType string `json:"mediaType,omitempty"` 439 } 440 441 type CommandType string