zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/extensions/search/convert/metadb.go (about) 1 package convert 2 3 import ( 4 "context" 5 "sort" 6 "strconv" 7 "time" 8 9 "github.com/99designs/gqlgen/graphql" 10 ispec "github.com/opencontainers/image-spec/specs-go/v1" 11 "github.com/vektah/gqlparser/v2/gqlerror" 12 13 zerr "zotregistry.io/zot/errors" 14 zcommon "zotregistry.io/zot/pkg/common" 15 cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" 16 "zotregistry.io/zot/pkg/extensions/search/gql_generated" 17 "zotregistry.io/zot/pkg/extensions/search/pagination" 18 "zotregistry.io/zot/pkg/log" 19 mTypes "zotregistry.io/zot/pkg/meta/types" 20 ) 21 22 type SkipQGLField struct { 23 Vulnerabilities bool 24 } 25 26 func UpdateLastUpdatedTimestamp(repoLastUpdatedTimestamp *time.Time, 27 lastUpdatedImageSummary *gql_generated.ImageSummary, imageSummary *gql_generated.ImageSummary, 28 ) *gql_generated.ImageSummary { 29 newLastUpdatedImageSummary := lastUpdatedImageSummary 30 31 if repoLastUpdatedTimestamp.Equal(time.Time{}) { 32 // initialize with first time value 33 *repoLastUpdatedTimestamp = *imageSummary.LastUpdated 34 newLastUpdatedImageSummary = imageSummary 35 } else if repoLastUpdatedTimestamp.Before(*imageSummary.LastUpdated) { 36 *repoLastUpdatedTimestamp = *imageSummary.LastUpdated 37 newLastUpdatedImageSummary = imageSummary 38 } 39 40 return newLastUpdatedImageSummary 41 } 42 43 func getReferrers(referrersInfo []mTypes.ReferrerInfo) []*gql_generated.Referrer { 44 referrers := make([]*gql_generated.Referrer, 0, len(referrersInfo)) 45 46 for _, referrerInfo := range referrersInfo { 47 referrerInfo := referrerInfo 48 49 referrers = append(referrers, &gql_generated.Referrer{ 50 MediaType: &referrerInfo.MediaType, 51 ArtifactType: &referrerInfo.ArtifactType, 52 Size: &referrerInfo.Size, 53 Digest: &referrerInfo.Digest, 54 Annotations: getAnnotationsFromMap(referrerInfo.Annotations), 55 }) 56 } 57 58 return referrers 59 } 60 61 func getAnnotationsFromMap(annotationsMap map[string]string) []*gql_generated.Annotation { 62 annotations := make([]*gql_generated.Annotation, 0, len(annotationsMap)) 63 64 for key, value := range annotationsMap { 65 key := key 66 value := value 67 68 annotations = append(annotations, &gql_generated.Annotation{ 69 Key: &key, 70 Value: &value, 71 }) 72 } 73 74 return annotations 75 } 76 77 func getImageBlobsInfo(manifestDigest string, manifestSize int64, configDigest string, configSize int64, 78 layers []ispec.Descriptor, 79 ) (int64, map[string]int64) { 80 imageBlobsMap := map[string]int64{} 81 imageSize := int64(0) 82 83 // add config size 84 imageSize += configSize 85 imageBlobsMap[configDigest] = configSize 86 87 // add manifest size 88 imageSize += manifestSize 89 imageBlobsMap[manifestDigest] = manifestSize 90 91 // add layers size 92 for _, layer := range layers { 93 imageBlobsMap[layer.Digest.String()] = layer.Size 94 imageSize += layer.Size 95 } 96 97 return imageSize, imageBlobsMap 98 } 99 100 func RepoMeta2ImageSummaries(ctx context.Context, repoMeta mTypes.RepoMeta, 101 imageMeta map[string]mTypes.ImageMeta, skip SkipQGLField, cveInfo cveinfo.CveInfo, 102 ) []*gql_generated.ImageSummary { 103 imageSummaries := make([]*gql_generated.ImageSummary, 0, len(repoMeta.Tags)) 104 105 // Make sure the tags are sorted 106 // We need to implement a proper fix for this taking into account 107 // the sorting criteria used in the requested page 108 tags := make([]string, 0, len(repoMeta.Tags)) 109 for tag := range repoMeta.Tags { 110 tags = append(tags, tag) 111 } 112 113 // Sorting ascending by tag name should do for now 114 sort.Strings(tags) 115 116 for _, tag := range tags { 117 descriptor := repoMeta.Tags[tag] 118 119 imageSummary, _, err := FullImageMeta2ImageSummary(ctx, GetFullImageMeta(tag, repoMeta, imageMeta[descriptor.Digest])) 120 if err != nil { 121 continue 122 } 123 124 // CVE scanning is expensive, only scan for final slice of results 125 updateImageSummaryVulnerabilities(ctx, imageSummary, skip, cveInfo) 126 127 imageSummaries = append(imageSummaries, imageSummary) 128 } 129 130 return imageSummaries 131 } 132 133 func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMeta, 134 imageMetaMap map[string]mTypes.ImageMeta, skip SkipQGLField, cveInfo cveinfo.CveInfo, log log.Logger, 135 ) (*gql_generated.RepoSummary, []*gql_generated.ImageSummary) { 136 repoName := repoMeta.Name 137 imageSummaries := make([]*gql_generated.ImageSummary, 0, len(repoMeta.Tags)) 138 139 for tag, descriptor := range repoMeta.Tags { 140 imageMeta := imageMetaMap[descriptor.Digest] 141 142 imageSummary, _, err := FullImageMeta2ImageSummary(ctx, GetFullImageMeta(tag, repoMeta, imageMeta)) 143 if err != nil { 144 log.Error().Str("repository", repoName).Str("reference", tag). 145 Msg("metadb: error while converting descriptor for image") 146 147 continue 148 } 149 150 updateImageSummaryVulnerabilities(ctx, imageSummary, skip, cveInfo) 151 152 imageSummaries = append(imageSummaries, imageSummary) 153 } 154 155 repoSummary := RepoMeta2RepoSummary(ctx, repoMeta, imageMetaMap) 156 157 updateRepoSummaryVulnerabilities(ctx, repoSummary, skip, cveInfo) 158 159 return repoSummary, imageSummaries 160 } 161 162 func GetFullImageMeta(tag string, repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta, 163 ) mTypes.FullImageMeta { 164 return mTypes.FullImageMeta{ 165 Repo: repoMeta.Name, 166 Tag: tag, 167 MediaType: imageMeta.MediaType, 168 Digest: imageMeta.Digest, 169 Size: imageMeta.Size, 170 Index: imageMeta.Index, 171 Manifests: GetFullManifestMeta(repoMeta, imageMeta.Manifests), 172 Referrers: repoMeta.Referrers[imageMeta.Digest.String()], 173 Statistics: repoMeta.Statistics[imageMeta.Digest.String()], 174 Signatures: repoMeta.Signatures[imageMeta.Digest.String()], 175 } 176 } 177 178 func GetFullManifestMeta(repoMeta mTypes.RepoMeta, manifests []mTypes.ManifestMeta) []mTypes.FullManifestMeta { 179 results := make([]mTypes.FullManifestMeta, 0, len(manifests)) 180 181 for i := range manifests { 182 results = append(results, mTypes.FullManifestMeta{ 183 ManifestMeta: manifests[i], 184 Referrers: repoMeta.Referrers[manifests[i].Digest.String()], 185 Statistics: repoMeta.Statistics[manifests[i].Digest.String()], 186 Signatures: repoMeta.Signatures[manifests[i].Digest.String()], 187 }) 188 } 189 190 return results 191 } 192 193 func StringMap2Annotations(strMap map[string]string) []*gql_generated.Annotation { 194 annotations := make([]*gql_generated.Annotation, 0, len(strMap)) 195 196 for key, value := range strMap { 197 key := key 198 value := value 199 200 annotations = append(annotations, &gql_generated.Annotation{ 201 Key: &key, 202 Value: &value, 203 }) 204 } 205 206 return annotations 207 } 208 209 func GetPreloads(ctx context.Context) map[string]bool { 210 if !graphql.HasOperationContext(ctx) { 211 return map[string]bool{} 212 } 213 214 nestedPreloads := GetNestedPreloads( 215 graphql.GetOperationContext(ctx), 216 graphql.CollectFieldsCtx(ctx, nil), 217 "", 218 ) 219 220 preloads := map[string]bool{} 221 222 for _, str := range nestedPreloads { 223 preloads[str] = true 224 } 225 226 return preloads 227 } 228 229 func GetNestedPreloads(ctx *graphql.OperationContext, fields []graphql.CollectedField, prefix string, 230 ) []string { 231 preloads := []string{} 232 233 for _, column := range fields { 234 prefixColumn := GetPreloadString(prefix, column.Name) 235 preloads = append(preloads, prefixColumn) 236 preloads = append(preloads, 237 GetNestedPreloads(ctx, graphql.CollectFields(ctx, column.Selections, nil), prefixColumn)..., 238 ) 239 } 240 241 return preloads 242 } 243 244 func GetPreloadString(prefix, name string) string { 245 if len(prefix) > 0 { 246 return prefix + "." + name 247 } 248 249 return name 250 } 251 252 func GetSignaturesInfo(isSigned bool, signatures mTypes.ManifestSignatures) []*gql_generated.SignatureSummary { 253 signaturesInfo := []*gql_generated.SignatureSummary{} 254 255 if !isSigned { 256 return signaturesInfo 257 } 258 259 for sigType, signatures := range signatures { 260 for _, sig := range signatures { 261 for _, layer := range sig.LayersInfo { 262 var ( 263 isTrusted bool 264 author string 265 tool string 266 ) 267 268 if layer.Signer != "" { 269 author = layer.Signer 270 271 if !layer.Date.IsZero() && time.Now().After(layer.Date) { 272 isTrusted = false 273 } else { 274 isTrusted = true 275 } 276 } else { 277 isTrusted = false 278 author = "" 279 } 280 281 tool = sigType 282 283 signaturesInfo = append(signaturesInfo, 284 &gql_generated.SignatureSummary{Tool: &tool, IsTrusted: &isTrusted, Author: &author}) 285 } 286 } 287 } 288 289 return signaturesInfo 290 } 291 292 func PaginatedRepoMeta2RepoSummaries(ctx context.Context, repoMetaList []mTypes.RepoMeta, 293 imageMetaMap map[string]mTypes.ImageMeta, filter mTypes.Filter, pageInput pagination.PageInput, 294 cveInfo cveinfo.CveInfo, skip SkipQGLField, 295 ) ([]*gql_generated.RepoSummary, zcommon.PageInfo, error) { 296 reposPageFinder, err := pagination.NewRepoSumPageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy) 297 if err != nil { 298 return []*gql_generated.RepoSummary{}, zcommon.PageInfo{}, err 299 } 300 301 for _, repoMeta := range repoMetaList { 302 repoSummary := RepoMeta2RepoSummary(ctx, repoMeta, imageMetaMap) 303 304 if RepoSumAcceptedByFilter(repoSummary, filter) { 305 reposPageFinder.Add(repoSummary) 306 } 307 } 308 309 page, pageInfo := reposPageFinder.Page() 310 311 // CVE scanning is expensive, only scan for the current page 312 for _, repoSummary := range page { 313 updateRepoSummaryVulnerabilities(ctx, repoSummary, skip, cveInfo) 314 } 315 316 return page, pageInfo, nil 317 } 318 319 func RepoMeta2RepoSummary(ctx context.Context, repoMeta mTypes.RepoMeta, 320 imageMetaMap map[string]mTypes.ImageMeta, 321 ) *gql_generated.RepoSummary { 322 var ( 323 repoName = repoMeta.Name 324 lastUpdatedImage = deref(repoMeta.LastUpdatedImage, mTypes.LastUpdatedImage{}) 325 lastUpdatedImageMeta = imageMetaMap[lastUpdatedImage.Digest] 326 lastUpdatedTag = lastUpdatedImage.Tag 327 repoLastUpdatedTimestamp = lastUpdatedImage.LastUpdated 328 repoPlatforms = repoMeta.Platforms 329 repoVendors = repoMeta.Vendors 330 repoDownloadCount = repoMeta.DownloadCount 331 repoStarCount = repoMeta.StarCount 332 repoIsUserStarred = repoMeta.IsStarred // value specific to the current user 333 repoIsUserBookMarked = repoMeta.IsBookmarked // value specific to the current user 334 repoSize = repoMeta.Size 335 ) 336 337 if repoLastUpdatedTimestamp == nil { 338 repoLastUpdatedTimestamp = &time.Time{} 339 } 340 341 imageSummary, _, err := FullImageMeta2ImageSummary(ctx, GetFullImageMeta(lastUpdatedTag, repoMeta, 342 lastUpdatedImageMeta)) 343 _ = err 344 345 return &gql_generated.RepoSummary{ 346 Name: &repoName, 347 LastUpdated: repoLastUpdatedTimestamp, 348 Size: ref(strconv.FormatInt(repoSize, 10)), 349 Platforms: getGqlPlatforms(repoPlatforms), 350 Vendors: getGqlVendors(repoVendors), 351 NewestImage: imageSummary, 352 DownloadCount: &repoDownloadCount, 353 StarCount: &repoStarCount, 354 IsBookmarked: &repoIsUserBookMarked, 355 IsStarred: &repoIsUserStarred, 356 Rank: ref(repoMeta.Rank), 357 } 358 } 359 360 func getGqlVendors(repoVendors []string) []*string { 361 result := make([]*string, 0, len(repoVendors)) 362 363 for i := range repoVendors { 364 result = append(result, &repoVendors[i]) 365 } 366 367 return result 368 } 369 370 func getGqlPlatforms(repoPlatforms []ispec.Platform) []*gql_generated.Platform { 371 result := make([]*gql_generated.Platform, 0, len(repoPlatforms)) 372 373 for i := range repoPlatforms { 374 result = append(result, &gql_generated.Platform{ 375 Os: ref(repoPlatforms[i].OS), 376 Arch: ref(getArch(repoPlatforms[i].Architecture, repoPlatforms[i].Variant)), 377 }) 378 } 379 380 return result 381 } 382 383 type ( 384 ManifestDigest = string 385 BlobDigest = string 386 ) 387 388 func FullImageMeta2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullImageMeta, 389 ) (*gql_generated.ImageSummary, map[BlobDigest]int64, error) { 390 switch fullImageMeta.MediaType { 391 case ispec.MediaTypeImageManifest: 392 return ImageManifest2ImageSummary(ctx, fullImageMeta) 393 case ispec.MediaTypeImageIndex: 394 return ImageIndex2ImageSummary(ctx, fullImageMeta) 395 default: 396 return nil, nil, zerr.ErrMediaTypeNotSupported 397 } 398 } 399 400 func ImageIndex2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullImageMeta, 401 ) (*gql_generated.ImageSummary, map[BlobDigest]int64, error) { 402 var ( 403 repo = fullImageMeta.Repo 404 tag = fullImageMeta.Tag 405 indexLastUpdated time.Time 406 isSigned = isImageSigned(fullImageMeta.Signatures) 407 indexSize = int64(0) 408 manifestAnnotations *ImageAnnotations 409 manifestSummaries = make([]*gql_generated.ManifestSummary, 0, len(fullImageMeta.Manifests)) 410 indexBlobs = map[string]int64{} 411 412 indexDigestStr = fullImageMeta.Digest.String() 413 indexMediaType = ispec.MediaTypeImageIndex 414 ) 415 416 for _, imageManifest := range fullImageMeta.Manifests { 417 imageManifestSummary, manifestBlobs, err := ImageManifest2ImageSummary(ctx, mTypes.FullImageMeta{ 418 Repo: fullImageMeta.Repo, 419 Tag: fullImageMeta.Tag, 420 MediaType: ispec.MediaTypeImageManifest, 421 Digest: imageManifest.Digest, 422 Size: imageManifest.Size, 423 Manifests: []mTypes.FullManifestMeta{imageManifest}, 424 Referrers: imageManifest.Referrers, 425 Statistics: imageManifest.Statistics, 426 Signatures: imageManifest.Signatures, 427 }) 428 if err != nil { 429 return &gql_generated.ImageSummary{}, map[string]int64{}, err 430 } 431 432 manifestSize := int64(0) 433 434 for digest, size := range manifestBlobs { 435 indexBlobs[digest] = size 436 manifestSize += size 437 } 438 439 if indexLastUpdated.Before(*imageManifestSummary.LastUpdated) { 440 indexLastUpdated = *imageManifestSummary.LastUpdated 441 } 442 443 annotations := GetAnnotations(imageManifest.Manifest.Annotations, imageManifest.Config.Config.Labels) 444 if manifestAnnotations == nil { 445 manifestAnnotations = &annotations 446 } 447 448 indexSize += manifestSize 449 450 manifestSummaries = append(manifestSummaries, imageManifestSummary.Manifests[0]) 451 } 452 453 signaturesInfo := GetSignaturesInfo(isSigned, fullImageMeta.Signatures) 454 455 if manifestAnnotations == nil { 456 manifestAnnotations = &ImageAnnotations{} 457 } 458 459 annotations := GetIndexAnnotations(fullImageMeta.Index.Annotations, manifestAnnotations) 460 461 indexSummary := gql_generated.ImageSummary{ 462 RepoName: &repo, 463 Tag: &tag, 464 Digest: &indexDigestStr, 465 MediaType: &indexMediaType, 466 Manifests: manifestSummaries, 467 LastUpdated: &indexLastUpdated, 468 IsSigned: &isSigned, 469 SignatureInfo: signaturesInfo, 470 Size: ref(strconv.FormatInt(indexSize, 10)), 471 DownloadCount: ref(fullImageMeta.Statistics.DownloadCount), 472 Description: &annotations.Description, 473 Title: &annotations.Title, 474 Documentation: &annotations.Documentation, 475 Licenses: &annotations.Licenses, 476 Labels: &annotations.Labels, 477 Source: &annotations.Source, 478 Vendor: &annotations.Vendor, 479 Authors: &annotations.Authors, 480 Referrers: getReferrers(fullImageMeta.Referrers), 481 } 482 483 return &indexSummary, indexBlobs, nil 484 } 485 486 func ImageManifest2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullImageMeta, 487 ) (*gql_generated.ImageSummary, map[BlobDigest]int64, error) { 488 manifest := fullImageMeta.Manifests[0] 489 490 var ( 491 repoName = fullImageMeta.Repo 492 tag = fullImageMeta.Tag 493 configDigest = manifest.Manifest.Config.Digest.String() 494 configSize = manifest.Manifest.Config.Size 495 manifestDigest = manifest.Digest.String() 496 manifestSize = manifest.Size 497 mediaType = manifest.Manifest.MediaType 498 artifactType = zcommon.GetManifestArtifactType(fullImageMeta.Manifests[0].Manifest) 499 platform = getPlatform(manifest.Config.Platform) 500 imageLastUpdated = zcommon.GetImageLastUpdated(manifest.Config) 501 downloadCount = fullImageMeta.Statistics.DownloadCount 502 isSigned = isImageSigned(fullImageMeta.Signatures) 503 ) 504 505 imageSize, imageBlobsMap := getImageBlobsInfo(manifestDigest, manifestSize, configDigest, configSize, 506 manifest.Manifest.Layers) 507 imageSizeStr := strconv.FormatInt(imageSize, 10) 508 annotations := GetAnnotations(manifest.Manifest.Annotations, manifest.Config.Config.Labels) 509 510 authors := annotations.Authors 511 if authors == "" { 512 authors = manifest.Config.Author 513 } 514 515 historyEntries, err := getAllHistory(manifest.Manifest, manifest.Config) 516 if err != nil { 517 graphql.AddError(ctx, gqlerror.Errorf("error generating history on tag %s in repo %s: "+ 518 "manifest digest: %s, error: %s", tag, repoName, manifest.Digest, err.Error())) 519 } 520 521 signaturesInfo := GetSignaturesInfo(isSigned, fullImageMeta.Signatures) 522 523 manifestSummary := gql_generated.ManifestSummary{ 524 Digest: &manifestDigest, 525 ConfigDigest: &configDigest, 526 LastUpdated: &imageLastUpdated, 527 Size: &imageSizeStr, 528 IsSigned: &isSigned, 529 SignatureInfo: signaturesInfo, 530 Platform: &platform, 531 DownloadCount: &downloadCount, 532 Layers: getLayersSummaries(manifest.Manifest), 533 History: historyEntries, 534 Referrers: getReferrers(fullImageMeta.Referrers), 535 ArtifactType: &artifactType, 536 } 537 538 imageSummary := gql_generated.ImageSummary{ 539 RepoName: &repoName, 540 Tag: &tag, 541 Digest: &manifestDigest, 542 MediaType: &mediaType, 543 Manifests: []*gql_generated.ManifestSummary{&manifestSummary}, 544 LastUpdated: &imageLastUpdated, 545 IsSigned: &isSigned, 546 SignatureInfo: signaturesInfo, 547 Size: &imageSizeStr, 548 DownloadCount: &downloadCount, 549 Description: &annotations.Description, 550 Title: &annotations.Title, 551 Documentation: &annotations.Documentation, 552 Licenses: &annotations.Licenses, 553 Labels: &annotations.Labels, 554 Source: &annotations.Source, 555 Vendor: &annotations.Vendor, 556 Authors: &authors, 557 Referrers: manifestSummary.Referrers, 558 } 559 560 return &imageSummary, imageBlobsMap, nil 561 } 562 563 func isImageSigned(manifestSignatures mTypes.ManifestSignatures) bool { 564 for _, signatures := range manifestSignatures { 565 if len(signatures) > 0 { 566 return true 567 } 568 } 569 570 return false 571 } 572 573 func getPlatform(platform ispec.Platform) gql_generated.Platform { 574 return gql_generated.Platform{ 575 Os: ref(platform.OS), 576 Arch: ref(getArch(platform.Architecture, platform.Variant)), 577 } 578 } 579 580 func getArch(arch string, variant string) string { 581 if variant != "" { 582 arch = arch + "/" + variant 583 } 584 585 return arch 586 } 587 588 func ref[T any](val T) *T { 589 ref := val 590 591 return &ref 592 } 593 594 func deref[T any](pointer *T, defaultVal T) T { 595 if pointer != nil { 596 return *pointer 597 } 598 599 return defaultVal 600 } 601 602 func PaginatedFullImageMeta2ImageSummaries(ctx context.Context, imageMetaList []mTypes.FullImageMeta, skip SkipQGLField, 603 cveInfo cveinfo.CveInfo, filter mTypes.Filter, pageInput pagination.PageInput, 604 ) ([]*gql_generated.ImageSummary, zcommon.PageInfo, error) { 605 imagePageFinder, err := pagination.NewImgSumPageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy) 606 if err != nil { 607 return []*gql_generated.ImageSummary{}, zcommon.PageInfo{}, err 608 } 609 610 for _, imageMeta := range imageMetaList { 611 imageSummary, _, err := FullImageMeta2ImageSummary(ctx, imageMeta) 612 if err != nil { 613 continue 614 } 615 616 if ImgSumAcceptedByFilter(imageSummary, filter) { 617 imagePageFinder.Add(imageSummary) 618 } 619 } 620 621 page, pageInfo := imagePageFinder.Page() 622 623 for _, imageSummary := range page { 624 // CVE scanning is expensive, only scan for this page 625 updateImageSummaryVulnerabilities(ctx, imageSummary, skip, cveInfo) 626 } 627 628 return page, pageInfo, nil 629 }