//go:build search // +build search package search_test import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "path" "strconv" "strings" "testing" "time" guuid "github.com/gofrs/uuid" regTypes "github.com/google/go-containerregistry/pkg/v1/types" notreg "github.com/notaryproject/notation-go/registry" 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" "gopkg.in/resty.v1" zerr "zotregistry.dev/zot/errors" "zotregistry.dev/zot/pkg/api" "zotregistry.dev/zot/pkg/api/config" "zotregistry.dev/zot/pkg/api/constants" zcommon "zotregistry.dev/zot/pkg/common" extconf "zotregistry.dev/zot/pkg/extensions/config" "zotregistry.dev/zot/pkg/extensions/monitoring" cveinfo "zotregistry.dev/zot/pkg/extensions/search/cve" cvemodel "zotregistry.dev/zot/pkg/extensions/search/cve/model" "zotregistry.dev/zot/pkg/log" mTypes "zotregistry.dev/zot/pkg/meta/types" "zotregistry.dev/zot/pkg/storage" "zotregistry.dev/zot/pkg/storage/local" storageTypes "zotregistry.dev/zot/pkg/storage/types" . "zotregistry.dev/zot/pkg/test/common" . "zotregistry.dev/zot/pkg/test/image-utils" "zotregistry.dev/zot/pkg/test/mocks" ociutils "zotregistry.dev/zot/pkg/test/oci-utils" "zotregistry.dev/zot/pkg/test/signature" tskip "zotregistry.dev/zot/pkg/test/skip" ) const ( graphqlQueryPrefix = constants.FullSearchPrefix DBFileName = "meta.db" ) var ( ErrTestError = errors.New("test error") ErrPutManifest = errors.New("can't put manifest") ) func readFileAndSearchString(filePath string, stringToMatch string, timeout time.Duration) (bool, error) { ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) defer cancelFunc() for { select { case <-ctx.Done(): return false, nil default: content, err := os.ReadFile(filePath) if err != nil { return false, err } if strings.Contains(string(content), stringToMatch) { return true, nil } } } } func verifyRepoSummaryFields(t *testing.T, actualRepoSummary, expectedRepoSummary *zcommon.RepoSummary, ) { t.Helper() t.Logf("Verify RepoSummary \n%v \nmatches fields of \n%v", actualRepoSummary, expectedRepoSummary, ) So(actualRepoSummary.Name, ShouldEqual, expectedRepoSummary.Name) So(actualRepoSummary.LastUpdated, ShouldEqual, expectedRepoSummary.LastUpdated) So(actualRepoSummary.Size, ShouldEqual, expectedRepoSummary.Size) So(len(actualRepoSummary.Vendors), ShouldEqual, len(expectedRepoSummary.Vendors)) for index, vendor := range actualRepoSummary.Vendors { So(vendor, ShouldEqual, expectedRepoSummary.Vendors[index]) } So(len(actualRepoSummary.Platforms), ShouldEqual, len(expectedRepoSummary.Platforms)) for index, platform := range actualRepoSummary.Platforms { So(platform.Os, ShouldEqual, expectedRepoSummary.Platforms[index].Os) So(platform.Arch, ShouldEqual, expectedRepoSummary.Platforms[index].Arch) } So(actualRepoSummary.NewestImage.Tag, ShouldEqual, expectedRepoSummary.NewestImage.Tag) verifyImageSummaryFields(t, &actualRepoSummary.NewestImage, &expectedRepoSummary.NewestImage) } func verifyImageSummaryFields(t *testing.T, actualImageSummary, expectedImageSummary *zcommon.ImageSummary, ) { t.Helper() t.Logf("Verify ImageSummary \n%v \nmatches fields of \n%v", actualImageSummary, expectedImageSummary, ) So(actualImageSummary.Tag, ShouldEqual, expectedImageSummary.Tag) So(actualImageSummary.LastUpdated, ShouldEqual, expectedImageSummary.LastUpdated) So(actualImageSummary.Size, ShouldEqual, expectedImageSummary.Size) So(actualImageSummary.IsSigned, ShouldEqual, expectedImageSummary.IsSigned) So(actualImageSummary.Vendor, ShouldEqual, expectedImageSummary.Vendor) 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.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, ) } } } func uploadNewRepoTag(tag string, repoName string, baseURL string, layers [][]byte) error { created := time.Now() config := ispec.Image{ Created: &created, Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", } configBlob, err := json.Marshal(config) So(err, ShouldBeNil) configDigest := godigest.FromBytes(configBlob) 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, }, baseURL, repoName, tag, ) return err } func getMockCveScanner(metaDB mTypes.MetaDB) cveinfo.Scanner { // MetaDB loaded with initial data, mock the scanner // Setup test CVE data in mock scanner getCveResults := func(image string) map[string]cvemodel.CVE { if image == "zot-cve-test:0.0.1" || image == "a/zot-cve-test:0.0.1" || image == "zot-test:0.0.1" || image == "a/zot-test:0.0.1" || strings.Contains(image, "sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") || strings.Contains(image, "sha256:db573b0146a853af339bde42256a810b911d89c9252d055e0218de53690e031e") || strings.Contains(image, "sha256:0d8d7cae58478b43cde65d4fd495e1b4ab3f6404450b17e75d7e04eb2040d297") { return map[string]cvemodel.CVE{ "CVE1": { ID: "CVE1", Severity: "MEDIUM", Title: "Title CVE1", Description: "Description CVE1", }, "CVE2": { ID: "CVE2", Severity: "HIGH", Title: "Title CVE2", Description: "Description CVE2", }, "CVE3": { ID: "CVE3", Severity: "LOW", Title: "Title CVE3", Description: "Description CVE3", }, "CVE4": { ID: "CVE4", Severity: "CRITICAL", Title: "Title CVE4", Description: "Description CVE4", }, } } if image == "test-repo:latest" || strings.Contains(image, "sha256:9f8e1a125c4fb03a0f157d75999b73284ccc5cba18eb772e4643e3499343607e") || strings.Contains(image, "sha256:5c6e8dd5dce1c488fc79d02690b11ff7a190fad21885297101dbd531f0db3597") { return map[string]cvemodel.CVE{ "CVE1": { ID: "CVE1", Severity: "MEDIUM", Title: "Title CVE1", Description: "Description CVE1", }, "CVE2": { ID: "CVE2", Severity: "HIGH", Title: "Title CVE2", Description: "Description CVE2", }, "CVE3": { ID: "CVE3", Severity: "LOW", Title: "Title CVE3", Description: "Description CVE3", }, "CVE4": { ID: "CVE4", Severity: "CRITICAL", Title: "Title CVE4", Description: "Description CVE4", }, } } // By default the image has no vulnerabilities return map[string]cvemodel.CVE{} } scanner := mocks.CveScannerMock{ ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { return getCveResults(image), nil }, GetCachedResultFn: func(digestStr string) map[string]cvemodel.CVE { return getCveResults(digestStr) }, IsResultCachedFn: func(digestStr string) bool { return true }, IsImageFormatScannableFn: func(repo string, reference string) (bool, error) { // Almost same logic compared to actual Trivy specific implementation imageDir := repo inputTag := reference repoMeta, err := metaDB.GetRepoMeta(context.Background(), imageDir) if err != nil { return false, err } manifestDigestStr := reference if zcommon.IsTag(reference) { var ok bool descriptor, ok := repoMeta.Tags[inputTag] if !ok { return false, zerr.ErrTagMetaNotFound } manifestDigestStr = descriptor.Digest } manifestDigest, err := godigest.Parse(manifestDigestStr) if err != nil { return false, err } manifestData, err := metaDB.GetImageMeta(manifestDigest) if err != nil { return false, err } for _, imageLayer := range manifestData.Manifests[0].Manifest.Layers { switch imageLayer.MediaType { case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer): return true, nil default: return false, zerr.ErrScanNotSupported } } return false, nil }, } return &scanner } func TestRepoListWithNewestImage(t *testing.T) { Convey("Test repoListWithNewestImage by tag with HTTP", t, func() { subpath := "/a" port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port rootDir := t.TempDir() subRootDir := t.TempDir() conf.Storage.RootDirectory = rootDir conf.Storage.SubPaths = make(map[string]config.StorageConfig) conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir} defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() uploadedImage := CreateImageWith().RandomLayers(1, 100).DefaultConfig().Build() err := UploadImage(uploadedImage, baseURL, "zot-cve-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(uploadedImage, baseURL, "a/zot-cve-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(uploadedImage, baseURL, "zot-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(uploadedImage, baseURL, "a/zot-test", "0.0.1") So(err, ShouldBeNil) resp, err := resty.R().Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) resp, err = resty.R().Get(baseURL + graphqlQueryPrefix) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 422) Convey("Test repoListWithNewestImage with pagination", func() { query := `{ RepoListWithNewestImage(requestedPage:{ limit: 2 offset: 0 sortBy: UPDATE_TIME }){ Page{ ItemCount TotalCount } Results{ Name NewestImage{ Tag } } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) var responseStruct zcommon.RepoWithNewestImageResponse err = json.Unmarshal(resp.Body(), &responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Results), ShouldEqual, 2) So(responseStruct.Page.ItemCount, ShouldEqual, 2) So(responseStruct.Page.TotalCount, ShouldEqual, 4) }) Convey("Test repoListWithNewestImage with pagination, no limit or offset", func() { query := `{ RepoListWithNewestImage(requestedPage:{ limit: 0 offset: 0 sortBy: UPDATE_TIME }){ Page{ ItemCount TotalCount } Results{ Name NewestImage{ Tag } } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) var responseStruct zcommon.RepoWithNewestImageResponse err = json.Unmarshal(resp.Body(), &responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Results), ShouldEqual, 4) So(responseStruct.Page.ItemCount, ShouldEqual, 4) So(responseStruct.Page.TotalCount, ShouldEqual, 4) }) Convey("Test repoListWithNewestImage multiple", func() { query := `{RepoListWithNewestImage{ Results{ Name NewestImage{ Tag } } }}` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) var responseStruct zcommon.RepoWithNewestImageResponse err = json.Unmarshal(resp.Body(), &responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Results), ShouldEqual, 4) images := responseStruct.Results So(images[0].NewestImage.Tag, ShouldEqual, "0.0.1") query = `{ RepoListWithNewestImage(requestedPage: { limit: 1 offset: 0 sortBy: UPDATE_TIME }){ Results{ Name NewestImage{ Tag } } } }` 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.Results), ShouldEqual, 1) repos := responseStruct.Results So(repos[0].NewestImage.Tag, ShouldEqual, "0.0.1") query = `{ RepoListWithNewestImage{ Results{ Name NewestImage{ Tag Vulnerabilities{ MaxSeverity UnknownCount LowCount MediumCount HighCount CriticalCount Count } } } } }` // Verify we don't return any vulnerabilities if CVE scanning is disabled 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.Results), ShouldEqual, 4) images = responseStruct.Results So(images[0].NewestImage.Tag, ShouldEqual, "0.0.1") So(images[0].NewestImage.Vulnerabilities.Count, ShouldEqual, 0) So(images[0].NewestImage.Vulnerabilities.UnknownCount, ShouldEqual, 0) So(images[0].NewestImage.Vulnerabilities.LowCount, ShouldEqual, 0) So(images[0].NewestImage.Vulnerabilities.MediumCount, ShouldEqual, 0) So(images[0].NewestImage.Vulnerabilities.HighCount, ShouldEqual, 0) So(images[0].NewestImage.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(images[0].NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "") query = `{ RepoListWithNewestImage{ Results{ Name NewestImage{ Tag } } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) err = os.Chmod(rootDir, 0o000) if err != nil { panic(err) } 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(responseStruct.Errors, ShouldBeNil) // Even if permissions fail data is coming from the DB err = os.Chmod(rootDir, 0o755) if err != nil { panic(err) } manifestDigest := uploadedImage.ManifestDescriptor.Digest configDigest := uploadedImage.ConfigDescriptor.Digest // Delete config blob and try. err = os.Remove(path.Join(subRootDir, "a/zot-test/blobs/sha256", configDigest.Encoded())) if err != nil { panic(err) } resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) err = os.Remove(path.Join(subRootDir, "a/zot-test/blobs/sha256", manifestDigest.Encoded())) if err != nil { panic(err) } resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) err = os.Remove(path.Join(rootDir, "zot-test/blobs/sha256", configDigest.Encoded())) if err != nil { panic(err) } resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) // Delete manifest blob also and try err = os.Remove(path.Join(rootDir, "zot-test/blobs/sha256", manifestDigest.Encoded())) if err != nil { panic(err) } resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) }) Convey("Test repoListWithNewestImage with vulnerability scan enabled", t, func() { subpath := "/a" port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port rootDir := t.TempDir() subRootDir := t.TempDir() conf.Storage.RootDirectory = rootDir conf.Storage.SubPaths = make(map[string]config.StorageConfig) conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir} defaultVal := true updateDuration, _ := time.ParseDuration("1h") trivyConfig := &extconf.TrivyConfig{ DBRepository: "ghcr.io/project-zot/trivy-db", } cveConfig := &extconf.CVEConfig{ UpdateInterval: updateDuration, Trivy: trivyConfig, } searchConfig := &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, CVE: cveConfig, } conf.Extensions = &extconf.ExtensionConfig{ Search: searchConfig, } // we won't use the logging config feature as we want logs in both // stdout and a file logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") So(err, ShouldBeNil) logPath := logFile.Name() defer os.Remove(logPath) writers := io.MultiWriter(os.Stdout, logFile) ctlr := api.NewController(conf) ctlr.Log.Logger = ctlr.Log.Output(writers) if err := ctlr.Init(); err != nil { panic(err) } ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB) go func() { if err := ctlr.Run(); !errors.Is(err, http.ErrServerClosed) { panic(err) } }() defer ctlr.Shutdown() substring := "{\"Search\":{\"Enable\":true,\"CVE\":{\"UpdateInterval\":3600000000000," + "\"Trivy\":{\"DBRepository\":\"ghcr.io/project-zot/trivy-db\",\"JavaDBRepository\":\"\"}}}" found, err := readFileAndSearchString(logPath, substring, 2*time.Minute) So(found, ShouldBeTrue) So(err, ShouldBeNil) found, err = readFileAndSearchString(logPath, "updating cve-db", 2*time.Minute) So(found, ShouldBeTrue) So(err, ShouldBeNil) found, err = readFileAndSearchString(logPath, "cve-db update completed, next update scheduled after interval", 4*time.Minute) So(found, ShouldBeTrue) So(err, ShouldBeNil) WaitTillServerReady(baseURL) resp, err := resty.R().Get(baseURL + graphqlQueryPrefix) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 422) uploadedImage := CreateDefaultImage() err = UploadImage(uploadedImage, baseURL, "zot-cve-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(uploadedImage, baseURL, "a/zot-cve-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(uploadedImage, baseURL, "zot-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(uploadedImage, baseURL, "a/zot-test", "0.0.1") So(err, ShouldBeNil) query := `{ RepoListWithNewestImage{ Results{ Name NewestImage{ Tag Digest Vulnerabilities{ MaxSeverity UnknownCount LowCount MediumCount HighCount CriticalCount Count } } } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) var responseStruct zcommon.RepoWithNewestImageResponse err = json.Unmarshal(resp.Body(), &responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Results), ShouldEqual, 4) repos := responseStruct.Results So(repos[0].NewestImage.Tag, ShouldEqual, "0.0.1") for _, repo := range repos { vulnerabilities := repo.NewestImage.Vulnerabilities So(vulnerabilities, ShouldNotBeNil) t.Logf("Found vulnerability summary %v", vulnerabilities) // Depends on test data, but current tested images contain hundreds So(vulnerabilities.Count, ShouldEqual, 4) So(vulnerabilities.UnknownCount, ShouldEqual, 0) So(vulnerabilities.LowCount, ShouldEqual, 1) So(vulnerabilities.MediumCount, ShouldEqual, 1) So(vulnerabilities.HighCount, ShouldEqual, 1) So(vulnerabilities.CriticalCount, ShouldEqual, 1) So(vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") } }) } func TestGetReferrersGQL(t *testing.T) { Convey("get referrers", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = t.TempDir() defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, Lint: &extconf.LintConfig{ BaseConfig: extconf.BaseConfig{ Enable: &defaultVal, }, }, } gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, graphqlQueryPrefix) conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() // ======================= uploadedImage := CreateImageWith().RandomLayers(1, 1000).DefaultConfig().Build() repo := "artifact-ref" err := UploadImage(uploadedImage, baseURL, repo, "1.0") So(err, ShouldBeNil) manifestDigest := uploadedImage.ManifestDescriptor.Digest manifestSize := uploadedImage.ManifestDescriptor.Size subjectDescriptor := &ispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Size: manifestSize, Digest: manifestDigest, } artifactContentBlob := []byte("test artifact") artifactContentBlobSize := int64(len(artifactContentBlob)) artifactContentType := "application/octet-stream" artifactContentBlobDigest := godigest.FromBytes(artifactContentBlob) artifactType := "com.artifact.test/type1" artifactImg := Image{ Manifest: ispec.Manifest{ Layers: []ispec.Descriptor{ { MediaType: artifactContentType, Digest: artifactContentBlobDigest, Size: artifactContentBlobSize, }, }, Subject: subjectDescriptor, ArtifactType: artifactType, Config: ispec.Descriptor{ MediaType: ispec.MediaTypeEmptyJSON, Digest: ispec.DescriptorEmptyJSON.Digest, Data: ispec.DescriptorEmptyJSON.Data, }, MediaType: ispec.MediaTypeImageManifest, Annotations: map[string]string{ "com.artifact.format": "test", }, }, Config: ispec.Image{}, Layers: [][]byte{artifactContentBlob}, } artifactImg.Manifest.SchemaVersion = 2 artifactManifestBlob, err := json.Marshal(artifactImg.Manifest) So(err, ShouldBeNil) artifactManifestDigest := godigest.FromBytes(artifactManifestBlob) err = UploadImage(artifactImg, baseURL, repo, artifactManifestDigest.String()) So(err, ShouldBeNil) gqlQuery := ` { Referrers( repo: "%s", digest: "%s", type: ""){ ArtifactType, Digest, MediaType, Size, Annotations{ Key Value } } }` strQuery := fmt.Sprintf(gqlQuery, repo, manifestDigest.String()) targetURL := fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery)) resp, err := resty.R().Get(targetURL) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) referrersResp := &zcommon.ReferrersResp{} err = json.Unmarshal(resp.Body(), referrersResp) So(err, ShouldBeNil) So(referrersResp.Errors, ShouldBeNil) So(referrersResp.Referrers[0].ArtifactType, ShouldEqual, artifactType) So(referrersResp.Referrers[0].MediaType, ShouldEqual, ispec.MediaTypeImageManifest) So(referrersResp.Referrers[0].Annotations[0].Key, ShouldEqual, "com.artifact.format") So(referrersResp.Referrers[0].Annotations[0].Value, ShouldEqual, "test") So(referrersResp.Referrers[0].Digest, ShouldEqual, artifactManifestDigest.String()) }) Convey("referrers for image index", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = t.TempDir() conf.Storage.GC = false defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, Lint: &extconf.LintConfig{ BaseConfig: extconf.BaseConfig{ Enable: &defaultVal, }, }, } gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, graphqlQueryPrefix) conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() // ======================= multiarch := CreateRandomMultiarch() repo := "artifact-ref" err := UploadMultiarchImage(multiarch, baseURL, repo, "multiarch") So(err, ShouldBeNil) indexBlob, err := json.Marshal(multiarch.Index) So(err, ShouldBeNil) indexDigest := godigest.FromBytes(indexBlob) indexSize := int64(len(indexBlob)) subjectDescriptor := &ispec.Descriptor{ MediaType: ispec.MediaTypeImageIndex, Size: indexSize, Digest: indexDigest, } artifactContentBlob := []byte("test artifact") artifactContentBlobSize := int64(len(artifactContentBlob)) artifactContentType := "application/octet-stream" artifactContentBlobDigest := godigest.FromBytes(artifactContentBlob) artifactType := "com.artifact.test/type2" configBlob, err := json.Marshal(ispec.Image{}) So(err, ShouldBeNil) artifactManifest := ispec.Manifest{ Layers: []ispec.Descriptor{ { MediaType: artifactContentType, Digest: artifactContentBlobDigest, Size: artifactContentBlobSize, }, }, Subject: subjectDescriptor, Config: ispec.Descriptor{ MediaType: artifactType, Digest: godigest.FromBytes(configBlob), }, MediaType: ispec.MediaTypeImageManifest, Annotations: map[string]string{ "com.artifact.format": "test", }, } artifactManifest.SchemaVersion = 2 artifactManifestBlob, err := json.Marshal(artifactManifest) So(err, ShouldBeNil) artifactManifestDigest := godigest.FromBytes(artifactManifestBlob) err = UploadImage( Image{ Manifest: artifactManifest, Config: ispec.Image{}, Layers: [][]byte{artifactContentBlob}, }, baseURL, repo, artifactManifestDigest.String()) So(err, ShouldBeNil) gqlQuery := ` { Referrers( repo: "%s", digest: "%s", type: "" ){ ArtifactType, Digest, MediaType, Size, Annotations{ Key Value } } }` strQuery := fmt.Sprintf(gqlQuery, repo, indexDigest.String()) targetURL := fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery)) resp, err := resty.R().Get(targetURL) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) referrersResp := &zcommon.ReferrersResp{} err = json.Unmarshal(resp.Body(), referrersResp) So(err, ShouldBeNil) So(referrersResp.Errors, ShouldBeNil) So(len(referrersResp.Referrers), ShouldEqual, 1) So(referrersResp.Referrers[0].ArtifactType, ShouldEqual, artifactType) So(referrersResp.Referrers[0].MediaType, ShouldEqual, ispec.MediaTypeImageManifest) So(referrersResp.Referrers[0].Annotations[0].Key, ShouldEqual, "com.artifact.format") So(referrersResp.Referrers[0].Annotations[0].Value, ShouldEqual, "test") So(referrersResp.Referrers[0].Digest, ShouldEqual, artifactManifestDigest.String()) }) Convey("Get referrers with index as referrer", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = t.TempDir() conf.Storage.GC = false defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, Lint: &extconf.LintConfig{ BaseConfig: extconf.BaseConfig{ Enable: &defaultVal, }, }, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() // Upload the index referrer targetImg := CreateRandomImage() targetDigest := targetImg.Digest() err := UploadImage(targetImg, baseURL, "repo", targetImg.DigestStr()) So(err, ShouldBeNil) artifactType := "com.artifact.art/type" indexReferrer := CreateMultiarchWith().RandomImages(2). ArtifactType(artifactType). Subject(targetImg.DescriptorRef()). Build() indexReferrerDigest := indexReferrer.Digest() err = UploadMultiarchImage(indexReferrer, baseURL, "repo", "ref") So(err, ShouldBeNil) // Call Referrers GQL referrersQuery := ` { Referrers( repo: "%s", digest: "%s"){ ArtifactType, Digest, MediaType, Size, Annotations{ Key Value } } }` referrersQuery = fmt.Sprintf(referrersQuery, "repo", targetDigest.String()) resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(referrersQuery)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) So(err, ShouldBeNil) referrersResp := &zcommon.ReferrersResp{} err = json.Unmarshal(resp.Body(), referrersResp) So(err, ShouldBeNil) So(len(referrersResp.Referrers), ShouldEqual, 1) So(referrersResp.Referrers[0].ArtifactType, ShouldResemble, artifactType) So(referrersResp.Referrers[0].Digest, ShouldResemble, indexReferrerDigest.String()) So(referrersResp.Referrers[0].MediaType, ShouldResemble, ispec.MediaTypeImageIndex) // Make REST call resp, err = resty.R().Get(baseURL + "/v2/repo/referrers/" + targetDigest.String()) So(err, ShouldBeNil) var index ispec.Index err = json.Unmarshal(resp.Body(), &index) So(err, ShouldBeNil) So(len(index.Manifests), ShouldEqual, 1) So(index.Manifests[0].ArtifactType, ShouldEqual, artifactType) So(index.Manifests[0].Digest.String(), ShouldResemble, indexReferrerDigest.String()) So(index.Manifests[0].MediaType, ShouldResemble, ispec.MediaTypeImageIndex) }) } func TestExpandedRepoInfo(t *testing.T) { Convey("Filter out manifests with no tag", t, func() { tagToBeRemoved := "3.0" repo1 := "test1" tempDir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = 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(tempDir, false, false, log.NewLogger("debug", ""), monitoring.NewMetricsServer(false, log.NewLogger("debug", "")), nil, nil) storeController := storage.StoreController{ DefaultStore: imageStore, } // init storage layout with 3 images for i := 1; i <= 3; i++ { image := CreateImageWith().RandomLayers(1, 100).DefaultConfig().Build() err := WriteImageToFileSystem(image, repo1, fmt.Sprintf("%d.0", i), storeController) So(err, ShouldBeNil) } // remote a tag from index.json indexPath := path.Join(tempDir, repo1, "index.json") indexFile, err := os.Open(indexPath) So(err, ShouldBeNil) buf, err := io.ReadAll(indexFile) So(err, ShouldBeNil) var index ispec.Index if err = json.Unmarshal(buf, &index); err == nil { for _, manifest := range index.Manifests { if val, ok := manifest.Annotations[ispec.AnnotationRefName]; ok && val == tagToBeRemoved { delete(manifest.Annotations, ispec.AnnotationRefName) break } } } buf, err = json.Marshal(index) So(err, ShouldBeNil) err = os.WriteFile(indexPath, buf, 0o600) So(err, ShouldBeNil) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() 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=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) responseStruct := &zcommon.ExpandedRepoInfoResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.ExpandedRepoInfoResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Summary, ShouldNotBeEmpty) So(responseStruct.Summary.Name, ShouldEqual, "test1") So(len(responseStruct.ImageSummaries), ShouldEqual, 2) }) Convey("Test expanded repo info", t, func() { subpath := "/a" rootDir := t.TempDir() subRootDir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) 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 conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() annotations := make(map[string]string) annotations["org.opencontainers.image.vendor"] = "zot" uploadedImage := CreateImageWith().RandomLayers(1, 100).DefaultConfig(). Annotations(annotations).Build() err := UploadImage(uploadedImage, baseURL, "zot-cve-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(uploadedImage, baseURL, "a/zot-cve-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(uploadedImage, baseURL, "zot-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(uploadedImage, baseURL, "a/zot-test", "0.0.1") So(err, ShouldBeNil) log := log.NewLogger("debug", "") metrics := monitoring.NewMetricsServer(false, log) testStorage := local.NewImageStore(rootDir, false, false, log, metrics, nil, nil) resp, err := resty.R().Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) resp, err = resty.R().Get(baseURL + graphqlQueryPrefix) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 422) query := `{ ExpandedRepoInfo(repo:"zot-cve-test"){ Summary { Name LastUpdated Size } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.ExpandedRepoInfoResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Summary, ShouldNotBeEmpty) So(responseStruct.Summary.Name, ShouldEqual, "zot-cve-test") query = `{ ExpandedRepoInfo(repo:"zot-cve-test"){ Images { Tag Manifests { Digest Layers {Size Digest} } IsSigned } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.ExpandedRepoInfoResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.ImageSummaries), ShouldNotEqual, 0) So(len(responseStruct.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.ImageSummaries { if m.Manifests[0].Digest == testManifestDigest.String() { found = true So(m.IsSigned, ShouldEqual, false) } } So(found, ShouldEqual, true) err = signature.SignImageUsingCosign("zot-cve-test:0.0.1", port, false) So(err, ShouldBeNil) 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.ImageSummaries), ShouldNotEqual, 0) So(len(responseStruct.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.ImageSummaries { if m.Manifests[0].Digest == testManifestDigest.String() { found = true So(m.IsSigned, ShouldEqual, true) } } So(found, ShouldEqual, true) query = `{ ExpandedRepoInfo(repo:""){ Images { Tag } } }` 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 { 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) err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.ImageSummaries), ShouldNotEqual, 0) So(len(responseStruct.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.ImageSummaries { if m.Manifests[0].Digest == testManifestDigest.String() { found = true So(m.IsSigned, ShouldEqual, false) } } So(found, ShouldEqual, true) err = signature.SignImageUsingCosign("zot-test@"+testManifestDigest.String(), port, false) So(err, ShouldBeNil) resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "/query?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.ImageSummaries), ShouldNotEqual, 0) So(len(responseStruct.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.ImageSummaries { if m.Manifests[0].Digest == testManifestDigest.String() { found = true So(m.IsSigned, ShouldEqual, true) } } So(found, ShouldEqual, true) manifestDigest := uploadedImage.ManifestDescriptor.Digest err = os.Remove(path.Join(rootDir, "zot-test/blobs/sha256", manifestDigest.Encoded())) So(err, ShouldBeNil) 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) }) Convey("Test expanded repo info with tagged referrers", t, func() { const testTag = "test" rootDir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = rootDir conf.Storage.GC = false defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() image := CreateImageWith().RandomLayers(1, 20).DefaultConfig().Build() err := UploadImage(image, baseURL, "repo", testTag) So(err, ShouldBeNil) referrer := CreateImageWith().RandomLayers(1, 100).DefaultConfig().Subject(&ispec.Descriptor{ Digest: image.ManifestDescriptor.Digest, MediaType: ispec.MediaTypeImageManifest, }).Build() tag := "test-ref-tag" err = UploadImage(referrer, baseURL, "repo", tag) So(err, ShouldBeNil) // ------- Make the call to GQL and see that it doesn't crash responseStruct := &zcommon.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.ImageSummaries), ShouldEqual, 2) repoInfo := responseStruct.RepoInfo foundTagTest := false foundTagRefTag := false for _, imgSum := range repoInfo.ImageSummaries { switch imgSum.Tag { case testTag: foundTagTest = true case "test-ref-tag": foundTagRefTag = true } } So(foundTagTest || foundTagRefTag, ShouldEqual, true) }) Convey("Test image tags order", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = 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) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() resp, err := resty.R().Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) resp, err = resty.R().Get(baseURL + graphqlQueryPrefix) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 422) // create test images repoName := "test-repo" //nolint:goconst layers := [][]byte{ {10, 11, 10, 11}, } err = uploadNewRepoTag("1.0", repoName, baseURL, layers) So(err, ShouldBeNil) err = uploadNewRepoTag("2.0", repoName, baseURL, layers) So(err, ShouldBeNil) err = uploadNewRepoTag("3.0", repoName, baseURL, layers) So(err, ShouldBeNil) responseStruct := &zcommon.ExpandedRepoInfoResp{} 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) err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.ImageSummaries), ShouldNotEqual, 0) So(len(responseStruct.ImageSummaries[0].Manifests[0].Layers), ShouldNotEqual, 0) So(responseStruct.ImageSummaries[0].Tag, ShouldEqual, "3.0") So(responseStruct.ImageSummaries[1].Tag, ShouldEqual, "2.0") So(responseStruct.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, false, log.NewLogger("debug", ""), monitoring.NewMetricsServer(false, log.NewLogger("debug", "")), nil, nil) storeController := storage.StoreController{ DefaultStore: imageStore, } // ------- Create test images indexSubImage11 := CreateImageWith().RandomLayers(1, 100).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "os11", Architecture: "arch11", }, }).Build() indexSubImage12 := CreateImageWith().RandomLayers(1, 100).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "os12", Architecture: "arch12", }, }).Build() multiImage1 := CreateMultiarchWith().Images([]Image{ indexSubImage11, indexSubImage12, }).Build() indexSubImage21 := CreateImageWith().RandomLayers(1, 100).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "os21", Architecture: "arch21", }, }).Build() indexSubImage22 := CreateImageWith().RandomLayers(1, 100).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "os22", Architecture: "arch22", }, }).Build() indexSubImage23 := CreateImageWith().RandomLayers(1, 100).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "os23", Architecture: "arch23", }, }).Build() multiImage2 := CreateMultiarchWith().Images([]Image{ indexSubImage21, indexSubImage22, indexSubImage23, }).Build() // ------- Write test Images err := WriteMultiArchImageToFileSystem(multiImage1, "repo", "1.0.0", storeController) So(err, ShouldBeNil) err = WriteMultiArchImageToFileSystem(multiImage2, "repo", "2.0.0", storeController) So(err, ShouldBeNil) // ------- Start Server /tmp/TestExpandedRepoInfo4021254039/005 ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(conf.HTTP.Port) defer ctlrManager.StopServer() // ------- Test ExpandedRepoInfo responseStruct := &zcommon.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.Summary.Platforms), ShouldNotEqual, 5) found := false for _, is := range responseStruct.ImageSummaries { if is.Tag == "1.0.0" { found = true So(len(is.Manifests), ShouldEqual, 2) } } So(found, ShouldBeTrue) found = false for _, is := range responseStruct.ImageSummaries { if is.Tag == "2.0.0" { found = true So(len(is.Manifests), ShouldEqual, 3) } } So(found, ShouldBeTrue) }) } func TestDerivedImageList(t *testing.T) { rootDir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = rootDir defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() Convey("Test dependency list for image working", t, func() { // create test images config := ispec.Image{ Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", } configBlob, err := json.Marshal(config) So(err, ShouldBeNil) configDigest := godigest.FromBytes(configBlob) layers := [][]byte{ {10, 11, 10, 11}, {11, 11, 11, 11}, {10, 10, 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[2]), Size: int64(len(layers[2])), }, }, } repoName := "test-repo" //nolint:goconst err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) // create image with the same layers 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[2]), Size: int64(len(layers[2])), }, }, } repoName = "same-layers" //nolint:goconst err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) // create image with missing layer layers = [][]byte{ {10, 11, 10, 11}, {10, 10, 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, }, } repoName = "missing-layer" err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) // create image with more layers than the original layers = [][]byte{ {10, 11, 10, 11}, {11, 11, 11, 11}, {10, 10, 10, 10}, {10, 10, 10, 11}, {11, 11, 10, 10}, {11, 10, 10, 10}, } 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[2]), Size: int64(len(layers[2])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[3]), Size: int64(len(layers[3])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[4]), Size: int64(len(layers[4])), }, }, } repoName = "more-layers" err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[2]), Size: int64(len(layers[2])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[3]), Size: int64(len(layers[3])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[4]), Size: int64(len(layers[4])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[5]), Size: int64(len(layers[5])), }, }, } repoName = "all-layers" err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) Convey("non paginated query", func() { 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(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeFalse) //nolint:goconst So(strings.Contains(string(resp.Body()), "missing-layers"), ShouldBeFalse) So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeTrue) So(strings.Contains(string(resp.Body()), "all-layers"), ShouldBeTrue) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) Convey("paginated query", func() { query := ` { DerivedImageList(image:"test-repo:latest", requestedPage:{limit: 1, offset: 0, sortBy:ALPHABETIC_ASC}){ Results{ RepoName Tag Manifests { Digest ConfigDigest LastUpdated Size } Size } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeFalse) //nolint:goconst So(strings.Contains(string(resp.Body()), "missing-layers"), ShouldBeFalse) So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeFalse) So(strings.Contains(string(resp.Body()), "all-layers"), ShouldBeTrue) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) }) Convey("Inexistent repository", t, func() { query := ` { DerivedImageList(image:"inexistent-image:latest"){ Results{ RepoName Tag Manifests { Digest ConfigDigest LastUpdated Size } Size } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(string(resp.Body()), ShouldContainSubstring, "repository not found") So(err, ShouldBeNil) }) Convey("Invalid query, no reference provided", t, func() { query := ` { DerivedImageList(image:"inexistent-image"){ Results{ RepoName Tag Manifests { Digest ConfigDigest LastUpdated Size } Size } } }` responseStruct := &zcommon.DerivedImageListResponse{} contains := false resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(err, ShouldBeNil) err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) for _, err := range responseStruct.Errors { result := strings.Contains(err.Message, "no reference provided") if result { contains = result } } So(contains, ShouldBeTrue) }) } //nolint:dupl func TestDerivedImageListNoRepos(t *testing.T) { Convey("No repositories found", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = 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) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() query := ` { DerivedImageList(image:"test-image: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(resp.StatusCode(), ShouldEqual, 200) So(string(resp.Body()), ShouldContainSubstring, "repository not found") So(err, ShouldBeNil) }) } func TestGetImageManifest(t *testing.T) { Convey("Test nonexistent image", t, func() { mockImageStore := mocks.MockedImageStore{} storeController := storage.StoreController{ DefaultStore: mockImageStore, } olu := ociutils.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", "")) _, _, err := olu.GetImageManifest("nonexistent-repo", "latest") So(err, ShouldNotBeNil) }) Convey("Test nonexistent image", t, func() { mockImageStore := mocks.MockedImageStore{ GetImageManifestFn: func(repo string, reference string) ([]byte, godigest.Digest, string, error) { return []byte{}, "", "", ErrTestError }, } storeController := storage.StoreController{ DefaultStore: mockImageStore, } olu := ociutils.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", "")) _, _, err := olu.GetImageManifest("test-repo", "latest") //nolint:goconst So(err, ShouldNotBeNil) }) } func TestBaseImageList(t *testing.T) { rootDir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = rootDir defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() Convey("Test base image list for image working", t, func() { // create test images config := ispec.Image{ Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", } configBlob, err := json.Marshal(config) So(err, ShouldBeNil) configDigest := godigest.FromBytes(configBlob) layers := [][]byte{ {10, 11, 10, 11}, {11, 11, 11, 11}, {10, 10, 10, 11}, {10, 10, 10, 10}, } 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[2]), Size: int64(len(layers[2])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[3]), Size: int64(len(layers[3])), }, }, } repoName := "test-repo" //nolint:goconst err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) // create image with the same layers 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[2]), Size: int64(len(layers[2])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[3]), Size: int64(len(layers[3])), }, }, } repoName = "same-layers" //nolint:goconst err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) // create image with less layers than the given image, but which are in the given image layers = [][]byte{ {10, 11, 10, 11}, {10, 10, 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, }, } repoName = "less-layers" err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) 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, }, baseURL, "one-layer", "latest", ) 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, }, baseURL, "one-layer", "latest", ) 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, }, baseURL, repoName, "latest", ) 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, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) // create image with less layers than the given image, but one layer isn't in the given image layers = [][]byte{ {10, 11, 10, 11}, {11, 10, 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, }, } repoName = "less-layers-false" err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) // create image with more layers than the original layers = [][]byte{ {10, 11, 10, 11}, {11, 11, 11, 11}, {10, 10, 10, 10}, {10, 10, 10, 11}, {11, 11, 10, 10}, } 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[2]), Size: int64(len(layers[2])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[3]), Size: int64(len(layers[3])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[4]), Size: int64(len(layers[4])), }, }, } repoName = "more-layers" err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) // create image with no shared layers with the given image layers = [][]byte{ {12, 12, 12, 12}, {12, 10, 10, 12}, } 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])), }, { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: godigest.FromBytes(layers[1]), Size: int64(len(layers[1])), }, }, } repoName = "diff-layers" err = UploadImage( Image{ Manifest: manifest, Config: config, Layers: layers, }, baseURL, repoName, "latest", ) So(err, ShouldBeNil) Convey("non paginated query", func() { query := ` { BaseImageList(image:"test-repo:latest"){ Results{ RepoName Tag IsSigned Manifests { Digest ConfigDigest LastUpdated Size } Size } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(strings.Contains(string(resp.Body()), "less-layers"), ShouldBeTrue) So(strings.Contains(string(resp.Body()), "one-layer"), ShouldBeTrue) So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeFalse) //nolint:goconst So(strings.Contains(string(resp.Body()), "less-layers-false"), ShouldBeFalse) So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeFalse) So(strings.Contains(string(resp.Body()), "diff-layers"), ShouldBeFalse) So(strings.Contains(string(resp.Body()), "test-repo"), ShouldBeFalse) //nolint:goconst // should not list given image So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) Convey("paginated query", func() { query := ` { BaseImageList(image:"test-repo:latest", requestedPage:{limit: 1, offset: 0, sortBy:RELEVANCE}){ Results{ RepoName Tag Manifests { Digest ConfigDigest LastUpdated Size } Size } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(strings.Contains(string(resp.Body()), "less-layers"), ShouldBeTrue) So(strings.Contains(string(resp.Body()), "one-layer"), ShouldBeFalse) So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeFalse) //nolint:goconst So(strings.Contains(string(resp.Body()), "less-layers-false"), ShouldBeFalse) So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeFalse) So(strings.Contains(string(resp.Body()), "diff-layers"), ShouldBeFalse) So(strings.Contains(string(resp.Body()), "test-repo"), ShouldBeFalse) //nolint:goconst // should not list given image So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) }) Convey("Nonexistent repository", t, func() { query := ` { BaseImageList(image:"nonexistent-image:latest"){ Results{ RepoName Tag Manifests { Digest ConfigDigest LastUpdated Size } Size } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(string(resp.Body()), ShouldContainSubstring, "repository not found") So(err, ShouldBeNil) }) Convey("Invalid query, no reference provided", t, func() { query := ` { BaseImageList(image:"nonexistent-image"){ Results{ RepoName Tag Manifests { Digest ConfigDigest LastUpdated Size } Size } } }` responseStruct := &zcommon.BaseImageListResponse{} contains := false resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(err, ShouldBeNil) err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) for _, err := range responseStruct.Errors { result := strings.Contains(err.Message, "no reference provided") if result { contains = result } } So(contains, ShouldBeTrue) }) } //nolint:dupl func TestBaseImageListNoRepos(t *testing.T) { Convey("No repositories found", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = 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) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() query := ` { BaseImageList(image:"test-image"){ Results{ RepoName Tag Manifests { Digest ConfigDigest LastUpdated Size } IsSigned Size } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(strings.Contains(string(resp.Body()), "no reference provided"), ShouldBeTrue) So(err, ShouldBeNil) }) } func TestGetRepositories(t *testing.T) { Convey("Test getting the repositories list", t, func() { mockImageStore := mocks.MockedImageStore{ GetRepositoriesFn: func() ([]string, error) { return []string{}, ErrTestError }, } storeController := storage.StoreController{ DefaultStore: mockImageStore, SubStore: map[string]storageTypes.ImageStore{"test": mockImageStore}, } olu := ociutils.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", "")) repoList, err := olu.GetRepositories() So(repoList, ShouldBeEmpty) So(err, ShouldNotBeNil) storeController = storage.StoreController{ DefaultStore: mocks.MockedImageStore{}, SubStore: map[string]storageTypes.ImageStore{"test": mockImageStore}, } olu = ociutils.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", "")) repoList, err = olu.GetRepositories() So(repoList, ShouldBeEmpty) So(err, ShouldNotBeNil) }) } func TestGlobalSearchImageAuthor(t *testing.T) { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port tempDir := t.TempDir() conf.Storage.RootDirectory = tempDir defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() Convey("Test global search with author in manifest's annotations", t, func() { annotations := make(map[string]string) annotations["org.opencontainers.image.authors"] = "author name" image := CreateImageWith().RandomLayers(1, 10000).DefaultConfig(). Annotations(annotations).Build() err := UploadImage(image, baseURL, "repowithauthor", "latest") So(err, ShouldBeNil) query := ` { GlobalSearch(query:"repowithauthor:latest"){ Images { RepoName Tag LastUpdated Size IsSigned Authors } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStructImages := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStructImages) So(err, ShouldBeNil) So(responseStructImages.Images[0].Authors, ShouldEqual, "author name") query = ` { GlobalSearch(query:"repowithauthor"){ Repos { Name LastUpdated Size Platforms { Os Arch } Vendors NewestImage { RepoName Tag LastUpdated Size IsSigned Authors } } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStructRepos := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStructRepos) So(err, ShouldBeNil) So(responseStructRepos.Repos[0].NewestImage.Authors, ShouldEqual, "author name") }) Convey("Test global search with author in manifest's config", t, func() { image := CreateImageWith().RandomLayers(1, 10000).DefaultConfig().Build() err := UploadImage(image, baseURL, "repowithauthorconfig", "latest") So(err, ShouldBeNil) query := ` { GlobalSearch(query:"repowithauthorconfig:latest"){ Images { RepoName Tag LastUpdated Size IsSigned Authors } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStructImages := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStructImages) So(err, ShouldBeNil) So(responseStructImages.Images[0].Authors, ShouldEqual, "ZotUser") query = ` { GlobalSearch(query:"repowithauthorconfig"){ Repos { Name LastUpdated Size Platforms { Os Arch } Vendors NewestImage { RepoName Tag LastUpdated Size IsSigned Authors } } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStructRepos := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStructRepos) So(err, ShouldBeNil) So(responseStructRepos.Repos[0].NewestImage.Authors, ShouldEqual, "ZotUser") }) } func TestGlobalSearch(t *testing.T) { Convey("Test searching for repos with vulnerabitity scanning disabled", t, func() { subpath := "/a" dir := t.TempDir() subDir := t.TempDir() subRootDir := path.Join(subDir, subpath) port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = dir conf.Storage.SubPaths = make(map[string]config.StorageConfig) conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir} defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() // push test images to repo 1 image 1 createdTime := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC) createdTimeL2 := time.Date(2010, 2, 1, 12, 0, 0, 0, time.UTC) config1 := ispec.Image{ Created: &createdTimeL2, Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", History: []ispec.History{ { Created: &createdTime, CreatedBy: "go test data", Author: "ZotUser", Comment: "Test history comment", EmptyLayer: true, }, { Created: &createdTimeL2, CreatedBy: "go test data 2", Author: "ZotUser", Comment: "Test history comment2", EmptyLayer: false, }, }, } image1 := CreateImageWith().LayerBlobs([][]byte{make([]byte, 100)}). ImageConfig(config1).Build() layersSize1 := 0 for _, l := range image1.Layers { layersSize1 += len(l) } err := UploadImage(image1, baseURL, "repo1", "1.0.1") So(err, ShouldBeNil) // push test images to repo 1 image 2 createdTime2 := time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC) createdTimeL2 = time.Date(2009, 2, 1, 12, 0, 0, 0, time.UTC) config2 := ispec.Image{ Created: &createdTimeL2, Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", History: []ispec.History{ { Created: &createdTime2, CreatedBy: "go test data", Author: "ZotUser", Comment: "Test history comment", EmptyLayer: true, }, { Created: &createdTimeL2, CreatedBy: "go test data 2", Author: "ZotUser", Comment: "Test history comment2", EmptyLayer: false, }, }, } image2 := CreateImageWith().LayerBlobs([][]byte{make([]byte, 200)}). ImageConfig(config2).Build() layersSize2 := 0 for _, l := range image2.Layers { layersSize2 += len(l) } err = UploadImage(image2, baseURL, "repo1", "1.0.2") So(err, ShouldBeNil) // push test images to repo 2 image 1 createdTime3 := time.Date(2009, 2, 1, 12, 0, 0, 0, time.UTC) config3 := ispec.Image{ Created: &createdTime3, Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", History: []ispec.History{{Created: &createdTime3}}, } image3 := CreateImageWith().LayerBlobs([][]byte{make([]byte, 300)}). ImageConfig(config3).Build() layersSize3 := 0 for _, l := range image3.Layers { layersSize3 += len(l) } err = UploadImage(image3, baseURL, "repo2", "1.0.0") So(err, ShouldBeNil) olu := ociutils.NewBaseOciLayoutUtils(ctlr.StoreController, log.NewLogger("debug", "")) // Initialize the objects containing the expected data repos, err := olu.GetRepositories() So(err, ShouldBeNil) allExpectedRepoInfoMap := make(map[string]zcommon.RepoInfo) allExpectedImageSummaryMap := make(map[string]zcommon.ImageSummary) for _, repo := range repos { repoInfo, err := olu.GetExpandedRepoInfo(repo) So(err, ShouldBeNil) allExpectedRepoInfoMap[repo] = repoInfo for _, image := range repoInfo.ImageSummaries { imageName := fmt.Sprintf("%s:%s", repo, image.Tag) allExpectedImageSummaryMap[imageName] = image } } query := ` { GlobalSearch(query:"repo"){ Images { RepoName Tag LastUpdated Size IsSigned Manifests { LastUpdated Size Platform { Os Arch } History { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } Vendor Vulnerabilities { Count MaxSeverity } } Repos { Name LastUpdated Size Platforms { Os Arch } Vendors NewestImage { RepoName Tag LastUpdated Size Digest Manifests{ Digest ConfigDigest LastUpdated Size Platform { Os Arch } History { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } } Layers { Digest Size } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) // Make sure the repo/image counts match before comparing actual content So(responseStruct.Images, ShouldNotBeNil) t.Logf("returned images: %v", responseStruct.Images) So(responseStruct.Images, ShouldBeEmpty) t.Logf("returned repos: %v", responseStruct.Repos) So(len(responseStruct.Repos), ShouldEqual, 2) t.Logf("returned layers: %v", responseStruct.GlobalSearch.Layers) So(responseStruct.Layers, ShouldBeEmpty) newestImageMap := make(map[string]zcommon.ImageSummary) actualRepoMap := make(map[string]zcommon.RepoSummary) for _, repo := range responseStruct.Repos { newestImageMap[repo.Name] = repo.NewestImage actualRepoMap[repo.Name] = repo } // Tag 1.0.2 has a history entry which is older compare to 1.0.1 So(newestImageMap["repo1"].Tag, ShouldEqual, "1.0.1") So(newestImageMap["repo1"].LastUpdated, ShouldEqual, time.Date(2010, 2, 1, 12, 0, 0, 0, time.UTC)) So(newestImageMap["repo2"].Tag, ShouldEqual, "1.0.0") So(newestImageMap["repo2"].LastUpdated, ShouldEqual, time.Date(2009, 2, 1, 12, 0, 0, 0, time.UTC)) for repoName, repoSummary := range actualRepoMap { repoSummary := repoSummary // Check if data in NewestImage is consistent with the data in RepoSummary So(repoName, ShouldEqual, repoSummary.NewestImage.RepoName) So(repoSummary.Name, ShouldEqual, repoSummary.NewestImage.RepoName) So(repoSummary.LastUpdated, ShouldEqual, repoSummary.NewestImage.LastUpdated) // The data in the RepoSummary returned from the request matches the data returned from the disk repoInfo := allExpectedRepoInfoMap[repoName] t.Logf("Validate repo summary returned by global search with vulnerability scanning disabled") verifyRepoSummaryFields(t, &repoSummary, &repoInfo.Summary) // RepoInfo object does not provide vulnerability information so we need to check differently // No vulnerabilities should be detected since trivy is disabled t.Logf("Found vulnerability summary %v", repoSummary.NewestImage.Vulnerabilities) So(repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.UnknownCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.LowCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.MediumCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.HighCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "") } query = ` { GlobalSearch(query:"repo1:1.0.1"){ Images { RepoName Tag LastUpdated Size Manifests { LastUpdated Size Platform { Os Arch } History { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } Repos { Name LastUpdated Size Platforms { Os Arch } Vendors NewestImage { RepoName Tag LastUpdated Size Manifests { LastUpdated Size Platform { Os Arch } History { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } } Layers { Digest Size } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images, ShouldNotBeEmpty) So(responseStruct.Repos, ShouldBeEmpty) So(responseStruct.Layers, ShouldBeEmpty) So(len(responseStruct.Images), ShouldEqual, 1) actualImageSummary := responseStruct.Images[0] So(actualImageSummary.Tag, ShouldEqual, "1.0.1") expectedImageSummary, ok := allExpectedImageSummaryMap["repo1:1.0.1"] So(ok, ShouldEqual, true) t.Logf("Validate image summary returned by global search with vulnerability scanning disabled") verifyImageSummaryFields(t, &actualImageSummary, &expectedImageSummary) // RepoInfo object does not provide vulnerability information so we need to check differently // 0 vulnerabilities should be detected since trivy is disabled t.Logf("Found vulnerability summary %v", actualImageSummary.Vulnerabilities) So(actualImageSummary.Vulnerabilities.Count, ShouldEqual, 0) So(actualImageSummary.Vulnerabilities.UnknownCount, ShouldEqual, 0) So(actualImageSummary.Vulnerabilities.LowCount, ShouldEqual, 0) So(actualImageSummary.Vulnerabilities.MediumCount, ShouldEqual, 0) So(actualImageSummary.Vulnerabilities.HighCount, ShouldEqual, 0) So(actualImageSummary.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(actualImageSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "") }) Convey("Test global search with real images and vulnerabitity scanning enabled", t, func() { subpath := "/a" dir := t.TempDir() subDir := t.TempDir() subRootDir := path.Join(subDir, subpath) port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = dir conf.Storage.SubPaths = make(map[string]config.StorageConfig) conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir} defaultVal := true updateDuration, _ := time.ParseDuration("1h") trivyConfig := &extconf.TrivyConfig{ DBRepository: "ghcr.io/project-zot/trivy-db", } cveConfig := &extconf.CVEConfig{ UpdateInterval: updateDuration, Trivy: trivyConfig, } searchConfig := &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, CVE: cveConfig, } conf.Extensions = &extconf.ExtensionConfig{ Search: searchConfig, } // we won't use the logging config feature as we want logs in both // stdout and a file logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") So(err, ShouldBeNil) logPath := logFile.Name() defer os.Remove(logPath) writers := io.MultiWriter(os.Stdout, logFile) ctlr := api.NewController(conf) ctlr.Log.Logger = ctlr.Log.Output(writers) if err := ctlr.Init(); err != nil { panic(err) } ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB) go func() { if err := ctlr.Run(); !errors.Is(err, http.ErrServerClosed) { panic(err) } }() defer ctlr.Shutdown() // Wait for trivy db to download substring := "{\"Search\":{\"Enable\":true,\"CVE\":{\"UpdateInterval\":3600000000000," + "\"Trivy\":{\"DBRepository\":\"ghcr.io/project-zot/trivy-db\",\"JavaDBRepository\":\"\"}}}" found, err := readFileAndSearchString(logPath, substring, 2*time.Minute) So(found, ShouldBeTrue) So(err, ShouldBeNil) found, err = readFileAndSearchString(logPath, "updating cve-db", 2*time.Minute) So(found, ShouldBeTrue) So(err, ShouldBeNil) found, err = readFileAndSearchString(logPath, "cve-db update completed, next update scheduled after interval", 4*time.Minute) So(found, ShouldBeTrue) So(err, ShouldBeNil) WaitTillServerReady(baseURL) // push test images to repo 1 image 1 createdTime := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC) config1 := ispec.Image{ Created: &createdTime, Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", History: []ispec.History{{Created: &createdTime}}, } image1 := CreateImageWith().LayerBlobs([][]byte{make([]byte, 100)}). ImageConfig(config1).Build() layersSize1 := 0 for _, l := range image1.Layers { layersSize1 += len(l) } err = UploadImage(image1, baseURL, "repo1", "1.0.1") So(err, ShouldBeNil) // push test images to repo 1 image 2 createdTime2 := time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC) config2 := ispec.Image{ Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", History: []ispec.History{{Created: &createdTime2}}, } image2 := CreateImageWith().LayerBlobs([][]byte{make([]byte, 200)}). ImageConfig(config2).Build() layersSize2 := 0 for _, l := range image2.Layers { layersSize2 += len(l) } err = UploadImage(image2, baseURL, "repo1", "1.0.2") So(err, ShouldBeNil) // push test images to repo 2 image 1 createdTime3 := time.Date(2009, 2, 1, 12, 0, 0, 0, time.UTC) config3 := ispec.Image{ Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", History: []ispec.History{{Created: &createdTime3}}, } image3 := CreateImageWith().LayerBlobs([][]byte{make([]byte, 300)}). ImageConfig(config3).Build() layersSize3 := 0 for _, l := range image3.Layers { layersSize3 += len(l) } err = UploadImage(image3, baseURL, "repo2", "1.0.0") So(err, ShouldBeNil) olu := ociutils.NewBaseOciLayoutUtils(ctlr.StoreController, log.NewLogger("debug", "")) // Initialize the objects containing the expected data repos, err := olu.GetRepositories() So(err, ShouldBeNil) allExpectedRepoInfoMap := make(map[string]zcommon.RepoInfo) allExpectedImageSummaryMap := make(map[string]zcommon.ImageSummary) for _, repo := range repos { repoInfo, err := olu.GetExpandedRepoInfo(repo) So(err, ShouldBeNil) allExpectedRepoInfoMap[repo] = repoInfo for _, image := range repoInfo.ImageSummaries { imageName := fmt.Sprintf("%s:%s", repo, image.Tag) allExpectedImageSummaryMap[imageName] = image } } query := ` { GlobalSearch(query:"repo"){ Images { RepoName Tag LastUpdated Size Manifests { LastUpdated Size Platform { Os Arch } History { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } Repos { Name LastUpdated Size Platforms { Os Arch } Vendors NewestImage { RepoName Tag LastUpdated Size Manifests { LastUpdated Size Platform { Os Arch } History { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } } Layers { Digest Size } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) // Make sure the repo/image counts match before comparing actual content So(responseStruct.Images, ShouldNotBeNil) t.Logf("returned images: %v", responseStruct.Images) So(responseStruct.Images, ShouldBeEmpty) t.Logf("returned repos: %v", responseStruct.Repos) So(len(responseStruct.Repos), ShouldEqual, 2) t.Logf("returned layers: %v", responseStruct.Layers) So(responseStruct.Layers, ShouldBeEmpty) newestImageMap := make(map[string]zcommon.ImageSummary) actualRepoMap := make(map[string]zcommon.RepoSummary) for _, repo := range responseStruct.Repos { newestImageMap[repo.Name] = repo.NewestImage actualRepoMap[repo.Name] = repo } // Tag 1.0.2 has a history entry which is older compare to 1.0.1 So(newestImageMap["repo1"].Tag, ShouldEqual, "1.0.1") So(newestImageMap["repo1"].LastUpdated, ShouldEqual, time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC)) So(newestImageMap["repo2"].Tag, ShouldEqual, "1.0.0") So(newestImageMap["repo2"].LastUpdated, ShouldEqual, time.Date(2009, 2, 1, 12, 0, 0, 0, time.UTC)) for repoName, repoSummary := range actualRepoMap { repoSummary := repoSummary // Check if data in NewestImage is consistent with the data in RepoSummary So(repoName, ShouldEqual, repoSummary.NewestImage.RepoName) So(repoSummary.Name, ShouldEqual, repoSummary.NewestImage.RepoName) So(repoSummary.LastUpdated, ShouldEqual, repoSummary.NewestImage.LastUpdated) // The data in the RepoSummary returned from the request matches the data returned from the disk repoInfo := allExpectedRepoInfoMap[repoName] t.Logf("Validate repo summary returned by global search with vulnerability scanning enabled") verifyRepoSummaryFields(t, &repoSummary, &repoInfo.Summary) // RepoInfo object does not provide vulnerability information so we need to check differently t.Logf("Found vulnerability summary %v", repoSummary.NewestImage.Vulnerabilities) if repoName == "repo1" { //nolint:goconst So(repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 4) // There are 4 vulnerabilities in the data used in tests So(repoSummary.NewestImage.Vulnerabilities.UnknownCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.LowCount, ShouldEqual, 1) So(repoSummary.NewestImage.Vulnerabilities.MediumCount, ShouldEqual, 1) So(repoSummary.NewestImage.Vulnerabilities.HighCount, ShouldEqual, 1) So(repoSummary.NewestImage.Vulnerabilities.CriticalCount, ShouldEqual, 1) So(repoSummary.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") } else { So(repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 0) // There are 0 vulnerabilities this data used in tests So(repoSummary.NewestImage.Vulnerabilities.UnknownCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.LowCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.MediumCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.HighCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "NONE") } } query = ` { GlobalSearch(query:"repo1:1.0.1"){ Images { RepoName Tag LastUpdated Size Manifests { LastUpdated Size Platform { Os Arch } History { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } Repos { Name LastUpdated Size Platforms { Os Arch } Vendors NewestImage { RepoName Tag LastUpdated Size Manifests { LastUpdated Size Platform { Os Arch } History { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } } Layers { Digest Size } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images, ShouldNotBeEmpty) So(responseStruct.Repos, ShouldBeEmpty) So(responseStruct.Layers, ShouldBeEmpty) So(len(responseStruct.Images), ShouldEqual, 1) actualImageSummary := responseStruct.Images[0] So(actualImageSummary.Tag, ShouldEqual, "1.0.1") expectedImageSummary, ok := allExpectedImageSummaryMap["repo1:1.0.1"] So(ok, ShouldEqual, true) t.Logf("Validate image summary returned by global search with vulnerability scanning enable") verifyImageSummaryFields(t, &actualImageSummary, &expectedImageSummary) // RepoInfo object does not provide vulnerability information so we need to check differently t.Logf("Found vulnerability summary %v", actualImageSummary.Vulnerabilities) // There are 4 vulnerabilities in the data used in tests So(actualImageSummary.Vulnerabilities.Count, ShouldEqual, 4) So(actualImageSummary.Vulnerabilities.UnknownCount, ShouldEqual, 0) So(actualImageSummary.Vulnerabilities.LowCount, ShouldEqual, 1) So(actualImageSummary.Vulnerabilities.MediumCount, ShouldEqual, 1) So(actualImageSummary.Vulnerabilities.HighCount, ShouldEqual, 1) So(actualImageSummary.Vulnerabilities.CriticalCount, ShouldEqual, 1) So(actualImageSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") }) Convey("global searching by digest", t, func() { log := log.NewLogger("debug", "") rootDir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = rootDir defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) storeCtlr := ociutils.GetDefaultStoreController(rootDir, log) image1 := CreateRandomImage() image2 := CreateRandomImage() multiArch := CreateRandomMultiarch() err := WriteImageToFileSystem(image1, "repo1", "tag1", storeCtlr) So(err, ShouldBeNil) err = WriteImageToFileSystem(image2, "repo1", "tag2", storeCtlr) So(err, ShouldBeNil) err = WriteMultiArchImageToFileSystem(multiArch, "repo1", "tag-multi", storeCtlr) So(err, ShouldBeNil) err = WriteImageToFileSystem(image2, "repo2", "tag2", storeCtlr) So(err, ShouldBeNil) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() // simple image results := GlobalSearchGQL(image1.DigestStr(), baseURL).GlobalSearch So(len(results.Images), ShouldEqual, 1) So(results.Images[0].Digest, ShouldResemble, image1.DigestStr()) So(results.Images[0].RepoName, ShouldResemble, "repo1") results = GlobalSearchGQL(image2.DigestStr(), baseURL).GlobalSearch So(len(results.Images), ShouldEqual, 2) repos := AccumulateField(results.Images, func(is zcommon.ImageSummary) string { return is.RepoName }) So(repos, ShouldContain, "repo1") So(repos, ShouldContain, "repo2") // multiarch results = GlobalSearchGQL(multiArch.DigestStr(), baseURL).GlobalSearch So(len(results.Images), ShouldEqual, 1) So(results.Images[0].Digest, ShouldResemble, multiArch.DigestStr()) So(len(results.Images[0].Manifests), ShouldEqual, len(multiArch.Images)) So(results.Images[0].RepoName, ShouldResemble, "repo1") results = GlobalSearchGQL(multiArch.Images[0].DigestStr(), baseURL).GlobalSearch So(len(results.Images), ShouldEqual, 1) So(results.Images[0].Digest, ShouldResemble, multiArch.DigestStr()) So(len(results.Images[0].Manifests), ShouldEqual, 1) So(results.Images[0].Manifests[0].Digest, ShouldResemble, multiArch.Images[0].DigestStr()) So(results.Images[0].RepoName, ShouldResemble, "repo1") }) Convey("global searching by tag cross repo", t, func() { log := log.NewLogger("debug", "") rootDir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = rootDir defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) storeCtlr := ociutils.GetDefaultStoreController(rootDir, log) image11 := CreateRandomImage() image12 := CreateRandomImage() err := WriteImageToFileSystem(image11, "repo1", "tag1", storeCtlr) So(err, ShouldBeNil) err = WriteImageToFileSystem(image12, "repo1", "tag2", storeCtlr) So(err, ShouldBeNil) image21 := CreateRandomImage() image22 := CreateRandomImage() multiArch2 := CreateRandomMultiarch() err = WriteImageToFileSystem(image21, "repo2", "tag1", storeCtlr) So(err, ShouldBeNil) err = WriteImageToFileSystem(image22, "repo2", "tag2", storeCtlr) So(err, ShouldBeNil) err = WriteMultiArchImageToFileSystem(multiArch2, "repo2", "tag-multi", storeCtlr) So(err, ShouldBeNil) image31 := CreateRandomImage() image32 := CreateRandomImage() err = WriteImageToFileSystem(image31, "repo3", "tag1", storeCtlr) So(err, ShouldBeNil) err = WriteImageToFileSystem(image32, "repo3", "tag2", storeCtlr) So(err, ShouldBeNil) image41 := CreateRandomImage() image42 := CreateRandomImage() multiArch4 := CreateRandomMultiarch() err = WriteImageToFileSystem(image41, "repo4", "tag1", storeCtlr) So(err, ShouldBeNil) err = WriteImageToFileSystem(image42, "repo4", "tag2", storeCtlr) So(err, ShouldBeNil) err = WriteMultiArchImageToFileSystem(multiArch4, "repo4", "tag-multi", storeCtlr) So(err, ShouldBeNil) image51 := CreateRandomImage() err = WriteImageToFileSystem(image51, "repo5", "tag1", storeCtlr) So(err, ShouldBeNil) multiArch62 := CreateRandomMultiarch() err = WriteMultiArchImageToFileSystem(multiArch62, "repo6", "tag2", storeCtlr) So(err, ShouldBeNil) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() // Search for a specific tag cross-repo and return single arch images results := GlobalSearchGQL(":tag1", baseURL).GlobalSearch So(len(results.Images), ShouldEqual, 5) So(len(results.Repos), ShouldEqual, 0) expectedRepos := []string{"repo1", "repo2", "repo3", "repo4", "repo5"} for _, image := range results.Images { So(image.Tag, ShouldEqual, "tag1") So(image.RepoName, ShouldBeIn, expectedRepos) So(len(image.Manifests), ShouldEqual, 1) } // Search for a specific tag cross-repo and return multi arch images results = GlobalSearchGQL(":tag-multi", baseURL).GlobalSearch So(len(results.Images), ShouldEqual, 2) So(len(results.Repos), ShouldEqual, 0) expectedRepos = []string{"repo2", "repo4"} for _, image := range results.Images { So(image.Tag, ShouldEqual, "tag-multi") So(image.RepoName, ShouldBeIn, expectedRepos) So(len(image.Manifests), ShouldEqual, 3) } // Search for a specific tag cross-repo and return mixed single and multiarch images results = GlobalSearchGQL(":tag2", baseURL).GlobalSearch So(len(results.Images), ShouldEqual, 5) So(len(results.Repos), ShouldEqual, 0) expectedRepos = []string{"repo1", "repo2", "repo3", "repo4", "repo6"} for _, image := range results.Images { So(image.Tag, ShouldEqual, "tag2") So(image.RepoName, ShouldBeIn, expectedRepos) if image.RepoName == "repo6" { So(len(image.Manifests), ShouldEqual, 3) } else { So(len(image.Manifests), ShouldEqual, 1) } } // Search for multiple tags using a partial match cross-repo and return multiarch images results = GlobalSearchGQL(":multi", baseURL).GlobalSearch So(len(results.Images), ShouldEqual, 2) So(len(results.Repos), ShouldEqual, 0) expectedRepos = []string{"repo2", "repo4"} for _, image := range results.Images { So(image.Tag, ShouldContainSubstring, "multi") So(image.RepoName, ShouldBeIn, expectedRepos) So(len(image.Manifests), ShouldEqual, 3) } // Search for multiple tags using a partial match cross-repo and return mixed single and multiarch images results = GlobalSearchGQL(":tag", baseURL).GlobalSearch So(len(results.Images), ShouldEqual, 12) So(len(results.Repos), ShouldEqual, 0) expectedRepos = []string{"repo1", "repo2", "repo3", "repo4", "repo5", "repo6"} for _, image := range results.Images { So(image.Tag, ShouldContainSubstring, "tag") So(image.RepoName, ShouldBeIn, expectedRepos) } // Search for a specific tag cross-repo and return mixt single and multiarch images result := GlobalSearchGQL(":", baseURL) errors := result.Errors So(len(errors), ShouldEqual, 1) results = result.GlobalSearch So(len(results.Images), ShouldEqual, 0) So(len(results.Repos), ShouldEqual, 0) }) } func TestCleaningFilteringParamsGlobalSearch(t *testing.T) { Convey("Test cleaning filtering parameters for global search", t, func() { dir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = dir 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() image := CreateImageWith().RandomLayers(1, 100). ImageConfig(ispec.Image{Platform: ispec.Platform{ OS: "windows", Architecture: "amd64", }}).Build() err := UploadImage(image, baseURL, "repo1", image.DigestStr()) So(err, ShouldBeNil) image = CreateImageWith().RandomLayers(1, 100). ImageConfig(ispec.Image{Platform: ispec.Platform{ OS: "linux", Architecture: "amd64", }}).Build() err = UploadImage(image, baseURL, "repo2", image.DigestStr()) So(err, ShouldBeNil) query := ` { GlobalSearch(query:"repo", requestedPage:{limit: 3, offset: 0, sortBy:RELEVANCE}, filter:{Os:[" linux", "Windows ", " "], Arch:["","aMd64 "]}) { Repos { Name } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) }) } func TestGlobalSearchFiltering(t *testing.T) { Convey("Global search HasToBeSigned filtering", t, func() { dir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = dir 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() image := CreateRandomImage() err := UploadImage(image, baseURL, "unsigned-repo", "test") So(err, ShouldBeNil) image = CreateRandomImage() err = UploadImage(image, baseURL, "signed-repo", "test") So(err, ShouldBeNil) err = signature.SignImageUsingCosign("signed-repo:test", port, false) So(err, ShouldBeNil) query := `{ GlobalSearch(query:"repo", filter:{HasToBeSigned:true}) { Repos { Name } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Repos, ShouldNotBeEmpty) So(responseStruct.Repos[0].Name, ShouldResemble, "signed-repo") }) } func TestGlobalSearchWithInvalidInput(t *testing.T) { Convey("Global search with invalid input", t, func() { dir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = dir 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() longString := RandomString(1000) query := fmt.Sprintf(` { GlobalSearch(query:"%s", requestedPage:{limit: 3, offset: 4, sortBy:RELEVANCE}, filter:{Os:["linux", "Windows", ""], Arch:["","amd64"]}) { Repos { Name } } }`, longString) resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Errors, ShouldNotBeEmpty) query = fmt.Sprintf(` { GlobalSearch(query:"repo", requestedPage:{limit: 3, offset: 4, sortBy:RELEVANCE}, filter:{Os:["%s", "Windows", ""], Arch:["","amd64"]}) { Repos { Name } } }`, longString) resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Errors, ShouldNotBeEmpty) query = fmt.Sprintf(` { GlobalSearch(query:"repo", requestedPage:{limit: 3, offset: 4, sortBy:RELEVANCE}, filter:{Os:["", "Windows", ""], Arch:["","%s"]}) { Repos { Name } } }`, longString) resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Errors, ShouldNotBeEmpty) }) } func TestImageList(t *testing.T) { Convey("Test ImageList", t, func() { rootDir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = rootDir defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() createdTime := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC) createdTimeL2 := time.Date(2010, 2, 1, 12, 0, 0, 0, time.UTC) config := ispec.Image{ Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, RootFS: ispec.RootFS{ Type: "layers", DiffIDs: []godigest.Digest{}, }, Author: "ZotUser", History: []ispec.History{ { Created: &createdTime, CreatedBy: "go test data", Author: "ZotUser", Comment: "Test history comment", EmptyLayer: true, }, { Created: &createdTimeL2, CreatedBy: "go test data 2", Author: "ZotUser", Comment: "Test history comment2", EmptyLayer: false, }, }, } image := CreateImageWith().RandomLayers(1, 100).ImageConfig(config).Build() err := UploadImage(image, baseURL, "zot-cve-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(image, baseURL, "a/zot-cve-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(image, baseURL, "zot-test", "0.0.1") So(err, ShouldBeNil) err = UploadImage(image, baseURL, "a/zot-test", "0.0.1") So(err, ShouldBeNil) imageStore := ctlr.StoreController.DefaultStore repos, err := imageStore.GetRepositories() So(err, ShouldBeNil) tags, err := imageStore.GetImageTags(repos[0]) So(err, ShouldBeNil) buf, _, _, err := imageStore.GetImageManifest(repos[0], tags[0]) So(err, ShouldBeNil) var imageManifest ispec.Manifest err = json.Unmarshal(buf, &imageManifest) So(err, ShouldBeNil) var imageConfigInfo ispec.Image imageConfigBuf, err := imageStore.GetBlobContent(repos[0], imageManifest.Config.Digest) So(err, ShouldBeNil) err = json.Unmarshal(imageConfigBuf, &imageConfigInfo) So(err, ShouldBeNil) Convey("without pagination, valid response", func() { query := fmt.Sprintf(`{ ImageList(repo:"%s"){ Results { Manifests { History{ HistoryDescription{ Author Comment Created CreatedBy EmptyLayer }, Layer{ Digest Size } } } } } }`, repos[0]) resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp, ShouldNotBeNil) var responseStruct zcommon.ImageListResponse err = json.Unmarshal(resp.Body(), &responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Results), ShouldEqual, len(tags)) So(len(responseStruct.Results[0].Manifests[0].History), ShouldEqual, len(imageConfigInfo.History)) }) Convey("Pagination with valid params", func() { limit := 1 query := fmt.Sprintf(`{ ImageList(repo:"%s", requestedPage:{limit: %d, offset: 0, sortBy:RELEVANCE}){ Results{ Manifests { History{ HistoryDescription{ Author Comment Created CreatedBy EmptyLayer }, Layer{ Digest Size } } } } } }`, repos[0], limit) resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp, ShouldNotBeNil) var responseStruct zcommon.ImageListResponse err = json.Unmarshal(resp.Body(), &responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Results), ShouldEqual, limit) }) }) } func TestGlobalSearchPagination(t *testing.T) { Convey("Test global search pagination", t, func() { dir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = dir 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() for i := 0; i < 3; i++ { image := CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build() err := UploadImage(image, baseURL, fmt.Sprintf("repo%d", i), "0.0.1") So(err, ShouldBeNil) } Convey("Limit is bigger than the repo count", func() { query := ` { GlobalSearch(query:"repo", requestedPage:{limit: 9, offset: 0, sortBy:RELEVANCE}){ Repos { Name } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images, ShouldBeEmpty) So(responseStruct.Repos, ShouldNotBeEmpty) So(responseStruct.Layers, ShouldBeEmpty) So(len(responseStruct.Repos), ShouldEqual, 3) }) Convey("Limit is lower than the repo count", func() { query := ` { GlobalSearch(query:"repo", requestedPage:{limit: 2, offset: 0, sortBy:RELEVANCE}){ Repos { Name } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images, ShouldBeEmpty) So(responseStruct.Repos, ShouldNotBeEmpty) So(responseStruct.Layers, ShouldBeEmpty) So(len(responseStruct.Repos), ShouldEqual, 2) }) Convey("PageInfo returned proper response", func() { query := ` { GlobalSearch(query:"repo", requestedPage:{limit: 2, offset: 0, sortBy:RELEVANCE}){ Repos { Name } Page{ ItemCount TotalCount } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images, ShouldBeEmpty) So(responseStruct.Repos, ShouldNotBeEmpty) So(responseStruct.Layers, ShouldBeEmpty) So(len(responseStruct.Repos), ShouldEqual, 2) So(responseStruct.Page.TotalCount, ShouldEqual, 3) So(responseStruct.Page.ItemCount, ShouldEqual, 2) }) Convey("PageInfo when limit is bigger than the repo count", func() { query := ` { GlobalSearch(query:"repo", requestedPage:{limit: 9, offset: 0, sortBy:RELEVANCE}){ Repos { Name } Page{ ItemCount TotalCount } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images, ShouldBeEmpty) So(responseStruct.Repos, ShouldNotBeEmpty) So(responseStruct.Layers, ShouldBeEmpty) So(len(responseStruct.Repos), ShouldEqual, 3) So(responseStruct.Page.TotalCount, ShouldEqual, 3) So(responseStruct.Page.ItemCount, ShouldEqual, 3) }) Convey("PageInfo when limit and offset have 0 value", func() { query := ` { GlobalSearch(query:"repo", requestedPage:{limit: 0, offset: 0, sortBy:RELEVANCE}){ Repos { Name } Page{ ItemCount TotalCount } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images, ShouldBeEmpty) So(responseStruct.Repos, ShouldNotBeEmpty) So(responseStruct.Layers, ShouldBeEmpty) So(len(responseStruct.Repos), ShouldEqual, 3) So(responseStruct.Page.TotalCount, ShouldEqual, 3) So(responseStruct.Page.ItemCount, ShouldEqual, 3) }) }) } func TestMetaDBWhenSigningImages(t *testing.T) { Convey("SigningImages", t, func() { subpath := "/a" dir := t.TempDir() subDir := t.TempDir() subRootDir := path.Join(subDir, subpath) port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = dir conf.Storage.SubPaths = make(map[string]config.StorageConfig) conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir} defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() // push test images to repo 1 image 1 image1 := CreateDefaultImage() err := UploadImage(image1, baseURL, "repo1", "1.0.1") So(err, ShouldBeNil) err = UploadImage(image1, baseURL, "repo1", "2.0.2") So(err, ShouldBeNil) manifestBlob, err := json.Marshal(image1.Manifest) So(err, ShouldBeNil) manifestDigest := godigest.FromBytes(manifestBlob) multiArch := CreateRandomMultiarch() err = UploadMultiarchImage(multiArch, baseURL, "repo1", "index") So(err, ShouldBeNil) queryImage1 := ` { GlobalSearch(query:"repo1:1.0"){ Images { RepoName Tag LastUpdated Size IsSigned Manifests{ LastUpdated Size } } } }` queryImage2 := ` { GlobalSearch(query:"repo1:2.0"){ Images { 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 = signature.SignImageUsingCosign("repo1:1.0.1", port, false) So(err, ShouldBeNil) resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryImage1)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images[0].IsSigned, ShouldBeTrue) // check image 2 is signed also because it has the same manifest resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryImage2)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images[0].IsSigned, ShouldBeTrue) // delete the signature resp, err = resty.R().Delete(baseURL + "/v2/" + "repo1" + "/manifests/" + fmt.Sprintf("sha256-%s.sig", manifestDigest.Encoded())) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) // check image 2 is not signed anymore resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryImage2)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images[0].IsSigned, ShouldBeFalse) }) Convey("Cover errors when signing with cosign", func() { Convey("imageIsSignature fails", func() { // make image store ignore the wrong format of the input ctlr.StoreController.DefaultStore = mocks.MockedImageStore{ PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, godigest.Digest, error, ) { return "", "", nil }, DeleteImageManifestFn: func(repo, reference string, dc bool) error { return ErrTestError }, } // push bad manifest blob resp, err := resty.R(). SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). SetBody([]byte("unmashable manifest blob")). Put(baseURL + "/v2/" + "repo" + "/manifests/" + "tag") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) }) Convey("image is a signature, AddManifestSignature fails", func() { ctlr.MetaDB = mocks.MetaDBMock{ AddManifestSignatureFn: func(repo string, signedManifestDigest godigest.Digest, sm mTypes.SignatureMetadata, ) error { return ErrTestError }, } err := signature.SignImageUsingCosign("repo1:1.0.1", port, false) So(err, ShouldNotBeNil) }) }) Convey("Sign with notation", func() { err = signature.SignImageUsingNotary("repo1:1.0.1", port, true) So(err, ShouldBeNil) resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryImage1)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images[0].IsSigned, ShouldBeTrue) }) Convey("Sign with notation index", func() { err = signature.SignImageUsingNotary("repo1:index", port, false) 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 := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images[0].IsSigned, ShouldBeTrue) }) Convey("Sign with cosign index", func() { err = signature.SignImageUsingCosign("repo1:index", port, false) 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 := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images[0].IsSigned, ShouldBeTrue) }) }) } func TestMetaDBWhenPushingImages(t *testing.T) { Convey("Cover errors when pushing", t, func() { dir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = dir 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() Convey("SetManifestMeta succeeds but SetRepoReference fails", func() { ctlr.MetaDB = mocks.MetaDBMock{ SetRepoReferenceFn: func(ctx context.Context, repo, reference string, imageMeta mTypes.ImageMeta) error { return ErrTestError }, } image := CreateRandomImage() ctlr.StoreController.DefaultStore = mocks.MockedImageStore{ NewBlobUploadFn: ctlr.StoreController.DefaultStore.NewBlobUpload, PutBlobChunkFn: ctlr.StoreController.DefaultStore.PutBlobChunk, GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) { return image.ConfigDescriptor.Data, nil }, } err := UploadImage(image, baseURL, "repo1", "1.0.1") So(err, ShouldNotBeNil) }) }) } func TestMetaDBIndexOperations(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() RunMetaDBIndexTests(baseURL, port) }) } func RunMetaDBIndexTests(baseURL, port string) { Convey("Push test index", func() { const repo = "repo" multiarchImage := CreateRandomMultiarch() err := UploadMultiarchImage(multiarchImage, baseURL, repo, "tag1") 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 := &zcommon.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 = signature.SignImageUsingCosign(fmt.Sprintf("repo@%s", multiarchImage.DigestStr()), port, false) 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 = &zcommon.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-" + multiarchImage.Digest().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 = &zcommon.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 := CreateImageWith().LayerBlobs([][]byte{ {10, 20, 30}, {11, 21, 31}, }).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "linux", Architecture: "amd64", }, }).Build() imageSomeArch := CreateImageWith().LayerBlobs([][]byte{ {18, 28, 38}, {12, 22, 32}, }).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "linux", Architecture: "someArch", }, }).Build() multiImage := CreateMultiarchWith().Images([]Image{imageAMD64, imageSomeArch}).Build() err := UploadMultiarchImage(multiImage, baseURL, "test-repo", "latest") So(err, ShouldBeNil) // ---------------- BASE IMAGE ------------------- // ---------------- SAME LAYERS ------------------- image1 := CreateImageWith().LayerBlobs([][]byte{{0, 0, 2}}).ImageConfig(imageSomeArch.Config).Build() image2 := CreateImageWith().LayerBlobs(imageAMD64.Layers).ImageConfig(imageAMD64.Config).Build() multiImage = CreateMultiarchWith().Images([]Image{image1, image2}).Build() err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-same-layers", "index-one-arch-same-layers") So(err, ShouldBeNil) // ---------------- SAME LAYERS ------------------- // ---------------- LESS LAYERS ------------------- image1 = CreateImageWith().LayerBlobs([][]byte{ {3, 2, 2}, {5, 2, 5}, }).ImageConfig(imageSomeArch.Config).Build() image2 = CreateImageWith().LayerBlobs([][]byte{ imageAMD64.Layers[0], }).ImageConfig(imageAMD64.Config).Build() multiImage = CreateMultiarchWith().Images([]Image{image1, image2}).Build() err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-less-layers", "index-one-arch-less-layers") So(err, ShouldBeNil) // ---------------- LESS LAYERS ------------------- // ---------------- LESS LAYERS FALSE ------------------- image1 = CreateImageWith().LayerBlobs([][]byte{ {3, 2, 2}, {5, 2, 5}, }).ImageConfig(imageSomeArch.Config).Build() auxLayer := imageAMD64.Layers[0] auxLayer[0] = 20 image2 = CreateImageWith().LayerBlobs([][]byte{ auxLayer, }).ImageConfig(imageAMD64.Config).Build() multiImage = CreateMultiarchWith().Images([]Image{image1, image2}).Build() err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-less-layers-false", "index-one-arch-less-layers-false") So(err, ShouldBeNil) // ---------------- LESS LAYERS FALSE ------------------- // ---------------- MORE LAYERS ------------------- image1 = CreateImageWith().LayerBlobs([][]byte{ {0, 0, 2}, {3, 0, 2}, }).ImageConfig(imageSomeArch.Config).Build() image2 = CreateImageWith().LayerBlobs( append(imageAMD64.Layers, []byte{1, 3, 55}), ).ImageConfig(imageAMD64.Config).Build() multiImage = CreateMultiarchWith().Images([]Image{image1, image2}).Build() err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-more-layers", "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 := CreateImageWith().LayerBlobs([][]byte{ {10, 20, 30}, {11, 21, 31}, }).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "linux", Architecture: "amd64", }, }).Build() baseLinuxAMD64Digest := imageAMD64.ManifestDescriptor.Digest imageSomeArch := CreateImageWith().LayerBlobs([][]byte{ {18, 28, 38}, {12, 22, 32}, }).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "linux", Architecture: "someArch", }, }).Build() baseLinuxSomeArchDigest := imageSomeArch.ManifestDescriptor.Digest multiImage := CreateMultiarchWith().Images([]Image{imageAMD64, imageSomeArch}).Build() err := UploadMultiarchImage(multiImage, baseURL, "test-repo", "index") So(err, ShouldBeNil) // ---------------- BASE IMAGE FOR LINUX AMD64 ------------------- image := CreateImageWith().LayerBlobs([][]byte{ imageAMD64.Layers[0], }).ImageConfig(imageAMD64.Config).Build() err = UploadImage(image, baseURL, "test-repo", "less-layers-linux-amd64") So(err, ShouldBeNil) // ---------------- BASE IMAGE FOR LINUX SOMEARCH ------------------- image = CreateImageWith().LayerBlobs([][]byte{ imageSomeArch.Layers[0], }).ImageConfig(imageAMD64.Config).Build() err = UploadImage(image, baseURL, "test-repo", "less-layers-linux-somearch") 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 := CreateImageWith().LayerBlobs([][]byte{ {10, 20, 30}, {11, 21, 31}, }).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "linux", Architecture: "amd64", }, }).Build() imageSomeArch := CreateImageWith().LayerBlobs([][]byte{ {18, 28, 38}, {12, 22, 32}, }).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "linux", Architecture: "someArch", }, }).Build() multiImage := CreateMultiarchWith().Images([]Image{imageAMD64, imageSomeArch}).Build() err := UploadMultiarchImage(multiImage, baseURL, "test-repo", "latest") So(err, ShouldBeNil) // ---------------- BASE IMAGE ------------------- // ---------------- SAME LAYERS ------------------- image1 := CreateImageWith().LayerBlobs([][]byte{ {0, 0, 2}, }).ImageConfig(imageSomeArch.Config).Build() image2 := CreateImageWith().LayerBlobs( imageAMD64.Layers, ).ImageConfig(imageAMD64.Config).Build() multiImage = CreateMultiarchWith().Images([]Image{image1, image2}).Build() err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-same-layers", "index-one-arch-same-layers") So(err, ShouldBeNil) // ---------------- SAME LAYERS ------------------- // ---------------- LESS LAYERS ------------------- image1 = CreateImageWith().LayerBlobs([][]byte{ {3, 2, 2}, {5, 2, 5}, }).ImageConfig(imageSomeArch.Config).Build() image2 = CreateImageWith().LayerBlobs([][]byte{ imageAMD64.Layers[0], }).ImageConfig(imageAMD64.Config).Build() multiImage = CreateMultiarchWith().Images([]Image{image1, image2}).Build() err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-less-layers", "index-one-arch-less-layers") So(err, ShouldBeNil) // ---------------- LESS LAYERS ------------------- // ---------------- LESS LAYERS FALSE ------------------- image1 = CreateImageWith().LayerBlobs([][]byte{ {3, 2, 2}, {5, 2, 5}, }).ImageConfig(imageSomeArch.Config).Build() image2 = CreateImageWith().LayerBlobs([][]byte{ {99, 100, 102}, }).ImageConfig(imageAMD64.Config).Build() multiImage = CreateMultiarchWith().Images([]Image{image1, image2}).Build() err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-less-layers-false", "index-one-arch-less-layers-false") So(err, ShouldBeNil) // ---------------- LESS LAYERS FALSE ------------------- // ---------------- MORE LAYERS ------------------- image1 = CreateImageWith().LayerBlobs([][]byte{ {0, 0, 2}, {3, 0, 2}, }).ImageConfig(imageSomeArch.Config).Build() image2 = CreateImageWith().LayerBlobs([][]byte{ imageAMD64.Layers[0], imageAMD64.Layers[1], {1, 3, 55}, }).ImageConfig(imageAMD64.Config).Build() multiImage = CreateMultiarchWith().Images([]Image{image1, image2}).Build() err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-more-layers", "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 := CreateImageWith().LayerBlobs([][]byte{ {10, 20, 30}, {11, 21, 31}, }).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "linux", Architecture: "amd64", }, }).Build() baseLinuxAMD64Digest := imageAMD64.ManifestDescriptor.Digest imageSomeArch := CreateImageWith().LayerBlobs([][]byte{ {18, 28, 38}, {12, 22, 32}, }).ImageConfig(ispec.Image{ Platform: ispec.Platform{ OS: "linux", Architecture: "someArch", }, }).Build() baseLinuxSomeArchDigest := imageSomeArch.ManifestDescriptor.Digest multiImage := CreateMultiarchWith().Images([]Image{imageAMD64, imageSomeArch}).Build() err := UploadMultiarchImage(multiImage, baseURL, "test-repo", "index") So(err, ShouldBeNil) // ---------------- BASE IMAGE FOR LINUX AMD64 ------------------- image := CreateImageWith().LayerBlobs([][]byte{ imageAMD64.Layers[0], imageAMD64.Layers[1], {0, 0, 0, 0}, {1, 1, 1, 1}, }).ImageConfig(imageAMD64.Config).Build() err = UploadImage(image, baseURL, "test-repo", "more-layers-linux-amd64") So(err, ShouldBeNil) // ---------------- BASE IMAGE FOR LINUX SOMEARCH ------------------- image = CreateImageWith().LayerBlobs([][]byte{ imageSomeArch.Layers[0], imageSomeArch.Layers[1], {3, 3, 3, 3}, {2, 2, 2, 2}, }).ImageConfig(imageAMD64.Config).Build() err = UploadImage(image, baseURL, "test-repo", "more-layers-linux-somearch") 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 TestMetaDBWhenReadingImages(t *testing.T) { Convey("Push test image", t, func() { dir := t.TempDir() port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = dir 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() image := CreateImageWith().RandomLayers(1, 100).DefaultConfig().Build() err := UploadImage(image, baseURL, "repo1", "1.0.1") So(err, ShouldBeNil) Convey("Download 3 times", func() { resp, err := resty.R().Get(baseURL + "/v2/" + "repo1" + "/manifests/" + "1.0.1") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) resp, err = resty.R().Get(baseURL + "/v2/" + "repo1" + "/manifests/" + "1.0.1") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) resp, err = resty.R().Get(baseURL + "/v2/" + "repo1" + "/manifests/" + "1.0.1") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) query := ` { GlobalSearch(query:"repo1:1.0"){ Images { RepoName Tag DownloadCount } } }` resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images, ShouldNotBeEmpty) So(responseStruct.Images[0].DownloadCount, ShouldEqual, 3) }) Convey("Error when incrementing", func() { ctlr.MetaDB = mocks.MetaDBMock{ UpdateStatsOnDownloadFn: func(repo string, tag string) error { return ErrTestError }, } resp, err := resty.R().Get(baseURL + "/v2/" + "repo1" + "/manifests/" + "1.0.1") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) }) }) } func TestMetaDBWhenDeletingImages(t *testing.T) { Convey("Setting up zot repo with test images", 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}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() // push test images to repo 1 image 1 image1 := CreateRandomImage() err := UploadImage(image1, baseURL, "repo1", "1.0.1") So(err, ShouldBeNil) // push test images to repo 1 image 2 createdTime2 := time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC) image2 := CreateImageWith().RandomLayers(1, 100).ImageConfig(ispec.Image{ Created: &createdTime2, History: []ispec.History{ { Created: &createdTime2, }, }, }).Build() err = UploadImage(image2, baseURL, "repo1", "1.0.2") So(err, ShouldBeNil) query := ` { GlobalSearch(query:"repo1:1.0"){ Images { RepoName Tag LastUpdated Size IsSigned Manifests{ Platform { Os Arch } 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 := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Images), ShouldEqual, 2) Convey("Delete a normal tag", func() { resp, err := resty.R().Delete(baseURL + "/v2/" + "repo1" + "/manifests/" + "1.0.1") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Images), ShouldEqual, 1) So(responseStruct.Images[0].Tag, ShouldEqual, "1.0.2") }) Convey("Delete a cosign signature", func() { repo := "repo1" err := signature.SignImageUsingCosign("repo1:1.0.1", port, false) So(err, ShouldBeNil) query := ` { GlobalSearch(query:"repo1:1.0.1"){ Images { RepoName Tag LastUpdated Size IsSigned Manifests{ Platform { Os Arch } LastUpdated Size } } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images[0].IsSigned, ShouldBeTrue) // get signatur digest log := log.NewLogger("debug", "") metrics := monitoring.NewMetricsServer(false, log) storage := local.NewImageStore(dir, false, false, log, metrics, nil, nil) indexBlob, err := storage.GetIndexContent(repo) So(err, ShouldBeNil) var indexContent ispec.Index err = json.Unmarshal(indexBlob, &indexContent) So(err, ShouldBeNil) signatureTag := "" for _, manifest := range indexContent.Manifests { tag := manifest.Annotations[ispec.AnnotationRefName] if zcommon.IsCosignTag(tag) { signatureTag = tag } } // delete the signature using the digest resp, err = resty.R().Delete(baseURL + "/v2/" + "repo1" + "/manifests/" + signatureTag) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) // verify isSigned again and it should be false resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images[0].IsSigned, ShouldBeFalse) }) Convey("Delete a notary signature", func() { repo := "repo1" err := signature.SignImageUsingNotary("repo1:1.0.1", port, true) So(err, ShouldBeNil) query := ` { GlobalSearch(query:"repo1:1.0.1"){ Images { RepoName Tag LastUpdated Size IsSigned Manifests{ Platform { Os Arch } LastUpdated Size } } } }` // test if it's signed resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images[0].IsSigned, ShouldBeTrue) // get signatur digest log := log.NewLogger("debug", "") metrics := monitoring.NewMetricsServer(false, log) storage := local.NewImageStore(dir, false, false, log, metrics, nil, nil) indexBlob, err := storage.GetIndexContent(repo) So(err, ShouldBeNil) var indexContent ispec.Index err = json.Unmarshal(indexBlob, &indexContent) So(err, ShouldBeNil) signatureReference := "" var sigManifestContent ispec.Manifest for _, manifest := range indexContent.Manifests { manifestBlob, _, _, err := storage.GetImageManifest(repo, manifest.Digest.String()) So(err, ShouldBeNil) var manifestContent ispec.Manifest err = json.Unmarshal(manifestBlob, &manifestContent) So(err, ShouldBeNil) if zcommon.GetManifestArtifactType(manifestContent) == notreg.ArtifactTypeNotation { signatureReference = manifest.Digest.String() manifestBlob, _, _, err := storage.GetImageManifest(repo, signatureReference) So(err, ShouldBeNil) err = json.Unmarshal(manifestBlob, &sigManifestContent) So(err, ShouldBeNil) } } So(sigManifestContent, ShouldNotBeZeroValue) // check notation signature manifest1Blob, err := json.Marshal(image1.Manifest) So(err, ShouldBeNil) manifest1Digest := godigest.FromBytes(manifest1Blob) So(sigManifestContent.Subject, ShouldNotBeNil) So(sigManifestContent.Subject.Digest.String(), ShouldEqual, manifest1Digest.String()) // delete the signature using the digest resp, err = resty.R().Delete(baseURL + "/v2/" + "repo1" + "/manifests/" + signatureReference) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) // verify isSigned again and it should be false resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(responseStruct.Images[0].IsSigned, ShouldBeFalse) }) Convey("Delete a referrer", func() { referredImageDescriptor := &ispec.Descriptor{ Digest: image1.ManifestDescriptor.Digest, MediaType: ispec.MediaTypeImageManifest, } referrerImage := CreateImageWith().RandomLayers(1, 100).DefaultConfig(). Subject(referredImageDescriptor).Build() err = UploadImage(referrerImage, baseURL, "repo1", referrerImage.DigestStr()) So(err, ShouldBeNil) // ------- check referrers for this image query := fmt.Sprintf(` { Referrers(repo:"repo1", digest:"%s"){ MediaType Digest } }`, image1.ManifestDescriptor.Digest.String()) resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.ReferrersResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Referrers), ShouldEqual, 1) So(responseStruct.Referrers[0].Digest, ShouldResemble, referrerImage.DigestStr()) statusCode, err := DeleteImage("repo1", referrerImage.DigestStr(), "badURL") So(err, ShouldNotBeNil) So(statusCode, ShouldEqual, -1) // ------- Delete the referrer and see if it disappears from metaDB also statusCode, err = DeleteImage("repo1", referrerImage.DigestStr(), baseURL) So(err, ShouldBeNil) So(statusCode, ShouldEqual, http.StatusAccepted) resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct = &zcommon.ReferrersResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Referrers), ShouldEqual, 0) }) Convey("Deleting causes errors", func() { Convey("error while backing up the manifest", func() { ctlr.StoreController.DefaultStore = mocks.MockedImageStore{ GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) { return []byte{}, "", "", zerr.ErrRepoNotFound }, } resp, err = resty.R().Delete(baseURL + "/v2/" + "repo1" + "/manifests/" + "signatureReference") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) ctlr.StoreController.DefaultStore = mocks.MockedImageStore{ GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) { return []byte{}, "", "", zerr.ErrBadManifest }, } resp, err = resty.R().Delete(baseURL + "/v2/" + "repo1" + "/manifests/" + "signatureReference") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) ctlr.StoreController.DefaultStore = mocks.MockedImageStore{ GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) { return []byte{}, "", "", zerr.ErrRepoNotFound }, } resp, err = resty.R().Delete(baseURL + "/v2/" + "repo1" + "/manifests/" + "signatureReference") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) }) Convey("imageIsSignature fails", func() { ctlr.StoreController.DefaultStore = mocks.MockedImageStore{ PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, godigest.Digest, error, ) { return "", "", nil }, DeleteImageManifestFn: func(repo, reference string, dc bool) error { return nil }, } resp, err = resty.R().Delete(baseURL + "/v2/" + "repo1" + "/manifests/" + "signatureReference") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) }) Convey("image is a signature, DeleteSignature fails", func() { ctlr.StoreController.DefaultStore = mocks.MockedImageStore{ NewBlobUploadFn: ctlr.StoreController.DefaultStore.NewBlobUpload, PutBlobChunkFn: ctlr.StoreController.DefaultStore.PutBlobChunk, GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) { configBlob, err := json.Marshal(ispec.Image{}) So(err, ShouldBeNil) return configBlob, nil }, PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, godigest.Digest, error, ) { return "", "", nil }, DeleteImageManifestFn: func(repo, reference string, dc bool) error { return nil }, GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) { return []byte("{}"), "1", "1", nil }, } resp, err = resty.R().Delete(baseURL + "/v2/" + "repo1" + "/manifests/" + "sha256-343ebab94a7674da181c6ea3da013aee4f8cbe357870f8dcaf6268d5343c3474.sig") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) }) Convey("image is a signature, PutImageManifest fails", func() { ctlr.StoreController.DefaultStore = mocks.MockedImageStore{ NewBlobUploadFn: ctlr.StoreController.DefaultStore.NewBlobUpload, PutBlobChunkFn: ctlr.StoreController.DefaultStore.PutBlobChunk, GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) { configBlob, err := json.Marshal(ispec.Image{}) So(err, ShouldBeNil) return configBlob, nil }, PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, godigest.Digest, error, ) { return "", "", ErrTestError }, DeleteImageManifestFn: func(repo, reference string, dc bool) error { return nil }, GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) { return []byte("{}"), "1", "1", nil }, } ctlr.MetaDB = mocks.MetaDBMock{ RemoveRepoReferenceFn: func(repo, reference string, manifestDigest godigest.Digest, ) error { return ErrTestError }, } resp, err = resty.R().Delete(baseURL + "/v2/" + "repo1" + "/manifests/" + "343ebab94a7674da181c6ea3da013aee4f8cbe357870f8dcaf6268d5343c3474.sig") So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError) }) }) }) } func TestSearchSize(t *testing.T) { Convey("Repo sizes", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port tr := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &tr}}, } ctlr := api.NewController(conf) dir := t.TempDir() ctlr.Config.Storage.RootDirectory = dir ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() repoName := "testrepo" uploadedImage := CreateImageWith().RandomLayers(1, 10000).DefaultConfig().Build() configSize := uploadedImage.ConfigDescriptor.Size manifestSize := uploadedImage.ManifestDescriptor.Size layersSize := int64(0) for _, l := range uploadedImage.Layers { layersSize += int64(len(l)) } err := UploadImage(uploadedImage, baseURL, repoName, "latest") So(err, ShouldBeNil) query := ` { GlobalSearch(query:"testrepo:"){ Images { RepoName Tag LastUpdated Size Vendor Manifests{ Platform { Os Arch } LastUpdated Size } } Repos { Name LastUpdated Size NewestImage { Manifests{ Platform { Os Arch } LastUpdated 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 := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) image := responseStruct.GlobalSearchResult.GlobalSearch.Images[0] So(image.Tag, ShouldResemble, "latest") size, err := strconv.Atoi(image.Size) So(err, ShouldBeNil) So(size, ShouldEqual, configSize+layersSize+manifestSize) query = ` { GlobalSearch(query:"testrepo"){ Images { RepoName Tag LastUpdated Size Manifests{ Platform { Os Arch } LastUpdated Size } } Repos { Name LastUpdated Size NewestImage { Manifests{ Platform { Os Arch } LastUpdated 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 = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) repo := responseStruct.GlobalSearchResult.GlobalSearch.Repos[0] size, err = strconv.Atoi(repo.Size) So(err, ShouldBeNil) So(size, ShouldEqual, configSize+layersSize+manifestSize) // add the same image with different tag err = UploadImage(uploadedImage, baseURL, repoName, "10.2.14") So(err, ShouldBeNil) // query for images query = ` { GlobalSearch(query:"testrepo:"){ 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 = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Images), ShouldEqual, 2) // check that the repo size is the same // query for repos query = ` { GlobalSearch(query:"testrepo"){ 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 = &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) repo = responseStruct.GlobalSearchResult.GlobalSearch.Repos[0] size, err = strconv.Atoi(repo.Size) So(err, ShouldBeNil) So(size, ShouldEqual, configSize+layersSize+manifestSize) }) } func TestImageSummary(t *testing.T) { Convey("GraphQL query ImageSummary", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = t.TempDir() conf.Storage.GC = false defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) gqlQuery := ` { Image(image:"%s:%s"){ RepoName Tag Digest MediaType Manifests { Digest ConfigDigest LastUpdated Size Platform { Os Arch } Layers { Digest Size } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } History { HistoryDescription { Created } Layer { Digest Size } } } LastUpdated Size Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } Referrers {MediaType ArtifactType Digest Annotations {Key Value}} } }` noTagQuery := ` { Image(image:"%s"){ RepoName, Tag, Digest, MediaType, Manifests { Digest ConfigDigest LastUpdated Size Platform { Os Arch } Layers { Digest Size } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } History { HistoryDescription { Created } Layer { Digest Size } } }, Size } }` gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, graphqlQueryPrefix) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() repoName := "test-repo" //nolint:goconst tagTarget := "latest" createdTime := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC) image := CreateImageWith().RandomLayers(1, 100).ImageConfig(ispec.Image{ History: []ispec.History{{Created: &createdTime}}, Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, }).Build() manifestDigest := image.ManifestDescriptor.Digest err := UploadImage(image, baseURL, repoName, tagTarget) So(err, ShouldBeNil) // ------ Add a referrer referrerImage := CreateImageWith().RandomLayers(1, 100). CustomConfigBlob( []byte{}, "application/test.artifact.type", ). Subject(&ispec.Descriptor{ Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest, }).Annotations(map[string]string{"testAnnotationKey": "testAnnotationValue"}). Build() referrerManifestDigest := referrerImage.Digest() err = UploadImage(referrerImage, baseURL, repoName, referrerManifestDigest.String()) So(err, ShouldBeNil) var ( imgSummaryResponse zcommon.ImageSummaryResult strQuery string targetURL string resp *resty.Response ) t.Log("starting test to retrieve image without reference") strQuery = fmt.Sprintf(noTagQuery, repoName) targetURL = fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery)) contains := false resp, err = resty.R().Get(targetURL) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) err = json.Unmarshal(resp.Body(), &imgSummaryResponse) So(err, ShouldBeNil) for _, err := range imgSummaryResponse.Errors { result := strings.Contains(err.Message, "no reference provided") if result { contains = result } } So(contains, ShouldBeTrue) t.Log("starting Test retrieve image based on image identifier") // gql is parametrized with the repo. strQuery = fmt.Sprintf(gqlQuery, repoName, tagTarget) targetURL = fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery)) resp, err = resty.R().Get(targetURL) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) err = json.Unmarshal(resp.Body(), &imgSummaryResponse) So(err, ShouldBeNil) So(imgSummaryResponse, ShouldNotBeNil) So(imgSummaryResponse.SingleImageSummary, ShouldNotBeNil) So(imgSummaryResponse.ImageSummary, ShouldNotBeNil) imgSummary := imgSummaryResponse.SingleImageSummary.ImageSummary So(imgSummary.RepoName, ShouldContainSubstring, repoName) So(imgSummary.Tag, ShouldContainSubstring, tagTarget) So(imgSummary.Digest, ShouldContainSubstring, manifestDigest.Encoded()) So(imgSummary.MediaType, ShouldContainSubstring, ispec.MediaTypeImageManifest) So(imgSummary.Manifests[0].ConfigDigest, ShouldContainSubstring, image.Manifest.Config.Digest.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, image.Manifest.Layers[0].Digest.Encoded()) So(imgSummary.LastUpdated, ShouldEqual, createdTime) So(imgSummary.IsSigned, ShouldEqual, false) 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.UnknownCount, ShouldEqual, 0) So(imgSummary.Vulnerabilities.LowCount, ShouldEqual, 0) So(imgSummary.Vulnerabilities.MediumCount, ShouldEqual, 0) So(imgSummary.Vulnerabilities.HighCount, ShouldEqual, 0) So(imgSummary.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(imgSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "") So(len(imgSummary.Referrers), ShouldEqual, 1) So(imgSummary.Referrers[0], ShouldResemble, zcommon.Referrer{ MediaType: ispec.MediaTypeImageManifest, ArtifactType: "application/test.artifact.type", Digest: referrerManifestDigest.String(), Annotations: []zcommon.Annotation{{Key: "testAnnotationKey", Value: "testAnnotationValue"}}, }) t.Log("starting Test retrieve duplicated image same layers based on image identifier") // gqlEndpoint strQuery = fmt.Sprintf(gqlQuery, "wrong-repo-does-not-exist", "latest") targetURL = fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery)) resp, err = resty.R().Get(targetURL) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) err = json.Unmarshal(resp.Body(), &imgSummaryResponse) So(err, ShouldBeNil) So(imgSummaryResponse, ShouldNotBeNil) So(imgSummaryResponse.SingleImageSummary, ShouldNotBeNil) So(imgSummaryResponse.ImageSummary, ShouldNotBeNil) So(len(imgSummaryResponse.Errors), ShouldEqual, 1) So(imgSummaryResponse.Errors[0].Message, ShouldContainSubstring, "repo metadata not found for given repo name") t.Log("starting Test retrieve image with bad tag") // gql is parametrized with the repo. strQuery = fmt.Sprintf(gqlQuery, repoName, "nonexisttag") targetURL = fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery)) resp, err = resty.R().Get(targetURL) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) err = json.Unmarshal(resp.Body(), &imgSummaryResponse) So(err, ShouldBeNil) So(imgSummaryResponse, ShouldNotBeNil) So(imgSummaryResponse.SingleImageSummary, ShouldNotBeNil) So(imgSummaryResponse.ImageSummary, ShouldNotBeNil) So(len(imgSummaryResponse.Errors), ShouldEqual, 1) So(imgSummaryResponse.Errors[0].Message, ShouldContainSubstring, "can't find image: test-repo:nonexisttag") }) Convey("GraphQL query ImageSummary with Vulnerability scan enabled", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = t.TempDir() defaultVal := true updateDuration, _ := time.ParseDuration("1h") trivyConfig := &extconf.TrivyConfig{ DBRepository: "ghcr.io/project-zot/trivy-db", } cveConfig := &extconf.CVEConfig{ UpdateInterval: updateDuration, Trivy: trivyConfig, } searchConfig := &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, CVE: cveConfig, } conf.Extensions = &extconf.ExtensionConfig{ Search: searchConfig, } ctlr := api.NewController(conf) gqlQuery := ` { Image(image:"%s:%s"){ RepoName Tag Manifests { Digest ConfigDigest LastUpdated Size Platform { Os Arch } Layers { Digest Size } Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } History { HistoryDescription { Created } Layer { Digest Size } } } LastUpdated Size Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } }` gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, graphqlQueryPrefix) createdTime := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC) image := CreateImageWith().DefaultLayers().ImageConfig(ispec.Image{ History: []ispec.History{{Created: &createdTime}}, Platform: ispec.Platform{ Architecture: "amd64", OS: "linux", }, }).Build() manifestDigest := image.ManifestDescriptor.Digest configDigest := image.ConfigDescriptor.Digest if err := ctlr.Init(); err != nil { panic(err) } ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB) go func() { if err := ctlr.Run(); !errors.Is(err, http.ErrServerClosed) { panic(err) } }() defer ctlr.Shutdown() WaitTillServerReady(baseURL) repoName := "test-repo" //nolint:goconst tagTarget := "latest" err := UploadImage(image, baseURL, repoName, tagTarget) So(err, ShouldBeNil) var ( imgSummaryResponse zcommon.ImageSummaryResult strQuery string targetURL string resp *resty.Response ) t.Log("starting Test retrieve image based on image identifier") // gql is parametrized with the repo. strQuery = fmt.Sprintf(gqlQuery, repoName, tagTarget) targetURL = fmt.Sprintf("%s%s", gqlEndpoint, url.QueryEscape(strQuery)) resp, err = resty.R().Get(targetURL) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) err = json.Unmarshal(resp.Body(), &imgSummaryResponse) So(err, ShouldBeNil) So(imgSummaryResponse, ShouldNotBeNil) So(imgSummaryResponse.SingleImageSummary, ShouldNotBeNil) So(imgSummaryResponse.ImageSummary, ShouldNotBeNil) imgSummary := imgSummaryResponse.ImageSummary So(imgSummary.RepoName, ShouldContainSubstring, repoName) So(imgSummary.Tag, ShouldContainSubstring, tagTarget) So(imgSummary.Manifests[0].ConfigDigest, ShouldContainSubstring, configDigest.Encoded()) So(imgSummary.Manifests[0].Digest, ShouldContainSubstring, manifestDigest.Encoded()) So(len(imgSummary.Manifests[0].Layers), ShouldEqual, 3) So(imgSummary.Manifests[0].Layers[0].Digest, ShouldContainSubstring, godigest.FromBytes(image.Layers[0]).Encoded()) So(imgSummary.LastUpdated, ShouldEqual, createdTime) So(imgSummary.IsSigned, ShouldEqual, false) 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) So(imgSummary.Vulnerabilities.UnknownCount, ShouldEqual, 0) So(imgSummary.Vulnerabilities.LowCount, ShouldEqual, 1) So(imgSummary.Vulnerabilities.MediumCount, ShouldEqual, 1) So(imgSummary.Vulnerabilities.HighCount, ShouldEqual, 1) So(imgSummary.Vulnerabilities.CriticalCount, ShouldEqual, 1) // There are 0 vulnerabilities this data used in tests So(imgSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") }) Convey("GraphQL query for Artifact Type", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = t.TempDir() conf.Storage.GC = false defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, } conf.Extensions.Search.CVE = nil ctlr := api.NewController(conf) query := ` { Image(image:"repo:art%d"){ RepoName Tag Manifests { Digest ArtifactType } Size } }` queryImg1 := fmt.Sprintf(query, 1) queryImg2 := fmt.Sprintf(query, 2) var imgSummaryResponse zcommon.ImageSummaryResult ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() // upload the images artType1 := "application/test.signature.v1" artType2 := "application/test.signature.v2" img1 := CreateImageWith().RandomLayers(1, 20).EmptyConfig().ArtifactType(artType1).Build() err := UploadImage(img1, baseURL, "repo", "art1") So(err, ShouldBeNil) img2 := CreateImageWith().RandomLayers(1, 20).DefaultConfig().ArtifactType(artType2).Build() err = UploadImage(img2, baseURL, "repo", "art2") So(err, ShouldBeNil) // GET image 1 resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryImg1)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) err = json.Unmarshal(resp.Body(), &imgSummaryResponse) So(err, ShouldBeNil) imgSum := imgSummaryResponse.SingleImageSummary.ImageSummary So(len(imgSum.Manifests), ShouldEqual, 1) So(imgSum.Manifests[0].ArtifactType, ShouldResemble, artType1) // GET image 2 resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryImg2)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) err = json.Unmarshal(resp.Body(), &imgSummaryResponse) So(err, ShouldBeNil) imgSum = imgSummaryResponse.SingleImageSummary.ImageSummary So(len(imgSum.Manifests), ShouldEqual, 1) So(imgSum.Manifests[0].ArtifactType, ShouldResemble, artType2) // Expanded repo info test queryExpRepoInfo := `{ ExpandedRepoInfo(repo:"test1"){ Images { Tag Manifests { Digest ArtifactType } } } }` var expandedRepoInfoResp zcommon.ExpandedRepoInfoResp resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryExpRepoInfo)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeNil) err = json.Unmarshal(resp.Body(), &expandedRepoInfoResp) So(err, ShouldBeNil) imgSums := expandedRepoInfoResp.ExpandedRepoInfo.RepoInfo.ImageSummaries for _, imgSum := range imgSums { switch imgSum.Digest { case img1.ManifestDescriptor.Digest.String(): So(imgSum.Manifests[0].ArtifactType, ShouldResemble, artType1) case img2.ManifestDescriptor.Digest.String(): So(imgSum.Manifests[0].ArtifactType, ShouldResemble, artType2) } } }) } func TestUploadingArtifactsWithDifferentMediaType(t *testing.T) { Convey("", t, func() { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = t.TempDir() conf.Storage.GC = false defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, CVE: nil}, } conf.Log = &config.LogConfig{Level: "debug", Output: "/dev/null"} ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() const customMediaType = "application/custom.media.type+json" imageWithIncompatibleConfig := CreateImageWith().DefaultLayers(). CustomConfigBlob([]byte(`{"author": {"key": "val"}}`), customMediaType).Build() defaultImage := CreateDefaultImage() var configContent ispec.Image err := json.Unmarshal(imageWithIncompatibleConfig.ConfigDescriptor.Data, &configContent) So(err, ShouldNotBeNil) err = UploadImage(imageWithIncompatibleConfig, baseURL, "repo", "incompatible-image") So(err, ShouldBeNil) err = UploadImage(defaultImage, baseURL, "repo", "default-image") So(err, ShouldBeNil) query := ` { GlobalSearch(query:"repo:incompatible-image"){ Images { RepoName Tag Manifests { Digest ConfigDigest } } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) So(len(responseStruct.Images), ShouldEqual, 1) So(responseStruct.Images[0].Manifests[0].Digest, ShouldResemble, imageWithIncompatibleConfig.ManifestDescriptor.Digest.String()) So(responseStruct.Images[0].Manifests[0].ConfigDigest, ShouldResemble, imageWithIncompatibleConfig.ConfigDescriptor.Digest.String()) }) } func TestReadUploadDeleteDynamoDB(t *testing.T) { tskip.SkipDynamo(t) uuid, err := guuid.NewV4() if err != nil { panic(err) } cacheTablename := "BlobTable" + uuid.String() repoMetaTablename := "RepoMetadataTable" + uuid.String() versionTablename := "Version" + uuid.String() userDataTablename := "UserDataTable" + uuid.String() apiKeyTablename := "ApiKeyTable" + uuid.String() imageMetaTablename := "ImageMeta" + uuid.String() repoBlobsTablename := "RepoBlobs" + uuid.String() cacheDriverParams := map[string]interface{}{ "name": "dynamoDB", "endpoint": os.Getenv("DYNAMODBMOCK_ENDPOINT"), "region": "us-east-2", "cachetablename": cacheTablename, "repometatablename": repoMetaTablename, "imagemetatablename": imageMetaTablename, "repoblobsinfotablename": repoBlobsTablename, "userdatatablename": userDataTablename, "apikeytablename": apiKeyTablename, "versiontablename": versionTablename, } port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = t.TempDir() conf.Storage.GC = false conf.Storage.CacheDriver = cacheDriverParams conf.Storage.RemoteCache = true conf.Log = &config.LogConfig{Level: "debug", Output: "/dev/null"} defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, CVE: nil}, } ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() RunReadUploadDeleteTests(t, baseURL) } func TestReadUploadDeleteBoltDB(t *testing.T) { port := GetFreePort() baseURL := GetBaseURL(port) conf := config.New() conf.HTTP.Port = port conf.Storage.RootDirectory = t.TempDir() conf.Storage.GC = false conf.Log = &config.LogConfig{Level: "debug", Output: "/dev/null"} defaultVal := true conf.Extensions = &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, CVE: nil}, } ctlr := api.NewController(conf) ctlrManager := NewControllerManager(ctlr) ctlrManager.StartAndWait(port) defer ctlrManager.StopServer() RunReadUploadDeleteTests(t, baseURL) } func RunReadUploadDeleteTests(t *testing.T, baseURL string) { t.Helper() repo1 := "repo1" image := CreateRandomImage() tag1 := "tag1" imageWithoutTag := CreateRandomImage() usedImages := []repoRef{ {repo1, tag1}, {repo1, imageWithoutTag.DigestStr()}, } Convey("Push-Read-Delete", t, func() { results := GlobalSearchGQL("", baseURL) So(len(results.Images), ShouldEqual, 0) So(len(results.Repos), ShouldEqual, 0) Convey("Push an image without tag", func() { err := UploadImage(imageWithoutTag, baseURL, repo1, imageWithoutTag.DigestStr()) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 0) Convey("Add tag and delete it", func() { err := UploadImage(image, baseURL, repo1, tag1) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) status, err := DeleteImage(repo1, tag1, baseURL) So(status, ShouldEqual, http.StatusAccepted) So(err, ShouldBeNil) results = GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 0) }) }) Convey("Push a random image", func() { err := UploadImage(image, baseURL, repo1, tag1) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) Convey("Delete the image pushed", func() { status, err := DeleteImage(repo1, tag1, baseURL) So(status, ShouldEqual, http.StatusAccepted) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 0) Convey("Push an image without tag", func() { err := UploadImage(imageWithoutTag, baseURL, repo1, imageWithoutTag.DigestStr()) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 0) }) }) Convey("Delete the image pushed multiple times", func() { for i := 0; i < 3; i++ { status, err := DeleteImage(repo1, tag1, baseURL) So(status, ShouldBeIn, []int{http.StatusAccepted, http.StatusNotFound, http.StatusBadRequest}) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 0) } }) Convey("Upload same image multiple times", func() { for i := 0; i < 3; i++ { err := UploadImage(image, baseURL, repo1, tag1) So(err, ShouldBeNil) } results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) }) }) deleteUsedImages(usedImages, baseURL) }) // Images with create time repoLatest := "repo-latest" afterImage := CreateImageWith().DefaultLayers(). ImageConfig(ispec.Image{Created: DateRef(2010, 1, 1, 1, 1, 1, 0, time.UTC)}).Build() tagAfter := "after" middleImage := CreateImageWith().DefaultLayers(). ImageConfig(ispec.Image{Created: DateRef(2005, 1, 1, 1, 1, 1, 0, time.UTC)}).Build() tagMiddle := "middle" beforeImage := CreateImageWith().DefaultLayers(). ImageConfig(ispec.Image{Created: DateRef(2000, 1, 1, 1, 1, 1, 0, time.UTC)}).Build() tagBefore := "before" imageWithoutTag = CreateImageWith().DefaultLayers(). ImageConfig(ispec.Image{Created: DateRef(2020, 1, 1, 1, 1, 1, 0, time.UTC)}).Build() imageWithoutCreateTime := CreateImageWith().DefaultLayers(). ImageConfig(ispec.Image{Created: nil}).Build() tagWithoutTime := "without-time" usedImages = []repoRef{ {repoLatest, tagAfter}, {repoLatest, tagMiddle}, {repoLatest, tagBefore}, {repoLatest, tagWithoutTime}, {repoLatest, imageWithoutTag.DigestStr()}, } Convey("Last Updated Image", t, func() { results := GlobalSearchGQL("", baseURL) So(len(results.Images), ShouldEqual, 0) So(len(results.Repos), ShouldEqual, 0) Convey("Without time", func() { err := UploadImage(imageWithoutCreateTime, baseURL, repoLatest, tagWithoutTime) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, imageWithoutCreateTime.DigestStr()) Convey("Add an image with create time and delete it", func() { err := UploadImage(beforeImage, baseURL, repoLatest, tagBefore) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, beforeImage.DigestStr()) status, err := DeleteImage(repoLatest, tagBefore, baseURL) So(status, ShouldEqual, http.StatusAccepted) So(err, ShouldBeNil) results = GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, imageWithoutCreateTime.DigestStr()) }) }) Convey("Upload middle image", func() { err := UploadImage(middleImage, baseURL, repoLatest, tagMiddle) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, middleImage.DigestStr()) Convey("Upload an image created before", func() { err := UploadImage(beforeImage, baseURL, repoLatest, tagBefore) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, middleImage.DigestStr()) Convey("Upload an image created after", func() { err := UploadImage(afterImage, baseURL, repoLatest, tagAfter) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, afterImage.DigestStr()) Convey("Delete middle then after", func() { status, err := DeleteImage(repoLatest, tagMiddle, baseURL) So(status, ShouldEqual, http.StatusAccepted) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, afterImage.DigestStr()) status, err = DeleteImage(repoLatest, tagAfter, baseURL) So(status, ShouldEqual, http.StatusAccepted) So(err, ShouldBeNil) results = GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, beforeImage.DigestStr()) }) }) }) Convey("Upload an image created after", func() { err := UploadImage(afterImage, baseURL, repoLatest, tagAfter) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, afterImage.DigestStr()) Convey("Add newer image without tag", func() { err := UploadImage(imageWithoutTag, baseURL, repoLatest, imageWithoutTag.DigestStr()) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, afterImage.DigestStr()) }) Convey("Delete afterImage", func() { status, err := DeleteImage(repoLatest, tagAfter, baseURL) So(status, ShouldEqual, http.StatusAccepted) So(err, ShouldBeNil) results := GlobalSearchGQL("", baseURL) So(len(results.Repos), ShouldEqual, 1) So(results.Repos[0].NewestImage.Digest, ShouldResemble, middleImage.DigestStr()) }) }) }) deleteUsedImages(usedImages, baseURL) }) } type repoRef struct { Repo string Tag string } func deleteUsedImages(repoTags []repoRef, baseURL string) { for _, image := range repoTags { status, err := DeleteImage(image.Repo, image.Tag, baseURL) So(status, ShouldBeIn, []int{http.StatusAccepted, http.StatusNotFound, http.StatusBadRequest}) So(err, ShouldBeNil) } } func GlobalSearchGQL(query, baseURL string) *zcommon.GlobalSearchResultResp { queryStr := ` { GlobalSearch(query:"` + query + `"){ Images { RepoName Tag Digest MediaType Size DownloadCount LastUpdated IsSigned Description Licenses Labels Title Source Documentation Authors Vendor Manifests { Digest ConfigDigest LastUpdated Size IsSigned DownloadCount SignatureInfo {Tool IsTrusted Author} Platform {Os Arch} Layers {Size Digest} History { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } Vulnerabilities {Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity} Referrers {MediaType ArtifactType Size Digest Annotations {Key Value}} } Referrers {MediaType ArtifactType Size Digest Annotations {Key Value}} Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } SignatureInfo {Tool IsTrusted Author} } Repos { Name LastUpdated Size DownloadCount StarCount IsBookmarked IsStarred Platforms { Os Arch } Vendors NewestImage { RepoName Tag Digest MediaType Size DownloadCount LastUpdated IsSigned Description Licenses Labels Title Source Documentation Authors Vendor Manifests { Digest ConfigDigest LastUpdated Size IsSigned DownloadCount SignatureInfo {Tool IsTrusted Author} Platform {Os Arch} Layers {Size Digest} History { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } Vulnerabilities {Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity} Referrers {MediaType ArtifactType Size Digest Annotations {Key Value}} } Referrers {MediaType ArtifactType Size Digest Annotations {Key Value}} Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } SignatureInfo {Tool IsTrusted Author} } } } }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(queryStr)) So(resp, ShouldNotBeNil) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) responseStruct := &zcommon.GlobalSearchResultResp{} err = json.Unmarshal(resp.Body(), responseStruct) So(err, ShouldBeNil) return responseStruct }