diff --git a/pkg/extensions/search/common/common.go b/pkg/extensions/search/common/common.go index f6018cdf..877b5707 100644 --- a/pkg/extensions/search/common/common.go +++ b/pkg/extensions/search/common/common.go @@ -11,11 +11,14 @@ import ( ) const ( - AnnotationLabels = "org.label-schema.labels" - LabelAnnotationCreated = "org.label-schema.build-date" - LabelAnnotationVendor = "org.label-schema.vendor" - LabelAnnotationDescription = "org.label-schema.description" - LabelAnnotationLicenses = "org.label-schema.license" + // See https://github.com/opencontainers/image-spec/blob/main/annotations.md#back-compatibility-with-label-schema + AnnotationLabels = "org.label-schema.labels" + LabelAnnotationCreated = "org.label-schema.build-date" + LabelAnnotationVendor = "org.label-schema.vendor" + LabelAnnotationDescription = "org.label-schema.description" + LabelAnnotationTitle = "org.label-schema.name" + LabelAnnotationDocumentation = "org.label-schema.usage" + LabelAnnotationSource = "org.label-schema.vcs-url" ) type TagInfo struct { @@ -103,40 +106,49 @@ func GetRoutePrefix(name string) string { return fmt.Sprintf("/%s", names[0]) } -func GetDescription(labels map[string]string) string { - desc, ok := labels[ispec.AnnotationDescription] - if !ok { - desc, ok = labels[LabelAnnotationDescription] - if !ok { - desc = "" - } - } - - return desc +type ImageAnnotations struct { + Description string + Licenses string + Title string + Documentation string + Source string + Labels string + Vendor string } -func GetLicense(labels map[string]string) string { - license, ok := labels[ispec.AnnotationLicenses] - if !ok { - license, ok = labels[LabelAnnotationLicenses] +/* OCI annotation/label with backwards compatibility +arg can be either lables or annotations +https://github.com/opencontainers/image-spec/blob/main/annotations.md.*/ +func GetAnnotationValue(annotations map[string]string, annotationKey, labelKey string) string { + value, ok := annotations[annotationKey] + if !ok || value == "" { + value, ok = annotations[labelKey] if !ok { - license = "" + value = "" } } - return license + return value } -func GetVendor(labels map[string]string) string { - vendor, ok := labels[ispec.AnnotationVendor] - if !ok { - vendor, ok = labels[LabelAnnotationVendor] - if !ok { - vendor = "" - } - } +func GetDescription(annotations map[string]string) string { + return GetAnnotationValue(annotations, ispec.AnnotationDescription, LabelAnnotationDescription) +} - return vendor +func GetVendor(annotations map[string]string) string { + return GetAnnotationValue(annotations, ispec.AnnotationVendor, LabelAnnotationVendor) +} + +func GetTitle(annotations map[string]string) string { + return GetAnnotationValue(annotations, ispec.AnnotationTitle, LabelAnnotationTitle) +} + +func GetDocumentation(annotations map[string]string) string { + return GetAnnotationValue(annotations, ispec.AnnotationDocumentation, LabelAnnotationDocumentation) +} + +func GetSource(annotations map[string]string) string { + return GetAnnotationValue(annotations, ispec.AnnotationSource, LabelAnnotationSource) } func GetCategories(labels map[string]string) string { @@ -144,3 +156,56 @@ func GetCategories(labels map[string]string) string { return categories } + +func GetLicenses(annotations map[string]string) string { + licenses := annotations[ispec.AnnotationLicenses] + + return licenses +} + +func GetAnnotations(annotations, labels map[string]string) ImageAnnotations { + description := GetDescription(annotations) + if description == "" { + description = GetDescription(labels) + } + + title := GetTitle(annotations) + if title == "" { + title = GetTitle(labels) + } + + documentation := GetDocumentation(annotations) + if documentation == "" { + documentation = GetDocumentation(annotations) + } + + source := GetSource(annotations) + if source == "" { + source = GetSource(labels) + } + + licenses := GetLicenses(annotations) + if licenses == "" { + licenses = GetLicenses(labels) + } + + categories := GetCategories(annotations) + if categories == "" { + categories = GetCategories(labels) + } + + vendor := GetVendor(annotations) + if vendor == "" { + vendor = GetVendor(labels) + } + + return ImageAnnotations{ + Description: description, + Title: title, + Documentation: documentation, + Source: source, + Licenses: licenses, + Labels: categories, + Vendor: vendor, + } +} diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index 6792bbe5..4611d19d 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -24,7 +24,6 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/sign" . "github.com/smartystreets/goconvey/convey" "gopkg.in/resty.v1" - zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" @@ -389,9 +388,8 @@ func TestRepoListWithNewestImage(t *testing.T) { "?query={RepoListWithNewestImage{Name%20NewestImage{Tag}}}") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) - errmsg := fmt.Sprint(zerr.ErrBlobNotFound) body := string(resp.Body()) - So(body, ShouldContainSubstring, errmsg) + So(body, ShouldContainSubstring, "can't get last updated manifest for repo:") So(resp.StatusCode(), ShouldEqual, 200) err = CopyFiles("../../../../test/data/zot-test", path.Join(rootDir, "zot-test")) @@ -415,9 +413,8 @@ func TestRepoListWithNewestImage(t *testing.T) { "?query={RepoListWithNewestImage{Name%20NewestImage{Tag}}}") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) - errmsg = fmt.Sprint(zerr.ErrBlobNotFound) body = string(resp.Body()) - So(body, ShouldContainSubstring, errmsg) + So(body, ShouldContainSubstring, "can't get last updated manifest for repo") So(resp.StatusCode(), ShouldEqual, 200) err = CopyFiles("../../../../test/data/zot-test", path.Join(rootDir, "zot-test")) @@ -440,7 +437,7 @@ func TestRepoListWithNewestImage(t *testing.T) { So(resp, ShouldNotBeNil) So(err, ShouldBeNil) body = string(resp.Body()) - So(body, ShouldContainSubstring, "reference not found for this manifest") + So(body, ShouldContainSubstring, "reference not found for manifest") So(resp.StatusCode(), ShouldEqual, 200) }) @@ -890,7 +887,7 @@ func TestUtilsMethod(t *testing.T) { desc := common.GetDescription(labels) So(desc, ShouldEqual, "") - license := common.GetLicense(labels) + license := common.GetLicenses(labels) So(license, ShouldEqual, "") vendor := common.GetVendor(labels) @@ -907,7 +904,7 @@ func TestUtilsMethod(t *testing.T) { desc = common.GetDescription(labels) So(desc, ShouldEqual, "zot-desc") - license = common.GetLicense(labels) + license = common.GetLicenses(labels) So(license, ShouldEqual, "zot-license") vendor = common.GetVendor(labels) @@ -921,12 +918,12 @@ func TestUtilsMethod(t *testing.T) { // Use diff key labels[common.LabelAnnotationVendor] = "zot-vendor" labels[common.LabelAnnotationDescription] = "zot-label-desc" - labels[common.LabelAnnotationLicenses] = "zot-label-license" + labels[ispec.AnnotationLicenses] = "zot-label-license" desc = common.GetDescription(labels) So(desc, ShouldEqual, "zot-label-desc") - license = common.GetLicense(labels) + license = common.GetLicenses(labels) So(license, ShouldEqual, "zot-label-license") vendor = common.GetVendor(labels) diff --git a/pkg/extensions/search/common/oci_layout.go b/pkg/extensions/search/common/oci_layout.go index 4f77d62a..4c330103 100644 --- a/pkg/extensions/search/common/oci_layout.go +++ b/pkg/extensions/search/common/oci_layout.go @@ -28,7 +28,6 @@ type OciLayoutUtils interface { GetImageTagsWithTimestamp(repo string) ([]TagInfo, error) GetImageLastUpdated(imageInfo ispec.Image) time.Time GetImagePlatform(imageInfo ispec.Image) (string, string) - GetImageVendor(imageInfo ispec.Image) string GetImageManifestSize(repo string, manifestDigest godigest.Digest) int64 GetRepoLastUpdated(repo string) (TagInfo, error) GetExpandedRepoInfo(name string) (RepoInfo, error) @@ -55,12 +54,33 @@ type Image struct { } type RepoSummary struct { - Name string `json:"name"` - LastUpdated time.Time `json:"lastUpdated"` - Size string `json:"size"` - Platforms []OsArch `json:"platforms"` - Vendors []string `json:"vendors"` - Score int `json:"score"` + Name string `json:"name"` + LastUpdated time.Time `json:"lastUpdated"` + Size string `json:"size"` + Platforms []OsArch `json:"platforms"` + Vendors []string `json:"vendors"` + Score int `json:"score"` + NewestImage ImageSummary `json:"newestImage"` +} + +type ImageSummary struct { + RepoName string `json:"repoName"` + Tag string `json:"tag"` + Digest string `json:"digest"` + ConfigDigest string `json:"configDigest"` + LastUpdated time.Time `json:"lastUpdated"` + IsSigned bool `json:"isSigned"` + Size string `json:"size"` + Platform OsArch `json:"platform"` + Vendor string `json:"vendor"` + Score int `json:"score"` + DownloadCount int `json:"downloadCount"` + Description string `json:"description"` + Licenses string `json:"licenses"` + Labels string `json:"labels"` + Title string `json:"title"` + Source string `json:"source"` + Documentation string `json:"documentation"` } type OsArch struct { @@ -311,10 +331,6 @@ func (olu BaseOciLayoutUtils) GetImageConfigInfo(repo string, manifestDigest god return imageInfo, nil } -func (olu BaseOciLayoutUtils) GetImageVendor(imageConfig ispec.Image) string { - return imageConfig.Config.Labels["vendor"] -} - func (olu BaseOciLayoutUtils) GetImageManifestSize(repo string, manifestDigest godigest.Digest) int64 { imageStore := olu.StoreController.GetImageStore(repo) @@ -360,11 +376,6 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) manifests := make([]Image, 0) - tagsInfo, err := olu.GetImageTagsWithTimestamp(name) - if err != nil { - olu.Log.Error().Err(err).Msgf("can't get tags info for repo: %s", name) - } - manifestList, err := olu.GetImageManifests(name) if err != nil { olu.Log.Error().Err(err).Msg("error getting image manifests") @@ -372,10 +383,21 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) return RepoInfo{}, err } - repoPlatforms := make([]OsArch, 0, len(tagsInfo)) + lastUpdatedTag, err := olu.GetRepoLastUpdated(name) + if err != nil { + olu.Log.Error().Err(err).Msgf("can't get last updated manifest for repo: %s", name) + + return RepoInfo{}, err + } + + repoPlatforms := make([]OsArch, 0) repoVendors := make([]string, 0, len(manifestList)) + var lastUpdatedImageSummary ImageSummary + for _, man := range manifestList { + imageLayersSize := int64(0) + manifestInfo := Image{} manifestInfo.Digest = man.Digest.Encoded() @@ -398,7 +420,8 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) return RepoInfo{}, err } - manifestInfo.IsSigned = olu.CheckManifestSignature(name, man.Digest) + isSigned := olu.CheckManifestSignature(name, man.Digest) + manifestInfo.IsSigned = isSigned manifestSize := olu.GetImageManifestSize(name, man.Digest) olu.Log.Debug().Msg(fmt.Sprintf("%v", man.Digest)) @@ -414,7 +437,6 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) continue } - vendor := olu.GetImageVendor(imageConfigInfo) os, arch := olu.GetImagePlatform(imageConfigInfo) osArch := OsArch{ Os: os, @@ -422,7 +444,6 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) } repoPlatforms = append(repoPlatforms, osArch) - repoVendors = append(repoVendors, vendor) layers := make([]Layer, 0) @@ -435,21 +456,54 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) layerInfo.Size = strconv.FormatInt(layer.Size, 10) + imageLayersSize += layer.Size + layers = append(layers, layerInfo) } + imageSize := imageLayersSize + manifestSize + configSize + manifestInfo.Layers = layers manifests = append(manifests, manifestInfo) + + // get image info from manifest annotation, if not found get from image config labels. + annotations := GetAnnotations(manifest.Annotations, imageConfigInfo.Config.Labels) + + repoVendors = append(repoVendors, annotations.Vendor) + + size := strconv.Itoa(int(imageSize)) + manifestDigest := man.Digest.Hex() + configDigest := manifest.Config.Digest.Hex + lastUpdated := olu.GetImageLastUpdated(imageConfigInfo) + score := 0 + + imageSummary := ImageSummary{ + RepoName: name, + Tag: tag, + LastUpdated: lastUpdated, + Digest: manifestDigest, + ConfigDigest: configDigest, + IsSigned: isSigned, + Size: size, + Platform: osArch, + Vendor: annotations.Vendor, + Score: score, + Description: annotations.Description, + Title: annotations.Title, + Documentation: annotations.Documentation, + Licenses: annotations.Licenses, + Labels: annotations.Labels, + Source: annotations.Source, + } + + if man.Digest.String() == lastUpdatedTag.Digest { + lastUpdatedImageSummary = imageSummary + } } repo.Images = manifests - lastUpdate, err := olu.GetRepoLastUpdated(name) - if err != nil { - olu.Log.Error().Err(err).Msgf("can't find latest update timestamp for repo: %s", name) - } - for blob := range repoBlob2Size { repoSize += repoBlob2Size[blob] } @@ -458,9 +512,10 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) summary := RepoSummary{ Name: name, - LastUpdated: lastUpdate.Timestamp, + LastUpdated: lastUpdatedTag.Timestamp, Size: size, Platforms: repoPlatforms, + NewestImage: lastUpdatedImageSummary, Vendors: repoVendors, Score: -1, } diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 59fa49df..98711f76 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -66,6 +66,7 @@ type ComplexityRoot struct { ConfigDigest func(childComplexity int) int Description func(childComplexity int) int Digest func(childComplexity int) int + Documentation func(childComplexity int) int DownloadCount func(childComplexity int) int IsSigned func(childComplexity int) int Labels func(childComplexity int) int @@ -76,7 +77,9 @@ type ComplexityRoot struct { RepoName func(childComplexity int) int Score func(childComplexity int) int Size func(childComplexity int) int + Source func(childComplexity int) int Tag func(childComplexity int) int + Title func(childComplexity int) int Vendor func(childComplexity int) int } @@ -244,6 +247,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.Digest(childComplexity), true + case "ImageSummary.Documentation": + if e.complexity.ImageSummary.Documentation == nil { + break + } + + return e.complexity.ImageSummary.Documentation(childComplexity), true + case "ImageSummary.DownloadCount": if e.complexity.ImageSummary.DownloadCount == nil { break @@ -314,6 +324,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.Size(childComplexity), true + case "ImageSummary.Source": + if e.complexity.ImageSummary.Source == nil { + break + } + + return e.complexity.ImageSummary.Source(childComplexity), true + case "ImageSummary.Tag": if e.complexity.ImageSummary.Tag == nil { break @@ -321,6 +338,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.Tag(childComplexity), true + case "ImageSummary.Title": + if e.complexity.ImageSummary.Title == nil { + break + } + + return e.complexity.ImageSummary.Title(childComplexity), true + case "ImageSummary.Vendor": if e.complexity.ImageSummary.Vendor == nil { break @@ -663,6 +687,9 @@ type ImageSummary { Description: String Licenses: String Labels: String + Title: String + Source: String + Documentation: String } # Brief on a specific repo to be used in queries returning a list of repos @@ -1250,6 +1277,12 @@ func (ec *executionContext) fieldContext_GlobalSearchResult_Images(ctx context.C return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": return ec.fieldContext_ImageSummary_Labels(ctx, field) + case "Title": + return ec.fieldContext_ImageSummary_Title(ctx, field) + case "Source": + return ec.fieldContext_ImageSummary_Source(ctx, field) + case "Documentation": + return ec.fieldContext_ImageSummary_Documentation(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -1998,6 +2031,129 @@ func (ec *executionContext) fieldContext_ImageSummary_Labels(ctx context.Context return fc, nil } +func (ec *executionContext) _ImageSummary_Title(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_Title(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Title, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageSummary_Title(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageSummary_Source(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_Source(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Source, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageSummary_Source(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageSummary_Documentation(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_Documentation(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Documentation, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageSummary_Documentation(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _LayerSummary_Size(ctx context.Context, field graphql.CollectedField, obj *LayerSummary) (ret graphql.Marshaler) { fc, err := ec.fieldContext_LayerSummary_Size(ctx, field) if err != nil { @@ -2453,6 +2609,12 @@ func (ec *executionContext) fieldContext_Query_ImageListForCVE(ctx context.Conte return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": return ec.fieldContext_ImageSummary_Labels(ctx, field) + case "Title": + return ec.fieldContext_ImageSummary_Title(ctx, field) + case "Source": + return ec.fieldContext_ImageSummary_Source(ctx, field) + case "Documentation": + return ec.fieldContext_ImageSummary_Documentation(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -2537,6 +2699,12 @@ func (ec *executionContext) fieldContext_Query_ImageListWithCVEFixed(ctx context return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": return ec.fieldContext_ImageSummary_Labels(ctx, field) + case "Title": + return ec.fieldContext_ImageSummary_Title(ctx, field) + case "Source": + return ec.fieldContext_ImageSummary_Source(ctx, field) + case "Documentation": + return ec.fieldContext_ImageSummary_Documentation(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -2621,6 +2789,12 @@ func (ec *executionContext) fieldContext_Query_ImageListForDigest(ctx context.Co return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": return ec.fieldContext_ImageSummary_Labels(ctx, field) + case "Title": + return ec.fieldContext_ImageSummary_Title(ctx, field) + case "Source": + return ec.fieldContext_ImageSummary_Source(ctx, field) + case "Documentation": + return ec.fieldContext_ImageSummary_Documentation(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -2771,6 +2945,12 @@ func (ec *executionContext) fieldContext_Query_ImageList(ctx context.Context, fi return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": return ec.fieldContext_ImageSummary_Labels(ctx, field) + case "Title": + return ec.fieldContext_ImageSummary_Title(ctx, field) + case "Source": + return ec.fieldContext_ImageSummary_Source(ctx, field) + case "Documentation": + return ec.fieldContext_ImageSummary_Documentation(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -3108,6 +3288,12 @@ func (ec *executionContext) fieldContext_RepoInfo_Images(ctx context.Context, fi return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": return ec.fieldContext_ImageSummary_Labels(ctx, field) + case "Title": + return ec.fieldContext_ImageSummary_Title(ctx, field) + case "Source": + return ec.fieldContext_ImageSummary_Source(ctx, field) + case "Documentation": + return ec.fieldContext_ImageSummary_Documentation(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -3496,6 +3682,12 @@ func (ec *executionContext) fieldContext_RepoSummary_NewestImage(ctx context.Con return ec.fieldContext_ImageSummary_Licenses(ctx, field) case "Labels": return ec.fieldContext_ImageSummary_Labels(ctx, field) + case "Title": + return ec.fieldContext_ImageSummary_Title(ctx, field) + case "Source": + return ec.fieldContext_ImageSummary_Source(ctx, field) + case "Documentation": + return ec.fieldContext_ImageSummary_Documentation(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -5580,6 +5772,18 @@ func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.Selection out.Values[i] = ec._ImageSummary_Labels(ctx, field, obj) + case "Title": + + out.Values[i] = ec._ImageSummary_Title(ctx, field, obj) + + case "Source": + + out.Values[i] = ec._ImageSummary_Source(ctx, field, obj) + + case "Documentation": + + out.Values[i] = ec._ImageSummary_Documentation(ctx, field, obj) + default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index 0c09c3a2..a17e79bb 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -41,6 +41,9 @@ type ImageSummary struct { Description *string `json:"Description"` Licenses *string `json:"Licenses"` Labels *string `json:"Labels"` + Title *string `json:"Title"` + Source *string `json:"Source"` + Documentation *string `json:"Documentation"` } type LayerSummary struct { diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index b5df3f1b..8aee953d 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -7,6 +7,7 @@ package search import ( "context" "errors" + "fmt" "sort" "strconv" "strings" @@ -116,36 +117,39 @@ func (r *queryResolver) getImageListForDigest(repoList []string, digest string) return imgResultForDigest, errResult } -// nolint:lll -func (r *queryResolver) repoListWithNewestImage(ctx context.Context, store storage.ImageStore) ([]*gql_generated.RepoSummary, error) { - repos := []*gql_generated.RepoSummary{} - olu := common.NewBaseOciLayoutUtils(r.storeController, r.log) +func repoListWithNewestImage( + ctx context.Context, + repoList []string, + olu common.OciLayoutUtils, + log log.Logger, +) ([]*gql_generated.RepoSummary, error) { + reposSummary := []*gql_generated.RepoSummary{} - repoNames, err := store.GetRepositories() - if err != nil { - return nil, err - } - - for _, repo := range repoNames { + for _, repo := range repoList { lastUpdatedTag, err := olu.GetRepoLastUpdated(repo) if err != nil { - graphql.AddError(ctx, err) + msg := fmt.Sprintf("can't get last updated manifest for repo: %s", repo) + log.Error().Err(err).Msg(msg) + + graphql.AddError(ctx, gqlerror.Errorf(msg)) continue } repoSize := int64(0) repoBlob2Size := make(map[string]int64, 10) - tagsInfo, _ := olu.GetImageTagsWithTimestamp(repo) manifests, err := olu.GetImageManifests(repo) if err != nil { - graphql.AddError(ctx, err) + msg := fmt.Sprintf("can't get manifests for repo: %s", repo) + + log.Error().Err(err).Msg(msg) + graphql.AddError(ctx, gqlerror.Errorf(msg)) continue } - repoPlatforms := make([]*gql_generated.OsArch, 0, len(tagsInfo)) + repoPlatforms := make([]*gql_generated.OsArch, 0) repoVendors := make([]*string, 0, len(manifests)) repoName := repo @@ -153,14 +157,24 @@ func (r *queryResolver) repoListWithNewestImage(ctx context.Context, store stora var brokenManifest bool - for i, manifest := range manifests { + for _, manifest := range manifests { imageLayersSize := int64(0) - manifestSize := olu.GetImageManifestSize(repo, manifests[i].Digest) + manifestSize := olu.GetImageManifestSize(repo, manifest.Digest) - imageBlobManifest, _ := olu.GetImageBlobManifest(repo, manifests[i].Digest) + imageBlobManifest, err := olu.GetImageBlobManifest(repo, manifest.Digest) + if err != nil { + msg := fmt.Sprintf("reference not found for manifest %s", manifest.Digest) + + log.Error().Err(err).Msg(msg) + graphql.AddError(ctx, gqlerror.Errorf(msg)) + + brokenManifest = true + + continue + } configSize := imageBlobManifest.Config.Size - repoBlob2Size[manifests[i].Digest.String()] = manifestSize + repoBlob2Size[manifest.Digest.String()] = manifestSize repoBlob2Size[imageBlobManifest.Config.Digest.Hex] = configSize for _, layer := range imageBlobManifest.Layers { @@ -170,7 +184,17 @@ func (r *queryResolver) repoListWithNewestImage(ctx context.Context, store stora imageSize := imageLayersSize + manifestSize + configSize - imageConfigInfo, _ := olu.GetImageConfigInfo(repo, manifests[i].Digest) + imageConfigInfo, err := olu.GetImageConfigInfo(repo, manifest.Digest) + if err != nil { + msg := fmt.Sprintf("can't get image config for manifest %s", manifest.Digest) + + log.Error().Err(err).Msg(msg) + graphql.AddError(ctx, gqlerror.Errorf(msg)) + + brokenManifest = true + + continue + } os, arch := olu.GetImagePlatform(imageConfigInfo) osArch := &gql_generated.OsArch{ @@ -179,12 +203,18 @@ func (r *queryResolver) repoListWithNewestImage(ctx context.Context, store stora } repoPlatforms = append(repoPlatforms, osArch) - vendor := olu.GetImageVendor(imageConfigInfo) - repoVendors = append(repoVendors, &vendor) + // get image info from manifest annotation, if not found get from image config labels. + annotations := common.GetAnnotations(imageBlobManifest.Annotations, imageConfigInfo.Config.Labels) + + repoVendors = append(repoVendors, &annotations.Vendor) manifestTag, ok := manifest.Annotations[ispec.AnnotationRefName] if !ok { - graphql.AddError(ctx, gqlerror.Errorf("reference not found for this manifest")) + msg := fmt.Sprintf("reference not found for manifest %s", manifest.Digest.String()) + + log.Error().Msg(msg) + graphql.AddError(ctx, gqlerror.Errorf(msg)) + brokenManifest = true break @@ -192,22 +222,32 @@ func (r *queryResolver) repoListWithNewestImage(ctx context.Context, store stora tag := manifestTag size := strconv.Itoa(int(imageSize)) - isSigned := olu.CheckManifestSignature(repo, manifests[i].Digest) + manifestDigest := manifest.Digest.Hex() + configDigest := imageBlobManifest.Config.Digest.Hex + isSigned := olu.CheckManifestSignature(repo, manifest.Digest) lastUpdated := olu.GetImageLastUpdated(imageConfigInfo) score := 0 imageSummary := gql_generated.ImageSummary{ - RepoName: &repoName, - Tag: &tag, - LastUpdated: &lastUpdated, - IsSigned: &isSigned, - Size: &size, - Platform: osArch, - Vendor: &vendor, - Score: &score, + RepoName: &repoName, + Tag: &tag, + LastUpdated: &lastUpdated, + Digest: &manifestDigest, + ConfigDigest: &configDigest, + IsSigned: &isSigned, + Size: &size, + Platform: osArch, + Vendor: &annotations.Vendor, + Score: &score, + Description: &annotations.Description, + Title: &annotations.Title, + Documentation: &annotations.Documentation, + Licenses: &annotations.Licenses, + Labels: &annotations.Labels, + Source: &annotations.Source, } - if tagsInfo[i].Digest == lastUpdatedTag.Digest { + if manifest.Digest.String() == lastUpdatedTag.Digest { lastUpdatedImageSummary = imageSummary } } @@ -223,7 +263,7 @@ func (r *queryResolver) repoListWithNewestImage(ctx context.Context, store stora repoSizeStr := strconv.FormatInt(repoSize, 10) index := 0 - repos = append(repos, &gql_generated.RepoSummary{ + reposSummary = append(reposSummary, &gql_generated.RepoSummary{ Name: &repoName, LastUpdated: &lastUpdatedTag.Timestamp, Size: &repoSizeStr, @@ -234,7 +274,7 @@ func (r *queryResolver) repoListWithNewestImage(ctx context.Context, store stora }) } - return repos, nil + return reposSummary, nil } func cleanQuerry(query string) string { @@ -282,7 +322,7 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils manifestTag, ok := manifest.Annotations[ispec.AnnotationRefName] if !ok { - log.Error().Msg("reference not found for this manifest") + log.Error().Str("digest", manifest.Digest.String()).Msg("reference not found for this manifest") continue } @@ -294,10 +334,10 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils continue } - manifestSize := olu.GetImageManifestSize(repo, manifests[i].Digest) + manifestSize := olu.GetImageManifestSize(repo, manifest.Digest) configSize := imageBlobManifest.Config.Size - repoBlob2Size[manifests[i].Digest.String()] = manifestSize + repoBlob2Size[manifest.Digest.String()] = manifestSize repoBlob2Size[imageBlobManifest.Config.Digest.Hex] = configSize for _, layer := range imageBlobManifest.Layers { @@ -340,7 +380,6 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils // update matching score score := calculateImageMatchingScore(repo, index, matchesTag) - vendor := olu.GetImageVendor(imageConfigInfo) lastUpdated := olu.GetImageLastUpdated(imageConfigInfo) os, arch := olu.GetImagePlatform(imageConfigInfo) osArch := &gql_generated.OsArch{ @@ -348,21 +387,35 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils Arch: &arch, } + // get image info from manifest annotation, if not found get from image config labels. + annotations := common.GetAnnotations(imageBlobManifest.Annotations, imageConfigInfo.Config.Labels) + + manifestDigest := manifest.Digest.Hex() + configDigest := imageBlobManifest.Config.Digest.Hex + repoPlatforms = append(repoPlatforms, osArch) - repoVendors = append(repoVendors, &vendor) + repoVendors = append(repoVendors, &annotations.Vendor) imageSummary := gql_generated.ImageSummary{ - RepoName: &repo, - Tag: &manifestTag, - LastUpdated: &lastUpdated, - IsSigned: &isSigned, - Size: &size, - Platform: osArch, - Vendor: &vendor, - Score: &score, + RepoName: &repo, + Tag: &manifestTag, + LastUpdated: &lastUpdated, + Digest: &manifestDigest, + ConfigDigest: &configDigest, + IsSigned: &isSigned, + Size: &size, + Platform: osArch, + Vendor: &annotations.Vendor, + Score: &score, + Description: &annotations.Description, + Title: &annotations.Title, + Documentation: &annotations.Documentation, + Licenses: &annotations.Licenses, + Labels: &annotations.Labels, + Source: &annotations.Source, } - if manifests[i].Digest.String() == lastUpdatedTag.Digest { + if manifest.Digest.String() == lastUpdatedTag.Digest { lastUpdatedImageSummary = imageSummary } diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index 6f12c3e7..7e4723f5 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/99designs/gqlgen/graphql" v1 "github.com/google/go-containerregistry/pkg/v1" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -178,6 +179,73 @@ func TestGlobalSearch(t *testing.T) { }) } +func TestRepoListWithNewestImage(t *testing.T) { + Convey("repoListWithNewestImage", t, func() { + Convey("GetImageManifests fail", func() { + mockOlum := mocks.OciLayoutUtilsMock{ + GetImageManifestsFn: func(image string) ([]ispec.Descriptor, error) { + return []ispec.Descriptor{}, ErrTestError + }, + } + + ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.Recover) + _, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + + errs := graphql.GetErrors(ctx) + So(errs, ShouldNotBeEmpty) + }) + + Convey("GetImageBlobManifest fail", func() { + mockOlum := mocks.OciLayoutUtilsMock{ + GetImageBlobManifestFn: func(imageDir string, digest godigest.Digest) (v1.Manifest, error) { + return v1.Manifest{}, ErrTestError + }, + GetImageManifestsFn: func(image string) ([]ispec.Descriptor, error) { + return []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Size: int64(0), + }, + }, nil + }, + } + + ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.Recover) + _, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + + errs := graphql.GetErrors(ctx) + So(errs, ShouldNotBeEmpty) + }) + + Convey("GetImageConfigInfo fail", func() { + mockOlum := mocks.OciLayoutUtilsMock{ + GetImageManifestsFn: func(image string) ([]ispec.Descriptor, error) { + return []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Size: int64(0), + }, + }, nil + }, + GetImageConfigInfoFn: func(repo string, manifestDigest godigest.Digest) (ispec.Image, error) { + return ispec.Image{ + Author: "test", + }, ErrTestError + }, + } + + ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.Recover) + _, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, log.NewLogger("debug", "")) + So(err, ShouldBeNil) + + errs := graphql.GetErrors(ctx) + So(errs, ShouldNotBeEmpty) + }) + }) +} + func TestUserAvailableRepos(t *testing.T) { Convey("Type assertion fails", t, func() { var invalid struct{} diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 562a4771..4330527f 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -50,6 +50,9 @@ type ImageSummary { Description: String Licenses: String Labels: String + Title: String + Source: String + Documentation: String } # Brief on a specific repo to be used in queries returning a list of repos diff --git a/pkg/extensions/search/schema.resolvers.go b/pkg/extensions/search/schema.resolvers.go index 216e08be..2db8086e 100644 --- a/pkg/extensions/search/schema.resolvers.go +++ b/pkg/extensions/search/schema.resolvers.go @@ -291,37 +291,43 @@ func (r *queryResolver) ImageListForDigest(ctx context.Context, id string) ([]*g func (r *queryResolver) RepoListWithNewestImage(ctx context.Context) ([]*gql_generated.RepoSummary, error) { r.log.Info().Msg("extension api: finding image list") - repoList := make([]*gql_generated.RepoSummary, 0) + olu := common.NewBaseOciLayoutUtils(r.storeController, r.log) - defaultStore := r.storeController.DefaultStore + reposSummary := make([]*gql_generated.RepoSummary, 0) - dsRepoList, err := r.repoListWithNewestImage(ctx, defaultStore) + repoList := []string{} + + defaultRepoList, err := r.storeController.DefaultStore.GetRepositories() if err != nil { - r.log.Error().Err(err).Msg("extension api: error extracting default store image list") + r.log.Error().Err(err).Msg("extension api: error extracting default store repo list") - return repoList, err + return reposSummary, err } - if len(dsRepoList) != 0 { - repoList = append(repoList, dsRepoList...) + if len(defaultRepoList) > 0 { + repoList = append(repoList, defaultRepoList...) } subStore := r.storeController.SubStore - for _, store := range subStore { - ssRepoList, err := r.repoListWithNewestImage(ctx, store) + subRepoList, err := store.GetRepositories() if err != nil { - r.log.Error().Err(err).Msg("extension api: error extracting substore image list") + r.log.Error().Err(err).Msg("extension api: error extracting substore repo list") - return repoList, err + return reposSummary, err } - if len(ssRepoList) != 0 { - repoList = append(repoList, ssRepoList...) - } + repoList = append(repoList, subRepoList...) } - return repoList, nil + reposSummary, err = repoListWithNewestImage(ctx, repoList, olu, r.log) + if err != nil { + r.log.Error().Err(err).Msg("extension api: error extracting substore image list") + + return reposSummary, err + } + + return reposSummary, nil } // ImageList is the resolver for the ImageList field. @@ -382,6 +388,27 @@ func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql summary.LastUpdated = &origRepoInfo.Summary.LastUpdated summary.Name = &origRepoInfo.Summary.Name summary.Platforms = []*gql_generated.OsArch{} + summary.NewestImage = &gql_generated.ImageSummary{ + RepoName: &origRepoInfo.Summary.NewestImage.RepoName, + Tag: &origRepoInfo.Summary.NewestImage.Tag, + LastUpdated: &origRepoInfo.Summary.NewestImage.LastUpdated, + Digest: &origRepoInfo.Summary.NewestImage.Digest, + ConfigDigest: &origRepoInfo.Summary.NewestImage.ConfigDigest, + IsSigned: &origRepoInfo.Summary.NewestImage.IsSigned, + Size: &origRepoInfo.Summary.NewestImage.Size, + Platform: &gql_generated.OsArch{ + Os: &origRepoInfo.Summary.NewestImage.Platform.Os, + Arch: &origRepoInfo.Summary.NewestImage.Platform.Arch, + }, + Vendor: &origRepoInfo.Summary.NewestImage.Vendor, + Score: &origRepoInfo.Summary.NewestImage.Score, + Description: &origRepoInfo.Summary.NewestImage.Description, + Title: &origRepoInfo.Summary.NewestImage.Title, + Documentation: &origRepoInfo.Summary.NewestImage.Documentation, + Licenses: &origRepoInfo.Summary.NewestImage.Licenses, + Labels: &origRepoInfo.Summary.NewestImage.Labels, + Source: &origRepoInfo.Summary.NewestImage.Source, + } for _, platform := range origRepoInfo.Summary.Platforms { platform := platform @@ -404,9 +431,7 @@ func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql for _, image := range origRepoInfo.Images { tag := image.Tag - digest := image.Digest - isSigned := image.IsSigned imageSummary := &gql_generated.ImageSummary{Tag: &tag, Digest: &digest, IsSigned: &isSigned} @@ -415,7 +440,6 @@ func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql for _, l := range image.Layers { size := l.Size - digest := l.Digest layerInfo := &gql_generated.LayerSummary{Digest: &digest, Size: &size}