Files
zot/pkg/extensions/search/convert/repodb.go
T
Andrei Aaron faa410a0c3 feat(cli): Fix multiple issues with zli output (#1612)
https://github.com/project-zot/zot/issues/1591
    - I will rename "IMAGE NAME" to "REPOSITORY" in order to make the header easier to parse
    - The order of the images cannot be predicted if zot is getting them 1 by 1 using the REST API for manifests, so they cannot be sorted when printed. We could wait on all calls to return but that may take minutes, and printing partial results as they become available is better.
    - The order of the images can be predicted when relying on the zot specific search API, but that is not available in all zot servers depending on build options. I added sorting ascending by default. We are planning to implement configurable sorting in a separate PR - see the work under https://github.com/project-zot/zot/pull/1577
    - With regards to the column widths/alignments that was discussed before, and the issue is we don't know the values beforehand for the REST API based responses. As mentioned above printing partial results as they become available is better.
    - The column widths/alignments are partially fixed in this PR for the search API, but we should properly fix this in - see https://github.com/project-zot/zot/pull/851

https://github.com/project-zot/zot/issues/1592
    - Fix missing space after help message

https://github.com/project-zot/zot/issues/1598
    - Fix table headers showing for json/yaml format
    - Fix spacing shown with json format, use 1 row per shown entry in order to be compatible with json lines format: https://jsonlines.org/
    - Add document header `---` to every image shown in yaml format to separate the entries

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-07-12 10:21:12 -07:00

814 lines
26 KiB
Go

package convert
import (
"context"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/99designs/gqlgen/graphql"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/vektah/gqlparser/v2/gqlerror"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/common"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/extensions/search/gql_generated"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
)
type SkipQGLField struct {
Vulnerabilities bool
}
func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata,
manifestMetaMap map[string]repodb.ManifestMetadata, indexDataMap map[string]repodb.IndexData,
skip SkipQGLField, cveInfo cveinfo.CveInfo,
) *gql_generated.RepoSummary {
var (
repoName = repoMeta.Name
repoLastUpdatedTimestamp = time.Time{}
repoPlatformsSet = map[string]*gql_generated.Platform{}
repoVendorsSet = map[string]bool{}
lastUpdatedImageSummary *gql_generated.ImageSummary
repoDownloadCount = 0
repoStarCount = repoMeta.Stars // total number of stars
repoIsUserStarred = repoMeta.IsStarred // value specific to the current user
repoIsUserBookMarked = repoMeta.IsBookmarked // value specific to the current user
// map used to keep track of all blobs of a repo without dublicates as
// some images may have the same layers
repoBlob2Size = make(map[string]int64, 10)
// made up of all manifests, configs and image layers
size = int64(0)
)
for tag, descriptor := range repoMeta.Tags {
imageSummary, imageBlobsMap, err := Descriptor2ImageSummary(ctx, descriptor, repoMeta.Name, tag, true, repoMeta,
manifestMetaMap, indexDataMap, cveInfo)
if err != nil {
continue
}
for blobDigest, blobSize := range imageBlobsMap {
repoBlob2Size[blobDigest] = blobSize
}
for _, manifestSummary := range imageSummary.Manifests {
if *manifestSummary.Platform.Os != "" || *manifestSummary.Platform.Arch != "" {
opSys, arch := *manifestSummary.Platform.Os, *manifestSummary.Platform.Arch
platformString := strings.TrimSpace(fmt.Sprintf("%s %s", opSys, arch))
repoPlatformsSet[platformString] = &gql_generated.Platform{Os: &opSys, Arch: &arch}
}
repoDownloadCount += manifestMetaMap[*manifestSummary.Digest].DownloadCount
}
if *imageSummary.Vendor != "" {
repoVendorsSet[*imageSummary.Vendor] = true
}
lastUpdatedImageSummary = UpdateLastUpdatedTimestamp(&repoLastUpdatedTimestamp, lastUpdatedImageSummary, imageSummary)
repoDownloadCount += repoMeta.Statistics[descriptor.Digest].DownloadCount
}
// calculate repo size = sum all manifest, config and layer blobs sizes
for _, blobSize := range repoBlob2Size {
size += blobSize
}
repoSize := strconv.FormatInt(size, 10)
repoPlatforms := make([]*gql_generated.Platform, 0, len(repoPlatformsSet))
for _, platform := range repoPlatformsSet {
repoPlatforms = append(repoPlatforms, platform)
}
repoVendors := make([]*string, 0, len(repoVendorsSet))
for vendor := range repoVendorsSet {
vendor := vendor
repoVendors = append(repoVendors, &vendor)
}
// We only scan the latest image on the repo for performance reasons
// Check if vulnerability scanning is disabled
if cveInfo != nil && lastUpdatedImageSummary != nil && !skip.Vulnerabilities {
imageCveSummary, err := cveInfo.GetCVESummaryForImageMedia(repoMeta.Name, *lastUpdatedImageSummary.Digest,
*lastUpdatedImageSummary.MediaType)
if err != nil {
// Log the error, but we should still include the image in results
graphql.AddError(
ctx,
gqlerror.Errorf(
"unable to run vulnerability scan on tag %s in repo %s: error: %s",
*lastUpdatedImageSummary.Tag, repoMeta.Name, err.Error(),
),
)
}
lastUpdatedImageSummary.Vulnerabilities = &gql_generated.ImageVulnerabilitySummary{
MaxSeverity: &imageCveSummary.MaxSeverity,
Count: &imageCveSummary.Count,
}
}
return &gql_generated.RepoSummary{
Name: &repoName,
LastUpdated: &repoLastUpdatedTimestamp,
Size: &repoSize,
Platforms: repoPlatforms,
Vendors: repoVendors,
NewestImage: lastUpdatedImageSummary,
DownloadCount: &repoDownloadCount,
StarCount: &repoStarCount,
IsBookmarked: &repoIsUserBookMarked,
IsStarred: &repoIsUserStarred,
}
}
func UpdateLastUpdatedTimestamp(repoLastUpdatedTimestamp *time.Time,
lastUpdatedImageSummary *gql_generated.ImageSummary, imageSummary *gql_generated.ImageSummary,
) *gql_generated.ImageSummary {
newLastUpdatedImageSummary := lastUpdatedImageSummary
if repoLastUpdatedTimestamp.Equal(time.Time{}) {
// initialize with first time value
*repoLastUpdatedTimestamp = *imageSummary.LastUpdated
newLastUpdatedImageSummary = imageSummary
} else if repoLastUpdatedTimestamp.Before(*imageSummary.LastUpdated) {
*repoLastUpdatedTimestamp = *imageSummary.LastUpdated
newLastUpdatedImageSummary = imageSummary
}
return newLastUpdatedImageSummary
}
func Descriptor2ImageSummary(ctx context.Context, descriptor repodb.Descriptor, repo, tag string, skipCVE bool,
repoMeta repodb.RepoMetadata, manifestMetaMap map[string]repodb.ManifestMetadata,
indexDataMap map[string]repodb.IndexData, cveInfo cveinfo.CveInfo,
) (*gql_generated.ImageSummary, map[string]int64, error) {
switch descriptor.MediaType {
case ispec.MediaTypeImageManifest:
return ImageManifest2ImageSummary(ctx, repo, tag, godigest.Digest(descriptor.Digest), skipCVE,
repoMeta, manifestMetaMap[descriptor.Digest], cveInfo)
case ispec.MediaTypeImageIndex:
return ImageIndex2ImageSummary(ctx, repo, tag, godigest.Digest(descriptor.Digest), skipCVE,
repoMeta, indexDataMap[descriptor.Digest], manifestMetaMap, cveInfo)
default:
return &gql_generated.ImageSummary{}, map[string]int64{}, zerr.ErrMediaTypeNotSupported
}
}
func ImageIndex2ImageSummary(ctx context.Context, repo, tag string, indexDigest godigest.Digest, skipCVE bool,
repoMeta repodb.RepoMetadata, indexData repodb.IndexData, manifestMetaMap map[string]repodb.ManifestMetadata,
cveInfo cveinfo.CveInfo,
) (*gql_generated.ImageSummary, map[string]int64, error) {
var indexContent ispec.Index
err := json.Unmarshal(indexData.IndexBlob, &indexContent)
if err != nil {
return &gql_generated.ImageSummary{}, map[string]int64{}, err
}
var (
indexLastUpdated time.Time
isSigned bool
totalIndexSize int64
indexSize string
totalDownloadCount int
maxSeverity string
manifestSummaries = make([]*gql_generated.ManifestSummary, 0, len(indexContent.Manifests))
indexBlobs = make(map[string]int64, 0)
indexDigestStr = indexDigest.String()
indexMediaType = ispec.MediaTypeImageIndex
)
for _, descriptor := range indexContent.Manifests {
manifestSummary, manifestBlobs, err := ImageManifest2ManifestSummary(ctx, repo, tag, descriptor, false,
repoMeta, manifestMetaMap[descriptor.Digest.String()], repoMeta.Referrers[descriptor.Digest.String()], cveInfo)
if err != nil {
return &gql_generated.ImageSummary{}, map[string]int64{}, err
}
manifestSize := int64(0)
for digest, size := range manifestBlobs {
indexBlobs[digest] = size
manifestSize += size
}
if indexLastUpdated.Before(*manifestSummary.LastUpdated) {
indexLastUpdated = *manifestSummary.LastUpdated
}
totalIndexSize += manifestSize
if cvemodel.SeverityValue(*manifestSummary.Vulnerabilities.MaxSeverity) >
cvemodel.SeverityValue(maxSeverity) {
maxSeverity = *manifestSummary.Vulnerabilities.MaxSeverity
}
manifestSummaries = append(manifestSummaries, manifestSummary)
}
for _, signatures := range repoMeta.Signatures[indexDigest.String()] {
if len(signatures) > 0 {
isSigned = true
}
}
imageCveSummary := cvemodel.ImageCVESummary{}
if cveInfo != nil && !skipCVE {
imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, indexDigestStr, ispec.MediaTypeImageIndex)
if err != nil {
// Log the error, but we should still include the manifest in results
graphql.AddError(ctx, gqlerror.Errorf("unable to run vulnerability scan on tag %s in repo %s: "+
"manifest digest: %s, error: %s", tag, repo, indexDigest, err.Error()))
}
}
indexSize = strconv.FormatInt(totalIndexSize, 10)
annotations := GetAnnotations(indexContent.Annotations, map[string]string{})
signaturesInfo := GetSignaturesInfo(isSigned, repoMeta, indexDigest)
indexSummary := gql_generated.ImageSummary{
RepoName: &repo,
Tag: &tag,
Digest: &indexDigestStr,
MediaType: &indexMediaType,
Manifests: manifestSummaries,
LastUpdated: &indexLastUpdated,
IsSigned: &isSigned,
SignatureInfo: signaturesInfo,
Size: &indexSize,
DownloadCount: &totalDownloadCount,
Description: &annotations.Description,
Title: &annotations.Title,
Documentation: &annotations.Documentation,
Licenses: &annotations.Licenses,
Labels: &annotations.Labels,
Source: &annotations.Source,
Vendor: &annotations.Vendor,
Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{
MaxSeverity: &imageCveSummary.MaxSeverity,
Count: &imageCveSummary.Count,
},
Referrers: getReferrers(repoMeta.Referrers[indexDigest.String()]),
}
return &indexSummary, indexBlobs, nil
}
func ImageManifest2ImageSummary(ctx context.Context, repo, tag string, digest godigest.Digest, skipCVE bool,
repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata, cveInfo cveinfo.CveInfo,
) (*gql_generated.ImageSummary, map[string]int64, error) {
var (
manifestContent ispec.Manifest
manifestDigest = digest.String()
mediaType = ispec.MediaTypeImageManifest
)
err := json.Unmarshal(manifestMeta.ManifestBlob, &manifestContent)
if err != nil {
graphql.AddError(ctx, gqlerror.Errorf("can't unmarshal manifest blob for image: %s:%s, manifest digest: %s, "+
"error: %s", repo, tag, manifestDigest, err.Error()))
return &gql_generated.ImageSummary{}, map[string]int64{}, err
}
var configContent ispec.Image
err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent)
if err != nil {
graphql.AddError(ctx, gqlerror.Errorf("can't unmarshal config blob for image: %s:%s, manifest digest: %s, error: %s",
repo, tag, manifestDigest, err.Error()))
return &gql_generated.ImageSummary{}, map[string]int64{}, err
}
var (
repoName = repo
configDigest = manifestContent.Config.Digest.String()
configSize = manifestContent.Config.Size
artifactType = common.GetManifestArtifactType(manifestContent)
imageLastUpdated = common.GetImageLastUpdated(configContent)
downloadCount = repoMeta.Statistics[digest.String()].DownloadCount
isSigned = false
)
opSys := configContent.OS
arch := configContent.Architecture
variant := configContent.Variant
if variant != "" {
arch = arch + "/" + variant
}
platform := gql_generated.Platform{Os: &opSys, Arch: &arch}
for _, signatures := range repoMeta.Signatures[digest.String()] {
if len(signatures) > 0 {
isSigned = true
}
}
size, imageBlobsMap := getImageBlobsInfo(
manifestDigest, int64(len(manifestMeta.ManifestBlob)),
configDigest, configSize,
manifestContent.Layers)
imageSize := strconv.FormatInt(size, 10)
annotations := GetAnnotations(manifestContent.Annotations, configContent.Config.Labels)
authors := annotations.Authors
if authors == "" {
authors = configContent.Author
}
historyEntries, err := getAllHistory(manifestContent, configContent)
if err != nil {
graphql.AddError(ctx, gqlerror.Errorf("error generating history on tag %s in repo %s: "+
"manifest digest: %s, error: %s", tag, repo, manifestDigest, err.Error()))
}
imageCveSummary := cvemodel.ImageCVESummary{}
if cveInfo != nil && !skipCVE {
imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, manifestDigest, ispec.MediaTypeImageManifest)
if err != nil {
// Log the error, but we should still include the manifest in results
graphql.AddError(ctx, gqlerror.Errorf("unable to run vulnerability scan on tag %s in repo %s: "+
"manifest digest: %s, error: %s", tag, repo, manifestDigest, err.Error()))
}
}
signaturesInfo := GetSignaturesInfo(isSigned, repoMeta, digest)
imageSummary := gql_generated.ImageSummary{
RepoName: &repoName,
Tag: &tag,
Digest: &manifestDigest,
MediaType: &mediaType,
Manifests: []*gql_generated.ManifestSummary{
{
Digest: &manifestDigest,
ConfigDigest: &configDigest,
LastUpdated: &imageLastUpdated,
Size: &imageSize,
IsSigned: &isSigned,
SignatureInfo: signaturesInfo,
Platform: &platform,
DownloadCount: &downloadCount,
Layers: getLayersSummaries(manifestContent),
History: historyEntries,
Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{
MaxSeverity: &imageCveSummary.MaxSeverity,
Count: &imageCveSummary.Count,
},
Referrers: getReferrers(repoMeta.Referrers[manifestDigest]),
ArtifactType: &artifactType,
},
},
LastUpdated: &imageLastUpdated,
IsSigned: &isSigned,
SignatureInfo: signaturesInfo,
Size: &imageSize,
DownloadCount: &downloadCount,
Description: &annotations.Description,
Title: &annotations.Title,
Documentation: &annotations.Documentation,
Licenses: &annotations.Licenses,
Labels: &annotations.Labels,
Source: &annotations.Source,
Vendor: &annotations.Vendor,
Authors: &authors,
Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{
MaxSeverity: &imageCveSummary.MaxSeverity,
Count: &imageCveSummary.Count,
},
Referrers: getReferrers(repoMeta.Referrers[manifestDigest]),
}
return &imageSummary, imageBlobsMap, nil
}
func getReferrers(referrersInfo []repodb.ReferrerInfo) []*gql_generated.Referrer {
referrers := make([]*gql_generated.Referrer, 0, len(referrersInfo))
for _, referrerInfo := range referrersInfo {
referrerInfo := referrerInfo
referrers = append(referrers, &gql_generated.Referrer{
MediaType: &referrerInfo.MediaType,
ArtifactType: &referrerInfo.ArtifactType,
Size: &referrerInfo.Size,
Digest: &referrerInfo.Digest,
Annotations: getAnnotationsFromMap(referrerInfo.Annotations),
})
}
return referrers
}
func getAnnotationsFromMap(annotationsMap map[string]string) []*gql_generated.Annotation {
annotations := make([]*gql_generated.Annotation, 0, len(annotationsMap))
for key, value := range annotationsMap {
key := key
value := value
annotations = append(annotations, &gql_generated.Annotation{
Key: &key,
Value: &value,
})
}
return annotations
}
func ImageManifest2ManifestSummary(ctx context.Context, repo, tag string, descriptor ispec.Descriptor,
skipCVE bool, repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata, referrersInfo []repodb.ReferrerInfo,
cveInfo cveinfo.CveInfo,
) (*gql_generated.ManifestSummary, map[string]int64, error) {
var (
manifestContent ispec.Manifest
digest = descriptor.Digest
)
err := json.Unmarshal(manifestMeta.ManifestBlob, &manifestContent)
if err != nil {
graphql.AddError(ctx, gqlerror.Errorf("can't unmarshal manifest blob for image: %s:%s, manifest digest: %s, "+
"error: %s", repo, tag, digest, err.Error()))
return &gql_generated.ManifestSummary{}, map[string]int64{}, err
}
var configContent ispec.Image
err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent)
if err != nil {
graphql.AddError(ctx, gqlerror.Errorf("can't unmarshal config blob for image: %s:%s, manifest digest: %s, error: %s",
repo, tag, digest, err.Error()))
return &gql_generated.ManifestSummary{}, map[string]int64{}, err
}
var (
manifestDigestStr = digest.String()
configDigest = manifestContent.Config.Digest.String()
configSize = manifestContent.Config.Size
artifactType = common.GetManifestArtifactType(manifestContent)
imageLastUpdated = common.GetImageLastUpdated(configContent)
downloadCount = manifestMeta.DownloadCount
isSigned = false
)
opSys := configContent.OS
arch := configContent.Architecture
variant := configContent.Variant
if variant != "" {
arch = arch + "/" + variant
}
platform := gql_generated.Platform{Os: &opSys, Arch: &arch}
size, imageBlobsMap := getImageBlobsInfo(
manifestDigestStr, int64(len(manifestMeta.ManifestBlob)),
configDigest, configSize,
manifestContent.Layers)
imageSize := strconv.FormatInt(size, 10)
historyEntries, err := getAllHistory(manifestContent, configContent)
if err != nil {
graphql.AddError(ctx, gqlerror.Errorf("error generating history on tag %s in repo %s: "+
"manifest digest: %s, error: %s", tag, repo, manifestDigestStr, err.Error()))
}
imageCveSummary := cvemodel.ImageCVESummary{}
if cveInfo != nil && !skipCVE {
imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, manifestDigestStr, ispec.MediaTypeImageManifest)
if err != nil {
// Log the error, but we should still include the manifest in results
graphql.AddError(ctx, gqlerror.Errorf("unable to run vulnerability scan on tag %s in repo %s: "+
"manifest digest: %s, error: %s", tag, repo, manifestDigestStr, err.Error()))
}
}
for _, signatures := range repoMeta.Signatures[manifestDigestStr] {
if len(signatures) > 0 {
isSigned = true
}
}
signaturesInfo := GetSignaturesInfo(isSigned, repoMeta, digest)
manifestSummary := gql_generated.ManifestSummary{
Digest: &manifestDigestStr,
ConfigDigest: &configDigest,
LastUpdated: &imageLastUpdated,
Size: &imageSize,
Platform: &platform,
DownloadCount: &downloadCount,
Layers: getLayersSummaries(manifestContent),
History: historyEntries,
IsSigned: &isSigned,
SignatureInfo: signaturesInfo,
Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{
MaxSeverity: &imageCveSummary.MaxSeverity,
Count: &imageCveSummary.Count,
},
Referrers: getReferrers(referrersInfo),
ArtifactType: &artifactType,
}
return &manifestSummary, imageBlobsMap, nil
}
func getImageBlobsInfo(manifestDigest string, manifestSize int64, configDigest string, configSize int64,
layers []ispec.Descriptor,
) (int64, map[string]int64) {
imageBlobsMap := map[string]int64{}
imageSize := int64(0)
// add config size
imageSize += configSize
imageBlobsMap[configDigest] = configSize
// add manifest size
imageSize += manifestSize
imageBlobsMap[manifestDigest] = manifestSize
// add layers size
for _, layer := range layers {
imageBlobsMap[layer.Digest.String()] = layer.Size
imageSize += layer.Size
}
return imageSize, imageBlobsMap
}
func RepoMeta2ImageSummaries(ctx context.Context, repoMeta repodb.RepoMetadata,
manifestMetaMap map[string]repodb.ManifestMetadata, indexDataMap map[string]repodb.IndexData,
skip SkipQGLField, cveInfo cveinfo.CveInfo,
) []*gql_generated.ImageSummary {
imageSummaries := make([]*gql_generated.ImageSummary, 0, len(repoMeta.Tags))
// Make sure the tags are sorted
// We need to implement a proper fix for this taking into account
// the sorting criteria used in the requested page
tags := make([]string, 0, len(repoMeta.Tags))
for tag := range repoMeta.Tags {
tags = append(tags, tag)
}
// Sorting ascending by tag name should do for now
sort.Strings(tags)
for _, tag := range tags {
descriptor := repoMeta.Tags[tag]
imageSummary, _, err := Descriptor2ImageSummary(ctx, descriptor, repoMeta.Name, tag, skip.Vulnerabilities,
repoMeta, manifestMetaMap, indexDataMap, cveInfo)
if err != nil {
continue
}
imageSummaries = append(imageSummaries, imageSummary)
}
return imageSummaries
}
func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta repodb.RepoMetadata,
manifestMetaMap map[string]repodb.ManifestMetadata, indexDataMap map[string]repodb.IndexData,
skip SkipQGLField, cveInfo cveinfo.CveInfo, log log.Logger,
) (*gql_generated.RepoSummary, []*gql_generated.ImageSummary) {
var (
repoName = repoMeta.Name
repoLastUpdatedTimestamp = time.Time{}
repoPlatformsSet = map[string]*gql_generated.Platform{}
repoVendorsSet = map[string]bool{}
lastUpdatedImageSummary *gql_generated.ImageSummary
repoDownloadCount = 0
repoStarCount = repoMeta.Stars // total number of stars
isStarred = repoMeta.IsStarred // value specific to the current user
isBookmarked = repoMeta.IsBookmarked // value specific to the current user
// map used to keep track of all blobs of a repo without dublicates as
// some images may have the same layers
repoBlob2Size = make(map[string]int64, 10)
// made up of all manifests, configs and image layers
size = int64(0)
imageSummaries = make([]*gql_generated.ImageSummary, 0, len(repoMeta.Tags))
)
for tag, descriptor := range repoMeta.Tags {
imageSummary, imageBlobs, err := Descriptor2ImageSummary(ctx, descriptor, repoName, tag,
skip.Vulnerabilities, repoMeta, manifestMetaMap, indexDataMap, cveInfo)
if err != nil {
log.Error().Str("repository", repoName).Str("reference", tag).
Msg("repodb: erorr while converting descriptor for image")
continue
}
for _, manifestSummary := range imageSummary.Manifests {
opSys, arch := *manifestSummary.Platform.Os, *manifestSummary.Platform.Arch
if opSys != "" || arch != "" {
platformString := strings.TrimSpace(fmt.Sprintf("%s %s", opSys, arch))
repoPlatformsSet[platformString] = &gql_generated.Platform{Os: &opSys, Arch: &arch}
}
updateRepoBlobsMap(imageBlobs, repoBlob2Size)
}
if *imageSummary.Vendor != "" {
repoVendorsSet[*imageSummary.Vendor] = true
}
lastUpdatedImageSummary = UpdateLastUpdatedTimestamp(&repoLastUpdatedTimestamp, lastUpdatedImageSummary, imageSummary)
repoDownloadCount += *imageSummary.DownloadCount
imageSummaries = append(imageSummaries, imageSummary)
}
// calculate repo size = sum all manifest, config and layer blobs sizes
for _, blobSize := range repoBlob2Size {
size += blobSize
}
repoSize := strconv.FormatInt(size, 10)
repoPlatforms := make([]*gql_generated.Platform, 0, len(repoPlatformsSet))
for _, platform := range repoPlatformsSet {
repoPlatforms = append(repoPlatforms, platform)
}
repoVendors := make([]*string, 0, len(repoVendorsSet))
for vendor := range repoVendorsSet {
vendor := vendor
repoVendors = append(repoVendors, &vendor)
}
// We only scan the latest image on the repo for performance reasons
// Check if vulnerability scanning is disabled
if cveInfo != nil && lastUpdatedImageSummary != nil && !skip.Vulnerabilities {
imageCveSummary, err := cveInfo.GetCVESummaryForImageMedia(repoMeta.Name, *lastUpdatedImageSummary.Digest,
*lastUpdatedImageSummary.MediaType)
if err != nil {
// Log the error, but we should still include the image in results
graphql.AddError(
ctx,
gqlerror.Errorf(
"unable to run vulnerability scan on tag %s in repo %s: error: %s",
*lastUpdatedImageSummary.Tag, repoMeta.Name, err.Error(),
),
)
}
lastUpdatedImageSummary.Vulnerabilities = &gql_generated.ImageVulnerabilitySummary{
MaxSeverity: &imageCveSummary.MaxSeverity,
Count: &imageCveSummary.Count,
}
}
summary := &gql_generated.RepoSummary{
Name: &repoName,
LastUpdated: &repoLastUpdatedTimestamp,
Size: &repoSize,
Platforms: repoPlatforms,
Vendors: repoVendors,
NewestImage: lastUpdatedImageSummary,
DownloadCount: &repoDownloadCount,
StarCount: &repoStarCount,
IsBookmarked: &isBookmarked,
IsStarred: &isStarred,
}
return summary, imageSummaries
}
func StringMap2Annotations(strMap map[string]string) []*gql_generated.Annotation {
annotations := make([]*gql_generated.Annotation, 0, len(strMap))
for key, value := range strMap {
key := key
value := value
annotations = append(annotations, &gql_generated.Annotation{
Key: &key,
Value: &value,
})
}
return annotations
}
func GetPreloads(ctx context.Context) map[string]bool {
if !graphql.HasOperationContext(ctx) {
return map[string]bool{}
}
nestedPreloads := GetNestedPreloads(
graphql.GetOperationContext(ctx),
graphql.CollectFieldsCtx(ctx, nil),
"",
)
preloads := map[string]bool{}
for _, str := range nestedPreloads {
preloads[str] = true
}
return preloads
}
func GetNestedPreloads(ctx *graphql.OperationContext, fields []graphql.CollectedField, prefix string,
) []string {
preloads := []string{}
for _, column := range fields {
prefixColumn := GetPreloadString(prefix, column.Name)
preloads = append(preloads, prefixColumn)
preloads = append(preloads,
GetNestedPreloads(ctx, graphql.CollectFields(ctx, column.Selections, nil), prefixColumn)...,
)
}
return preloads
}
func GetPreloadString(prefix, name string) string {
if len(prefix) > 0 {
return prefix + "." + name
}
return name
}
func GetSignaturesInfo(isSigned bool, repoMeta repodb.RepoMetadata, indexDigest godigest.Digest,
) []*gql_generated.SignatureSummary {
signaturesInfo := []*gql_generated.SignatureSummary{}
if !isSigned {
return signaturesInfo
}
for sigType, signatures := range repoMeta.Signatures[indexDigest.String()] {
for _, sig := range signatures {
for _, layer := range sig.LayersInfo {
var (
isTrusted bool
author string
tool string
)
if layer.Signer != "" {
author = layer.Signer
if !layer.Date.IsZero() && time.Now().After(layer.Date) {
isTrusted = false
} else {
isTrusted = true
}
} else {
isTrusted = false
author = ""
}
tool = sigType
signaturesInfo = append(signaturesInfo,
&gql_generated.SignatureSummary{Tool: &tool, IsTrusted: &isTrusted, Author: &author})
}
}
}
return signaturesInfo
}