From d62c09e2cc964303c7e69fd5dbf316f329bece91 Mon Sep 17 00:00:00 2001 From: LaurentiuNiculae Date: Mon, 27 Feb 2023 21:23:18 +0200 Subject: [PATCH] feat(repodb): Multiarch Image support (#1147) * feat(repodb): index logic + tests Signed-off-by: Laurentiu Niculae * feat(cli): printing indexes support using the rest api Signed-off-by: Laurentiu Niculae --------- Signed-off-by: Laurentiu Niculae --- errors/errors.go | 2 + pkg/api/controller.go | 4 + pkg/api/controller_test.go | 137 +- pkg/cli/client.go | 369 ++- pkg/cli/client_utils_test.go | 652 ++++++ pkg/cli/cve_cmd_test.go | 55 +- pkg/cli/image_cmd_test.go | 273 ++- pkg/cli/searcher.go | 33 +- pkg/cli/service.go | 413 +++- pkg/compliance/v1_0_0/check.go | 64 +- pkg/extensions/extension_ui_test.go | 8 +- pkg/extensions/lint/lint_test.go | 5 + pkg/extensions/search/common/common.go | 44 +- pkg/extensions/search/common/common_test.go | 2083 +++++++++++++---- pkg/extensions/search/common/model.go | 32 +- pkg/extensions/search/common/oci_layout.go | 72 +- pkg/extensions/search/convert/convert_test.go | 182 +- pkg/extensions/search/convert/oci.go | 115 +- pkg/extensions/search/convert/repodb.go | 699 +++--- pkg/extensions/search/cve/cve.go | 195 +- pkg/extensions/search/cve/cve_test.go | 199 +- pkg/extensions/search/cve/model/models.go | 20 + pkg/extensions/search/cve/pagination_test.go | 27 +- pkg/extensions/search/cve/trivy/scanner.go | 46 +- .../search/cve/trivy/scanner_internal_test.go | 17 +- pkg/extensions/search/digest/digest.go | 2 +- pkg/extensions/search/digest/digest_test.go | 69 +- .../search/gql_generated/generated.go | 1547 +++++++----- .../search/gql_generated/models_gen.go | 75 +- pkg/extensions/search/resolver.go | 345 ++- pkg/extensions/search/resolver_test.go | 631 ++++- pkg/extensions/search/schema.graphql | 102 +- pkg/extensions/search/schema.resolvers.go | 10 +- pkg/extensions/sync/sync_test.go | 8 +- .../repodb/boltdb-wrapper/boltdb_wrapper.go | 652 +++++- .../boltdb-wrapper/boltdb_wrapper_test.go | 198 +- .../dynamodb-wrapper/dynamo_internal_test.go | 3 + .../repodb/dynamodb-wrapper/dynamo_test.go | 363 ++- .../repodb/dynamodb-wrapper/dynamo_wrapper.go | 633 ++++- .../dynamodb-wrapper/params/parameters.go | 3 +- pkg/meta/repodb/repodb.go | 26 +- pkg/meta/repodb/repodb_test.go | 553 ++++- .../repodbfactory/repodb_factory_test.go | 1 + pkg/meta/repodb/sync_repodb.go | 93 +- pkg/meta/repodb/sync_repodb_test.go | 66 +- pkg/meta/repodb/update/update.go | 45 +- pkg/meta/repodb/update/update_test.go | 35 +- pkg/meta/repodb/version/version_test.go | 1 + pkg/test/common.go | 345 ++- pkg/test/common_test.go | 8 +- pkg/test/mocks/cve_mock.go | 25 +- pkg/test/mocks/repo_db_mock.go | 45 +- test/blackbox/annotations.bats | 11 +- test/blackbox/cloud-only.bats | 3 +- 54 files changed, 8656 insertions(+), 2988 deletions(-) create mode 100644 pkg/cli/client_utils_test.go diff --git a/errors/errors.go b/errors/errors.go index 96c55390..0bbbc9d6 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -63,6 +63,7 @@ var ( ErrManifestConflict = errors.New("manifest: multiple manifests found") ErrManifestMetaNotFound = errors.New("repodb: image metadata not found for given manifest digest") ErrManifestDataNotFound = errors.New("repodb: image data not found for given manifest digest") + ErrIndexDataNotFount = errors.New("repodb: index data not found for given digest") ErrRepoMetaNotFound = errors.New("repodb: repo metadata not found for given repo name") ErrTagMetaNotFound = errors.New("repodb: tag metadata not found for given repo and tag names") ErrTypeAssertionFailed = errors.New("storage: failed DatabaseDriver type assertion") @@ -77,4 +78,5 @@ var ( ErrOffsetIsNegative = errors.New("pageturner: offset has negative value") ErrSortCriteriaNotSupported = errors.New("pageturner: the sort criteria is not supported") ErrTimeout = errors.New("operation timeout") + ErrNotImplemented = errors.New("not implemented") ) diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 8952c3ab..3a1faeb2 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -551,6 +551,9 @@ func getDynamoParams(cacheDriverConfig map[string]interface{}, log log.Logger) d manifestDataTablename, ok := toStringIfOk(cacheDriverConfig, "manifestdatatablename", log) allParametersOk = allParametersOk && ok + indexDataTablename, ok := toStringIfOk(cacheDriverConfig, "indexdatatablename", log) + allParametersOk = allParametersOk && ok + versionTablename, ok := toStringIfOk(cacheDriverConfig, "versiontablename", log) allParametersOk = allParametersOk && ok @@ -563,6 +566,7 @@ func getDynamoParams(cacheDriverConfig map[string]interface{}, log log.Logger) d Region: region, RepoMetaTablename: repoMetaTablename, ManifestDataTablename: manifestDataTablename, + IndexDataTablename: indexDataTablename, VersionTablename: versionTablename, } } diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 56ed40e5..7eeeecff 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -3860,10 +3860,10 @@ func TestImageSignatures(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "1.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "1.0", }, baseURL, repoName) So(err, ShouldBeNil) @@ -4100,10 +4100,10 @@ func TestArtifactReferences(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "1.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "1.0", }, baseURL, repoName) So(err, ShouldBeNil) @@ -4967,10 +4967,10 @@ func TestStorageCommit(t *testing.T) { repoName := "repo7" err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:1.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:1.0", }, baseURL, repoName) So(err, ShouldBeNil) @@ -5002,10 +5002,10 @@ func TestStorageCommit(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:1.0.1", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:1.0.1", }, baseURL, repoName) So(err, ShouldBeNil) @@ -5014,10 +5014,10 @@ func TestStorageCommit(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:2.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:2.0", }, baseURL, repoName) So(err, ShouldBeNil) @@ -5124,10 +5124,10 @@ func TestManifestImageIndex(t *testing.T) { repoName := "index" err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:1.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:1.0", }, baseURL, repoName) So(err, ShouldBeNil) @@ -5552,10 +5552,10 @@ func TestManifestCollision(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:1.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:1.0", }, baseURL, "index") So(err, ShouldBeNil) @@ -5579,10 +5579,10 @@ func TestManifestCollision(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:2.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:2.0", }, baseURL, "index") So(err, ShouldBeNil) @@ -6215,10 +6215,10 @@ func TestGCSignaturesAndUntaggedManifests(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: tag, + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: tag, }, baseURL, repoName) So(err, ShouldNotBeNil) @@ -6232,10 +6232,10 @@ func TestGCSignaturesAndUntaggedManifests(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: tag, + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: tag, }, baseURL, repoName) So(err, ShouldNotBeNil) @@ -6253,10 +6253,10 @@ func TestGCSignaturesAndUntaggedManifests(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: untaggedManifestDigest.String(), + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: untaggedManifestDigest.String(), }, baseURL, repoName) So(err, ShouldBeNil) @@ -6266,10 +6266,10 @@ func TestGCSignaturesAndUntaggedManifests(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: tag, + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: tag, }, baseURL, repoName) So(err, ShouldBeNil) @@ -6342,10 +6342,10 @@ func TestGCSignaturesAndUntaggedManifests(t *testing.T) { err = test.UploadImage( test.Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: manifestDigest.String(), + Manifest: manifest, + Config: config, + Layers: layers, + Reference: manifestDigest.String(), }, baseURL, repoName) @@ -6527,10 +6527,10 @@ func TestSearchRoutes(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "latest", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "latest", }, baseURL, repoName) So(err, ShouldBeNil) @@ -6541,10 +6541,10 @@ func TestSearchRoutes(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "latest", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "latest", }, baseURL, inaccessibleRepo) So(err, ShouldBeNil) @@ -6615,10 +6615,10 @@ func TestSearchRoutes(t *testing.T) { err = test.UploadImageWithBasicAuth( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "latest", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "latest", }, baseURL, repoName, user1, password1) @@ -6630,10 +6630,10 @@ func TestSearchRoutes(t *testing.T) { err = test.UploadImageWithBasicAuth( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "latest", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "latest", }, baseURL, inaccessibleRepo, user1, password1) @@ -6657,7 +6657,6 @@ func TestSearchRoutes(t *testing.T) { So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) - So(string(resp.Body()), ShouldContainSubstring, repoName) So(string(resp.Body()), ShouldNotContainSubstring, inaccessibleRepo) diff --git a/pkg/cli/client.go b/pkg/cli/client.go index b6123d13..c1ca2182 100644 --- a/pkg/cli/client.go +++ b/pkg/cli/client.go @@ -52,6 +52,19 @@ func makeGETRequest(ctx context.Context, url, username, password string, return doHTTPRequest(req, verifyTLS, debug, resultsPtr, configWriter) } +func makeHEADRequest(ctx context.Context, url, username, password string, verifyTLS bool, + debug bool, +) (http.Header, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) + if err != nil { + return nil, err + } + + req.SetBasicAuth(username, password) + + return doHTTPRequest(req, verifyTLS, debug, nil, io.Discard) +} + func makeGraphQLRequest(ctx context.Context, url, query, username, password string, verifyTLS bool, debug bool, resultsPtr interface{}, configWriter io.Writer, ) error { @@ -126,6 +139,10 @@ func doHTTPRequest(req *http.Request, verifyTLS bool, debug bool, return nil, errors.New(string(bodyBytes)) //nolint: goerr113 } + if resultsPtr == nil { + return resp.Header, nil + } + if err := json.NewDecoder(resp.Body).Decode(resultsPtr); err != nil { return nil, err } @@ -140,26 +157,25 @@ func isURL(str string) bool { } // from https://stackoverflow.com/a/55551215 type requestsPool struct { - jobs chan *manifestJob + jobs chan *httpJob done chan struct{} wtgrp *sync.WaitGroup outputCh chan stringResult } -type manifestJob struct { - url string - username string - password string - imageName string - tagName string - config searchConfig - manifestResp manifestResponse +type httpJob struct { + url string + username string + password string + imageName string + tagName string + config searchConfig } const rateLimiterBuffer = 5000 func newSmoothRateLimiter(wtgrp *sync.WaitGroup, opch chan stringResult) *requestsPool { - ch := make(chan *manifestJob, rateLimiterBuffer) + ch := make(chan *httpJob, rateLimiterBuffer) return &requestsPool{ jobs: ch, @@ -188,11 +204,12 @@ func (p *requestsPool) startRateLimiter(ctx context.Context) { } } -func (p *requestsPool) doJob(ctx context.Context, job *manifestJob) { +func (p *requestsPool) doJob(ctx context.Context, job *httpJob) { defer p.wtgrp.Done() - header, err := makeGETRequest(ctx, job.url, job.username, job.password, - *job.config.verifyTLS, *job.config.debug, &job.manifestResp, job.config.resultWriter) + // Check manifest media type + header, err := makeHEADRequest(ctx, job.url, job.username, job.password, *job.config.verifyTLS, + *job.config.debug) if err != nil { if isContextDone(ctx) { return @@ -200,88 +217,298 @@ func (p *requestsPool) doJob(ctx context.Context, job *manifestJob) { p.outputCh <- stringResult{"", err} } - digestStr := header.Get("docker-content-digest") - configDigest := job.manifestResp.Config.Digest + switch header.Get("Content-Type") { + case ispec.MediaTypeImageManifest: + image, err := fetchImageManifestStruct(ctx, job) + if err != nil { + if isContextDone(ctx) { + return + } + p.outputCh <- stringResult{"", err} - var size uint64 + return + } + platformStr := getPlatformStr(image.Manifests[0].Platform) + + str, err := image.string(*job.config.outputFormat, len(job.imageName), len(job.tagName), len(platformStr)) + if err != nil { + if isContextDone(ctx) { + return + } + p.outputCh <- stringResult{"", err} + + return + } + + if isContextDone(ctx) { + return + } + + p.outputCh <- stringResult{str, nil} + case ispec.MediaTypeImageIndex: + image, err := fetchImageIndexStruct(ctx, job) + if err != nil { + if isContextDone(ctx) { + return + } + p.outputCh <- stringResult{"", err} + + return + } + + platformStr := getPlatformStr(image.Manifests[0].Platform) + + str, err := image.string(*job.config.outputFormat, len(job.imageName), len(job.tagName), len(platformStr)) + if err != nil { + if isContextDone(ctx) { + return + } + p.outputCh <- stringResult{"", err} + + return + } + + if isContextDone(ctx) { + return + } + + p.outputCh <- stringResult{str, nil} + default: + return + } +} + +func fetchImageIndexStruct(ctx context.Context, job *httpJob) (*imageStruct, error) { + var indexContent ispec.Index + + header, err := makeGETRequest(ctx, job.url, job.username, job.password, + *job.config.verifyTLS, *job.config.debug, &indexContent, job.config.resultWriter) + if err != nil { + if isContextDone(ctx) { + return nil, context.Canceled + } + + return nil, err + } + + indexDigest := header.Get("docker-content-digest") + + indexSize, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64) + if err != nil { + return nil, err + } + + imageSize := indexSize + + manifestList := make([]manifestStruct, 0, len(indexContent.Manifests)) + + for _, manifestDescriptor := range indexContent.Manifests { + manifest, err := fetchManifestStruct(ctx, job.imageName, manifestDescriptor.Digest.String(), + job.config, job.username, job.password) + if err != nil { + return nil, err + } + + imageSize += int64(atoiWithDefault(manifest.Size, 0)) + + if manifestDescriptor.Platform != nil { + manifest.Platform = platform{ + Os: manifestDescriptor.Platform.OS, + Arch: manifestDescriptor.Platform.Architecture, + Variant: manifestDescriptor.Platform.Variant, + } + } + + manifestList = append(manifestList, manifest) + } + + isIndexSigned := isCosignSigned(ctx, job.imageName, indexDigest, job.config, job.username, job.password) || + isNotationSigned(ctx, job.imageName, indexDigest, job.config, job.username, job.password) + + return &imageStruct{ + verbose: *job.config.verbose, + RepoName: job.imageName, + Tag: job.tagName, + Size: strconv.FormatInt(imageSize, 10), + IsSigned: isIndexSigned, + Manifests: manifestList, + }, nil +} + +func atoiWithDefault(size string, defaultVal int) int { + val, err := strconv.Atoi(size) + if err != nil { + return defaultVal + } + + return val +} + +func fetchImageManifestStruct(ctx context.Context, job *httpJob) (*imageStruct, error) { + manifest, err := fetchManifestStruct(ctx, job.imageName, job.tagName, job.config, job.username, job.password) + if err != nil { + return nil, err + } + + return &imageStruct{ + verbose: *job.config.verbose, + RepoName: job.imageName, + Tag: job.tagName, + Size: manifest.Size, + IsSigned: manifest.IsSigned, + Manifests: []manifestStruct{ + manifest, + }, + }, nil +} + +func fetchManifestStruct(ctx context.Context, repo, manifestReference string, searchConf searchConfig, + username, password string, +) (manifestStruct, error) { + manifestResp := ispec.Manifest{} + + URL := fmt.Sprintf("%s/v2/%s/manifests/%s", + *searchConf.servURL, repo, manifestReference) + + header, err := makeGETRequest(ctx, URL, username, password, + *searchConf.verifyTLS, *searchConf.debug, &manifestResp, searchConf.resultWriter) + if err != nil { + if isContextDone(ctx) { + return manifestStruct{}, context.Canceled + } + + return manifestStruct{}, err + } + + manifestDigest := header.Get("docker-content-digest") + configDigest := manifestResp.Config.Digest.String() + + configContent, err := fetchConfig(ctx, repo, configDigest, searchConf, username, password) + if err != nil { + if isContextDone(ctx) { + return manifestStruct{}, context.Canceled + } + + return manifestStruct{}, err + } + + opSys := "" + arch := "" + variant := "" + + if manifestResp.Config.Platform != nil { + opSys = manifestResp.Config.Platform.OS + arch = manifestResp.Config.Platform.Architecture + variant = manifestResp.Config.Platform.Variant + } + + if opSys == "" { + opSys = configContent.OS + } + + if arch == "" { + arch = configContent.Architecture + } + + if variant == "" { + variant = configContent.Variant + } + + manifestSize, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64) + if err != nil { + return manifestStruct{}, err + } + + var imageSize int64 + + imageSize += manifestResp.Config.Size + imageSize += manifestSize layers := []layer{} - for _, entry := range job.manifestResp.Layers { - size += entry.Size + for _, entry := range manifestResp.Layers { + imageSize += entry.Size layers = append( layers, layer{ Size: entry.Size, - Digest: entry.Digest, + Digest: entry.Digest.String(), }, ) } - size += uint64(job.manifestResp.Config.Size) + isSigned := isCosignSigned(ctx, repo, manifestDigest, searchConf, username, password) || + isNotationSigned(ctx, repo, manifestDigest, searchConf, username, password) - manifestSize, err := strconv.Atoi(header.Get("Content-Length")) - if err != nil { - p.outputCh <- stringResult{"", err} - } + return manifestStruct{ + ConfigDigest: configDigest, + Digest: manifestDigest, + Layers: layers, + Platform: platform{Os: opSys, Arch: arch, Variant: variant}, + Size: strconv.FormatInt(imageSize, 10), + IsSigned: isSigned, + }, nil +} - isSigned := false - cosignTag := strings.Replace(digestStr, ":", "-", 1) + "." + remote.SignatureTagSuffix +func fetchConfig(ctx context.Context, repo, configDigest string, searchConf searchConfig, + username, password string, +) (ispec.Image, error) { + configContent := ispec.Image{} - _, err = makeGETRequest(ctx, *job.config.servURL+"/v2/"+job.imageName+ - "/manifests/"+cosignTag, job.username, job.password, - *job.config.verifyTLS, *job.config.debug, &job.manifestResp, job.config.resultWriter) - if err == nil { - isSigned = true - } + URL := fmt.Sprintf("%s/v2/%s/blobs/%s", + *searchConf.servURL, repo, configDigest) - var referrers ispec.Index - - if !isSigned { - _, err = makeGETRequest(ctx, fmt.Sprintf("%s/v2/%s/referrers/%s?artifactType=%s", - *job.config.servURL, job.imageName, digestStr, notreg.ArtifactTypeNotation), job.username, job.password, - *job.config.verifyTLS, *job.config.debug, &referrers, job.config.resultWriter) - if err == nil { - for _, reference := range referrers.Manifests { - if reference.ArtifactType == notreg.ArtifactTypeNotation { - isSigned = true - - break - } - } - } - } - - size += uint64(manifestSize) - - image := &imageStruct{} - image.verbose = *job.config.verbose - image.RepoName = job.imageName - image.Tag = job.tagName - image.Digest = digestStr - image.Size = strconv.Itoa(int(size)) - image.ConfigDigest = configDigest - image.Layers = layers - image.IsSigned = isSigned - - str, err := image.string(*job.config.outputFormat, len(job.imageName), len(job.tagName)) + _, err := makeGETRequest(ctx, URL, username, password, + *searchConf.verifyTLS, *searchConf.debug, &configContent, searchConf.resultWriter) if err != nil { if isContextDone(ctx) { - return + return ispec.Image{}, context.Canceled } - p.outputCh <- stringResult{"", err} - return + return ispec.Image{}, err } - if isContextDone(ctx) { - return - } - - p.outputCh <- stringResult{str, nil} + return configContent, nil } -func (p *requestsPool) submitJob(job *manifestJob) { +func isNotationSigned(ctx context.Context, repo, digestStr string, searchConf searchConfig, + username, password string, +) bool { + var referrers ispec.Index + + URL := fmt.Sprintf("%s/v2/%s/referrers/%s?artifactType=%s", + *searchConf.servURL, repo, digestStr, notreg.ArtifactTypeNotation) + + _, err := makeGETRequest(ctx, URL, username, password, + *searchConf.verifyTLS, *searchConf.debug, &referrers, searchConf.resultWriter) + if err != nil { + return false + } + + for _, reference := range referrers.Manifests { + if reference.ArtifactType == notreg.ArtifactTypeNotation { + return true + } + } + + return false +} + +func isCosignSigned(ctx context.Context, repo, digestStr string, searchConf searchConfig, + username, password string, +) bool { + var result interface{} + cosignTag := strings.Replace(digestStr, ":", "-", 1) + "." + remote.SignatureTagSuffix + + URL := fmt.Sprintf("%s/v2/%s/manifests/%s", *searchConf.servURL, repo, cosignTag) + + _, err := makeGETRequest(ctx, URL, username, password, *searchConf.verifyTLS, + *searchConf.debug, &result, searchConf.resultWriter) + + return err == nil +} + +func (p *requestsPool) submitJob(job *httpJob) { p.jobs <- job } diff --git a/pkg/cli/client_utils_test.go b/pkg/cli/client_utils_test.go new file mode 100644 index 00000000..4b763e9d --- /dev/null +++ b/pkg/cli/client_utils_test.go @@ -0,0 +1,652 @@ +//go:build search +// +build search + +package cli //nolint:testpackage + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "sync" + "testing" + + "github.com/gorilla/mux" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + . "github.com/smartystreets/goconvey/convey" + + "zotregistry.io/zot/pkg/test" +) + +type RouteHandler struct { + Route string + // HandlerFunc is the HTTP handler function that receives a writer for output and an HTTP request as input. + HandlerFunc http.HandlerFunc + // AllowedMethods specifies the HTTP methods allowed for the current route. + AllowedMethods []string +} + +// Routes is a map that associates HTTP paths to their corresponding HTTP handlers. +type HTTPRoutes []RouteHandler + +func StartTestHTTPServer(routes HTTPRoutes, port string) *http.Server { + baseURL := test.GetBaseURL(port) + mux := mux.NewRouter() + + mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("{}")) + if err != nil { + return + } + }).Methods(http.MethodGet) + + for _, routeHandler := range routes { + mux.HandleFunc(routeHandler.Route, routeHandler.HandlerFunc).Methods(routeHandler.AllowedMethods...) + } + + server := &http.Server{ //nolint:gosec + Addr: fmt.Sprintf(":%s", port), + Handler: mux, + } + + go func() { + if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + return + } + }() + + test.WaitTillServerReady(baseURL + "/test") + + return server +} + +func getDefaultSearchConf(baseURL string) searchConfig { + verifyTLS := false + debug := false + verbose := true + outputFormat := "text" + + return searchConfig{ + servURL: &baseURL, + resultWriter: io.Discard, + verifyTLS: &verifyTLS, + debug: &debug, + verbose: &verbose, + outputFormat: &outputFormat, + } +} + +func TestDoHTTPRequest(t *testing.T) { + Convey("doHTTPRequest nil result pointer", t, func() { + port := test.GetFreePort() + server := StartTestHTTPServer(nil, port) + defer server.Close() + + url := fmt.Sprintf("http://127.0.0.1:%s/asd", port) + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, nil) + So(err, ShouldBeNil) + + So(func() { _, _ = doHTTPRequest(req, false, false, nil, io.Discard) }, ShouldNotPanic) + }) + + Convey("doHTTPRequest bad return json", t, func() { + port := test.GetFreePort() + server := StartTestHTTPServer(HTTPRoutes{ + { + Route: "/test", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("bad json")) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodGet}, + }, + }, port) + defer server.Close() + + url := fmt.Sprintf("http://127.0.0.1:%s/test", port) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + So(err, ShouldBeNil) + + So(func() { _, _ = doHTTPRequest(req, false, false, &ispec.Manifest{}, io.Discard) }, ShouldNotPanic) + }) + + Convey("makeGraphQLRequest bad request context", t, func() { + err := makeGraphQLRequest(nil, "", "", "", "", false, false, nil, io.Discard) //nolint:staticcheck + So(err, ShouldNotBeNil) + }) + + Convey("makeHEADRequest bad request context", t, func() { + _, err := makeHEADRequest(nil, "", "", "", false, false) //nolint:staticcheck + So(err, ShouldNotBeNil) + }) + + Convey("makeGETRequest bad request context", t, func() { + _, err := makeGETRequest(nil, "", "", "", false, false, nil, io.Discard) //nolint:staticcheck + So(err, ShouldNotBeNil) + }) + + Convey("fetchImageManifestStruct errors", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + searchConf := getDefaultSearchConf(baseURL) + + // 404 erorr will appear + server := StartTestHTTPServer(HTTPRoutes{}, port) + defer server.Close() + + URL := baseURL + "/v2/repo/manifests/tag" + + _, err := fetchImageManifestStruct(context.Background(), &httpJob{ + url: URL, + username: "", + password: "", + imageName: "repo", + tagName: "tag", + config: searchConf, + }) + + So(err, ShouldNotBeNil) + }) + + Convey("fetchManifestStruct errors", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + searchConf := getDefaultSearchConf(baseURL) + + Convey("makeGETRequest manifest error, context is done", func() { + server := StartTestHTTPServer(HTTPRoutes{}, port) + defer server.Close() + + ctx, cancel := context.WithCancel(context.Background()) + + cancel() + + _, err := fetchManifestStruct(ctx, "repo", "tag", searchConf, + "", "") + + So(err, ShouldNotBeNil) + }) + + Convey("makeGETRequest manifest error, context is not done", func() { + server := StartTestHTTPServer(HTTPRoutes{}, port) + defer server.Close() + + _, err := fetchManifestStruct(context.Background(), "repo", "tag", searchConf, + "", "") + + So(err, ShouldNotBeNil) + }) + + Convey("makeGETRequest config error, context is not done", func() { + server := StartTestHTTPServer(HTTPRoutes{ + { + Route: "/v2/{name}/manifests/{reference}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(`{"config":{"digest":"digest","size":0}}`)) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodGet}, + }, + }, port) + defer server.Close() + + _, err := fetchManifestStruct(context.Background(), "repo", "tag", searchConf, + "", "") + + So(err, ShouldNotBeNil) + }) + + Convey("Platforms on config", func() { + server := StartTestHTTPServer(HTTPRoutes{ + { + Route: "/v2/{name}/manifests/{reference}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(` + { + "config":{ + "digest":"digest", + "size":0, + "platform" : { + "os": "", + "architecture": "", + "variant": "" + } + } + } + `)) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodGet}, + }, + { + Route: "/v2/{name}/blobs/{digest}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(` + { + "architecture": "arch", + "os": "os", + "variant": "var" + } + `)) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodGet}, + }, + }, port) + defer server.Close() + + _, err := fetchManifestStruct(context.Background(), "repo", "tag", searchConf, + "", "") + + So(err, ShouldBeNil) + }) + + Convey("isNotationSigned error", func() { + isSigned := isNotationSigned(context.Background(), "repo", "digest", searchConf, + "", "") + So(isSigned, ShouldBeFalse) + }) + + Convey("fetchImageIndexStruct no errors", func() { + server := StartTestHTTPServer(HTTPRoutes{ + { + Route: "/v2/{name}/manifests/{reference}", + HandlerFunc: func(writer http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + + if vars["reference"] == "indexRef" { + _, err := writer.Write([]byte(` + { + "manifests": [ + { + "digest": "manifestRef", + "platform": { + "architecture": "arch", + "os": "os", + "variant": "var" + } + } + ] + } + `)) + if err != nil { + return + } + } else if vars["reference"] == "manifestRef" { + _, err := writer.Write([]byte(` + { + "config":{ + "digest":"digest", + "size":0 + } + } + `)) + if err != nil { + return + } + } + }, + AllowedMethods: []string{http.MethodGet}, + }, + { + Route: "/v2/{name}/blobs/{digest}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(`{}`)) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodGet}, + }, + }, port) + defer server.Close() + + URL := baseURL + "/v2/repo/manifests/indexRef" + + imageStruct, err := fetchImageIndexStruct(context.Background(), &httpJob{ + url: URL, + username: "", + password: "", + imageName: "repo", + tagName: "tag", + config: searchConf, + }) + So(err, ShouldBeNil) + So(imageStruct, ShouldNotBeNil) + }) + + Convey("fetchImageIndexStruct makeGETRequest errors context done", func() { + server := StartTestHTTPServer(HTTPRoutes{}, port) + defer server.Close() + + ctx, cancel := context.WithCancel(context.Background()) + + cancel() + + URL := baseURL + "/v2/repo/manifests/indexRef" + + imageStruct, err := fetchImageIndexStruct(ctx, &httpJob{ + url: URL, + username: "", + password: "", + imageName: "repo", + tagName: "tag", + config: searchConf, + }) + So(err, ShouldNotBeNil) + So(imageStruct, ShouldBeNil) + }) + + Convey("fetchImageIndexStruct makeGETRequest errors context not done", func() { + server := StartTestHTTPServer(HTTPRoutes{}, port) + defer server.Close() + + URL := baseURL + "/v2/repo/manifests/indexRef" + + imageStruct, err := fetchImageIndexStruct(context.Background(), &httpJob{ + url: URL, + username: "", + password: "", + imageName: "repo", + tagName: "tag", + config: searchConf, + }) + So(err, ShouldNotBeNil) + So(imageStruct, ShouldBeNil) + }) + }) +} + +func TestDoJobErrors(t *testing.T) { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + searchConf := getDefaultSearchConf(baseURL) + + reqPool := &requestsPool{ + jobs: make(chan *httpJob), + done: make(chan struct{}), + wtgrp: &sync.WaitGroup{}, + outputCh: make(chan stringResult), + } + + Convey("Do Job errors", t, func() { + reqPool.wtgrp.Add(1) + + Convey("Do Job makeHEADRequest error context done", func() { + server := StartTestHTTPServer(HTTPRoutes{}, port) + defer server.Close() + + URL := baseURL + "/v2/repo/manifests/manifestRef" + + ctx, cancel := context.WithCancel(context.Background()) + + cancel() + + reqPool.doJob(ctx, &httpJob{ + url: URL, + username: "", + password: "", + imageName: "", + tagName: "", + config: searchConf, + }) + }) + + Convey("Do Job makeHEADRequest error context not done", func() { + server := StartTestHTTPServer(HTTPRoutes{}, port) + defer server.Close() + + URL := baseURL + "/v2/repo/manifests/manifestRef" + + ctx := context.Background() + + go reqPool.doJob(ctx, &httpJob{ + url: URL, + username: "", + password: "", + imageName: "", + tagName: "", + config: searchConf, + }) + + result := <-reqPool.outputCh + So(result.Err, ShouldNotBeNil) + So(result.StrValue, ShouldResemble, "") + }) + + Convey("Do Job fetchManifestStruct errors context canceled", func() { + server := StartTestHTTPServer(HTTPRoutes{ + { + Route: "/v2/{name}/manifests/{reference}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", ispec.MediaTypeImageManifest) + _, err := w.Write([]byte("")) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodHead}, + }, + }, port) + defer server.Close() + + URL := baseURL + "/v2/repo/manifests/manifestRef" + + ctx, cancel := context.WithCancel(context.Background()) + + cancel() + // context not canceled + + reqPool.doJob(ctx, &httpJob{ + url: URL, + username: "", + password: "", + imageName: "", + tagName: "", + config: searchConf, + }) + }) + + Convey("Do Job fetchManifestStruct errors context not canceled", func() { + server := StartTestHTTPServer(HTTPRoutes{ + { + Route: "/v2/{name}/manifests/{reference}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", ispec.MediaTypeImageManifest) + _, err := w.Write([]byte("")) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodHead}, + }, + }, port) + defer server.Close() + + URL := baseURL + "/v2/repo/manifests/manifestRef" + + ctx := context.Background() + + go reqPool.doJob(ctx, &httpJob{ + url: URL, + username: "", + password: "", + imageName: "", + tagName: "", + config: searchConf, + }) + + result := <-reqPool.outputCh + So(result.Err, ShouldNotBeNil) + So(result.StrValue, ShouldResemble, "") + }) + + Convey("Do Job fetchIndexStruct errors context canceled", func() { + server := StartTestHTTPServer(HTTPRoutes{ + { + Route: "/v2/{name}/manifests/{reference}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", ispec.MediaTypeImageIndex) + _, err := w.Write([]byte("")) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodHead}, + }, + }, port) + defer server.Close() + + URL := baseURL + "/v2/repo/manifests/indexRef" + + ctx, cancel := context.WithCancel(context.Background()) + + cancel() + // context not canceled + + reqPool.doJob(ctx, &httpJob{ + url: URL, + username: "", + password: "", + imageName: "", + tagName: "", + config: searchConf, + }) + }) + + Convey("Do Job fetchIndexStruct errors context not canceled", func() { + server := StartTestHTTPServer(HTTPRoutes{ + { + Route: "/v2/{name}/manifests/{reference}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", ispec.MediaTypeImageIndex) + _, err := w.Write([]byte("")) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodHead}, + }, + }, port) + defer server.Close() + + URL := baseURL + "/v2/repo/manifests/indexRef" + + ctx := context.Background() + + go reqPool.doJob(ctx, &httpJob{ + url: URL, + username: "", + password: "", + imageName: "", + tagName: "", + config: searchConf, + }) + + result := <-reqPool.outputCh + So(result.Err, ShouldNotBeNil) + So(result.StrValue, ShouldResemble, "") + }) + Convey("Do Job fetchIndexStruct not supported content type", func() { + server := StartTestHTTPServer(HTTPRoutes{ + { + Route: "/v2/{name}/manifests/{reference}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "some-media-type") + _, err := w.Write([]byte("")) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodHead}, + }, + }, port) + defer server.Close() + + URL := baseURL + "/v2/repo/manifests/indexRef" + + ctx := context.Background() + + reqPool.doJob(ctx, &httpJob{ + url: URL, + username: "", + password: "", + imageName: "", + tagName: "", + config: searchConf, + }) + }) + + Convey("Media type is MediaTypeImageIndex image.string erorrs", func() { + server := StartTestHTTPServer(HTTPRoutes{ + { + Route: "/v2/{name}/manifests/{reference}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", ispec.MediaTypeImageIndex) + + _, err := w.Write([]byte("")) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodHead}, + }, + { + Route: "/v2/{name}/manifests/{reference}", + HandlerFunc: func(writer http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + + if vars["reference"] == "indexRef" { + _, err := writer.Write([]byte(`{"manifests": [{"digest": "manifestRef"}]}`)) + if err != nil { + return + } + } + + if vars["reference"] == "manifestRef" { + _, err := writer.Write([]byte(`{"config": {"digest": "confDigest"}}`)) + if err != nil { + return + } + } + }, + AllowedMethods: []string{http.MethodGet}, + }, + { + Route: "/v2/{name}/blobs/{digest}", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(`{}`)) + if err != nil { + return + } + }, + AllowedMethods: []string{http.MethodGet}, + }, + }, port) + defer server.Close() + URL := baseURL + "/v2/repo/manifests/indexRef" + + go reqPool.doJob(context.Background(), &httpJob{ + url: URL, + username: "", + password: "", + imageName: "repo", + tagName: "indexRef", + config: searchConf, + }) + + result := <-reqPool.outputCh + So(result.Err, ShouldNotBeNil) + So(result.StrValue, ShouldResemble, "") + }) + }) +} diff --git a/pkg/cli/cve_cmd_test.go b/pkg/cli/cve_cmd_test.go index bae1fd50..b2db53ca 100644 --- a/pkg/cli/cve_cmd_test.go +++ b/pkg/cli/cve_cmd_test.go @@ -179,7 +179,8 @@ func TestSearchCVECmd(t *testing.T) { So(err, ShouldBeNil) space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE dummyImageName tag 6e2f80bf false 123kB") + So(strings.TrimSpace(str), ShouldEqual, + "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE dummyImageName tag 6e2f80bf os/arch false 123kB") }) Convey("Test CVE by name and CVE ID - using shorthand", t, func() { @@ -195,7 +196,8 @@ func TestSearchCVECmd(t *testing.T) { So(err, ShouldBeNil) space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE dummyImageName tag 6e2f80bf false 123kB") + So(strings.TrimSpace(str), ShouldEqual, + "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE dummyImageName tag 6e2f80bf os/arch false 123kB") }) Convey("Test CVE by image name - in text format", t, func() { @@ -278,7 +280,7 @@ func TestSearchCVECmd(t *testing.T) { err := cveCmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE anImage tag 6e2f80bf false 123kB") + So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE anImage tag 6e2f80bf os/arch false 123kB") //nolint:lll So(err, ShouldBeNil) }) @@ -323,7 +325,7 @@ func TestSearchCVECmd(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(err, ShouldBeNil) - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE fixedImage tag 6e2f80bf false 123kB") + So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE fixedImage tag 6e2f80bf os/arch false 123kB") //nolint:lll }) Convey("Test fixed tags by and image name CVE ID - invalid image name", t, func() { @@ -625,8 +627,8 @@ func TestServerCVEResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") str = strings.TrimSpace(str) So(err, ShouldBeNil) - So(str, ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE zot-cve-test 0.0.1 "+ - test.GetTestBlobDigest("zot-cve-test", "manifest").Encoded()[:8]+" false 75MB") + So(str, ShouldEqual, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE zot-cve-test 0.0.1 "+ + test.GetTestBlobDigest("zot-cve-test", "manifest").Encoded()[:8]+" N/A false 75MB") }) Convey("Test images by CVE ID - GQL - invalid CVE ID", t, func() { @@ -643,7 +645,7 @@ func TestServerCVEResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") str = strings.TrimSpace(str) So(err, ShouldBeNil) - So(str, ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") + So(str, ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") }) Convey("Test images by CVE ID - GQL - invalid output format", t, func() { @@ -691,7 +693,7 @@ func TestServerCVEResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") str = strings.TrimSpace(str) So(err, ShouldBeNil) - So(strings.TrimSpace(str), ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") + So(strings.TrimSpace(str), ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") }) Convey("Test fixed tags by image name and CVE ID - GQL - random image", t, func() { @@ -708,7 +710,7 @@ func TestServerCVEResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") str = strings.TrimSpace(str) So(err, ShouldNotBeNil) - So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") + So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") }) Convey("Test fixed tags by image name and CVE ID - GQL - invalid image", t, func() { @@ -725,7 +727,7 @@ func TestServerCVEResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") str = strings.TrimSpace(str) So(err, ShouldNotBeNil) - So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") + So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") }) Convey("Test CVE by name and CVE ID - GQL - positive", t, func() { @@ -741,8 +743,8 @@ func TestServerCVEResponse(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(err, ShouldBeNil) - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE zot-cve-test 0.0.1 "+ - test.GetTestBlobDigest("zot-cve-test", "manifest").Encoded()[:8]+" false 75MB") + So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE zot-cve-test 0.0.1 "+ + test.GetTestBlobDigest("zot-cve-test", "manifest").Encoded()[:8]+" N/A false 75MB") }) Convey("Test CVE by name and CVE ID - GQL - invalid name and CVE ID", t, func() { @@ -820,8 +822,8 @@ func TestServerCVEResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") str = strings.TrimSpace(str) So(err, ShouldBeNil) - So(str, ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE zot-cve-test 0.0.1 "+ - test.GetTestBlobDigest("zot-cve-test", "manifest").Encoded()[:8]+" false 75MB") + So(str, ShouldEqual, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE zot-cve-test 0.0.1 "+ + test.GetTestBlobDigest("zot-cve-test", "manifest").Encoded()[:8]+" linux/amd64 false 75MB") }) Convey("Test images by CVE ID - invalid CVE ID", t, func() { @@ -838,7 +840,7 @@ func TestServerCVEResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") str = strings.TrimSpace(str) So(err, ShouldBeNil) - So(str, ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") + So(str, ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") }) Convey("Test fixed tags by and image name CVE ID - positive", t, func() { @@ -872,7 +874,7 @@ func TestServerCVEResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") str = strings.TrimSpace(str) So(err, ShouldBeNil) - So(strings.TrimSpace(str), ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") + So(strings.TrimSpace(str), ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") }) Convey("Test fixed tags by and image name CVE ID - invalid image", t, func() { @@ -889,7 +891,7 @@ func TestServerCVEResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") str = strings.TrimSpace(str) So(err, ShouldNotBeNil) - So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") + So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") }) Convey("Test CVE by name and CVE ID - positive", t, func() { @@ -905,8 +907,8 @@ func TestServerCVEResponse(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(err, ShouldBeNil) - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE zot-cve-test 0.0.1 "+ - test.GetTestBlobDigest("zot-cve-test", "manifest").Encoded()[:8]+" false 75MB") + So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE zot-cve-test 0.0.1 "+ + test.GetTestBlobDigest("zot-cve-test", "manifest").Encoded()[:8]+" linux/amd64 false 75MB") }) Convey("Test CVE by name and CVE ID - invalid name and CVE ID", t, func() { @@ -922,7 +924,7 @@ func TestServerCVEResponse(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(err, ShouldBeNil) - So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") + So(strings.TrimSpace(str), ShouldNotContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") }) } @@ -1082,17 +1084,10 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo { CompareSeveritiesFn: func(severity1, severity2 string) int { return severities[severity2] - severities[severity1] }, - IsImageFormatScannableFn: func(image string) (bool, error) { + IsImageFormatScannableFn: func(repo string, reference string) (bool, error) { // Almost same logic compared to actual Trivy specific implementation - var imageDir string - - var inputTag string - - if strings.Contains(image, ":") { - imageDir, inputTag, _ = strings.Cut(image, ":") - } else { - imageDir = image - } + imageDir := repo + inputTag := reference repoMeta, err := repoDB.GetRepoMeta(imageDir) if err != nil { diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index acce9c19..dc4b4c59 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -185,7 +185,8 @@ func TestSearchImageCmd(t *testing.T) { err := cmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE dummyImageName tag 6e2f80bf false 123kB") + So(strings.TrimSpace(str), ShouldEqual, + "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE dummyImageName tag 6e2f80bf os/arch false 123kB") So(err, ShouldBeNil) }) @@ -201,7 +202,8 @@ func TestSearchImageCmd(t *testing.T) { err := imageCmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE dummyImageName tag 6e2f80bf false 123kB") + So(strings.TrimSpace(str), ShouldEqual, + "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE dummyImageName tag 6e2f80bf os/arch false 123kB") So(err, ShouldBeNil) Convey("using shorthand", func() { args := []string{"imagetest", "-n", "dummyImageName", "--url", "someUrlImage"} @@ -216,7 +218,8 @@ func TestSearchImageCmd(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE dummyImageName tag 6e2f80bf false 123kB") + So(strings.TrimSpace(str), ShouldEqual, + "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE dummyImageName tag 6e2f80bf os/arch false 123kB") So(err, ShouldBeNil) }) }) @@ -233,7 +236,8 @@ func TestSearchImageCmd(t *testing.T) { err := imageCmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE anImage tag 6e2f80bf false 123kB") + So(strings.TrimSpace(str), ShouldEqual, + "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE anImage tag 6e2f80bf os/arch false 123kB") So(err, ShouldBeNil) Convey("invalid URL format", func() { @@ -282,10 +286,10 @@ func TestSignature(t *testing.T) { repoName := "repo7" err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:1.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:1.0", }, url, repoName) So(err, ShouldBeNil) @@ -326,8 +330,8 @@ func TestSignature(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:1.0 6742241d true 447B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:1.0 6742241d linux/amd64 true 447B") t.Log("Test getting all images using rest calls to get catalog and individual manifests") cmd = MockNewImageCommand(new(searchService)) @@ -339,8 +343,8 @@ func TestSignature(t *testing.T) { So(err, ShouldBeNil) str = space.ReplaceAllString(buff.String(), " ") actual = strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:1.0 6742241d true 447B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:1.0 6742241d linux/amd64 true 447B") err = os.Chdir(currentWorkingDir) So(err, ShouldBeNil) @@ -374,10 +378,10 @@ func TestSignature(t *testing.T) { repoName := "repo7" err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "0.0.1", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "0.0.1", }, url, repoName) So(err, ShouldBeNil) @@ -403,8 +407,8 @@ func TestSignature(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 0.0.1 6742241d true 447B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 0.0.1 6742241d linux/amd64 true 447B") t.Log("Test getting all images using rest calls to get catalog and individual manifests") cmd = MockNewImageCommand(new(searchService)) @@ -416,8 +420,8 @@ func TestSignature(t *testing.T) { So(err, ShouldBeNil) str = space.ReplaceAllString(buff.String(), " ") actual = strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 0.0.1 6742241d true 447B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 0.0.1 6742241d linux/amd64 true 447B") err = os.Chdir(currentWorkingDir) So(err, ShouldBeNil) @@ -465,8 +469,8 @@ func TestDerivedImageList(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:1.0 2694fdb0 false 824B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:1.0 2694fdb0 N/A false 824B") }) Convey("Test derived images list fails", func() { @@ -538,8 +542,8 @@ func TestBaseImageList(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 3fc80493 false 494B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 3fc80493 N/A false 494B") }) Convey("Test base images list fail", func() { @@ -727,7 +731,8 @@ func TestOutputFormat(t *testing.T) { err := cmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIGNED SIZE dummyImageName tag 6e2f80bf false 123kB") + So(strings.TrimSpace(str), ShouldEqual, + "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE dummyImageName tag 6e2f80bf os/arch false 123kB") So(err, ShouldBeNil) }) @@ -746,10 +751,11 @@ func TestOutputFormat(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(strings.TrimSpace(str), ShouldEqual, `{ "repoName": "dummyImageName", "tag": "tag", `+ - `"configDigest": "sha256:4c10985c40365538426f2ba8cf0c21384a7769be502a550dcc0601b3736625e0", `+ + `"Manifests": [ { "configDigest": "sha256:4c10985c40365538426f2ba8cf0c21384a7769be502a550dcc0601b3736625e0", `+ `"digest": "sha256:6e2f80bf9cfaabad474fbaf8ad68fdb652f776ea80b63492ecca404e5f6446a6", `+ - `"layers": [ { "size": "0", `+ - `"digest": "sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6" } ], `+ + `"layers": [ { "size": "0", "digest": "sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6" } ], `+ //nolint:lll + `"platform": { "os": "os", "arch": "arch", "variant": "" }, `+ + `"size": "123445", "isSigned": false } ], `+ `"size": "123445", "isSigned": false }`) So(err, ShouldBeNil) }) @@ -770,9 +776,12 @@ func TestOutputFormat(t *testing.T) { strings.TrimSpace(str), ShouldEqual, `reponame: dummyImageName tag: tag `+ + `manifests: - `+ `configdigest: sha256:4c10985c40365538426f2ba8cf0c21384a7769be502a550dcc0601b3736625e0 `+ `digest: sha256:6e2f80bf9cfaabad474fbaf8ad68fdb652f776ea80b63492ecca404e5f6446a6 `+ `layers: - size: 0 digest: sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6 `+ + `platform: os: os arch: arch variant: "" `+ + `size: "123445" issigned: false `+ `size: "123445" issigned: false`, ) So(err, ShouldBeNil) @@ -796,9 +805,12 @@ func TestOutputFormat(t *testing.T) { strings.TrimSpace(str), ShouldEqual, `reponame: dummyImageName tag: tag `+ + `manifests: - `+ `configdigest: sha256:4c10985c40365538426f2ba8cf0c21384a7769be502a550dcc0601b3736625e0 `+ `digest: sha256:6e2f80bf9cfaabad474fbaf8ad68fdb652f776ea80b63492ecca404e5f6446a6 `+ `layers: - size: 0 digest: sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6 `+ + `platform: os: os arch: arch variant: "" `+ + `size: "123445" issigned: false `+ `size: "123445" issigned: false`, ) So(err, ShouldBeNil) @@ -855,9 +867,9 @@ func TestServerResponseGQL(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 false 492B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 linux/amd64 false 492B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 linux/amd64 false 492B") Convey("Test all images invalid output format", func() { args := []string{"imagetest", "-o", "random"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) @@ -888,14 +900,14 @@ func TestServerResponseGQL(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) // Actual cli output should be something similar to (order of images may differ): - // IMAGE NAME TAG DIGEST CONFIG SIGNED LAYERS SIZE - // repo7 test:2.0 a0ca253b b8781e88 false 492B - // b8781e88 15B - // repo7 test:1.0 a0ca253b b8781e88 false 492B - // b8781e88 15B - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST CONFIG SIGNED LAYERS SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 3a1d2d0c false 492B b8781e88 15B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 3a1d2d0c false 492B b8781e88 15B") + // IMAGE NAME TAG DIGEST CONFIG OS/ARCH SIGNED LAYERS SIZE + // repo7 test:2.0 a0ca253b b8781e88 linux/amd64 false 492B + // b8781e88 15B + // repo7 test:1.0 a0ca253b b8781e88 linux/amd64 false 492B + // b8781e88 15B + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST CONFIG OS/ARCH SIGNED LAYERS SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 3a1d2d0c linux/amd64 false 492B b8781e88 15B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 3a1d2d0c linux/amd64 false 492B b8781e88 15B") }) Convey("Test all images with debug flag", func() { @@ -913,9 +925,9 @@ func TestServerResponseGQL(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldContainSubstring, "GET") - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 false 492B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 linux/amd64 false 492B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 linux/amd64 false 492B") }) Convey("Test image by name config url", func() { @@ -932,9 +944,9 @@ func TestServerResponseGQL(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 false 492B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 linux/amd64 false 492B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 linux/amd64 false 492B") Convey("with shorthand", func() { args := []string{"imagetest", "-n", "repo7"} @@ -950,9 +962,9 @@ func TestServerResponseGQL(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 false 492B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 linux/amd64 false 492B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 linux/amd64 false 492B") }) Convey("invalid output format", func() { @@ -985,12 +997,12 @@ func TestServerResponseGQL(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) // Actual cli output should be something similar to (order of images may differ): - // IMAGE NAME TAG DIGEST SIZE - // repo7 test:2.0 a0ca253b 15B - // repo7 test:1.0 a0ca253b 15B - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 false 492B") + // IMAGE NAME TAG DIGEST OS/ARCH SIZE + // repo7 test:2.0 a0ca253b N/A 15B + // repo7 test:1.0 a0ca253b N/A 15B + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 N/A false 492B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 N/A false 492B") Convey("with shorthand", func() { args := []string{"imagetest", "-d", "883fc0c5"} @@ -1006,9 +1018,9 @@ func TestServerResponseGQL(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 false 492B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 N/A false 492B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 N/A false 492B") }) Convey("nonexistent digest", func() { @@ -1116,9 +1128,9 @@ func TestServerResponse(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 false 492B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 linux/amd64 false 492B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 linux/amd64 false 492B") }) Convey("Test all images verbose", func() { @@ -1136,14 +1148,14 @@ func TestServerResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) // Actual cli output should be something similar to (order of images may differ): - // IMAGE NAME TAG DIGEST CONFIG SIGNED LAYERS SIZE - // repo7 test:2.0 a0ca253b b8781e88 false 492B - // b8781e88 15B - // repo7 test:1.0 a0ca253b b8781e88 false 492B - // b8781e88 15B - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST CONFIG SIGNED LAYERS SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 3a1d2d0c false 492B b8781e88 15B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 3a1d2d0c false 492B b8781e88 15B") + // IMAGE NAME TAG DIGEST CONFIG OS/ARCH SIGNED LAYERS SIZE + // repo7 test:2.0 a0ca253b b8781e88 linux/amd64 false 492B + // linux/amd64 b8781e88 15B + // repo7 test:1.0 a0ca253b b8781e88 linux/amd64 false 492B + // linux/amd64 b8781e88 15B + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST CONFIG OS/ARCH SIGNED LAYERS SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 3a1d2d0c linux/amd64 false 492B b8781e88 15B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 3a1d2d0c linux/amd64 false 492B b8781e88 15B") }) Convey("Test image by name", func() { @@ -1160,9 +1172,9 @@ func TestServerResponse(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 false 492B") + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 linux/amd64 false 492B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 linux/amd64 false 492B") }) Convey("Test image by digest", func() { @@ -1180,12 +1192,12 @@ func TestServerResponse(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) // Actual cli output should be something similar to (order of images may differ): - // IMAGE NAME TAG DIGEST SIZE - // repo7 test:2.0 a0ca253b 492B - // repo7 test:1.0 a0ca253b 492B - So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 false 492B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 false 492B") + // IMAGE NAME TAG DIGEST OS/ARCH SIZE + // repo7 test:2.0 a0ca253b linux/amd64 492B + // repo7 test:1.0 a0ca253b linux/amd64 492B + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST OS/ARCH SIGNED SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 linux/amd64 false 492B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 linux/amd64 false 492B") Convey("nonexistent digest", func() { args := []string{"imagetest", "--digest", "d1g35t"} @@ -1538,12 +1550,17 @@ func (service mockService) getDerivedImageListGQL(ctx context.Context, config se imageListGQLResponse := &imageListStructForDerivedImagesGQL{} imageListGQLResponse.Data.Results = []imageStruct{ { - RepoName: "dummyImageName", - Tag: "tag", - Digest: godigest.FromString("Digest").String(), - ConfigDigest: godigest.FromString("ConfigDigest").String(), - Size: "123445", - Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + RepoName: "dummyImageName", + Tag: "tag", + Manifests: []manifestStruct{ + { + Digest: godigest.FromString("Digest").String(), + ConfigDigest: godigest.FromString("ConfigDigest").String(), + Size: "123445", + Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + }, + }, + Size: "123445", }, } @@ -1556,12 +1573,17 @@ func (service mockService) getBaseImageListGQL(ctx context.Context, config searc imageListGQLResponse := &imageListStructForBaseImagesGQL{} imageListGQLResponse.Data.Results = []imageStruct{ { - RepoName: "dummyImageName", - Tag: "tag", - Digest: godigest.FromString("Digest").String(), - ConfigDigest: godigest.FromString("ConfigDigest").String(), - Size: "123445", - Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + RepoName: "dummyImageName", + Tag: "tag", + Manifests: []manifestStruct{ + { + Digest: godigest.FromString("Digest").String(), + ConfigDigest: godigest.FromString("ConfigDigest").String(), + Size: "123445", + Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + }, + }, + Size: "123445", }, } @@ -1574,12 +1596,17 @@ func (service mockService) getImagesGQL(ctx context.Context, config searchConfig imageListGQLResponse := &imageListStructGQL{} imageListGQLResponse.Data.Results = []imageStruct{ { - RepoName: "dummyImageName", - Tag: "tag", - Digest: godigest.FromString("Digest").String(), - ConfigDigest: godigest.FromString("ConfigDigest").String(), - Size: "123445", - Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + RepoName: "dummyImageName", + Tag: "tag", + Manifests: []manifestStruct{ + { + Digest: godigest.FromString("Digest").String(), + ConfigDigest: godigest.FromString("ConfigDigest").String(), + Size: "123445", + Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + }, + }, + Size: "123445", }, } @@ -1592,12 +1619,17 @@ func (service mockService) getImagesByDigestGQL(ctx context.Context, config sear imageListGQLResponse := &imageListStructForDigestGQL{} imageListGQLResponse.Data.Results = []imageStruct{ { - RepoName: "randomimageName", - Tag: "tag", - Digest: godigest.FromString("Digest").String(), - ConfigDigest: godigest.FromString("ConfigDigest").String(), - Size: "123445", - Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + RepoName: "randomimageName", + Tag: "tag", + Manifests: []manifestStruct{ + { + Digest: godigest.FromString("Digest").String(), + ConfigDigest: godigest.FromString("ConfigDigest").String(), + Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + Size: "123445", + }, + }, + Size: "123445", }, } @@ -1691,10 +1723,15 @@ func (service mockService) getMockedImageByName(imageName string) imageStruct { image := imageStruct{} image.RepoName = imageName image.Tag = "tag" - image.Digest = godigest.FromString("Digest").String() - image.ConfigDigest = godigest.FromString("ConfigDigest").String() + image.Manifests = []manifestStruct{ + { + Digest: godigest.FromString("Digest").String(), + ConfigDigest: godigest.FromString("ConfigDigest").String(), + Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + Size: "123445", + }, + } image.Size = "123445" - image.Layers = []layer{{Digest: godigest.FromString("LayerDigest").String()}} return image } @@ -1708,12 +1745,18 @@ func (service mockService) getAllImages(ctx context.Context, config searchConfig image := &imageStruct{} image.RepoName = "randomimageName" image.Tag = "tag" - image.Digest = godigest.FromString("Digest").String() - image.ConfigDigest = godigest.FromString("ConfigDigest").String() + image.Manifests = []manifestStruct{ + { + Digest: godigest.FromString("Digest").String(), + ConfigDigest: godigest.FromString("ConfigDigest").String(), + Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + Size: "123445", + Platform: platform{Os: "os", Arch: "arch"}, + }, + } image.Size = "123445" - image.Layers = []layer{{Digest: godigest.FromString("LayerDigest").String()}} - str, err := image.string(*config.outputFormat, len(image.RepoName), len(image.Tag)) + str, err := image.string(*config.outputFormat, len(image.RepoName), len(image.Tag), len("os/Arch")) if err != nil { channel <- stringResult{"", err} @@ -1732,12 +1775,18 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf image := &imageStruct{} image.RepoName = imageName image.Tag = "tag" - image.Digest = godigest.FromString("Digest").String() - image.ConfigDigest = godigest.FromString("ConfigDigest").String() + image.Manifests = []manifestStruct{ + { + Digest: godigest.FromString("Digest").String(), + ConfigDigest: godigest.FromString("ConfigDigest").String(), + Layers: []layer{{Digest: godigest.FromString("LayerDigest").String()}}, + Size: "123445", + Platform: platform{Os: "os", Arch: "arch"}, + }, + } image.Size = "123445" - image.Layers = []layer{{Digest: godigest.FromString("LayerDigest").String()}} - str, err := image.string(*config.outputFormat, len(image.RepoName), len(image.Tag)) + str, err := image.string(*config.outputFormat, len(image.RepoName), len(image.Tag), len("os/Arch")) if err != nil { channel <- stringResult{"", err} diff --git a/pkg/cli/searcher.go b/pkg/cli/searcher.go index 0eaf8936..3a556625 100644 --- a/pkg/cli/searcher.go +++ b/pkg/cli/searcher.go @@ -359,7 +359,7 @@ func (search cveByImageSearcherGQL) search(config searchConfig) (bool, error) { if len(cveList.Data.CVEListForImage.CVEList) > 0 && (*config.outputFormat == defaultOutoutFormat || *config.outputFormat == "") { - printCVETableHeader(&builder, *config.verbose, 0, 0) + printCVETableHeader(&builder, *config.verbose, 0, 0, 0) fmt.Fprint(config.resultWriter, builder.String()) } @@ -596,7 +596,7 @@ func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan strin if !foundResult && (*config.outputFormat == defaultOutoutFormat || *config.outputFormat == "") { var builder strings.Builder - printHeader(&builder, *config.verbose, 0, 0) + printHeader(&builder, *config.verbose, 0, 0, 0) fmt.Fprint(config.resultWriter, builder.String()) } @@ -696,13 +696,14 @@ type stringResult struct { Err error } -type printHeader func(writer io.Writer, verbose bool, maxImageNameLen, maxTagLen int) +type printHeader func(writer io.Writer, verbose bool, maxImageNameLen, maxTagLen, maxPlatformLen int) -func printImageTableHeader(writer io.Writer, verbose bool, maxImageNameLen, maxTagLen int) { +func printImageTableHeader(writer io.Writer, verbose bool, maxImageNameLen, maxTagLen, maxPlatformLen int) { table := getImageTableWriter(writer) table.SetColMinWidth(colImageNameIndex, imageNameWidth) table.SetColMinWidth(colTagIndex, tagWidth) + table.SetColMinWidth(colPlatformIndex, platformWidth) table.SetColMinWidth(colDigestIndex, digestWidth) table.SetColMinWidth(colSizeIndex, sizeWidth) table.SetColMinWidth(colIsSignedIndex, isSignedWidth) @@ -712,7 +713,7 @@ func printImageTableHeader(writer io.Writer, verbose bool, maxImageNameLen, maxT table.SetColMinWidth(colLayersIndex, layersWidth) } - row := make([]string, 7) //nolint:gomnd + row := make([]string, 8) //nolint:gomnd // adding spaces so that image name and tag columns are aligned // in case the name/tag are fully shown and too long @@ -731,6 +732,13 @@ func printImageTableHeader(writer io.Writer, verbose bool, maxImageNameLen, maxT row[colTagIndex] = "TAG" } + if maxPlatformLen > len("OS/ARCH") { + offset = strings.Repeat(" ", maxPlatformLen-len("OS/ARCH")) + row[colPlatformIndex] = "OS/ARCH" + offset + } else { + row[colPlatformIndex] = "OS/ARCH" + } + row[colDigestIndex] = "DIGEST" row[colSizeIndex] = "SIZE" row[colIsSignedIndex] = "SIGNED" @@ -744,7 +752,7 @@ func printImageTableHeader(writer io.Writer, verbose bool, maxImageNameLen, maxT table.Render() } -func printCVETableHeader(writer io.Writer, verbose bool, maxImgLen, maxTagLen int) { +func printCVETableHeader(writer io.Writer, verbose bool, maxImgLen, maxTagLen, maxPlatformLen int) { table := getCVETableWriter(writer) row := make([]string, 3) //nolint:gomnd row[colCVEIDIndex] = "ID" @@ -759,6 +767,7 @@ func printResult(config searchConfig, imageList []imageStruct) error { var builder strings.Builder maxImgNameLen := 0 maxTagLen := 0 + maxPlatformLen := 0 if len(imageList) > 0 { for i := range imageList { @@ -769,9 +778,17 @@ func printResult(config searchConfig, imageList []imageStruct) error { if maxTagLen < len(imageList[i].Tag) { maxTagLen = len(imageList[i].Tag) } + + for j := range imageList[i].Manifests { + platform := imageList[i].Manifests[j].Platform.Os + "/" + imageList[i].Manifests[j].Platform.Arch + + if maxPlatformLen < len(platform) { + maxPlatformLen = len(platform) + } + } } - printImageTableHeader(&builder, *config.verbose, maxImgNameLen, maxTagLen) + printImageTableHeader(&builder, *config.verbose, maxImgNameLen, maxTagLen, maxPlatformLen) fmt.Fprint(config.resultWriter, builder.String()) } @@ -779,7 +796,7 @@ func printResult(config searchConfig, imageList []imageStruct) error { img := imageList[i] img.verbose = *config.verbose - out, err := img.string(*config.outputFormat, maxImgNameLen, maxTagLen) + out, err := img.string(*config.outputFormat, maxImgNameLen, maxTagLen, maxPlatformLen) if err != nil { return err } diff --git a/pkg/cli/service.go b/pkg/cli/service.go index c6bc0266..1c6a0c7b 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -74,9 +74,13 @@ func (service searchService) getDerivedImageListGQL(ctx context.Context, config Results{ RepoName, Tag, - Digest, - ConfigDigest, - Layers {Size Digest}, + Manifests { + Digest, + ConfigDigest, + Layers {Size Digest}, + LastUpdated, + Size + }, LastUpdated, IsSigned, Size @@ -103,9 +107,13 @@ func (service searchService) getBaseImageListGQL(ctx context.Context, config sea Results{ RepoName, Tag, - Digest, - ConfigDigest, - Layers {Size Digest}, + Manifests { + Digest, + ConfigDigest, + Layers {Size Digest}, + LastUpdated, + Size + }, LastUpdated, IsSigned, Size @@ -126,9 +134,23 @@ func (service searchService) getBaseImageListGQL(ctx context.Context, config sea func (service searchService) getImagesGQL(ctx context.Context, config searchConfig, username, password string, imageName string, ) (*imageListStructGQL, error) { - query := fmt.Sprintf(`{ImageList(repo: "%s") { Results {`+` - RepoName Tag Digest ConfigDigest Size Layers {Size Digest} IsSigned}} - }`, + query := fmt.Sprintf(` + { + ImageList(repo: "%s") { + Results { + RepoName Tag + Manifests { + Digest + ConfigDigest + Size + Platform {Os Arch} + Layers {Size Digest} + } + Size + IsSigned + } + } + }`, imageName) result := &imageListStructGQL{} @@ -144,9 +166,22 @@ func (service searchService) getImagesGQL(ctx context.Context, config searchConf func (service searchService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string, digest string, ) (*imageListStructForDigestGQL, error) { - query := fmt.Sprintf(`{ImageListForDigest(id: "%s") { Results{`+` - RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}} - }`, + query := fmt.Sprintf(` + { + ImageListForDigest(id: "%s") { + Results { + RepoName Tag + Manifests { + Digest + ConfigDigest + Size + Layers {Size Digest} + } + Size + IsSigned + } + } + }`, digest) result := &imageListStructForDigestGQL{} @@ -162,9 +197,22 @@ func (service searchService) getImagesByDigestGQL(ctx context.Context, config se func (service searchService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password, cveID string, ) (*imagesForCve, error) { - query := fmt.Sprintf(`{ImageListForCVE(id: "%s") { Results {`+` - RepoName Tag Digest ConfigDigest Layers {Size Digest} Size}} - }`, + query := fmt.Sprintf(` + { + ImageListForCVE(id: "%s") { + Results { + RepoName Tag + Manifests { + Digest + ConfigDigest + Size + Layers {Size Digest} + } + Size + IsSigned + } + } + }`, cveID) result := &imagesForCve{} @@ -199,9 +247,21 @@ func (service searchService) getCveByImageGQL(ctx context.Context, config search func (service searchService) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string, ) (*imagesForCve, error) { - query := fmt.Sprintf(`{ImageListForCVE(id: "%s") { Results {`+` - RepoName Tag Digest ConfigDigest Layers {Size Digest} Size}} - }`, + query := fmt.Sprintf(` + { + ImageListForCVE(id: "%s") { + Results { + RepoName Tag + Manifests { + Digest + ConfigDigest + Size + Layers {Size Digest} + } + Size + } + } + }`, cveID) result := &imagesForCve{} @@ -217,9 +277,21 @@ func (service searchService) getTagsForCVEGQL(ctx context.Context, config search func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string, ) (*fixedTags, error) { - query := fmt.Sprintf(`{ImageListWithCVEFixed(id: "%s", image: "%s") { Results {`+` - RepoName Tag Digest ConfigDigest Layers {Size Digest} Size}} - }`, + query := fmt.Sprintf(` + { + ImageListWithCVEFixed(id: "%s", image: "%s") { + Results { + RepoName Tag + Manifests { + Digest + ConfigDigest + Size + Layers {Size Digest} + } + Size + } + } + }`, cveID, imageName) result := &fixedTags{} @@ -349,10 +421,23 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search defer wtgrp.Done() defer close(rch) - query := fmt.Sprintf(`{ImageListForCVE(id: "%s") { Results {`+` - RepoName Tag Digest ConfigDigest Layers {Size Digest} Size}} - }`, + query := fmt.Sprintf( + `{ + ImageListForCVE(id: "%s") { + Results { + RepoName Tag + Manifests { + Digest + ConfigDigest + Size + Layers {Size Digest} + } + Size + } + } + }`, cvid) + result := &imagesForCve{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) @@ -402,10 +487,23 @@ func (service searchService) getImagesByDigest(ctx context.Context, config searc defer wtgrp.Done() defer close(rch) - query := fmt.Sprintf(`{ImageListForDigest(id: "%s") { Results {`+` - RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}} - }`, + query := fmt.Sprintf( + `{ + ImageListForDigest(id: "%s") { + Results { + RepoName Tag + Manifests { + Digest + ConfigDigest + Size + Layers {Size Digest} + } + Size + } + } + }`, digest) + result := &imagesForDigest{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) @@ -455,10 +553,23 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config defer wtgrp.Done() defer close(rch) - query := fmt.Sprintf(`{ImageListForCVE(id: "%s") { Results {`+` - RepoName Tag Digest ConfigDigest Size Layers {Size Digest}}} - }`, + query := fmt.Sprintf( + `{ + ImageListForCVE(id: "%s") { + Results { + RepoName Tag + Manifests { + Digest + ConfigDigest + Size + Layers {Size Digest} + } + Size + } + } + }`, cvid) + result := &imagesForCve{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) @@ -566,10 +677,22 @@ func (service searchService) getFixedTagsForCVE(ctx context.Context, config sear defer wtgrp.Done() defer close(rch) - query := fmt.Sprintf(`{ImageListWithCVEFixed (id: "%s", image: "%s") { Results {`+` - RepoName Tag Digest ConfigDigest Layers {Size Digest} Size}} - }`, - cvid, imageName) + query := fmt.Sprintf(` + { + ImageListWithCVEFixed (id: "%s", image: "%s") { + Results { + RepoName Tag + Manifests { + Digest + ConfigDigest + Size + Layers {Size Digest} + } + Size + } + } + }`, cvid, imageName) + result := &fixedTags{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) @@ -719,8 +842,6 @@ func addManifestCallToPool(ctx context.Context, config searchConfig, pool *reque ) { defer wtgrp.Done() - resultManifest := manifestResponse{} - manifestEndpoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/manifests/%s", imageName, tagName)) if err != nil { @@ -730,14 +851,13 @@ func addManifestCallToPool(ctx context.Context, config searchConfig, pool *reque rch <- stringResult{"", err} } - job := manifestJob{ - url: manifestEndpoint, - username: username, - imageName: imageName, - password: password, - tagName: tagName, - manifestResp: resultManifest, - config: config, + job := httpJob{ + url: manifestEndpoint, + username: username, + imageName: imageName, + password: password, + tagName: tagName, + config: config, } wtgrp.Add(1) @@ -860,14 +980,27 @@ type PaginatedImagesResult struct { } type imageStruct struct { - RepoName string `json:"repoName"` - Tag string `json:"tag"` - ConfigDigest string `json:"configDigest"` - Digest string `json:"digest"` - Layers []layer `json:"layers"` - Size string `json:"size"` - verbose bool - IsSigned bool `json:"isSigned"` + RepoName string `json:"repoName"` + Tag string `json:"tag"` + Manifests []manifestStruct + Size string `json:"size"` + verbose bool + IsSigned bool `json:"isSigned"` +} + +type manifestStruct struct { + ConfigDigest string `json:"configDigest"` + Digest string `json:"digest"` + Layers []layer `json:"layers"` + Platform platform `json:"platform"` + Size string `json:"size"` + IsSigned bool `json:"isSigned"` +} + +type platform struct { + Os string `json:"os"` + Arch string `json:"arch"` + Variant string `json:"variant"` } type DerivedImageList struct { @@ -913,14 +1046,14 @@ type imagesForDigest struct { } type layer struct { - Size uint64 `json:"size,string"` + Size int64 `json:"size,string"` Digest string `json:"digest"` } -func (img imageStruct) string(format string, maxImgNameLen, maxTagLen int) (string, error) { +func (img imageStruct) string(format string, maxImgNameLen, maxTagLen, maxPlatformLen int) (string, error) { switch strings.ToLower(format) { case "", defaultOutoutFormat: - return img.stringPlainText(maxImgNameLen, maxTagLen) + return img.stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen) case "json": return img.stringJSON() case "yml", "yaml": @@ -930,14 +1063,14 @@ func (img imageStruct) string(format string, maxImgNameLen, maxTagLen int) (stri } } -func (img imageStruct) stringPlainText(maxImgNameLen, maxTagLen int) (string, error) { +func (img imageStruct) stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen int) (string, error) { var builder strings.Builder table := getImageTableWriter(&builder) table.SetColMinWidth(colImageNameIndex, maxImgNameLen) table.SetColMinWidth(colTagIndex, maxTagLen) - + table.SetColMinWidth(colPlatformIndex, platformWidth) table.SetColMinWidth(colDigestIndex, digestWidth) table.SetColMinWidth(colSizeIndex, sizeWidth) table.SetColMinWidth(colIsSignedIndex, isSignedWidth) @@ -952,57 +1085,89 @@ func (img imageStruct) stringPlainText(maxImgNameLen, maxTagLen int) (string, er imageName = img.RepoName tagName = img.Tag - manifestDigest, err := godigest.Parse(img.Digest) - if err != nil { - return "", fmt.Errorf("error parsing manifest digest %s: %w", img.Digest, err) + if imageNameWidth > maxImgNameLen { + maxImgNameLen = imageNameWidth } - configDigest, err := godigest.Parse(img.ConfigDigest) - if err != nil { - return "", fmt.Errorf("error parsing config digest %s: %w", img.ConfigDigest, err) + if tagWidth > maxTagLen { + maxTagLen = tagWidth } - minifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "") - configDigestStr := ellipsize(configDigest.Encoded(), configWidth, "") - imgSize, _ := strconv.ParseUint(img.Size, 10, 64) - size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis) - isSigned := img.IsSigned - row := make([]string, 7) //nolint:gomnd - - row[colImageNameIndex] = imageName - row[colTagIndex] = tagName - row[colDigestIndex] = minifestDigestStr - row[colSizeIndex] = size - row[colIsSignedIndex] = strconv.FormatBool(isSigned) - - if img.verbose { - row[colConfigIndex] = configDigestStr - row[colLayersIndex] = "" + // adding spaces so that image name and tag columns are aligned + // in case the name/tag are fully shown and too long + var offset string + if maxImgNameLen > len(imageName) { + offset = strings.Repeat(" ", maxImgNameLen-len(imageName)) + imageName += offset } - table.Append(row) + if maxTagLen > len(tagName) { + offset = strings.Repeat(" ", maxTagLen-len(tagName)) + tagName += offset + } - if img.verbose { - for _, entry := range img.Layers { - layerSize := entry.Size - size := ellipsize(strings.ReplaceAll(humanize.Bytes(layerSize), " ", ""), sizeWidth, ellipsis) + for i := range img.Manifests { + manifestDigest, err := godigest.Parse(img.Manifests[i].Digest) + if err != nil { + return "", fmt.Errorf("error parsing manifest digest %s: %w", img.Manifests[i].Digest, err) + } - layerDigest, err := godigest.Parse(entry.Digest) - if err != nil { - return "", fmt.Errorf("error parsing layer digest %s: %w", entry.Digest, err) + configDigest, err := godigest.Parse(img.Manifests[i].ConfigDigest) + if err != nil { + return "", fmt.Errorf("error parsing config digest %s: %w", img.Manifests[i].ConfigDigest, err) + } + + platform := getPlatformStr(img.Manifests[i].Platform) + + if maxPlatformLen > len(platform) { + offset = strings.Repeat(" ", maxPlatformLen-len(platform)) + platform += offset + } + + minifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "") + configDigestStr := ellipsize(configDigest.Encoded(), configWidth, "") + imgSize, _ := strconv.ParseUint(img.Manifests[i].Size, 10, 64) + size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis) + isSigned := img.IsSigned + row := make([]string, 8) //nolint:gomnd + + row[colImageNameIndex] = imageName + row[colTagIndex] = tagName + row[colDigestIndex] = minifestDigestStr + row[colPlatformIndex] = platform + row[colSizeIndex] = size + row[colIsSignedIndex] = strconv.FormatBool(isSigned) + + if img.verbose { + row[colConfigIndex] = configDigestStr + row[colLayersIndex] = "" + } + + table.Append(row) + + if img.verbose { + for _, entry := range img.Manifests[i].Layers { + layerSize := entry.Size + size := ellipsize(strings.ReplaceAll(humanize.Bytes(uint64(layerSize)), " ", ""), sizeWidth, ellipsis) + + layerDigest, err := godigest.Parse(entry.Digest) + if err != nil { + return "", fmt.Errorf("error parsing layer digest %s: %w", entry.Digest, err) + } + + layerDigestStr := ellipsize(layerDigest.Encoded(), digestWidth, "") + + layerRow := make([]string, 8) //nolint:gomnd + layerRow[colImageNameIndex] = "" + layerRow[colTagIndex] = "" + layerRow[colDigestIndex] = "" + layerRow[colPlatformIndex] = "" + layerRow[colSizeIndex] = size + layerRow[colConfigIndex] = "" + layerRow[colLayersIndex] = layerDigestStr + + table.Append(layerRow) } - - layerDigestStr := ellipsize(layerDigest.Encoded(), digestWidth, "") - - layerRow := make([]string, 7) //nolint:gomnd - layerRow[colImageNameIndex] = "" - layerRow[colTagIndex] = "" - layerRow[colDigestIndex] = "" - layerRow[colSizeIndex] = size - layerRow[colConfigIndex] = "" - layerRow[colLayersIndex] = layerDigestStr - - table.Append(layerRow) } } @@ -1011,6 +1176,25 @@ func (img imageStruct) stringPlainText(maxImgNameLen, maxTagLen int) (string, er return builder.String(), nil } +func getPlatformStr(platf platform) string { + if platf.Arch == "" && platf.Os == "" { + return "N/A" + } + + platform := platf.Os + + if platf.Arch != "" { + platform = platform + "/" + platf.Arch + platform = strings.Trim(platform, "/") + + if platf.Variant != "" { + platform = platform + "/" + platf.Variant + } + } + + return platform +} + func (img imageStruct) stringJSON() (string, error) { json := jsoniter.ConfigCompatibleWithStandardLibrary @@ -1035,25 +1219,6 @@ type catalogResponse struct { Repositories []string `json:"repositories"` } -//nolint:tagliatelle -type manifestResponse struct { - Layers []struct { - MediaType string `json:"mediaType"` - Digest string `json:"digest"` - Size uint64 `json:"size"` - } `json:"layers"` - Annotations struct { - WsTychoStackerStackerYaml string `json:"ws.tycho.stacker.stacker_yaml"` - WsTychoStackerGitVersion string `json:"ws.tycho.stacker.git_version"` - } `json:"annotations"` - Config struct { - Size int `json:"size"` - Digest string `json:"digest"` - MediaType string `json:"mediaType"` - } `json:"config"` - SchemaVersion int `json:"schemaVersion"` -} - func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) { if !isURL(serverURL) { return "", zotErrors.ErrInvalidURL @@ -1157,9 +1322,10 @@ func (service searchService) getRepos(ctx context.Context, config searchConfig, } const ( - imageNameWidth = 32 - tagWidth = 24 + imageNameWidth = 10 + tagWidth = 8 digestWidth = 8 + platformWidth = 14 sizeWidth = 8 isSignedWidth = 8 configWidth = 8 @@ -1170,9 +1336,10 @@ const ( colTagIndex = 1 colDigestIndex = 2 colConfigIndex = 3 - colIsSignedIndex = 4 - colLayersIndex = 5 - colSizeIndex = 6 + colPlatformIndex = 4 + colIsSignedIndex = 5 + colLayersIndex = 6 + colSizeIndex = 7 cveIDWidth = 16 cveSeverityWidth = 8 diff --git a/pkg/compliance/v1_0_0/check.go b/pkg/compliance/v1_0_0/check.go index fca2fb7f..02f30355 100644 --- a/pkg/compliance/v1_0_0/check.go +++ b/pkg/compliance/v1_0_0/check.go @@ -490,10 +490,10 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { repoName := "repo7" err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:1.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:1.0", }, baseURL, repoName) So(err, ShouldBeNil) @@ -504,10 +504,10 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:1.0.1", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:1.0.1", }, baseURL, repoName) So(err, ShouldBeNil) @@ -522,10 +522,10 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:2.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:2.0", }, baseURL, repoName) So(err, ShouldBeNil) @@ -601,10 +601,10 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { repoName := "page0" err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: fmt.Sprintf("test:%d.0", index), + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: fmt.Sprintf("test:%d.0", index), }, baseURL, repoName) So(err, ShouldBeNil) @@ -742,10 +742,10 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { // subpath firsttest err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:1.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:1.0", }, baseURL, "firsttest/first") So(err, ShouldBeNil) @@ -757,10 +757,10 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { // subpath secondtest err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:1.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:1.0", }, baseURL, "secondtest/second") So(err, ShouldBeNil) @@ -776,10 +776,10 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { // subpath firsttest err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:2.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:2.0", }, baseURL, "firsttest/first") So(err, ShouldBeNil) @@ -791,10 +791,10 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { // subpath secondtest err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "test:2.0", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "test:2.0", }, baseURL, "secondtest/second") So(err, ShouldBeNil) diff --git a/pkg/extensions/extension_ui_test.go b/pkg/extensions/extension_ui_test.go index 82df5c84..fd77030c 100644 --- a/pkg/extensions/extension_ui_test.go +++ b/pkg/extensions/extension_ui_test.go @@ -66,10 +66,10 @@ func TestUIExtension(t *testing.T) { // Upload a test image err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: tagName, + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: tagName, }, baseURL, repoName) So(err, ShouldBeNil) diff --git a/pkg/extensions/lint/lint_test.go b/pkg/extensions/lint/lint_test.go index 3ddcfb8f..8e6b5d25 100644 --- a/pkg/extensions/lint/lint_test.go +++ b/pkg/extensions/lint/lint_test.go @@ -418,6 +418,11 @@ func TestVerifyMandatoryAnnotations(t *testing.T) { test.CopyTestFiles("../../../test/data", dir) + files, err := os.ReadDir(dir) + So(err, ShouldBeNil) + + t.Log("Files in dir:", dir, ": ", files) + ctlr.Config.Storage.RootDirectory = dir cm := test.NewControllerManager(ctlr) diff --git a/pkg/extensions/search/common/common.go b/pkg/extensions/search/common/common.go index dceae749..a695a68c 100644 --- a/pkg/extensions/search/common/common.go +++ b/pkg/extensions/search/common/common.go @@ -24,10 +24,15 @@ const ( LabelAnnotationSource = "org.label-schema.vcs-url" ) -type TagInfo struct { - Name string +type Descriptor struct { Digest godigest.Digest - Timestamp time.Time + MediaType string +} + +type TagInfo struct { + Name string + Descriptor Descriptor + Timestamp time.Time } func GetRootDir(image string, storeController storage.StoreController) string { @@ -78,6 +83,33 @@ func GetImageDirAndTag(imageName string) (string, string) { return imageDir, imageTag } +func GetImageDirAndDigest(imageName string) (string, string) { + var imageDir string + + var imageDigest string + + if strings.Contains(imageName, "@") { + imageDir, imageDigest, _ = strings.Cut(imageName, "@") + } else { + imageDir = imageName + } + + return imageDir, imageDigest +} + +// GetImageDirAndReference returns the repo, digest and isTag. +func GetImageDirAndReference(imageName string) (string, string, bool) { + if strings.Contains(imageName, "@") { + repo, digest := GetImageDirAndDigest(imageName) + + return repo, digest, false + } + + repo, tag := GetImageDirAndTag(imageName) + + return repo, tag, true +} + // GetImageLastUpdated This method will return last updated timestamp. // The Created timestamp is used, but if it is missing, look at the // history field and, if provided, return the timestamp of last entry in history. @@ -277,3 +309,9 @@ func GetAnnotations(annotations, labels map[string]string) ImageAnnotations { Authors: authors, } } + +func ReferenceIsDigest(reference string) bool { + _, err := godigest.Parse(reference) + + return err == nil +} diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index a33458db..67a0c4cd 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -150,23 +150,35 @@ func getTags() ([]common.TagInfo, []common.TagInfo) { tags := make([]common.TagInfo, 0) firstTag := common.TagInfo{ - Name: "1.0.0", - Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb", + Name: "1.0.0", + Descriptor: common.Descriptor{ + Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb", + MediaType: ispec.MediaTypeImageManifest, + }, Timestamp: time.Now(), } secondTag := common.TagInfo{ - Name: "1.0.1", - Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb", + Name: "1.0.1", + Descriptor: common.Descriptor{ + Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb", + MediaType: ispec.MediaTypeImageManifest, + }, Timestamp: time.Now(), } thirdTag := common.TagInfo{ - Name: "1.0.2", - Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb", + Name: "1.0.2", + Descriptor: common.Descriptor{ + Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb", + MediaType: ispec.MediaTypeImageManifest, + }, Timestamp: time.Now(), } fourthTag := common.TagInfo{ - Name: "1.0.3", - Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb", + Name: "1.0.3", + Descriptor: common.Descriptor{ + Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb", + MediaType: ispec.MediaTypeImageManifest, + }, Timestamp: time.Now(), } @@ -242,44 +254,51 @@ func verifyImageSummaryFields(t *testing.T, So(actualImageSummary.Size, ShouldEqual, expectedImageSummary.Size) So(actualImageSummary.IsSigned, ShouldEqual, expectedImageSummary.IsSigned) So(actualImageSummary.Vendor, ShouldEqual, expectedImageSummary.Vendor) - So(actualImageSummary.Platform.Os, ShouldEqual, expectedImageSummary.Platform.Os) - So(actualImageSummary.Platform.Arch, ShouldEqual, expectedImageSummary.Platform.Arch) So(actualImageSummary.Title, ShouldEqual, expectedImageSummary.Title) So(actualImageSummary.Description, ShouldEqual, expectedImageSummary.Description) So(actualImageSummary.Source, ShouldEqual, expectedImageSummary.Source) So(actualImageSummary.Documentation, ShouldEqual, expectedImageSummary.Documentation) So(actualImageSummary.Licenses, ShouldEqual, expectedImageSummary.Licenses) - So(len(actualImageSummary.History), ShouldEqual, len(expectedImageSummary.History)) - for index, history := range actualImageSummary.History { - // Digest could be empty string if the history entry is not associated with a layer - So(history.Layer.Digest, ShouldEqual, expectedImageSummary.History[index].Layer.Digest) - So(history.Layer.Size, ShouldEqual, expectedImageSummary.History[index].Layer.Size) - So( - history.HistoryDescription.Author, - ShouldEqual, - expectedImageSummary.History[index].HistoryDescription.Author, - ) - So( - history.HistoryDescription.Created, - ShouldEqual, - expectedImageSummary.History[index].HistoryDescription.Created, - ) - So( - history.HistoryDescription.CreatedBy, - ShouldEqual, - expectedImageSummary.History[index].HistoryDescription.CreatedBy, - ) - So( - history.HistoryDescription.EmptyLayer, - ShouldEqual, - expectedImageSummary.History[index].HistoryDescription.EmptyLayer, - ) - So( - history.HistoryDescription.Comment, - ShouldEqual, - expectedImageSummary.History[index].HistoryDescription.Comment, - ) + So(len(actualImageSummary.Manifests), ShouldEqual, len(expectedImageSummary.Manifests)) + + for i := range actualImageSummary.Manifests { + So(actualImageSummary.Manifests[i].Platform.Os, ShouldEqual, expectedImageSummary.Manifests[i].Platform.Os) + So(actualImageSummary.Manifests[i].Platform.Arch, ShouldEqual, expectedImageSummary.Manifests[i].Platform.Arch) + So(len(actualImageSummary.Manifests[i].History), ShouldEqual, len(expectedImageSummary.Manifests[i].History)) + + expectedHistories := expectedImageSummary.Manifests[i].History + + for index, history := range actualImageSummary.Manifests[i].History { + // Digest could be empty string if the history entry is not associated with a layer + So(history.Layer.Digest, ShouldEqual, expectedHistories[index].Layer.Digest) + So(history.Layer.Size, ShouldEqual, expectedHistories[index].Layer.Size) + So( + history.HistoryDescription.Author, + ShouldEqual, + expectedHistories[index].HistoryDescription.Author, + ) + So( + history.HistoryDescription.Created, + ShouldEqual, + expectedHistories[index].HistoryDescription.Created, + ) + So( + history.HistoryDescription.CreatedBy, + ShouldEqual, + expectedHistories[index].HistoryDescription.CreatedBy, + ) + So( + history.HistoryDescription.EmptyLayer, + ShouldEqual, + expectedHistories[index].HistoryDescription.EmptyLayer, + ) + So( + history.HistoryDescription.Comment, + ShouldEqual, + expectedHistories[index].HistoryDescription.Comment, + ) + } } } @@ -323,10 +342,10 @@ func uploadNewRepoTag(tag string, repoName string, baseURL string, layers [][]by err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: tag, + Manifest: manifest, + Config: config, + Layers: layers, + Reference: tag, }, baseURL, repoName, @@ -423,17 +442,10 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo { CompareSeveritiesFn: func(severity1, severity2 string) int { return severities[severity2] - severities[severity1] }, - IsImageFormatScannableFn: func(image string) (bool, error) { + IsImageFormatScannableFn: func(repo string, reference string) (bool, error) { // Almost same logic compared to actual Trivy specific implementation - var imageDir string - - var inputTag string - - if strings.Contains(image, ":") { - imageDir, inputTag, _ = strings.Cut(image, ":") - } else { - imageDir = image - } + imageDir := repo + inputTag := reference repoMeta, err := repoDB.GetRepoMeta(imageDir) if err != nil { @@ -956,10 +968,10 @@ func TestGetReferrersGQL(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "1.0", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "1.0", }, baseURL, repo) @@ -1084,10 +1096,10 @@ func TestExpandedRepoInfo(t *testing.T) { err = WriteImageToFileSystem( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: fmt.Sprintf("%d.0", i), + Manifest: manifest, + Config: config, + Layers: layers, + Reference: fmt.Sprintf("%d.0", i), }, repo1, storeController) @@ -1121,14 +1133,34 @@ func TestExpandedRepoInfo(t *testing.T) { ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() - query := "{ExpandedRepoInfo(repo:\"test1\"){Summary%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20Vendors%20Score}%20Images%20{Digest%20IsSigned%20Tag%20Layers%20{Size%20Digest}}}}" //nolint: lll + query := `{ + ExpandedRepoInfo(repo:"test1"){ + Summary { + Name LastUpdated Size + Platforms {Os Arch} + Vendors + } + Images { + Tag + Manifests { + Digest + Layers {Size Digest} + } + } + } + }` - resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) + responseStruct := &ExpandedRepoInfoResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) - responseStruct := &ExpandedRepoInfoResp{} + responseStruct = &ExpandedRepoInfoResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) @@ -1146,6 +1178,7 @@ func TestExpandedRepoInfo(t *testing.T) { conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = rootDir + conf.Storage.GC = false conf.Storage.SubPaths = make(map[string]config.StorageConfig) conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir} defaultVal := true @@ -1197,9 +1230,15 @@ func TestExpandedRepoInfo(t *testing.T) { So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 422) - query := "{ExpandedRepoInfo(repo:\"zot-cve-test\"){Summary%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20Vendors%20Score}}}" //nolint: lll + query := `{ + ExpandedRepoInfo(repo:"zot-cve-test"){ + Summary { + Name LastUpdated Size + } + } + }` - resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -1211,9 +1250,20 @@ func TestExpandedRepoInfo(t *testing.T) { So(responseStruct.ExpandedRepoInfo.RepoInfo.Summary, ShouldNotBeEmpty) So(responseStruct.ExpandedRepoInfo.RepoInfo.Summary.Name, ShouldEqual, "zot-cve-test") - query = "{ExpandedRepoInfo(repo:\"zot-cve-test\"){Images%20{Digest%20IsSigned%20Tag%20Layers%20{Size%20Digest}}}}" + query = `{ + ExpandedRepoInfo(repo:"zot-cve-test"){ + Images { + Tag + Manifests { + Digest + Layers {Size Digest} + } + IsSigned + } + } + }` - resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -1223,14 +1273,14 @@ func TestExpandedRepoInfo(t *testing.T) { err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries), ShouldNotEqual, 0) - So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Layers), ShouldNotEqual, 0) + So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Manifests[0].Layers), ShouldNotEqual, 0) _, testManifestDigest, _, err := testStorage.GetImageManifest("zot-cve-test", "0.0.1") So(err, ShouldBeNil) found := false for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries { - if m.Digest == testManifestDigest.String() { + if m.Manifests[0].Digest == testManifestDigest.String() { found = true So(m.IsSigned, ShouldEqual, false) } @@ -1240,7 +1290,7 @@ func TestExpandedRepoInfo(t *testing.T) { err = SignImageUsingCosign("zot-cve-test:0.0.1", port) So(err, ShouldBeNil) - resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -1248,29 +1298,46 @@ func TestExpandedRepoInfo(t *testing.T) { err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries), ShouldNotEqual, 0) - So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Layers), ShouldNotEqual, 0) + So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Manifests[0].Layers), ShouldNotEqual, 0) _, testManifestDigest, _, err = testStorage.GetImageManifest("zot-cve-test", "0.0.1") So(err, ShouldBeNil) found = false for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries { - if m.Digest == testManifestDigest.String() { + if m.Manifests[0].Digest == testManifestDigest.String() { found = true So(m.IsSigned, ShouldEqual, true) } } So(found, ShouldEqual, true) - query = "{ExpandedRepoInfo(repo:\"\"){Images%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}" + query = `{ + ExpandedRepoInfo(repo:""){ + Images { + Tag + } + } + }` - resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) - query = "{ExpandedRepoInfo(repo:\"zot-test\"){Images%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}" - resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) + query = `{ + ExpandedRepoInfo(repo:"zot-test"){ + Images { + RepoName + Tag IsSigned + Manifests{ + Digest + Layers {Size Digest} + } + } + } + }` + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -1278,24 +1345,24 @@ func TestExpandedRepoInfo(t *testing.T) { err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries), ShouldNotEqual, 0) - So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Layers), ShouldNotEqual, 0) + So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Manifests[0].Layers), ShouldNotEqual, 0) _, testManifestDigest, _, err = testStorage.GetImageManifest("zot-test", "0.0.1") So(err, ShouldBeNil) found = false for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries { - if m.Digest == testManifestDigest.String() { + if m.Manifests[0].Digest == testManifestDigest.String() { found = true So(m.IsSigned, ShouldEqual, false) } } So(found, ShouldEqual, true) - err = SignImageUsingCosign("zot-test:0.0.1", port) + err = SignImageUsingCosign("zot-test@"+testManifestDigest.String(), port) So(err, ShouldBeNil) - resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "/query?query=" + query) + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "/query?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -1303,14 +1370,14 @@ func TestExpandedRepoInfo(t *testing.T) { err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries), ShouldNotEqual, 0) - So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Layers), ShouldNotEqual, 0) + So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Manifests[0].Layers), ShouldNotEqual, 0) _, testManifestDigest, _, err = testStorage.GetImageManifest("zot-test", "0.0.1") So(err, ShouldBeNil) found = false for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries { - if m.Digest == testManifestDigest.String() { + if m.Manifests[0].Digest == testManifestDigest.String() { found = true So(m.IsSigned, ShouldEqual, true) } @@ -1323,7 +1390,7 @@ func TestExpandedRepoInfo(t *testing.T) { err = os.Remove(path.Join(rootDir, "zot-test/blobs/sha256", manifestDigest.Encoded())) So(err, ShouldBeNil) - resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -1378,8 +1445,20 @@ func TestExpandedRepoInfo(t *testing.T) { So(err, ShouldBeNil) responseStruct := &ExpandedRepoInfoResp{} - query := "{ExpandedRepoInfo(repo:\"test-repo\"){Images%20{RepoName%20Digest%20Tag%20LastUpdated%20Layers%20{Size%20Digest}}}}" //nolint: lll - resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query) + query := ` + { + ExpandedRepoInfo(repo:"test-repo"){ + Images { + RepoName + Tag + Manifests { + Digest + Layers {Size Digest} + } + } + } + }` + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -1387,12 +1466,140 @@ func TestExpandedRepoInfo(t *testing.T) { err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries), ShouldNotEqual, 0) - So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Layers), ShouldNotEqual, 0) + So(len(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Manifests[0].Layers), ShouldNotEqual, 0) So(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[0].Tag, ShouldEqual, "3.0") So(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[1].Tag, ShouldEqual, "2.0") So(responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries[2].Tag, ShouldEqual, "1.0") }) + + Convey("With Multiarch Images", t, func() { + conf := config.New() + conf.HTTP.Port = GetFreePort() + baseURL := GetBaseURL(conf.HTTP.Port) + conf.Storage.RootDirectory = t.TempDir() + + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, + } + + conf.Extensions.Search.CVE = nil + ctlr := api.NewController(conf) + + imageStore := local.NewImageStore(conf.Storage.RootDirectory, false, 0, false, false, + log.NewLogger("debug", ""), monitoring.NewMetricsServer(false, log.NewLogger("debug", "")), nil, nil) + + storeController := storage.StoreController{ + DefaultStore: imageStore, + } + + // ------- Create test images + + indexSubImage11, err := GetImageWithConfig(ispec.Image{ + Platform: ispec.Platform{ + OS: "os11", + Architecture: "arch11", + }, + }) + So(err, ShouldBeNil) + + indexSubImage12, err := GetImageWithConfig(ispec.Image{ + Platform: ispec.Platform{ + OS: "os12", + Architecture: "arch12", + }, + }) + So(err, ShouldBeNil) + + multiImage1 := GetMultiarchImageForImages("1.0.0", []Image{indexSubImage11, indexSubImage12}) + + indexSubImage21, err := GetImageWithConfig(ispec.Image{ + Platform: ispec.Platform{ + OS: "os21", + Architecture: "arch21", + }, + }) + So(err, ShouldBeNil) + + indexSubImage22, err := GetImageWithConfig(ispec.Image{ + Platform: ispec.Platform{ + OS: "os22", + Architecture: "arch22", + }, + }) + So(err, ShouldBeNil) + + indexSubImage23, err := GetImageWithConfig(ispec.Image{ + Platform: ispec.Platform{ + OS: "os23", + Architecture: "arch23", + }, + }) + So(err, ShouldBeNil) + + multiImage2 := GetMultiarchImageForImages("2.0.0", + []Image{indexSubImage21, indexSubImage22, indexSubImage23}, + ) + + // ------- Write test Images + err = WriteMultiArchImageToFileSystem(multiImage1, "repo", storeController) + So(err, ShouldBeNil) + + err = WriteMultiArchImageToFileSystem(multiImage2, "repo", storeController) + So(err, ShouldBeNil) + // ------- Start Server /tmp/TestExpandedRepoInfo4021254039/005 + + ctlrManager := NewControllerManager(ctlr) + ctlrManager.StartAndWait(conf.HTTP.Port) + defer ctlrManager.StopServer() + + // ------- Test ExpandedRepoInfo + responseStruct := &ExpandedRepoInfoResp{} + + query := ` + { + ExpandedRepoInfo(repo:"repo"){ + Images { + RepoName + Tag + Manifests { + Digest + Layers {Size Digest} + } + } + } + }` + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Summary.Platforms), ShouldNotEqual, 5) + + found := false + for _, is := range responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries { + if is.Tag == "1.0.0" { + found = true + + So(len(is.Manifests), ShouldEqual, 2) + } + } + So(found, ShouldBeTrue) + + found = false + for _, is := range responseStruct.ExpandedRepoInfo.RepoInfo.ImageSummaries { + if is.Tag == "2.0.0" { + found = true + + So(len(is.Manifests), ShouldEqual, 3) + } + } + So(found, ShouldBeTrue) + }) } func TestUtilsMethod(t *testing.T) { @@ -1511,6 +1718,10 @@ func TestUtilsMethod(t *testing.T) { dir = common.GetRootDir("b/zot-cve-test", storeController) So(dir, ShouldEqual, subRootDir) + + repo, digest := common.GetImageDirAndDigest("image") + So(repo, ShouldResemble, "image") + So(digest, ShouldResemble, "") }) } @@ -1592,10 +1803,10 @@ func TestDerivedImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -1635,10 +1846,10 @@ func TestDerivedImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -1678,10 +1889,10 @@ func TestDerivedImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -1740,10 +1951,10 @@ func TestDerivedImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -1797,10 +2008,10 @@ func TestDerivedImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -1812,12 +2023,14 @@ func TestDerivedImageList(t *testing.T) { { DerivedImageList(image:"test-repo:latest"){ Results{ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } Size } } @@ -1838,12 +2051,14 @@ func TestDerivedImageList(t *testing.T) { { DerivedImageList(image:"test-repo:latest", requestedPage:{limit: 1, offset: 0, sortBy:ALPHABETIC_ASC}){ Results{ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } Size } } @@ -1864,13 +2079,15 @@ func TestDerivedImageList(t *testing.T) { query := ` { DerivedImageList(image:"inexistent-image:latest"){ - Results { - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + Results{ + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } Size } } @@ -1885,13 +2102,15 @@ func TestDerivedImageList(t *testing.T) { query := ` { DerivedImageList(image:"inexistent-image"){ - Results { - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + Results{ + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } Size } } @@ -1938,14 +2157,16 @@ func TestDerivedImageListNoRepos(t *testing.T) { query := ` { - DerivedImageList(image:"test-image"){ + DerivedImageList(image:"test-image:latest"){ Results{ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } Size } } @@ -1956,7 +2177,7 @@ func TestDerivedImageListNoRepos(t *testing.T) { So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) - So(strings.Contains(string(resp.Body()), "no reference provided"), ShouldBeTrue) + So(strings.Contains(string(resp.Body()), "repository: not found"), ShouldBeTrue) So(err, ShouldBeNil) }) } @@ -2075,10 +2296,10 @@ func TestBaseImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -2123,10 +2344,10 @@ func TestBaseImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -2166,10 +2387,117 @@ func TestBaseImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", + }, + baseURL, + repoName, + ) + So(err, ShouldBeNil) + + // create image with one layer, which is also present in the given image + layers = [][]byte{ + {10, 11, 10, 11}, + } + + manifest = ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[0]), + Size: int64(len(layers[0])), + }, + }, + } + + err = UploadImage( + Image{ + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", + }, + baseURL, + "one-layer", + ) + So(err, ShouldBeNil) + + // create image with one layer, which is also present in the given image + layers = [][]byte{ + {10, 11, 10, 11}, + } + + manifest = ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[0]), + Size: int64(len(layers[0])), + }, + }, + } + + err = UploadImage( + Image{ + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", + }, + baseURL, + "one-layer", + ) + So(err, ShouldBeNil) + + // create image with one layer, which is also present in the given image + layers = [][]byte{ + {10, 11, 10, 11}, + } + + manifest = ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layers[0]), + Size: int64(len(layers[0])), + }, + }, + } + + repoName = "one-layer" + + err = UploadImage( + Image{ + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -2203,10 +2531,10 @@ func TestBaseImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -2246,10 +2574,10 @@ func TestBaseImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -2307,10 +2635,10 @@ func TestBaseImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -2350,10 +2678,10 @@ func TestBaseImageList(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -2365,12 +2693,14 @@ func TestBaseImageList(t *testing.T) { { BaseImageList(image:"test-repo:latest"){ Results{ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + RepoName + Tag IsSigned + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } Size } } @@ -2394,12 +2724,14 @@ func TestBaseImageList(t *testing.T) { { BaseImageList(image:"test-repo:latest", requestedPage:{limit: 1, offset: 0, sortBy:RELEVANCE}){ Results{ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } Size } } @@ -2424,12 +2756,14 @@ func TestBaseImageList(t *testing.T) { { BaseImageList(image:"nonexistent-image:latest"){ Results{ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } Size } } @@ -2445,12 +2779,14 @@ func TestBaseImageList(t *testing.T) { { BaseImageList(image:"nonexistent-image"){ Results{ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } Size } } @@ -2499,12 +2835,15 @@ func TestBaseImageListNoRepos(t *testing.T) { { BaseImageList(image:"test-image"){ Results{ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } + IsSigned Size } } @@ -2575,10 +2914,10 @@ func TestGlobalSearchImageAuthor(t *testing.T) { manifest.Annotations["org.opencontainers.image.authors"] = "author name" err = UploadImage( Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "latest", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "latest", }, baseURL, "repowithauthor") So(err, ShouldBeNil) @@ -2587,8 +2926,7 @@ func TestGlobalSearchImageAuthor(t *testing.T) { { GlobalSearch(query:"repowithauthor:latest"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } + RepoName Tag LastUpdated Size IsSigned Authors } } @@ -2613,8 +2951,7 @@ func TestGlobalSearchImageAuthor(t *testing.T) { Platforms { Os Arch } Vendors Score NewestImage { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } + RepoName Tag LastUpdated Size IsSigned Authors } } @@ -2640,10 +2977,10 @@ func TestGlobalSearchImageAuthor(t *testing.T) { err = UploadImage( Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: "latest", + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: "latest", }, baseURL, "repowithauthorconfig") So(err, ShouldBeNil) @@ -2652,8 +2989,7 @@ func TestGlobalSearchImageAuthor(t *testing.T) { { GlobalSearch(query:"repowithauthorconfig:latest"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } + RepoName Tag LastUpdated Size IsSigned Authors } } @@ -2678,8 +3014,7 @@ func TestGlobalSearchImageAuthor(t *testing.T) { Platforms { Os Arch } Vendors Score NewestImage { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } + RepoName Tag LastUpdated Size IsSigned Authors } } @@ -2775,10 +3110,10 @@ func TestGlobalSearch(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest1, - Config: config1, - Layers: layers1, - Tag: "1.0.1", + Manifest: manifest1, + Config: config1, + Layers: layers1, + Reference: "1.0.1", }, baseURL, "repo1", @@ -2817,10 +3152,10 @@ func TestGlobalSearch(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest2, - Config: config2, - Layers: layers2, - Tag: "1.0.2", + Manifest: manifest2, + Config: config2, + Layers: layers2, + Reference: "1.0.2", }, baseURL, "repo1", @@ -2842,10 +3177,10 @@ func TestGlobalSearch(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest3, - Config: config3, - Layers: layers3, - Tag: "1.0.0", + Manifest: manifest3, + Config: config3, + Layers: layers3, + Reference: "1.0.0", }, baseURL, "repo2", @@ -2874,26 +3209,35 @@ func TestGlobalSearch(t *testing.T) { { GlobalSearch(query:"repo"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } - Vulnerabilities { Count MaxSeverity } - History { - Layer { Size Digest } - HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + RepoName Tag LastUpdated Size IsSigned + Manifests { + LastUpdated + Size + Platform { Os Arch } + History { + Layer { Size Digest } + HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + } + Vulnerabilities { Count MaxSeverity } } + Vendor + Vulnerabilities { Count MaxSeverity } } Repos { Name LastUpdated Size Platforms { Os Arch } Vendors Score NewestImage { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } - Vulnerabilities { Count MaxSeverity } - History { - Layer { Size Digest } - HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + RepoName Tag LastUpdated Size + Manifests{ + LastUpdated Size + Platform { Os Arch } + History { + Layer { Size Digest } + HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + } } + Vulnerabilities { Count MaxSeverity } } } Layers { Digest Size } @@ -2957,26 +3301,32 @@ func TestGlobalSearch(t *testing.T) { { GlobalSearch(query:"repo1:1.0.1"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } - Vulnerabilities { Count MaxSeverity } - History { - Layer { Size Digest } - HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + RepoName Tag LastUpdated Size + Manifests { + LastUpdated Size + Platform { Os Arch } + History { + Layer { Size Digest } + HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + } } + Vulnerabilities { Count MaxSeverity } } Repos { Name LastUpdated Size Platforms { Os Arch } Vendors Score NewestImage { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } - Vulnerabilities { Count MaxSeverity } - History { - Layer { Size Digest } - HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + RepoName Tag LastUpdated Size + Manifests { + LastUpdated Size + Platform { Os Arch } + History { + Layer { Size Digest } + HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + } } + Vulnerabilities { Count MaxSeverity } } } Layers { Digest Size } @@ -3106,10 +3456,10 @@ func TestGlobalSearch(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest1, - Config: config1, - Layers: layers1, - Tag: "1.0.1", + Manifest: manifest1, + Config: config1, + Layers: layers1, + Reference: "1.0.1", }, baseURL, "repo1", @@ -3131,10 +3481,10 @@ func TestGlobalSearch(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest2, - Config: config2, - Layers: layers2, - Tag: "1.0.2", + Manifest: manifest2, + Config: config2, + Layers: layers2, + Reference: "1.0.2", }, baseURL, "repo1", @@ -3156,10 +3506,10 @@ func TestGlobalSearch(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest3, - Config: config3, - Layers: layers3, - Tag: "1.0.0", + Manifest: manifest3, + Config: config3, + Layers: layers3, + Reference: "1.0.0", }, baseURL, "repo2", @@ -3188,31 +3538,38 @@ func TestGlobalSearch(t *testing.T) { { GlobalSearch(query:"repo"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } - Vulnerabilities { Count MaxSeverity } - History { - Layer { Size Digest } - HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + RepoName Tag LastUpdated Size + Manifests { + LastUpdated Size + Platform { Os Arch } + History { + Layer { Size Digest } + HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + } } + Vulnerabilities { Count MaxSeverity } } Repos { Name LastUpdated Size Platforms { Os Arch } Vendors Score NewestImage { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } - Vulnerabilities { Count MaxSeverity } - History { - Layer { Size Digest } - HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + RepoName Tag LastUpdated Size + Manifests { + LastUpdated Size + Platform { Os Arch } + History { + Layer { Size Digest } + HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + } } + Vulnerabilities { Count MaxSeverity } } } Layers { Digest Size } } }` + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) @@ -3271,26 +3628,32 @@ func TestGlobalSearch(t *testing.T) { { GlobalSearch(query:"repo1:1.0.1"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } - Vulnerabilities { Count MaxSeverity } - History { - Layer { Size Digest } - HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + RepoName Tag LastUpdated Size + Manifests { + LastUpdated Size + Platform { Os Arch } + History { + Layer { Size Digest } + HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + } } + Vulnerabilities { Count MaxSeverity } } Repos { Name LastUpdated Size Platforms { Os Arch } Vendors Score NewestImage { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } - Vulnerabilities { Count MaxSeverity } - History { - Layer { Size Digest } - HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + RepoName Tag LastUpdated Size + Manifests { + LastUpdated Size + Platform { Os Arch } + History { + Layer { Size Digest } + HistoryDescription { Author Comment Created CreatedBy EmptyLayer } + } } + Vulnerabilities { Count MaxSeverity } } } Layers { Digest Size } @@ -3349,7 +3712,7 @@ func TestCleaningFilteringParamsGlobalSearch(t *testing.T) { ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() - config, layers, manifest, err := GetImageWithConfig(ispec.Image{ + image, err := GetImageWithConfig(ispec.Image{ Platform: ispec.Platform{ OS: "windows", Architecture: "amd64", @@ -3358,18 +3721,13 @@ func TestCleaningFilteringParamsGlobalSearch(t *testing.T) { So(err, ShouldBeNil) err = UploadImage( - Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "0.0.1", - }, + image, baseURL, "repo1", ) So(err, ShouldBeNil) - config, layers, manifest, err = GetImageWithConfig(ispec.Image{ + image, err = GetImageWithConfig(ispec.Image{ Platform: ispec.Platform{ OS: "linux", Architecture: "amd64", @@ -3378,12 +3736,7 @@ func TestCleaningFilteringParamsGlobalSearch(t *testing.T) { So(err, ShouldBeNil) err = UploadImage( - Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "0.0.1", - }, + image, baseURL, "repo2", ) @@ -3436,10 +3789,10 @@ func TestGlobalSearchFiltering(t *testing.T) { err = UploadImage( Image{ - Config: config, - Layers: layers, - Manifest: manifest, - Tag: "test", + Config: config, + Layers: layers, + Manifest: manifest, + Reference: "test", }, baseURL, "unsigned-repo", @@ -3451,10 +3804,10 @@ func TestGlobalSearchFiltering(t *testing.T) { err = UploadImage( Image{ - Config: config, - Layers: layers, - Manifest: manifest, - Tag: "test", + Config: config, + Layers: layers, + Manifest: manifest, + Reference: "test", }, baseURL, "signed-repo", @@ -3665,18 +4018,20 @@ func TestImageList(t *testing.T) { Convey("without pagination, valid response", func() { query := fmt.Sprintf(`{ ImageList(repo:"%s"){ - Results{ - History{ - HistoryDescription{ - Author - Comment - Created - CreatedBy - EmptyLayer - }, - Layer{ - Digest - Size + Results { + Manifests { + History{ + HistoryDescription{ + Author + Comment + Created + CreatedBy + EmptyLayer + }, + Layer{ + Digest + Size + } } } } @@ -3693,7 +4048,7 @@ func TestImageList(t *testing.T) { So(err, ShouldBeNil) So(len(responseStruct.ImageList.Results), ShouldEqual, len(tags)) - So(len(responseStruct.ImageList.Results[0].History), ShouldEqual, len(imageConfigInfo.History)) + So(len(responseStruct.ImageList.Results[0].Manifests[0].History), ShouldEqual, len(imageConfigInfo.History)) }) Convey("Pagination with valid params", func() { @@ -3701,17 +4056,19 @@ func TestImageList(t *testing.T) { query := fmt.Sprintf(`{ ImageList(repo:"%s", requestedPage:{limit: %d, offset: 0, sortBy:RELEVANCE}){ Results{ - History{ - HistoryDescription{ - Author - Comment - Created - CreatedBy - EmptyLayer - }, - Layer{ - Digest - Size + Manifests { + History{ + HistoryDescription{ + Author + Comment + Created + CreatedBy + EmptyLayer + }, + Layer{ + Digest + Size + } } } } @@ -3758,10 +4115,10 @@ func TestGlobalSearchPagination(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "0.0.1", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "0.0.1", }, baseURL, fmt.Sprintf("repo%d", i), @@ -3954,24 +4311,23 @@ func TestRepoDBWhenSigningImages(t *testing.T) { defer ctlrManager.StopServer() // push test images to repo 1 image 1 - config1, layers1, manifest1, err := GetImageComponents(100) - So(err, ShouldBeNil) createdTime := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC) - config1.History = append(config1.History, ispec.History{Created: &createdTime}) - manifest1, err = updateManifestConfig(manifest1, config1) - So(err, ShouldBeNil) - layersSize1 := 0 - for _, l := range layers1 { - layersSize1 += len(l) - } + image1, err := GetImageWithConfig(ispec.Image{ + History: []ispec.History{ + { + Created: &createdTime, + }, + }, + }) + So(err, ShouldBeNil) err = UploadImage( Image{ - Manifest: manifest1, - Config: config1, - Layers: layers1, - Tag: "1.0.1", + Manifest: image1.Manifest, + Config: image1.Config, + Layers: image1.Layers, + Reference: "1.0.1", }, baseURL, "repo1", @@ -3980,27 +4336,39 @@ func TestRepoDBWhenSigningImages(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest1, - Config: config1, - Layers: layers1, - Tag: "2.0.2", + Manifest: image1.Manifest, + Config: image1.Config, + Layers: image1.Layers, + Reference: "2.0.2", }, baseURL, "repo1", ) So(err, ShouldBeNil) - manifestBlob, err := json.Marshal(manifest1) + manifestBlob, err := json.Marshal(image1.Manifest) So(err, ShouldBeNil) manifestDigest := godigest.FromBytes(manifestBlob) + multiArch, err := GetRandomMultiarchImage("index") + So(err, ShouldBeNil) + + err = UploadMultiarchImage( + multiArch, + baseURL, + "repo1", + ) + So(err, ShouldBeNil) + queryImage1 := ` { GlobalSearch(query:"repo1:1.0"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } + RepoName Tag LastUpdated Size IsSigned + Manifests{ + LastUpdated Size + } } } }` @@ -4009,12 +4377,23 @@ func TestRepoDBWhenSigningImages(t *testing.T) { { GlobalSearch(query:"repo1:2.0"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } + RepoName Tag LastUpdated Size IsSigned + Manifests { LastUpdated Size Platform { Os Arch } } } } }` + queryIndex := ` + { + GlobalSearch(query:"repo1:index"){ + Images { + RepoName Tag LastUpdated Size IsSigned + Manifests { LastUpdated Size Platform { Os Arch } } + } + } + } + ` + Convey("Sign with cosign", func() { err = SignImageUsingCosign("repo1:1.0.1", port) So(err, ShouldBeNil) @@ -4115,6 +4494,41 @@ func TestRepoDBWhenSigningImages(t *testing.T) { So(responseStruct.GlobalSearchResult.GlobalSearch.Images[0].IsSigned, ShouldBeTrue) }) + + Convey("Sign with notation index", func() { + err = SignImageUsingNotary("repo1:index", port) + So(err, ShouldBeNil) + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryIndex)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + responseStruct := &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + So(responseStruct.GlobalSearchResult.GlobalSearch.Images[0].IsSigned, ShouldBeTrue) + }) + + Convey("Sign with cosign index", func() { + err = SignImageUsingCosign("repo1:index", port) + So(err, ShouldBeNil) + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryIndex)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + + So(resp.StatusCode(), ShouldEqual, 200) + + responseStruct := &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + So(responseStruct.GlobalSearchResult.GlobalSearch.Images[0].IsSigned, ShouldBeTrue) + }) }) } @@ -4140,7 +4554,7 @@ func TestRepoDBWhenPushingImages(t *testing.T) { Convey("SetManifestMeta fails", func() { ctlr.RepoDB = mocks.RepoDBMock{ - SetManifestMetaFn: func(repo string, manifestDigest godigest.Digest, mm repodb.ManifestMetadata) error { + SetManifestDataFn: func(manifestDigest godigest.Digest, mm repodb.ManifestData) error { return ErrTestError }, } @@ -4163,10 +4577,10 @@ func TestRepoDBWhenPushingImages(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest1, - Config: config1, - Layers: layers1, - Tag: "1.0.1", + Manifest: manifest1, + Config: config1, + Layers: layers1, + Reference: "1.0.1", }, baseURL, "repo1", @@ -4197,10 +4611,10 @@ func TestRepoDBWhenPushingImages(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest1, - Config: config1, - Layers: layers1, - Tag: "1.0.1", + Manifest: manifest1, + Config: config1, + Layers: layers1, + Reference: "1.0.1", }, baseURL, "repo1", @@ -4210,6 +4624,652 @@ func TestRepoDBWhenPushingImages(t *testing.T) { }) } +func TestRepoDBIndexOperations(t *testing.T) { + Convey("Idex Operations BoltDB", t, func() { + dir := t.TempDir() + + port := GetFreePort() + baseURL := GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = dir + conf.Storage.GC = false + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, + } + + ctlr := api.NewController(conf) + + ctlrManager := NewControllerManager(ctlr) + ctlrManager.StartAndWait(port) + defer ctlrManager.StopServer() + + RunRepoDBIndexTests(baseURL, port) + }) +} + +func RunRepoDBIndexTests(baseURL, port string) { + Convey("Push test index", func() { + repo := "repo" + + multiarchImage, err := GetRandomMultiarchImage("tag1") + So(err, ShouldBeNil) + + indexBlob, err := json.Marshal(multiarchImage.Index) + So(err, ShouldBeNil) + + indexDigest := godigest.FromBytes(indexBlob) + + err = UploadMultiarchImage(multiarchImage, baseURL, repo) + So(err, ShouldBeNil) + + query := ` + { + GlobalSearch(query:"repo:tag1"){ + Images { + RepoName Tag DownloadCount + IsSigned + Manifests { + Digest + ConfigDigest + Platform {Os Arch} + Layers {Size Digest} + LastUpdated + Size + } + } + } + }` + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + responseStruct := &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + responseImages := responseStruct.GlobalSearchResult.GlobalSearch.Images + So(responseImages, ShouldNotBeEmpty) + responseImage := responseImages[0] + So(len(responseImage.Manifests), ShouldEqual, 3) + + err = SignImageUsingCosign(fmt.Sprintf("repo@%s", indexDigest), port) + So(err, ShouldBeNil) + + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + responseStruct = &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + responseImages = responseStruct.GlobalSearchResult.GlobalSearch.Images + So(responseImages, ShouldNotBeEmpty) + responseImage = responseImages[0] + + So(responseImage.IsSigned, ShouldBeTrue) + + // remove signature + cosignTag := "sha256-" + indexDigest.Encoded() + ".sig" + _, err = resty.R().Delete(baseURL + "/v2/" + "repo" + "/manifests/" + cosignTag) + So(err, ShouldBeNil) + + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + responseStruct = &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + responseImages = responseStruct.GlobalSearchResult.GlobalSearch.Images + So(responseImages, ShouldNotBeEmpty) + responseImage = responseImages[0] + + So(responseImage.IsSigned, ShouldBeFalse) + }) + Convey("Index base images", func() { + // ---------------- BASE IMAGE ------------------- + imageAMD64, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + [][]byte{ + {10, 20, 30}, + {11, 21, 31}, + }) + So(err, ShouldBeNil) + + imageSomeArch, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "linux", + Architecture: "someArch", + }, + }, [][]byte{ + {18, 28, 38}, + {12, 22, 32}, + }) + So(err, ShouldBeNil) + + multiImage := GetMultiarchImageForImages("latest", []Image{ + imageAMD64, + imageSomeArch, + }) + err = UploadMultiarchImage(multiImage, baseURL, "test-repo") + So(err, ShouldBeNil) + // ---------------- BASE IMAGE ------------------- + + // ---------------- SAME LAYERS ------------------- + image1, err := GetImageWithComponents( + imageSomeArch.Config, + [][]byte{ + {0, 0, 2}, + }, + ) + So(err, ShouldBeNil) + + image2, err := GetImageWithComponents( + imageAMD64.Config, + imageAMD64.Layers, + ) + So(err, ShouldBeNil) + + multiImage = GetMultiarchImageForImages("index-one-arch-same-layers", []Image{ + image1, image2, + }) + + err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-same-layers") + So(err, ShouldBeNil) + // ---------------- SAME LAYERS ------------------- + + // ---------------- LESS LAYERS ------------------- + image1, err = GetImageWithComponents( + imageSomeArch.Config, + [][]byte{ + {3, 2, 2}, + {5, 2, 5}, + }, + ) + So(err, ShouldBeNil) + + image2, err = GetImageWithComponents( + imageAMD64.Config, + [][]byte{imageAMD64.Layers[0]}, + ) + So(err, ShouldBeNil) + multiImage = GetMultiarchImageForImages("index-one-arch-less-layers", []Image{ + image1, image2, + }) + err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-less-layers") + So(err, ShouldBeNil) + // ---------------- LESS LAYERS ------------------- + + // ---------------- LESS LAYERS FALSE ------------------- + image1, err = GetImageWithComponents( + imageSomeArch.Config, + [][]byte{ + {3, 2, 2}, + {5, 2, 5}, + }, + ) + So(err, ShouldBeNil) + auxLayer := imageAMD64.Layers[0] + auxLayer[0] = 20 + + image2, err = GetImageWithComponents( + imageAMD64.Config, + [][]byte{auxLayer}, + ) + So(err, ShouldBeNil) + multiImage = GetMultiarchImageForImages("index-one-arch-less-layers-false", []Image{ + image1, image2, + }) + err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-less-layers-false") + So(err, ShouldBeNil) + // ---------------- LESS LAYERS FALSE ------------------- + + // ---------------- MORE LAYERS ------------------- + image1, err = GetImageWithComponents( + imageSomeArch.Config, + [][]byte{ + {0, 0, 2}, + {3, 0, 2}, + }, + ) + So(err, ShouldBeNil) + + image2, err = GetImageWithComponents( + imageAMD64.Config, + append(imageAMD64.Layers, []byte{1, 3, 55}), + ) + So(err, ShouldBeNil) + multiImage = GetMultiarchImageForImages("index-one-arch-more-layers", []Image{ + image1, image2, + }) + + err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-more-layers") + So(err, ShouldBeNil) + // ---------------- MORE LAYERS ------------------- + + query := ` + { + BaseImageList(image:"test-repo:latest"){ + Results{ + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } + Size + } + } + }` + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + + So(strings.Contains(string(resp.Body()), "index-one-arch-less-layers"), ShouldBeTrue) + So(strings.Contains(string(resp.Body()), "index-one-arch-same-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "index-one-arch-less-layers-false"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "index-one-arch-more-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "test-repo"), ShouldBeFalse) + }) + + Convey("Index base images for digest", func() { + // ---------------- BASE IMAGE ------------------- + imageAMD64, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + [][]byte{ + {10, 20, 30}, + {11, 21, 31}, + }) + So(err, ShouldBeNil) + + baseLinuxAMD64Digest, err := imageAMD64.Digest() + So(err, ShouldBeNil) + + imageSomeArch, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "linux", + Architecture: "someArch", + }, + }, [][]byte{ + {18, 28, 38}, + {12, 22, 32}, + }) + So(err, ShouldBeNil) + + baseLinuxSomeArchDigest, err := imageSomeArch.Digest() + So(err, ShouldBeNil) + + multiImage := GetMultiarchImageForImages("index", []Image{ + imageAMD64, + imageSomeArch, + }) + err = UploadMultiarchImage(multiImage, baseURL, "test-repo") + So(err, ShouldBeNil) + // ---------------- BASE IMAGE FOR LINUX AMD64 ------------------- + + image, err := GetImageWithComponents( + imageAMD64.Config, + [][]byte{imageAMD64.Layers[0]}, + ) + So(err, ShouldBeNil) + image.Reference = "less-layers-linux-amd64" + + err = UploadImage(image, baseURL, "test-repo") + So(err, ShouldBeNil) + + // ---------------- BASE IMAGE FOR LINUX SOMEARCH ------------------- + + image, err = GetImageWithComponents( + imageAMD64.Config, + [][]byte{imageSomeArch.Layers[0]}, + ) + So(err, ShouldBeNil) + image.Reference = "less-layers-linux-somearch" + + err = UploadImage(image, baseURL, "test-repo") + So(err, ShouldBeNil) + + // ------- TEST + + query := ` + { + BaseImageList(image:"test-repo:index", digest:"%s"){ + Results{ + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } + Size + } + } + }` + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + + url.QueryEscape( + fmt.Sprintf(query, baseLinuxAMD64Digest.String()), + ), + ) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + + So(strings.Contains(string(resp.Body()), "less-layers-linux-amd64"), ShouldEqual, true) + So(strings.Contains(string(resp.Body()), "less-layers-linux-somearch"), ShouldEqual, false) + + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + + url.QueryEscape( + fmt.Sprintf(query, baseLinuxSomeArchDigest.String()), + ), + ) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + + So(strings.Contains(string(resp.Body()), "less-layers-linux-amd64"), ShouldEqual, false) + So(strings.Contains(string(resp.Body()), "less-layers-linux-somearch"), ShouldEqual, true) + }) + + Convey("Index derived images", func() { + // ---------------- BASE IMAGE ------------------- + imageAMD64, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + [][]byte{ + {10, 20, 30}, + {11, 21, 31}, + }) + So(err, ShouldBeNil) + + imageSomeArch, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "linux", + Architecture: "someArch", + }, + }, [][]byte{ + {18, 28, 38}, + {12, 22, 32}, + }) + So(err, ShouldBeNil) + + multiImage := GetMultiarchImageForImages("latest", []Image{ + imageAMD64, + imageSomeArch, + }) + err = UploadMultiarchImage(multiImage, baseURL, "test-repo") + So(err, ShouldBeNil) + // ---------------- BASE IMAGE ------------------- + + // ---------------- SAME LAYERS ------------------- + image1, err := GetImageWithComponents( + imageSomeArch.Config, + [][]byte{ + {0, 0, 2}, + }, + ) + So(err, ShouldBeNil) + + image2, err := GetImageWithComponents( + imageAMD64.Config, + imageAMD64.Layers, + ) + So(err, ShouldBeNil) + multiImage = GetMultiarchImageForImages("index-one-arch-same-layers", []Image{ + image1, image2, + }) + + err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-same-layers") + So(err, ShouldBeNil) + // ---------------- SAME LAYERS ------------------- + + // ---------------- LESS LAYERS ------------------- + image1, err = GetImageWithComponents( + imageSomeArch.Config, + [][]byte{ + {3, 2, 2}, + {5, 2, 5}, + }, + ) + So(err, ShouldBeNil) + + image2, err = GetImageWithComponents( + imageAMD64.Config, + [][]byte{imageAMD64.Layers[0]}, + ) + So(err, ShouldBeNil) + multiImage = GetMultiarchImageForImages("index-one-arch-less-layers", []Image{ + image1, image2, + }) + err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-less-layers") + So(err, ShouldBeNil) + // ---------------- LESS LAYERS ------------------- + + // ---------------- LESS LAYERS FALSE ------------------- + image1, err = GetImageWithComponents( + imageSomeArch.Config, + [][]byte{ + {3, 2, 2}, + {5, 2, 5}, + }, + ) + So(err, ShouldBeNil) + + image2, err = GetImageWithComponents( + imageAMD64.Config, + [][]byte{{99, 100, 102}}, + ) + So(err, ShouldBeNil) + multiImage = GetMultiarchImageForImages("index-one-arch-less-layers-false", []Image{ + image1, image2, + }) + err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-less-layers-false") + So(err, ShouldBeNil) + // ---------------- LESS LAYERS FALSE ------------------- + + // ---------------- MORE LAYERS ------------------- + image1, err = GetImageWithComponents( + imageSomeArch.Config, + [][]byte{ + {0, 0, 2}, + {3, 0, 2}, + }, + ) + So(err, ShouldBeNil) + + image2, err = GetImageWithComponents( + imageAMD64.Config, + [][]byte{ + imageAMD64.Layers[0], + imageAMD64.Layers[1], + {1, 3, 55}, + }, + ) + So(err, ShouldBeNil) + multiImage = GetMultiarchImageForImages("index-one-arch-more-layers", []Image{ + image1, + image2, + }) + + err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-more-layers") + So(err, ShouldBeNil) + // ---------------- MORE LAYERS ------------------- + + query := ` + { + DerivedImageList(image:"test-repo:latest"){ + Results{ + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } + Size + } + } + }` + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + + So(strings.Contains(string(resp.Body()), "index-one-arch-less-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "index-one-arch-same-layers"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "index-one-arch-less-layers-false"), ShouldBeFalse) + So(strings.Contains(string(resp.Body()), "index-one-arch-more-layers"), ShouldBeTrue) + So(strings.Contains(string(resp.Body()), "test-repo"), ShouldBeFalse) + }) + + Convey("Index derived images for digest", func() { + // ---------------- BASE IMAGE ------------------- + imageAMD64, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + [][]byte{ + {10, 20, 30}, + {11, 21, 31}, + }) + So(err, ShouldBeNil) + + baseLinuxAMD64Digest, err := imageAMD64.Digest() + So(err, ShouldBeNil) + + imageSomeArch, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "linux", + Architecture: "someArch", + }, + }, [][]byte{ + {18, 28, 38}, + {12, 22, 32}, + }) + So(err, ShouldBeNil) + + baseLinuxSomeArchDigest, err := imageSomeArch.Digest() + So(err, ShouldBeNil) + + multiImage := GetMultiarchImageForImages("index", []Image{ + imageAMD64, + imageSomeArch, + }) + err = UploadMultiarchImage(multiImage, baseURL, "test-repo") + So(err, ShouldBeNil) + // ---------------- BASE IMAGE FOR LINUX AMD64 ------------------- + + image, err := GetImageWithComponents( + imageAMD64.Config, + [][]byte{ + imageAMD64.Layers[0], + imageAMD64.Layers[1], + {0, 0, 0, 0}, + {1, 1, 1, 1}, + }, + ) + So(err, ShouldBeNil) + image.Reference = "more-layers-linux-amd64" + + err = UploadImage(image, baseURL, "test-repo") + So(err, ShouldBeNil) + + // ---------------- BASE IMAGE FOR LINUX SOMEARCH ------------------- + + image, err = GetImageWithComponents( + imageAMD64.Config, + [][]byte{ + imageSomeArch.Layers[0], + imageSomeArch.Layers[1], + {3, 3, 3, 3}, + {2, 2, 2, 2}, + }, + ) + So(err, ShouldBeNil) + image.Reference = "more-layers-linux-somearch" + + err = UploadImage(image, baseURL, "test-repo") + So(err, ShouldBeNil) + + // ------- TEST + + query := ` + { + DerivedImageList(image:"test-repo:index", digest:"%s"){ + Results{ + RepoName + Tag + Manifests { + Digest + ConfigDigest + LastUpdated + Size + } + Size + } + } + }` + + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + + url.QueryEscape( + fmt.Sprintf(query, baseLinuxAMD64Digest.String()), + ), + ) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + + So(strings.Contains(string(resp.Body()), "more-layers-linux-amd64"), ShouldEqual, true) + So(strings.Contains(string(resp.Body()), "more-layers-linux-somearch"), ShouldEqual, false) + + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + + url.QueryEscape( + fmt.Sprintf(query, baseLinuxSomeArchDigest.String()), + ), + ) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + + So(strings.Contains(string(resp.Body()), "more-layers-linux-amd64"), ShouldEqual, false) + So(strings.Contains(string(resp.Body()), "more-layers-linux-somearch"), ShouldEqual, true) + }) +} + func TestRepoDBWhenReadingImages(t *testing.T) { Convey("Push test image", t, func() { dir := t.TempDir() @@ -4235,10 +5295,10 @@ func TestRepoDBWhenReadingImages(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest1, - Config: config1, - Layers: layers1, - Tag: "1.0.1", + Manifest: manifest1, + Config: config1, + Layers: layers1, + Reference: "1.0.1", }, baseURL, "repo1", @@ -4334,10 +5394,10 @@ func TestRepoDBWhenDeletingImages(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest1, - Config: config1, - Layers: layers1, - Tag: "1.0.1", + Manifest: manifest1, + Config: config1, + Layers: layers1, + Reference: "1.0.1", }, baseURL, "repo1", @@ -4359,10 +5419,10 @@ func TestRepoDBWhenDeletingImages(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest2, - Config: config2, - Layers: layers2, - Tag: "1.0.2", + Manifest: manifest2, + Config: config2, + Layers: layers2, + Reference: "1.0.2", }, baseURL, "repo1", @@ -4373,18 +5433,21 @@ func TestRepoDBWhenDeletingImages(t *testing.T) { { GlobalSearch(query:"repo1:1.0"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } + RepoName Tag LastUpdated Size IsSigned + Manifests{ + Platform { Os Arch } + LastUpdated Size + } } Repos { Name LastUpdated Size Platforms { Os Arch } Vendors Score - NewestImage { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { - Os - Arch + NewestImage { + RepoName Tag LastUpdated Size IsSigned + Manifests{ + Platform { Os Arch } + LastUpdated Size } } } @@ -4436,8 +5499,11 @@ func TestRepoDBWhenDeletingImages(t *testing.T) { { GlobalSearch(query:"repo1:1.0.1"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } + RepoName Tag LastUpdated Size IsSigned + Manifests{ + Platform { Os Arch } + LastUpdated Size + } } } }` @@ -4509,8 +5575,11 @@ func TestRepoDBWhenDeletingImages(t *testing.T) { { GlobalSearch(query:"repo1:1.0.1"){ Images { - RepoName Tag LastUpdated Size IsSigned Vendor Score - Platform { Os Arch } + RepoName Tag LastUpdated Size IsSigned + Manifests{ + Platform { Os Arch } + LastUpdated Size + } } } }` @@ -4992,10 +6061,10 @@ func TestBaseOciLayoutUtils(t *testing.T) { tag := "1.0.1" err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: tag, + Manifest: manifest, + Config: config, + Layers: layers, + Reference: tag, }, baseURL, repo, @@ -5057,10 +6126,10 @@ func TestSearchSize(t *testing.T) { err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "latest", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "latest", }, baseURL, repoName, @@ -5070,19 +6139,27 @@ func TestSearchSize(t *testing.T) { query := ` { GlobalSearch(query:"testrepo:"){ - Images { RepoName Tag LastUpdated Size Score } - Repos { - Name LastUpdated Size Vendors Score - Platforms { - Os - Arch + Images { + RepoName Tag LastUpdated Size Vendor + Manifests{ + Platform { Os Arch } + LastUpdated Size + } + } + Repos { + Name LastUpdated Size + NewestImage { + Manifests{ + Platform { Os Arch } + LastUpdated Size + } } } - Layers { Digest Size } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(configSize+layersSize+manifestSize, ShouldNotBeZeroValue) responseStruct := &GlobalSearchResultResp{} @@ -5099,19 +6176,27 @@ func TestSearchSize(t *testing.T) { query = ` { GlobalSearch(query:"testrepo"){ - Images { RepoName Tag LastUpdated Size Score } - Repos { - Name LastUpdated Size Vendors Score - Platforms { - Os - Arch - } + Images { + RepoName Tag LastUpdated Size + Manifests{ + Platform { Os Arch } + LastUpdated Size + } + } + Repos { + Name LastUpdated Size + NewestImage { + Manifests{ + Platform { Os Arch } + LastUpdated Size + } + } } - Layers { Digest Size } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(configSize+layersSize+manifestSize, ShouldNotBeZeroValue) responseStruct = &GlobalSearchResultResp{} @@ -5126,10 +6211,10 @@ func TestSearchSize(t *testing.T) { // add the same image with different tag err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: "10.2.14", + Manifest: manifest, + Config: config, + Layers: layers, + Reference: "10.2.14", }, baseURL, repoName, @@ -5140,13 +6225,21 @@ func TestSearchSize(t *testing.T) { query = ` { GlobalSearch(query:"testrepo:"){ - Images { RepoName Tag LastUpdated Size Score } + Images { + RepoName Tag LastUpdated Size + Manifests{ + Platform { Os Arch } + LastUpdated Size + } + } Repos { - Name LastUpdated Size Vendors Score - Platforms { - Os - Arch - } + Name LastUpdated Size + NewestImage { + Manifests{ + Platform { Os Arch } + LastUpdated Size + } + } } Layers { Digest Size } } @@ -5154,6 +6247,7 @@ func TestSearchSize(t *testing.T) { resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(configSize+layersSize+manifestSize, ShouldNotBeZeroValue) responseStruct = &GlobalSearchResultResp{} @@ -5166,13 +6260,21 @@ func TestSearchSize(t *testing.T) { query = ` { GlobalSearch(query:"testrepo"){ - Images { RepoName Tag LastUpdated Size Score } + Images { + RepoName Tag LastUpdated Size + Manifests{ + Platform { Os Arch } + LastUpdated Size + } + } Repos { - Name LastUpdated Size Vendors Score - Platforms { - Os - Arch - } + Name LastUpdated Size + NewestImage { + Manifests{ + Platform { Os Arch } + LastUpdated Size + } + } } Layers { Digest Size } } @@ -5180,6 +6282,7 @@ func TestSearchSize(t *testing.T) { resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(configSize+layersSize+manifestSize, ShouldNotBeZeroValue) responseStruct = &GlobalSearchResultResp{} @@ -5215,18 +6318,22 @@ func TestImageSummary(t *testing.T) { Image(image:"%s:%s"){ RepoName Tag - Digest - ConfigDigest - LastUpdated - IsSigned - Size - Platform { Os Arch } - Layers { Digest Size } - Vulnerabilities { Count MaxSeverity } - History { - HistoryDescription { Created } - Layer { Digest Size } + Manifests { + Digest + ConfigDigest + LastUpdated + Size + Platform { Os Arch } + Layers { Digest Size } + Vulnerabilities { Count MaxSeverity } + History { + HistoryDescription { Created } + Layer { Digest Size } + } } + LastUpdated + Size + Vulnerabilities { Count MaxSeverity } } }` @@ -5235,12 +6342,20 @@ func TestImageSummary(t *testing.T) { Image(image:"%s"){ RepoName, Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, + Manifests { + Digest + ConfigDigest + LastUpdated + Size + Platform { Os Arch } + Layers { Digest Size } + Vulnerabilities { Count MaxSeverity } + History { + HistoryDescription { Created } + Layer { Digest Size } + } + }, Size - Layers { Digest Size } } }` @@ -5268,10 +6383,10 @@ func TestImageSummary(t *testing.T) { tagTarget := "latest" err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: tagTarget, + Manifest: manifest, + Config: config, + Layers: layers, + Reference: tagTarget, }, baseURL, repoName, @@ -5324,17 +6439,17 @@ func TestImageSummary(t *testing.T) { imgSummary := imgSummaryResponse.SingleImageSummary.ImageSummary So(imgSummary.RepoName, ShouldContainSubstring, repoName) So(imgSummary.Tag, ShouldContainSubstring, tagTarget) - So(imgSummary.ConfigDigest, ShouldContainSubstring, configDigest.Encoded()) - So(imgSummary.Digest, ShouldContainSubstring, manifestDigest.Encoded()) - So(len(imgSummary.Layers), ShouldEqual, 1) - So(imgSummary.Layers[0].Digest, ShouldContainSubstring, + So(imgSummary.Manifests[0].ConfigDigest, ShouldContainSubstring, configDigest.Encoded()) + So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, manifestDigest.Encoded()) + So(len(imgSummary.Manifests[0].Layers), ShouldEqual, 1) + So(imgSummary.Manifests[0].Layers[0].Digest, ShouldContainSubstring, godigest.FromBytes(layers[0]).Encoded()) So(imgSummary.LastUpdated, ShouldEqual, createdTime) So(imgSummary.IsSigned, ShouldEqual, false) - So(imgSummary.Platform.Os, ShouldEqual, "linux") - So(imgSummary.Platform.Arch, ShouldEqual, "amd64") - So(len(imgSummary.History), ShouldEqual, 1) - So(imgSummary.History[0].HistoryDescription.Created, ShouldEqual, createdTime) + So(imgSummary.Manifests[0].Platform.Os, ShouldEqual, "linux") + So(imgSummary.Manifests[0].Platform.Arch, ShouldEqual, "amd64") + So(len(imgSummary.Manifests[0].History), ShouldEqual, 1) + So(imgSummary.Manifests[0].History[0].HistoryDescription.Created, ShouldEqual, createdTime) // No vulnerabilities should be detected since trivy is disabled So(imgSummary.Vulnerabilities.Count, ShouldEqual, 0) So(imgSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "") @@ -5411,18 +6526,22 @@ func TestImageSummary(t *testing.T) { Image(image:"%s:%s"){ RepoName Tag - Digest - ConfigDigest - LastUpdated - IsSigned - Size - Platform { Os Arch } - Layers { Digest Size } - Vulnerabilities { Count MaxSeverity } - History { - HistoryDescription { Created } - Layer { Digest Size } + Manifests { + Digest + ConfigDigest + LastUpdated + Size + Platform { Os Arch } + Layers { Digest Size } + Vulnerabilities { Count MaxSeverity } + History { + HistoryDescription { Created } + Layer { Digest Size } + } } + LastUpdated + Size + Vulnerabilities { Count MaxSeverity } } }` @@ -5465,10 +6584,10 @@ func TestImageSummary(t *testing.T) { tagTarget := "latest" err = UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: tagTarget, + Manifest: manifest, + Config: config, + Layers: layers, + Reference: tagTarget, }, baseURL, repoName, @@ -5501,17 +6620,17 @@ func TestImageSummary(t *testing.T) { imgSummary := imgSummaryResponse.SingleImageSummary.ImageSummary So(imgSummary.RepoName, ShouldContainSubstring, repoName) So(imgSummary.Tag, ShouldContainSubstring, tagTarget) - So(imgSummary.ConfigDigest, ShouldContainSubstring, configDigest.Encoded()) - So(imgSummary.Digest, ShouldContainSubstring, manifestDigest.Encoded()) - So(len(imgSummary.Layers), ShouldEqual, 1) - So(imgSummary.Layers[0].Digest, ShouldContainSubstring, + So(imgSummary.Manifests[0].ConfigDigest, ShouldContainSubstring, configDigest.Encoded()) + So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, manifestDigest.Encoded()) + So(len(imgSummary.Manifests[0].Layers), ShouldEqual, 1) + So(imgSummary.Manifests[0].Layers[0].Digest, ShouldContainSubstring, godigest.FromBytes(layers[0]).Encoded()) So(imgSummary.LastUpdated, ShouldEqual, createdTime) So(imgSummary.IsSigned, ShouldEqual, false) - So(imgSummary.Platform.Os, ShouldEqual, "linux") - So(imgSummary.Platform.Arch, ShouldEqual, "amd64") - So(len(imgSummary.History), ShouldEqual, 1) - So(imgSummary.History[0].HistoryDescription.Created, ShouldEqual, createdTime) + So(imgSummary.Manifests[0].Platform.Os, ShouldEqual, "linux") + So(imgSummary.Manifests[0].Platform.Arch, ShouldEqual, "amd64") + So(len(imgSummary.Manifests[0].History), ShouldEqual, 1) + So(imgSummary.Manifests[0].History[0].HistoryDescription.Created, ShouldEqual, createdTime) So(imgSummary.Vulnerabilities.Count, ShouldEqual, 4) // There are 0 vulnerabilities this data used in tests So(imgSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") diff --git a/pkg/extensions/search/common/model.go b/pkg/extensions/search/common/model.go index a522513e..399d99ce 100644 --- a/pkg/extensions/search/common/model.go +++ b/pkg/extensions/search/common/model.go @@ -13,7 +13,7 @@ type RepoSummary struct { Name string `json:"name"` LastUpdated time.Time `json:"lastUpdated"` Size string `json:"size"` - Platforms []OsArch `json:"platforms"` + Platforms []Platform `json:"platforms"` Vendors []string `json:"vendors"` Score int `json:"score"` NewestImage ImageSummary `json:"newestImage"` @@ -22,28 +22,36 @@ type RepoSummary struct { 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"` + Manifests []ManifestSummary `json:"manifests"` Size string `json:"size"` - Platform OsArch `json:"platform"` - Vendor string `json:"vendor"` - Score int `json:"score"` DownloadCount int `json:"downloadCount"` + LastUpdated time.Time `json:"lastUpdated"` Description string `json:"description"` + IsSigned bool `json:"isSigned"` Licenses string `json:"licenses"` Labels string `json:"labels"` Title string `json:"title"` + Score int `json:"score"` Source string `json:"source"` Documentation string `json:"documentation"` - History []LayerHistory `json:"history"` - Layers []LayerSummary `json:"layers"` - Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"` Authors string `json:"authors"` + Vendor string `json:"vendor"` + Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"` } -type OsArch struct { +type ManifestSummary struct { + Digest string `json:"digest"` + ConfigDigest string `json:"configDigest"` + LastUpdated time.Time `json:"lastUpdated"` + Size string `json:"size"` + Platform Platform `json:"platform"` + DownloadCount int `json:"downloadCount"` + Layers []LayerSummary `json:"layers"` + History []LayerHistory `json:"history"` + Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"` +} + +type Platform struct { Os string `json:"os"` Arch string `json:"arch"` } diff --git a/pkg/extensions/search/common/oci_layout.go b/pkg/extensions/search/common/oci_layout.go index b3ddbde5..fdf9f995 100644 --- a/pkg/extensions/search/common/oci_layout.go +++ b/pkg/extensions/search/common/oci_layout.go @@ -206,7 +206,16 @@ func (olu BaseOciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]TagInfo, timeStamp := GetImageLastUpdated(imageInfo) - tagsInfo = append(tagsInfo, TagInfo{Name: val, Timestamp: timeStamp, Digest: digest}) + tagsInfo = append(tagsInfo, + TagInfo{ + Name: val, + Timestamp: timeStamp, + Descriptor: Descriptor{ + Digest: digest, + MediaType: manifest.MediaType, + }, + }, + ) } } @@ -327,9 +336,8 @@ func (olu BaseOciLayoutUtils) GetRepoLastUpdated(repo string) (TagInfo, error) { return latestTag, nil } -func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) { +func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(repoName string) (RepoInfo, error) { repo := RepoInfo{} - repoBlob2Size := make(map[string]int64, 10) // made up of all manifests, configs and image layers @@ -337,22 +345,22 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) imageSummaries := make([]ImageSummary, 0) - manifestList, err := olu.GetImageManifests(name) + manifestList, err := olu.GetImageManifests(repoName) if err != nil { olu.Log.Error().Err(err).Msg("error getting image manifests") return RepoInfo{}, err } - lastUpdatedTag, err := olu.GetRepoLastUpdated(name) + lastUpdatedTag, err := olu.GetRepoLastUpdated(repoName) if err != nil { - olu.Log.Error().Err(err).Msgf("can't get last updated manifest for repo: %s", name) + olu.Log.Error().Err(err).Msgf("can't get last updated manifest for repo: %s", repoName) return RepoInfo{}, err } repoVendorsSet := make(map[string]bool, len(manifestList)) - repoPlatformsSet := make(map[string]OsArch, len(manifestList)) + repoPlatformsSet := make(map[string]Platform, len(manifestList)) var lastUpdatedImageSummary ImageSummary @@ -367,38 +375,38 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) continue } - manifest, err := olu.GetImageBlobManifest(name, man.Digest) + manifest, err := olu.GetImageBlobManifest(repoName, man.Digest) if err != nil { olu.Log.Error().Err(err).Msg("error getting image manifest blob") return RepoInfo{}, err } - isSigned := olu.CheckManifestSignature(name, man.Digest) + isSigned := olu.CheckManifestSignature(repoName, man.Digest) - manifestSize := olu.GetImageManifestSize(name, man.Digest) + manifestSize := olu.GetImageManifestSize(repoName, man.Digest) olu.Log.Debug().Msg(fmt.Sprintf("%v", man.Digest.String())) configSize := manifest.Config.Size repoBlob2Size[man.Digest.String()] = manifestSize repoBlob2Size[manifest.Config.Digest.String()] = configSize - imageConfigInfo, err := olu.GetImageConfigInfo(name, man.Digest) + imageConfigInfo, err := olu.GetImageConfigInfo(repoName, man.Digest) if err != nil { - olu.Log.Error().Err(err).Msgf("can't retrieve config info for the image %s %s", name, man.Digest) + olu.Log.Error().Err(err).Msgf("can't retrieve config info for the image %s %s", repoName, man.Digest) continue } opSys, arch := olu.GetImagePlatform(imageConfigInfo) - osArch := OsArch{ + platform := Platform{ Os: opSys, Arch: arch, } if opSys != "" || arch != "" { - osArchString := strings.TrimSpace(fmt.Sprintf("%s %s", opSys, arch)) - repoPlatformsSet[osArchString] = osArch + platformString := strings.TrimSpace(fmt.Sprintf("%s %s", opSys, arch)) + repoPlatformsSet[platformString] = platform } layers := make([]LayerSummary, 0) @@ -457,7 +465,7 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) if layersIterator+1 > len(layers) { olu.Log.Error().Err(errors.ErrBadLayerCount). - Msgf("error on creating layer history for imaeg %s %s", name, man.Digest) + Msgf("error on creating layer history for image %s %s", repoName, man.Digest) break } @@ -477,29 +485,35 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) score := 0 imageSummary := ImageSummary{ - RepoName: name, - Tag: tag, + RepoName: repoName, + Tag: tag, + Manifests: []ManifestSummary{ + { + Digest: manifestDigest, + ConfigDigest: configDigest, + LastUpdated: lastUpdated, + Size: size, + Platform: platform, + Layers: layers, + History: allHistory, + }, + }, 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, + Vendor: annotations.Vendor, Source: annotations.Source, - Layers: layers, - History: allHistory, } imageSummaries = append(imageSummaries, imageSummary) - if man.Digest.String() == lastUpdatedTag.Digest.String() { + if man.Digest.String() == lastUpdatedTag.Descriptor.Digest.String() { lastUpdatedImageSummary = imageSummary } } @@ -512,10 +526,10 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) size := strconv.FormatInt(repoSize, 10) - repoPlatforms := make([]OsArch, 0, len(repoPlatformsSet)) + repoPlatforms := make([]Platform, 0, len(repoPlatformsSet)) - for _, osArch := range repoPlatformsSet { - repoPlatforms = append(repoPlatforms, osArch) + for _, platform := range repoPlatformsSet { + repoPlatforms = append(repoPlatforms, platform) } repoVendors := make([]string, 0, len(repoVendorsSet)) @@ -526,7 +540,7 @@ func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) } summary := RepoSummary{ - Name: name, + Name: repoName, LastUpdated: lastUpdatedTag.Timestamp, Size: size, Platforms: repoPlatforms, diff --git a/pkg/extensions/search/convert/convert_test.go b/pkg/extensions/search/convert/convert_test.go index 86bc2668..8d64c16b 100644 --- a/pkg/extensions/search/convert/convert_test.go +++ b/pkg/extensions/search/convert/convert_test.go @@ -19,6 +19,7 @@ import ( "zotregistry.io/zot/pkg/extensions/search/common" "zotregistry.io/zot/pkg/extensions/search/convert" cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" + "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta/repodb" bolt "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper" . "zotregistry.io/zot/pkg/test" @@ -28,7 +29,7 @@ import ( var ErrTestError = errors.New("TestError") func TestConvertErrors(t *testing.T) { - Convey("", t, func() { + Convey("Convert Errors", t, func() { repoDB, err := bolt.NewBoltDBWrapper(bolt.DBParameters{ RootDir: t.TempDir(), }) @@ -59,7 +60,7 @@ func TestConvertErrors(t *testing.T) { err = repoDB.SetRepoTag("repo1", "0.1.0", digest11, ispec.MediaTypeImageManifest) So(err, ShouldBeNil) - repoMetas, manifestMetaMap, _, err := repoDB.SearchRepos(context.Background(), "", repodb.Filter{}, + repoMetas, manifestMetaMap, _, _, err := repoDB.SearchRepos(context.Background(), "", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) @@ -70,9 +71,11 @@ func TestConvertErrors(t *testing.T) { ctx, repoMetas[0], manifestMetaMap, + map[string]repodb.IndexData{}, convert.SkipQGLField{}, mocks.CveInfoMock{ - GetCVESummaryForImageFn: func(image string) (cveinfo.ImageCVESummary, error) { + GetCVESummaryForImageFn: func(repo string, reference string, + ) (cveinfo.ImageCVESummary, error) { return cveinfo.ImageCVESummary{}, ErrTestError }, }, @@ -80,6 +83,167 @@ func TestConvertErrors(t *testing.T) { So(graphql.GetErrors(ctx).Error(), ShouldContainSubstring, "unable to run vulnerability scan on tag") }) + + Convey("ImageIndex2ImageSummary errors", t, func() { + ctx := graphql.WithResponseContext(context.Background(), + graphql.DefaultErrorPresenter, graphql.DefaultRecover) + + _, _, err := convert.ImageIndex2ImageSummary( + ctx, + "repo", + "tag", + godigest.FromString("indexDigest"), + true, + repodb.RepoMetadata{}, + repodb.IndexData{ + IndexBlob: []byte("bad json"), + }, + map[string]repodb.ManifestMetadata{}, + mocks.CveInfoMock{}, + ) + So(err, ShouldNotBeNil) + }) + + Convey("ImageIndex2ImageSummary cve scanning", t, func() { + ctx := graphql.WithResponseContext(context.Background(), + graphql.DefaultErrorPresenter, graphql.DefaultRecover) + + _, _, err := convert.ImageIndex2ImageSummary( + ctx, + "repo", + "tag", + godigest.FromString("indexDigest"), + false, + repodb.RepoMetadata{}, + repodb.IndexData{ + IndexBlob: []byte("{}"), + }, + map[string]repodb.ManifestMetadata{}, + mocks.CveInfoMock{ + GetCVESummaryForImageFn: func(repo, reference string, + ) (cveinfo.ImageCVESummary, error) { + return cveinfo.ImageCVESummary{}, ErrTestError + }, + }, + ) + So(err, ShouldBeNil) + }) + + Convey("ImageManifest2ImageSummary", t, func() { + ctx := graphql.WithResponseContext(context.Background(), + graphql.DefaultErrorPresenter, graphql.DefaultRecover) + + _, _, err := convert.ImageManifest2ImageSummary( + ctx, + "repo", + "tag", + godigest.FromString("manifestDigest"), + false, + repodb.RepoMetadata{}, + repodb.ManifestMetadata{ + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }, + mocks.CveInfoMock{ + GetCVESummaryForImageFn: func(repo, reference string, + ) (cveinfo.ImageCVESummary, error) { + return cveinfo.ImageCVESummary{}, ErrTestError + }, + }, + ) + So(err, ShouldBeNil) + }) + + Convey("ImageManifest2ManifestSummary", t, func() { + ctx := graphql.WithResponseContext(context.Background(), + graphql.DefaultErrorPresenter, graphql.DefaultRecover) + + // with bad config json, error while unmarshaling + _, _, err := convert.ImageManifest2ManifestSummary( + ctx, + "repo", + "tag", + ispec.Descriptor{ + Digest: "dig", + MediaType: ispec.MediaTypeImageManifest, + }, + false, + repodb.ManifestMetadata{ + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("bad json"), + }, + mocks.CveInfoMock{ + GetCVESummaryForImageFn: func(repo, reference string, + ) (cveinfo.ImageCVESummary, error) { + return cveinfo.ImageCVESummary{}, ErrTestError + }, + }, + ) + So(err, ShouldNotBeNil) + + // CVE scan using platform + configBlob, err := json.Marshal(ispec.Image{ + Platform: ispec.Platform{ + OS: "os", + Architecture: "arch", + }, + }) + So(err, ShouldBeNil) + + _, _, err = convert.ImageManifest2ManifestSummary( + ctx, + "repo", + "tag", + ispec.Descriptor{ + Digest: "dig", + MediaType: ispec.MediaTypeImageManifest, + }, + false, + repodb.ManifestMetadata{ + ManifestBlob: []byte("{}"), + ConfigBlob: configBlob, + }, + mocks.CveInfoMock{ + GetCVESummaryForImageFn: func(repo, reference string, + ) (cveinfo.ImageCVESummary, error) { + return cveinfo.ImageCVESummary{}, ErrTestError + }, + }, + ) + So(err, ShouldBeNil) + }) + + Convey("RepoMeta2ExpandedRepoInfo", t, func() { + ctx := graphql.WithResponseContext(context.Background(), + graphql.DefaultErrorPresenter, graphql.DefaultRecover) + + // with bad config json, error while unmarshaling + _, imageSummaries := convert.RepoMeta2ExpandedRepoInfo( + ctx, + repodb.RepoMetadata{ + Tags: map[string]repodb.Descriptor{ + "tag1": {Digest: "dig", MediaType: ispec.MediaTypeImageManifest}, + }, + }, + map[string]repodb.ManifestMetadata{ + "dig": { + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("bad json"), + }, + }, + map[string]repodb.IndexData{}, + convert.SkipQGLField{ + Vulnerabilities: false, + }, + mocks.CveInfoMock{ + GetCVESummaryForImageFn: func(repo, reference string, + ) (cveinfo.ImageCVESummary, error) { + return cveinfo.ImageCVESummary{}, ErrTestError + }, + }, log.NewLogger("debug", ""), + ) + So(len(imageSummaries), ShouldEqual, 0) + }) } func TestBuildImageInfo(t *testing.T) { @@ -159,7 +323,7 @@ func TestBuildImageInfo(t *testing.T) { Layers: [][]byte{ layerblob, }, - Tag: "0.0.1", + Reference: "0.0.1", }, baseURL, imageName, @@ -174,7 +338,7 @@ func TestBuildImageInfo(t *testing.T) { imageSummary := convert.BuildImageInfo(imageName, imageName, manifestDigest, ispecManifest, imageConfig, isSigned) - So(len(imageSummary.Layers), ShouldEqual, len(ispecManifest.Layers)) + So(len(imageSummary.Manifests[0].Layers), ShouldEqual, len(ispecManifest.Layers)) imageSummaryLayerSize, err := strconv.Atoi(*imageSummary.Size) So(err, ShouldBeNil) So(imageSummaryLayerSize, ShouldEqual, manifestLayersSize) @@ -249,7 +413,7 @@ func TestBuildImageInfo(t *testing.T) { layerblob, layerblob2, }, - Tag: "0.0.1", + Reference: "0.0.1", }, baseURL, imageName, @@ -264,7 +428,7 @@ func TestBuildImageInfo(t *testing.T) { imageSummary := convert.BuildImageInfo(imageName, imageName, manifestDigest, ispecManifest, imageConfig, isSigned) - So(len(imageSummary.Layers), ShouldEqual, len(ispecManifest.Layers)) + So(len(imageSummary.Manifests[0].Layers), ShouldEqual, len(ispecManifest.Layers)) imageSummaryLayerSize, err := strconv.Atoi(*imageSummary.Size) So(err, ShouldBeNil) So(imageSummaryLayerSize, ShouldEqual, manifestLayersSize) @@ -331,7 +495,7 @@ func TestBuildImageInfo(t *testing.T) { Layers: [][]byte{ layerblob, }, - Tag: "0.0.1", + Reference: "0.0.1", }, baseURL, imageName, @@ -346,7 +510,7 @@ func TestBuildImageInfo(t *testing.T) { imageSummary := convert.BuildImageInfo(imageName, imageName, manifestDigest, ispecManifest, imageConfig, isSigned) - So(len(imageSummary.Layers), ShouldEqual, len(ispecManifest.Layers)) + So(len(imageSummary.Manifests[0].Layers), ShouldEqual, len(ispecManifest.Layers)) imageSummaryLayerSize, err := strconv.Atoi(*imageSummary.Size) So(err, ShouldBeNil) So(imageSummaryLayerSize, ShouldEqual, manifestLayersSize) diff --git a/pkg/extensions/search/convert/oci.go b/pkg/extensions/search/convert/oci.go index c819a32e..be591736 100644 --- a/pkg/extensions/search/convert/oci.go +++ b/pkg/extensions/search/convert/oci.go @@ -10,7 +10,6 @@ import ( "zotregistry.io/zot/pkg/extensions/search/common" "zotregistry.io/zot/pkg/extensions/search/gql_generated" "zotregistry.io/zot/pkg/log" - "zotregistry.io/zot/pkg/meta/repodb" ) func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest, @@ -56,14 +55,23 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest, formattedSize := strconv.FormatInt(size, 10) imageInfo := &gql_generated.ImageSummary{ - RepoName: &repo, - Tag: &tag, - Digest: &formattedManifestDigest, - ConfigDigest: &configDigest, + RepoName: &repo, + Tag: &tag, + Manifests: []*gql_generated.ManifestSummary{ + { + Digest: &formattedManifestDigest, + ConfigDigest: &configDigest, + Layers: layers, + Size: &formattedSize, + History: allHistory, + Platform: &gql_generated.Platform{ + Os: &imageConfig.OS, + Arch: &imageConfig.Architecture, + }, + LastUpdated: &lastUpdated, + }, + }, Size: &formattedSize, - Layers: layers, - History: allHistory, - Vendor: &annotations.Vendor, Description: &annotations.Description, Title: &annotations.Title, Documentation: &annotations.Documentation, @@ -71,12 +79,9 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest, Labels: &annotations.Labels, Source: &annotations.Source, Authors: &authors, + Vendor: &annotations.Vendor, LastUpdated: &lastUpdated, IsSigned: &isSigned, - Platform: &gql_generated.OsArch{ - Os: &imageConfig.OS, - Arch: &imageConfig.Architecture, - }, } return imageInfo @@ -106,15 +111,25 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest, log.Error().Err(zerr.ErrBadLayerCount).Msg("error on creating layer history for ImageSummary") return &gql_generated.ImageSummary{ - RepoName: &repo, - Tag: &tag, - Digest: &formattedManifestDigest, - ConfigDigest: &configDigest, + RepoName: &repo, + Tag: &tag, + Manifests: []*gql_generated.ManifestSummary{ + { + Digest: &formattedManifestDigest, + ConfigDigest: &configDigest, + Layers: layers, + Size: &formattedSize, + History: allHistory, + Platform: &gql_generated.Platform{ + Os: &imageConfig.OS, + Arch: &imageConfig.Architecture, + }, + LastUpdated: &lastUpdated, + }, + }, Size: &formattedSize, - Layers: layers, - History: allHistory, - Vendor: &annotations.Vendor, Description: &annotations.Description, + Vendor: &annotations.Vendor, Title: &annotations.Title, Documentation: &annotations.Documentation, Licenses: &annotations.Licenses, @@ -123,10 +138,6 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest, Authors: &authors, LastUpdated: &lastUpdated, IsSigned: &isSigned, - Platform: &gql_generated.OsArch{ - Os: &imageConfig.OS, - Arch: &imageConfig.Architecture, - }, } } @@ -152,27 +163,33 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest, formattedSize := strconv.FormatInt(size, 10) imageInfo := &gql_generated.ImageSummary{ - RepoName: &repo, - Tag: &tag, - Digest: &formattedManifestDigest, - ConfigDigest: &configDigest, + RepoName: &repo, + Tag: &tag, + Manifests: []*gql_generated.ManifestSummary{ + { + Digest: &formattedManifestDigest, + ConfigDigest: &configDigest, + Layers: layers, + History: allHistory, + Platform: &gql_generated.Platform{ + Os: &imageConfig.OS, + Arch: &imageConfig.Architecture, + }, + Size: &formattedSize, + LastUpdated: &lastUpdated, + }, + }, Size: &formattedSize, - Layers: layers, - History: allHistory, - Vendor: &annotations.Vendor, Description: &annotations.Description, Title: &annotations.Title, Documentation: &annotations.Documentation, Licenses: &annotations.Licenses, Labels: &annotations.Labels, Source: &annotations.Source, + Vendor: &annotations.Vendor, Authors: &authors, LastUpdated: &lastUpdated, IsSigned: &isSigned, - Platform: &gql_generated.OsArch{ - Os: &imageConfig.OS, - Arch: &imageConfig.Architecture, - }, } return imageInfo @@ -180,23 +197,12 @@ func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest, // updateRepoBlobsMap adds all the image blobs and their respective size to the repo blobs map // and returnes the total size of the image. -func updateRepoBlobsMap(manifestDigest string, manifestSize int64, configDigest string, configSize int64, - layers []ispec.Descriptor, repoBlob2Size map[string]int64, -) int64 { +func updateRepoBlobsMap(imageBlobs map[string]int64, repoBlob2Size map[string]int64) int64 { imgSize := int64(0) - // add config size - imgSize += configSize - repoBlob2Size[configDigest] = configSize - - // add manifest size - imgSize += manifestSize - repoBlob2Size[manifestDigest] = manifestSize - - // add layers size - for _, layer := range layers { - repoBlob2Size[layer.Digest.String()] = layer.Size - imgSize += layer.Size + for digest, size := range imageBlobs { + repoBlob2Size[digest] = size + imgSize += size } return imgSize @@ -267,14 +273,3 @@ func getAllHistory(manifestContent ispec.Manifest, configContent ispec.Image) ( return allHistory, nil } - -func imageHasSignatures(signatures repodb.ManifestSignatures) bool { - // (sigType, signatures) - for _, sigs := range signatures { - if len(sigs) > 0 { - return true - } - } - - return false -} diff --git a/pkg/extensions/search/convert/repodb.go b/pkg/extensions/search/convert/repodb.go index 714b063b..f50b740d 100644 --- a/pkg/extensions/search/convert/repodb.go +++ b/pkg/extensions/search/convert/repodb.go @@ -9,12 +9,15 @@ import ( "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" "zotregistry.io/zot/pkg/extensions/search/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" ) @@ -23,11 +26,12 @@ type SkipQGLField struct { } func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata, - manifestMetaMap map[string]repodb.ManifestMetadata, skip SkipQGLField, cveInfo cveinfo.CveInfo, + manifestMetaMap map[string]repodb.ManifestMetadata, indexDataMap map[string]repodb.IndexData, + skip SkipQGLField, cveInfo cveinfo.CveInfo, ) *gql_generated.RepoSummary { var ( repoLastUpdatedTimestamp = time.Time{} - repoPlatformsSet = map[string]*gql_generated.OsArch{} + repoPlatformsSet = map[string]*gql_generated.Platform{} repoVendorsSet = map[string]bool{} lastUpdatedImageSummary *gql_generated.ImageSummary repoStarCount = repoMeta.Stars @@ -45,107 +49,32 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata, ) for tag, descriptor := range repoMeta.Tags { - var ( - manifestContent ispec.Manifest - manifestDigest = descriptor.Digest - imageSignatures = repoMeta.Signatures[descriptor.Digest] - ) - - err := json.Unmarshal(manifestMetaMap[manifestDigest].ManifestBlob, &manifestContent) + imageSummary, imageBlobsMap, err := Descriptor2ImageSummary(ctx, descriptor, repoMeta.Name, tag, true, repoMeta, + manifestMetaMap, indexDataMap, cveInfo) if err != nil { - graphql.AddError(ctx, gqlerror.Errorf("can't unmarshal manifest blob for image: %s:%s, manifest digest: %s, "+ - "error: %s", repoMeta.Name, tag, manifestDigest, err.Error())) - continue } - var configContent ispec.Image - - err = json.Unmarshal(manifestMetaMap[manifestDigest].ConfigBlob, &configContent) - if err != nil { - graphql.AddError(ctx, gqlerror.Errorf("can't unmarshal config blob for image: %s:%s, manifest digest: %s, error: %s", - repoMeta.Name, tag, manifestDigest, err.Error())) - - continue + for blobDigest, blobSize := range imageBlobsMap { + repoBlob2Size[blobDigest] = blobSize } - var ( - tag = tag - isSigned = imageHasSignatures(imageSignatures) - configDigest = manifestContent.Config.Digest.String() - configSize = manifestContent.Config.Size - opSys = configContent.OS - arch = configContent.Architecture - osArch = gql_generated.OsArch{Os: &opSys, Arch: &arch} - imageLastUpdated = common.GetImageLastUpdated(configContent) - downloadCount = repoMeta.Statistics[descriptor.Digest].DownloadCount + for _, manifestSummary := range imageSummary.Manifests { + if *manifestSummary.Platform.Os != "" || *manifestSummary.Platform.Arch != "" { + opSys, arch := *manifestSummary.Platform.Os, *manifestSummary.Platform.Arch - size = updateRepoBlobsMap( - manifestDigest, int64(len(manifestMetaMap[manifestDigest].ManifestBlob)), - configDigest, configSize, - manifestContent.Layers, - repoBlob2Size) - imageSize = strconv.FormatInt(size, 10) - ) + platformString := strings.TrimSpace(fmt.Sprintf("%s %s", opSys, arch)) + repoPlatformsSet[platformString] = &gql_generated.Platform{Os: &opSys, Arch: &arch} + } - annotations := common.GetAnnotations(manifestContent.Annotations, configContent.Config.Labels) - - authors := annotations.Authors - if authors == "" { - authors = configContent.Author + repoDownloadCount += manifestMetaMap[*manifestSummary.Digest].DownloadCount } - 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, repoMeta.Name, manifestDigest, err.Error())) + if *imageSummary.Vendor != "" { + repoVendorsSet[*imageSummary.Vendor] = true } - imageCveSummary := cveinfo.ImageCVESummary{} - - imageSummary := gql_generated.ImageSummary{ - RepoName: &repoName, - Tag: &tag, - Digest: &manifestDigest, - ConfigDigest: &configDigest, - LastUpdated: &imageLastUpdated, - IsSigned: &isSigned, - Size: &imageSize, - Platform: &osArch, - Vendor: &annotations.Vendor, - DownloadCount: &downloadCount, - Layers: getLayersSummaries(manifestContent), - Description: &annotations.Description, - Title: &annotations.Title, - Documentation: &annotations.Documentation, - Licenses: &annotations.Licenses, - Labels: &annotations.Labels, - Source: &annotations.Source, - Authors: &authors, - History: historyEntries, - Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{ - MaxSeverity: &imageCveSummary.MaxSeverity, - Count: &imageCveSummary.Count, - }, - } - - if annotations.Vendor != "" { - repoVendorsSet[annotations.Vendor] = true - } - - if opSys != "" || arch != "" { - osArchString := strings.TrimSpace(fmt.Sprintf("%s %s", opSys, arch)) - repoPlatformsSet[osArchString] = &gql_generated.OsArch{Os: &opSys, Arch: &arch} - } - - if repoLastUpdatedTimestamp.Equal(time.Time{}) { - // initialize with first time value - repoLastUpdatedTimestamp = imageLastUpdated - lastUpdatedImageSummary = &imageSummary - } else if repoLastUpdatedTimestamp.Before(imageLastUpdated) { - repoLastUpdatedTimestamp = imageLastUpdated - lastUpdatedImageSummary = &imageSummary - } + lastUpdatedImageSummary = UpdateLastUpdatedTimestam(&repoLastUpdatedTimestamp, lastUpdatedImageSummary, imageSummary) repoDownloadCount += repoMeta.Statistics[descriptor.Digest].DownloadCount } @@ -158,9 +87,9 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata, repoSize := strconv.FormatInt(size, 10) score := 0 - repoPlatforms := make([]*gql_generated.OsArch, 0, len(repoPlatformsSet)) - for _, osArch := range repoPlatformsSet { - repoPlatforms = append(repoPlatforms, osArch) + repoPlatforms := make([]*gql_generated.Platform, 0, len(repoPlatformsSet)) + for _, platform := range repoPlatformsSet { + repoPlatforms = append(repoPlatforms, platform) } repoVendors := make([]*string, 0, len(repoVendorsSet)) @@ -173,9 +102,7 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata, // 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 { - imageName := fmt.Sprintf("%s:%s", repoMeta.Name, *lastUpdatedImageSummary.Tag) - - imageCveSummary, err := cveInfo.GetCVESummaryForImage(imageName) + imageCveSummary, err := cveInfo.GetCVESummaryForImage(repoMeta.Name, *lastUpdatedImageSummary.Tag) if err != nil { // Log the error, but we should still include the image in results graphql.AddError( @@ -208,121 +135,393 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata, } } +func UpdateLastUpdatedTimestam(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{}, nil + } +} + +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) + ) + + for _, descriptor := range indexContent.Manifests { + manifestSummary, manifestBlobs, err := ImageManifest2ManifestSummary(ctx, repo, tag, descriptor, false, + manifestMetaMap[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 := cveinfo.ImageCVESummary{} + + if cveInfo != nil && !skipCVE { + imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag) + + 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 := common.GetAnnotations(indexContent.Annotations, map[string]string{}) + + indexSummary := gql_generated.ImageSummary{ + RepoName: &repo, + Tag: &tag, + Manifests: manifestSummaries, + LastUpdated: &indexLastUpdated, + IsSigned: &isSigned, + 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, + }, + } + + 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() + ) + + 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 + 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 := common.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 := cveinfo.ImageCVESummary{} + + if cveInfo != nil && !skipCVE { + imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag) + + 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())) + } + } + + imageSummary := gql_generated.ImageSummary{ + RepoName: &repoName, + Tag: &tag, + Manifests: []*gql_generated.ManifestSummary{ + { + Digest: &manifestDigest, + ConfigDigest: &configDigest, + LastUpdated: &imageLastUpdated, + Size: &imageSize, + Platform: &platform, + DownloadCount: &downloadCount, + Layers: getLayersSummaries(manifestContent), + History: historyEntries, + Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{ + MaxSeverity: &imageCveSummary.MaxSeverity, + Count: &imageCveSummary.Count, + }, + }, + }, + LastUpdated: &imageLastUpdated, + IsSigned: &isSigned, + 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, + }, + } + + return &imageSummary, imageBlobsMap, nil +} + +func ImageManifest2ManifestSummary(ctx context.Context, repo, tag string, descriptor ispec.Descriptor, + skipCVE bool, manifestMeta repodb.ManifestMetadata, 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 + imageLastUpdated = common.GetImageLastUpdated(configContent) + downloadCount = manifestMeta.DownloadCount + ) + + 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 := cveinfo.ImageCVESummary{} + + if cveInfo != nil && !skipCVE { + imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag) + + 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())) + } + } + + manifestSummary := gql_generated.ManifestSummary{ + Digest: &manifestDigestStr, + ConfigDigest: &configDigest, + LastUpdated: &imageLastUpdated, + Size: &imageSize, + Platform: &platform, + DownloadCount: &downloadCount, + Layers: getLayersSummaries(manifestContent), + History: historyEntries, + Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{ + MaxSeverity: &imageCveSummary.MaxSeverity, + Count: &imageCveSummary.Count, + }, + } + + 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, skip SkipQGLField, cveInfo cveinfo.CveInfo, + 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)) for tag, descriptor := range repoMeta.Tags { - var ( - manifestContent ispec.Manifest - manifestDigest = descriptor.Digest - imageSignatures = repoMeta.Signatures[descriptor.Digest] - ) - - err := json.Unmarshal(manifestMetaMap[manifestDigest].ManifestBlob, &manifestContent) + imageSummary, _, err := Descriptor2ImageSummary(ctx, descriptor, repoMeta.Name, tag, skip.Vulnerabilities, + repoMeta, manifestMetaMap, indexDataMap, cveInfo) if err != nil { - graphql.AddError(ctx, gqlerror.Errorf("can't unmarshal manifest blob for image: %s:%s, "+ - "manifest digest: %s, error: %s", repoMeta.Name, tag, manifestDigest, err.Error())) - continue } - var configContent ispec.Image - - err = json.Unmarshal(manifestMetaMap[manifestDigest].ConfigBlob, &configContent) - if err != nil { - graphql.AddError(ctx, gqlerror.Errorf("can't unmarshal config blob for image: %s:%s, "+ - "manifest digest: %s, error: %s", repoMeta.Name, tag, manifestDigest, err.Error())) - - continue - } - - imageCveSummary := cveinfo.ImageCVESummary{} - // Check if vulnerability scanning is disabled - if cveInfo != nil && !skip.Vulnerabilities { - imageName := fmt.Sprintf("%s:%s", repoMeta.Name, tag) - imageCveSummary, err = cveInfo.GetCVESummaryForImage(imageName) - - 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, repoMeta.Name, manifestDigest, err.Error())) - } - } - - imgSize := int64(0) - imgSize += manifestContent.Config.Size - imgSize += int64(len(manifestMetaMap[manifestDigest].ManifestBlob)) - - for _, layer := range manifestContent.Layers { - imgSize += layer.Size - } - - var ( - repoName = repoMeta.Name - tag = tag - configDigest = manifestContent.Config.Digest.String() - imageLastUpdated = common.GetImageLastUpdated(configContent) - isSigned = imageHasSignatures(imageSignatures) - imageSize = strconv.FormatInt(imgSize, 10) - os = configContent.OS - arch = configContent.Architecture - osArch = gql_generated.OsArch{Os: &os, Arch: &arch} - downloadCount = repoMeta.Statistics[descriptor.Digest].DownloadCount - ) - - annotations := common.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, repoMeta.Name, manifestDigest, err.Error())) - } - - imageSummary := gql_generated.ImageSummary{ - RepoName: &repoName, - Tag: &tag, - Digest: &manifestDigest, - ConfigDigest: &configDigest, - LastUpdated: &imageLastUpdated, - IsSigned: &isSigned, - Size: &imageSize, - Platform: &osArch, - Vendor: &annotations.Vendor, - DownloadCount: &downloadCount, - Layers: getLayersSummaries(manifestContent), - Description: &annotations.Description, - Title: &annotations.Title, - Documentation: &annotations.Documentation, - Licenses: &annotations.Licenses, - Labels: &annotations.Labels, - Source: &annotations.Source, - Authors: &authors, - History: historyEntries, - Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{ - MaxSeverity: &imageCveSummary.MaxSeverity, - Count: &imageCveSummary.Count, - }, - } - - imageSummaries = append(imageSummaries, &imageSummary) + imageSummaries = append(imageSummaries, imageSummary) } return imageSummaries } func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta repodb.RepoMetadata, - manifestMetaMap map[string]repodb.ManifestMetadata, skip SkipQGLField, cveInfo cveinfo.CveInfo, + 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 ( repoLastUpdatedTimestamp = time.Time{} - repoPlatformsSet = map[string]*gql_generated.OsArch{} + repoPlatformsSet = map[string]*gql_generated.Platform{} repoVendorsSet = map[string]bool{} lastUpdatedImageSummary *gql_generated.ImageSummary repoStarCount = repoMeta.Stars @@ -342,104 +541,33 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta repodb.RepoMetadata ) for tag, descriptor := range repoMeta.Tags { - var ( - manifestContent ispec.Manifest - manifestDigest = descriptor.Digest - imageSignatures = repoMeta.Signatures[descriptor.Digest] - ) - - err := json.Unmarshal(manifestMetaMap[manifestDigest].ManifestBlob, &manifestContent) + imageSummary, imageBlobs, err := Descriptor2ImageSummary(ctx, descriptor, repoName, tag, + skip.Vulnerabilities, repoMeta, manifestMetaMap, indexDataMap, cveInfo) if err != nil { - graphql.AddError(ctx, gqlerror.Errorf("can't unmarshal manifest blob for image: %s:%s, manifest digest: %s, "+ - "error: %s", repoMeta.Name, tag, manifestDigest, err.Error())) + log.Error().Msgf("repodb: erorr while converting descriptor for image '%s:%s'", repoName, tag) continue } - var configContent ispec.Image + 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} + } - err = json.Unmarshal(manifestMetaMap[manifestDigest].ConfigBlob, &configContent) - if err != nil { - graphql.AddError(ctx, gqlerror.Errorf("can't unmarshal config blob for image: %s:%s, manifest digest: %s, error: %s", - repoMeta.Name, tag, manifestDigest, err.Error())) - - continue + updateRepoBlobsMap(imageBlobs, repoBlob2Size) } - var ( - tag = tag - isSigned = imageHasSignatures(imageSignatures) - configDigest = manifestContent.Config.Digest.String() - configSize = manifestContent.Config.Size - opSys = configContent.OS - arch = configContent.Architecture - osArch = gql_generated.OsArch{Os: &opSys, Arch: &arch} - imageLastUpdated = common.GetImageLastUpdated(configContent) - downloadCount = repoMeta.Statistics[descriptor.Digest].DownloadCount - - size = updateRepoBlobsMap( - manifestDigest, int64(len(manifestMetaMap[manifestDigest].ManifestBlob)), - configDigest, configSize, - manifestContent.Layers, - repoBlob2Size) - imageSize = strconv.FormatInt(size, 10) - ) - - annotations := common.GetAnnotations(manifestContent.Annotations, configContent.Config.Labels) - - authors := annotations.Authors - if authors == "" { - authors = configContent.Author + if *imageSummary.Vendor != "" { + repoVendorsSet[*imageSummary.Vendor] = true } - imageCveSummary := cveinfo.ImageCVESummary{} + lastUpdatedImageSummary = UpdateLastUpdatedTimestam(&repoLastUpdatedTimestamp, lastUpdatedImageSummary, imageSummary) - imageSummary := gql_generated.ImageSummary{ - RepoName: &repoName, - Tag: &tag, - Digest: &manifestDigest, - ConfigDigest: &configDigest, - LastUpdated: &imageLastUpdated, - IsSigned: &isSigned, - Size: &imageSize, - Platform: &osArch, - Vendor: &annotations.Vendor, - DownloadCount: &downloadCount, - Layers: getLayersSummaries(manifestContent), - Description: &annotations.Description, - Title: &annotations.Title, - Documentation: &annotations.Documentation, - Licenses: &annotations.Licenses, - Labels: &annotations.Labels, - Source: &annotations.Source, - Authors: &authors, - Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{ - MaxSeverity: &imageCveSummary.MaxSeverity, - Count: &imageCveSummary.Count, - }, - } + repoDownloadCount += *imageSummary.DownloadCount - imageSummaries = append(imageSummaries, &imageSummary) - - if annotations.Vendor != "" { - repoVendorsSet[annotations.Vendor] = true - } - - if opSys != "" || arch != "" { - osArchString := strings.TrimSpace(fmt.Sprintf("%s %s", opSys, arch)) - repoPlatformsSet[osArchString] = &gql_generated.OsArch{Os: &opSys, Arch: &arch} - } - - if repoLastUpdatedTimestamp.Equal(time.Time{}) { - // initialize with first time value - repoLastUpdatedTimestamp = imageLastUpdated - lastUpdatedImageSummary = &imageSummary - } else if repoLastUpdatedTimestamp.Before(imageLastUpdated) { - repoLastUpdatedTimestamp = imageLastUpdated - lastUpdatedImageSummary = &imageSummary - } - - repoDownloadCount += repoMeta.Statistics[descriptor.Digest].DownloadCount + imageSummaries = append(imageSummaries, imageSummary) } // calculate repo size = sum all manifest, config and layer blobs sizes @@ -450,9 +578,9 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta repodb.RepoMetadata repoSize := strconv.FormatInt(size, 10) score := 0 - repoPlatforms := make([]*gql_generated.OsArch, 0, len(repoPlatformsSet)) - for _, osArch := range repoPlatformsSet { - repoPlatforms = append(repoPlatforms, osArch) + repoPlatforms := make([]*gql_generated.Platform, 0, len(repoPlatformsSet)) + for _, platform := range repoPlatformsSet { + repoPlatforms = append(repoPlatforms, platform) } repoVendors := make([]*string, 0, len(repoVendorsSet)) @@ -461,13 +589,10 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta repodb.RepoMetadata 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 { - imageName := fmt.Sprintf("%s:%s", repoMeta.Name, *lastUpdatedImageSummary.Tag) - - imageCveSummary, err := cveInfo.GetCVESummaryForImage(imageName) + imageCveSummary, err := cveInfo.GetCVESummaryForImage(repoMeta.Name, *lastUpdatedImageSummary.Tag) if err != nil { // Log the error, but we should still include the image in results graphql.AddError( diff --git a/pkg/extensions/search/cve/cve.go b/pkg/extensions/search/cve/cve.go index ef398135..e46cb004 100644 --- a/pkg/extensions/search/cve/cve.go +++ b/pkg/extensions/search/cve/cve.go @@ -7,6 +7,7 @@ import ( godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" + "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/extensions/search/common" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" "zotregistry.io/zot/pkg/extensions/search/cve/trivy" @@ -18,15 +19,15 @@ import ( type CveInfo interface { GetImageListForCVE(repo, cveID string) ([]common.TagInfo, error) GetImageListWithCVEFixed(repo, cveID string) ([]common.TagInfo, error) - GetCVEListForImage(image string, pageinput PageInput) ([]cvemodel.CVE, PageInfo, error) - GetCVESummaryForImage(image string) (ImageCVESummary, error) + GetCVEListForImage(repo, tag string, pageinput PageInput) ([]cvemodel.CVE, PageInfo, error) + GetCVESummaryForImage(repo, tag string) (ImageCVESummary, error) CompareSeverities(severity1, severity2 string) int UpdateDB() error } type Scanner interface { ScanImage(image string) (map[string]cvemodel.CVE, error) - IsImageFormatScannable(image string) (bool, error) + IsImageFormatScannable(repo, tag string) (bool, error) CompareSeverities(severity1, severity2 string) int UpdateDB() error } @@ -66,48 +67,37 @@ func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]common.TagI } for tag, descriptor := range repoMeta.Tags { - manifestDigestStr := descriptor.Digest + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + manifestDigestStr := descriptor.Digest - manifestDigest, err := godigest.Parse(manifestDigestStr) - if err != nil { - cveinfo.Log.Error().Err(err).Str("repo", repo).Str("tag", tag). - Str("cve-id", cveID).Str("digest", manifestDigestStr).Msg("unable to parse digest") + manifestDigest := godigest.Digest(manifestDigestStr) - return nil, err - } + isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag) + if !isScanableImage || err != nil { + cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image is not scanable") - manifestMeta, err := cveinfo.RepoDB.GetManifestMeta(repo, manifestDigest) - if err != nil { - return nil, err - } + continue + } - var manifestContent ispec.Manifest + cveMap, err := cveinfo.Scanner.ScanImage(getImageString(repo, tag)) + if err != nil { + cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image scan failed") - err = json.Unmarshal(manifestMeta.ManifestBlob, &manifestContent) - if err != nil { - cveinfo.Log.Error().Err(err).Str("repo", repo).Str("tag", tag). - Str("cve-id", cveID).Msg("unable to unmashal manifest blob") + continue + } - continue - } - - image := fmt.Sprintf("%s:%s", repo, tag) - - isValidImage, _ := cveinfo.Scanner.IsImageFormatScannable(image) - if !isValidImage { - continue - } - - cveMap, err := cveinfo.Scanner.ScanImage(image) - if err != nil { - continue - } - - if _, hasCVE := cveMap[cveID]; hasCVE { - imgList = append(imgList, common.TagInfo{ - Name: tag, - Digest: manifestDigest, - }) + if _, hasCVE := cveMap[cveID]; hasCVE { + imgList = append(imgList, common.TagInfo{ + Name: tag, + Descriptor: common.Descriptor{ + Digest: manifestDigest, + MediaType: descriptor.MediaType, + }, + }) + } + default: + cveinfo.Log.Error().Msgf("type '%s' not supported for scanning", descriptor.MediaType) } } @@ -126,67 +116,87 @@ func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]commo vulnerableTags := make([]common.TagInfo, 0) allTags := make([]common.TagInfo, 0) + var hasCVE bool + for tag, descriptor := range repoMeta.Tags { manifestDigestStr := descriptor.Digest - manifestDigest, err := godigest.Parse(manifestDigestStr) - if err != nil { - cveinfo.Log.Error().Err(err).Str("repo", repo).Str("tag", tag). - Str("cve-id", cveID).Str("digest", manifestDigestStr).Msg("unable to parse digest") + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + manifestDigest, err := godigest.Parse(manifestDigestStr) + if err != nil { + cveinfo.Log.Error().Err(err).Str("repo", repo).Str("tag", tag). + Str("cve-id", cveID).Str("digest", manifestDigestStr).Msg("unable to parse digest") - continue - } + continue + } - manifestMeta, err := cveinfo.RepoDB.GetManifestMeta(repo, manifestDigest) - if err != nil { - cveinfo.Log.Error().Err(err).Str("repo", repo).Str("tag", tag). - Str("cve-id", cveID).Msg("unable to obtain manifest meta") + manifestMeta, err := cveinfo.RepoDB.GetManifestMeta(repo, manifestDigest) + if err != nil { + cveinfo.Log.Error().Err(err).Str("repo", repo).Str("tag", tag). + Str("cve-id", cveID).Msg("unable to obtain manifest meta") - continue - } + continue + } - var configContent ispec.Image + var configContent ispec.Image - err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent) - if err != nil { - cveinfo.Log.Error().Err(err).Str("repo", repo).Str("tag", tag). - Str("cve-id", cveID).Msg("unable to unmashal manifest blob") + err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent) + if err != nil { + cveinfo.Log.Error().Err(err).Str("repo", repo).Str("tag", tag). + Str("cve-id", cveID).Msg("unable to unmashal manifest blob") - continue - } + continue + } - tagInfo := common.TagInfo{ - Name: tag, - Timestamp: common.GetImageLastUpdated(configContent), - Digest: manifestDigest, - } + tagInfo := common.TagInfo{ + Name: tag, + Timestamp: common.GetImageLastUpdated(configContent), + Descriptor: common.Descriptor{Digest: manifestDigest, MediaType: descriptor.MediaType}, + } - allTags = append(allTags, tagInfo) + allTags = append(allTags, tagInfo) - image := fmt.Sprintf("%s:%s", repo, tag) + image := fmt.Sprintf("%s:%s", repo, tag) - isValidImage, _ := cveinfo.Scanner.IsImageFormatScannable(image) - if !isValidImage { - cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID). - Msg("image media type not supported for scanning, adding as a vulnerable image") + isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag) + if !isValidImage || err != nil { + cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID). + Msg("image media type not supported for scanning, adding as a vulnerable image") - vulnerableTags = append(vulnerableTags, tagInfo) + vulnerableTags = append(vulnerableTags, tagInfo) - continue - } + continue + } - cveMap, err := cveinfo.Scanner.ScanImage(image) - if err != nil { - cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID). - Msg("scanning failed, adding as a vulnerable image") + cveMap, err := cveinfo.Scanner.ScanImage(getImageString(repo, tag)) + if err != nil { + cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID). + Msg("scanning failed, adding as a vulnerable image") - vulnerableTags = append(vulnerableTags, tagInfo) + vulnerableTags = append(vulnerableTags, tagInfo) - continue - } + continue + } - if _, hasCVE := cveMap[cveID]; hasCVE { - vulnerableTags = append(vulnerableTags, tagInfo) + hasCVE = false + + for id := range cveMap { + if id == cveID { + hasCVE = true + + break + } + } + + if hasCVE { + vulnerableTags = append(vulnerableTags, tagInfo) + } + default: + cveinfo.Log.Error().Msgf("media type not supported '%s'", descriptor.MediaType) + + return []common.TagInfo{}, + fmt.Errorf("media type '%s' is not supported: %w", descriptor.MediaType, errors.ErrNotImplemented) } } @@ -205,16 +215,18 @@ func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]commo return fixedTags, nil } -func (cveinfo BaseCveInfo) GetCVEListForImage(image string, pageInput PageInput) ( +func (cveinfo BaseCveInfo) GetCVEListForImage(repo, tag string, pageInput PageInput) ( []cvemodel.CVE, PageInfo, error, ) { - isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(image) + isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag) if !isValidImage { return []cvemodel.CVE{}, PageInfo{}, err } + image := getImageString(repo, tag) + cveMap, err := cveinfo.Scanner.ScanImage(image) if err != nil { return []cvemodel.CVE{}, PageInfo{}, err @@ -234,7 +246,8 @@ func (cveinfo BaseCveInfo) GetCVEListForImage(image string, pageInput PageInput) return cveList, pageInfo, nil } -func (cveinfo BaseCveInfo) GetCVESummaryForImage(image string) (ImageCVESummary, error) { +func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, tag string, +) (ImageCVESummary, error) { // There are several cases, expected returned values below: // not scannable / error during scan - max severity "" - cve count 0 - Errors // scannable no issues found - max severity "NONE" - cve count 0 - no Errors @@ -244,11 +257,13 @@ func (cveinfo BaseCveInfo) GetCVESummaryForImage(image string) (ImageCVESummary, MaxSeverity: "", } - isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(image) + isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag) if !isValidImage { return imageCVESummary, err } + image := getImageString(repo, tag) + cveMap, err := cveinfo.Scanner.ScanImage(image) if err != nil { return imageCVESummary, err @@ -272,6 +287,16 @@ func (cveinfo BaseCveInfo) GetCVESummaryForImage(image string) (ImageCVESummary, return imageCVESummary, nil } +func getImageString(repo, reference string) string { + image := repo + ":" + reference + + if common.ReferenceIsDigest(reference) { + image = repo + "@" + reference + } + + return image +} + func (cveinfo BaseCveInfo) UpdateDB() error { return cveinfo.Scanner.UpdateDB() } diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index 1dac242c..72800648 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -27,7 +27,6 @@ import ( "zotregistry.io/zot/pkg/api/constants" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/monitoring" - "zotregistry.io/zot/pkg/extensions/search/common" cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" "zotregistry.io/zot/pkg/log" @@ -148,7 +147,7 @@ func generateTestData(dbDir string) error { //nolint: gocyclo return err } - content = fmt.Sprintf(`{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:2a9b097b4e4c613dd8185eba55163201a221909f3d430f8df87cd3639afc5929","size":1240,"annotations":{"org.opencontainers.image.ref.name":"commit-aaa7c6e7-squashfs"},"platform":{"architecture":"amd64","os":"linux"}}]} + content = fmt.Sprint(`{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:2a9b097b4e4c613dd8185eba55163201a221909f3d430f8df87cd3639afc5929","size":1240,"annotations":{"org.opencontainers.image.ref.name":"commit-aaa7c6e7-squashfs"},"platform":{"architecture":"amd64","os":"linux"}}]} `) err = makeTestFile(path.Join(dbDir, "zot-squashfs-invalid-blob", "index.json"), content) @@ -156,7 +155,7 @@ func generateTestData(dbDir string) error { //nolint: gocyclo return err } - content = fmt.Sprintf(`{"schemaVersion":2,"config"{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:4b37d4133908ac9a3032ba996020f2ad41354a616b071ca7e726a1df18a0f354","size":1691},"layers":[{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:a01a66356aace53222e92fb6fd990b23eb44ab0e58dff6f853fa9f771ecf3ac5","size":54996992},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:91c26d6934ef2b5c5c4d8458af9bfc4ca46cf90c22380193154964abc8298a7a","size":52330496},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:f281a550ca49746cfc6b8f1ac52f8086b3d5845db2ca18fde980dab62ae3bf7d","size":23343104},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:7ee02568717acdda336c9d56d4dc6ea7f3b1c553e43bb0c0ecc6fd3bbd059d1a","size":5910528},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:8fb33b130588b239235dedd560cdf49d29bbf6f2db5419ac68e4592a85c1f416","size":123269120},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:1b49f0b33d4a696bb94d84c9acab3623e2c195bfb446d446a583a2f9f27b04c3","size":113901568}],"annotations":{"com.cisco.stacker.git_version":"7-dev19-63-gaaa7c6e7","ws.tycho.stacker.git_version":"0.3.26"}} + content = fmt.Sprint(`{"schemaVersion":2,"config"{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:4b37d4133908ac9a3032ba996020f2ad41354a616b071ca7e726a1df18a0f354","size":1691},"layers":[{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:a01a66356aace53222e92fb6fd990b23eb44ab0e58dff6f853fa9f771ecf3ac5","size":54996992},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:91c26d6934ef2b5c5c4d8458af9bfc4ca46cf90c22380193154964abc8298a7a","size":52330496},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:f281a550ca49746cfc6b8f1ac52f8086b3d5845db2ca18fde980dab62ae3bf7d","size":23343104},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:7ee02568717acdda336c9d56d4dc6ea7f3b1c553e43bb0c0ecc6fd3bbd059d1a","size":5910528},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:8fb33b130588b239235dedd560cdf49d29bbf6f2db5419ac68e4592a85c1f416","size":123269120},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:1b49f0b33d4a696bb94d84c9acab3623e2c195bfb446d446a583a2f9f27b04c3","size":113901568}],"annotations":{"com.cisco.stacker.git_version":"7-dev19-63-gaaa7c6e7","ws.tycho.stacker.git_version":"0.3.26"}} `) err = makeTestFile(path.Join(dbDir, "zot-squashfs-invalid-blob", "blobs/sha256", "2a9b097b4e4c613dd8185eba55163201a221909f3d430f8df87cd3639afc5929"), content) @@ -187,49 +186,49 @@ func generateTestData(dbDir string) error { //nolint: gocyclo return err } - content = fmt.Sprintf(`{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.25"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:45df53588e59759a12bd3eca553cdc9063939baac9a28d7ebb6101e4ec230b76","size":873,"annotations":{"org.opencontainers.image.ref.name":"0.3.22-squashfs"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:71448405a4b89539fcfa581afb4dc7d257f98857686b8138b08a1c539f313099","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.19"},"platform":{"architecture":"amd64","os":"linux"}}]}`) + content = fmt.Sprint(`{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.25"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:45df53588e59759a12bd3eca553cdc9063939baac9a28d7ebb6101e4ec230b76","size":873,"annotations":{"org.opencontainers.image.ref.name":"0.3.22-squashfs"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:71448405a4b89539fcfa581afb4dc7d257f98857686b8138b08a1c539f313099","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.19"},"platform":{"architecture":"amd64","os":"linux"}}]}`) err = makeTestFile(path.Join(dbDir, "zot-squashfs-test", "index.json"), content) if err != nil { return err } - content = fmt.Sprintf(`{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:c5c2fd2b07ad4cb025cf20936d6bce6085584b8377780599be4da8a91739f0e8","size":1738},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:3414b5ef0ad2f0390daaf55b63c422eeedef6191d47036a69d8ee396fabdce72","size":58993484},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:a3b04fff744c13dfa4883e01fa35e01af8daa7f72d9e9b6b7fad1f28843846b6","size":55631733},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:754f517f58f302190aa94e025c25890c18e1e811127aed1ef25c189278ec4ab0","size":113612795},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:ec004cd43488b803d6e232599e83a3164394d44fcd9f44755fed7b5791087ede","size":108889651}],"annotations":{"ws.tycho.stacker.git_version":"0.3.19"}}`) + content = fmt.Sprint(`{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:c5c2fd2b07ad4cb025cf20936d6bce6085584b8377780599be4da8a91739f0e8","size":1738},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:3414b5ef0ad2f0390daaf55b63c422eeedef6191d47036a69d8ee396fabdce72","size":58993484},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:a3b04fff744c13dfa4883e01fa35e01af8daa7f72d9e9b6b7fad1f28843846b6","size":55631733},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:754f517f58f302190aa94e025c25890c18e1e811127aed1ef25c189278ec4ab0","size":113612795},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:ec004cd43488b803d6e232599e83a3164394d44fcd9f44755fed7b5791087ede","size":108889651}],"annotations":{"ws.tycho.stacker.git_version":"0.3.19"}}`) err = makeTestFile(path.Join(dbDir, "zot-squashfs-test", "blobs/sha256", "71448405a4b89539fcfa581afb4dc7d257f98857686b8138b08a1c539f313099"), content) if err != nil { return err } - content = fmt.Sprintf(`{"created": "2020-04-08T05:32:49.805795564Z","author": "","architecture": "amd64","os": "linux","config": {"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs": {"type": "layers","diff_ids": []},"history": [{"created": "2020-04-08T05:08:43.590117872Z","created_by": "stacker umoci repack"}, {"created": "2020-04-08T05:08:53.213437118Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:12:15.999154739Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:12:31.0513552Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:20:38.068800557Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:21:01.956154957Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:32:24.582132274Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:32:49.805795564Z","created_by": "stacker build","author": "","empty_layer": true}]}`) + content = fmt.Sprint(`{"created": "2020-04-08T05:32:49.805795564Z","author": "","architecture": "amd64","os": "linux","config": {"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs": {"type": "layers","diff_ids": []},"history": [{"created": "2020-04-08T05:08:43.590117872Z","created_by": "stacker umoci repack"}, {"created": "2020-04-08T05:08:53.213437118Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:12:15.999154739Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:12:31.0513552Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:20:38.068800557Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:21:01.956154957Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:32:24.582132274Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:32:49.805795564Z","created_by": "stacker build","author": "","empty_layer": true}]}`) err = makeTestFile(path.Join(dbDir, "zot-squashfs-test", "blobs/sha256", "c5c2fd2b07ad4cb025cf20936d6bce6085584b8377780599be4da8a91739f0e8"), content) if err != nil { return err } - content = fmt.Sprintf(`{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:5f00b5570a5561a6f9b7e66e4f26e2e30c4d09b43a8d3f993f3c1c99be6137a6","size":1740},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:f8b7e41ce10d9a0f614f068326c43431c2777e6fc346f729c2a643bfab24af83","size":59451113},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:9ca9274f196b56a708a7a672d3de88184c0158a30744d355dd0411f3a6850fa6","size":55685756},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:6c1ca50788f93937e9ce9341b564f86cbbcd28e367ed6a57cfc776aee4a9d050","size":113726186},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:d1a92139df86bdf00c818db75bf1ecc860857d142b426e9971a62f5f90e2aa57","size":108755643}],"annotations":{"ws.tycho.stacker.git_version":"0.3.25"}}`) + content = fmt.Sprint(`{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:5f00b5570a5561a6f9b7e66e4f26e2e30c4d09b43a8d3f993f3c1c99be6137a6","size":1740},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:f8b7e41ce10d9a0f614f068326c43431c2777e6fc346f729c2a643bfab24af83","size":59451113},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:9ca9274f196b56a708a7a672d3de88184c0158a30744d355dd0411f3a6850fa6","size":55685756},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:6c1ca50788f93937e9ce9341b564f86cbbcd28e367ed6a57cfc776aee4a9d050","size":113726186},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:d1a92139df86bdf00c818db75bf1ecc860857d142b426e9971a62f5f90e2aa57","size":108755643}],"annotations":{"ws.tycho.stacker.git_version":"0.3.25"}}`) err = makeTestFile(path.Join(dbDir, "zot-squashfs-test", "blobs/sha256", "eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb"), content) if err != nil { return err } - content = fmt.Sprintf(`{"created": "2020-04-08T05:32:49.805795564Z","author": "","architecture": "amd64","os": "linux","config": {"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs": {"type": "layers","diff_ids": []},"history": [{"created": "2020-05-11T18:17:24.516727354Z","created_by": "stacker umoci repack"}, {"created": "2020-04-08T05:08:53.213437118Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:12:15.999154739Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:12:31.0513552Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:20:38.068800557Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:21:01.956154957Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:32:24.582132274Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:32:49.805795564Z","created_by": "stacker build","author": "","empty_layer": true}]}`) + content = fmt.Sprint(`{"created": "2020-04-08T05:32:49.805795564Z","author": "","architecture": "amd64","os": "linux","config": {"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs": {"type": "layers","diff_ids": []},"history": [{"created": "2020-05-11T18:17:24.516727354Z","created_by": "stacker umoci repack"}, {"created": "2020-04-08T05:08:53.213437118Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:12:15.999154739Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:12:31.0513552Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:20:38.068800557Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:21:01.956154957Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:32:24.582132274Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:32:49.805795564Z","created_by": "stacker build","author": "","empty_layer": true}]}`) err = makeTestFile(path.Join(dbDir, "zot-squashfs-test", "blobs/sha256", "5f00b5570a5561a6f9b7e66e4f26e2e30c4d09b43a8d3f993f3c1c99be6137a6"), content) if err != nil { return err } - content = fmt.Sprintf(`{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:1fc1d045b241b04fea54333d76d4f57eb1961f9a314413f02a956b76e77a99f0","size":1218},"layers":[{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:c40d72b1556293c00a3e4b6c64c46ef4c7ae4d876dc18bad942b7d1903e8e5b7","size":54745420},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:4115890e3e2563e545e03f264bfecb0097e24e02306ae3e7668dea52e00c6cc2","size":52213357},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:91859e13e0cf704d5405199d73a2d1a0718391dbb183a77c4cb85d99e923ff57","size":109479329},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:20aef84d8098d47a0643a2f99ce05f0deed957b3a259fb708c538f23ed97cc82","size":103996238}],"annotations":{"ws.tycho.stacker.git_version":"0.3.25"}}`) + content = fmt.Sprint(`{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:1fc1d045b241b04fea54333d76d4f57eb1961f9a314413f02a956b76e77a99f0","size":1218},"layers":[{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:c40d72b1556293c00a3e4b6c64c46ef4c7ae4d876dc18bad942b7d1903e8e5b7","size":54745420},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:4115890e3e2563e545e03f264bfecb0097e24e02306ae3e7668dea52e00c6cc2","size":52213357},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:91859e13e0cf704d5405199d73a2d1a0718391dbb183a77c4cb85d99e923ff57","size":109479329},{"mediaType":"application/vnd.oci.image.layer.squashfs","digest":"sha256:20aef84d8098d47a0643a2f99ce05f0deed957b3a259fb708c538f23ed97cc82","size":103996238}],"annotations":{"ws.tycho.stacker.git_version":"0.3.25"}}`) err = makeTestFile(path.Join(dbDir, "zot-squashfs-test", "blobs/sha256", "45df53588e59759a12bd3eca553cdc9063939baac9a28d7ebb6101e4ec230b76"), content) if err != nil { return err } - content = fmt.Sprintf(`{"created": "2020-04-08T05:32:49.805795564Z","author": "","architecture": "amd64","os": "linux","config": {"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs": {"type": "layers","diff_ids": []},"history": [{"created": "2020-05-11T18:17:24.516727354Z","created_by": "stacker umoci repack"}, {"created": "2020-04-08T05:08:53.213437118Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:12:15.999154739Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-05-11T19:30:02.467956112Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:20:38.068800557Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:21:01.956154957Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:32:24.582132274Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:32:49.805795564Z","created_by": "stacker build","author": "","empty_layer": true}]}`) + content = fmt.Sprint(`{"created": "2020-04-08T05:32:49.805795564Z","author": "","architecture": "amd64","os": "linux","config": {"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs": {"type": "layers","diff_ids": []},"history": [{"created": "2020-05-11T18:17:24.516727354Z","created_by": "stacker umoci repack"}, {"created": "2020-04-08T05:08:53.213437118Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:12:15.999154739Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-05-11T19:30:02.467956112Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:20:38.068800557Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:21:01.956154957Z","created_by": "stacker build","author": "","empty_layer": true}, {"created": "2020-04-08T05:32:24.582132274Z","created_by": "stacker umoci repack","author": ""}, {"created": "2020-04-08T05:32:49.805795564Z","created_by": "stacker build","author": "","empty_layer": true}]}`) err = makeTestFile(path.Join(dbDir, "zot-squashfs-test", "blobs/sha256", "1fc1d045b241b04fea54333d76d4f57eb1961f9a314413f02a956b76e77a99f0"), content) if err != nil { @@ -243,21 +242,21 @@ func generateTestData(dbDir string) error { //nolint: gocyclo return err } - content = fmt.Sprintf(`{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.25"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:45df53588e59759a12bd3eca553cdc9063939baac9a28d7ebb6101e4ec230b76","size":873,"annotations":{"org.opencontainers.image.ref.name":"0.3.22-squashfs"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:71448405a4b89539fcfa581afb4dc7d257f98857686b8138b08a1c539f313099","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.19"},"platform":{"architecture":"amd64","os":"linux"}}]}`) + content = fmt.Sprint(`{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.25"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:45df53588e59759a12bd3eca553cdc9063939baac9a28d7ebb6101e4ec230b76","size":873,"annotations":{"org.opencontainers.image.ref.name":"0.3.22-squashfs"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:71448405a4b89539fcfa581afb4dc7d257f98857686b8138b08a1c539f313099","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.19"},"platform":{"architecture":"amd64","os":"linux"}}]}`) err = makeTestFile(path.Join(dbDir, "zot-invalid-layer", "index.json"), content) if err != nil { return err } - content = fmt.Sprintf(`{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:5f00b5570a5561a6f9b7e66e4f26e2e30c4d09b43a8d3f993f3c1c99be6137a6","size":1740},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:f8b7e41ce10d9a0f614f068326c43431c2777e6fc346f729c2a643bfab24af83","size":59451113},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:9ca9274f196b56a708a7a672d3de88184c0158a30744d355dd0411f3a6850fa6","size":55685756},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:6c1ca50788f93937e9ce9341b564f86cbbcd28e367ed6a57cfc776aee4a9d050","size":113726186},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:d1a92139df86bdf00c818db75bf1ecc860857d142b426e9971a62f5f90e2aa57","size":108755643}],"annotations":{"ws.tycho.stacker.git_version":"0.3.25"}}`) + content = fmt.Sprint(`{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:5f00b5570a5561a6f9b7e66e4f26e2e30c4d09b43a8d3f993f3c1c99be6137a6","size":1740},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:f8b7e41ce10d9a0f614f068326c43431c2777e6fc346f729c2a643bfab24af83","size":59451113},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:9ca9274f196b56a708a7a672d3de88184c0158a30744d355dd0411f3a6850fa6","size":55685756},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:6c1ca50788f93937e9ce9341b564f86cbbcd28e367ed6a57cfc776aee4a9d050","size":113726186},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:d1a92139df86bdf00c818db75bf1ecc860857d142b426e9971a62f5f90e2aa57","size":108755643}],"annotations":{"ws.tycho.stacker.git_version":"0.3.25"}}`) err = makeTestFile(path.Join(dbDir, "zot-invalid-layer", "blobs/sha256", "eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb"), content) if err != nil { return err } - content = fmt.Sprintf(`{"created":"2020-05-11T19:12:23.239785708Z","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs":{"type":"layers","diff_ids":["sha256:8817d297aa60796f41f559ba688d29b31830854014091233575d474f3a6e808e","sha256:dd5a09481ae1f5caf8d1dbc87bc7f86a01af030796467ba25851ad69964d226d","sha256:a8bce2aaf5ce6f1a5459b72de64927a1e507a911453789bf60df06752222cacd","sha256:dc0b750a934e8f376af23de6dcab1af282967498844a6510aed2c61277f20c11"]},"history":[{"created":"2020-05-11T18:17:24.516727354Z","created_by":"stacker umoci repack"},{"created":"2020-05-11T18:17:33.111086359Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T18:18:43.147035914Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T18:19:03.346279546Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T18:27:01.623678656Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T18:27:23.420280147Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T19:11:54.886053615Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T19:12:23.239785708Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true}]`) + content = fmt.Sprint(`{"created":"2020-05-11T19:12:23.239785708Z","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs":{"type":"layers","diff_ids":["sha256:8817d297aa60796f41f559ba688d29b31830854014091233575d474f3a6e808e","sha256:dd5a09481ae1f5caf8d1dbc87bc7f86a01af030796467ba25851ad69964d226d","sha256:a8bce2aaf5ce6f1a5459b72de64927a1e507a911453789bf60df06752222cacd","sha256:dc0b750a934e8f376af23de6dcab1af282967498844a6510aed2c61277f20c11"]},"history":[{"created":"2020-05-11T18:17:24.516727354Z","created_by":"stacker umoci repack"},{"created":"2020-05-11T18:17:33.111086359Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T18:18:43.147035914Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T18:19:03.346279546Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T18:27:01.623678656Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T18:27:23.420280147Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T19:11:54.886053615Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T19:12:23.239785708Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true}]`) err = makeTestFile(path.Join(dbDir, "zot-invalid-layer", "blobs/sha256", "5f00b5570a5561a6f9b7e66e4f26e2e30c4d09b43a8d3f993f3c1c99be6137a6"), content) if err != nil { @@ -271,21 +270,21 @@ func generateTestData(dbDir string) error { //nolint: gocyclo return err } - content = fmt.Sprintf(`{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.25"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:45df53588e59759a12bd3eca553cdc9063939baac9a28d7ebb6101e4ec230b76","size":873,"annotations":{"org.opencontainers.image.ref.name":"0.3.22-squashfs"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:71448405a4b89539fcfa581afb4dc7d257f98857686b8138b08a1c539f313099","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.19"},"platform":{"architecture":"amd64","os":"linux"}}]}`) + content = fmt.Sprint(`{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.25"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:45df53588e59759a12bd3eca553cdc9063939baac9a28d7ebb6101e4ec230b76","size":873,"annotations":{"org.opencontainers.image.ref.name":"0.3.22-squashfs"},"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:71448405a4b89539fcfa581afb4dc7d257f98857686b8138b08a1c539f313099","size":886,"annotations":{"org.opencontainers.image.ref.name":"0.3.19"},"platform":{"architecture":"amd64","os":"linux"}}]}`) err = makeTestFile(path.Join(dbDir, "zot-no-layer", "index.json"), content) if err != nil { return err } - content = fmt.Sprintf(`{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:5f00b5570a5561a6f9b7e66e4f26e2e30c4d09b43a8d3f993f3c1c99be6137a6","size":1740},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:f8b7e41ce10d9a0f614f068326c43431c2777e6fc346f729c2a643bfab24af83","size":59451113},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:9ca9274f196b56a708a7a672d3de88184c0158a30744d355dd0411f3a6850fa6","size":55685756},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:6c1ca50788f93937e9ce9341b564f86cbbcd28e367ed6a57cfc776aee4a9d050","size":113726186},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:d1a92139df86bdf00c818db75bf1ecc860857d142b426e9971a62f5f90e2aa57","size":108755643}],"annotations":{"ws.tycho.stacker.git_version":"0.3.25"}}`) + content = fmt.Sprint(`{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:5f00b5570a5561a6f9b7e66e4f26e2e30c4d09b43a8d3f993f3c1c99be6137a6","size":1740},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:f8b7e41ce10d9a0f614f068326c43431c2777e6fc346f729c2a643bfab24af83","size":59451113},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:9ca9274f196b56a708a7a672d3de88184c0158a30744d355dd0411f3a6850fa6","size":55685756},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:6c1ca50788f93937e9ce9341b564f86cbbcd28e367ed6a57cfc776aee4a9d050","size":113726186},{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:d1a92139df86bdf00c818db75bf1ecc860857d142b426e9971a62f5f90e2aa57","size":108755643}],"annotations":{"ws.tycho.stacker.git_version":"0.3.25"}}`) err = makeTestFile(path.Join(dbDir, "zot-no-layer", "blobs/sha256", "eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb"), content) if err != nil { return err } - content = fmt.Sprintf(`{"created":"2020-05-11T19:12:23.239785708Z","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs":{"type":"layers","diff_ids":["sha256:8817d297aa60796f41f559ba688d29b31830854014091233575d474f3a6e808e","sha256:dd5a09481ae1f5caf8d1dbc87bc7f86a01af030796467ba25851ad69964d226d","sha256:a8bce2aaf5ce6f1a5459b72de64927a1e507a911453789bf60df06752222cacd","sha256:dc0b750a934e8f376af23de6dcab1af282967498844a6510aed2c61277f20c11"]},"history":[{"created":"2020-05-11T18:17:24.516727354Z","created_by":"stacker umoci repack"},{"created":"2020-05-11T18:17:33.111086359Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T18:18:43.147035914Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T18:19:03.346279546Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T18:27:01.623678656Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T18:27:23.420280147Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T19:11:54.886053615Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T19:12:23.239785708Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true}]`) + content = fmt.Sprint(`{"created":"2020-05-11T19:12:23.239785708Z","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs":{"type":"layers","diff_ids":["sha256:8817d297aa60796f41f559ba688d29b31830854014091233575d474f3a6e808e","sha256:dd5a09481ae1f5caf8d1dbc87bc7f86a01af030796467ba25851ad69964d226d","sha256:a8bce2aaf5ce6f1a5459b72de64927a1e507a911453789bf60df06752222cacd","sha256:dc0b750a934e8f376af23de6dcab1af282967498844a6510aed2c61277f20c11"]},"history":[{"created":"2020-05-11T18:17:24.516727354Z","created_by":"stacker umoci repack"},{"created":"2020-05-11T18:17:33.111086359Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T18:18:43.147035914Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T18:19:03.346279546Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T18:27:01.623678656Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T18:27:23.420280147Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true},{"created":"2020-05-11T19:11:54.886053615Z","created_by":"stacker umoci repack","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI"},{"created":"2020-05-11T19:12:23.239785708Z","created_by":"stacker build","author":"root@jenkinsProduction-Atom-Full-Build-c3-master-159CI","empty_layer":true}]`) err = makeTestFile(path.Join(dbDir, "zot-no-layer", "blobs/sha256", "5f00b5570a5561a6f9b7e66e4f26e2e30c4d09b43a8d3f993f3c1c99be6137a"), content) if err != nil { @@ -324,50 +323,73 @@ func TestImageFormat(t *testing.T) { cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", log) - isValidImage, err := cveInfo.Scanner.IsImageFormatScannable("zot-test") + isValidImage, err := cveInfo.Scanner.IsImageFormatScannable("zot-test", "") So(err, ShouldNotBeNil) So(isValidImage, ShouldEqual, false) - isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-test:0.0.1") + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-test", "0.0.1") So(err, ShouldBeNil) So(isValidImage, ShouldEqual, true) - isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-test:0.0.") + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-test", "0.0.") So(err, ShouldNotBeNil) So(isValidImage, ShouldEqual, false) - isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-noindex-test") + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-noindex-test", "") So(err, ShouldNotBeNil) So(isValidImage, ShouldEqual, false) - isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot--tet") + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot--tet", "") So(err, ShouldNotBeNil) So(isValidImage, ShouldEqual, false) - isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-noindex-test") + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-noindex-test", "") So(err, ShouldNotBeNil) So(isValidImage, ShouldEqual, false) - isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-noblobs") + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-noblobs", "") So(err, ShouldNotBeNil) So(isValidImage, ShouldEqual, false) - isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-invalid-index") + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-invalid-index", "") So(err, ShouldNotBeNil) So(isValidImage, ShouldEqual, false) - isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-invalid-blob") + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-invalid-blob", "") So(err, ShouldNotBeNil) So(isValidImage, ShouldEqual, false) - isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-test:0.3.22-squashfs") + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-test:0.3.22-squashfs", "") So(err, ShouldNotBeNil) So(isValidImage, ShouldEqual, false) - isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-nonreadable-test") + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-nonreadable-test", "") So(err, ShouldNotBeNil) So(isValidImage, ShouldEqual, false) }) + + Convey("isIndexScanable", t, func() { + log := log.NewLogger("debug", "") + + repoDB := &mocks.RepoDBMock{ + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Tags: map[string]repodb.Descriptor{ + "tag": {MediaType: ispec.MediaTypeImageIndex}, + }, + }, nil + }, + } + storeController := storage.StoreController{ + DefaultStore: mocks.MockedImageStore{}, + } + + cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", log) + + isScanable, err := cveInfo.Scanner.IsImageFormatScannable("repo", "tag") + So(err, ShouldBeNil) + So(isScanable, ShouldBeFalse) + }) } func TestCVESearchDisabled(t *testing.T) { @@ -1023,6 +1045,43 @@ func TestCVEStruct(t *testing.T) { err = repoDB.SetRepoTag("repo5", "nonexitent-manifest", digest51, ispec.MediaTypeImageManifest) So(err, ShouldBeNil) + // ------ Multiarch image + _, _, manifestContent1, err := GetRandomImageComponents(100) + So(err, ShouldBeNil) + manifestContent1Blob, err := json.Marshal(manifestContent1) + So(err, ShouldBeNil) + diestManifestFromIndex1 := godigest.FromBytes(manifestContent1Blob) + err = repoDB.SetManifestData(diestManifestFromIndex1, repodb.ManifestData{ + ManifestBlob: manifestContent1Blob, + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + _, _, manifestContent2, err := GetRandomImageComponents(100) + So(err, ShouldBeNil) + manifestContent2Blob, err := json.Marshal(manifestContent2) + So(err, ShouldBeNil) + diestManifestFromIndex2 := godigest.FromBytes(manifestContent2Blob) + err = repoDB.SetManifestData(diestManifestFromIndex1, repodb.ManifestData{ + ManifestBlob: manifestContent2Blob, + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + indexBlob, err := GetIndexBlobWithManifests( + []godigest.Digest{diestManifestFromIndex1, diestManifestFromIndex2}, + ) + So(err, ShouldBeNil) + + indexDigest := godigest.FromBytes(indexBlob) + err = repoDB.SetIndexData(indexDigest, repodb.IndexData{ + IndexBlob: indexBlob, + }) + So(err, ShouldBeNil) + + err = repoDB.SetRepoTag("repoIndex", "tagIndex", indexDigest, ispec.MediaTypeImageIndex) + So(err, ShouldBeNil) + // RepoDB loaded with initial data, mock the scanner severities := map[string]int{ "UNKNOWN": 0, @@ -1100,15 +1159,30 @@ func TestCVEStruct(t *testing.T) { }, nil } + if image == "repoIndex:tagIndex" { + return map[string]cvemodel.CVE{ + "CVE1": { + ID: "CVE1", + Severity: "MEDIUM", + Title: "Title CVE1", + Description: "Description CVE1", + }, + }, nil + } + // By default the image has no vulnerabilities return map[string]cvemodel.CVE{}, nil }, CompareSeveritiesFn: func(severity1, severity2 string) int { return severities[severity2] - severities[severity1] }, - IsImageFormatScannableFn: func(image string) (bool, error) { + IsImageFormatScannableFn: func(repo string, reference string) (bool, error) { + if repo == "repoIndex" { + return true, nil + } + // Almost same logic compared to actual Trivy specific implementation - imageDir, inputTag := common.GetImageDirAndTag(image) + imageDir, inputTag := repo, reference repoMeta, err := repoDB.GetRepoMeta(imageDir) if err != nil { @@ -1158,51 +1232,51 @@ func TestCVEStruct(t *testing.T) { t.Log("Test GetCVESummaryForImage") // Image is found - cveSummary, err := cveInfo.GetCVESummaryForImage("repo1:0.1.0") + cveSummary, err := cveInfo.GetCVESummaryForImage("repo1", "0.1.0") So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 1) So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") - cveSummary, err = cveInfo.GetCVESummaryForImage("repo1:1.0.0") + cveSummary, err = cveInfo.GetCVESummaryForImage("repo1", "1.0.0") So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 3) So(cveSummary.MaxSeverity, ShouldEqual, "HIGH") - cveSummary, err = cveInfo.GetCVESummaryForImage("repo1:1.0.1") + cveSummary, err = cveInfo.GetCVESummaryForImage("repo1", "1.0.1") So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 2) So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") - cveSummary, err = cveInfo.GetCVESummaryForImage("repo1:1.1.0") + cveSummary, err = cveInfo.GetCVESummaryForImage("repo1", "1.1.0") So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 1) So(cveSummary.MaxSeverity, ShouldEqual, "LOW") - cveSummary, err = cveInfo.GetCVESummaryForImage("repo6:1.0.0") + cveSummary, err = cveInfo.GetCVESummaryForImage("repo6", "1.0.0") So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "NONE") // Image is not scannable - cveSummary, err = cveInfo.GetCVESummaryForImage("repo2:1.0.0") + cveSummary, err = cveInfo.GetCVESummaryForImage("repo2", "1.0.0") So(err, ShouldEqual, zerr.ErrScanNotSupported) So(cveSummary.Count, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") // Tag is not found - cveSummary, err = cveInfo.GetCVESummaryForImage("repo3:1.0.0") + cveSummary, err = cveInfo.GetCVESummaryForImage("repo3", "1.0.0") So(err, ShouldEqual, zerr.ErrTagMetaNotFound) So(cveSummary.Count, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") // Manifest is not found - cveSummary, err = cveInfo.GetCVESummaryForImage("repo5:nonexitent-manifest") + cveSummary, err = cveInfo.GetCVESummaryForImage("repo5", "nonexitent-manifest") So(err, ShouldEqual, zerr.ErrManifestDataNotFound) So(cveSummary.Count, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") // Repo is not found - cveSummary, err = cveInfo.GetCVESummaryForImage("repo100:1.0.0") + cveSummary, err = cveInfo.GetCVESummaryForImage("repo100", "1.0.0") So(err, ShouldEqual, zerr.ErrRepoMetaNotFound) So(cveSummary.Count, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") @@ -1214,14 +1288,14 @@ func TestCVEStruct(t *testing.T) { } // Image is found - cveList, pageInfo, err := cveInfo.GetCVEListForImage("repo1:0.1.0", pageInput) + cveList, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 1) So(cveList[0].ID, ShouldEqual, "CVE1") So(pageInfo.ItemCount, ShouldEqual, 1) So(pageInfo.TotalCount, ShouldEqual, 1) - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 3) So(cveList[0].ID, ShouldEqual, "CVE2") @@ -1230,7 +1304,7 @@ func TestCVEStruct(t *testing.T) { So(pageInfo.ItemCount, ShouldEqual, 3) So(pageInfo.TotalCount, ShouldEqual, 3) - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.1", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.1", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 2) So(cveList[0].ID, ShouldEqual, "CVE1") @@ -1238,42 +1312,42 @@ func TestCVEStruct(t *testing.T) { So(pageInfo.ItemCount, ShouldEqual, 2) So(pageInfo.TotalCount, ShouldEqual, 2) - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.1.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.1.0", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 1) So(cveList[0].ID, ShouldEqual, "CVE3") So(pageInfo.ItemCount, ShouldEqual, 1) So(pageInfo.TotalCount, ShouldEqual, 1) - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo6:1.0.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo6", "1.0.0", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) // Image is not scannable - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo2:1.0.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo2", "1.0.0", pageInput) So(err, ShouldEqual, zerr.ErrScanNotSupported) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) // Tag is not found - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo3:1.0.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo3", "1.0.0", pageInput) So(err, ShouldEqual, zerr.ErrTagMetaNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) // Manifest is not found - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo5:nonexitent-manifest", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo5", "nonexitent-manifest", pageInput) So(err, ShouldEqual, zerr.ErrManifestDataNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) // Repo is not found - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo100:1.0.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo100", "1.0.0", pageInput) So(err, ShouldEqual, zerr.ErrRepoMetaNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) @@ -1388,12 +1462,12 @@ func TestCVEStruct(t *testing.T) { cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: faultyScanner, RepoDB: repoDB} - cveSummary, err = cveInfo.GetCVESummaryForImage("repo1:0.1.0") + cveSummary, err = cveInfo.GetCVESummaryForImage("repo1", "0.1.0") So(err, ShouldNotBeNil) So(cveSummary.Count, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1:0.1.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", pageInput) So(err, ShouldNotBeNil) So(cveList, ShouldBeEmpty) So(pageInfo.ItemCount, ShouldEqual, 0) @@ -1410,5 +1484,32 @@ func TestCVEStruct(t *testing.T) { // but do not return an error So(err, ShouldBeNil) So(len(tagList), ShouldEqual, 0) + + cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: scanner, RepoDB: repoDB} + + tagList, err = cveInfo.GetImageListForCVE("repoIndex", "CVE1") + So(err, ShouldBeNil) + So(len(tagList), ShouldEqual, 0) + + cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: mocks.CveScannerMock{ + IsImageFormatScannableFn: func(repo, reference string) (bool, error) { + return false, nil + }, + }, RepoDB: repoDB} + + _, err = cveInfo.GetImageListForCVE("repoIndex", "CVE1") + So(err, ShouldBeNil) + + cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: mocks.CveScannerMock{ + IsImageFormatScannableFn: func(repo, reference string) (bool, error) { + return true, nil + }, + ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) { + return nil, zerr.ErrTypeAssertionFailed + }, + }, RepoDB: repoDB} + + _, err = cveInfo.GetImageListForCVE("repoIndex", "CVE1") + So(err, ShouldBeNil) }) } diff --git a/pkg/extensions/search/cve/model/models.go b/pkg/extensions/search/cve/model/models.go index a4e2cdc6..47d8390c 100644 --- a/pkg/extensions/search/cve/model/models.go +++ b/pkg/extensions/search/cve/model/models.go @@ -15,3 +15,23 @@ type Package struct { InstalledVersion string `json:"InstalledVersion"` FixedVersion string `json:"FixedVersion"` } + +const ( + None = iota + Low + Medium + High + Critical +) + +func SeverityValue(severity string) int { + sevMap := map[string]int{ + "NONE": None, + "LOW": Low, + "MEDIUM": Medium, + "HIGH": High, + "CRITICAL": Critical, + } + + return sevMap[severity] +} diff --git a/pkg/extensions/search/cve/pagination_test.go b/pkg/extensions/search/cve/pagination_test.go index e22e7a13..15fe3e6c 100644 --- a/pkg/extensions/search/cve/pagination_test.go +++ b/pkg/extensions/search/cve/pagination_test.go @@ -182,7 +182,7 @@ func TestCVEPagination(t *testing.T) { Convey("Page", func() { Convey("defaults", func() { // By default expect unlimitted results sorted by severity - cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{}) + cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5) @@ -193,7 +193,7 @@ func TestCVEPagination(t *testing.T) { previousSeverity = severityToInt[cve.Severity] } - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", cveinfo.PageInput{}) + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", cveinfo.PageInput{}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -211,7 +211,8 @@ func TestCVEPagination(t *testing.T) { cveIds = append(cveIds, fmt.Sprintf("CVE%d", i)) } - cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) + cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", + cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5) @@ -221,7 +222,7 @@ func TestCVEPagination(t *testing.T) { } sort.Strings(cveIds) - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -231,7 +232,7 @@ func TestCVEPagination(t *testing.T) { } sort.Sort(sort.Reverse(sort.StringSlice(cveIds))) - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", cveinfo.PageInput{SortBy: cveinfo.AlphabeticDsc}) + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", cveinfo.PageInput{SortBy: cveinfo.AlphabeticDsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -240,7 +241,7 @@ func TestCVEPagination(t *testing.T) { So(cve.ID, ShouldEqual, cveIds[i]) } - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", cveinfo.PageInput{SortBy: cveinfo.SeverityDsc}) + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", cveinfo.PageInput{SortBy: cveinfo.SeverityDsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -258,7 +259,7 @@ func TestCVEPagination(t *testing.T) { cveIds = append(cveIds, fmt.Sprintf("CVE%d", i)) } - cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{ + cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ Limit: 3, Offset: 1, SortBy: cveinfo.AlphabeticAsc, @@ -271,7 +272,7 @@ func TestCVEPagination(t *testing.T) { So(cves[1].ID, ShouldEqual, "CVE2") So(cves[2].ID, ShouldEqual, "CVE3") - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{ + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ Limit: 2, Offset: 1, SortBy: cveinfo.AlphabeticDsc, @@ -283,7 +284,7 @@ func TestCVEPagination(t *testing.T) { So(cves[0].ID, ShouldEqual, "CVE3") So(cves[1].ID, ShouldEqual, "CVE2") - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{ + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ Limit: 3, Offset: 1, SortBy: cveinfo.SeverityDsc, @@ -299,7 +300,7 @@ func TestCVEPagination(t *testing.T) { } sort.Strings(cveIds) - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", cveinfo.PageInput{ + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", cveinfo.PageInput{ Limit: 5, Offset: 20, SortBy: cveinfo.AlphabeticAsc, @@ -314,7 +315,7 @@ func TestCVEPagination(t *testing.T) { }) Convey("limit > len(cves)", func() { - cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{ + cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.AlphabeticAsc, @@ -326,7 +327,7 @@ func TestCVEPagination(t *testing.T) { So(cves[0].ID, ShouldEqual, "CVE3") So(cves[1].ID, ShouldEqual, "CVE4") - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{ + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.AlphabeticDsc, @@ -338,7 +339,7 @@ func TestCVEPagination(t *testing.T) { So(cves[0].ID, ShouldEqual, "CVE1") So(cves[1].ID, ShouldEqual, "CVE0") - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{ + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.SeverityDsc, diff --git a/pkg/extensions/search/cve/trivy/scanner.go b/pkg/extensions/search/cve/trivy/scanner.go index 2460fc97..cf59c5c7 100644 --- a/pkg/extensions/search/cve/trivy/scanner.go +++ b/pkg/extensions/search/cve/trivy/scanner.go @@ -14,6 +14,7 @@ import ( regTypes "github.com/google/go-containerregistry/pkg/v1/types" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/extensions/search/common" @@ -173,24 +174,49 @@ func (scanner Scanner) runTrivy(opts flag.Options) (types.Report, error) { return report, nil } -func (scanner Scanner) IsImageFormatScannable(image string) (bool, error) { +func (scanner Scanner) IsImageFormatScannable(repo, tag string) (bool, error) { + image := repo + ":" + tag + if scanner.cache.Get(image) != nil { return true, nil } - imageDir, inputTag := common.GetImageDirAndTag(image) - - repoMeta, err := scanner.repoDB.GetRepoMeta(imageDir) + repoMeta, err := scanner.repoDB.GetRepoMeta(repo) if err != nil { return false, err } - manifestDigestStr, ok := repoMeta.Tags[inputTag] + var ok bool + + imageDescriptor, ok := repoMeta.Tags[tag] if !ok { return false, zerr.ErrTagMetaNotFound } - manifestDigest, err := godigest.Parse(manifestDigestStr.Digest) + switch imageDescriptor.MediaType { + case ispec.MediaTypeImageManifest: + ok, err := scanner.isManifestScanable(imageDescriptor) + if err != nil { + return ok, errors.Wrapf(err, "image '%s'", image) + } + + return ok, nil + case ispec.MediaTypeImageIndex: + ok, err := scanner.isIndexScanable(imageDescriptor) + if err != nil { + return ok, errors.Wrapf(err, "image '%s'", image) + } + + return ok, nil + } + + return false, nil +} + +func (scanner Scanner) isManifestScanable(descriptor repodb.Descriptor) (bool, error) { + manifestDigestStr := descriptor.Digest + + manifestDigest, err := godigest.Parse(manifestDigestStr) if err != nil { return false, err } @@ -204,7 +230,7 @@ func (scanner Scanner) IsImageFormatScannable(image string) (bool, error) { err = json.Unmarshal(manifestData.ManifestBlob, &manifestContent) if err != nil { - scanner.log.Error().Err(err).Str("image", image).Msg("unable to unmashal manifest blob") + scanner.log.Error().Err(err).Msg("unable to unmashal manifest blob") return false, zerr.ErrScanNotSupported } @@ -214,7 +240,7 @@ func (scanner Scanner) IsImageFormatScannable(image string) (bool, error) { case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer): continue default: - scanner.log.Debug().Str("image", image). + scanner.log.Debug(). Msgf("image media type %s not supported for scanning", imageLayer.MediaType) return false, zerr.ErrScanNotSupported @@ -224,6 +250,10 @@ func (scanner Scanner) IsImageFormatScannable(image string) (bool, error) { return true, nil } +func (scanner Scanner) isIndexScanable(descriptor repodb.Descriptor) (bool, error) { + return false, nil +} + func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) { if scanner.cache.Get(image) != nil { return scanner.cache.Get(image), nil diff --git a/pkg/extensions/search/cve/trivy/scanner_internal_test.go b/pkg/extensions/search/cve/trivy/scanner_internal_test.go index 497368ff..ff06f3c2 100644 --- a/pkg/extensions/search/cve/trivy/scanner_internal_test.go +++ b/pkg/extensions/search/cve/trivy/scanner_internal_test.go @@ -119,6 +119,7 @@ func TestMultipleStoragePath(t *testing.T) { // Scanning image in default store cveMap, err := scanner.ScanImage(img0) + So(err, ShouldBeNil) So(len(cveMap), ShouldEqual, 0) @@ -200,7 +201,7 @@ func TestTrivyLibraryErrors(t *testing.T) { So(err, ShouldNotBeNil) // Scanning image with invalid input to trigger a scanner error - opts = scanner.getTrivyOptions("nonexisting_image:0.0.1") + opts = scanner.getTrivyOptions("nilnonexisting_image:0.0.1") _, err = scanner.runTrivy(opts) So(err, ShouldNotBeNil) @@ -358,43 +359,43 @@ func TestImageScannable(t *testing.T) { scanner := NewScanner(storeController, repoDB, "ghcr.io/project-zot/trivy-db", log) Convey("Valid image should be scannable", t, func() { - result, err := scanner.IsImageFormatScannable("repo1:valid") + result, err := scanner.IsImageFormatScannable("repo1", "valid") So(err, ShouldBeNil) So(result, ShouldBeTrue) }) Convey("Image with layers of unsupported types should be unscannable", t, func() { - result, err := scanner.IsImageFormatScannable("repo1:unscannable-layer") + result, err := scanner.IsImageFormatScannable("repo1", "unscannable-layer") So(err, ShouldNotBeNil) So(result, ShouldBeFalse) }) Convey("Image with unmarshable manifests should be unscannable", t, func() { - result, err := scanner.IsImageFormatScannable("repo1:unmarshable") + result, err := scanner.IsImageFormatScannable("repo1", "unmarshable") So(err, ShouldNotBeNil) So(result, ShouldBeFalse) }) Convey("Image with missing manifest meta should be unscannable", t, func() { - result, err := scanner.IsImageFormatScannable("repo1:missing") + result, err := scanner.IsImageFormatScannable("repo1", "missing") So(err, ShouldNotBeNil) So(result, ShouldBeFalse) }) Convey("Image with invalid manifest digest should be unscannable", t, func() { - result, err := scanner.IsImageFormatScannable("repo1:invalid-digest") + result, err := scanner.IsImageFormatScannable("repo1", "invalid-digest") So(err, ShouldNotBeNil) So(result, ShouldBeFalse) }) Convey("Image with unknown tag should be unscannable", t, func() { - result, err := scanner.IsImageFormatScannable("repo1:unknown-tag") + result, err := scanner.IsImageFormatScannable("repo1", "unknown-tag") So(err, ShouldNotBeNil) So(result, ShouldBeFalse) }) Convey("Image with unknown repo should be unscannable", t, func() { - result, err := scanner.IsImageFormatScannable("unknown-repo:sometag") + result, err := scanner.IsImageFormatScannable("unknown-repo", "sometag") So(err, ShouldNotBeNil) So(result, ShouldBeFalse) }) diff --git a/pkg/extensions/search/digest/digest.go b/pkg/extensions/search/digest/digest.go index 2b879986..c5d9574e 100644 --- a/pkg/extensions/search/digest/digest.go +++ b/pkg/extensions/search/digest/digest.go @@ -55,7 +55,7 @@ func (digestinfo DigestInfo) GetImageTagsByDigest(repo, digest string) ([]ImageI tags := []*string{} - // Check the image manigest in index.json matches the search digest + // Check the image manifest in index.json matches the search digest // This is a blob with mediaType application/vnd.oci.image.manifest.v1+json if strings.Contains(manifest.Digest.String(), digest) { tags = append(tags, &val) diff --git a/pkg/extensions/search/digest/digest_test.go b/pkg/extensions/search/digest/digest_test.go index bc919db5..e361bee1 100644 --- a/pkg/extensions/search/digest/digest_test.go +++ b/pkg/extensions/search/digest/digest_test.go @@ -65,11 +65,11 @@ func testSetup(t *testing.T) (string, string, *digestinfo.DigestInfo) { subRootDir := subDir // Test images used/copied: - // IMAGE NAME TAG DIGEST CONFIG LAYERS SIZE - // zot-test 0.0.1 2bacca16 adf3bb6c 76MB - // 2d473b07 76MB - // zot-cve-test 0.0.1 63a795ca 8dd57e17 75MB - // 7a0437f0 75MB + // IMAGE NAME TAG DIGEST OS/ARCH CONFIG LAYERS SIZE + // zot-test 0.0.1 2bacca16 linux/amd64 adf3bb6c 76MB + // 2d473b07 76MB + // zot-cve-test 0.0.1 63a795ca linux/amd64 8dd57e17 75MB + // 7a0437f0 75MB err := os.Mkdir(subDir+"/a", 0o700) if err != nil { @@ -159,9 +159,20 @@ func TestDigestSearchHTTP(t *testing.T) { So(resp.StatusCode(), ShouldEqual, 422) // "sha" should match all digests in all images + query := `{ + ImageListForDigest(id:"sha") { + Results { + RepoName Tag + Manifests { + Digest ConfigDigest Size + Layers { Digest } + } + Size + } + } + }` resp, err = resty.R().Get( - baseURL + constants.FullSearchPrefix + `?query={ImageListForDigest(id:"sha")` + - `{Results{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}}`, + baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query), ) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) @@ -178,7 +189,7 @@ func TestDigestSearchHTTP(t *testing.T) { // GetTestBlobDigest("zot-test", "manifest").Encoded() should match the manifest of 1 image gqlQuery := url.QueryEscape(`{ImageListForDigest(id:"` + GetTestBlobDigest("zot-test", "manifest").Encoded() + `") - {Results{RepoName Tag Digest ConfigDigest Size Layers { Digest }}}}`) + {Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`) targetURL := baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery resp, err = resty.R().Get(targetURL) @@ -196,7 +207,7 @@ func TestDigestSearchHTTP(t *testing.T) { // GetTestBlobDigest("zot-test", "config").Encoded() should match the config of 1 image. gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"` + GetTestBlobDigest("zot-test", "config").Encoded() + `") - {Results{RepoName Tag Digest ConfigDigest Size Layers { Digest }}}}`) + {Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`) targetURL = baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery resp, err = resty.R().Get(targetURL) @@ -215,7 +226,7 @@ func TestDigestSearchHTTP(t *testing.T) { // Call should return {"data":{"ImageListForDigest":[{"Name":"zot-cve-test","Tags":["0.0.1"]}]}} // GetTestBlobDigest("zot-cve-test", "layer").Encoded() should match the layer of 1 image gqlQuery = url.QueryEscape(`{ImageListForDigest(id:"` + GetTestBlobDigest("zot-cve-test", "layer").Encoded() + `") - {Results{RepoName Tag Digest ConfigDigest Size Layers { Digest }}}}`) + {Results{RepoName Tag Manifests {Digest ConfigDigest Size Layers { Digest }}}}}`) targetURL = baseURL + constants.FullSearchPrefix + `?query=` + gqlQuery resp, err = resty.R().Get( @@ -236,9 +247,20 @@ func TestDigestSearchHTTP(t *testing.T) { // Call should return {"data":{"ImageListForDigest":[]}} // "1111111" should match 0 images + query = ` + { + ImageListForDigest(id:"1111111") { + Results { + RepoName Tag + Manifests { + Digest ConfigDigest Size + Layers { Digest } + } + } + } + }` resp, err = resty.R().Get( - baseURL + constants.FullSearchPrefix + `?query={ImageListForDigest(id:"1111111")` + - `{Results{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}}`, + baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query), ) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) @@ -250,9 +272,14 @@ func TestDigestSearchHTTP(t *testing.T) { So(len(responseStruct.ImgListForDigest.Results), ShouldEqual, 0) // Call should return {"errors": [{....}]", data":null}} + query = `{ + ImageListForDigest(id:"1111111") { + Results { + RepoName Tag343s + } + }` resp, err = resty.R().Get( - baseURL + constants.FullSearchPrefix + `?query={ImageListForDigest(id:"1111111")` + - `{Results{RepoName%20Tag343s}}}`, + baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query), ) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) @@ -306,9 +333,19 @@ func TestDigestSearchHTTPSubPaths(t *testing.T) { So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 422) + query := `{ + ImageListForDigest(id:"sha") { + Results { + RepoName Tag + Manifests { + Digest ConfigDigest Size + Layers { Digest } + } + } + } + }` resp, err = resty.R().Get( - baseURL + constants.FullSearchPrefix + `?query={ImageListForDigest(id:"sha")` + - `{Results{RepoName%20Tag%20Digest%20ConfigDigest%20Size%20Layers%20{%20Digest}}}}`, + baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query), ) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 7e2eccba..42916475 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -79,18 +79,14 @@ type ComplexityRoot struct { ImageSummary struct { Authors func(childComplexity int) int - 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 - History func(childComplexity int) int IsSigned func(childComplexity int) int Labels func(childComplexity int) int LastUpdated func(childComplexity int) int - Layers func(childComplexity int) int Licenses func(childComplexity int) int - Platform func(childComplexity int) int + Manifests func(childComplexity int) int RepoName func(childComplexity int) int Score func(childComplexity int) int Size func(childComplexity int) int @@ -117,9 +113,16 @@ type ComplexityRoot struct { Size func(childComplexity int) int } - OsArch struct { - Arch func(childComplexity int) int - Os func(childComplexity int) int + ManifestSummary struct { + ConfigDigest func(childComplexity int) int + Digest func(childComplexity int) int + DownloadCount func(childComplexity int) int + History func(childComplexity int) int + LastUpdated func(childComplexity int) int + Layers func(childComplexity int) int + Platform func(childComplexity int) int + Size func(childComplexity int) int + Vulnerabilities func(childComplexity int) int } PackageInfo struct { @@ -143,10 +146,15 @@ type ComplexityRoot struct { Results func(childComplexity int) int } + Platform struct { + Arch func(childComplexity int) int + Os func(childComplexity int) int + } + Query struct { - BaseImageList func(childComplexity int, image string, requestedPage *PageInput) int + BaseImageList func(childComplexity int, image string, digest *string, requestedPage *PageInput) int CVEListForImage func(childComplexity int, image string, requestedPage *PageInput) int - DerivedImageList func(childComplexity int, image string, requestedPage *PageInput) int + DerivedImageList func(childComplexity int, image string, digest *string, requestedPage *PageInput) int ExpandedRepoInfo func(childComplexity int, repo string) int GlobalSearch func(childComplexity int, query string, filter *Filter, requestedPage *PageInput) int Image func(childComplexity int, image string) int @@ -195,8 +203,8 @@ type QueryResolver interface { ImageList(ctx context.Context, repo string, requestedPage *PageInput) (*PaginatedImagesResult, error) ExpandedRepoInfo(ctx context.Context, repo string) (*RepoInfo, error) GlobalSearch(ctx context.Context, query string, filter *Filter, requestedPage *PageInput) (*GlobalSearchResult, error) - DerivedImageList(ctx context.Context, image string, requestedPage *PageInput) (*PaginatedImagesResult, error) - BaseImageList(ctx context.Context, image string, requestedPage *PageInput) (*PaginatedImagesResult, error) + DerivedImageList(ctx context.Context, image string, digest *string, requestedPage *PageInput) (*PaginatedImagesResult, error) + BaseImageList(ctx context.Context, image string, digest *string, requestedPage *PageInput) (*PaginatedImagesResult, error) Image(ctx context.Context, image string) (*ImageSummary, error) Referrers(ctx context.Context, repo string, digest string, typeArg []string) ([]*Referrer, error) } @@ -356,13 +364,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.Authors(childComplexity), true - case "ImageSummary.ConfigDigest": - if e.complexity.ImageSummary.ConfigDigest == nil { - break - } - - return e.complexity.ImageSummary.ConfigDigest(childComplexity), true - case "ImageSummary.Description": if e.complexity.ImageSummary.Description == nil { break @@ -370,13 +371,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.Description(childComplexity), true - case "ImageSummary.Digest": - if e.complexity.ImageSummary.Digest == nil { - break - } - - return e.complexity.ImageSummary.Digest(childComplexity), true - case "ImageSummary.Documentation": if e.complexity.ImageSummary.Documentation == nil { break @@ -391,13 +385,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.DownloadCount(childComplexity), true - case "ImageSummary.History": - if e.complexity.ImageSummary.History == nil { - break - } - - return e.complexity.ImageSummary.History(childComplexity), true - case "ImageSummary.IsSigned": if e.complexity.ImageSummary.IsSigned == nil { break @@ -419,13 +406,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.LastUpdated(childComplexity), true - case "ImageSummary.Layers": - if e.complexity.ImageSummary.Layers == nil { - break - } - - return e.complexity.ImageSummary.Layers(childComplexity), true - case "ImageSummary.Licenses": if e.complexity.ImageSummary.Licenses == nil { break @@ -433,12 +413,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.Licenses(childComplexity), true - case "ImageSummary.Platform": - if e.complexity.ImageSummary.Platform == nil { + case "ImageSummary.Manifests": + if e.complexity.ImageSummary.Manifests == nil { break } - return e.complexity.ImageSummary.Platform(childComplexity), true + return e.complexity.ImageSummary.Manifests(childComplexity), true case "ImageSummary.RepoName": if e.complexity.ImageSummary.RepoName == nil { @@ -545,19 +525,68 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.LayerSummary.Size(childComplexity), true - case "OsArch.Arch": - if e.complexity.OsArch.Arch == nil { + case "ManifestSummary.ConfigDigest": + if e.complexity.ManifestSummary.ConfigDigest == nil { break } - return e.complexity.OsArch.Arch(childComplexity), true + return e.complexity.ManifestSummary.ConfigDigest(childComplexity), true - case "OsArch.Os": - if e.complexity.OsArch.Os == nil { + case "ManifestSummary.Digest": + if e.complexity.ManifestSummary.Digest == nil { break } - return e.complexity.OsArch.Os(childComplexity), true + return e.complexity.ManifestSummary.Digest(childComplexity), true + + case "ManifestSummary.DownloadCount": + if e.complexity.ManifestSummary.DownloadCount == nil { + break + } + + return e.complexity.ManifestSummary.DownloadCount(childComplexity), true + + case "ManifestSummary.History": + if e.complexity.ManifestSummary.History == nil { + break + } + + return e.complexity.ManifestSummary.History(childComplexity), true + + case "ManifestSummary.LastUpdated": + if e.complexity.ManifestSummary.LastUpdated == nil { + break + } + + return e.complexity.ManifestSummary.LastUpdated(childComplexity), true + + case "ManifestSummary.Layers": + if e.complexity.ManifestSummary.Layers == nil { + break + } + + return e.complexity.ManifestSummary.Layers(childComplexity), true + + case "ManifestSummary.Platform": + if e.complexity.ManifestSummary.Platform == nil { + break + } + + return e.complexity.ManifestSummary.Platform(childComplexity), true + + case "ManifestSummary.Size": + if e.complexity.ManifestSummary.Size == nil { + break + } + + return e.complexity.ManifestSummary.Size(childComplexity), true + + case "ManifestSummary.Vulnerabilities": + if e.complexity.ManifestSummary.Vulnerabilities == nil { + break + } + + return e.complexity.ManifestSummary.Vulnerabilities(childComplexity), true case "PackageInfo.FixedVersion": if e.complexity.PackageInfo.FixedVersion == nil { @@ -622,6 +651,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PaginatedReposResult.Results(childComplexity), true + case "Platform.Arch": + if e.complexity.Platform.Arch == nil { + break + } + + return e.complexity.Platform.Arch(childComplexity), true + + case "Platform.Os": + if e.complexity.Platform.Os == nil { + break + } + + return e.complexity.Platform.Os(childComplexity), true + case "Query.BaseImageList": if e.complexity.Query.BaseImageList == nil { break @@ -632,7 +675,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.BaseImageList(childComplexity, args["image"].(string), args["requestedPage"].(*PageInput)), true + return e.complexity.Query.BaseImageList(childComplexity, args["image"].(string), args["digest"].(*string), args["requestedPage"].(*PageInput)), true case "Query.CVEListForImage": if e.complexity.Query.CVEListForImage == nil { @@ -656,7 +699,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.DerivedImageList(childComplexity, args["image"].(string), args["requestedPage"].(*PageInput)), true + return e.complexity.Query.DerivedImageList(childComplexity, args["image"].(string), args["digest"].(*string), args["requestedPage"].(*PageInput)), true case "Query.ExpandedRepoInfo": if e.complexity.Query.ExpandedRepoInfo == nil { @@ -1073,53 +1116,33 @@ type ImageSummary { """ Tag: String """ - Digest of the manifest file associated with this image + List of manifests for all supported versions of the image for different operating systems and architectures """ - Digest: String + Manifests: [ManifestSummary] """ - Digest of the config file associated with this image - """ - ConfigDigest: String - """ - Timestamp of the last modification done to the image (from config or the last updated layer) - """ - LastUpdated: Time - """ - True if the image has a signature associated with it, false otherwise - """ - IsSigned: Boolean - """ - Total size of the files associated with this image (manigest, config, layers) + Total size of the files associated with all images (manifest, config, layers) """ Size: String """ - OS and architecture supported by this image - """ - Platform: OsArch - """ - Vendor associated with this image, the distributing entity, organization or individual - """ - Vendor: String - """ - Integer used to rank search results by relevance - """ - Score: Int - """ Number of downloads of the manifest of this image """ DownloadCount: Int """ - Information on the layers of this image + Timestamp of the last modification done to the image (from config or the last updated layer) """ - Layers: [LayerSummary] + LastUpdated: Time """ Human-readable description of the software packaged in the image """ Description: String """ + True if the image has a signature associated with it, false otherwise + """ + IsSigned: Boolean + """ License(s) under which contained software is distributed as an SPDX License Expression """ - Licenses: String + Licenses: String # The value of the annotation if present, 'unknown' otherwise). """ Labels associated with this image NOTE: currently this field is unused @@ -1130,6 +1153,10 @@ type ImageSummary { """ Title: String """ + Integer used to rank search results by relevance + """ + Score: Int + """ URL to get source code for building the image """ Source: String @@ -1138,6 +1165,52 @@ type ImageSummary { """ Documentation: String """ + Vendor associated with this image, the distributing entity, organization or individual + """ + Vendor: String + """ + Contact details of the people or organization responsible for the image + """ + Authors: String + """ + Short summary of the identified CVEs + """ + Vulnerabilities: ImageVulnerabilitySummary +} +""" +Details about a specific version of an image for a certain operating system and architecture. +""" +type ManifestSummary { + """ + Digest of the manifest file associated with this image + """ + Digest: String + """ + Digest of the config file associated with this image + """ + ConfigDigest: String + """ + Timestamp of the last update to an image inside this repository + """ + LastUpdated: Time + """ + Total size of the files associated with this manifest (manifest, config, layers) + """ + Size: String + """ + OS and architecture supported by this image + """ + Platform: Platform + """ + Total numer of image manifest downloads from this repository + """ + DownloadCount: Int + """ + List of layers matching the search criteria + NOTE: the actual search logic for layers is not implemented at the moment + """ + Layers: [LayerSummary] + """ Information about the history of the specific image, see LayerHistory """ History: [LayerHistory] @@ -1145,10 +1218,6 @@ type ImageSummary { Short summary of the identified CVEs """ Vulnerabilities: ImageVulnerabilitySummary - """ - Contact details of the people or organization responsible for the image - """ - Authors: String } """ @@ -1184,7 +1253,7 @@ type RepoSummary { """ List of platforms supported by this repository """ - Platforms: [OsArch] + Platforms: [Platform] """ Vendors associated with this image, the distributing entities, organizations or individuals """ @@ -1320,7 +1389,7 @@ type Referrer { """ Contains details about the OS and architecture of the image """ -type OsArch { +type Platform { """ The name of the operating system which the image is built to run on, Should be values listed in the Go Language document https://go.dev/doc/install/source#environment @@ -1555,6 +1624,8 @@ type Query { DerivedImageList( "Image name in the format ` + "`" + `repository:tag` + "`" + `" image: String!, + "Digest of a specific manifest inside the image. When null whole image is considered" + digest: String, "Sets the parameters of the requested page" requestedPage: PageInput ): PaginatedImagesResult! @@ -1565,6 +1636,8 @@ type Query { BaseImageList( "Image name in the format ` + "`" + `repository:tag` + "`" + `" image: String!, + "Digest of a specific manifest inside the image. When null whole image is considered" + digest: String, "Sets the parameters of the requested page" requestedPage: PageInput ): PaginatedImagesResult! @@ -1610,15 +1683,24 @@ func (ec *executionContext) field_Query_BaseImageList_args(ctx context.Context, } } args["image"] = arg0 - var arg1 *PageInput - if tmp, ok := rawArgs["requestedPage"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestedPage")) - arg1, err = ec.unmarshalOPageInput2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInput(ctx, tmp) + var arg1 *string + if tmp, ok := rawArgs["digest"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("digest")) + arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp) if err != nil { return nil, err } } - args["requestedPage"] = arg1 + args["digest"] = arg1 + var arg2 *PageInput + if tmp, ok := rawArgs["requestedPage"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestedPage")) + arg2, err = ec.unmarshalOPageInput2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["requestedPage"] = arg2 return args, nil } @@ -1658,15 +1740,24 @@ func (ec *executionContext) field_Query_DerivedImageList_args(ctx context.Contex } } args["image"] = arg0 - var arg1 *PageInput - if tmp, ok := rawArgs["requestedPage"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestedPage")) - arg1, err = ec.unmarshalOPageInput2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInput(ctx, tmp) + var arg1 *string + if tmp, ok := rawArgs["digest"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("digest")) + arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp) if err != nil { return nil, err } } - args["requestedPage"] = arg1 + args["digest"] = arg1 + var arg2 *PageInput + if tmp, ok := rawArgs["requestedPage"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestedPage")) + arg2, err = ec.unmarshalOPageInput2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["requestedPage"] = arg2 return args, nil } @@ -2462,44 +2553,36 @@ func (ec *executionContext) fieldContext_GlobalSearchResult_Images(ctx context.C return ec.fieldContext_ImageSummary_RepoName(ctx, field) case "Tag": return ec.fieldContext_ImageSummary_Tag(ctx, field) - case "Digest": - return ec.fieldContext_ImageSummary_Digest(ctx, field) - case "ConfigDigest": - return ec.fieldContext_ImageSummary_ConfigDigest(ctx, field) - case "LastUpdated": - return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) - case "IsSigned": - return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "Manifests": + return ec.fieldContext_ImageSummary_Manifests(ctx, field) case "Size": return ec.fieldContext_ImageSummary_Size(ctx, field) - case "Platform": - return ec.fieldContext_ImageSummary_Platform(ctx, field) - case "Vendor": - return ec.fieldContext_ImageSummary_Vendor(ctx, field) - case "Score": - return ec.fieldContext_ImageSummary_Score(ctx, field) case "DownloadCount": return ec.fieldContext_ImageSummary_DownloadCount(ctx, field) - case "Layers": - return ec.fieldContext_ImageSummary_Layers(ctx, field) + case "LastUpdated": + return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) case "Description": return ec.fieldContext_ImageSummary_Description(ctx, field) + case "IsSigned": + return ec.fieldContext_ImageSummary_IsSigned(ctx, field) case "Licenses": 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 "Score": + return ec.fieldContext_ImageSummary_Score(ctx, field) case "Source": return ec.fieldContext_ImageSummary_Source(ctx, field) case "Documentation": return ec.fieldContext_ImageSummary_Documentation(ctx, field) - case "History": - return ec.fieldContext_ImageSummary_History(ctx, field) - case "Vulnerabilities": - return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) + case "Vendor": + return ec.fieldContext_ImageSummary_Vendor(ctx, field) case "Authors": return ec.fieldContext_ImageSummary_Authors(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -2908,8 +2991,8 @@ func (ec *executionContext) fieldContext_ImageSummary_Tag(ctx context.Context, f return fc, nil } -func (ec *executionContext) _ImageSummary_Digest(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ImageSummary_Digest(ctx, field) +func (ec *executionContext) _ImageSummary_Manifests(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_Manifests(ctx, field) if err != nil { return graphql.Null } @@ -2922,7 +3005,7 @@ func (ec *executionContext) _ImageSummary_Digest(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Digest, nil + return obj.Manifests, nil }) if err != nil { ec.Error(ctx, err) @@ -2931,142 +3014,39 @@ func (ec *executionContext) _ImageSummary_Digest(ctx context.Context, field grap if resTmp == nil { return graphql.Null } - res := resTmp.(*string) + res := resTmp.([]*ManifestSummary) fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + return ec.marshalOManifestSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐManifestSummary(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_ImageSummary_Digest(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_ImageSummary_Manifests(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_ConfigDigest(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ImageSummary_ConfigDigest(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.ConfigDigest, 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_ConfigDigest(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_LastUpdated(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ImageSummary_LastUpdated(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.LastUpdated, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*time.Time) - fc.Result = res - return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_ImageSummary_LastUpdated(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 Time does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _ImageSummary_IsSigned(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ImageSummary_IsSigned(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.IsSigned, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*bool) - fc.Result = res - return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_ImageSummary_IsSigned(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 Boolean does not have child fields") + switch field.Name { + case "Digest": + return ec.fieldContext_ManifestSummary_Digest(ctx, field) + case "ConfigDigest": + return ec.fieldContext_ManifestSummary_ConfigDigest(ctx, field) + case "LastUpdated": + return ec.fieldContext_ManifestSummary_LastUpdated(ctx, field) + case "Size": + return ec.fieldContext_ManifestSummary_Size(ctx, field) + case "Platform": + return ec.fieldContext_ManifestSummary_Platform(ctx, field) + case "DownloadCount": + return ec.fieldContext_ManifestSummary_DownloadCount(ctx, field) + case "Layers": + return ec.fieldContext_ManifestSummary_Layers(ctx, field) + case "History": + return ec.fieldContext_ManifestSummary_History(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ManifestSummary_Vulnerabilities(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ManifestSummary", field.Name) }, } return fc, nil @@ -3113,135 +3093,6 @@ func (ec *executionContext) fieldContext_ImageSummary_Size(ctx context.Context, return fc, nil } -func (ec *executionContext) _ImageSummary_Platform(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ImageSummary_Platform(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.Platform, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*OsArch) - fc.Result = res - return ec.marshalOOsArch2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐOsArch(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_ImageSummary_Platform(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) { - switch field.Name { - case "Os": - return ec.fieldContext_OsArch_Os(ctx, field) - case "Arch": - return ec.fieldContext_OsArch_Arch(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type OsArch", field.Name) - }, - } - return fc, nil -} - -func (ec *executionContext) _ImageSummary_Vendor(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ImageSummary_Vendor(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.Vendor, 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_Vendor(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_Score(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ImageSummary_Score(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.Score, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*int) - fc.Result = res - return ec.marshalOInt2ᚖint(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_ImageSummary_Score(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 Int does not have child fields") - }, - } - return fc, nil -} - func (ec *executionContext) _ImageSummary_DownloadCount(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ImageSummary_DownloadCount(ctx, field) if err != nil { @@ -3283,8 +3134,8 @@ func (ec *executionContext) fieldContext_ImageSummary_DownloadCount(ctx context. return fc, nil } -func (ec *executionContext) _ImageSummary_Layers(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ImageSummary_Layers(ctx, field) +func (ec *executionContext) _ImageSummary_LastUpdated(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_LastUpdated(ctx, field) if err != nil { return graphql.Null } @@ -3297,7 +3148,7 @@ func (ec *executionContext) _ImageSummary_Layers(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Layers, nil + return obj.LastUpdated, nil }) if err != nil { ec.Error(ctx, err) @@ -3306,27 +3157,19 @@ func (ec *executionContext) _ImageSummary_Layers(ctx context.Context, field grap if resTmp == nil { return graphql.Null } - res := resTmp.([]*LayerSummary) + res := resTmp.(*time.Time) fc.Result = res - return ec.marshalOLayerSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐLayerSummary(ctx, field.Selections, res) + return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_ImageSummary_Layers(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_ImageSummary_LastUpdated(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) { - switch field.Name { - case "Size": - return ec.fieldContext_LayerSummary_Size(ctx, field) - case "Digest": - return ec.fieldContext_LayerSummary_Digest(ctx, field) - case "Score": - return ec.fieldContext_LayerSummary_Score(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type LayerSummary", field.Name) + return nil, errors.New("field of type Time does not have child fields") }, } return fc, nil @@ -3373,6 +3216,47 @@ func (ec *executionContext) fieldContext_ImageSummary_Description(ctx context.Co return fc, nil } +func (ec *executionContext) _ImageSummary_IsSigned(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_IsSigned(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.IsSigned, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*bool) + fc.Result = res + return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageSummary_IsSigned(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 Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _ImageSummary_Licenses(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ImageSummary_Licenses(ctx, field) if err != nil { @@ -3496,6 +3380,47 @@ func (ec *executionContext) fieldContext_ImageSummary_Title(ctx context.Context, return fc, nil } +func (ec *executionContext) _ImageSummary_Score(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_Score(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.Score, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageSummary_Score(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 Int 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 { @@ -3578,8 +3503,8 @@ func (ec *executionContext) fieldContext_ImageSummary_Documentation(ctx context. return fc, nil } -func (ec *executionContext) _ImageSummary_History(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ImageSummary_History(ctx, field) +func (ec *executionContext) _ImageSummary_Vendor(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_Vendor(ctx, field) if err != nil { return graphql.Null } @@ -3592,7 +3517,7 @@ func (ec *executionContext) _ImageSummary_History(ctx context.Context, field gra }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.History, nil + return obj.Vendor, nil }) if err != nil { ec.Error(ctx, err) @@ -3601,25 +3526,60 @@ func (ec *executionContext) _ImageSummary_History(ctx context.Context, field gra if resTmp == nil { return graphql.Null } - res := resTmp.([]*LayerHistory) + res := resTmp.(*string) fc.Result = res - return ec.marshalOLayerHistory2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐLayerHistory(ctx, field.Selections, res) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_ImageSummary_History(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_ImageSummary_Vendor(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) { - switch field.Name { - case "Layer": - return ec.fieldContext_LayerHistory_Layer(ctx, field) - case "HistoryDescription": - return ec.fieldContext_LayerHistory_HistoryDescription(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type LayerHistory", field.Name) + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageSummary_Authors(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_Authors(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.Authors, 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_Authors(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 @@ -3672,47 +3632,6 @@ func (ec *executionContext) fieldContext_ImageSummary_Vulnerabilities(ctx contex return fc, nil } -func (ec *executionContext) _ImageSummary_Authors(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ImageSummary_Authors(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.Authors, 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_Authors(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) _ImageVulnerabilitySummary_MaxSeverity(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field) if err != nil { @@ -4020,8 +3939,8 @@ func (ec *executionContext) fieldContext_LayerSummary_Score(ctx context.Context, return fc, nil } -func (ec *executionContext) _OsArch_Os(ctx context.Context, field graphql.CollectedField, obj *OsArch) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_OsArch_Os(ctx, field) +func (ec *executionContext) _ManifestSummary_Digest(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ManifestSummary_Digest(ctx, field) if err != nil { return graphql.Null } @@ -4034,7 +3953,7 @@ func (ec *executionContext) _OsArch_Os(ctx context.Context, field graphql.Collec }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Os, nil + return obj.Digest, nil }) if err != nil { ec.Error(ctx, err) @@ -4048,9 +3967,9 @@ func (ec *executionContext) _OsArch_Os(ctx context.Context, field graphql.Collec return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_OsArch_Os(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_ManifestSummary_Digest(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "OsArch", + Object: "ManifestSummary", Field: field, IsMethod: false, IsResolver: false, @@ -4061,8 +3980,8 @@ func (ec *executionContext) fieldContext_OsArch_Os(ctx context.Context, field gr return fc, nil } -func (ec *executionContext) _OsArch_Arch(ctx context.Context, field graphql.CollectedField, obj *OsArch) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_OsArch_Arch(ctx, field) +func (ec *executionContext) _ManifestSummary_ConfigDigest(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ManifestSummary_ConfigDigest(ctx, field) if err != nil { return graphql.Null } @@ -4075,7 +3994,7 @@ func (ec *executionContext) _OsArch_Arch(ctx context.Context, field graphql.Coll }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Arch, nil + return obj.ConfigDigest, nil }) if err != nil { ec.Error(ctx, err) @@ -4089,9 +4008,9 @@ func (ec *executionContext) _OsArch_Arch(ctx context.Context, field graphql.Coll return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_OsArch_Arch(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_ManifestSummary_ConfigDigest(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "OsArch", + Object: "ManifestSummary", Field: field, IsMethod: false, IsResolver: false, @@ -4102,6 +4021,319 @@ func (ec *executionContext) fieldContext_OsArch_Arch(ctx context.Context, field return fc, nil } +func (ec *executionContext) _ManifestSummary_LastUpdated(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ManifestSummary_LastUpdated(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.LastUpdated, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*time.Time) + fc.Result = res + return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ManifestSummary_LastUpdated(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ManifestSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Time does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ManifestSummary_Size(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ManifestSummary_Size(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.Size, 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_ManifestSummary_Size(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ManifestSummary", + 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) _ManifestSummary_Platform(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ManifestSummary_Platform(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.Platform, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*Platform) + fc.Result = res + return ec.marshalOPlatform2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPlatform(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ManifestSummary_Platform(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ManifestSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Os": + return ec.fieldContext_Platform_Os(ctx, field) + case "Arch": + return ec.fieldContext_Platform_Arch(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Platform", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _ManifestSummary_DownloadCount(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ManifestSummary_DownloadCount(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.DownloadCount, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ManifestSummary_DownloadCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ManifestSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ManifestSummary_Layers(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ManifestSummary_Layers(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.Layers, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*LayerSummary) + fc.Result = res + return ec.marshalOLayerSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐLayerSummary(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ManifestSummary_Layers(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ManifestSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Size": + return ec.fieldContext_LayerSummary_Size(ctx, field) + case "Digest": + return ec.fieldContext_LayerSummary_Digest(ctx, field) + case "Score": + return ec.fieldContext_LayerSummary_Score(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type LayerSummary", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _ManifestSummary_History(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ManifestSummary_History(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.History, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*LayerHistory) + fc.Result = res + return ec.marshalOLayerHistory2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐLayerHistory(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ManifestSummary_History(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ManifestSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Layer": + return ec.fieldContext_LayerHistory_Layer(ctx, field) + case "HistoryDescription": + return ec.fieldContext_LayerHistory_HistoryDescription(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type LayerHistory", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _ManifestSummary_Vulnerabilities(ctx context.Context, field graphql.CollectedField, obj *ManifestSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ManifestSummary_Vulnerabilities(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.Vulnerabilities, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*ImageVulnerabilitySummary) + fc.Result = res + return ec.marshalOImageVulnerabilitySummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageVulnerabilitySummary(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ManifestSummary_Vulnerabilities(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ManifestSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "MaxSeverity": + return ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field) + case "Count": + return ec.fieldContext_ImageVulnerabilitySummary_Count(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ImageVulnerabilitySummary", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _PackageInfo_Name(ctx context.Context, field graphql.CollectedField, obj *PackageInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PackageInfo_Name(ctx, field) if err != nil { @@ -4403,44 +4635,36 @@ func (ec *executionContext) fieldContext_PaginatedImagesResult_Results(ctx conte return ec.fieldContext_ImageSummary_RepoName(ctx, field) case "Tag": return ec.fieldContext_ImageSummary_Tag(ctx, field) - case "Digest": - return ec.fieldContext_ImageSummary_Digest(ctx, field) - case "ConfigDigest": - return ec.fieldContext_ImageSummary_ConfigDigest(ctx, field) - case "LastUpdated": - return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) - case "IsSigned": - return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "Manifests": + return ec.fieldContext_ImageSummary_Manifests(ctx, field) case "Size": return ec.fieldContext_ImageSummary_Size(ctx, field) - case "Platform": - return ec.fieldContext_ImageSummary_Platform(ctx, field) - case "Vendor": - return ec.fieldContext_ImageSummary_Vendor(ctx, field) - case "Score": - return ec.fieldContext_ImageSummary_Score(ctx, field) case "DownloadCount": return ec.fieldContext_ImageSummary_DownloadCount(ctx, field) - case "Layers": - return ec.fieldContext_ImageSummary_Layers(ctx, field) + case "LastUpdated": + return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) case "Description": return ec.fieldContext_ImageSummary_Description(ctx, field) + case "IsSigned": + return ec.fieldContext_ImageSummary_IsSigned(ctx, field) case "Licenses": 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 "Score": + return ec.fieldContext_ImageSummary_Score(ctx, field) case "Source": return ec.fieldContext_ImageSummary_Source(ctx, field) case "Documentation": return ec.fieldContext_ImageSummary_Documentation(ctx, field) - case "History": - return ec.fieldContext_ImageSummary_History(ctx, field) - case "Vulnerabilities": - return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) + case "Vendor": + return ec.fieldContext_ImageSummary_Vendor(ctx, field) case "Authors": return ec.fieldContext_ImageSummary_Authors(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -4563,6 +4787,88 @@ func (ec *executionContext) fieldContext_PaginatedReposResult_Results(ctx contex return fc, nil } +func (ec *executionContext) _Platform_Os(ctx context.Context, field graphql.CollectedField, obj *Platform) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Platform_Os(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.Os, 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_Platform_Os(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Platform", + 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) _Platform_Arch(ctx context.Context, field graphql.CollectedField, obj *Platform) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Platform_Arch(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.Arch, 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_Platform_Arch(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Platform", + 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) _Query_CVEListForImage(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_CVEListForImage(ctx, field) if err != nil { @@ -5071,7 +5377,7 @@ func (ec *executionContext) _Query_DerivedImageList(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().DerivedImageList(rctx, fc.Args["image"].(string), fc.Args["requestedPage"].(*PageInput)) + return ec.resolvers.Query().DerivedImageList(rctx, fc.Args["image"].(string), fc.Args["digest"].(*string), fc.Args["requestedPage"].(*PageInput)) }) if err != nil { ec.Error(ctx, err) @@ -5132,7 +5438,7 @@ func (ec *executionContext) _Query_BaseImageList(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().BaseImageList(rctx, fc.Args["image"].(string), fc.Args["requestedPage"].(*PageInput)) + return ec.resolvers.Query().BaseImageList(rctx, fc.Args["image"].(string), fc.Args["digest"].(*string), fc.Args["requestedPage"].(*PageInput)) }) if err != nil { ec.Error(ctx, err) @@ -5222,44 +5528,36 @@ func (ec *executionContext) fieldContext_Query_Image(ctx context.Context, field return ec.fieldContext_ImageSummary_RepoName(ctx, field) case "Tag": return ec.fieldContext_ImageSummary_Tag(ctx, field) - case "Digest": - return ec.fieldContext_ImageSummary_Digest(ctx, field) - case "ConfigDigest": - return ec.fieldContext_ImageSummary_ConfigDigest(ctx, field) - case "LastUpdated": - return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) - case "IsSigned": - return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "Manifests": + return ec.fieldContext_ImageSummary_Manifests(ctx, field) case "Size": return ec.fieldContext_ImageSummary_Size(ctx, field) - case "Platform": - return ec.fieldContext_ImageSummary_Platform(ctx, field) - case "Vendor": - return ec.fieldContext_ImageSummary_Vendor(ctx, field) - case "Score": - return ec.fieldContext_ImageSummary_Score(ctx, field) case "DownloadCount": return ec.fieldContext_ImageSummary_DownloadCount(ctx, field) - case "Layers": - return ec.fieldContext_ImageSummary_Layers(ctx, field) + case "LastUpdated": + return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) case "Description": return ec.fieldContext_ImageSummary_Description(ctx, field) + case "IsSigned": + return ec.fieldContext_ImageSummary_IsSigned(ctx, field) case "Licenses": 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 "Score": + return ec.fieldContext_ImageSummary_Score(ctx, field) case "Source": return ec.fieldContext_ImageSummary_Source(ctx, field) case "Documentation": return ec.fieldContext_ImageSummary_Documentation(ctx, field) - case "History": - return ec.fieldContext_ImageSummary_History(ctx, field) - case "Vulnerabilities": - return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) + case "Vendor": + return ec.fieldContext_ImageSummary_Vendor(ctx, field) case "Authors": return ec.fieldContext_ImageSummary_Authors(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -5728,44 +6026,36 @@ func (ec *executionContext) fieldContext_RepoInfo_Images(ctx context.Context, fi return ec.fieldContext_ImageSummary_RepoName(ctx, field) case "Tag": return ec.fieldContext_ImageSummary_Tag(ctx, field) - case "Digest": - return ec.fieldContext_ImageSummary_Digest(ctx, field) - case "ConfigDigest": - return ec.fieldContext_ImageSummary_ConfigDigest(ctx, field) - case "LastUpdated": - return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) - case "IsSigned": - return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "Manifests": + return ec.fieldContext_ImageSummary_Manifests(ctx, field) case "Size": return ec.fieldContext_ImageSummary_Size(ctx, field) - case "Platform": - return ec.fieldContext_ImageSummary_Platform(ctx, field) - case "Vendor": - return ec.fieldContext_ImageSummary_Vendor(ctx, field) - case "Score": - return ec.fieldContext_ImageSummary_Score(ctx, field) case "DownloadCount": return ec.fieldContext_ImageSummary_DownloadCount(ctx, field) - case "Layers": - return ec.fieldContext_ImageSummary_Layers(ctx, field) + case "LastUpdated": + return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) case "Description": return ec.fieldContext_ImageSummary_Description(ctx, field) + case "IsSigned": + return ec.fieldContext_ImageSummary_IsSigned(ctx, field) case "Licenses": 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 "Score": + return ec.fieldContext_ImageSummary_Score(ctx, field) case "Source": return ec.fieldContext_ImageSummary_Source(ctx, field) case "Documentation": return ec.fieldContext_ImageSummary_Documentation(ctx, field) - case "History": - return ec.fieldContext_ImageSummary_History(ctx, field) - case "Vulnerabilities": - return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) + case "Vendor": + return ec.fieldContext_ImageSummary_Vendor(ctx, field) case "Authors": return ec.fieldContext_ImageSummary_Authors(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -5984,9 +6274,9 @@ func (ec *executionContext) _RepoSummary_Platforms(ctx context.Context, field gr if resTmp == nil { return graphql.Null } - res := resTmp.([]*OsArch) + res := resTmp.([]*Platform) fc.Result = res - return ec.marshalOOsArch2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐOsArch(ctx, field.Selections, res) + return ec.marshalOPlatform2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPlatform(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_RepoSummary_Platforms(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -5998,11 +6288,11 @@ func (ec *executionContext) fieldContext_RepoSummary_Platforms(ctx context.Conte Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "Os": - return ec.fieldContext_OsArch_Os(ctx, field) + return ec.fieldContext_Platform_Os(ctx, field) case "Arch": - return ec.fieldContext_OsArch_Arch(ctx, field) + return ec.fieldContext_Platform_Arch(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type OsArch", field.Name) + return nil, fmt.Errorf("no field named %q was found under type Platform", field.Name) }, } return fc, nil @@ -6130,44 +6420,36 @@ func (ec *executionContext) fieldContext_RepoSummary_NewestImage(ctx context.Con return ec.fieldContext_ImageSummary_RepoName(ctx, field) case "Tag": return ec.fieldContext_ImageSummary_Tag(ctx, field) - case "Digest": - return ec.fieldContext_ImageSummary_Digest(ctx, field) - case "ConfigDigest": - return ec.fieldContext_ImageSummary_ConfigDigest(ctx, field) - case "LastUpdated": - return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) - case "IsSigned": - return ec.fieldContext_ImageSummary_IsSigned(ctx, field) + case "Manifests": + return ec.fieldContext_ImageSummary_Manifests(ctx, field) case "Size": return ec.fieldContext_ImageSummary_Size(ctx, field) - case "Platform": - return ec.fieldContext_ImageSummary_Platform(ctx, field) - case "Vendor": - return ec.fieldContext_ImageSummary_Vendor(ctx, field) - case "Score": - return ec.fieldContext_ImageSummary_Score(ctx, field) case "DownloadCount": return ec.fieldContext_ImageSummary_DownloadCount(ctx, field) - case "Layers": - return ec.fieldContext_ImageSummary_Layers(ctx, field) + case "LastUpdated": + return ec.fieldContext_ImageSummary_LastUpdated(ctx, field) case "Description": return ec.fieldContext_ImageSummary_Description(ctx, field) + case "IsSigned": + return ec.fieldContext_ImageSummary_IsSigned(ctx, field) case "Licenses": 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 "Score": + return ec.fieldContext_ImageSummary_Score(ctx, field) case "Source": return ec.fieldContext_ImageSummary_Source(ctx, field) case "Documentation": return ec.fieldContext_ImageSummary_Documentation(ctx, field) - case "History": - return ec.fieldContext_ImageSummary_History(ctx, field) - case "Vulnerabilities": - return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) + case "Vendor": + return ec.fieldContext_ImageSummary_Vendor(ctx, field) case "Authors": return ec.fieldContext_ImageSummary_Authors(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -8407,50 +8689,30 @@ func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.Selection out.Values[i] = ec._ImageSummary_Tag(ctx, field, obj) - case "Digest": + case "Manifests": - out.Values[i] = ec._ImageSummary_Digest(ctx, field, obj) - - case "ConfigDigest": - - out.Values[i] = ec._ImageSummary_ConfigDigest(ctx, field, obj) - - case "LastUpdated": - - out.Values[i] = ec._ImageSummary_LastUpdated(ctx, field, obj) - - case "IsSigned": - - out.Values[i] = ec._ImageSummary_IsSigned(ctx, field, obj) + out.Values[i] = ec._ImageSummary_Manifests(ctx, field, obj) case "Size": out.Values[i] = ec._ImageSummary_Size(ctx, field, obj) - case "Platform": - - out.Values[i] = ec._ImageSummary_Platform(ctx, field, obj) - - case "Vendor": - - out.Values[i] = ec._ImageSummary_Vendor(ctx, field, obj) - - case "Score": - - out.Values[i] = ec._ImageSummary_Score(ctx, field, obj) - case "DownloadCount": out.Values[i] = ec._ImageSummary_DownloadCount(ctx, field, obj) - case "Layers": + case "LastUpdated": - out.Values[i] = ec._ImageSummary_Layers(ctx, field, obj) + out.Values[i] = ec._ImageSummary_LastUpdated(ctx, field, obj) case "Description": out.Values[i] = ec._ImageSummary_Description(ctx, field, obj) + case "IsSigned": + + out.Values[i] = ec._ImageSummary_IsSigned(ctx, field, obj) + case "Licenses": out.Values[i] = ec._ImageSummary_Licenses(ctx, field, obj) @@ -8463,6 +8725,10 @@ func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.Selection out.Values[i] = ec._ImageSummary_Title(ctx, field, obj) + case "Score": + + out.Values[i] = ec._ImageSummary_Score(ctx, field, obj) + case "Source": out.Values[i] = ec._ImageSummary_Source(ctx, field, obj) @@ -8471,18 +8737,18 @@ func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.Selection out.Values[i] = ec._ImageSummary_Documentation(ctx, field, obj) - case "History": + case "Vendor": - out.Values[i] = ec._ImageSummary_History(ctx, field, obj) - - case "Vulnerabilities": - - out.Values[i] = ec._ImageSummary_Vulnerabilities(ctx, field, obj) + out.Values[i] = ec._ImageSummary_Vendor(ctx, field, obj) case "Authors": out.Values[i] = ec._ImageSummary_Authors(ctx, field, obj) + case "Vulnerabilities": + + out.Values[i] = ec._ImageSummary_Vulnerabilities(ctx, field, obj) + default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -8585,23 +8851,51 @@ func (ec *executionContext) _LayerSummary(ctx context.Context, sel ast.Selection return out } -var osArchImplementors = []string{"OsArch"} +var manifestSummaryImplementors = []string{"ManifestSummary"} -func (ec *executionContext) _OsArch(ctx context.Context, sel ast.SelectionSet, obj *OsArch) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, osArchImplementors) +func (ec *executionContext) _ManifestSummary(ctx context.Context, sel ast.SelectionSet, obj *ManifestSummary) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, manifestSummaryImplementors) out := graphql.NewFieldSet(fields) var invalids uint32 for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("OsArch") - case "Os": + out.Values[i] = graphql.MarshalString("ManifestSummary") + case "Digest": - out.Values[i] = ec._OsArch_Os(ctx, field, obj) + out.Values[i] = ec._ManifestSummary_Digest(ctx, field, obj) - case "Arch": + case "ConfigDigest": - out.Values[i] = ec._OsArch_Arch(ctx, field, obj) + out.Values[i] = ec._ManifestSummary_ConfigDigest(ctx, field, obj) + + case "LastUpdated": + + out.Values[i] = ec._ManifestSummary_LastUpdated(ctx, field, obj) + + case "Size": + + out.Values[i] = ec._ManifestSummary_Size(ctx, field, obj) + + case "Platform": + + out.Values[i] = ec._ManifestSummary_Platform(ctx, field, obj) + + case "DownloadCount": + + out.Values[i] = ec._ManifestSummary_DownloadCount(ctx, field, obj) + + case "Layers": + + out.Values[i] = ec._ManifestSummary_Layers(ctx, field, obj) + + case "History": + + out.Values[i] = ec._ManifestSummary_History(ctx, field, obj) + + case "Vulnerabilities": + + out.Values[i] = ec._ManifestSummary_Vulnerabilities(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) @@ -8746,6 +9040,35 @@ func (ec *executionContext) _PaginatedReposResult(ctx context.Context, sel ast.S return out } +var platformImplementors = []string{"Platform"} + +func (ec *executionContext) _Platform(ctx context.Context, sel ast.SelectionSet, obj *Platform) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, platformImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Platform") + case "Os": + + out.Values[i] = ec._Platform_Os(ctx, field, obj) + + case "Arch": + + out.Values[i] = ec._Platform_Arch(ctx, field, obj) + + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var queryImplementors = []string{"Query"} func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -10339,7 +10662,7 @@ func (ec *executionContext) marshalOLayerSummary2ᚖzotregistryᚗioᚋzotᚋpkg return ec._LayerSummary(ctx, sel, v) } -func (ec *executionContext) marshalOOsArch2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐOsArch(ctx context.Context, sel ast.SelectionSet, v []*OsArch) graphql.Marshaler { +func (ec *executionContext) marshalOManifestSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐManifestSummary(ctx context.Context, sel ast.SelectionSet, v []*ManifestSummary) graphql.Marshaler { if v == nil { return graphql.Null } @@ -10366,7 +10689,7 @@ func (ec *executionContext) marshalOOsArch2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋ if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalOOsArch2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐOsArch(ctx, sel, v[i]) + ret[i] = ec.marshalOManifestSummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐManifestSummary(ctx, sel, v[i]) } if isLen1 { f(i) @@ -10380,11 +10703,11 @@ func (ec *executionContext) marshalOOsArch2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋ return ret } -func (ec *executionContext) marshalOOsArch2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐOsArch(ctx context.Context, sel ast.SelectionSet, v *OsArch) graphql.Marshaler { +func (ec *executionContext) marshalOManifestSummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐManifestSummary(ctx context.Context, sel ast.SelectionSet, v *ManifestSummary) graphql.Marshaler { if v == nil { return graphql.Null } - return ec._OsArch(ctx, sel, v) + return ec._ManifestSummary(ctx, sel, v) } func (ec *executionContext) marshalOPackageInfo2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPackageInfo(ctx context.Context, sel ast.SelectionSet, v []*PackageInfo) graphql.Marshaler { @@ -10450,6 +10773,54 @@ func (ec *executionContext) unmarshalOPageInput2ᚖzotregistryᚗioᚋzotᚋpkg return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalOPlatform2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPlatform(ctx context.Context, sel ast.SelectionSet, v []*Platform) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOPlatform2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPlatform(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + +func (ec *executionContext) marshalOPlatform2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPlatform(ctx context.Context, sel ast.SelectionSet, v *Platform) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Platform(ctx, sel, v) +} + func (ec *executionContext) marshalOReferrer2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐReferrer(ctx context.Context, sel ast.SelectionSet, v *Referrer) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index c7179c56..8b1f536b 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -91,28 +91,18 @@ type ImageSummary struct { RepoName *string `json:"RepoName"` // Tag identifying the image within the repository Tag *string `json:"Tag"` - // Digest of the manifest file associated with this image - Digest *string `json:"Digest"` - // Digest of the config file associated with this image - ConfigDigest *string `json:"ConfigDigest"` - // Timestamp of the last modification done to the image (from config or the last updated layer) - LastUpdated *time.Time `json:"LastUpdated"` - // True if the image has a signature associated with it, false otherwise - IsSigned *bool `json:"IsSigned"` - // Total size of the files associated with this image (manigest, config, layers) + // List of manifests for all supported versions of the image for different operating systems and architectures + Manifests []*ManifestSummary `json:"Manifests"` + // Total size of the files associated with all images (manifest, config, layers) Size *string `json:"Size"` - // OS and architecture supported by this image - Platform *OsArch `json:"Platform"` - // Vendor associated with this image, the distributing entity, organization or individual - Vendor *string `json:"Vendor"` - // Integer used to rank search results by relevance - Score *int `json:"Score"` // Number of downloads of the manifest of this image DownloadCount *int `json:"DownloadCount"` - // Information on the layers of this image - Layers []*LayerSummary `json:"Layers"` + // Timestamp of the last modification done to the image (from config or the last updated layer) + LastUpdated *time.Time `json:"LastUpdated"` // Human-readable description of the software packaged in the image Description *string `json:"Description"` + // True if the image has a signature associated with it, false otherwise + IsSigned *bool `json:"IsSigned"` // License(s) under which contained software is distributed as an SPDX License Expression Licenses *string `json:"Licenses"` // Labels associated with this image @@ -120,16 +110,18 @@ type ImageSummary struct { Labels *string `json:"Labels"` // Human-readable title of the image Title *string `json:"Title"` + // Integer used to rank search results by relevance + Score *int `json:"Score"` // URL to get source code for building the image Source *string `json:"Source"` // URL to get documentation on the image Documentation *string `json:"Documentation"` - // Information about the history of the specific image, see LayerHistory - History []*LayerHistory `json:"History"` - // Short summary of the identified CVEs - Vulnerabilities *ImageVulnerabilitySummary `json:"Vulnerabilities"` + // Vendor associated with this image, the distributing entity, organization or individual + Vendor *string `json:"Vendor"` // Contact details of the people or organization responsible for the image Authors *string `json:"Authors"` + // Short summary of the identified CVEs + Vulnerabilities *ImageVulnerabilitySummary `json:"Vulnerabilities"` } // Contains summary of vulnerabilities found in a specific image @@ -158,14 +150,27 @@ type LayerSummary struct { Score *int `json:"Score"` } -// Contains details about the OS and architecture of the image -type OsArch struct { - // The name of the operating system which the image is built to run on, - // Should be values listed in the Go Language document https://go.dev/doc/install/source#environment - Os *string `json:"Os"` - // The name of the compilation architecture which the image is built to run on, - // Should be values listed in the Go Language document https://go.dev/doc/install/source#environment - Arch *string `json:"Arch"` +// Details about a specific version of an image for a certain operating system and architecture. +type ManifestSummary struct { + // Digest of the manifest file associated with this image + Digest *string `json:"Digest"` + // Digest of the config file associated with this image + ConfigDigest *string `json:"ConfigDigest"` + // Timestamp of the last update to an image inside this repository + LastUpdated *time.Time `json:"LastUpdated"` + // Total size of the files associated with this manifest (manifest, config, layers) + Size *string `json:"Size"` + // OS and architecture supported by this image + Platform *Platform `json:"Platform"` + // Total numer of image manifest downloads from this repository + DownloadCount *int `json:"DownloadCount"` + // List of layers matching the search criteria + // NOTE: the actual search logic for layers is not implemented at the moment + Layers []*LayerSummary `json:"Layers"` + // Information about the history of the specific image, see LayerHistory + History []*LayerHistory `json:"History"` + // Short summary of the identified CVEs + Vulnerabilities *ImageVulnerabilitySummary `json:"Vulnerabilities"` } // Contains the name of the package, the current installed version and the version where the CVE was fixed @@ -215,6 +220,16 @@ type PaginatedReposResult struct { Results []*RepoSummary `json:"Results"` } +// Contains details about the OS and architecture of the image +type Platform struct { + // The name of the operating system which the image is built to run on, + // Should be values listed in the Go Language document https://go.dev/doc/install/source#environment + Os *string `json:"Os"` + // The name of the compilation architecture which the image is built to run on, + // Should be values listed in the Go Language document https://go.dev/doc/install/source#environment + Arch *string `json:"Arch"` +} + // A referrer is an object which has a reference to a another object type Referrer struct { // Referrer MediaType @@ -248,7 +263,7 @@ type RepoSummary struct { // Total size of the files within this repository Size *string `json:"Size"` // List of platforms supported by this repository - Platforms []*OsArch `json:"Platforms"` + Platforms []*Platform `json:"Platforms"` // Vendors associated with this image, the distributing entities, organizations or individuals Vendors []*string `json:"Vendors"` // Integer used to rank search results by relevance diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index c29eafd4..428fad28 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -7,6 +7,7 @@ package search import ( "context" "encoding/json" + "fmt" "sort" "strings" @@ -137,13 +138,14 @@ func getImageListForDigest(ctx context.Context, digest string, repoDB repodb.Rep } // get all repos - reposMeta, manifestMetaMap, pageInfo, err := repoDB.FilterTags(ctx, FilterByDigest(digest), pageInput) + reposMeta, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.FilterTags(ctx, FilterByDigest(digest), pageInput) if err != nil { return &gql_generated.PaginatedImagesResult{}, err } for _, repoMeta := range reposMeta { - imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, indexDataMap, + skip, cveInfo) imageList = append(imageList, imageSummaries...) } @@ -157,7 +159,7 @@ func getImageListForDigest(ctx context.Context, digest string, repoDB repodb.Rep }, nil } -func getImageSummary(ctx context.Context, repo, tag string, repoDB repodb.RepoDB, +func getImageSummary(ctx context.Context, repo, tag string, digest *string, repoDB repodb.RepoDB, cveInfo cveinfo.CveInfo, log log.Logger, //nolint:unparam ) ( *gql_generated.ImageSummary, error, @@ -172,28 +174,115 @@ func getImageSummary(ctx context.Context, repo, tag string, repoDB repodb.RepoDB return nil, gqlerror.Errorf("can't find image: %s:%s", repo, tag) } - manifestDigest := manifestDescriptor.Digest - for t := range repoMeta.Tags { if t != tag { delete(repoMeta.Tags, t) } } - manifestMeta, err := repoDB.GetManifestMeta(repo, godigest.Digest(manifestDigest)) - if err != nil { - return nil, err - } + var ( + manifestMetaMap = map[string]repodb.ManifestMetadata{} + indexDataMap = map[string]repodb.IndexData{} + ) - manifestMetaMap := map[string]repodb.ManifestMetadata{ - manifestDigest: manifestMeta, + switch manifestDescriptor.MediaType { + case ispec.MediaTypeImageManifest: + manifestDigest := manifestDescriptor.Digest + + if digest != nil && *digest != manifestDigest { + return nil, fmt.Errorf("resolver: can't get ManifestData for digest %s for image '%s:%s' %w", + manifestDigest, repo, tag, zerr.ErrManifestDataNotFound) + } + + manifestData, err := repoDB.GetManifestData(godigest.Digest(manifestDigest)) + if err != nil { + return nil, err + } + + manifestMetaMap[manifestDigest] = repodb.ManifestMetadata{ + ManifestBlob: manifestData.ManifestBlob, + ConfigBlob: manifestData.ConfigBlob, + } + case ispec.MediaTypeImageIndex: + indexDigest := manifestDescriptor.Digest + + indexData, err := repoDB.GetIndexData(godigest.Digest(indexDigest)) + if err != nil { + return nil, err + } + + var indexContent ispec.Index + + err = json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return nil, err + } + + if digest != nil { + manifestDigest := *digest + + digestFound := false + + for _, manifest := range indexContent.Manifests { + if manifest.Digest.String() == manifestDigest { + digestFound = true + + break + } + } + + if !digestFound { + return nil, fmt.Errorf("resolver: can't get ManifestData for digest %s for image '%s:%s' %w", + manifestDigest, repo, tag, zerr.ErrManifestDataNotFound) + } + + manifestData, err := repoDB.GetManifestData(godigest.Digest(manifestDigest)) + if err != nil { + return nil, fmt.Errorf("resolver: can't get ManifestData for digest %s for image '%s:%s' %w", + manifestDigest, repo, tag, err) + } + + manifestMetaMap[manifestDigest] = repodb.ManifestMetadata{ + ManifestBlob: manifestData.ManifestBlob, + ConfigBlob: manifestData.ConfigBlob, + } + + // We update the tag descriptor to be the manifest descriptor with digest specified in the + // 'digest' parameter. We treat it as a standalone image. + repoMeta.Tags[tag] = repodb.Descriptor{ + Digest: manifestDigest, + MediaType: ispec.MediaTypeImageManifest, + } + + break + } + + for _, manifest := range indexContent.Manifests { + manifestData, err := repoDB.GetManifestData(manifest.Digest) + if err != nil { + return nil, fmt.Errorf("resolver: can't get ManifestData for digest %s for image '%s:%s' %w", + manifest.Digest, repo, tag, err) + } + + manifestMetaMap[manifest.Digest.String()] = repodb.ManifestMetadata{ + ManifestBlob: manifestData.ManifestBlob, + ConfigBlob: manifestData.ConfigBlob, + } + } + + indexDataMap[indexDigest] = indexData + default: + log.Error().Msgf("resolver: media type '%s' not supported", manifestDescriptor.MediaType) } skip := convert.SkipQGLField{ Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"), } + imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, indexDataMap, skip, cveInfo) - imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + if len(imageSummaries) == 0 { + return &gql_generated.ImageSummary{}, nil + } return imageSummaries[0], nil } @@ -217,13 +306,17 @@ func getCVEListForImage( ), } - _, copyImgTag := common.GetImageDirAndTag(image) + repo, ref, isTag := common.GetImageDirAndReference(image) - if copyImgTag == "" { + if ref == "" { return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("no reference provided") } - cveList, pageInfo, err := cveInfo.GetCVEListForImage(image, pageInput) + if !isTag { + return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("reference by digest not supported") + } + + cveList, pageInfo, err := cveInfo.GetCVEListForImage(repo, ref, pageInput) if err != nil { return &gql_generated.CVEResultForImage{}, err } @@ -262,7 +355,7 @@ func getCVEListForImage( } return &gql_generated.CVEResultForImage{ - Tag: ©ImgTag, + Tag: &ref, CVEList: cveids, Page: &gql_generated.PageInfo{ TotalCount: pageInfo.TotalCount, @@ -276,7 +369,7 @@ func FilterByTagInfo(tagsInfo []common.TagInfo) repodb.FilterFunc { manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String() for _, tagInfo := range tagsInfo { - if tagInfo.Digest.String() == manifestDigest { + if tagInfo.Descriptor.Digest.String() == manifestDigest { return true } } @@ -342,13 +435,14 @@ func getImageListForCVE( } // get all repos - reposMeta, manifestMetaMap, pageInfo, err := repoDB.FilterTags(ctx, FilterByTagInfo(affectedImages), pageInput) + reposMeta, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.FilterTags(ctx, + FilterByTagInfo(affectedImages), pageInput) if err != nil { return &gql_generated.PaginatedImagesResult{}, err } for _, repoMeta := range reposMeta { - imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, indexDataMap, skip, cveInfo) imageList = append(imageList, imageSummaries...) } @@ -403,7 +497,7 @@ func getImageListWithCVEFixed( } // get all repos - reposMeta, manifestMetaMap, pageInfo, err := repoDB.FilterTags(ctx, FilterByTagInfo(tagsInfo), pageInput) + reposMeta, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.FilterTags(ctx, FilterByTagInfo(tagsInfo), pageInput) if err != nil { return &gql_generated.PaginatedImagesResult{}, err } @@ -413,7 +507,7 @@ func getImageListWithCVEFixed( continue } - imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, indexDataMap, skip, cveInfo) imageList = append(imageList, imageSummaries...) } @@ -452,13 +546,14 @@ func repoListWithNewestImage( ), } - reposMeta, manifestMetaMap, pageInfo, err := repoDB.SearchRepos(ctx, "", repodb.Filter{}, pageInput) + reposMeta, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.SearchRepos(ctx, "", repodb.Filter{}, pageInput) if err != nil { return &gql_generated.PaginatedReposResult{}, err } for _, repoMeta := range reposMeta { - repoSummary := convert.RepoMeta2RepoSummary(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + repoSummary := convert.RepoMeta2RepoSummary(ctx, repoMeta, manifestMetaMap, indexDataMap, + skip, cveInfo) repos = append(repos, repoSummary) } @@ -507,13 +602,14 @@ func globalSearch(ctx context.Context, query string, repoDB repodb.RepoDB, filte ), } - reposMeta, manifestMetaMap, pageInfo, err := repoDB.SearchRepos(ctx, query, localFilter, pageInput) + reposMeta, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.SearchRepos(ctx, query, localFilter, pageInput) if err != nil { return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err } for _, repoMeta := range reposMeta { - repoSummary := convert.RepoMeta2RepoSummary(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + repoSummary := convert.RepoMeta2RepoSummary(ctx, repoMeta, manifestMetaMap, indexDataMap, + skip, cveInfo) repos = append(repos, repoSummary) } @@ -537,13 +633,13 @@ func globalSearch(ctx context.Context, query string, repoDB repodb.RepoDB, filte ), } - reposMeta, manifestMetaMap, pageInfo, err := repoDB.SearchTags(ctx, query, localFilter, pageInput) + reposMeta, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.SearchTags(ctx, query, localFilter, pageInput) if err != nil { return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err } for _, repoMeta := range reposMeta { - imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, indexDataMap, skip, cveInfo) images = append(images, imageSummaries...) } @@ -563,7 +659,7 @@ func canSkipField(preloads map[string]bool, s string) bool { return !fieldIsPresent } -func derivedImageList(ctx context.Context, image string, repoDB repodb.RepoDB, +func derivedImageList(ctx context.Context, image string, digest *string, repoDB repodb.RepoDB, requestedPage *gql_generated.PageInput, cveInfo cveinfo.CveInfo, log log.Logger, ) (*gql_generated.PaginatedImagesResult, error) { @@ -590,7 +686,7 @@ func derivedImageList(ctx context.Context, image string, repoDB repodb.RepoDB, return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided") } - searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, repoDB, cveInfo, log) + searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, digest, repoDB, cveInfo, log) if err != nil { if errors.Is(err, zerr.ErrRepoMetaNotFound) { return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("repository: not found") @@ -600,7 +696,7 @@ func derivedImageList(ctx context.Context, image string, repoDB repodb.RepoDB, } // we need all available tags - reposMeta, manifestMetaMap, pageInfo, err := repoDB.FilterTags(ctx, + reposMeta, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.FilterTags(ctx, filterDerivedImages(searchedImage), pageInput) if err != nil { @@ -608,7 +704,7 @@ func derivedImageList(ctx context.Context, image string, repoDB repodb.RepoDB, } for _, repoMeta := range reposMeta { - summary := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + summary := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, indexDataMap, skip, cveInfo) derivedList = append(derivedList, summary...) } @@ -641,37 +737,42 @@ func filterDerivedImages(image *gql_generated.ImageSummary) repodb.FilterFunc { return false } - manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String() - if manifestDigest == *image.Digest { - return false - } + for i := range image.Manifests { + manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String() + if manifestDigest == *image.Manifests[i].Digest { + return false + } + imageLayers := image.Manifests[i].Layers - imageLayers := image.Layers + addImageToList = false + layers := imageManifest.Layers - addImageToList = false - layers := imageManifest.Layers + sameLayer := 0 - sameLayer := 0 - - for _, l := range imageLayers { - for _, k := range layers { - if k.Digest.String() == *l.Digest { - sameLayer++ + for _, l := range imageLayers { + for _, k := range layers { + if k.Digest.String() == *l.Digest { + sameLayer++ + } } } + + // if all layers are the same + if sameLayer == len(imageLayers) { + // it's a derived image + addImageToList = true + } + + if addImageToList { + return true + } } - // if all layers are the same - if sameLayer == len(imageLayers) { - // it's a derived image - addImageToList = true - } - - return addImageToList + return false } } -func baseImageList(ctx context.Context, image string, repoDB repodb.RepoDB, +func baseImageList(ctx context.Context, image string, digest *string, repoDB repodb.RepoDB, requestedPage *gql_generated.PageInput, cveInfo cveinfo.CveInfo, log log.Logger, ) (*gql_generated.PaginatedImagesResult, error) { @@ -699,7 +800,7 @@ func baseImageList(ctx context.Context, image string, repoDB repodb.RepoDB, return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided") } - searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, repoDB, cveInfo, log) + searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, digest, repoDB, cveInfo, log) if err != nil { if errors.Is(err, zerr.ErrRepoMetaNotFound) { return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("repository: not found") @@ -709,7 +810,7 @@ func baseImageList(ctx context.Context, image string, repoDB repodb.RepoDB, } // we need all available tags - reposMeta, manifestMetaMap, pageInfo, err := repoDB.FilterTags(ctx, + reposMeta, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.FilterTags(ctx, filterBaseImages(searchedImage), pageInput) if err != nil { @@ -717,7 +818,7 @@ func baseImageList(ctx context.Context, image string, repoDB repodb.RepoDB, } for _, repoMeta := range reposMeta { - summary := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + summary := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, indexDataMap, skip, cveInfo) imageSummaries = append(imageSummaries, summary...) } @@ -743,42 +844,45 @@ func filterBaseImages(image *gql_generated.ImageSummary) repodb.FilterFunc { return func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { var addImageToList bool - var imageManifest ispec.Manifest + var manifestContent ispec.Manifest - err := json.Unmarshal(manifestMeta.ManifestBlob, &imageManifest) + err := json.Unmarshal(manifestMeta.ManifestBlob, &manifestContent) if err != nil { return false } - manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String() - if manifestDigest == *image.Digest { - return false - } + for i := range image.Manifests { + manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String() + if manifestDigest == *image.Manifests[i].Digest { + return false + } - imageLayers := image.Layers + addImageToList = true - addImageToList = true - layers := imageManifest.Layers + for _, l := range manifestContent.Layers { + foundLayer := false - for _, l := range layers { - foundLayer := false + for _, k := range image.Manifests[i].Layers { + if l.Digest.String() == *k.Digest { + foundLayer = true - for _, k := range imageLayers { - if l.Digest.String() == *k.Digest { - foundLayer = true + break + } + } + + if !foundLayer { + addImageToList = false break } } - if !foundLayer { - addImageToList = false - - break + if addImageToList { + return true } } - return addImageToList + return false } } @@ -921,31 +1025,88 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv return &gql_generated.RepoInfo{}, err } - manifestMetaMap := map[string]repodb.ManifestMetadata{} + var ( + manifestMetaMap = map[string]repodb.ManifestMetadata{} + indexDataMap = map[string]repodb.IndexData{} + ) for tag, descriptor := range repoMeta.Tags { - digest := descriptor.Digest + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + digest := descriptor.Digest - if _, alreadyDownloaded := manifestMetaMap[digest]; alreadyDownloaded { - continue + if _, alreadyDownloaded := manifestMetaMap[digest]; alreadyDownloaded { + continue + } + + manifestMeta, err := repoDB.GetManifestMeta(repo, godigest.Digest(digest)) + if err != nil { + graphql.AddError(ctx, errors.Wrapf(err, + "resolver: failed to get manifest meta for image %s:%s with manifest digest %s", repo, tag, digest)) + + continue + } + + manifestMetaMap[digest] = manifestMeta + case ispec.MediaTypeImageIndex: + digest := descriptor.Digest + + if _, alreadyDownloaded := indexDataMap[digest]; alreadyDownloaded { + continue + } + + indexData, err := repoDB.GetIndexData(godigest.Digest(digest)) + if err != nil { + graphql.AddError(ctx, errors.Wrapf(err, + "resolver: failed to get manifest meta for image %s:%s with manifest digest %s", repo, tag, digest)) + + continue + } + + var indexContent ispec.Index + + err = json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + graphql.AddError(ctx, errors.Wrapf(err, + "resolver: failed to unmarshal index content for image %s:%s with digest %s", repo, tag, digest)) + + continue + } + + var errorOccured bool + + for _, descriptor := range indexContent.Manifests { + manifestMeta, err := repoDB.GetManifestMeta(repo, descriptor.Digest) + if err != nil { + graphql.AddError(ctx, errors.Wrapf(err, + "resolver: failed to get manifest meta with digest '%s' for multiarch image %s:%s", + digest, repo, tag), + ) + + errorOccured = true + + break + } + + manifestMetaMap[descriptor.Digest.String()] = manifestMeta + } + + if errorOccured { + continue + } + + indexDataMap[digest] = indexData + default: } - - manifestMeta, err := repoDB.GetManifestMeta(repo, godigest.Digest(digest)) - if err != nil { - graphql.AddError(ctx, errors.Wrapf(err, - "resolver: failed to get manifest meta for image %s:%s with manifest digest %s", repo, tag, digest)) - - continue - } - - manifestMetaMap[digest] = manifestMeta } skip := convert.SkipQGLField{ - Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Summary.NewestImage.Vulnerabilities"), + Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Summary.NewestImage.Vulnerabilities") && + canSkipField(convert.GetPreloads(ctx), "Images.Vulnerabilities"), } - repoSummary, imageSummaries := convert.RepoMeta2ExpandedRepoInfo(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + repoSummary, imageSummaries := convert.RepoMeta2ExpandedRepoInfo(ctx, repoMeta, manifestMetaMap, indexDataMap, + skip, cveInfo, log) dateSortedImages := make(timeSlice, 0, len(imageSummaries)) for _, imgSummary := range imageSummaries { @@ -1005,7 +1166,7 @@ func getImageList(ctx context.Context, repo string, repoDB repodb.RepoDB, cveInf } // reposMeta, manifestMetaMap, err := repoDB.SearchRepos(ctx, repo, repodb.Filter{}, pageInput) - reposMeta, manifestMetaMap, pageInfo, err := repoDB.FilterTags(ctx, + reposMeta, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.FilterTags(ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true }, @@ -1018,7 +1179,7 @@ func getImageList(ctx context.Context, repo string, repoDB repodb.RepoDB, cveInf if repoMeta.Name != repo && repo != "" { continue } - imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo) + imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, indexDataMap, skip, cveInfo) imageList = append(imageList, imageSummaries...) } diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index b1864e27..dc0f9472 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -35,8 +35,11 @@ func TestGlobalSearch(t *testing.T) { Convey("RepoDB SearchRepos error", func() { mockRepoDB := mocks.RepoDBMock{ SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, ErrTestError + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, + error, + ) { + return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), + map[string]repodb.IndexData{}, repodb.PageInfo{}, ErrTestError }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, @@ -53,7 +56,7 @@ func TestGlobalSearch(t *testing.T) { Convey("RepoDB SearchRepo is successful", func() { mockRepoDB := mocks.RepoDBMock{ SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "repo1", @@ -67,6 +70,13 @@ func TestGlobalSearch(t *testing.T) { MediaType: ispec.MediaTypeImageManifest, }, }, + Signatures: map[string]repodb.ManifestSignatures{ + "digestTag1.0.1": { + "cosign": []repodb.SignatureInfo{ + {SignatureManifestDigest: "testSignature", LayersInfo: []repodb.LayerInfo{}}, + }, + }, + }, Stars: 100, }, } @@ -105,7 +115,7 @@ func TestGlobalSearch(t *testing.T) { }, } - return repos, manifestMetas, repodb.PageInfo{}, nil + return repos, manifestMetas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -134,7 +144,7 @@ func TestGlobalSearch(t *testing.T) { Convey("RepoDB SearchRepo Bad manifest referenced", func() { mockRepoDB := mocks.RepoDBMock{ SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "repo1", @@ -144,6 +154,13 @@ func TestGlobalSearch(t *testing.T) { MediaType: ispec.MediaTypeImageManifest, }, }, + Signatures: map[string]repodb.ManifestSignatures{ + "digestTag1.0.1": { + "cosign": []repodb.SignatureInfo{ + {SignatureManifestDigest: "testSignature", LayersInfo: []repodb.LayerInfo{}}, + }, + }, + }, Stars: 100, }, } @@ -158,7 +175,7 @@ func TestGlobalSearch(t *testing.T) { }, } - return repos, manifestMetas, repodb.PageInfo{}, nil + return repos, manifestMetas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -198,7 +215,7 @@ func TestGlobalSearch(t *testing.T) { Convey("RepoDB SearchRepo good manifest referenced and bad config blob", func() { mockRepoDB := mocks.RepoDBMock{ SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "repo1", @@ -208,6 +225,13 @@ func TestGlobalSearch(t *testing.T) { MediaType: ispec.MediaTypeImageManifest, }, }, + Signatures: map[string]repodb.ManifestSignatures{ + "digestTag1.0.1": { + "cosign": []repodb.SignatureInfo{ + {SignatureManifestDigest: "testSignature", LayersInfo: []repodb.LayerInfo{}}, + }, + }, + }, Stars: 100, }, } @@ -222,7 +246,7 @@ func TestGlobalSearch(t *testing.T) { }, } - return repos, manifestMetas, repodb.PageInfo{}, nil + return repos, manifestMetas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -261,8 +285,11 @@ func TestGlobalSearch(t *testing.T) { Convey("RepoDB SearchTags gives error", func() { mockRepoDB := mocks.RepoDBMock{ SearchTagsFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, ErrTestError + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, + error, + ) { + return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), + map[string]repodb.IndexData{}, repodb.PageInfo{}, ErrTestError }, } const query = "repo1:1.0.1" @@ -281,7 +308,7 @@ func TestGlobalSearch(t *testing.T) { Convey("RepoDB SearchTags is successful", func() { mockRepoDB := mocks.RepoDBMock{ SearchTagsFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "repo1", @@ -291,6 +318,13 @@ func TestGlobalSearch(t *testing.T) { MediaType: ispec.MediaTypeImageManifest, }, }, + Signatures: map[string]repodb.ManifestSignatures{ + "digestTag1.0.1": { + "cosign": []repodb.SignatureInfo{ + {SignatureManifestDigest: "testSignature", LayersInfo: []repodb.LayerInfo{}}, + }, + }, + }, Stars: 100, }, } @@ -327,7 +361,7 @@ func TestGlobalSearch(t *testing.T) { }, } - return repos, manifestMetas, repodb.PageInfo{}, nil + return repos, manifestMetas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -359,9 +393,13 @@ func TestRepoListWithNewestImage(t *testing.T) { Convey("RepoListWithNewestImage", t, func() { Convey("RepoDB SearchRepos error", func() { mockRepoDB := mocks.RepoDBMock{ - SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, ErrTestError + SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, + requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, + error, + ) { + return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), + map[string]repodb.IndexData{}, repodb.PageInfo{}, ErrTestError }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, @@ -384,7 +422,7 @@ func TestRepoListWithNewestImage(t *testing.T) { Convey("RepoDB SearchRepo Bad manifest referenced", func() { mockRepoDB := mocks.RepoDBMock{ SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "repo1", @@ -394,6 +432,13 @@ func TestRepoListWithNewestImage(t *testing.T) { MediaType: ispec.MediaTypeImageManifest, }, }, + Signatures: map[string]repodb.ManifestSignatures{ + "digestTag1.0.1": { + "cosign": []repodb.SignatureInfo{ + {SignatureManifestDigest: "testSignature", LayersInfo: []repodb.LayerInfo{}}, + }, + }, + }, Stars: 100, }, { @@ -404,6 +449,13 @@ func TestRepoListWithNewestImage(t *testing.T) { MediaType: ispec.MediaTypeImageManifest, }, }, + Signatures: map[string]repodb.ManifestSignatures{ + "digestTag1.0.1": { + "cosign": []repodb.SignatureInfo{ + {SignatureManifestDigest: "testSignature", LayersInfo: []repodb.LayerInfo{}}, + }, + }, + }, Stars: 100, }, } @@ -426,7 +478,7 @@ func TestRepoListWithNewestImage(t *testing.T) { }, } - return repos, manifestMetas, repodb.PageInfo{}, nil + return repos, manifestMetas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -452,7 +504,7 @@ func TestRepoListWithNewestImage(t *testing.T) { createTime2 := createTime.Add(time.Second) mockRepoDB := mocks.RepoDBMock{ SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { pageFinder, err := repodb.NewBaseRepoPageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) So(err, ShouldBeNil) @@ -465,6 +517,13 @@ func TestRepoListWithNewestImage(t *testing.T) { MediaType: ispec.MediaTypeImageManifest, }, }, + Signatures: map[string]repodb.ManifestSignatures{ + "digestTag1.0.1": { + "cosign": []repodb.SignatureInfo{ + {SignatureManifestDigest: "testSignature", LayersInfo: []repodb.LayerInfo{}}, + }, + }, + }, Stars: 100, }, { @@ -475,6 +534,13 @@ func TestRepoListWithNewestImage(t *testing.T) { MediaType: ispec.MediaTypeImageManifest, }, }, + Signatures: map[string]repodb.ManifestSignatures{ + "digestTag1.0.1": { + "cosign": []repodb.SignatureInfo{ + {SignatureManifestDigest: "testSignature", LayersInfo: []repodb.LayerInfo{}}, + }, + }, + }, Stars: 100, }, } @@ -519,7 +585,7 @@ func TestRepoListWithNewestImage(t *testing.T) { }, } - return repos, manifestMetas, repodb.PageInfo{}, nil + return repos, manifestMetas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } Convey("RepoDB missing requestedPage", func() { @@ -563,8 +629,11 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, + error, + ) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, ErrTestError }, } @@ -578,7 +647,7 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -605,7 +674,7 @@ func TestImageListForDigest(t *testing.T) { }, } - return repos, manifestMetaDatas, repodb.PageInfo{}, nil + return repos, manifestMetaDatas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -626,7 +695,7 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -648,10 +717,10 @@ func TestImageListForDigest(t *testing.T) { }, } matchedTags := repos[0].Tags - for tag, descriptor := range repos[0].Tags { - if !filter(repos[0], manifestMetaDatas[descriptor.Digest]) { + for tag, manifestDescriptor := range repos[0].Tags { + if !filter(repos[0], manifestMetaDatas[manifestDescriptor.Digest]) { delete(matchedTags, tag) - delete(manifestMetaDatas, descriptor.Digest) + delete(manifestMetaDatas, manifestDescriptor.Digest) continue } @@ -659,7 +728,7 @@ func TestImageListForDigest(t *testing.T) { repos[0].Tags = matchedTags - return repos, manifestMetaDatas, repodb.PageInfo{}, nil + return repos, manifestMetaDatas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -699,7 +768,7 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -726,10 +795,10 @@ func TestImageListForDigest(t *testing.T) { } matchedTags := repos[0].Tags - for tag, descriptor := range repos[0].Tags { - if !filter(repos[0], manifestMetaDatas[descriptor.Digest]) { + for tag, manifestDescriptor := range repos[0].Tags { + if !filter(repos[0], manifestMetaDatas[manifestDescriptor.Digest]) { delete(matchedTags, tag) - delete(manifestMetaDatas, descriptor.Digest) + delete(manifestMetaDatas, manifestDescriptor.Digest) continue } @@ -737,7 +806,7 @@ func TestImageListForDigest(t *testing.T) { repos[0].Tags = matchedTags - return repos, manifestMetaDatas, repodb.PageInfo{}, nil + return repos, manifestMetaDatas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -773,7 +842,7 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -802,10 +871,10 @@ func TestImageListForDigest(t *testing.T) { } matchedTags := repos[0].Tags - for tag, descriptor := range repos[0].Tags { - if !filter(repos[0], manifestMetaDatas[descriptor.Digest]) { + for tag, manifestDescriptor := range repos[0].Tags { + if !filter(repos[0], manifestMetaDatas[manifestDescriptor.Digest]) { delete(matchedTags, tag) - delete(manifestMetaDatas, descriptor.Digest) + delete(manifestMetaDatas, manifestDescriptor.Digest) continue } @@ -813,7 +882,7 @@ func TestImageListForDigest(t *testing.T) { repos[0].Tags = matchedTags - return repos, manifestMetaDatas, repodb.PageInfo{}, nil + return repos, manifestMetaDatas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -847,7 +916,7 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -870,10 +939,10 @@ func TestImageListForDigest(t *testing.T) { for i, repo := range repos { matchedTags := repo.Tags - for tag, descriptor := range repo.Tags { - if !filter(repo, manifestMetaDatas[descriptor.Digest]) { + for tag, manifestDescriptor := range repo.Tags { + if !filter(repo, manifestMetaDatas[manifestDescriptor.Digest]) { delete(matchedTags, tag) - delete(manifestMetaDatas, descriptor.Digest) + delete(manifestMetaDatas, manifestDescriptor.Digest) continue } @@ -882,7 +951,7 @@ func TestImageListForDigest(t *testing.T) { repos[i].Tags = matchedTags } - return repos, manifestMetaDatas, repodb.PageInfo{}, nil + return repos, manifestMetaDatas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -916,10 +985,14 @@ func TestImageListForDigest(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, + error, + ) { + pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, + requestedPage.SortBy) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, err } repos := []repodb.RepoMetadata{ @@ -944,10 +1017,10 @@ func TestImageListForDigest(t *testing.T) { for i, repo := range repos { matchedTags := repo.Tags - for tag, descriptor := range repo.Tags { - if !filter(repo, manifestMetaDatas[descriptor.Digest]) { + for tag, manifestDescriptor := range repo.Tags { + if !filter(repo, manifestMetaDatas[manifestDescriptor.Digest]) { delete(matchedTags, tag) - delete(manifestMetaDatas, descriptor.Digest) + delete(manifestMetaDatas, manifestDescriptor.Digest) continue } @@ -962,7 +1035,7 @@ func TestImageListForDigest(t *testing.T) { repos, _ = pageFinder.Page() - return repos, manifestMetaDatas, repodb.PageInfo{}, nil + return repos, manifestMetaDatas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -986,6 +1059,208 @@ func TestImageListForDigest(t *testing.T) { }) } +func TestGetImageSummary(t *testing.T) { + Convey("GetImageSummary", t, func() { + responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, + graphql.DefaultRecover) + + Convey("Media Type: ImageManifest", func() { + Convey("repoDB.GetManifestMeta fails", func() { + var ( + repoDB = mocks.RepoDBMock{ + GetManifestDataFn: func(manifestDigest godigest.Digest) (repodb.ManifestData, error) { + return repodb.ManifestData{}, ErrTestError + }, + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Tags: map[string]repodb.Descriptor{ + "tag": {MediaType: ispec.MediaTypeImageManifest, Digest: "digest"}, + }, + }, nil + }, + } + + log = log.NewLogger("debug", "") + ) + + _, err := getImageSummary(responseContext, "repo", "tag", nil, repoDB, mocks.CveInfoMock{}, log) + So(err, ShouldNotBeNil) + }) + + Convey("0 len return", func() { + var ( + repoDB = mocks.RepoDBMock{ + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Tags: map[string]repodb.Descriptor{ + "tag": {MediaType: ispec.MediaTypeImageManifest, Digest: "digest"}, + }, + }, nil + }, + } + + log = log.NewLogger("debug", "") + ) + + _, err := getImageSummary(responseContext, "repo", "tag", nil, repoDB, mocks.CveInfoMock{}, log) + So(err, ShouldBeNil) + }) + + Convey("digest != nil && *digest != actual image digest", func() { + var ( + repoDB = mocks.RepoDBMock{ + GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + return repodb.ManifestMetadata{}, ErrTestError + }, + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Tags: map[string]repodb.Descriptor{ + "tag": {MediaType: ispec.MediaTypeImageManifest, Digest: "digest"}, + }, + }, nil + }, + } + + log = log.NewLogger("debug", "") + + digest = "wrongDigest" + ) + + _, err := getImageSummary(responseContext, "repo", "tag", &digest, repoDB, mocks.CveInfoMock{}, log) + So(err, ShouldNotBeNil) + }) + }) + + Convey("Media Type: ImageIndex", func() { + Convey("repoDB.GetIndexData fails", func() { + var ( + repoDB = mocks.RepoDBMock{ + GetIndexDataFn: func(indexDigest godigest.Digest) (repodb.IndexData, error) { + return repodb.IndexData{}, ErrTestError + }, + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Tags: map[string]repodb.Descriptor{ + "tag": {MediaType: ispec.MediaTypeImageIndex, Digest: "digest"}, + }, + }, nil + }, + } + + log = log.NewLogger("debug", "") + ) + + _, err := getImageSummary(responseContext, "repo", "tag", nil, repoDB, mocks.CveInfoMock{}, log) + So(err, ShouldNotBeNil) + }) + + Convey("json.Unmarshal(indexData.IndexBlob, &indexContent) fails", func() { + var ( + repoDB = mocks.RepoDBMock{ + GetIndexDataFn: func(indexDigest godigest.Digest) (repodb.IndexData, error) { + return repodb.IndexData{ + IndexBlob: []byte("bad json"), + }, nil + }, + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Tags: map[string]repodb.Descriptor{ + "tag": {MediaType: ispec.MediaTypeImageIndex, Digest: "digest"}, + }, + }, nil + }, + } + + log = log.NewLogger("debug", "") + ) + + _, err := getImageSummary(responseContext, "repo", "tag", nil, repoDB, mocks.CveInfoMock{}, log) + So(err, ShouldNotBeNil) + }) + + Convey("digest != nil", func() { + index := ispec.Index{ + Manifests: []ispec.Descriptor{ + { + Digest: "digest", + MediaType: ispec.MediaTypeImageManifest, + }, + }, + } + + indexBlob, err := json.Marshal(index) + So(err, ShouldBeNil) + + repoDB := mocks.RepoDBMock{ + GetIndexDataFn: func(indexDigest godigest.Digest) (repodb.IndexData, error) { + return repodb.IndexData{ + IndexBlob: indexBlob, + }, nil + }, + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Tags: map[string]repodb.Descriptor{ + "tag": {MediaType: ispec.MediaTypeImageIndex, Digest: "digest"}, + }, + }, nil + }, + } + + log := log.NewLogger("debug", "") + + goodDigest := "goodDigest" + + Convey("digest not found", func() { + wrongDigest := "wrongDigest" + _, err = getImageSummary(responseContext, "repo", "tag", &wrongDigest, repoDB, mocks.CveInfoMock{}, log) + So(err, ShouldNotBeNil) + }) + + Convey("GetManifestData error", func() { + repoDB.GetManifestDataFn = func(manifestDigest godigest.Digest) (repodb.ManifestData, error) { + return repodb.ManifestData{}, ErrTestError + } + + _, err = getImageSummary(responseContext, "repo", "tag", &goodDigest, repoDB, mocks.CveInfoMock{}, log) + So(err, ShouldNotBeNil) + }) + }) + }) + + Convey("Media Type: not supported", func() { + var ( + repoDB = mocks.RepoDBMock{ + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Tags: map[string]repodb.Descriptor{ + "tag": {MediaType: "unknown", Digest: "digest"}, + }, + }, nil + }, + } + + log = log.NewLogger("debug", "") + ) + + _, err := getImageSummary(responseContext, "repo", "tag", nil, repoDB, mocks.CveInfoMock{}, log) + So(err, ShouldBeNil) + }) + }) +} + +func TestFilterBaseImagesFn(t *testing.T) { + Convey("FilterBaseImages", t, func() { + filterFunc := filterBaseImages(&gql_generated.ImageSummary{}) + ok := filterFunc( + repodb.RepoMetadata{}, + repodb.ManifestMetadata{ + ManifestBlob: []byte("bad json"), + }, + ) + So(ok, ShouldBeFalse) + }) +} + func TestImageList(t *testing.T) { Convey("getImageList", t, func() { testLogger := log.NewLogger("debug", "") @@ -993,8 +1268,11 @@ func TestImageList(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, + error, + ) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, + map[string]repodb.IndexData{}, repodb.PageInfo{}, ErrTestError }, } @@ -1009,7 +1287,7 @@ func TestImageList(t *testing.T) { mockSearchDB := mocks.RepoDBMock{ FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { repos := []repodb.RepoMetadata{ { Name: "test", @@ -1022,7 +1300,7 @@ func TestImageList(t *testing.T) { Signatures: map[string]repodb.ManifestSignatures{ "digestTag1.0.1": { "cosign": []repodb.SignatureInfo{ - {SignatureManifestDigest: "digestSignature1"}, + {SignatureManifestDigest: "testSignature", LayersInfo: []repodb.LayerInfo{}}, }, }, }, @@ -1053,7 +1331,7 @@ func TestImageList(t *testing.T) { }, } - return repos, manifestMetaDatas, repodb.PageInfo{}, nil + return repos, manifestMetaDatas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } @@ -1272,7 +1550,8 @@ func TestExtractImageDetails(t *testing.T) { func TestQueryResolverErrors(t *testing.T) { Convey("Errors", t, func() { log := log.NewLogger("debug", "") - ctx := context.Background() + ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, + graphql.DefaultRecover) Convey("GlobalSearch error bad requested page", func() { resolverConfig := NewResolver( @@ -1341,10 +1620,12 @@ func TestQueryResolverErrors(t *testing.T) { DefaultStore: mocks.MockedImageStore{}, }, mocks.RepoDBMock{ - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, ErrTestError }, }, mocks.CveInfoMock{}, @@ -1365,10 +1646,12 @@ func TestQueryResolverErrors(t *testing.T) { DefaultStore: mocks.MockedImageStore{}, }, mocks.RepoDBMock{ - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, ErrTestError }, }, mocks.CveInfoMock{}, @@ -1391,8 +1674,10 @@ func TestQueryResolverErrors(t *testing.T) { mocks.RepoDBMock{ SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return nil, nil, repodb.PageInfo{}, ErrTestError + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { + return nil, nil, nil, repodb.PageInfo{}, ErrTestError }, }, mocks.CveInfoMock{}, @@ -1411,10 +1696,12 @@ func TestQueryResolverErrors(t *testing.T) { log, storage.StoreController{}, mocks.RepoDBMock{ - SearchReposFn: func(ctx context.Context, searchText string, - filter repodb.Filter, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return nil, nil, repodb.PageInfo{}, ErrTestError + SearchReposFn: func(ctx context.Context, searchText string, filter repodb.Filter, + requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { + return nil, nil, nil, repodb.PageInfo{}, ErrTestError }, }, mocks.CveInfoMock{}, @@ -1433,10 +1720,12 @@ func TestQueryResolverErrors(t *testing.T) { log, storage.StoreController{}, mocks.RepoDBMock{ - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, ErrTestError }, }, mocks.CveInfoMock{}, @@ -1475,7 +1764,7 @@ func TestQueryResolverErrors(t *testing.T) { resolverConfig, } - _, err := qr.DerivedImageList(ctx, "repo:tag", &gql_generated.PageInput{}) + _, err := qr.DerivedImageList(ctx, "repo:tag", nil, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) @@ -1504,7 +1793,7 @@ func TestQueryResolverErrors(t *testing.T) { resolverConfig, } - _, err := qr.BaseImageList(ctx, "repo:tag", &gql_generated.PageInput{}) + _, err := qr.BaseImageList(ctx, "repo:tag", nil, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) @@ -1527,10 +1816,12 @@ func TestQueryResolverErrors(t *testing.T) { log, storage.StoreController{}, mocks.RepoDBMock{ - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, ErrTestError + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, ErrTestError }, GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { return repodb.RepoMetadata{ @@ -1554,10 +1845,10 @@ func TestQueryResolverErrors(t *testing.T) { resolverConfig, } - _, err = resolver.DerivedImageList(ctx, "repo:tag", &gql_generated.PageInput{}) + _, err = resolver.DerivedImageList(ctx, "repo:tag", nil, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) - _, err = resolver.BaseImageList(ctx, "repo:tag", &gql_generated.PageInput{}) + _, err = resolver.BaseImageList(ctx, "repo:tag", nil, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) @@ -1831,6 +2122,12 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) + dig := godigest.FromString("dig") + repoWithDigestRef := fmt.Sprintf("repo@%s", dig) + + _, err := getCVEListForImage(responseContext, repoWithDigestRef, cveInfo, pageInput, log) + So(err.Error(), ShouldContainSubstring, "reference by digest not supported") + cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") @@ -2276,10 +2573,12 @@ func getPageInput(limit int, offset int) *gql_generated.PageInput { func TestDerivedImageList(t *testing.T) { Convey("RepoDB FilterTags error", t, func() { mockSearchDB := mocks.RepoDBMock{ - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, ErrTestError + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { + return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), + make(map[string]repodb.IndexData), repodb.PageInfo{}, ErrTestError }, GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { return repodb.RepoMetadata{}, ErrTestError @@ -2292,7 +2591,7 @@ func TestDerivedImageList(t *testing.T) { graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} - images, err := derivedImageList(responseContext, "repo1:1.0.1", mockSearchDB, &gql_generated.PageInput{}, + images, err := derivedImageList(responseContext, "repo1:1.0.1", nil, mockSearchDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) So(images.Results, ShouldBeEmpty) @@ -2315,10 +2614,12 @@ func TestDerivedImageList(t *testing.T) { manifestDigest := godigest.FromBytes(manifestBlob) mockSearchDB := mocks.RepoDBMock{ - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, nil + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, nil }, GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { return repodb.RepoMetadata{ @@ -2339,7 +2640,7 @@ func TestDerivedImageList(t *testing.T) { graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} - images, err := derivedImageList(responseContext, "repo1:1.0.1", mockSearchDB, &gql_generated.PageInput{}, + images, err := derivedImageList(responseContext, "repo1:1.0.1", nil, mockSearchDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldBeEmpty) @@ -2457,15 +2758,16 @@ func TestDerivedImageList(t *testing.T) { }, }, nil }, - GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { - return repodb.ManifestMetadata{ + GetManifestDataFn: func(manifestDigest godigest.Digest) (repodb.ManifestData, error) { + return repodb.ManifestData{ ManifestBlob: manifestBlob, ConfigBlob: configBlob, }, nil }, - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) So(err, ShouldBeNil) @@ -2501,7 +2803,7 @@ func TestDerivedImageList(t *testing.T) { } repos, pageInfo := pageFinder.Page() - return repos, manifestMetas, pageInfo, nil + return repos, manifestMetas, map[string]repodb.IndexData{}, pageInfo, nil }, } @@ -2511,7 +2813,7 @@ func TestDerivedImageList(t *testing.T) { mockCve := mocks.CveInfoMock{} Convey("valid derivedImageList, results not affected by pageInput", func() { - images, err := derivedImageList(responseContext, "repo1:1.0.1", mockSearchDB, &gql_generated.PageInput{}, + images, err := derivedImageList(responseContext, "repo1:1.0.1", nil, mockSearchDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldNotBeEmpty) @@ -2528,7 +2830,7 @@ func TestDerivedImageList(t *testing.T) { SortBy: &sortCriteria, } - images, err := derivedImageList(responseContext, "repo1:1.0.1", mockSearchDB, &pageInput, + images, err := derivedImageList(responseContext, "repo1:1.0.1", nil, mockSearchDB, &pageInput, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldNotBeEmpty) @@ -2540,23 +2842,25 @@ func TestDerivedImageList(t *testing.T) { func TestBaseImageList(t *testing.T) { Convey("RepoDB FilterTags error", t, func() { mockSearchDB := mocks.RepoDBMock{ - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, ErrTestError + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, ErrTestError }, GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { return repodb.RepoMetadata{}, ErrTestError }, - GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { - return repodb.ManifestMetadata{}, ErrTestError + GetManifestDataFn: func(manifestDigest godigest.Digest) (repodb.ManifestData, error) { + return repodb.ManifestData{}, ErrTestError }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} - images, err := baseImageList(responseContext, "repo1:1.0.2", mockSearchDB, &gql_generated.PageInput{}, + images, err := baseImageList(responseContext, "repo1:1.0.2", nil, mockSearchDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) So(images.Results, ShouldBeEmpty) @@ -2579,10 +2883,12 @@ func TestBaseImageList(t *testing.T) { manifestDigest := godigest.FromBytes(manifestBlob) mockSearchDB := mocks.RepoDBMock{ - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { - return make([]repodb.RepoMetadata, 0), make(map[string]repodb.ManifestMetadata), repodb.PageInfo{}, nil + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, nil }, GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { return repodb.RepoMetadata{ @@ -2592,8 +2898,8 @@ func TestBaseImageList(t *testing.T) { }, }, nil }, - GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { - return repodb.ManifestMetadata{ + GetManifestDataFn: func(manifestDigest godigest.Digest) (repodb.ManifestData, error) { + return repodb.ManifestData{ ManifestBlob: manifestBlob, ConfigBlob: configBlob, }, nil @@ -2603,7 +2909,7 @@ func TestBaseImageList(t *testing.T) { graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} - images, err := baseImageList(responseContext, "repo1:1.0.2", mockSearchDB, &gql_generated.PageInput{}, + images, err := baseImageList(responseContext, "repo1:1.0.2", nil, mockSearchDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldBeEmpty) @@ -2715,15 +3021,16 @@ func TestBaseImageList(t *testing.T) { }, }, nil }, - GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { - return repodb.ManifestMetadata{ + GetManifestDataFn: func(manifestDigest godigest.Digest) (repodb.ManifestData, error) { + return repodb.ManifestData{ ManifestBlob: derivedManifestBlob, ConfigBlob: configBlob, }, nil }, - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) So(err, ShouldBeNil) @@ -2760,7 +3067,7 @@ func TestBaseImageList(t *testing.T) { repos, pageInfo := pageFinder.Page() - return repos, manifestMetas, pageInfo, nil + return repos, manifestMetas, map[string]repodb.IndexData{}, pageInfo, nil }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, @@ -2769,7 +3076,7 @@ func TestBaseImageList(t *testing.T) { mockCve := mocks.CveInfoMock{} Convey("valid baseImageList, results not affected by pageInput", func() { - images, err := baseImageList(responseContext, "repo1:1.0.2", mockSearchDB, + images, err := baseImageList(responseContext, "repo1:1.0.2", nil, mockSearchDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldNotBeEmpty) @@ -2789,7 +3096,7 @@ func TestBaseImageList(t *testing.T) { SortBy: &sortCriteria, } - images, err := baseImageList(responseContext, "repo1:1.0.2", mockSearchDB, + images, err := baseImageList(responseContext, "repo1:1.0.2", nil, mockSearchDB, &pageInput, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldNotBeEmpty) @@ -2889,15 +3196,16 @@ func TestBaseImageList(t *testing.T) { }, }, nil }, - GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { - return repodb.ManifestMetadata{ + GetManifestDataFn: func(manifestDigest godigest.Digest) (repodb.ManifestData, error) { + return repodb.ManifestData{ ManifestBlob: derivedManifestBlob, ConfigBlob: configBlob, }, nil }, - FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, - requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { + FilterTagsFn: func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, + ) { pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) So(err, ShouldBeNil) @@ -2931,16 +3239,81 @@ func TestBaseImageList(t *testing.T) { }) } - return repos, manifestMetas, repodb.PageInfo{}, nil + return repos, manifestMetas, map[string]repodb.IndexData{}, repodb.PageInfo{}, nil }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} - images, err := baseImageList(responseContext, "repo1:1.0.2", mockSearchDB, &gql_generated.PageInput{}, + images, err := baseImageList(responseContext, "repo1:1.0.2", nil, mockSearchDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldBeEmpty) }) } + +func TestExpandedRepoInfo(t *testing.T) { + Convey("ExpandedRepoInfo Errors", t, func() { + responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, + graphql.DefaultRecover) + + repoDB := mocks.RepoDBMock{ + GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { + return repodb.RepoMetadata{ + Tags: map[string]repodb.Descriptor{ + "tagManifest": { + Digest: "errorDigest", + MediaType: ispec.MediaTypeImageManifest, + }, + "tagIndex": { + Digest: "digestIndex", + MediaType: ispec.MediaTypeImageIndex, + }, + "tagGoodIndexBadManifests": { + Digest: "goodIndexBadManifests", + MediaType: ispec.MediaTypeImageIndex, + }, + }, + }, nil + }, + GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + switch manifestDigest { + case "errorDigest": + return repodb.ManifestMetadata{}, ErrTestError + default: + return repodb.ManifestMetadata{ + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }, nil + } + }, + GetIndexDataFn: func(indexDigest godigest.Digest) (repodb.IndexData, error) { + goodIndexBadManifestsBlob, err := json.Marshal(ispec.Index{ + Manifests: []ispec.Descriptor{ + { + Digest: "errorDigest", + MediaType: ispec.MediaTypeImageManifest, + }, + }, + }) + So(err, ShouldBeNil) + + switch indexDigest { + case "errorIndexDigest": + return repodb.IndexData{}, ErrTestError + case "goodIndexBadManifests": + return repodb.IndexData{ + IndexBlob: goodIndexBadManifestsBlob, + }, nil + default: + return repodb.IndexData{}, nil + } + }, + } + log := log.NewLogger("debug", "") + + _, err := expandedRepoInfo(responseContext, "repo", repoDB, mocks.CveInfoMock{}, log) + So(err, ShouldBeNil) + }) +} diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 802a778d..28357ea2 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -124,53 +124,33 @@ type ImageSummary { """ Tag: String """ - Digest of the manifest file associated with this image + List of manifests for all supported versions of the image for different operating systems and architectures """ - Digest: String + Manifests: [ManifestSummary] """ - Digest of the config file associated with this image - """ - ConfigDigest: String - """ - Timestamp of the last modification done to the image (from config or the last updated layer) - """ - LastUpdated: Time - """ - True if the image has a signature associated with it, false otherwise - """ - IsSigned: Boolean - """ - Total size of the files associated with this image (manigest, config, layers) + Total size of the files associated with all images (manifest, config, layers) """ Size: String """ - OS and architecture supported by this image - """ - Platform: OsArch - """ - Vendor associated with this image, the distributing entity, organization or individual - """ - Vendor: String - """ - Integer used to rank search results by relevance - """ - Score: Int - """ Number of downloads of the manifest of this image """ DownloadCount: Int """ - Information on the layers of this image + Timestamp of the last modification done to the image (from config or the last updated layer) """ - Layers: [LayerSummary] + LastUpdated: Time """ Human-readable description of the software packaged in the image """ Description: String """ + True if the image has a signature associated with it, false otherwise + """ + IsSigned: Boolean + """ License(s) under which contained software is distributed as an SPDX License Expression """ - Licenses: String + Licenses: String # The value of the annotation if present, 'unknown' otherwise). """ Labels associated with this image NOTE: currently this field is unused @@ -181,6 +161,10 @@ type ImageSummary { """ Title: String """ + Integer used to rank search results by relevance + """ + Score: Int + """ URL to get source code for building the image """ Source: String @@ -189,6 +173,52 @@ type ImageSummary { """ Documentation: String """ + Vendor associated with this image, the distributing entity, organization or individual + """ + Vendor: String + """ + Contact details of the people or organization responsible for the image + """ + Authors: String + """ + Short summary of the identified CVEs + """ + Vulnerabilities: ImageVulnerabilitySummary +} +""" +Details about a specific version of an image for a certain operating system and architecture. +""" +type ManifestSummary { + """ + Digest of the manifest file associated with this image + """ + Digest: String + """ + Digest of the config file associated with this image + """ + ConfigDigest: String + """ + Timestamp of the last update to an image inside this repository + """ + LastUpdated: Time + """ + Total size of the files associated with this manifest (manifest, config, layers) + """ + Size: String + """ + OS and architecture supported by this image + """ + Platform: Platform + """ + Total numer of image manifest downloads from this repository + """ + DownloadCount: Int + """ + List of layers matching the search criteria + NOTE: the actual search logic for layers is not implemented at the moment + """ + Layers: [LayerSummary] + """ Information about the history of the specific image, see LayerHistory """ History: [LayerHistory] @@ -196,10 +226,6 @@ type ImageSummary { Short summary of the identified CVEs """ Vulnerabilities: ImageVulnerabilitySummary - """ - Contact details of the people or organization responsible for the image - """ - Authors: String } """ @@ -235,7 +261,7 @@ type RepoSummary { """ List of platforms supported by this repository """ - Platforms: [OsArch] + Platforms: [Platform] """ Vendors associated with this image, the distributing entities, organizations or individuals """ @@ -371,7 +397,7 @@ type Referrer { """ Contains details about the OS and architecture of the image """ -type OsArch { +type Platform { """ The name of the operating system which the image is built to run on, Should be values listed in the Go Language document https://go.dev/doc/install/source#environment @@ -606,6 +632,8 @@ type Query { DerivedImageList( "Image name in the format `repository:tag`" image: String!, + "Digest of a specific manifest inside the image. When null whole image is considered" + digest: String, "Sets the parameters of the requested page" requestedPage: PageInput ): PaginatedImagesResult! @@ -616,6 +644,8 @@ type Query { BaseImageList( "Image name in the format `repository:tag`" image: String!, + "Digest of a specific manifest inside the image. When null whole image is considered" + digest: String, "Sets the parameters of the requested page" requestedPage: PageInput ): PaginatedImagesResult! diff --git a/pkg/extensions/search/schema.resolvers.go b/pkg/extensions/search/schema.resolvers.go index 1791fc34..919ab804 100644 --- a/pkg/extensions/search/schema.resolvers.go +++ b/pkg/extensions/search/schema.resolvers.go @@ -104,15 +104,15 @@ func (r *queryResolver) GlobalSearch(ctx context.Context, query string, filter * } // DependencyListForImage is the resolver for the DependencyListForImage field. -func (r *queryResolver) DerivedImageList(ctx context.Context, image string, requestedPage *gql_generated.PageInput) (*gql_generated.PaginatedImagesResult, error) { - derivedList, err := derivedImageList(ctx, image, r.repoDB, requestedPage, r.cveInfo, r.log) +func (r *queryResolver) DerivedImageList(ctx context.Context, image string, digest *string, requestedPage *gql_generated.PageInput) (*gql_generated.PaginatedImagesResult, error) { + derivedList, err := derivedImageList(ctx, image, digest, r.repoDB, requestedPage, r.cveInfo, r.log) return derivedList, err } // BaseImageList is the resolver for the BaseImageList field. -func (r *queryResolver) BaseImageList(ctx context.Context, image string, requestedPage *gql_generated.PageInput) (*gql_generated.PaginatedImagesResult, error) { - imageList, err := baseImageList(ctx, image, r.repoDB, requestedPage, r.cveInfo, r.log) +func (r *queryResolver) BaseImageList(ctx context.Context, image string, digest *string, requestedPage *gql_generated.PageInput) (*gql_generated.PaginatedImagesResult, error) { + imageList, err := baseImageList(ctx, image, digest, r.repoDB, requestedPage, r.cveInfo, r.log) return imageList, err } @@ -125,7 +125,7 @@ func (r *queryResolver) Image(ctx context.Context, image string) (*gql_generated return &gql_generated.ImageSummary{}, gqlerror.Errorf("no reference provided") } - return getImageSummary(ctx, repo, tag, r.repoDB, r.cveInfo, r.log) + return getImageSummary(ctx, repo, tag, nil, r.repoDB, r.cveInfo, r.log) } // Referrers is the resolver for the Referrers field. diff --git a/pkg/extensions/sync/sync_test.go b/pkg/extensions/sync/sync_test.go index d3971717..5c8a3b4f 100644 --- a/pkg/extensions/sync/sync_test.go +++ b/pkg/extensions/sync/sync_test.go @@ -4624,10 +4624,10 @@ func TestSyncImageIndex(t *testing.T) { err = test.UploadImage( test.Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: manifestDigest.String(), + Manifest: manifest, + Config: config, + Layers: layers, + Reference: manifestDigest.String(), }, srcBaseURL, "index") diff --git a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go index 820725bb..1274ec00 100644 --- a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go +++ b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go @@ -56,6 +56,11 @@ func NewBoltDBWrapper(params DBParameters) (*DBWrapper, error) { return err } + _, err = transaction.CreateBucketIfNotExists([]byte(repodb.IndexDataBucket)) + if err != nil { + return err + } + _, err = transaction.CreateBucketIfNotExists([]byte(repodb.RepoMetadataBucket)) if err != nil { return err @@ -209,6 +214,51 @@ func (bdw DBWrapper) GetManifestMeta(repo string, manifestDigest godigest.Digest return manifestMetadata, err } +func (bdw DBWrapper) SetIndexData(indexDigest godigest.Digest, indexMetadata repodb.IndexData) error { + // we make the assumption that the oci layout is consistent and all manifests refferenced inside the + // index are present + err := bdw.DB.Update(func(tx *bolt.Tx) error { + buck := tx.Bucket([]byte(repodb.IndexDataBucket)) + + imBlob, err := json.Marshal(indexMetadata) + if err != nil { + return errors.Wrapf(err, "repodb: error while calculating blob for manifest with digest %s", indexDigest) + } + + err = buck.Put([]byte(indexDigest), imBlob) + if err != nil { + return errors.Wrapf(err, "repodb: error while setting manifest meta with for digest %s", indexDigest) + } + + return nil + }) + + return err +} + +func (bdw DBWrapper) GetIndexData(indexDigest godigest.Digest) (repodb.IndexData, error) { + var indexMetadata repodb.IndexData + + err := bdw.DB.View(func(tx *bolt.Tx) error { + buck := tx.Bucket([]byte(repodb.IndexDataBucket)) + + mmBlob := buck.Get([]byte(indexDigest)) + + if len(mmBlob) == 0 { + return zerr.ErrManifestMetaNotFound + } + + err := json.Unmarshal(mmBlob, &indexMetadata) + if err != nil { + return errors.Wrapf(err, "repodb: error while unmashaling manifest meta for digest %s", indexDigest) + } + + return nil + }) + + return indexMetadata, err +} + func (bdw DBWrapper) SetRepoTag(repo string, tag string, manifestDigest godigest.Digest, mediaType string, ) error { @@ -622,24 +672,30 @@ func (bdw DBWrapper) DeleteSignature(repo string, signedManifestDigest godigest. func (bdw DBWrapper) SearchRepos(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, + error, +) { var ( foundRepos = make([]repodb.RepoMetadata, 0) foundManifestMetadataMap = make(map[string]repodb.ManifestMetadata) + foundindexDataMap = make(map[string]repodb.IndexData) pageFinder repodb.PageFinder pageInfo repodb.PageInfo ) pageFinder, err := repodb.NewBaseRepoPageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, err } - err = bdw.DB.View(func(tx *bolt.Tx) error { + err = bdw.DB.View(func(transaction *bolt.Tx) error { var ( manifestMetadataMap = make(map[string]repodb.ManifestMetadata) - repoBuck = tx.Bucket([]byte(repodb.RepoMetadataBucket)) - dataBuck = tx.Bucket([]byte(repodb.ManifestDataBucket)) + indexDataMap = make(map[string]repodb.IndexData) + repoBuck = transaction.Bucket([]byte(repodb.RepoMetadataBucket)) + indexBuck = transaction.Bucket([]byte(repodb.IndexDataBucket)) + manifestBuck = transaction.Bucket([]byte(repodb.ManifestDataBucket)) ) cursor := repoBuck.Cursor() @@ -667,47 +723,87 @@ func (bdw DBWrapper) SearchRepos(ctx context.Context, searchText string, filter isSigned = false ) - for _, descriptor := range repoMeta.Tags { - var manifestMeta repodb.ManifestMetadata + for tag, descriptor := range repoMeta.Tags { + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + manifestDigest := descriptor.Digest - manifestMeta, manifestDownloaded := manifestMetadataMap[descriptor.Digest] - - if !manifestDownloaded { - manifestMetaBlob := dataBuck.Get([]byte(descriptor.Digest)) - if manifestMetaBlob == nil { - return zerr.ErrManifestMetaNotFound - } - - err := json.Unmarshal(manifestMetaBlob, &manifestMeta) + manifestMeta, err := fetchManifestMetaWithCheck(repoMeta, manifestDigest, + manifestMetadataMap, manifestBuck) if err != nil { - return errors.Wrapf(err, "repodb: error while unmarshaling manifest metadata for digest %s", descriptor.Digest) + return errors.Wrapf(err, "repodb: error fetching manifest meta for manifest with digest %s", + manifestDigest) } + + manifestFilterData, err := collectImageManifestFilterData(manifestDigest, repoMeta, manifestMeta) + if err != nil { + return errors.Wrapf(err, "repodb: error collecting filter data for manifest with digest %s", + manifestDigest) + } + + repoDownloads += manifestFilterData.DownloadCount + + for _, os := range manifestFilterData.OsList { + osSet[os] = true + } + for _, arch := range manifestFilterData.ArchList { + archSet[arch] = true + } + + if firstImageChecked || repoLastUpdated.Before(manifestFilterData.LastUpdated) { + repoLastUpdated = manifestFilterData.LastUpdated + firstImageChecked = false + + isSigned = manifestFilterData.IsSigned + } + + manifestMetadataMap[descriptor.Digest] = manifestMeta + case ispec.MediaTypeImageIndex: + var indexLastUpdated time.Time + + indexDigest := descriptor.Digest + + indexData, err := fetchIndexDataWithCheck(indexDigest, indexDataMap, indexBuck) + if err != nil { + return errors.Wrapf(err, "repodb: error fetching index data for index with digest %s", + indexDigest) + } + + var indexContent ispec.Index + + err = json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return errors.Wrapf(err, "repodb: error while unmashaling index content for %s:%s", repoName, tag) + } + + // this also updates manifestMetadataMap + imageFilterData, err := collectImageIndexFilterInfo(indexDigest, repoMeta, indexData, manifestMetadataMap, + manifestBuck) + if err != nil { + return errors.Wrapf(err, "repodb: error collecting filter data for index with digest %s", + indexDigest) + } + + for _, arch := range imageFilterData.ArchList { + archSet[arch] = true + } + + for _, os := range imageFilterData.OsList { + osSet[os] = true + } + + repoDownloads += imageFilterData.DownloadCount + + if repoLastUpdated.Before(imageFilterData.LastUpdated) { + repoLastUpdated = indexLastUpdated + + isSigned = imageFilterData.IsSigned + } + + indexDataMap[indexDigest] = indexData + default: + bdw.Log.Error().Msgf("Unsupported type: %s", descriptor.MediaType) } - - // get fields related to filtering - var configContent ispec.Image - - err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent) - if err != nil { - return errors.Wrapf(err, "repodb: error while unmarshaling config content for digest %s", descriptor.Digest) - } - - osSet[configContent.OS] = true - archSet[configContent.Architecture] = true - - // get fields related to sorting - repoDownloads += repoMeta.Statistics[descriptor.Digest].DownloadCount - - imageLastUpdated := common.GetImageLastUpdatedTimestamp(configContent) - - if firstImageChecked || repoLastUpdated.Before(imageLastUpdated) { - repoLastUpdated = imageLastUpdated - firstImageChecked = false - - isSigned = common.CheckIsSigned(repoMeta.Signatures[descriptor.Digest]) - } - - manifestMetadataMap[descriptor.Digest] = manifestMeta } repoFilterData := repodb.FilterData{ @@ -731,40 +827,226 @@ func (bdw DBWrapper) SearchRepos(ctx context.Context, searchText string, filter foundRepos, pageInfo = pageFinder.Page() - // keep just the manifestMeta we need + // keep just the manifestMeta and indexData we need for _, repoMeta := range foundRepos { - for _, manifestDigest := range repoMeta.Tags { - foundManifestMetadataMap[manifestDigest.Digest] = manifestMetadataMap[manifestDigest.Digest] + for _, descriptor := range repoMeta.Tags { + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + foundManifestMetadataMap[descriptor.Digest] = manifestMetadataMap[descriptor.Digest] + case ispec.MediaTypeImageIndex: + indexData := indexDataMap[descriptor.Digest] + + var indexContent ispec.Index + + err := json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return err + } + + for _, manifestDescriptor := range indexContent.Manifests { + manifestDigest := manifestDescriptor.Digest.String() + + foundManifestMetadataMap[manifestDigest] = manifestMetadataMap[manifestDigest] + } + + foundindexDataMap[descriptor.Digest] = indexData + default: + bdw.Log.Error().Msgf("Unsupported type: %s", descriptor.MediaType) + } } } return nil }) - return foundRepos, foundManifestMetadataMap, pageInfo, err + return foundRepos, foundManifestMetadataMap, foundindexDataMap, pageInfo, err +} + +func fetchManifestMetaWithCheck(repoMeta repodb.RepoMetadata, manifestDigest string, + manifestMetadataMap map[string]repodb.ManifestMetadata, manifestBuck *bolt.Bucket, +) (repodb.ManifestMetadata, error) { + manifestMeta, manifestDownloaded := manifestMetadataMap[manifestDigest] + + if !manifestDownloaded { + var manifestData repodb.ManifestData + + manifestDataBlob := manifestBuck.Get([]byte(manifestDigest)) + if manifestDataBlob == nil { + return repodb.ManifestMetadata{}, zerr.ErrManifestMetaNotFound + } + + err := json.Unmarshal(manifestDataBlob, &manifestData) + if err != nil { + return repodb.ManifestMetadata{}, errors.Wrapf(err, + "repodb: error while unmarshaling manifest metadata for digest %s", manifestDigest) + } + + manifestMeta = NewManifestMetadata(manifestDigest, repoMeta, manifestData) + } + + return manifestMeta, nil +} + +func fetchIndexDataWithCheck(indexDigest string, indexDataMap map[string]repodb.IndexData, + indexBuck *bolt.Bucket, +) (repodb.IndexData, error) { + var ( + indexData repodb.IndexData + err error + ) + + indexData, indexExists := indexDataMap[indexDigest] + + if !indexExists { + indexDataBlob := indexBuck.Get([]byte(indexDigest)) + if indexDataBlob == nil { + return repodb.IndexData{}, zerr.ErrIndexDataNotFount + } + + err := json.Unmarshal(indexDataBlob, &indexData) + if err != nil { + return repodb.IndexData{}, + errors.Wrapf(err, "repodb: error while unmashaling index data for digest %s", indexDigest) + } + } + + return indexData, err +} + +func collectImageManifestFilterData(digest string, repoMeta repodb.RepoMetadata, + manifestMeta repodb.ManifestMetadata, +) (repodb.FilterData, error) { + // get fields related to filtering + var ( + configContent ispec.Image + osList []string + archList []string + ) + + err := json.Unmarshal(manifestMeta.ConfigBlob, &configContent) + if err != nil { + return repodb.FilterData{}, + errors.Wrapf(err, "repodb: error while unmarshaling config content") + } + + if configContent.OS != "" { + osList = append(osList, configContent.OS) + } + + if configContent.Architecture != "" { + archList = append(archList, configContent.Architecture) + } + + return repodb.FilterData{ + DownloadCount: repoMeta.Statistics[digest].DownloadCount, + OsList: osList, + ArchList: archList, + LastUpdated: common.GetImageLastUpdatedTimestamp(configContent), + IsSigned: common.CheckIsSigned(repoMeta.Signatures[digest]), + }, nil +} + +func collectImageIndexFilterInfo(indexDigest string, repoMeta repodb.RepoMetadata, + indexData repodb.IndexData, manifestMetadataMap map[string]repodb.ManifestMetadata, + manifestBuck *bolt.Bucket, +) (repodb.FilterData, error) { + var indexContent ispec.Index + + err := json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return repodb.FilterData{}, + errors.Wrapf(err, "repodb: error while unmarshaling index content for digest %s", indexDigest) + } + + var ( + indexLastUpdated time.Time + firstManifestChecked = false + indexOsList = []string{} + indexArchList = []string{} + ) + + for _, manifest := range indexContent.Manifests { + manifestDigest := manifest.Digest + + manifestMeta, err := fetchManifestMetaWithCheck(repoMeta, manifestDigest.String(), + manifestMetadataMap, manifestBuck) + if err != nil { + return repodb.FilterData{}, + errors.Wrapf(err, "") + } + + manifestFilterData, err := collectImageManifestFilterData(manifestDigest.String(), repoMeta, + manifestMeta) + if err != nil { + return repodb.FilterData{}, + errors.Wrapf(err, "") + } + + indexOsList = append(indexOsList, manifestFilterData.OsList...) + indexArchList = append(indexArchList, manifestFilterData.ArchList...) + + if !firstManifestChecked || indexLastUpdated.Before(manifestFilterData.LastUpdated) { + indexLastUpdated = manifestFilterData.LastUpdated + firstManifestChecked = true + } + + manifestMetadataMap[manifest.Digest.String()] = manifestMeta + } + + return repodb.FilterData{ + DownloadCount: repoMeta.Statistics[indexDigest].DownloadCount, + LastUpdated: indexLastUpdated, + OsList: indexOsList, + ArchList: indexArchList, + IsSigned: common.CheckIsSigned(repoMeta.Signatures[indexDigest]), + }, nil +} + +func NewManifestMetadata(manifestDigest string, repoMeta repodb.RepoMetadata, + manifestData repodb.ManifestData, +) repodb.ManifestMetadata { + manifestMeta := repodb.ManifestMetadata{ + ManifestBlob: manifestData.ManifestBlob, + ConfigBlob: manifestData.ConfigBlob, + } + + manifestMeta.DownloadCount = repoMeta.Statistics[manifestDigest].DownloadCount + + manifestMeta.Signatures = repodb.ManifestSignatures{} + if repoMeta.Signatures[manifestDigest] != nil { + manifestMeta.Signatures = repoMeta.Signatures[manifestDigest] + } + + return manifestMeta } func (bdw DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, + repodb.PageInfo, error, +) { var ( foundRepos = make([]repodb.RepoMetadata, 0) + manifestMetadataMap = make(map[string]repodb.ManifestMetadata) + indexDataMap = make(map[string]repodb.IndexData) foundManifestMetadataMap = make(map[string]repodb.ManifestMetadata) + foundindexDataMap = make(map[string]repodb.IndexData) pageFinder repodb.PageFinder pageInfo repodb.PageInfo ) pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, err } err = bdw.DB.View(func(tx *bolt.Tx) error { var ( - manifestMetadataMap = make(map[string]repodb.ManifestMetadata) - repoBuck = tx.Bucket([]byte(repodb.RepoMetadataBucket)) - dataBuck = tx.Bucket([]byte(repodb.ManifestDataBucket)) - cursor = repoBuck.Cursor() + repoBuck = tx.Bucket([]byte(repodb.RepoMetadataBucket)) + indexBuck = tx.Bucket([]byte(repodb.IndexDataBucket)) + manifestBuck = tx.Bucket([]byte(repodb.ManifestDataBucket)) + cursor = repoBuck.Cursor() ) repoName, repoMetaBlob := cursor.First() @@ -784,46 +1066,69 @@ func (bdw DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, matchedTags := make(map[string]repodb.Descriptor) // take all manifestMetas for tag, descriptor := range repoMeta.Tags { - manifestDigest := descriptor.Digest - matchedTags[tag] = descriptor + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + manifestDigest := descriptor.Digest - // in case tags reference the same manifest we don't download from DB multiple times - manifestMeta, manifestExists := manifestMetadataMap[manifestDigest] - - if !manifestExists { - manifestDataBlob := dataBuck.Get([]byte(manifestDigest)) - if manifestDataBlob == nil { - return zerr.ErrManifestMetaNotFound - } - - var manifestData repodb.ManifestData - - err := json.Unmarshal(manifestDataBlob, &manifestData) + manifestMeta, err := fetchManifestMetaWithCheck(repoMeta, manifestDigest, manifestMetadataMap, manifestBuck) if err != nil { return errors.Wrapf(err, "repodb: error while unmashaling manifest metadata for digest %s", manifestDigest) } - var configContent ispec.Image + if !filter(repoMeta, manifestMeta) { + delete(matchedTags, tag) - err = json.Unmarshal(manifestData.ConfigBlob, &configContent) + continue + } + + manifestMetadataMap[manifestDigest] = manifestMeta + case ispec.MediaTypeImageIndex: + indexDigest := descriptor.Digest + + indexData, err := fetchIndexDataWithCheck(indexDigest, indexDataMap, indexBuck) if err != nil { - return errors.Wrapf(err, "repodb: error while unmashaling config for manifest with digest %s", manifestDigest) + return errors.Wrapf(err, "repodb: error while getting index data for digest %s", indexDigest) } - manifestMeta = repodb.ManifestMetadata{ - ConfigBlob: manifestData.ConfigBlob, - ManifestBlob: manifestData.ManifestBlob, + var indexContent ispec.Index + + err = json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return errors.Wrapf(err, "repodb: error while unmashaling index content for digest %s", indexDigest) } + + manifestHasBeenMatched := false + + for _, manifest := range indexContent.Manifests { + manifestDigest := manifest.Digest.String() + + manifestMeta, err := fetchManifestMetaWithCheck(repoMeta, manifestDigest, manifestMetadataMap, manifestBuck) + if err != nil { + return errors.Wrapf(err, "repodb: error while getting manifest data for digest %s", manifestDigest) + } + + manifestMetadataMap[manifestDigest] = manifestMeta + + if filter(repoMeta, manifestMeta) { + manifestHasBeenMatched = true + } + } + + if !manifestHasBeenMatched { + delete(matchedTags, tag) + + for _, manifest := range indexContent.Manifests { + delete(manifestMetadataMap, manifest.Digest.String()) + } + + continue + } + + indexDataMap[indexDigest] = indexData + default: + bdw.Log.Error().Msgf("Unsupported type: %s", descriptor.MediaType) } - - if !filter(repoMeta, manifestMeta) { - delete(matchedTags, tag) - - continue - } - - manifestMetadataMap[manifestDigest] = manifestMeta } if len(matchedTags) == 0 { @@ -839,25 +1144,50 @@ func (bdw DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, foundRepos, pageInfo = pageFinder.Page() - // keep just the manifestMeta we need + // keep just the manifestMeta and indexData we need for _, repoMeta := range foundRepos { for _, descriptor := range repoMeta.Tags { - foundManifestMetadataMap[descriptor.Digest] = manifestMetadataMap[descriptor.Digest] + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + foundManifestMetadataMap[descriptor.Digest] = manifestMetadataMap[descriptor.Digest] + case ispec.MediaTypeImageIndex: + indexData := indexDataMap[descriptor.Digest] + + var indexContent ispec.Index + + err := json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return err + } + + for _, manifestDescriptor := range indexContent.Manifests { + manifestDigest := manifestDescriptor.Digest.String() + + foundManifestMetadataMap[manifestDigest] = manifestMetadataMap[manifestDigest] + } + + foundindexDataMap[descriptor.Digest] = indexData + default: + bdw.Log.Error().Msgf("Unsupported type: %s", descriptor.MediaType) + } } } return nil }) - return foundRepos, foundManifestMetadataMap, pageInfo, err + return foundRepos, foundManifestMetadataMap, foundindexDataMap, pageInfo, err } func (bdw DBWrapper) SearchTags(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { var ( foundRepos = make([]repodb.RepoMetadata, 0) + manifestMetadataMap = make(map[string]repodb.ManifestMetadata) + indexDataMap = make(map[string]repodb.IndexData) foundManifestMetadataMap = make(map[string]repodb.ManifestMetadata) + foundindexDataMap = make(map[string]repodb.IndexData) pageInfo repodb.PageInfo pageFinder repodb.PageFinder @@ -865,21 +1195,23 @@ func (bdw DBWrapper) SearchTags(ctx context.Context, searchText string, filter r pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, err } searchedRepo, searchedTag, err := common.GetRepoTag(searchText) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + repodb.PageInfo{}, errors.Wrap(err, "repodb: error while parsing search text, invalid format") } err = bdw.DB.View(func(tx *bolt.Tx) error { var ( - manifestMetadataMap = make(map[string]repodb.ManifestMetadata) - repoBuck = tx.Bucket([]byte(repodb.RepoMetadataBucket)) - dataBuck = tx.Bucket([]byte(repodb.ManifestDataBucket)) - cursor = repoBuck.Cursor() + repoBuck = tx.Bucket([]byte(repodb.RepoMetadataBucket)) + indexBuck = tx.Bucket([]byte(repodb.IndexDataBucket)) + manifestBuck = tx.Bucket([]byte(repodb.ManifestDataBucket)) + cursor = repoBuck.Cursor() ) repoName, repoMetaBlob := cursor.Seek([]byte(searchedRepo)) @@ -906,46 +1238,84 @@ func (bdw DBWrapper) SearchTags(ctx context.Context, searchText string, filter r matchedTags[tag] = descriptor - // in case tags reference the same manifest we don't download from DB multiple times - if manifestMeta, manifestExists := manifestMetadataMap[descriptor.Digest]; manifestExists { + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + manifestDigest := descriptor.Digest + + manifestMeta, err := fetchManifestMetaWithCheck(repoMeta, manifestDigest, manifestMetadataMap, manifestBuck) + if err != nil { + return errors.Wrapf(err, "repodb: error fetching manifest meta for manifest with digest %s", + manifestDigest) + } + + imageFilterData, err := collectImageManifestFilterData(manifestDigest, repoMeta, manifestMeta) + if err != nil { + return errors.Wrapf(err, "repodb: error collecting filter data for manifest with digest %s", + manifestDigest) + } + + if !common.AcceptedByFilter(filter, imageFilterData) { + delete(matchedTags, tag) + + continue + } + manifestMetadataMap[descriptor.Digest] = manifestMeta + case ispec.MediaTypeImageIndex: + indexDigest := descriptor.Digest - continue + indexData, err := fetchIndexDataWithCheck(indexDigest, indexDataMap, indexBuck) + if err != nil { + return errors.Wrapf(err, "repodb: error fetching index data for index with digest %s", + indexDigest) + } + + var indexContent ispec.Index + + err = json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return errors.Wrapf(err, "repodb: error collecting filter data for index with digest %s", + indexDigest) + } + + manifestHasBeenMatched := false + + for _, manifest := range indexContent.Manifests { + manifestDigest := manifest.Digest.String() + + manifestMeta, err := fetchManifestMetaWithCheck(repoMeta, manifestDigest, manifestMetadataMap, manifestBuck) + if err != nil { + return errors.Wrapf(err, "repodb: error fetching from db manifest meta for manifest with digest %s", + manifestDigest) + } + + manifestFilterData, err := collectImageManifestFilterData(manifestDigest, repoMeta, manifestMeta) + if err != nil { + return errors.Wrapf(err, "repodb: error collecting filter data for manifest with digest %s", + manifestDigest) + } + + manifestMetadataMap[manifestDigest] = manifestMeta + + if common.AcceptedByFilter(filter, manifestFilterData) { + manifestHasBeenMatched = true + } + } + + if !manifestHasBeenMatched { + delete(matchedTags, tag) + + for _, manifest := range indexContent.Manifests { + delete(manifestMetadataMap, manifest.Digest.String()) + } + + continue + } + + indexDataMap[indexDigest] = indexData + default: + bdw.Log.Error().Msgf("Unsupported type: %s", descriptor.MediaType) } - - manifestMetaBlob := dataBuck.Get([]byte(descriptor.Digest)) - if manifestMetaBlob == nil { - return zerr.ErrManifestMetaNotFound - } - - var manifestMeta repodb.ManifestMetadata - - err := json.Unmarshal(manifestMetaBlob, &manifestMeta) - if err != nil { - return errors.Wrapf(err, "repodb: error while unmashaling manifest metadata for digest %s", descriptor.Digest) - } - - var configContent ispec.Image - - err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent) - if err != nil { - return errors.Wrapf(err, "repodb: error while unmashaling config for manifest with digest %s", descriptor.Digest) - } - - imageFilterData := repodb.FilterData{ - OsList: []string{configContent.OS}, - ArchList: []string{configContent.Architecture}, - IsSigned: false, - } - - if !common.AcceptedByFilter(filter, imageFilterData) { - delete(matchedTags, tag) - delete(manifestMetadataMap, descriptor.Digest) - - continue - } - - manifestMetadataMap[descriptor.Digest] = manifestMeta } if len(matchedTags) == 0 { @@ -962,17 +1332,39 @@ func (bdw DBWrapper) SearchTags(ctx context.Context, searchText string, filter r foundRepos, pageInfo = pageFinder.Page() - // keep just the manifestMeta we need + // keep just the manifestMeta and indexData we need for _, repoMeta := range foundRepos { for _, descriptor := range repoMeta.Tags { - foundManifestMetadataMap[descriptor.Digest] = manifestMetadataMap[descriptor.Digest] + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + foundManifestMetadataMap[descriptor.Digest] = manifestMetadataMap[descriptor.Digest] + case ispec.MediaTypeImageIndex: + indexData := indexDataMap[descriptor.Digest] + + var indexContent ispec.Index + + err := json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return err + } + + for _, manifestDescriptor := range indexContent.Manifests { + manifestDigest := manifestDescriptor.Digest.String() + + foundManifestMetadataMap[manifestDigest] = manifestMetadataMap[manifestDigest] + } + + foundindexDataMap[descriptor.Digest] = indexData + default: + bdw.Log.Error().Msgf("Unsupported type: %s", descriptor.MediaType) + } } } return nil }) - return foundRepos, foundManifestMetadataMap, pageInfo, err + return foundRepos, foundManifestMetadataMap, foundindexDataMap, pageInfo, err } func (bdw *DBWrapper) PatchDB() error { diff --git a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go index 9b4448be..24211eec 100644 --- a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go +++ b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper_test.go @@ -13,10 +13,12 @@ import ( "zotregistry.io/zot/pkg/meta/repodb" bolt "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper" + "zotregistry.io/zot/pkg/test" ) func TestWrapperErrors(t *testing.T) { Convey("Errors", t, func() { + ctx := context.Background() tmpDir := t.TempDir() boltDBParams := bolt.DBParameters{RootDir: tmpDir} boltdbWrapper, err := bolt.NewBoltDBWrapper(boltDBParams) @@ -300,7 +302,7 @@ func TestWrapperErrors(t *testing.T) { }) So(err, ShouldBeNil) - _, _, _, err = boltdbWrapper.SearchRepos(context.Background(), "", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = boltdbWrapper.SearchRepos(context.Background(), "", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error { @@ -339,10 +341,10 @@ func TestWrapperErrors(t *testing.T) { }) So(err, ShouldBeNil) - _, _, _, err = boltdbWrapper.SearchRepos(context.Background(), "repo1", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = boltdbWrapper.SearchRepos(context.Background(), "repo1", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) - _, _, _, err = boltdbWrapper.SearchRepos(context.Background(), "repo2", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = boltdbWrapper.SearchRepos(context.Background(), "repo2", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error { @@ -378,10 +380,85 @@ func TestWrapperErrors(t *testing.T) { }) So(err, ShouldBeNil) - _, _, _, err = boltdbWrapper.SearchRepos(context.Background(), "repo1", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = boltdbWrapper.SearchRepos(context.Background(), "repo1", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) }) + Convey("Index Errors", func() { + Convey("Bad index data", func() { + indexDigest := digest.FromString("indexDigest") + + err := boltdbWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + err = setBadIndexData(boltdbWrapper.DB, indexDigest.String()) + So(err, ShouldBeNil) + + _, _, _, _, err = boltdbWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + + _, _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + }) + + Convey("Bad indexBlob in IndexData", func() { + indexDigest := digest.FromString("indexDigest") + + err := boltdbWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + err = boltdbWrapper.SetIndexData(indexDigest, repodb.IndexData{ + IndexBlob: []byte("bad json"), + }) + So(err, ShouldBeNil) + + _, _, _, _, err = boltdbWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + + _, _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + }) + + Convey("Good index data, bad manifest inside index", func() { + var ( + indexDigest = digest.FromString("indexDigest") + manifestDigestFromIndex1 = digest.FromString("manifestDigestFromIndex1") + manifestDigestFromIndex2 = digest.FromString("manifestDigestFromIndex2") + ) + + err := boltdbWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + indexBlob, err := test.GetIndexBlobWithManifests([]digest.Digest{ + manifestDigestFromIndex1, manifestDigestFromIndex2, + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.SetIndexData(indexDigest, repodb.IndexData{ + IndexBlob: indexBlob, + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.SetManifestData(manifestDigestFromIndex1, repodb.ManifestData{ + ManifestBlob: []byte("Bad Manifest"), + ConfigBlob: []byte("Bad Manifest"), + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.SetManifestData(manifestDigestFromIndex2, repodb.ManifestData{ + ManifestBlob: []byte("Bad Manifest"), + ConfigBlob: []byte("Bad Manifest"), + }) + So(err, ShouldBeNil) + + _, _, _, _, err = boltdbWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + + _, _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + }) + }) + Convey("SearchTags", func() { ctx := context.Background() @@ -392,10 +469,10 @@ func TestWrapperErrors(t *testing.T) { }) So(err, ShouldBeNil) - _, _, _, err = boltdbWrapper.SearchTags(ctx, "", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = boltdbWrapper.SearchTags(ctx, "", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) - _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo1:", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo1:", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) err = boltdbWrapper.DB.Update(func(tx *bbolt.Tx) error { @@ -466,14 +543,117 @@ func TestWrapperErrors(t *testing.T) { }) So(err, ShouldBeNil) - _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo1:", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo1:", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) - _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo2:", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo2:", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) - _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo3:", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo3:", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) }) + + Convey("FilterTags Index errors", func() { + Convey("FilterTags bad IndexData", func() { + indexDigest := digest.FromString("indexDigest") + + err := boltdbWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + err = setBadIndexData(boltdbWrapper.DB, indexDigest.String()) + So(err, ShouldBeNil) + + _, _, _, _, err = boltdbWrapper.FilterTags(ctx, + func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true }, + repodb.PageInput{}, + ) + So(err, ShouldNotBeNil) + }) + + Convey("FilterTags bad indexBlob in IndexData", func() { + indexDigest := digest.FromString("indexDigest") + + err := boltdbWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + err = boltdbWrapper.SetIndexData(indexDigest, repodb.IndexData{ + IndexBlob: []byte("bad json"), + }) + So(err, ShouldBeNil) + + _, _, _, _, err = boltdbWrapper.FilterTags(ctx, + func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true }, + repodb.PageInput{}, + ) + So(err, ShouldNotBeNil) + }) + + Convey("FilterTags didn't match any index manifest", func() { + var ( + indexDigest = digest.FromString("indexDigest") + manifestDigestFromIndex1 = digest.FromString("manifestDigestFromIndex1") + manifestDigestFromIndex2 = digest.FromString("manifestDigestFromIndex2") + ) + + err := boltdbWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + indexBlob, err := test.GetIndexBlobWithManifests([]digest.Digest{ + manifestDigestFromIndex1, manifestDigestFromIndex2, + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.SetIndexData(indexDigest, repodb.IndexData{ + IndexBlob: indexBlob, + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.SetManifestData(manifestDigestFromIndex1, repodb.ManifestData{ + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + err = boltdbWrapper.SetManifestData(manifestDigestFromIndex2, repodb.ManifestData{ + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + _, _, _, _, err = boltdbWrapper.FilterTags(ctx, + func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return false }, + repodb.PageInput{}, + ) + So(err, ShouldBeNil) + }) + }) + + Convey("Unsuported type", func() { + digest := digest.FromString("digest") + + err := boltdbWrapper.SetRepoTag("repo", "tag1", digest, "invalid type") //nolint:contextcheck + So(err, ShouldBeNil) + + _, _, _, _, err = boltdbWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldBeNil) + + _, _, _, _, err = boltdbWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldBeNil) + + _, _, _, _, err = boltdbWrapper.FilterTags( + ctx, + func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true }, + repodb.PageInput{}, + ) + So(err, ShouldBeNil) + }) + }) +} + +func setBadIndexData(dB *bbolt.DB, digest string) error { + return dB.Update(func(tx *bbolt.Tx) error { + indexDataBuck := tx.Bucket([]byte(repodb.IndexDataBucket)) + + return indexDataBuck.Put([]byte(digest), []byte("bad json")) }) } diff --git a/pkg/meta/repodb/dynamodb-wrapper/dynamo_internal_test.go b/pkg/meta/repodb/dynamodb-wrapper/dynamo_internal_test.go index 4f4cc44b..e3a933da 100644 --- a/pkg/meta/repodb/dynamodb-wrapper/dynamo_internal_test.go +++ b/pkg/meta/repodb/dynamodb-wrapper/dynamo_internal_test.go @@ -64,6 +64,9 @@ func TestWrapperErrors(t *testing.T) { err = dynamoWrapper.createManifestDataTable() So(err, ShouldNotBeNil) + err = dynamoWrapper.createIndexDataTable() + So(err, ShouldNotBeNil) + err = dynamoWrapper.createVersionTable() So(err, ShouldNotBeNil) }) diff --git a/pkg/meta/repodb/dynamodb-wrapper/dynamo_test.go b/pkg/meta/repodb/dynamodb-wrapper/dynamo_test.go index a30e793b..6be628e0 100644 --- a/pkg/meta/repodb/dynamodb-wrapper/dynamo_test.go +++ b/pkg/meta/repodb/dynamodb-wrapper/dynamo_test.go @@ -12,6 +12,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" guuid "github.com/gofrs/uuid" + "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/rs/zerolog" . "github.com/smartystreets/goconvey/convey" @@ -20,6 +22,7 @@ import ( dynamo "zotregistry.io/zot/pkg/meta/repodb/dynamodb-wrapper" "zotregistry.io/zot/pkg/meta/repodb/dynamodb-wrapper/iterator" dynamoParams "zotregistry.io/zot/pkg/meta/repodb/dynamodb-wrapper/params" + "zotregistry.io/zot/pkg/test" ) func TestIterator(t *testing.T) { @@ -36,6 +39,7 @@ func TestIterator(t *testing.T) { repoMetaTablename := "RepoMetadataTable" + uuid.String() manifestDataTablename := "ManifestDataTable" + uuid.String() versionTablename := "Version" + uuid.String() + indexDataTablename := "IndexDataTable" + uuid.String() Convey("TestIterator", t, func() { dynamoWrapper, err := dynamo.NewDynamoDBWrapper(dynamoParams.DBDriverParameters{ @@ -43,6 +47,7 @@ func TestIterator(t *testing.T) { Region: region, RepoMetaTablename: repoMetaTablename, ManifestDataTablename: manifestDataTablename, + IndexDataTablename: indexDataTablename, VersionTablename: versionTablename, }) So(err, ShouldBeNil) @@ -127,6 +132,7 @@ func TestWrapperErrors(t *testing.T) { repoMetaTablename := "RepoMetadataTable" + uuid.String() manifestDataTablename := "ManifestDataTable" + uuid.String() versionTablename := "Version" + uuid.String() + indexDataTablename := "IndexDataTable" + uuid.String() ctx := context.Background() @@ -136,6 +142,7 @@ func TestWrapperErrors(t *testing.T) { Region: region, RepoMetaTablename: repoMetaTablename, ManifestDataTablename: manifestDataTablename, + IndexDataTablename: indexDataTablename, VersionTablename: versionTablename, }) So(err, ShouldBeNil) @@ -144,7 +151,7 @@ func TestWrapperErrors(t *testing.T) { So(dynamoWrapper.ResetRepoMetaTable(), ShouldBeNil) //nolint:contextcheck Convey("SetManifestData", func() { - dynamoWrapper.ManifestDataTablename = "WRONG table" + dynamoWrapper.ManifestDataTablename = "WRONG tables" err := dynamoWrapper.SetManifestData("dig", repodb.ManifestData{}) So(err, ShouldNotBeNil) @@ -165,6 +172,21 @@ func TestWrapperErrors(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("GetIndexData", func() { + dynamoWrapper.IndexDataTablename = "WRONG table" + + _, err := dynamoWrapper.GetIndexData("dig") + So(err, ShouldNotBeNil) + }) + + Convey("GetIndexData unmarshal error", func() { + err := setBadIndexData(dynamoWrapper.Client, indexDataTablename, "dig") + So(err, ShouldBeNil) + + _, err = dynamoWrapper.GetManifestData("dig") + So(err, ShouldNotBeNil) + }) + Convey("SetManifestMeta GetRepoMeta error", func() { err := setBadRepoMeta(dynamoWrapper.Client, repoMetaTablename, "repo1") So(err, ShouldBeNil) @@ -255,14 +277,6 @@ func TestWrapperErrors(t *testing.T) { So(err, ShouldNotBeNil) }) - Convey("IncrementImageDownloads GetManifestMeta error", func() { - err := dynamoWrapper.SetRepoTag("repo", "tag", "dig", "") - So(err, ShouldBeNil) - - err = dynamoWrapper.IncrementImageDownloads("repo", "tag") - So(err, ShouldNotBeNil) - }) - Convey("AddManifestSignature GetRepoMeta error", func() { err := dynamoWrapper.SetRepoTag("repo", "tag", "dig", "") So(err, ShouldBeNil) @@ -329,22 +343,22 @@ func TestWrapperErrors(t *testing.T) { err = setBadRepoMeta(dynamoWrapper.Client, repoMetaTablename, "repo") //nolint:contextcheck So(err, ShouldBeNil) - _, _, _, err = dynamoWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = dynamoWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) }) Convey("SearchRepos GetManifestMeta error", func() { - err := dynamoWrapper.SetRepoTag("repo", "tag1", "notFoundDigest", "") //nolint:contextcheck + err := dynamoWrapper.SetRepoTag("repo", "tag1", "notFoundDigest", ispec.MediaTypeImageManifest) //nolint:contextcheck So(err, ShouldBeNil) - _, _, _, err = dynamoWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = dynamoWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) }) Convey("SearchRepos config unmarshal error", func() { - err := dynamoWrapper.SetRepoTag("repo", "tag1", "dig1", "") //nolint:contextcheck + err := dynamoWrapper.SetRepoTag("repo", "tag1", "dig1", ispec.MediaTypeImageManifest) //nolint:contextcheck So(err, ShouldBeNil) err = dynamoWrapper.SetManifestData("dig1", repodb.ManifestData{ //nolint:contextcheck @@ -353,31 +367,116 @@ func TestWrapperErrors(t *testing.T) { }) So(err, ShouldBeNil) - _, _, _, err = dynamoWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = dynamoWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) }) + Convey("Unsuported type", func() { + digest := digest.FromString("digest") + + err := dynamoWrapper.SetRepoTag("repo", "tag1", digest, "invalid type") //nolint:contextcheck + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.FilterTags( + ctx, + func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true }, + repodb.PageInput{}, + ) + So(err, ShouldBeNil) + }) + + Convey("SearchRepos bad index data", func() { + indexDigest := digest.FromString("indexDigest") + + err := dynamoWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + err = setBadIndexData(dynamoWrapper.Client, indexDataTablename, indexDigest.String()) //nolint:contextcheck + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + }) + + Convey("SearchRepos bad indexBlob in IndexData", func() { + indexDigest := digest.FromString("indexDigest") + + err := dynamoWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + err = dynamoWrapper.SetIndexData(indexDigest, repodb.IndexData{ //nolint:contextcheck + IndexBlob: []byte("bad json"), + }) + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + }) + + Convey("SearchRepos good index data, bad manifest inside index", func() { + var ( + indexDigest = digest.FromString("indexDigest") + manifestDigestFromIndex1 = digest.FromString("manifestDigestFromIndex1") + manifestDigestFromIndex2 = digest.FromString("manifestDigestFromIndex2") + ) + + err := dynamoWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + indexBlob, err := test.GetIndexBlobWithManifests([]digest.Digest{ + manifestDigestFromIndex1, manifestDigestFromIndex2, + }) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetIndexData(indexDigest, repodb.IndexData{ //nolint:contextcheck + IndexBlob: indexBlob, + }) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetManifestData(manifestDigestFromIndex1, repodb.ManifestData{ //nolint:contextcheck + ManifestBlob: []byte("Bad Manifest"), + ConfigBlob: []byte("Bad Manifest"), + }) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetManifestData(manifestDigestFromIndex2, repodb.ManifestData{ //nolint:contextcheck + ManifestBlob: []byte("Bad Manifest"), + ConfigBlob: []byte("Bad Manifest"), + }) + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + }) + Convey("SearchTags repoMeta unmarshal error", func() { err = setBadRepoMeta(dynamoWrapper.Client, repoMetaTablename, "repo") //nolint:contextcheck So(err, ShouldBeNil) - _, _, _, err = dynamoWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = dynamoWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) }) Convey("SearchTags GetManifestMeta error", func() { - err := dynamoWrapper.SetRepoTag("repo", "tag1", "manifestNotFound", "") //nolint:contextcheck + err := dynamoWrapper.SetRepoTag("repo", "tag1", "manifestNotFound", //nolint:contextcheck + ispec.MediaTypeImageManifest) So(err, ShouldBeNil) - _, _, _, err = dynamoWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = dynamoWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) }) Convey("SearchTags config unmarshal error", func() { - err := dynamoWrapper.SetRepoTag("repo", "tag1", "dig1", "") //nolint:contextcheck + err := dynamoWrapper.SetRepoTag("repo", "tag1", "dig1", ispec.MediaTypeImageManifest) //nolint:contextcheck So(err, ShouldBeNil) err = dynamoWrapper.SetManifestData( //nolint:contextcheck @@ -389,16 +488,80 @@ func TestWrapperErrors(t *testing.T) { ) So(err, ShouldBeNil) - _, _, _, err = dynamoWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + _, _, _, _, err = dynamoWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) }) + Convey("SearchTags bad index data", func() { + indexDigest := digest.FromString("indexDigest") + + err := dynamoWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + err = setBadIndexData(dynamoWrapper.Client, indexDataTablename, indexDigest.String()) //nolint:contextcheck + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + }) + + Convey("SearchTags bad indexBlob in IndexData", func() { + indexDigest := digest.FromString("indexDigest") + + err := dynamoWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + err = dynamoWrapper.SetIndexData(indexDigest, repodb.IndexData{ //nolint:contextcheck + IndexBlob: []byte("bad json"), + }) + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + }) + + Convey("SearchTags good index data, bad manifest inside index", func() { + var ( + indexDigest = digest.FromString("indexDigest") + manifestDigestFromIndex1 = digest.FromString("manifestDigestFromIndex1") + manifestDigestFromIndex2 = digest.FromString("manifestDigestFromIndex2") + ) + + err := dynamoWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + indexBlob, err := test.GetIndexBlobWithManifests([]digest.Digest{ + manifestDigestFromIndex1, manifestDigestFromIndex2, + }) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetIndexData(indexDigest, repodb.IndexData{ //nolint:contextcheck + IndexBlob: indexBlob, + }) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetManifestData(manifestDigestFromIndex1, repodb.ManifestData{ //nolint:contextcheck + ManifestBlob: []byte("Bad Manifest"), + ConfigBlob: []byte("Bad Manifest"), + }) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetManifestData(manifestDigestFromIndex2, repodb.ManifestData{ //nolint:contextcheck + ManifestBlob: []byte("Bad Manifest"), + ConfigBlob: []byte("Bad Manifest"), + }) + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.SearchTags(ctx, "repo:", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldNotBeNil) + }) + Convey("FilterTags repoMeta unmarshal error", func() { err = setBadRepoMeta(dynamoWrapper.Client, repoMetaTablename, "repo") //nolint:contextcheck So(err, ShouldBeNil) - _, _, _, err = dynamoWrapper.FilterTags( + _, _, _, _, err = dynamoWrapper.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -410,10 +573,11 @@ func TestWrapperErrors(t *testing.T) { }) Convey("FilterTags manifestMeta not found", func() { - err := dynamoWrapper.SetRepoTag("repo", "tag1", "manifestNotFound", "") //nolint:contextcheck + err := dynamoWrapper.SetRepoTag("repo", "tag1", "manifestNotFound", //nolint:contextcheck + ispec.MediaTypeImageManifest) So(err, ShouldBeNil) - _, _, _, err = dynamoWrapper.FilterTags( + _, _, _, _, err = dynamoWrapper.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -425,13 +589,13 @@ func TestWrapperErrors(t *testing.T) { }) Convey("FilterTags manifestMeta unmarshal error", func() { - err := dynamoWrapper.SetRepoTag("repo", "tag1", "dig", "") //nolint:contextcheck + err := dynamoWrapper.SetRepoTag("repo", "tag1", "dig", ispec.MediaTypeImageManifest) //nolint:contextcheck So(err, ShouldBeNil) err = setBadManifestData(dynamoWrapper.Client, manifestDataTablename, "dig") //nolint:contextcheck So(err, ShouldBeNil) - _, _, _, err = dynamoWrapper.FilterTags( + _, _, _, _, err = dynamoWrapper.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -442,26 +606,130 @@ func TestWrapperErrors(t *testing.T) { So(err, ShouldNotBeNil) }) - Convey("FilterTags config unmarshal error", func() { - err := dynamoWrapper.SetRepoTag("repo", "tag1", "dig1", "") //nolint:contextcheck + Convey("FilterTags bad IndexData", func() { + indexDigest := digest.FromString("indexDigest") + + err := dynamoWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck So(err, ShouldBeNil) - err = dynamoWrapper.SetManifestData("dig1", repodb.ManifestData{ //nolint:contextcheck - ManifestBlob: []byte("{}"), - ConfigBlob: []byte("bad json"), + err = setBadIndexData(dynamoWrapper.Client, indexDataTablename, indexDigest.String()) //nolint:contextcheck + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.FilterTags(ctx, + func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true }, + repodb.PageInput{}, + ) + So(err, ShouldNotBeNil) + }) + + Convey("FilterTags bad indexBlob in IndexData", func() { + indexDigest := digest.FromString("indexDigest") + + err := dynamoWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + err = dynamoWrapper.SetIndexData(indexDigest, repodb.IndexData{ //nolint:contextcheck + IndexBlob: []byte("bad json"), }) So(err, ShouldBeNil) - _, _, _, err = dynamoWrapper.FilterTags( - ctx, - func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { - return true - }, + _, _, _, _, err = dynamoWrapper.FilterTags(ctx, + func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true }, repodb.PageInput{}, ) - So(err, ShouldNotBeNil) }) + + Convey("FilterTags didn't match any index manifest", func() { + var ( + indexDigest = digest.FromString("indexDigest") + manifestDigestFromIndex1 = digest.FromString("manifestDigestFromIndex1") + manifestDigestFromIndex2 = digest.FromString("manifestDigestFromIndex2") + ) + + err := dynamoWrapper.SetRepoTag("repo", "tag1", indexDigest, ispec.MediaTypeImageIndex) //nolint:contextcheck + So(err, ShouldBeNil) + + indexBlob, err := test.GetIndexBlobWithManifests([]digest.Digest{ + manifestDigestFromIndex1, manifestDigestFromIndex2, + }) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetIndexData(indexDigest, repodb.IndexData{ //nolint:contextcheck + IndexBlob: indexBlob, + }) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetManifestData(manifestDigestFromIndex1, repodb.ManifestData{ //nolint:contextcheck + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + err = dynamoWrapper.SetManifestData(manifestDigestFromIndex2, repodb.ManifestData{ //nolint:contextcheck + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + _, _, _, _, err = dynamoWrapper.FilterTags(ctx, + func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return false }, + repodb.PageInput{}, + ) + So(err, ShouldBeNil) + }) + }) + + Convey("NewDynamoDBWrapper errors", t, func() { + _, err := dynamo.NewDynamoDBWrapper(dynamoParams.DBDriverParameters{ //nolint:contextcheck + Endpoint: endpoint, + Region: region, + RepoMetaTablename: "", + ManifestDataTablename: manifestDataTablename, + IndexDataTablename: indexDataTablename, + VersionTablename: versionTablename, + }) + So(err, ShouldNotBeNil) + + _, err = dynamo.NewDynamoDBWrapper(dynamoParams.DBDriverParameters{ //nolint:contextcheck + Endpoint: endpoint, + Region: region, + RepoMetaTablename: repoMetaTablename, + ManifestDataTablename: "", + IndexDataTablename: indexDataTablename, + VersionTablename: versionTablename, + }) + So(err, ShouldNotBeNil) + + _, err = dynamo.NewDynamoDBWrapper(dynamoParams.DBDriverParameters{ //nolint:contextcheck + Endpoint: endpoint, + Region: region, + RepoMetaTablename: repoMetaTablename, + ManifestDataTablename: manifestDataTablename, + IndexDataTablename: "", + VersionTablename: versionTablename, + }) + So(err, ShouldNotBeNil) + + _, err = dynamo.NewDynamoDBWrapper(dynamoParams.DBDriverParameters{ //nolint:contextcheck + Endpoint: endpoint, + Region: region, + RepoMetaTablename: repoMetaTablename, + ManifestDataTablename: manifestDataTablename, + IndexDataTablename: indexDataTablename, + VersionTablename: "", + }) + So(err, ShouldNotBeNil) + + _, err = dynamo.NewDynamoDBWrapper(dynamoParams.DBDriverParameters{ //nolint:contextcheck + Endpoint: endpoint, + Region: region, + RepoMetaTablename: repoMetaTablename, + ManifestDataTablename: manifestDataTablename, + IndexDataTablename: indexDataTablename, + VersionTablename: versionTablename, + }) + So(err, ShouldBeNil) }) } @@ -490,6 +758,31 @@ func setBadManifestData(client *dynamodb.Client, manifestDataTableName, digest s return err } +func setBadIndexData(client *dynamodb.Client, indexDataTableName, digest string) error { + mdAttributeValue, err := attributevalue.Marshal("string") + if err != nil { + return err + } + + _, err = client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{ + ExpressionAttributeNames: map[string]string{ + "#ID": "IndexData", + }, + ExpressionAttributeValues: map[string]types.AttributeValue{ + ":IndexData": mdAttributeValue, + }, + Key: map[string]types.AttributeValue{ + "IndexDigest": &types.AttributeValueMemberS{ + Value: digest, + }, + }, + TableName: aws.String(indexDataTableName), + UpdateExpression: aws.String("SET #ID = :IndexData"), + }) + + return err +} + func setBadRepoMeta(client *dynamodb.Client, repoMetadataTableName, repoName string) error { repoAttributeValue, err := attributevalue.Marshal("string") if err != nil { diff --git a/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go b/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go index beb7a6a3..c95c92d9 100644 --- a/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go +++ b/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go @@ -30,6 +30,7 @@ import ( type DBWrapper struct { Client *dynamodb.Client RepoMetaTablename string + IndexDataTablename string ManifestDataTablename string VersionTablename string Patches []func(client *dynamodb.Client, tableNames map[string]string) error @@ -60,6 +61,7 @@ func NewDynamoDBWrapper(params dynamoParams.DBDriverParameters) (*DBWrapper, err Client: dynamodb.NewFromConfig(cfg), RepoMetaTablename: params.RepoMetaTablename, ManifestDataTablename: params.ManifestDataTablename, + IndexDataTablename: params.IndexDataTablename, VersionTablename: params.VersionTablename, Patches: version.GetDynamoDBPatches(), Log: log.Logger{Logger: zerolog.New(os.Stdout)}, @@ -80,6 +82,11 @@ func NewDynamoDBWrapper(params dynamoParams.DBDriverParameters) (*DBWrapper, err return nil, err } + err = dynamoWrapper.createIndexDataTable() + if err != nil { + return nil, err + } + // Using the Config value, create the DynamoDB client return &dynamoWrapper, nil } @@ -248,6 +255,58 @@ func (dwr DBWrapper) GetRepoStars(repo string) (int, error) { return repoMeta.Stars, nil } +func (dwr DBWrapper) SetIndexData(indexDigest godigest.Digest, indexData repodb.IndexData) error { + indexAttributeValue, err := attributevalue.Marshal(indexData) + if err != nil { + return err + } + + _, err = dwr.Client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{ + ExpressionAttributeNames: map[string]string{ + "#ID": "IndexData", + }, + ExpressionAttributeValues: map[string]types.AttributeValue{ + ":IndexData": indexAttributeValue, + }, + Key: map[string]types.AttributeValue{ + "IndexDigest": &types.AttributeValueMemberS{ + Value: indexDigest.String(), + }, + }, + TableName: aws.String(dwr.IndexDataTablename), + UpdateExpression: aws.String("SET #ID = :IndexData"), + }) + + return err +} + +func (dwr DBWrapper) GetIndexData(indexDigest godigest.Digest) (repodb.IndexData, error) { + resp, err := dwr.Client.GetItem(context.TODO(), &dynamodb.GetItemInput{ + TableName: aws.String(dwr.IndexDataTablename), + Key: map[string]types.AttributeValue{ + "IndexDigest": &types.AttributeValueMemberS{ + Value: indexDigest.String(), + }, + }, + }) + if err != nil { + return repodb.IndexData{}, err + } + + if resp.Item == nil { + return repodb.IndexData{}, zerr.ErrRepoMetaNotFound + } + + var indexData repodb.IndexData + + err = attributevalue.Unmarshal(resp.Item["IndexData"], &indexData) + if err != nil { + return repodb.IndexData{}, err + } + + return indexData, nil +} + func (dwr DBWrapper) SetRepoTag(repo string, tag string, manifestDigest godigest.Digest, mediaType string) error { if err := common.ValidateRepoTagInput(repo, tag, manifestDigest); err != nil { return err @@ -377,7 +436,7 @@ func (dwr DBWrapper) IncrementImageDownloads(repo string, reference string) erro return err } - manifestDigest := reference + descriptorDigest := reference if !common.ReferenceIsDigest(reference) { // search digest for tag @@ -387,19 +446,14 @@ func (dwr DBWrapper) IncrementImageDownloads(repo string, reference string) erro return zerr.ErrManifestMetaNotFound } - manifestDigest = descriptor.Digest + descriptorDigest = descriptor.Digest } - manifestMeta, err := dwr.GetManifestMeta(repo, godigest.Digest(manifestDigest)) - if err != nil { - return err - } + manifestStatistics := repoMeta.Statistics[descriptorDigest] + manifestStatistics.DownloadCount++ + repoMeta.Statistics[descriptorDigest] = manifestStatistics - manifestMeta.DownloadCount++ - - err = dwr.SetManifestMeta(repo, godigest.Digest(manifestDigest), manifestMeta) - - return err + return dwr.setRepoMeta(repo, repoMeta) } func (dwr DBWrapper) AddManifestSignature(repo string, signedManifestDigest godigest.Digest, @@ -531,11 +585,10 @@ func (dwr DBWrapper) GetMultipleRepoMeta(ctx context.Context, func (dwr DBWrapper) SearchRepos(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { var ( - foundManifestMetadataMap = make(map[string]repodb.ManifestMetadata) - manifestMetadataMap = make(map[string]repodb.ManifestMetadata) - + manifestMetadataMap = make(map[string]repodb.ManifestMetadata) + indexDataMap = make(map[string]repodb.IndexData) repoMetaAttributeIterator iterator.AttributesIterator pageFinder repodb.PageFinder pageInfo repodb.PageInfo @@ -547,7 +600,8 @@ func (dwr DBWrapper) SearchRepos(ctx context.Context, searchText string, filter pageFinder, err := repodb.NewBaseRepoPageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, err } repoMetaAttribute, err := repoMetaAttributeIterator.First(ctx) @@ -555,14 +609,16 @@ func (dwr DBWrapper) SearchRepos(ctx context.Context, searchText string, filter for ; repoMetaAttribute != nil; repoMetaAttribute, err = repoMetaAttributeIterator.Next(ctx) { if err != nil { // log - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, err } var repoMeta repodb.RepoMetadata err := attributevalue.Unmarshal(repoMetaAttribute, &repoMeta) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, err } if ok, err := localCtx.RepoIsUserAvailable(ctx, repoMeta.Name); !ok || err != nil { @@ -581,43 +637,84 @@ func (dwr DBWrapper) SearchRepos(ctx context.Context, searchText string, filter ) for _, descriptor := range repoMeta.Tags { - var manifestMeta repodb.ManifestMetadata + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + manifestDigest := descriptor.Digest - manifestMeta, manifestDownloaded := manifestMetadataMap[descriptor.Digest] - - if !manifestDownloaded { - manifestMeta, err = dwr.GetManifestMeta(repoMeta.Name, godigest.Digest(descriptor.Digest)) //nolint:contextcheck + manifestMeta, err := dwr.fetchManifestMetaWithCheck(repoMeta.Name, manifestDigest, //nolint:contextcheck + manifestMetadataMap) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, - errors.Wrapf(err, "repodb: error while unmarshaling manifest metadata for digest %s", descriptor.Digest) + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "") } + + manifestFilterData, err := collectImageManifestFilterData(manifestDigest, repoMeta, manifestMeta) + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "") + } + + repoDownloads += manifestFilterData.DownloadCount + + for _, os := range manifestFilterData.OsList { + osSet[os] = true + } + + for _, arch := range manifestFilterData.ArchList { + archSet[arch] = true + } + + if firstImageChecked || repoLastUpdated.Before(manifestFilterData.LastUpdated) { + repoLastUpdated = manifestFilterData.LastUpdated + firstImageChecked = false + + isSigned = manifestFilterData.IsSigned + } + + manifestMetadataMap[descriptor.Digest] = manifestMeta + case ispec.MediaTypeImageIndex: + var indexLastUpdated time.Time + + indexDigest := descriptor.Digest + + indexData, err := dwr.fetchIndexDataWithCheck(indexDigest, indexDataMap) //nolint:contextcheck + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "") + } + + // this also updates manifestMetadataMap + imageFilterData, err := dwr.collectImageIndexFilterInfo(indexDigest, repoMeta, indexData, //nolint:contextcheck + manifestMetadataMap) + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "") + } + + for _, arch := range imageFilterData.ArchList { + archSet[arch] = true + } + + for _, os := range imageFilterData.OsList { + osSet[os] = true + } + + repoDownloads += imageFilterData.DownloadCount + + if repoLastUpdated.Before(imageFilterData.LastUpdated) { + repoLastUpdated = indexLastUpdated + + isSigned = imageFilterData.IsSigned + } + + indexDataMap[indexDigest] = indexData + default: + dwr.Log.Error().Msgf("Unsupported type: %s", descriptor.MediaType) } - - // get fields related to filtering - var configContent ispec.Image - - err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent) - if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, - errors.Wrapf(err, "repodb: error while unmarshaling config content for digest %s", descriptor.Digest) - } - - osSet[configContent.OS] = true - archSet[configContent.Architecture] = true - - // get fields related to sorting - repoDownloads += repoMeta.Statistics[descriptor.Digest].DownloadCount - - imageLastUpdated := common.GetImageLastUpdatedTimestamp(configContent) - - if firstImageChecked || repoLastUpdated.Before(imageLastUpdated) { - repoLastUpdated = imageLastUpdated - firstImageChecked = false - - isSigned = common.CheckIsSigned(manifestMeta.Signatures) - } - - manifestMetadataMap[descriptor.Digest] = manifestMeta } repoFilterData := repodb.FilterData{ @@ -641,22 +738,145 @@ func (dwr DBWrapper) SearchRepos(ctx context.Context, searchText string, filter foundRepos, pageInfo := pageFinder.Page() - // keep just the manifestMeta we need - for _, repoMeta := range foundRepos { - for _, descriptor := range repoMeta.Tags { - foundManifestMetadataMap[descriptor.Digest] = manifestMetadataMap[descriptor.Digest] + foundManifestMetadataMap, foundindexDataMap, err := filterFoundData(foundRepos, manifestMetadataMap, indexDataMap) + + return foundRepos, foundManifestMetadataMap, foundindexDataMap, pageInfo, err +} + +func (dwr DBWrapper) fetchManifestMetaWithCheck(repoName string, manifestDigest string, + manifestMetadataMap map[string]repodb.ManifestMetadata, +) (repodb.ManifestMetadata, error) { + var ( + manifestMeta repodb.ManifestMetadata + err error + ) + + manifestMeta, manifestDownloaded := manifestMetadataMap[manifestDigest] + + if !manifestDownloaded { + manifestMeta, err = dwr.GetManifestMeta(repoName, godigest.Digest(manifestDigest)) //nolint:contextcheck + if err != nil { + return repodb.ManifestMetadata{}, err } } - return foundRepos, foundManifestMetadataMap, pageInfo, err + return manifestMeta, nil +} + +func collectImageManifestFilterData(digest string, repoMeta repodb.RepoMetadata, + manifestMeta repodb.ManifestMetadata, +) (repodb.FilterData, error) { + // get fields related to filtering + var ( + configContent ispec.Image + osList []string + archList []string + ) + + err := json.Unmarshal(manifestMeta.ConfigBlob, &configContent) + if err != nil { + return repodb.FilterData{}, + errors.Wrapf(err, "repodb: error while unmarshaling config content") + } + + if configContent.OS != "" { + osList = append(osList, configContent.OS) + } + + if configContent.Architecture != "" { + archList = append(archList, configContent.Architecture) + } + + return repodb.FilterData{ + DownloadCount: repoMeta.Statistics[digest].DownloadCount, + OsList: osList, + ArchList: archList, + LastUpdated: common.GetImageLastUpdatedTimestamp(configContent), + IsSigned: common.CheckIsSigned(repoMeta.Signatures[digest]), + }, nil +} + +func (dwr DBWrapper) fetchIndexDataWithCheck(indexDigest string, indexDataMap map[string]repodb.IndexData, +) (repodb.IndexData, error) { + var ( + indexData repodb.IndexData + err error + ) + + indexData, indexExists := indexDataMap[indexDigest] + + if !indexExists { + indexData, err = dwr.GetIndexData(godigest.Digest(indexDigest)) //nolint:contextcheck + if err != nil { + return repodb.IndexData{}, + errors.Wrapf(err, "repodb: error while unmarshaling index data for digest %s", indexDigest) + } + } + + return indexData, err +} + +func (dwr DBWrapper) collectImageIndexFilterInfo(indexDigest string, repoMeta repodb.RepoMetadata, + indexData repodb.IndexData, manifestMetadataMap map[string]repodb.ManifestMetadata, +) (repodb.FilterData, error) { + var indexContent ispec.Index + + err := json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return repodb.FilterData{}, + errors.Wrapf(err, "repodb: error while unmarshaling index content for digest %s", indexDigest) + } + + var ( + indexLastUpdated time.Time + firstManifestChecked = false + indexOsList = []string{} + indexArchList = []string{} + ) + + for _, manifest := range indexContent.Manifests { + manifestDigest := manifest.Digest + + manifestMeta, err := dwr.fetchManifestMetaWithCheck(repoMeta.Name, manifestDigest.String(), + manifestMetadataMap) + if err != nil { + return repodb.FilterData{}, + errors.Wrapf(err, "") + } + + manifestFilterData, err := collectImageManifestFilterData(manifestDigest.String(), repoMeta, + manifestMeta) + if err != nil { + return repodb.FilterData{}, + errors.Wrapf(err, "") + } + + indexOsList = append(indexOsList, manifestFilterData.OsList...) + indexArchList = append(indexArchList, manifestFilterData.ArchList...) + + if !firstManifestChecked || indexLastUpdated.Before(manifestFilterData.LastUpdated) { + indexLastUpdated = manifestFilterData.LastUpdated + firstManifestChecked = true + } + + manifestMetadataMap[manifest.Digest.String()] = manifestMeta + } + + return repodb.FilterData{ + DownloadCount: repoMeta.Statistics[indexDigest].DownloadCount, + LastUpdated: indexLastUpdated, + OsList: indexOsList, + ArchList: indexArchList, + IsSigned: common.CheckIsSigned(repoMeta.Signatures[indexDigest]), + }, nil } func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { var ( - foundManifestMetadataMap = make(map[string]repodb.ManifestMetadata) manifestMetadataMap = make(map[string]repodb.ManifestMetadata) + indexDataMap = make(map[string]repodb.IndexData) pageFinder repodb.PageFinder repoMetaAttributeIterator iterator.AttributesIterator pageInfo repodb.PageInfo @@ -668,7 +888,8 @@ func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, err } repoMetaAttribute, err := repoMetaAttributeIterator.First(ctx) @@ -676,14 +897,16 @@ func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, for ; repoMetaAttribute != nil; repoMetaAttribute, err = repoMetaAttributeIterator.Next(ctx) { if err != nil { // log - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, err } var repoMeta repodb.RepoMetadata err := attributevalue.Unmarshal(repoMetaAttribute, &repoMeta) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, err } if ok, err := localCtx.RepoIsUserAvailable(ctx, repoMeta.Name); !ok || err != nil { @@ -692,36 +915,80 @@ func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, matchedTags := make(map[string]repodb.Descriptor) // take all manifestMetas for tag, descriptor := range repoMeta.Tags { - manifestDigest := descriptor.Digest - matchedTags[tag] = descriptor - // in case tags reference the same manifest we don't download from DB multiple times - manifestMeta, manifestExists := manifestMetadataMap[manifestDigest] + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + manifestDigest := descriptor.Digest - if !manifestExists { - manifestMeta, err := dwr.GetManifestMeta(repoMeta.Name, godigest.Digest(manifestDigest)) //nolint:contextcheck + manifestMeta, err := dwr.fetchManifestMetaWithCheck(repoMeta.Name, manifestDigest, //nolint:contextcheck + manifestMetadataMap) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, errors.Wrapf(err, "repodb: error while unmashaling manifest metadata for digest %s", manifestDigest) } - var configContent ispec.Image + if !filter(repoMeta, manifestMeta) { + delete(matchedTags, tag) - err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent) - if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, - errors.Wrapf(err, "repodb: error while unmashaling config for manifest with digest %s", manifestDigest) + continue } + + manifestMetadataMap[manifestDigest] = manifestMeta + case ispec.MediaTypeImageIndex: + indexDigest := descriptor.Digest + + indexData, err := dwr.fetchIndexDataWithCheck(indexDigest, indexDataMap) //nolint:contextcheck + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "repodb: error while getting index data for digest %s", indexDigest) + } + + var indexContent ispec.Index + + err = json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "repodb: error while unmashaling index content for digest %s", indexDigest) + } + + manifestHasBeenMatched := false + + for _, manifest := range indexContent.Manifests { + manifestDigest := manifest.Digest.String() + + manifestMeta, err := dwr.fetchManifestMetaWithCheck(repoMeta.Name, manifestDigest, //nolint:contextcheck + manifestMetadataMap) + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "repodb: error while getting manifest data for digest %s", manifestDigest) + } + + manifestMetadataMap[manifestDigest] = manifestMeta + + if filter(repoMeta, manifestMeta) { + manifestHasBeenMatched = true + } + } + + if !manifestHasBeenMatched { + delete(matchedTags, tag) + + for _, manifest := range indexContent.Manifests { + delete(manifestMetadataMap, manifest.Digest.String()) + } + + continue + } + + indexDataMap[indexDigest] = indexData + default: + dwr.Log.Error().Msgf("Unsupported type: %s", descriptor.MediaType) } - - if !filter(repoMeta, manifestMeta) { - delete(matchedTags, tag) - - continue - } - - manifestMetadataMap[manifestDigest] = manifestMeta } if len(matchedTags) == 0 { @@ -737,22 +1004,17 @@ func (dwr DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, foundRepos, pageInfo := pageFinder.Page() - // keep just the manifestMeta we need - for _, repoMeta := range foundRepos { - for _, descriptor := range repoMeta.Tags { - foundManifestMetadataMap[descriptor.Digest] = manifestMetadataMap[descriptor.Digest] - } - } + foundManifestMetadataMap, foundindexDataMap, err := filterFoundData(foundRepos, manifestMetadataMap, indexDataMap) - return foundRepos, foundManifestMetadataMap, pageInfo, err + return foundRepos, foundManifestMetadataMap, foundindexDataMap, pageInfo, err } func (dwr DBWrapper) SearchTags(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { var ( - foundManifestMetadataMap = make(map[string]repodb.ManifestMetadata) manifestMetadataMap = make(map[string]repodb.ManifestMetadata) + indexDataMap = make(map[string]repodb.IndexData) repoMetaAttributeIterator = iterator.NewBaseDynamoAttributesIterator( dwr.Client, dwr.RepoMetaTablename, "RepoMetadata", 0, dwr.Log, ) @@ -763,12 +1025,14 @@ func (dwr DBWrapper) SearchTags(ctx context.Context, searchText string, filter r pageFinder, err := repodb.NewBaseImagePageFinder(requestedPage.Limit, requestedPage.Offset, requestedPage.SortBy) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, err } searchedRepo, searchedTag, err := common.GetRepoTag(searchText) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, errors.Wrap(err, "repodb: error while parsing search text, invalid format") } @@ -777,14 +1041,16 @@ func (dwr DBWrapper) SearchTags(ctx context.Context, searchText string, filter r for ; repoMetaAttribute != nil; repoMetaAttribute, err = repoMetaAttributeIterator.Next(ctx) { if err != nil { // log - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, err } var repoMeta repodb.RepoMetadata err := attributevalue.Unmarshal(repoMetaAttribute, &repoMeta) if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, err + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, err } if ok, err := localCtx.RepoIsUserAvailable(ctx, repoMeta.Name); !ok || err != nil { @@ -801,41 +1067,92 @@ func (dwr DBWrapper) SearchTags(ctx context.Context, searchText string, filter r matchedTags[tag] = descriptor - // in case tags reference the same manifest we don't download from DB multiple times - if manifestMeta, manifestExists := manifestMetadataMap[descriptor.Digest]; manifestExists { + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + manifestDigest := descriptor.Digest + + manifestMeta, err := dwr.fetchManifestMetaWithCheck(repoMeta.Name, manifestDigest, //nolint:contextcheck + manifestMetadataMap) + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "repodb: error while unmashaling manifest metadata for digest %s", descriptor.Digest) + } + + imageFilterData, err := collectImageManifestFilterData(manifestDigest, repoMeta, manifestMeta) + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "") + } + + if !common.AcceptedByFilter(filter, imageFilterData) { + delete(matchedTags, tag) + + continue + } + manifestMetadataMap[descriptor.Digest] = manifestMeta + case ispec.MediaTypeImageIndex: + indexDigest := descriptor.Digest - continue + indexData, err := dwr.fetchIndexDataWithCheck(indexDigest, indexDataMap) //nolint:contextcheck + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "") + } + + var indexContent ispec.Index + + err = json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "repodb: error while unmashaling index content for digest %s", indexDigest) + } + + manifestHasBeenMatched := false + + for _, manifest := range indexContent.Manifests { + manifestDigest := manifest.Digest.String() + + manifestMeta, err := dwr.fetchManifestMetaWithCheck(repoMeta.Name, manifestDigest, //nolint:contextcheck + manifestMetadataMap) + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "") + } + + manifestFilterData, err := collectImageManifestFilterData(manifestDigest, repoMeta, manifestMeta) + if err != nil { + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + pageInfo, + errors.Wrapf(err, "") + } + + manifestMetadataMap[manifestDigest] = manifestMeta + + if common.AcceptedByFilter(filter, manifestFilterData) { + manifestHasBeenMatched = true + } + } + + if !manifestHasBeenMatched { + delete(matchedTags, tag) + + for _, manifest := range indexContent.Manifests { + delete(manifestMetadataMap, manifest.Digest.String()) + } + + continue + } + + indexDataMap[indexDigest] = indexData + default: + dwr.Log.Error().Msgf("Unsupported type: %s", descriptor.MediaType) } - - manifestMeta, err := dwr.GetManifestMeta(repoMeta.Name, godigest.Digest(descriptor.Digest)) //nolint:contextcheck - if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, - errors.Wrapf(err, "repodb: error while unmashaling manifest metadata for digest %s", descriptor.Digest) - } - - var configContent ispec.Image - - err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent) - if err != nil { - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, pageInfo, - errors.Wrapf(err, "repodb: error while unmashaling config for manifest with digest %s", descriptor.Digest) - } - - imageFilterData := repodb.FilterData{ - OsList: []string{configContent.OS}, - ArchList: []string{configContent.Architecture}, - IsSigned: false, - } - - if !common.AcceptedByFilter(filter, imageFilterData) { - delete(matchedTags, tag) - delete(manifestMetadataMap, descriptor.Digest) - - continue - } - - manifestMetadataMap[descriptor.Digest] = manifestMeta } if len(matchedTags) == 0 { @@ -852,14 +1169,49 @@ func (dwr DBWrapper) SearchTags(ctx context.Context, searchText string, filter r foundRepos, pageInfo := pageFinder.Page() + foundManifestMetadataMap, foundindexDataMap, err := filterFoundData(foundRepos, manifestMetadataMap, indexDataMap) + + return foundRepos, foundManifestMetadataMap, foundindexDataMap, pageInfo, err +} + +func filterFoundData(foundRepos []repodb.RepoMetadata, manifestMetadataMap map[string]repodb.ManifestMetadata, + indexDataMap map[string]repodb.IndexData, +) (map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, error) { + var ( + foundManifestMetadataMap = make(map[string]repodb.ManifestMetadata) + foundindexDataMap = make(map[string]repodb.IndexData) + ) + // keep just the manifestMeta we need for _, repoMeta := range foundRepos { for _, descriptor := range repoMeta.Tags { - foundManifestMetadataMap[descriptor.Digest] = manifestMetadataMap[descriptor.Digest] + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + foundManifestMetadataMap[descriptor.Digest] = manifestMetadataMap[descriptor.Digest] + case ispec.MediaTypeImageIndex: + indexData := indexDataMap[descriptor.Digest] + + var indexContent ispec.Index + + err := json.Unmarshal(indexData.IndexBlob, &indexContent) + if err != nil { + return map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{}, + errors.Wrapf(err, "repodb: error while getting manifest data for digest %s", descriptor.Digest) + } + + for _, manifestDescriptor := range indexContent.Manifests { + manifestDigest := manifestDescriptor.Digest.String() + + foundManifestMetadataMap[manifestDigest] = manifestMetadataMap[manifestDigest] + } + + foundindexDataMap[descriptor.Digest] = indexData + default: + } } } - return foundRepos, foundManifestMetadataMap, pageInfo, err + return foundManifestMetadataMap, foundindexDataMap, nil } func (dwr *DBWrapper) PatchDB() error { @@ -1008,6 +1360,31 @@ func (dwr DBWrapper) createManifestDataTable() error { return dwr.waitTableToBeCreated(dwr.ManifestDataTablename) } +func (dwr DBWrapper) createIndexDataTable() error { + _, err := dwr.Client.CreateTable(context.Background(), &dynamodb.CreateTableInput{ + TableName: aws.String(dwr.IndexDataTablename), + AttributeDefinitions: []types.AttributeDefinition{ + { + AttributeName: aws.String("IndexDigest"), + AttributeType: types.ScalarAttributeTypeS, + }, + }, + KeySchema: []types.KeySchemaElement{ + { + AttributeName: aws.String("IndexDigest"), + KeyType: types.KeyTypeHash, + }, + }, + BillingMode: types.BillingModePayPerRequest, + }) + + if err != nil && strings.Contains(err.Error(), "Table already exists") { + return nil + } + + return dwr.waitTableToBeCreated(dwr.IndexDataTablename) +} + func (dwr *DBWrapper) createVersionTable() error { _, err := dwr.Client.CreateTable(context.Background(), &dynamodb.CreateTableInput{ TableName: aws.String(dwr.VersionTablename), diff --git a/pkg/meta/repodb/dynamodb-wrapper/params/parameters.go b/pkg/meta/repodb/dynamodb-wrapper/params/parameters.go index b1d62266..f5be16d5 100644 --- a/pkg/meta/repodb/dynamodb-wrapper/params/parameters.go +++ b/pkg/meta/repodb/dynamodb-wrapper/params/parameters.go @@ -1,5 +1,6 @@ package params type DBDriverParameters struct { - Endpoint, Region, RepoMetaTablename, ManifestDataTablename, VersionTablename string + Endpoint, Region, RepoMetaTablename, ManifestDataTablename, IndexDataTablename, + VersionTablename string } diff --git a/pkg/meta/repodb/repodb.go b/pkg/meta/repodb/repodb.go index 2da3ab34..e0495161 100644 --- a/pkg/meta/repodb/repodb.go +++ b/pkg/meta/repodb/repodb.go @@ -2,6 +2,7 @@ package repodb import ( "context" + "time" godigest "github.com/opencontainers/go-digest" ) @@ -9,6 +10,7 @@ import ( // MetadataDB. const ( ManifestDataBucket = "ManifestData" + IndexDataBucket = "IndexData" UserMetadataBucket = "UserMeta" RepoMetadataBucket = "RepoMetadata" VersionBucket = "Version" @@ -59,6 +61,12 @@ type RepoDB interface { //nolint:interfacebloat // GetManifestMeta sets ManifestMetadata for a given manifest in the database SetManifestMeta(repo string, manifestDigest godigest.Digest, mm ManifestMetadata) error + // SetIndexData sets indexData for a given index in the database + SetIndexData(digest godigest.Digest, indexData IndexData) error + + // GetIndexData returns indexData for a given Index from the database + GetIndexData(indexDigest godigest.Digest) (IndexData, error) + // IncrementManifestDownloads adds 1 to the download count of a manifest IncrementImageDownloads(repo string, reference string) error @@ -70,15 +78,15 @@ type RepoDB interface { //nolint:interfacebloat // SearchRepos searches for repos given a search string SearchRepos(ctx context.Context, searchText string, filter Filter, requestedPage PageInput) ( - []RepoMetadata, map[string]ManifestMetadata, PageInfo, error) + []RepoMetadata, map[string]ManifestMetadata, map[string]IndexData, PageInfo, error) // SearchTags searches for images(repo:tag) given a search string SearchTags(ctx context.Context, searchText string, filter Filter, requestedPage PageInput) ( - []RepoMetadata, map[string]ManifestMetadata, PageInfo, error) + []RepoMetadata, map[string]ManifestMetadata, map[string]IndexData, PageInfo, error) // FilterTags filters for images given a filter function FilterTags(ctx context.Context, filter FilterFunc, - requestedPage PageInput) ([]RepoMetadata, map[string]ManifestMetadata, PageInfo, error) + requestedPage PageInput) ([]RepoMetadata, map[string]ManifestMetadata, map[string]IndexData, PageInfo, error) PatchDB() error } @@ -90,6 +98,10 @@ type ManifestMetadata struct { Signatures ManifestSignatures } +type IndexData struct { + IndexBlob []byte +} + type ManifestData struct { ManifestBlob []byte ConfigBlob []byte @@ -163,7 +175,9 @@ type Filter struct { } type FilterData struct { - OsList []string - ArchList []string - IsSigned bool + DownloadCount int + LastUpdated time.Time + OsList []string + ArchList []string + IsSigned bool } diff --git a/pkg/meta/repodb/repodb_test.go b/pkg/meta/repodb/repodb_test.go index 8f3d018d..b154674c 100644 --- a/pkg/meta/repodb/repodb_test.go +++ b/pkg/meta/repodb/repodb_test.go @@ -13,7 +13,7 @@ import ( "time" guuid "github.com/gofrs/uuid" - "github.com/opencontainers/go-digest" + godigest "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" @@ -24,6 +24,7 @@ import ( dynamo "zotregistry.io/zot/pkg/meta/repodb/dynamodb-wrapper" dynamoParams "zotregistry.io/zot/pkg/meta/repodb/dynamodb-wrapper/params" localCtx "zotregistry.io/zot/pkg/requestcontext" + "zotregistry.io/zot/pkg/test" ) const ( @@ -74,12 +75,14 @@ func TestDynamoDBWrapper(t *testing.T) { repoMetaTablename := "RepoMetadataTable" + uuid.String() manifestDataTablename := "ManifestDataTable" + uuid.String() versionTablename := "Version" + uuid.String() + indexDataTablename := "IndexDataTable" + uuid.String() Convey("DynamoDB Wrapper", t, func() { dynamoDBDriverParams := dynamoParams.DBDriverParameters{ Endpoint: os.Getenv("DYNAMODBMOCK_ENDPOINT"), RepoMetaTablename: repoMetaTablename, ManifestDataTablename: manifestDataTablename, + IndexDataTablename: indexDataTablename, VersionTablename: versionTablename, Region: "us-east-2", } @@ -114,7 +117,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { configBlob, manifestBlob, err := generateTestImage() So(err, ShouldBeNil) - manifestDigest := digest.FromBytes(manifestBlob) + manifestDigest := godigest.FromBytes(manifestBlob) err = repoDB.SetManifestData(manifestDigest, repodb.ManifestData{ ManifestBlob: manifestBlob, @@ -133,16 +136,56 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(err, ShouldNotBeNil) }) + Convey("Test SetManifestMeta", func() { + Convey("RepoMeta not found", func() { + var ( + manifestDigest = godigest.FromString("dig") + manifestBlob = []byte("manifestBlob") + configBlob = []byte("configBlob") + + signatures = repodb.ManifestSignatures{ + "digest1": []repodb.SignatureInfo{ + { + SignatureManifestDigest: "signatureDigest", + LayersInfo: []repodb.LayerInfo{ + { + LayerDigest: "layerDigest", + LayerContent: []byte("layerContent"), + }, + }, + }, + }, + } + ) + + err := repoDB.SetManifestMeta("repo", manifestDigest, repodb.ManifestMetadata{ + ManifestBlob: manifestBlob, + ConfigBlob: configBlob, + DownloadCount: 10, + Signatures: signatures, + }) + So(err, ShouldBeNil) + + manifestMeta, err := repoDB.GetManifestMeta("repo", manifestDigest) + So(err, ShouldBeNil) + + So(manifestMeta.ManifestBlob, ShouldResemble, manifestBlob) + So(manifestMeta.ConfigBlob, ShouldResemble, configBlob) + So(manifestMeta.DownloadCount, ShouldEqual, 10) + So(manifestMeta.Signatures, ShouldResemble, signatures) + }) + }) + Convey("Test SetRepoTag", func() { // test behaviours var ( repo1 = "repo1" repo2 = "repo2" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") tag2 = "0.0.2" - manifestDigest2 = digest.FromString("fake-manifes2") + manifestDigest2 = godigest.FromString("fake-manifes2") ) Convey("Setting a good repo", func() { @@ -203,11 +246,11 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { var ( repo1 = "repo1" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") repo2 = "repo2" tag2 = "0.0.2" - manifestDigest2 = digest.FromString("fake-manifest2") + manifestDigest2 = godigest.FromString("fake-manifest2") InexistentRepo = "InexistentRepo" ) @@ -239,9 +282,9 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { var ( repo = "repo1" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") tag2 = "0.0.2" - manifestDigest2 = digest.FromString("fake-manifest2") + manifestDigest2 = godigest.FromString("fake-manifest2") ) err := repoDB.SetRepoTag(repo, tag1, manifestDigest1, ispec.MediaTypeImageManifest) @@ -304,9 +347,9 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { repo1 = "repo1" repo2 = "repo2" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") tag2 = "0.0.2" - manifestDigest2 = digest.FromString("fake-manifest2") + manifestDigest2 = godigest.FromString("fake-manifest2") ) err := repoDB.SetRepoTag(repo1, tag1, manifestDigest1, ispec.MediaTypeImageManifest) @@ -361,7 +404,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { var ( repo1 = "repo1" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") ) err := repoDB.SetRepoTag(repo1, tag1, manifestDigest1, ispec.MediaTypeImageManifest) @@ -393,7 +436,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { var ( repo1 = "repo1" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") ) err := repoDB.SetRepoTag(repo1, tag1, manifestDigest1, ispec.MediaTypeImageManifest) @@ -428,7 +471,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { var ( repo1 = "repo1" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") ) err := repoDB.SetRepoTag(repo1, tag1, manifestDigest1, ispec.MediaTypeImageManifest) @@ -463,7 +506,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { configBlob, manifestBlob, err := generateTestImage() So(err, ShouldBeNil) - manifestDigest := digest.FromBytes(manifestBlob) + manifestDigest := godigest.FromBytes(manifestBlob) err = repoDB.SetRepoTag(repo1, tag1, manifestDigest, ispec.MediaTypeImageManifest) So(err, ShouldBeNil) @@ -498,7 +541,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { var ( repo1 = "repo1" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") ) err := repoDB.SetRepoTag(repo1, tag1, manifestDigest1, ispec.MediaTypeImageManifest) @@ -526,7 +569,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { var ( repo1 = "repo1" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") ) err := repoDB.SetRepoTag(repo1, tag1, manifestDigest1, ispec.MediaTypeImageManifest) @@ -569,11 +612,11 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { repo2 = "repo2" repo3 = "repo3" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") tag2 = "0.0.2" - manifestDigest2 = digest.FromString("fake-manifest2") + manifestDigest2 = godigest.FromString("fake-manifest2") tag3 = "0.0.3" - manifestDigest3 = digest.FromString("fake-manifest3") + manifestDigest3 = godigest.FromString("fake-manifest3") ctx = context.Background() emptyManifest ispec.Manifest emptyConfig ispec.Manifest @@ -604,13 +647,13 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { err = repoDB.SetManifestMeta(repo1, manifestDigest3, emptyRepoMeta) So(err, ShouldBeNil) - repos, manifesMetaMap, _, err := repoDB.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + repos, manifestMetaMap, _, _, err := repoDB.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 2) - So(len(manifesMetaMap), ShouldEqual, 3) - So(manifesMetaMap, ShouldContainKey, manifestDigest1.String()) - So(manifesMetaMap, ShouldContainKey, manifestDigest2.String()) - So(manifesMetaMap, ShouldContainKey, manifestDigest3.String()) + So(len(manifestMetaMap), ShouldEqual, 3) + So(manifestMetaMap, ShouldContainKey, manifestDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest2.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest3.String()) }) Convey("Search a repo by name", func() { @@ -620,11 +663,11 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { err = repoDB.SetManifestMeta(repo1, manifestDigest1, emptyRepoMeta) So(err, ShouldBeNil) - repos, manifesMetaMap, _, err := repoDB.SearchRepos(ctx, repo1, repodb.Filter{}, repodb.PageInput{}) + repos, manifestMetaMap, _, _, err := repoDB.SearchRepos(ctx, repo1, repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 1) - So(len(manifesMetaMap), ShouldEqual, 1) - So(manifesMetaMap, ShouldContainKey, manifestDigest1.String()) + So(len(manifestMetaMap), ShouldEqual, 1) + So(manifestMetaMap, ShouldContainKey, manifestDigest1.String()) }) Convey("Search non-existing repo by name", func() { @@ -634,10 +677,11 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { err = repoDB.SetRepoTag(repo1, tag2, manifestDigest2, ispec.MediaTypeImageManifest) So(err, ShouldBeNil) - repos, manifesMetaMap, _, err := repoDB.SearchRepos(ctx, "RepoThatDoesntExist", repodb.Filter{}, repodb.PageInput{}) + repos, manifestMetaMap, _, _, err := repoDB.SearchRepos(ctx, "RepoThatDoesntExist", repodb.Filter{}, + repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 0) - So(len(manifesMetaMap), ShouldEqual, 0) + So(len(manifestMetaMap), ShouldEqual, 0) }) Convey("Search with partial match", func() { @@ -655,12 +699,12 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { err = repoDB.SetManifestMeta("golang", manifestDigest3, emptyRepoMeta) So(err, ShouldBeNil) - repos, manifesMetaMap, _, err := repoDB.SearchRepos(ctx, "pine", repodb.Filter{}, repodb.PageInput{}) + repos, manifestMetaMap, _, _, err := repoDB.SearchRepos(ctx, "pine", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 2) - So(manifesMetaMap, ShouldContainKey, manifestDigest1.String()) - So(manifesMetaMap, ShouldContainKey, manifestDigest2.String()) - So(manifesMetaMap, ShouldNotContainKey, manifestDigest3.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest2.String()) + So(manifestMetaMap, ShouldNotContainKey, manifestDigest3.String()) }) Convey("Search multiple repos that share manifests", func() { @@ -678,10 +722,10 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { err = repoDB.SetManifestMeta(repo3, manifestDigest1, emptyRepoMeta) So(err, ShouldBeNil) - repos, manifesMetaMap, _, err := repoDB.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) + repos, manifestMetaMap, _, _, err := repoDB.SearchRepos(ctx, "", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 3) - So(len(manifesMetaMap), ShouldEqual, 1) + So(len(manifestMetaMap), ShouldEqual, 1) }) Convey("Search repos with access control", func() { @@ -709,7 +753,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { authzCtxKey := localCtx.GetContextKey() ctx := context.WithValue(context.Background(), authzCtxKey, acCtx) - repos, _, _, err := repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{}) + repos, _, _, _, err := repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 2) for _, k := range repos { @@ -722,7 +766,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { repoNameBuilder := strings.Builder{} for _, i := range rand.Perm(reposCount) { - manifestDigest := digest.FromString("fakeManifest" + strconv.Itoa(i)) + manifestDigest := godigest.FromString("fakeManifest" + strconv.Itoa(i)) timeString := fmt.Sprintf("1%02d0-01-01 04:35", i) createdTime, err := time.Parse("2006-01-02 15:04", timeString) So(err, ShouldBeNil) @@ -754,18 +798,18 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { repoNameBuilder.Reset() } - repos, _, _, err := repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{}) + repos, _, _, _, err := repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, reposCount) - repos, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 20, SortBy: repodb.AlphabeticAsc, }) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 20) - repos, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 0, SortBy: repodb.AlphabeticAsc, @@ -774,7 +818,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(len(repos), ShouldEqual, 1) So(repos[0].Name, ShouldResemble, "repo0") - repos, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 1, SortBy: repodb.AlphabeticAsc, @@ -783,7 +827,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(len(repos), ShouldEqual, 1) So(repos[0].Name, ShouldResemble, "repo1") - repos, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 49, SortBy: repodb.AlphabeticAsc, @@ -792,7 +836,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(len(repos), ShouldEqual, 1) So(repos[0].Name, ShouldResemble, "repo9") - repos, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 49, SortBy: repodb.AlphabeticDsc, @@ -801,7 +845,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(len(repos), ShouldEqual, 1) So(repos[0].Name, ShouldResemble, "repo0") - repos, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 0, SortBy: repodb.AlphabeticDsc, @@ -811,7 +855,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(repos[0].Name, ShouldResemble, "repo9") // sort by downloads - repos, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 0, SortBy: repodb.Downloads, @@ -821,7 +865,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(repos[0].Name, ShouldResemble, "repo49") // sort by last update - repos, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 0, SortBy: repodb.UpdateTime, @@ -830,7 +874,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(len(repos), ShouldEqual, 1) So(repos[0].Name, ShouldResemble, "repo49") - repos, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 100, SortBy: repodb.UpdateTime, @@ -841,43 +885,120 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { }) Convey("Search with wrong pagination input", func() { - _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + _, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 100, SortBy: repodb.UpdateTime, }) So(err, ShouldBeNil) - _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + _, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: -1, Offset: 100, SortBy: repodb.UpdateTime, }) So(err, ShouldNotBeNil) - _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + _, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: -1, SortBy: repodb.UpdateTime, }) So(err, ShouldNotBeNil) - _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ + _, _, _, _, err = repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 1, SortBy: repodb.SortCriteria("InvalidSortingCriteria"), }) So(err, ShouldNotBeNil) }) + + Convey("Search Repos with Indexes", func() { + var ( + tag4 = "0.0.4" + indexDigest = godigest.FromString("Multiarch") + manifestDigest1 = godigest.FromString("manifestDigest1") + manifestDigest2 = godigest.FromString("manifestDigest2") + + tag5 = "0.0.5" + manifestDigest3 = godigest.FromString("manifestDigest3") + ) + + err := repoDB.SetManifestData(manifestDigest1, repodb.ManifestData{ + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + config := ispec.Image{ + Platform: ispec.Platform{ + Architecture: "arch", + OS: "os", + }, + } + + confBlob, err := json.Marshal(config) + So(err, ShouldBeNil) + + err = repoDB.SetManifestData(manifestDigest2, repodb.ManifestData{ + ManifestBlob: []byte("{}"), + ConfigBlob: confBlob, + }) + So(err, ShouldBeNil) + err = repoDB.SetManifestData(manifestDigest3, repodb.ManifestData{ + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + indexContent := ispec.Index{ + MediaType: ispec.MediaTypeImageIndex, + Manifests: []ispec.Descriptor{ + { + Digest: manifestDigest1, + }, + { + Digest: manifestDigest2, + }, + }, + } + + indexBlob, err := json.Marshal(indexContent) + So(err, ShouldBeNil) + + err = repoDB.SetIndexData(indexDigest, repodb.IndexData{ + IndexBlob: indexBlob, + }) + So(err, ShouldBeNil) + + err = repoDB.SetRepoTag("repo", tag4, indexDigest, ispec.MediaTypeImageIndex) + So(err, ShouldBeNil) + + err = repoDB.SetRepoTag("repo", tag5, manifestDigest3, ispec.MediaTypeImageManifest) + So(err, ShouldBeNil) + + repos, manifestMetaMap, indexDataMap, _, err := repoDB.SearchRepos(ctx, "repo", repodb.Filter{}, repodb.PageInput{}) + So(err, ShouldBeNil) + + So(len(repos), ShouldEqual, 1) + So(repos[0].Name, ShouldResemble, "repo") + So(repos[0].Tags, ShouldContainKey, tag4) + So(repos[0].Tags, ShouldContainKey, tag5) + So(manifestMetaMap, ShouldContainKey, manifestDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest2.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest3.String()) + So(indexDataMap, ShouldContainKey, indexDigest.String()) + }) }) Convey("Test SearchTags", func() { var ( repo1 = "repo1" repo2 = "repo2" - manifestDigest1 = digest.FromString("fake-manifest1") - manifestDigest2 = digest.FromString("fake-manifest2") - manifestDigest3 = digest.FromString("fake-manifest3") + manifestDigest1 = godigest.FromString("fake-manifest1") + manifestDigest2 = godigest.FromString("fake-manifest2") + manifestDigest3 = godigest.FromString("fake-manifest3") ctx = context.Background() emptyManifest ispec.Manifest emptyConfig ispec.Manifest @@ -917,48 +1038,48 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(err, ShouldBeNil) Convey("With exact match", func() { - repos, manifesMetaMap, _, err := repoDB.SearchTags(ctx, "repo1:0.0.1", repodb.Filter{}, repodb.PageInput{}) + repos, manifestMetaMap, _, _, err := repoDB.SearchTags(ctx, "repo1:0.0.1", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 1) So(len(repos[0].Tags), ShouldEqual, 1) So(repos[0].Tags, ShouldContainKey, "0.0.1") - So(manifesMetaMap, ShouldContainKey, manifestDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest1.String()) }) Convey("With partial repo path", func() { - repos, manifesMetaMap, _, err := repoDB.SearchTags(ctx, "repo:0.0.1", repodb.Filter{}, repodb.PageInput{}) + repos, manifestMetaMap, _, _, err := repoDB.SearchTags(ctx, "repo:0.0.1", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 0) - So(len(manifesMetaMap), ShouldEqual, 0) + So(len(manifestMetaMap), ShouldEqual, 0) }) Convey("With partial tag", func() { - repos, manifesMetaMap, _, err := repoDB.SearchTags(ctx, "repo1:0.0", repodb.Filter{}, repodb.PageInput{}) + repos, manifestMetaMap, _, _, err := repoDB.SearchTags(ctx, "repo1:0.0", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 1) So(len(repos[0].Tags), ShouldEqual, 2) So(repos[0].Tags, ShouldContainKey, "0.0.2") So(repos[0].Tags, ShouldContainKey, "0.0.1") - So(manifesMetaMap, ShouldContainKey, manifestDigest1.String()) - So(manifesMetaMap, ShouldContainKey, manifestDigest3.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest3.String()) - repos, manifesMetaMap, _, err = repoDB.SearchTags(ctx, "repo1:0.", repodb.Filter{}, repodb.PageInput{}) + repos, manifestMetaMap, _, _, err = repoDB.SearchTags(ctx, "repo1:0.", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 1) So(len(repos[0].Tags), ShouldEqual, 3) So(repos[0].Tags, ShouldContainKey, "0.0.1") So(repos[0].Tags, ShouldContainKey, "0.0.2") So(repos[0].Tags, ShouldContainKey, "0.1.0") - So(manifesMetaMap, ShouldContainKey, manifestDigest1.String()) - So(manifesMetaMap, ShouldContainKey, manifestDigest2.String()) - So(manifesMetaMap, ShouldContainKey, manifestDigest3.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest2.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest3.String()) }) Convey("With bad query", func() { - repos, manifesMetaMap, _, err := repoDB.SearchTags(ctx, "repo:0.0.1:test", repodb.Filter{}, repodb.PageInput{}) + repos, manifestMetaMap, _, _, err := repoDB.SearchTags(ctx, "repo:0.0.1:test", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldNotBeNil) So(len(repos), ShouldEqual, 0) - So(len(manifesMetaMap), ShouldEqual, 0) + So(len(manifestMetaMap), ShouldEqual, 0) }) Convey("Search with access control", func() { @@ -967,7 +1088,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { repo2 = "repo2" repo3 = "repo3" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") tag2 = "0.0.2" tag3 = "0.0.3" ) @@ -1000,30 +1121,115 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { authzCtxKey := localCtx.GetContextKey() ctx := context.WithValue(context.Background(), authzCtxKey, acCtx) - repos, _, _, err := repoDB.SearchTags(ctx, "repo1:", repodb.Filter{}, repodb.PageInput{}) + repos, _, _, _, err := repoDB.SearchTags(ctx, "repo1:", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 1) So(repos[0].Name, ShouldResemble, repo1) - repos, _, _, err = repoDB.SearchTags(ctx, "repo2:", repodb.Filter{}, repodb.PageInput{}) + repos, _, _, _, err = repoDB.SearchTags(ctx, "repo2:", repodb.Filter{}, repodb.PageInput{}) So(err, ShouldBeNil) So(repos, ShouldBeEmpty) }) Convey("With wrong pagination input", func() { - repos, _, _, err := repoDB.SearchTags(ctx, "repo2:", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err := repoDB.SearchTags(ctx, "repo2:", repodb.Filter{}, repodb.PageInput{ Limit: -1, }) So(err, ShouldNotBeNil) So(repos, ShouldBeEmpty) }) + + Convey("Search Tags with Indexes", func() { + var ( + tag4 = "0.0.4" + indexDigest = godigest.FromString("Multiarch") + manifestDigest1 = godigest.FromString("manifestDigest1") + manifestDigest2 = godigest.FromString("manifestDigest2") + + tag5 = "0.0.5" + manifestDigest3 = godigest.FromString("manifestDigest3") + + tag6 = "6.0.0" + manifestDigest4 = godigest.FromString("manifestDigest4") + ) + + err := repoDB.SetManifestData(manifestDigest1, repodb.ManifestData{ + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + config := ispec.Image{ + Platform: ispec.Platform{ + Architecture: "arch", + OS: "os", + }, + } + + confBlob, err := json.Marshal(config) + So(err, ShouldBeNil) + + err = repoDB.SetManifestData(manifestDigest2, repodb.ManifestData{ + ManifestBlob: []byte("{}"), + ConfigBlob: confBlob, + }) + So(err, ShouldBeNil) + err = repoDB.SetManifestData(manifestDigest3, repodb.ManifestData{ + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + err = repoDB.SetManifestData(manifestDigest4, repodb.ManifestData{ + ManifestBlob: []byte("{}"), + ConfigBlob: []byte("{}"), + }) + So(err, ShouldBeNil) + + indexBlob, err := test.GetIndexBlobWithManifests( + []godigest.Digest{ + manifestDigest1, + manifestDigest2, + }, + ) + So(err, ShouldBeNil) + + err = repoDB.SetIndexData(indexDigest, repodb.IndexData{ + IndexBlob: indexBlob, + }) + So(err, ShouldBeNil) + + err = repoDB.SetRepoTag("repo", tag4, indexDigest, ispec.MediaTypeImageIndex) + So(err, ShouldBeNil) + + err = repoDB.SetRepoTag("repo", tag5, manifestDigest3, ispec.MediaTypeImageManifest) + So(err, ShouldBeNil) + + err = repoDB.SetRepoTag("repo", tag6, manifestDigest4, ispec.MediaTypeImageManifest) + So(err, ShouldBeNil) + + repos, manifestMetaMap, indexDataMap, _, err := repoDB.SearchTags(ctx, "repo:0.0", repodb.Filter{}, + repodb.PageInput{}) + So(err, ShouldBeNil) + + So(len(repos), ShouldEqual, 1) + So(repos[0].Name, ShouldResemble, "repo") + So(repos[0].Tags, ShouldContainKey, tag4) + So(repos[0].Tags, ShouldContainKey, tag5) + So(repos[0].Tags, ShouldNotContainKey, tag6) + So(manifestMetaMap, ShouldContainKey, manifestDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest2.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest3.String()) + So(manifestMetaMap, ShouldNotContainKey, manifestDigest4.String()) + So(indexDataMap, ShouldContainKey, indexDigest.String()) + }) }) Convey("Paginated tag search", func() { var ( repo1 = "repo1" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") tag2 = "0.0.2" tag3 = "0.0.3" tag4 = "0.0.4" @@ -1048,7 +1254,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { err = repoDB.SetManifestMeta(repo1, manifestDigest1, repodb.ManifestMetadata{ConfigBlob: configBlob}) So(err, ShouldBeNil) - repos, _, _, err := repoDB.SearchTags(context.TODO(), "repo1:", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err := repoDB.SearchTags(context.TODO(), "repo1:", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 0, SortBy: repodb.AlphabeticAsc, @@ -1061,7 +1267,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { keys = append(keys, k) } - repos, _, _, err = repoDB.SearchTags(context.TODO(), "repo1:", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchTags(context.TODO(), "repo1:", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 1, SortBy: repodb.AlphabeticAsc, @@ -1073,7 +1279,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { keys = append(keys, k) } - repos, _, _, err = repoDB.SearchTags(context.TODO(), "repo1:", repodb.Filter{}, repodb.PageInput{ + repos, _, _, _, err = repoDB.SearchTags(context.TODO(), "repo1:", repodb.Filter{}, repodb.PageInput{ Limit: 1, Offset: 2, SortBy: repodb.AlphabeticAsc, @@ -1098,9 +1304,9 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { repo4 = "repo4" tag1 = "0.0.1" tag2 = "0.0.2" - manifestDigest1 = digest.FromString("fake-manifest1") - manifestDigest2 = digest.FromString("fake-manifest2") - manifestDigest3 = digest.FromString("fake-manifest3") + manifestDigest1 = godigest.FromString("fake-manifest1") + manifestDigest2 = godigest.FromString("fake-manifest2") + manifestDigest3 = godigest.FromString("fake-manifest3") ) err := repoDB.SetRepoTag(repo1, tag1, manifestDigest1, ispec.MediaTypeImageManifest) @@ -1157,7 +1363,8 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { Os: []*string{&opSys}, } - repos, _, _, err := repoDB.SearchRepos(context.TODO(), "", filter, repodb.PageInput{SortBy: repodb.AlphabeticAsc}) + repos, _, _, _, err := repoDB.SearchRepos(context.TODO(), "", filter, + repodb.PageInput{SortBy: repodb.AlphabeticAsc}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 2) So(repos[0].Name, ShouldResemble, "repo1") @@ -1167,7 +1374,8 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { filter = repodb.Filter{ Os: []*string{&opSys}, } - repos, _, _, err = repoDB.SearchRepos(context.TODO(), "repo", filter, repodb.PageInput{SortBy: repodb.AlphabeticAsc}) + repos, _, _, _, err = repoDB.SearchRepos(context.TODO(), "repo", filter, + repodb.PageInput{SortBy: repodb.AlphabeticAsc}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 2) So(repos[0].Name, ShouldResemble, "repo1") @@ -1177,7 +1385,8 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { filter = repodb.Filter{ Os: []*string{&opSys}, } - repos, _, _, err = repoDB.SearchRepos(context.TODO(), "repo", filter, repodb.PageInput{SortBy: repodb.AlphabeticAsc}) + repos, _, _, _, err = repoDB.SearchRepos(context.TODO(), "repo", filter, + repodb.PageInput{SortBy: repodb.AlphabeticAsc}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 0) @@ -1187,7 +1396,8 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { Os: []*string{&opSys}, Arch: []*string{&arch}, } - repos, _, _, err = repoDB.SearchRepos(context.TODO(), "repo", filter, repodb.PageInput{SortBy: repodb.AlphabeticAsc}) + repos, _, _, _, err = repoDB.SearchRepos(context.TODO(), "repo", filter, + repodb.PageInput{SortBy: repodb.AlphabeticAsc}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 2) So(repos[0].Name, ShouldResemble, "repo1") @@ -1199,7 +1409,8 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { Os: []*string{&opSys}, Arch: []*string{&arch}, } - repos, _, _, err = repoDB.SearchRepos(context.TODO(), "repo", filter, repodb.PageInput{SortBy: repodb.AlphabeticAsc}) + repos, _, _, _, err = repoDB.SearchRepos(context.TODO(), "repo", filter, + repodb.PageInput{SortBy: repodb.AlphabeticAsc}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 1) }) @@ -1212,12 +1423,33 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { repo4 = "repo4" tag1 = "0.0.1" tag2 = "0.0.2" - manifestDigest1 = digest.FromString("fake-manifest1") - manifestDigest2 = digest.FromString("fake-manifest2") - manifestDigest3 = digest.FromString("fake-manifest3") + tag3 = "0.0.3" + manifestDigest1 = godigest.FromString("fake-manifest1") + manifestDigest2 = godigest.FromString("fake-manifest2") + manifestDigest3 = godigest.FromString("fake-manifest3") + + indexDigest = godigest.FromString("index-digest") + manifestFromIndexDigest1 = godigest.FromString("fake-manifestFromIndexDigest1") + manifestFromIndexDigest2 = godigest.FromString("fake-manifestFromIndexDigest2") ) - err := repoDB.SetRepoTag(repo1, tag1, manifestDigest1, ispec.MediaTypeImageManifest) + err := repoDB.SetRepoTag(repo1, tag3, indexDigest, ispec.MediaTypeImageIndex) + So(err, ShouldBeNil) + + indexBlob, err := test.GetIndexBlobWithManifests( + []godigest.Digest{ + manifestFromIndexDigest1, + manifestFromIndexDigest2, + }, + ) + So(err, ShouldBeNil) + + err = repoDB.SetIndexData(indexDigest, repodb.IndexData{ + IndexBlob: indexBlob, + }) + So(err, ShouldBeNil) + + err = repoDB.SetRepoTag(repo1, tag1, manifestDigest1, ispec.MediaTypeImageManifest) So(err, ShouldBeNil) err = repoDB.SetRepoTag(repo1, tag2, manifestDigest2, ispec.MediaTypeImageManifest) So(err, ShouldBeNil) @@ -1265,13 +1497,21 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { err = repoDB.SetManifestMeta(repo4, manifestDigest3, repodb.ManifestMetadata{ConfigBlob: configBlob3}) So(err, ShouldBeNil) + err = repoDB.SetManifestMeta(repo1, manifestFromIndexDigest1, + repodb.ManifestMetadata{ConfigBlob: []byte("{}")}) + So(err, ShouldBeNil) + + err = repoDB.SetManifestMeta(repo1, manifestFromIndexDigest2, + repodb.ManifestMetadata{ConfigBlob: []byte("{}")}) + So(err, ShouldBeNil) + opSys := LINUX arch := AMD filter := repodb.Filter{ Os: []*string{&opSys}, Arch: []*string{&arch}, } - repos, _, _, err := repoDB.SearchTags(context.TODO(), "repo1:", filter, + repos, _, _, _, err := repoDB.SearchTags(context.TODO(), "repo1:", filter, repodb.PageInput{SortBy: repodb.AlphabeticAsc}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 1) @@ -1283,7 +1523,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { Os: []*string{&opSys}, Arch: []*string{&arch}, } - repos, _, _, err = repoDB.SearchTags(context.TODO(), "repo1:", filter, + repos, _, _, _, err = repoDB.SearchTags(context.TODO(), "repo1:", filter, repodb.PageInput{SortBy: repodb.AlphabeticAsc}) So(err, ShouldBeNil) So(len(repos), ShouldEqual, 0) @@ -1291,14 +1531,18 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { Convey("Test FilterTags", func() { var ( - repo1 = "repo1" - repo2 = "repo2" - manifestDigest1 = digest.FromString("fake-manifest1") - manifestDigest2 = digest.FromString("fake-manifest2") - manifestDigest3 = digest.FromString("fake-manifest3") - ctx = context.Background() - emptyManifest ispec.Manifest - emptyConfig ispec.Image + repo1 = "repo1" + repo2 = "repo2" + manifestDigest1 = godigest.FromString("fake-manifest1") + manifestDigest2 = godigest.FromString("fake-manifest2") + manifestDigest3 = godigest.FromString("fake-manifest3") + indexDigest = godigest.FromString("index-digest") + manifestFromIndexDigest1 = godigest.FromString("fake-manifestFromIndexDigest1") + manifestFromIndexDigest2 = godigest.FromString("fake-manifestFromIndexDigest2") + + emptyManifest ispec.Manifest + emptyConfig ispec.Image + ctx = context.Background() ) emptyManifestBlob, err := json.Marshal(emptyManifest) @@ -1307,11 +1551,30 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { emptyConfigBlob, err := json.Marshal(emptyConfig) So(err, ShouldBeNil) - emptyRepoMeta := repodb.ManifestMetadata{ + emptyManifestMeta := repodb.ManifestMetadata{ ManifestBlob: emptyManifestBlob, ConfigBlob: emptyConfigBlob, } + emptyManifestData := repodb.ManifestData{ + ManifestBlob: emptyManifestBlob, + ConfigBlob: emptyConfigBlob, + } + + err = repoDB.SetRepoTag(repo1, "2.0.0", indexDigest, ispec.MediaTypeImageIndex) + So(err, ShouldBeNil) + + indexBlob, err := test.GetIndexBlobWithManifests([]godigest.Digest{ + manifestFromIndexDigest1, + manifestFromIndexDigest2, + }) + So(err, ShouldBeNil) + + err = repoDB.SetIndexData(indexDigest, repodb.IndexData{ + IndexBlob: indexBlob, + }) + So(err, ShouldBeNil) + err = repoDB.SetRepoTag(repo1, "0.0.1", manifestDigest1, ispec.MediaTypeImageManifest) So(err, ShouldBeNil) err = repoDB.SetRepoTag(repo1, "0.0.2", manifestDigest3, ispec.MediaTypeImageManifest) @@ -1325,17 +1588,22 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { err = repoDB.SetRepoTag(repo2, "0.0.1", manifestDigest3, ispec.MediaTypeImageManifest) So(err, ShouldBeNil) - err = repoDB.SetManifestMeta(repo1, manifestDigest1, emptyRepoMeta) + err = repoDB.SetManifestMeta(repo1, manifestDigest1, emptyManifestMeta) So(err, ShouldBeNil) - err = repoDB.SetManifestMeta(repo1, manifestDigest2, emptyRepoMeta) + err = repoDB.SetManifestMeta(repo1, manifestDigest2, emptyManifestMeta) So(err, ShouldBeNil) - err = repoDB.SetManifestMeta(repo1, manifestDigest3, emptyRepoMeta) + err = repoDB.SetManifestMeta(repo1, manifestDigest3, emptyManifestMeta) So(err, ShouldBeNil) - err = repoDB.SetManifestMeta(repo2, manifestDigest3, emptyRepoMeta) + err = repoDB.SetManifestMeta(repo2, manifestDigest3, emptyManifestMeta) + So(err, ShouldBeNil) + + err = repoDB.SetManifestData(manifestFromIndexDigest1, emptyManifestData) + So(err, ShouldBeNil) + err = repoDB.SetManifestData(manifestFromIndexDigest2, emptyManifestData) So(err, ShouldBeNil) Convey("Return all tags", func() { - repos, manifesMetaMap, pageInfo, err := repoDB.FilterTags( + repos, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -1347,23 +1615,27 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(len(repos), ShouldEqual, 2) So(repos[0].Name, ShouldEqual, "repo1") So(repos[1].Name, ShouldEqual, "repo2") - So(len(repos[0].Tags), ShouldEqual, 5) + So(len(repos[0].Tags), ShouldEqual, 6) So(len(repos[1].Tags), ShouldEqual, 1) So(repos[0].Tags, ShouldContainKey, "0.0.1") So(repos[0].Tags, ShouldContainKey, "0.0.2") So(repos[0].Tags, ShouldContainKey, "0.1.0") So(repos[0].Tags, ShouldContainKey, "1.0.0") So(repos[0].Tags, ShouldContainKey, "1.0.1") + So(repos[0].Tags, ShouldContainKey, "2.0.0") So(repos[1].Tags, ShouldContainKey, "0.0.1") - So(manifesMetaMap, ShouldContainKey, manifestDigest1.String()) - So(manifesMetaMap, ShouldContainKey, manifestDigest2.String()) - So(manifesMetaMap, ShouldContainKey, manifestDigest3.String()) - So(pageInfo.ItemCount, ShouldEqual, 6) - So(pageInfo.TotalCount, ShouldEqual, 6) + So(manifestMetaMap, ShouldContainKey, manifestDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest2.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest3.String()) + So(indexDataMap, ShouldContainKey, indexDigest.String()) + So(manifestMetaMap, ShouldContainKey, manifestFromIndexDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestFromIndexDigest2.String()) + So(pageInfo.ItemCount, ShouldEqual, 7) + So(pageInfo.TotalCount, ShouldEqual, 7) }) Convey("Return all tags in a specific repo", func() { - repos, manifesMetaMap, pageInfo, err := repoDB.FilterTags( + repos, manifestMetaMap, indexDataMap, pageInfo, err := repoDB.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return repoMeta.Name == repo1 @@ -1374,21 +1646,25 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(err, ShouldBeNil) So(len(repos), ShouldEqual, 1) So(repos[0].Name, ShouldEqual, repo1) - So(len(repos[0].Tags), ShouldEqual, 5) + So(len(repos[0].Tags), ShouldEqual, 6) So(repos[0].Tags, ShouldContainKey, "0.0.1") So(repos[0].Tags, ShouldContainKey, "0.0.2") So(repos[0].Tags, ShouldContainKey, "0.1.0") So(repos[0].Tags, ShouldContainKey, "1.0.0") So(repos[0].Tags, ShouldContainKey, "1.0.1") - So(manifesMetaMap, ShouldContainKey, manifestDigest1.String()) - So(manifesMetaMap, ShouldContainKey, manifestDigest2.String()) - So(manifesMetaMap, ShouldContainKey, manifestDigest3.String()) - So(pageInfo.ItemCount, ShouldEqual, 5) - So(pageInfo.TotalCount, ShouldEqual, 5) + So(repos[0].Tags, ShouldContainKey, "2.0.0") + So(manifestMetaMap, ShouldContainKey, manifestDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest2.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest3.String()) + So(indexDataMap, ShouldContainKey, indexDigest.String()) + So(manifestMetaMap, ShouldContainKey, manifestFromIndexDigest1.String()) + So(manifestMetaMap, ShouldContainKey, manifestFromIndexDigest2.String()) + So(pageInfo.ItemCount, ShouldEqual, 6) + So(pageInfo.TotalCount, ShouldEqual, 6) }) Convey("Filter everything out", func() { - repos, manifesMetaMap, pageInfo, err := repoDB.FilterTags( + repos, manifestMetaMap, _, pageInfo, err := repoDB.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return false @@ -1398,7 +1674,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(err, ShouldBeNil) So(len(repos), ShouldEqual, 0) - So(len(manifesMetaMap), ShouldEqual, 0) + So(len(manifestMetaMap), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) }) @@ -1415,7 +1691,7 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { authzCtxKey := localCtx.GetContextKey() ctx := context.WithValue(context.Background(), authzCtxKey, acCtx) - repos, manifesMetaMap, pageInfo, err := repoDB.FilterTags( + repos, manifestMetaMap, _, pageInfo, err := repoDB.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -1428,13 +1704,13 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(repos[0].Name, ShouldResemble, repo2) So(len(repos[0].Tags), ShouldEqual, 1) So(repos[0].Tags, ShouldContainKey, "0.0.1") - So(manifesMetaMap, ShouldContainKey, manifestDigest3.String()) + So(manifestMetaMap, ShouldContainKey, manifestDigest3.String()) So(pageInfo.ItemCount, ShouldEqual, 1) So(pageInfo.TotalCount, ShouldEqual, 1) }) Convey("With wrong pagination input", func() { - repos, _, _, err := repoDB.FilterTags( + repos, _, _, _, err := repoDB.FilterTags( ctx, func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool { return true @@ -1445,6 +1721,27 @@ func RunRepoDBTests(repoDB repodb.RepoDB, preparationFuncs ...func() error) { So(repos, ShouldBeEmpty) }) }) + + Convey("Test index logic", func() { + multiArch, err := test.GetRandomMultiarchImage("tag1") + So(err, ShouldBeNil) + + indexDigest, err := multiArch.Digest() + So(err, ShouldBeNil) + + indexData, err := multiArch.IndexData() + So(err, ShouldBeNil) + + err = repoDB.SetIndexData(indexDigest, indexData) + So(err, ShouldBeNil) + + result, err := repoDB.GetIndexData(indexDigest) + So(err, ShouldBeNil) + So(result, ShouldResemble, indexData) + + _, err = repoDB.GetIndexData(godigest.FromString("inexistent")) + So(err, ShouldNotBeNil) + }) }) } @@ -1480,11 +1777,11 @@ func TestRelevanceSorting(t *testing.T) { repo3 = "notalpine" repo4 = "unmached/repo" tag1 = "0.0.1" - manifestDigest1 = digest.FromString("fake-manifest1") + manifestDigest1 = godigest.FromString("fake-manifest1") tag2 = "0.0.2" - manifestDigest2 = digest.FromString("fake-manifest2") + manifestDigest2 = godigest.FromString("fake-manifest2") tag3 = "0.0.3" - manifestDigest3 = digest.FromString("fake-manifest3") + manifestDigest3 = godigest.FromString("fake-manifest3") ctx = context.Background() emptyManifest ispec.Manifest emptyConfig ispec.Manifest @@ -1526,7 +1823,7 @@ func TestRelevanceSorting(t *testing.T) { err = repoDB.SetManifestMeta(repo4, manifestDigest3, emptyRepoMeta) So(err, ShouldBeNil) - repos, _, _, err := repoDB.SearchRepos(ctx, "pine", repodb.Filter{}, + repos, _, _, _, err := repoDB.SearchRepos(ctx, "pine", repodb.Filter{}, repodb.PageInput{SortBy: repodb.Relevance}, ) @@ -1547,7 +1844,7 @@ func generateTestImage() ([]byte, []byte, error) { }, RootFS: ispec.RootFS{ Type: "layers", - DiffIDs: []digest.Digest{}, + DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", } @@ -1557,7 +1854,7 @@ func generateTestImage() ([]byte, []byte, error) { return []byte{}, []byte{}, err } - configDigest := digest.FromBytes(configBlob) + configDigest := godigest.FromBytes(configBlob) layers := [][]byte{ make([]byte, 100), @@ -1584,7 +1881,7 @@ func generateTestImage() ([]byte, []byte, error) { Layers: []ispec.Descriptor{ { MediaType: "application/vnd.oci.image.layer.v1.tar", - Digest: digest.FromBytes(layers[0]), + Digest: godigest.FromBytes(layers[0]), Size: int64(len(layers[0])), }, }, diff --git a/pkg/meta/repodb/repodbfactory/repodb_factory_test.go b/pkg/meta/repodb/repodbfactory/repodb_factory_test.go index e270527d..5672f58c 100644 --- a/pkg/meta/repodb/repodbfactory/repodb_factory_test.go +++ b/pkg/meta/repodb/repodbfactory/repodb_factory_test.go @@ -19,6 +19,7 @@ func TestCreateDynamo(t *testing.T) { Endpoint: os.Getenv("DYNAMODBMOCK_ENDPOINT"), RepoMetaTablename: "RepoMetadataTable", ManifestDataTablename: "ManifestDataTable", + IndexDataTablename: "IndexDataTable", VersionTablename: "Version", Region: "us-east-2", } diff --git a/pkg/meta/repodb/sync_repodb.go b/pkg/meta/repodb/sync_repodb.go index e02f4576..d3681181 100644 --- a/pkg/meta/repodb/sync_repodb.go +++ b/pkg/meta/repodb/sync_repodb.go @@ -74,12 +74,6 @@ func SyncRepo(repo string, repoDB RepoDB, storeController storage.StoreControlle for _, manifest := range indexContent.Manifests { tag, hasTag := manifest.Annotations[ispec.AnnotationRefName] - if !hasTag { - log.Warn().Msgf("sync-repo: image without tag found, will not be synced into RepoDB") - - continue - } - manifestMetaIsPresent, err := isManifestMetaPresent(repo, manifest, repoDB) if err != nil { log.Error().Err(err).Msgf("sync-repo: error checking manifestMeta in RepoDB") @@ -87,7 +81,7 @@ func SyncRepo(repo string, repoDB RepoDB, storeController storage.StoreControlle return err } - if manifestMetaIsPresent { + if manifestMetaIsPresent && hasTag { err = repoDB.SetRepoTag(repo, tag, manifest.Digest, manifest.MediaType) if err != nil { log.Error().Err(err).Msgf("sync-repo: failed to set repo tag for %s:%s", repo, tag) @@ -131,31 +125,16 @@ func SyncRepo(repo string, repoDB RepoDB, storeController storage.StoreControlle continue } - manifestData, err := NewManifestData(repo, manifestBlob, storeController) - if err != nil { - log.Error().Err(err).Msgf("sync-repo: failed to create manifest data for image %s:%s manifest digest %s ", - repo, tag, manifest.Digest.String()) + reference := tag - return err + if tag == "" { + reference = manifest.Digest.String() } - err = repoDB.SetManifestMeta(repo, manifest.Digest, ManifestMetadata{ - ManifestBlob: manifestData.ManifestBlob, - ConfigBlob: manifestData.ConfigBlob, - DownloadCount: 0, - Signatures: ManifestSignatures{}, - }) + err = SetMetadataFromInput(repo, reference, manifest.MediaType, manifest.Digest, manifestBlob, + storeController, repoDB, log) if err != nil { - log.Error().Err(err).Msgf("sync-repo: failed to set manifest meta for image %s:%s manifest digest %s ", - repo, tag, manifest.Digest.String()) - - return err - } - - err = repoDB.SetRepoTag(repo, tag, manifest.Digest, manifest.MediaType) - if err != nil { - log.Error().Err(err).Msgf("sync-repo: failed to repo tag for repo %s and tag %s", - repo, tag) + log.Error().Err(err).Msgf("sync-repo: failed to set metadata for %s:%s", repo, tag) return err } @@ -271,3 +250,61 @@ func NewManifestData(repoName string, manifestBlob []byte, storeController stora return manifestData, nil } + +func NewIndexData(repoName string, indexBlob []byte, storeController storage.StoreController, +) IndexData { + indexData := IndexData{} + + indexData.IndexBlob = indexBlob + + return indexData +} + +// SetMetadataFromInput tries to set manifest metadata and update repo metadata by adding the current tag +// (in case the reference is a tag). The function expects image manifests and indexes (multi arch images). +func SetMetadataFromInput(repo, reference, mediaType string, digest godigest.Digest, descriptorBlob []byte, + storeController storage.StoreController, repoDB RepoDB, log log.Logger, +) error { + switch mediaType { + case ispec.MediaTypeImageManifest: + imageData, err := NewManifestData(repo, descriptorBlob, storeController) + if err != nil { + return err + } + + err = repoDB.SetManifestData(digest, imageData) + if err != nil { + log.Error().Err(err).Msg("repodb: error while putting manifest meta") + + return err + } + case ispec.MediaTypeImageIndex: + indexData := NewIndexData(repo, descriptorBlob, storeController) + + err := repoDB.SetIndexData(digest, indexData) + if err != nil { + log.Error().Err(err).Msg("repodb: error while putting index data") + + return err + } + } + + if refferenceIsDigest(reference) { + return nil + } + + err := repoDB.SetRepoTag(repo, reference, digest, mediaType) + if err != nil { + log.Error().Err(err).Msg("repodb: error while putting repo meta") + + return err + } + + return nil +} + +func refferenceIsDigest(reference string) bool { + _, err := godigest.Parse(reference) + + return err == nil +} diff --git a/pkg/meta/repodb/sync_repodb_test.go b/pkg/meta/repodb/sync_repodb_test.go index cce2f240..1191e098 100644 --- a/pkg/meta/repodb/sync_repodb_test.go +++ b/pkg/meta/repodb/sync_repodb_test.go @@ -298,10 +298,10 @@ func TestSyncRepoDBWithStorage(t *testing.T) { err = test.WriteImageToFileSystem( test.Image{ - Config: config, - Layers: layers, - Manifest: manifest, - Tag: fmt.Sprintf("tag%d", i), + Config: config, + Layers: layers, + Manifest: manifest, + Reference: fmt.Sprintf("tag%d", i), }, repo, storeController) @@ -322,10 +322,10 @@ func TestSyncRepoDBWithStorage(t *testing.T) { err = test.WriteImageToFileSystem( test.Image{ - Config: config, - Layers: layers, - Manifest: manifest, - Tag: signatureTag, + Config: config, + Layers: layers, + Manifest: manifest, + Reference: signatureTag, }, repo, storeController) @@ -398,10 +398,10 @@ func TestSyncRepoDBWithStorage(t *testing.T) { err = test.WriteImageToFileSystem( test.Image{ - Config: config, - Layers: layers, - Manifest: manifest, - Tag: "tag1", + Config: config, + Layers: layers, + Manifest: manifest, + Reference: "tag1", }, repo, storeController) @@ -420,10 +420,10 @@ func TestSyncRepoDBWithStorage(t *testing.T) { err = test.WriteImageToFileSystem( test.Image{ - Config: config, - Layers: layers, - Manifest: manifest, - Tag: signatureTag, + Config: config, + Layers: layers, + Manifest: manifest, + Reference: signatureTag, }, repo, storeController) @@ -470,10 +470,10 @@ func TestSyncRepoDBDynamoWrapper(t *testing.T) { err = test.WriteImageToFileSystem( test.Image{ - Config: config, - Layers: layers, - Manifest: manifest, - Tag: fmt.Sprintf("tag%d", i), + Config: config, + Layers: layers, + Manifest: manifest, + Reference: fmt.Sprintf("tag%d", i), }, repo, storeController) @@ -494,10 +494,10 @@ func TestSyncRepoDBDynamoWrapper(t *testing.T) { err = test.WriteImageToFileSystem( test.Image{ - Config: config, - Layers: layers, - Manifest: manifest, - Tag: signatureTag, + Config: config, + Layers: layers, + Manifest: manifest, + Reference: signatureTag, }, repo, storeController) @@ -531,6 +531,7 @@ func TestSyncRepoDBDynamoWrapper(t *testing.T) { Region: "us-east-2", RepoMetaTablename: "RepoMetadataTable", ManifestDataTablename: "ManifestDataTable", + IndexDataTablename: "IndexDataTable", VersionTablename: "Version", }) So(err, ShouldBeNil) @@ -580,10 +581,10 @@ func TestSyncRepoDBDynamoWrapper(t *testing.T) { err = test.WriteImageToFileSystem( test.Image{ - Config: config, - Layers: layers, - Manifest: manifest, - Tag: "tag1", + Config: config, + Layers: layers, + Manifest: manifest, + Reference: "tag1", }, repo, storeController) @@ -602,10 +603,10 @@ func TestSyncRepoDBDynamoWrapper(t *testing.T) { err = test.WriteImageToFileSystem( test.Image{ - Config: config, - Layers: layers, - Manifest: manifest, - Tag: signatureTag, + Config: config, + Layers: layers, + Manifest: manifest, + Reference: signatureTag, }, repo, storeController) @@ -617,6 +618,7 @@ func TestSyncRepoDBDynamoWrapper(t *testing.T) { Region: "us-east-2", RepoMetaTablename: "RepoMetadataTable", ManifestDataTablename: "ManifestDataTable", + IndexDataTablename: "IndexDataTable", VersionTablename: "Version", }) So(err, ShouldBeNil) diff --git a/pkg/meta/repodb/update/update.go b/pkg/meta/repodb/update/update.go index 497566c9..6b2fb154 100644 --- a/pkg/meta/repodb/update/update.go +++ b/pkg/meta/repodb/update/update.go @@ -51,7 +51,7 @@ func OnUpdateManifest(name, reference, mediaType string, digest godigest.Digest, metadataSuccessfullySet = false } } else { - err := SetMetadataFromInput(name, reference, mediaType, digest, body, + err := repodb.SetMetadataFromInput(name, reference, mediaType, digest, body, storeController, repoDB, log) if err != nil { metadataSuccessfullySet = false @@ -160,46 +160,3 @@ func OnGetManifest(name, reference string, digest godigest.Digest, body []byte, return nil } - -// SetMetadataFromInput receives raw information about the manifest pushed and tries to set manifest metadata -// and update repo metadata by adding the current tag (in case the reference is a tag). -// The function expects image manifest. -func SetMetadataFromInput(repo, reference, mediaType string, digest godigest.Digest, manifestBlob []byte, - storeController storage.StoreController, repoDB repodb.RepoDB, log log.Logger, -) error { - imageMetadata, err := repodb.NewManifestData(repo, manifestBlob, storeController) - if err != nil { - return err - } - - err = repoDB.SetManifestMeta(repo, digest, repodb.ManifestMetadata{ - ManifestBlob: imageMetadata.ManifestBlob, - ConfigBlob: imageMetadata.ConfigBlob, - DownloadCount: 0, - Signatures: repodb.ManifestSignatures{}, - }) - if err != nil { - log.Error().Err(err).Msg("repodb: error while putting image meta") - - return err - } - - if refferenceIsDigest(reference) { - return nil - } - - err = repoDB.SetRepoTag(repo, reference, digest, mediaType) - if err != nil { - log.Error().Err(err).Msg("repodb: error while putting repo meta") - - return err - } - - return nil -} - -func refferenceIsDigest(reference string) bool { - _, err := godigest.Parse(reference) - - return err == nil -} diff --git a/pkg/meta/repodb/update/update_test.go b/pkg/meta/repodb/update/update_test.go index 5f028b6a..53fd21b6 100644 --- a/pkg/meta/repodb/update/update_test.go +++ b/pkg/meta/repodb/update/update_test.go @@ -14,6 +14,7 @@ import ( zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/meta/repodb" bolt_wrapper "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper" repoDBUpdate "zotregistry.io/zot/pkg/meta/repodb/update" "zotregistry.io/zot/pkg/storage" @@ -42,8 +43,12 @@ func TestOnUpdateManifest(t *testing.T) { config, layers, manifest, err := test.GetRandomImageComponents(100) So(err, ShouldBeNil) - err = test.WriteImageToFileSystem(test.Image{Config: config, Manifest: manifest, Layers: layers, Tag: "tag1"}, - "repo", storeController) + err = test.WriteImageToFileSystem( + test.Image{ + Config: config, Manifest: manifest, Layers: layers, Reference: "tag1", + }, + "repo", + storeController) So(err, ShouldBeNil) manifestBlob, err := json.Marshal(manifest) @@ -59,6 +64,26 @@ func TestOnUpdateManifest(t *testing.T) { So(repoMeta.Tags, ShouldContainKey, "tag1") }) + + Convey("metadataSuccessfullySet is false", t, func() { + rootDir := t.TempDir() + storeController := storage.StoreController{} + log := log.NewLogger("debug", "") + metrics := monitoring.NewMetricsServer(false, log) + storeController.DefaultStore = local.NewImageStore(rootDir, true, 1*time.Second, + true, true, log, metrics, nil, nil, + ) + + repoDB := mocks.RepoDBMock{ + SetManifestDataFn: func(manifestDigest godigest.Digest, mm repodb.ManifestData) error { + return ErrTestError + }, + } + + err := repoDBUpdate.OnUpdateManifest("repo", "tag1", ispec.MediaTypeImageManifest, "digest", + []byte("{}"), storeController, repoDB, log) + So(err, ShouldNotBeNil) + }) } func TestUpdateErrors(t *testing.T) { @@ -160,8 +185,8 @@ func TestUpdateErrors(t *testing.T) { repoDB := mocks.RepoDBMock{} log := log.NewLogger("debug", "") - err := repoDBUpdate.SetMetadataFromInput("repo", "ref", "digest", "", []byte("BadManifestBlob"), - storeController, repoDB, log) + err := repodb.SetMetadataFromInput("repo", "ref", ispec.MediaTypeImageManifest, "digest", + []byte("BadManifestBlob"), storeController, repoDB, log) So(err, ShouldNotBeNil) // reference is digest @@ -177,7 +202,7 @@ func TestUpdateErrors(t *testing.T) { return []byte("{}"), nil } - err = repoDBUpdate.SetMetadataFromInput("repo", string(godigest.FromString("reference")), "", "digest", + err = repodb.SetMetadataFromInput("repo", string(godigest.FromString("reference")), "", "digest", manifestBlob, storeController, repoDB, log) So(err, ShouldBeNil) }) diff --git a/pkg/meta/repodb/version/version_test.go b/pkg/meta/repodb/version/version_test.go index dfa7f06e..ea401e08 100644 --- a/pkg/meta/repodb/version/version_test.go +++ b/pkg/meta/repodb/version/version_test.go @@ -119,6 +119,7 @@ func TestVersioningDynamoDB(t *testing.T) { Region: region, RepoMetaTablename: "RepoMetadataTable", ManifestDataTablename: "ManifestDataTable", + IndexDataTablename: "IndexDataTable", VersionTablename: "Version", }) So(err, ShouldBeNil) diff --git a/pkg/test/common.go b/pkg/test/common.go index acd950be..4c032b1d 100644 --- a/pkg/test/common.go +++ b/pkg/test/common.go @@ -45,6 +45,7 @@ import ( "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" + "zotregistry.io/zot/pkg/meta/repodb" "zotregistry.io/zot/pkg/storage" ) @@ -85,13 +86,47 @@ var ( ErrAlreadyExists = errors.New("already exists") ErrKeyNotFound = errors.New("key not found") ErrSignatureVerification = errors.New("signature verification failed") + ErrPutIndex = errors.New("can't put index") ) type Image struct { - Manifest ispec.Manifest - Config ispec.Image - Layers [][]byte - Tag string + Manifest ispec.Manifest + Config ispec.Image + Layers [][]byte + Reference string +} + +func (img Image) Digest() (godigest.Digest, error) { + blob, err := json.Marshal(img.Manifest) + if err != nil { + return "", err + } + + return godigest.FromBytes(blob), nil +} + +type MultiarchImage struct { + Index ispec.Index + Images []Image + Reference string +} + +func (mi *MultiarchImage) Digest() (godigest.Digest, error) { + indexBlob, err := json.Marshal(mi.Index) + if err != nil { + return "", err + } + + return godigest.FromBytes(indexBlob), nil +} + +func (mi *MultiarchImage) IndexData() (repodb.IndexData, error) { + indexBlob, err := json.Marshal(mi.Index) + if err != nil { + return repodb.IndexData{}, err + } + + return repodb.IndexData{IndexBlob: indexBlob}, nil } func GetFreePort() string { @@ -298,7 +333,7 @@ func WriteImageToFileSystem(image Image, repoName string, storeController storag return err } - _, err = store.PutImageManifest(repoName, image.Tag, ispec.MediaTypeImageManifest, manifestBlob) + _, err = store.PutImageManifest(repoName, image.Reference, ispec.MediaTypeImageManifest, manifestBlob) if err != nil { return err } @@ -306,6 +341,34 @@ func WriteImageToFileSystem(image Image, repoName string, storeController storag return nil } +func WriteMultiArchImageToFileSystem(multiarchImage MultiarchImage, repoName string, + storeController storage.StoreController, +) error { + store := storeController.GetImageStore(repoName) + + err := store.InitRepo(repoName) + if err != nil { + return err + } + + for _, image := range multiarchImage.Images { + err := WriteImageToFileSystem(image, repoName, storeController) + if err != nil { + return err + } + } + + indexBlob, err := json.Marshal(multiarchImage.Index) + if err != nil { + return err + } + + _, err = store.PutImageManifest(repoName, multiarchImage.Reference, ispec.MediaTypeImageIndex, + indexBlob) + + return err +} + func WaitTillServerReady(url string) { for { _, err := resty.R().Get(url) @@ -417,7 +480,7 @@ func GetOciLayoutDigests(imagePath string) (godigest.Digest, godigest.Digest, go oci, err := umoci.OpenLayout(imagePath) if err != nil { - panic(err) + panic(fmt.Errorf("error opening layout at '%s' : %w", imagePath, err)) } defer oci.Close() @@ -560,7 +623,7 @@ func GetRandomImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manif return config, layers, manifest, nil } -func GetImageWithConfig(conf ispec.Image) (ispec.Image, [][]byte, ispec.Manifest, error) { +func GetImageComponentsWithConfig(conf ispec.Image) (ispec.Image, [][]byte, ispec.Manifest, error) { configBlob, err := json.Marshal(conf) if err = Error(err); err != nil { return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err @@ -603,6 +666,68 @@ func GetImageWithConfig(conf ispec.Image) (ispec.Image, [][]byte, ispec.Manifest return conf, layers, manifest, nil } +func GetImageWithConfig(conf ispec.Image) (Image, error) { + config, layers, manifest, err := GetImageComponentsWithConfig(conf) + if err != nil { + return Image{}, err + } + + blob, err := json.Marshal(manifest) + if err != nil { + return Image{}, err + } + + return Image{ + Manifest: manifest, + Config: config, + Layers: layers, + Reference: godigest.FromBytes(blob).String(), + }, nil +} + +func GetImageWithComponents(config ispec.Image, layers [][]byte) (Image, error) { + configBlob, err := json.Marshal(config) + if err != nil { + return Image{}, err + } + + manifestLayers := make([]ispec.Descriptor, 0, len(layers)) + + for _, layer := range layers { + manifestLayers = append(manifestLayers, ispec.Descriptor{ + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(layer), + Size: int64(len(layer)), + }) + } + + const schemaVersion = 2 + + manifest := ispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: schemaVersion, + }, + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: godigest.FromBytes(configBlob), + Size: int64(len(configBlob)), + }, + Layers: manifestLayers, + } + + manifestBlob, err := json.Marshal(manifest) + if err != nil { + return Image{}, err + } + + return Image{ + Manifest: manifest, + Config: config, + Layers: layers, + Reference: godigest.FromBytes(manifestBlob).String(), + }, nil +} + func GetCosignSignatureTagForManifest(manifest ispec.Manifest) (string, error) { manifestBlob, err := json.Marshal(manifest) if err != nil { @@ -692,7 +817,11 @@ func UploadImage(img Image, baseURL, repo string) error { resp, err = resty.R(). SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). SetBody(manifestBlob). - Put(baseURL + "/v2/" + repo + "/manifests/" + img.Tag) + Put(baseURL + "/v2/" + repo + "/manifests/" + img.Reference) + + if ErrStatusCode(resp.StatusCode()) != http.StatusCreated { + return ErrPutBlob + } if ErrStatusCode(resp.StatusCode()) != http.StatusCreated { return ErrPutBlob @@ -756,10 +885,10 @@ func PushTestImage(repoName string, tag string, //nolint:unparam ) error { err := UploadImage( Image{ - Manifest: manifest, - Config: config, - Layers: layers, - Tag: tag, + Manifest: manifest, + Config: config, + Layers: layers, + Reference: tag, }, baseURL, repoName, @@ -1332,7 +1461,7 @@ func UploadImageWithBasicAuth(img Image, baseURL, repo, user, password string) e SetBasicAuth(user, password). SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). SetBody(manifestBlob). - Put(baseURL + "/v2/" + repo + "/manifests/" + img.Tag) + Put(baseURL + "/v2/" + repo + "/manifests/" + img.Reference) return err } @@ -1364,8 +1493,10 @@ func SignImageUsingCosign(repoTag, port string) error { imageURL := fmt.Sprintf("localhost:%s/%s", port, repoTag) + const timeoutPeriod = 5 + // sign the image - return sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute}, + return sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: timeoutPeriod * time.Minute}, options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass}, options.RegistryOptions{AllowInsecure: true}, map[string]interface{}{"tag": "1.0"}, @@ -1408,3 +1539,189 @@ func SignImageUsingNotary(repoTag, port string) error { return err } + +func GetRandomMultiarchImageComponents() (ispec.Index, []Image, error) { + const layerSize = 100 + + randomLayer1 := make([]byte, layerSize) + + _, err := rand.Read(randomLayer1) + if err != nil { + return ispec.Index{}, []Image{}, err + } + + image1, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + [][]byte{ + randomLayer1, + }) + if err != nil { + return ispec.Index{}, []Image{}, err + } + + image1.Reference = getManifestDigest(image1.Manifest).String() + + randomLayer2 := make([]byte, layerSize) + + _, err = rand.Read(randomLayer2) + if err != nil { + return ispec.Index{}, []Image{}, err + } + + image2, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "linux", + Architecture: "386", + }, + }, + [][]byte{ + randomLayer2, + }) + if err != nil { + return ispec.Index{}, []Image{}, err + } + + image2.Reference = getManifestDigest(image2.Manifest).String() + + randomLayer3 := make([]byte, layerSize) + + _, err = rand.Read(randomLayer3) + if err != nil { + return ispec.Index{}, []Image{}, err + } + + image3, err := GetImageWithComponents( + ispec.Image{ + Platform: ispec.Platform{ + OS: "windows", + Architecture: "amd64", + }, + }, + [][]byte{ + randomLayer3, + }) + if err != nil { + return ispec.Index{}, []Image{}, err + } + + image3.Reference = getManifestDigest(image3.Manifest).String() + + index := ispec.Index{ + MediaType: ispec.MediaTypeImageIndex, + Manifests: []ispec.Descriptor{ + { + MediaType: ispec.MediaTypeImageManifest, + Digest: getManifestDigest(image1.Manifest), + Size: getManifestSize(image1.Manifest), + }, + { + MediaType: ispec.MediaTypeImageManifest, + Digest: getManifestDigest(image2.Manifest), + Size: getManifestSize(image2.Manifest), + }, + { + MediaType: ispec.MediaTypeImageManifest, + Digest: getManifestDigest(image3.Manifest), + Size: getManifestSize(image3.Manifest), + }, + }, + } + + return index, []Image{image1, image2, image3}, nil +} + +func GetRandomMultiarchImage(reference string) (MultiarchImage, error) { + index, images, err := GetRandomMultiarchImageComponents() + if err != nil { + return MultiarchImage{}, err + } + + return MultiarchImage{ + Index: index, Images: images, Reference: reference, + }, err +} + +func GetMultiarchImageForImages(reference string, images []Image) MultiarchImage { + var index ispec.Index + + for i, image := range images { + index.Manifests = append(index.Manifests, ispec.Descriptor{ + MediaType: ispec.MediaTypeImageManifest, + Digest: getManifestDigest(image.Manifest), + Size: getManifestSize(image.Manifest), + }) + + // update the reference with the digest of the manifest + images[i].Reference = getManifestDigest(image.Manifest).String() + } + + return MultiarchImage{Index: index, Images: images, Reference: reference} +} + +func getManifestSize(manifest ispec.Manifest) int64 { + manifestBlob, err := json.Marshal(manifest) + if err != nil { + return 0 + } + + return int64(len(manifestBlob)) +} + +func getManifestDigest(manifest ispec.Manifest) godigest.Digest { + manifestBlob, err := json.Marshal(manifest) + if err != nil { + return "" + } + + return godigest.FromBytes(manifestBlob) +} + +func UploadMultiarchImage(multiImage MultiarchImage, baseURL string, repo string) error { + for _, image := range multiImage.Images { + err := UploadImage(image, baseURL, repo) + if err != nil { + return err + } + } + + // put manifest + indexBlob, err := json.Marshal(multiImage.Index) + if err = Error(err); err != nil { + return err + } + + resp, err := resty.R(). + SetHeader("Content-type", ispec.MediaTypeImageIndex). + SetBody(indexBlob). + Put(baseURL + "/v2/" + repo + "/manifests/" + multiImage.Reference) + + if resp.StatusCode() != http.StatusCreated { + return ErrPutIndex + } + + return err +} + +func GetIndexBlobWithManifests(manifestDigests []godigest.Digest) ([]byte, error) { + manifests := make([]ispec.Descriptor, 0, len(manifestDigests)) + + for _, manifestDigest := range manifestDigests { + manifests = append(manifests, ispec.Descriptor{ + Digest: manifestDigest, + MediaType: ispec.MediaTypeImageManifest, + }) + } + + indexContent := ispec.Index{ + MediaType: ispec.MediaTypeImageIndex, + Manifests: manifests, + } + + return json.Marshal(indexContent) +} diff --git a/pkg/test/common_test.go b/pkg/test/common_test.go index b922bda1..9223bfc5 100644 --- a/pkg/test/common_test.go +++ b/pkg/test/common_test.go @@ -1061,10 +1061,10 @@ func TestVerifyWithNotation(t *testing.T) { err = test.UploadImage( test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - Tag: tag, + Config: cfg, + Layers: layers, + Manifest: manifest, + Reference: tag, }, baseURL, repoName) So(err, ShouldBeNil) diff --git a/pkg/test/mocks/cve_mock.go b/pkg/test/mocks/cve_mock.go index 1931017d..8223648f 100644 --- a/pkg/test/mocks/cve_mock.go +++ b/pkg/test/mocks/cve_mock.go @@ -9,10 +9,12 @@ import ( type CveInfoMock struct { GetImageListForCVEFn func(repo, cveID string) ([]common.TagInfo, error) GetImageListWithCVEFixedFn func(repo, cveID string) ([]common.TagInfo, error) - GetCVEListForImageFn func(image string, pageInput cveinfo.PageInput) ([]cvemodel.CVE, cveinfo.PageInfo, error) - GetCVESummaryForImageFn func(image string) (cveinfo.ImageCVESummary, error) - CompareSeveritiesFn func(severity1, severity2 string) int - UpdateDBFn func() error + GetCVEListForImageFn func(repo string, reference string, pageInput cveinfo.PageInput, + ) ([]cvemodel.CVE, cveinfo.PageInfo, error) + GetCVESummaryForImageFn func(repo string, reference string, + ) (cveinfo.ImageCVESummary, error) + CompareSeveritiesFn func(severity1, severity2 string) int + UpdateDBFn func() error } func (cveInfo CveInfoMock) GetImageListForCVE(repo, cveID string) ([]common.TagInfo, error) { @@ -31,21 +33,22 @@ func (cveInfo CveInfoMock) GetImageListWithCVEFixed(repo, cveID string) ([]commo return []common.TagInfo{}, nil } -func (cveInfo CveInfoMock) GetCVEListForImage(image string, pageInput cveinfo.PageInput) ( +func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string, pageInput cveinfo.PageInput) ( []cvemodel.CVE, cveinfo.PageInfo, error, ) { if cveInfo.GetCVEListForImageFn != nil { - return cveInfo.GetCVEListForImageFn(image, pageInput) + return cveInfo.GetCVEListForImageFn(repo, reference, pageInput) } return []cvemodel.CVE{}, cveinfo.PageInfo{}, nil } -func (cveInfo CveInfoMock) GetCVESummaryForImage(image string) (cveinfo.ImageCVESummary, error) { +func (cveInfo CveInfoMock) GetCVESummaryForImage(repo string, reference string, +) (cveinfo.ImageCVESummary, error) { if cveInfo.GetCVESummaryForImageFn != nil { - return cveInfo.GetCVESummaryForImageFn(image) + return cveInfo.GetCVESummaryForImageFn(repo, reference) } return cveinfo.ImageCVESummary{}, nil @@ -68,15 +71,15 @@ func (cveInfo CveInfoMock) UpdateDB() error { } type CveScannerMock struct { - IsImageFormatScannableFn func(image string) (bool, error) + IsImageFormatScannableFn func(repo string, reference string) (bool, error) ScanImageFn func(image string) (map[string]cvemodel.CVE, error) CompareSeveritiesFn func(severity1, severity2 string) int UpdateDBFn func() error } -func (scanner CveScannerMock) IsImageFormatScannable(image string) (bool, error) { +func (scanner CveScannerMock) IsImageFormatScannable(repo string, reference string) (bool, error) { if scanner.IsImageFormatScannableFn != nil { - return scanner.IsImageFormatScannableFn(image) + return scanner.IsImageFormatScannableFn(repo, reference) } return true, nil diff --git a/pkg/test/mocks/repo_db_mock.go b/pkg/test/mocks/repo_db_mock.go index c6c7559d..73ebdde3 100644 --- a/pkg/test/mocks/repo_db_mock.go +++ b/pkg/test/mocks/repo_db_mock.go @@ -36,6 +36,10 @@ type RepoDBMock struct { SetManifestMetaFn func(repo string, manifestDigest godigest.Digest, mm repodb.ManifestMetadata) error + SetIndexDataFn func(digest godigest.Digest, indexData repodb.IndexData) error + + GetIndexDataFn func(indexDigest godigest.Digest) (repodb.IndexData, error) + IncrementImageDownloadsFn func(repo string, reference string) error AddManifestSignatureFn func(repo string, signedManifestDigest godigest.Digest, sm repodb.SignatureMetadata) error @@ -43,14 +47,14 @@ type RepoDBMock struct { DeleteSignatureFn func(repo string, signedManifestDigest godigest.Digest, sm repodb.SignatureMetadata) error SearchReposFn func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput) ( - []repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) + []repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) SearchTagsFn func(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput) ( - []repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) + []repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) FilterTagsFn func(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, - ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) + ) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) SearchDigestsFn func(ctx context.Context, searchText string, requestedPage repodb.PageInput) ( []repodb.RepoMetadata, map[string]repodb.ManifestMetadata, error) @@ -143,7 +147,7 @@ func (sdm RepoDBMock) GetMultipleRepoMeta(ctx context.Context, filter func(repoM func (sdm RepoDBMock) GetManifestData(manifestDigest godigest.Digest) (repodb.ManifestData, error) { if sdm.GetManifestDataFn != nil { - return sdm.GetManifestData(manifestDigest) + return sdm.GetManifestDataFn(manifestDigest) } return repodb.ManifestData{}, nil @@ -151,7 +155,7 @@ func (sdm RepoDBMock) GetManifestData(manifestDigest godigest.Digest) (repodb.Ma func (sdm RepoDBMock) SetManifestData(manifestDigest godigest.Digest, md repodb.ManifestData) error { if sdm.SetManifestDataFn != nil { - return sdm.SetManifestData(manifestDigest, md) + return sdm.SetManifestDataFn(manifestDigest, md) } return nil @@ -203,32 +207,35 @@ func (sdm RepoDBMock) DeleteSignature(repo string, signedManifestDigest godigest func (sdm RepoDBMock) SearchRepos(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { if sdm.SearchReposFn != nil { return sdm.SearchReposFn(ctx, searchText, filter, requestedPage) } - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, nil + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, + map[string]repodb.IndexData{}, repodb.PageInfo{}, nil } func (sdm RepoDBMock) SearchTags(ctx context.Context, searchText string, filter repodb.Filter, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { if sdm.SearchTagsFn != nil { return sdm.SearchTagsFn(ctx, searchText, filter, requestedPage) } - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, nil + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, + map[string]repodb.IndexData{}, repodb.PageInfo{}, nil } func (sdm RepoDBMock) FilterTags(ctx context.Context, filter repodb.FilterFunc, requestedPage repodb.PageInput, -) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, repodb.PageInfo, error) { +) ([]repodb.RepoMetadata, map[string]repodb.ManifestMetadata, map[string]repodb.IndexData, repodb.PageInfo, error) { if sdm.FilterTagsFn != nil { return sdm.FilterTagsFn(ctx, filter, requestedPage) } - return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, repodb.PageInfo{}, nil + return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, + map[string]repodb.IndexData{}, repodb.PageInfo{}, nil } func (sdm RepoDBMock) SearchDigests(ctx context.Context, searchText string, requestedPage repodb.PageInput, @@ -268,6 +275,22 @@ func (sdm RepoDBMock) SearchForDescendantImages(ctx context.Context, searchText return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, nil } +func (sdm RepoDBMock) SetIndexData(digest godigest.Digest, indexData repodb.IndexData) error { + if sdm.SetIndexDataFn != nil { + return sdm.SetIndexDataFn(digest, indexData) + } + + return nil +} + +func (sdm RepoDBMock) GetIndexData(indexDigest godigest.Digest) (repodb.IndexData, error) { + if sdm.GetIndexDataFn != nil { + return sdm.GetIndexDataFn(indexDigest) + } + + return repodb.IndexData{}, nil +} + func (sdm RepoDBMock) PatchDB() error { if sdm.PatchDBFn != nil { return sdm.PatchDBFn() diff --git a/test/blackbox/annotations.bats b/test/blackbox/annotations.bats index 8b58d3db..726f9a88 100644 --- a/test/blackbox/annotations.bats +++ b/test/blackbox/annotations.bats @@ -74,7 +74,8 @@ function teardown_file() { [ "$status" -eq 0 ] run podman push 127.0.0.1:8080/annotations:latest --tls-verify=false --format=oci [ "$status" -eq 0 ] - run curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ ImageList(repo: \"annotations\") { Results { RepoName Tag Digest ConfigDigest Size Layers {Size Digest } Vendor Licenses }}}"}' http://localhost:8080/v2/_zot/ext/search + run curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ ImageList(repo: \"annotations\") { Results { RepoName Tag Manifests {Digest ConfigDigest Size Layers { Size Digest }} Vendor Licenses }}}"}' http://localhost:8080/v2/_zot/ext/search + [ "$status" -eq 0 ] # [ $(echo "${lines[-1]}" | jq '.data.ImageList') ] [ $(echo "${lines[-1]}" | jq '.data.ImageList.Results[0].RepoName') = '"annotations"' ] @@ -87,7 +88,7 @@ function teardown_file() { [ "$status" -eq 0 ] run stacker --oci-dir ${BATS_FILE_TMPDIR}/stackeroci --stacker-dir ${BATS_FILE_TMPDIR}/.stacker --roots-dir ${BATS_FILE_TMPDIR}/roots publish -f ${BATS_FILE_TMPDIR}/stacker.yaml --substitute IMAGE_NAME="ghcr.io/project-zot/golang" --substitute IMAGE_TAG="1.20" --substitute DESCRIPTION="mydesc" --substitute VENDOR="CentOs" --substitute LICENSES="GPLv2" --url docker://127.0.0.1:8080 --tag 1.20 --skip-tls [ "$status" -eq 0 ] - run curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ ImageList(repo: \"ghcr.io/project-zot/golang\") { Results { RepoName Tag Digest ConfigDigest Size Layers {Size Digest } Description Vendor Licenses }}}"}' http://localhost:8080/v2/_zot/ext/search + run curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ ImageList(repo: \"ghcr.io/project-zot/golang\") { Results { RepoName Tag Manifests {Digest ConfigDigest Size Layers { Size Digest }} Vendor Licenses Description }}}"}' http://localhost:8080/v2/_zot/ext/search [ "$status" -eq 0 ] [ $(echo "${lines[-1]}" | jq '.data.ImageList.Results[0].RepoName') = '"ghcr.io/project-zot/golang"' ] [ $(echo "${lines[-1]}" | jq '.data.ImageList.Results[0].Description') = '"mydesc"' ] @@ -96,10 +97,10 @@ function teardown_file() { } @test "sign/verify with cosign" { - run curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ ImageList(repo: \"annotations\") { Results { RepoName Tag Digest ConfigDigest Size Layers {Size Digest } Vendor Licenses }}}"}' http://localhost:8080/v2/_zot/ext/search + run curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ ImageList(repo: \"annotations\") { Results { RepoName Tag Manifests {Digest ConfigDigest Size Layers { Size Digest }} Vendor Licenses }}}"}' http://localhost:8080/v2/_zot/ext/search [ "$status" -eq 0 ] [ $(echo "${lines[-1]}" | jq '.data.ImageList.Results[0].RepoName') = '"annotations"' ] - local digest=$(echo "${lines[-1]}" | jq -r '.data.ImageList.Results[0].Digest') + local digest=$(echo "${lines[-1]}" | jq -r '.data.ImageList.Results[0].Manifests[0].Digest') run cosign initialize [ "$status" -eq 0 ] @@ -115,7 +116,7 @@ function teardown_file() { } @test "sign/verify with notation" { - run curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ ImageList(repo: \"annotations\") { Results { RepoName Tag Digest ConfigDigest Size Layers {Size Digest } }}}"}' http://localhost:8080/v2/_zot/ext/search + run curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ ImageList(repo: \"annotations\") { Results { RepoName Tag Manifests {Digest ConfigDigest Size Layers { Size Digest }} Vendor Licenses }}}"}' http://localhost:8080/v2/_zot/ext/search [ "$status" -eq 0 ] [ $(echo "${lines[-1]}" | jq '.data.ImageList.Results[0].RepoName') = '"annotations"' ] [ "$status" -eq 0 ] diff --git a/test/blackbox/cloud-only.bats b/test/blackbox/cloud-only.bats index 5d05e55b..e219db3d 100644 --- a/test/blackbox/cloud-only.bats +++ b/test/blackbox/cloud-only.bats @@ -37,6 +37,7 @@ function setup() { "cacheTablename": "BlobTable", "repoMetaTablename": "RepoMetadataTable", "manifestDataTablename": "ManifestDataTable", + "indexDataTablename": "IndexDataTable", "versionTablename": "Version" } }, @@ -66,8 +67,6 @@ function setup() { EOF awslocal s3 --region "us-east-2" mb s3://zot-storage awslocal dynamodb --region "us-east-2" create-table --table-name "BlobTable" --attribute-definitions AttributeName=Digest,AttributeType=S --key-schema AttributeName=Digest,KeyType=HASH --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=5 - awslocal dynamodb --region "us-east-2" create-table --table-name "RepoMetadataTable" --attribute-definitions AttributeName=RepoName,AttributeType=S --key-schema AttributeName=RepoName,KeyType=HASH --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=5 - awslocal dynamodb --region "us-east-2" create-table --table-name "ManifestDataTable" --attribute-definitions AttributeName=Digest,AttributeType=S --key-schema AttributeName=Digest,KeyType=HASH --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=5 zot_serve_strace ${zot_config_file} wait_zot_reachable "http://127.0.0.1:8080/v2/_catalog" }