0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-01-20 22:52:51 -05:00
zot/pkg/extensions/search/search_test.go
Andrei Aaron ba6f347d8d
refactor(pkg/test): split logic in pkg/test/common.go into multiple packages (#1861)
Which could be imported independently. See more details:
1. "zotregistry.io/zot/pkg/test/common" - currently used as
   tcommon "zotregistry.io/zot/pkg/test/common" - inside pkg/test
   test "zotregistry.io/zot/pkg/test/common" - in tests
   . "zotregistry.io/zot/pkg/test/common" - in tests
Decouple zb from code in test/pkg in order to keep the size small.

2. "zotregistry.io/zot/pkg/test/image-utils" - curently used as
   . "zotregistry.io/zot/pkg/test/image-utils"

3. "zotregistry.io/zot/pkg/test/deprecated" -  curently used as
   "zotregistry.io/zot/pkg/test/deprecated"
This one will bre replaced gradually by image-utils in the future.

4. "zotregistry.io/zot/pkg/test/signature" - (cosign + notation) use as
   "zotregistry.io/zot/pkg/test/signature"

5. "zotregistry.io/zot/pkg/test/auth" - (bearer + oidc)  curently used as
   authutils "zotregistry.io/zot/pkg/test/auth"

 6. "zotregistry.io/zot/pkg/test/oci-utils" -  curently used as
   ociutils "zotregistry.io/zot/pkg/test/oci-utils"

Some unused functions were removed, some were replaced, and in
a few cases specific funtions were moved to the files they were used in.

Added an interface for the StoreController, this reduces the number of imports
of the entire image store, decreasing binary size for tests.
If the zb code was still coupled with pkg/test, this would have reflected in zb size.

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-09-27 11:34:48 -07:00

6479 lines
178 KiB
Go

//go:build search
// +build search
package search_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"regexp"
"strconv"
"strings"
"testing"
"time"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
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.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/local"
storageTypes "zotregistry.io/zot/pkg/storage/types"
. "zotregistry.io/zot/pkg/test/common"
"zotregistry.io/zot/pkg/test/deprecated"
. "zotregistry.io/zot/pkg/test/image-utils"
"zotregistry.io/zot/pkg/test/mocks"
ociutils "zotregistry.io/zot/pkg/test/oci-utils"
"zotregistry.io/zot/pkg/test/signature"
)
const (
graphqlQueryPrefix = constants.FullSearchPrefix
DBFileName = "repo.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") {
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") {
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(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(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.GetManifestData(manifestDigest)
if err != nil {
return false, err
}
var manifestContent ispec.Manifest
err = json.Unmarshal(manifestData.ManifestBlob, &manifestContent)
if err != nil {
return false, zerr.ErrScanNotSupported
}
for _, imageLayer := range manifestContent.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()
config, layers, _, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
uploadedImage := CreateImageWith().LayerBlobs(layers).ImageConfig(config).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
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.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)
ctx := context.Background()
if err := ctlr.Init(ctx); err != nil {
panic(err)
}
ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB)
go func() {
if err := ctlr.Run(ctx); !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 the CVE database", 2*time.Minute)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = readFileAndSearchString(logPath, "DB update completed, next update scheduled", 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)
config, layers, manifest, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
err = UploadImage(Image{Manifest: manifest, Config: config, Layers: layers}, baseURL, "zot-cve-test", "0.0.1")
So(err, ShouldBeNil)
err = UploadImage(Image{Manifest: manifest, Config: config, Layers: layers}, baseURL, "a/zot-cve-test", "0.0.1")
So(err, ShouldBeNil)
err = UploadImage(Image{Manifest: manifest, Config: config, Layers: layers}, baseURL, "zot-test", "0.0.1")
So(err, ShouldBeNil)
err = UploadImage(Image{Manifest: manifest, Config: config, Layers: layers}, baseURL, "a/zot-test", "0.0.1")
So(err, ShouldBeNil)
query := `{
RepoListWithNewestImage{
Results{
Name
NewestImage{
Tag
Digest
Vulnerabilities{
MaxSeverity
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, ShouldBeGreaterThan, 1)
So(
dbTypes.CompareSeverityString(dbTypes.SeverityUnknown.String(), vulnerabilities.MaxSeverity),
ShouldBeGreaterThan,
0,
)
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()
// =======================
config, layers, manifest, err := deprecated.GetImageComponents(1000) //nolint:staticcheck
So(err, ShouldBeNil)
repo := "artifact-ref"
err = UploadImage(
Image{
Manifest: manifest,
Config: config,
Layers: layers,
}, baseURL, repo, "1.0")
So(err, ShouldBeNil)
manifestBlob, err := json.Marshal(manifest)
So(err, ShouldBeNil)
manifestDigest := godigest.FromBytes(manifestBlob)
manifestSize := int64(len(manifestBlob))
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, err := deprecated.GetRandomMultiarchImage("multiarch") //nolint:staticcheck
So(err, ShouldBeNil)
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, err := deprecated.GetRandomImage() //nolint:staticcheck
So(err, ShouldBeNil)
targetDigest := targetImg.Digest()
err = UploadImage(targetImg, baseURL, "repo", targetDigest.String())
So(err, ShouldBeNil)
indexReferrer, err := deprecated.GetRandomMultiarchImage("ref") //nolint:staticcheck
So(err, ShouldBeNil)
artifactType := "com.artifact.art/type"
indexReferrer.Index.ArtifactType = artifactType
indexReferrer.Index.Subject = &ispec.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: targetDigest,
}
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++ {
config, layers, manifest, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
err = WriteImageToFileSystem(
Image{
Manifest: manifest,
Config: config,
Layers: layers,
}, 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()
config, layers, _, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
annotations := make(map[string]string)
annotations["org.opencontainers.image.vendor"] = "zot"
uploadedImage := CreateImageWith().LayerBlobs(layers).ImageConfig(config).
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)
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)
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, err := deprecated.GetRandomImage() //nolint:staticcheck
So(err, ShouldBeNil)
manifestDigest := image.Digest()
err = UploadImage(image, baseURL, "repo", testTag)
So(err, ShouldBeNil)
referrer, err := deprecated.GetImageWithSubject(manifestDigest, //nolint:staticcheck
ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
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, err := deprecated.GetImageWithConfig(ispec.Image{ //nolint:staticcheck
Platform: ispec.Platform{
OS: "os11",
Architecture: "arch11",
},
})
So(err, ShouldBeNil)
indexSubImage12, err := deprecated.GetImageWithConfig(ispec.Image{ //nolint:staticcheck
Platform: ispec.Platform{
OS: "os12",
Architecture: "arch12",
},
})
So(err, ShouldBeNil)
multiImage1 := deprecated.GetMultiarchImageForImages([]Image{indexSubImage11, //nolint:staticcheck
indexSubImage12})
indexSubImage21, err := deprecated.GetImageWithConfig(ispec.Image{ //nolint:staticcheck
Platform: ispec.Platform{
OS: "os21",
Architecture: "arch21",
},
})
So(err, ShouldBeNil)
indexSubImage22, err := deprecated.GetImageWithConfig(ispec.Image{ //nolint:staticcheck
Platform: ispec.Platform{
OS: "os22",
Architecture: "arch22",
},
})
So(err, ShouldBeNil)
indexSubImage23, err := deprecated.GetImageWithConfig(ispec.Image{ //nolint:staticcheck
Platform: ispec.Platform{
OS: "os23",
Architecture: "arch23",
},
})
So(err, ShouldBeNil)
multiImage2 := deprecated.GetMultiarchImageForImages([]Image{indexSubImage21, //nolint:staticcheck
indexSubImage22, indexSubImage23})
// ------- 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(strings.Contains(string(resp.Body()), "repository: not found"), ShouldBeTrue)
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(strings.Contains(string(resp.Body()), "repository: not found"), ShouldBeTrue)
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(strings.Contains(string(resp.Body()), "repository: not found"), ShouldBeTrue)
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() {
cfg, layers, manifest, err := deprecated.GetImageComponents(10000) //nolint:staticcheck
So(err, ShouldBeNil)
manifest.Annotations = make(map[string]string)
manifest.Annotations["org.opencontainers.image.authors"] = "author name"
err = UploadImage(
Image{
Config: cfg,
Layers: layers,
Manifest: manifest,
}, 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() {
cfg, layers, manifest, err := deprecated.GetImageComponents(10000) //nolint:staticcheck
So(err, ShouldBeNil)
err = UploadImage(
Image{
Config: cfg,
Layers: layers,
Manifest: manifest,
}, 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
_, layers1, manifest1, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
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",
}
config1.History = append(
config1.History,
ispec.History{
Created: &createdTime,
CreatedBy: "go test data",
Author: "ZotUser",
Comment: "Test history comment",
EmptyLayer: true,
},
ispec.History{
Created: &createdTimeL2,
CreatedBy: "go test data 2",
Author: "ZotUser",
Comment: "Test history comment2",
EmptyLayer: false,
},
)
manifest1, err = updateManifestConfig(manifest1, config1)
So(err, ShouldBeNil)
layersSize1 := 0
for _, l := range layers1 {
layersSize1 += len(l)
}
err = UploadImage(
Image{
Manifest: manifest1,
Config: config1,
Layers: layers1,
}, baseURL, "repo1", "1.0.1",
)
So(err, ShouldBeNil)
// push test images to repo 1 image 2
config2, layers2, manifest2, err := deprecated.GetImageComponents(200) //nolint:staticcheck
So(err, ShouldBeNil)
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.History = append(
config2.History,
ispec.History{
Created: &createdTime2,
CreatedBy: "go test data",
Author: "ZotUser",
Comment: "Test history comment",
EmptyLayer: true,
},
ispec.History{
Created: &createdTimeL2,
CreatedBy: "go test data 2",
Author: "ZotUser",
Comment: "Test history comment2",
EmptyLayer: false,
},
)
manifest2, err = updateManifestConfig(manifest2, config2)
So(err, ShouldBeNil)
layersSize2 := 0
for _, l := range layers2 {
layersSize2 += len(l)
}
err = UploadImage(
Image{
Manifest: manifest2,
Config: config2,
Layers: layers2,
}, baseURL, "repo1", "1.0.2",
)
So(err, ShouldBeNil)
// push test images to repo 2 image 1
config3, layers3, manifest3, err := deprecated.GetImageComponents(300) //nolint:staticcheck
So(err, ShouldBeNil)
createdTime3 := time.Date(2009, 2, 1, 12, 0, 0, 0, time.UTC)
config3.History = append(config3.History, ispec.History{Created: &createdTime3})
manifest3, err = updateManifestConfig(manifest3, config3)
So(err, ShouldBeNil)
layersSize3 := 0
for _, l := range layers3 {
layersSize3 += len(l)
}
err = UploadImage(
Image{
Manifest: manifest3,
Config: config3,
Layers: layers3,
}, 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 MaxSeverity }
}
Vendor
Vulnerabilities { Count 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 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.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 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 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.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)
ctx := context.Background()
if err := ctlr.Init(ctx); err != nil {
panic(err)
}
ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB)
go func() {
if err := ctlr.Run(ctx); !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 the CVE database", 2*time.Minute)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = readFileAndSearchString(logPath, "DB update completed, next update scheduled", 4*time.Minute)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
WaitTillServerReady(baseURL)
// push test images to repo 1 image 1
config1, layers1, manifest1, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
createdTime := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC)
config1.History = append(config1.History, ispec.History{Created: &createdTime})
manifest1, err = updateManifestConfig(manifest1, config1)
So(err, ShouldBeNil)
layersSize1 := 0
for _, l := range layers1 {
layersSize1 += len(l)
}
err = UploadImage(
Image{
Manifest: manifest1,
Config: config1,
Layers: layers1,
}, baseURL, "repo1", "1.0.1",
)
So(err, ShouldBeNil)
// push test images to repo 1 image 2
config2, layers2, manifest2, err := deprecated.GetImageComponents(200) //nolint:staticcheck
So(err, ShouldBeNil)
createdTime2 := time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC)
config2.History = append(config2.History, ispec.History{Created: &createdTime2})
manifest2, err = updateManifestConfig(manifest2, config2)
So(err, ShouldBeNil)
layersSize2 := 0
for _, l := range layers2 {
layersSize2 += len(l)
}
err = UploadImage(
Image{
Manifest: manifest2,
Config: config2,
Layers: layers2,
}, baseURL, "repo1", "1.0.2",
)
So(err, ShouldBeNil)
// push test images to repo 2 image 1
config3, layers3, manifest3, err := deprecated.GetImageComponents(300) //nolint:staticcheck
So(err, ShouldBeNil)
createdTime3 := time.Date(2009, 2, 1, 12, 0, 0, 0, time.UTC)
config3.History = append(config3.History, ispec.History{Created: &createdTime3})
manifest3, err = updateManifestConfig(manifest3, config3)
So(err, ShouldBeNil)
layersSize3 := 0
for _, l := range layers3 {
layersSize3 += len(l)
}
err = UploadImage(
Image{
Manifest: manifest3,
Config: config3,
Layers: layers3,
}, 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 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 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.MaxSeverity, ShouldEqual, "CRITICAL")
} else {
So(repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 0)
// There are 0 vulnerabilities this data used in tests
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 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 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.MaxSeverity, ShouldEqual, "CRITICAL")
})
}
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, err := deprecated.GetImageWithConfig(ispec.Image{ //nolint:staticcheck
Platform: ispec.Platform{
OS: "windows",
Architecture: "amd64",
},
})
So(err, ShouldBeNil)
err = UploadImage(image, baseURL, "repo1", image.DigestStr())
So(err, ShouldBeNil)
image, err = deprecated.GetImageWithConfig(ispec.Image{ //nolint:staticcheck
Platform: ispec.Platform{
OS: "linux",
Architecture: "amd64",
},
})
So(err, ShouldBeNil)
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()
config, layers, manifest, err := deprecated.GetRandomImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
err = UploadImage(
Image{
Config: config,
Layers: layers,
Manifest: manifest,
}, baseURL, "unsigned-repo", "test",
)
So(err, ShouldBeNil)
config, layers, manifest, err = deprecated.GetRandomImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
err = UploadImage(
Image{
Config: config,
Layers: layers,
Manifest: manifest,
}, baseURL, "signed-repo", "test",
)
So(err, ShouldBeNil)
err = signature.SignImageUsingCosign("signed-repo:test", port)
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()
config, layers, manifest, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
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.History = append(
config.History,
ispec.History{
Created: &createdTime,
CreatedBy: "go test data",
Author: "ZotUser",
Comment: "Test history comment",
EmptyLayer: true,
},
ispec.History{
Created: &createdTimeL2,
CreatedBy: "go test data 2",
Author: "ZotUser",
Comment: "Test history comment2",
EmptyLayer: false,
},
)
manifest, err = updateManifestConfig(manifest, config)
So(err, ShouldBeNil)
err = UploadImage(Image{Manifest: manifest, Config: config, Layers: layers}, baseURL, "zot-cve-test", "0.0.1")
So(err, ShouldBeNil)
err = UploadImage(Image{Manifest: manifest, Config: config, Layers: layers}, baseURL, "a/zot-cve-test", "0.0.1")
So(err, ShouldBeNil)
err = UploadImage(Image{Manifest: manifest, Config: config, Layers: layers}, baseURL, "zot-test", "0.0.1")
So(err, ShouldBeNil)
err = UploadImage(Image{Manifest: manifest, Config: config, Layers: layers}, 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++ {
config, layers, manifest, err := deprecated.GetImageComponents(10) //nolint:staticcheck
So(err, ShouldBeNil)
err = UploadImage(
Image{
Manifest: manifest,
Config: config,
Layers: layers,
}, 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
createdTime := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC)
image1, err := deprecated.GetImageWithConfig(ispec.Image{ //nolint:staticcheck
History: []ispec.History{
{
Created: &createdTime,
},
},
})
So(err, ShouldBeNil)
err = UploadImage(
Image{
Manifest: image1.Manifest,
Config: image1.Config,
Layers: image1.Layers,
}, baseURL, "repo1", "1.0.1",
)
So(err, ShouldBeNil)
err = UploadImage(
Image{
Manifest: image1.Manifest,
Config: image1.Config,
Layers: image1.Layers,
}, baseURL, "repo1", "2.0.2",
)
So(err, ShouldBeNil)
manifestBlob, err := json.Marshal(image1.Manifest)
So(err, ShouldBeNil)
manifestDigest := godigest.FromBytes(manifestBlob)
multiArch, err := deprecated.GetRandomMultiarchImage("index") //nolint:staticcheck
So(err, ShouldBeNil)
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)
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)
So(err, ShouldNotBeNil)
})
})
Convey("Sign with notation", func() {
err = signature.SignImageUsingNotary("repo1:1.0.1", port)
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)
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)
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 fails", func() {
ctlr.MetaDB = mocks.MetaDBMock{
SetManifestDataFn: func(manifestDigest godigest.Digest, mm mTypes.ManifestData) error {
return ErrTestError
},
}
config1, layers1, manifest1, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
configBlob, err := json.Marshal(config1)
So(err, ShouldBeNil)
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 configBlob, nil
},
DeleteImageManifestFn: func(repo, reference string, dc bool) error {
return ErrTestError
},
}
err = UploadImage(
Image{
Manifest: manifest1,
Config: config1,
Layers: layers1,
}, baseURL, "repo1", "1.0.1",
)
So(err, ShouldNotBeNil)
})
Convey("SetManifestMeta succeeds but SetRepoReference fails", func() {
ctlr.MetaDB = mocks.MetaDBMock{
SetRepoReferenceFn: func(repo, reference string, manifestDigest godigest.Digest, mediaType string) error {
return ErrTestError
},
}
config1, layers1, manifest1, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
configBlob, err := json.Marshal(config1)
So(err, ShouldBeNil)
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 configBlob, nil
},
}
err = UploadImage(
Image{
Manifest: manifest1,
Config: config1,
Layers: layers1,
}, 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, err := deprecated.GetRandomMultiarchImage("tag1") //nolint:staticcheck
So(err, ShouldBeNil)
indexBlob, err := json.Marshal(multiarchImage.Index)
So(err, ShouldBeNil)
indexDigest := godigest.FromBytes(indexBlob)
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", indexDigest), port)
So(err, ShouldBeNil)
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
responseStruct = &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-" + indexDigest.Encoded() + ".sig"
_, err = resty.R().Delete(baseURL + "/v2/" + "repo" + "/manifests/" + cosignTag)
So(err, ShouldBeNil)
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
responseStruct = &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, err := deprecated.GetImageWithComponents( //nolint:staticcheck
ispec.Image{
Platform: ispec.Platform{
OS: "linux",
Architecture: "amd64",
},
},
[][]byte{
{10, 20, 30},
{11, 21, 31},
})
So(err, ShouldBeNil)
imageSomeArch, err := deprecated.GetImageWithComponents( //nolint:staticcheck
ispec.Image{
Platform: ispec.Platform{
OS: "linux",
Architecture: "someArch",
},
}, [][]byte{
{18, 28, 38},
{12, 22, 32},
})
So(err, ShouldBeNil)
multiImage := deprecated.GetMultiarchImageForImages([]Image{ //nolint:staticcheck
imageAMD64,
imageSomeArch,
})
err = UploadMultiarchImage(multiImage, baseURL, "test-repo", "latest")
So(err, ShouldBeNil)
// ---------------- BASE IMAGE -------------------
// ---------------- SAME LAYERS -------------------
image1, err := deprecated.GetImageWithComponents( //nolint:staticcheck
imageSomeArch.Config,
[][]byte{
{0, 0, 2},
},
)
So(err, ShouldBeNil)
image2, err := deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
imageAMD64.Layers,
)
So(err, ShouldBeNil)
multiImage = deprecated.GetMultiarchImageForImages([]Image{image1, image2}) //nolint:staticcheck
err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-same-layers", "index-one-arch-same-layers")
So(err, ShouldBeNil)
// ---------------- SAME LAYERS -------------------
// ---------------- LESS LAYERS -------------------
image1, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageSomeArch.Config,
[][]byte{
{3, 2, 2},
{5, 2, 5},
},
)
So(err, ShouldBeNil)
image2, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
[][]byte{imageAMD64.Layers[0]},
)
So(err, ShouldBeNil)
multiImage = deprecated.GetMultiarchImageForImages([]Image{image1, image2}) //nolint:staticcheck
err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-less-layers", "index-one-arch-less-layers")
So(err, ShouldBeNil)
// ---------------- LESS LAYERS -------------------
// ---------------- LESS LAYERS FALSE -------------------
image1, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageSomeArch.Config,
[][]byte{
{3, 2, 2},
{5, 2, 5},
},
)
So(err, ShouldBeNil)
auxLayer := imageAMD64.Layers[0]
auxLayer[0] = 20
image2, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
[][]byte{auxLayer},
)
So(err, ShouldBeNil)
multiImage = deprecated.GetMultiarchImageForImages([]Image{image1, image2}) //nolint:staticcheck
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, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageSomeArch.Config,
[][]byte{
{0, 0, 2},
{3, 0, 2},
},
)
So(err, ShouldBeNil)
image2, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
append(imageAMD64.Layers, []byte{1, 3, 55}),
)
So(err, ShouldBeNil)
multiImage = deprecated.GetMultiarchImageForImages([]Image{image1, image2}) //nolint:staticcheck
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, err := deprecated.GetImageWithComponents( //nolint:staticcheck
ispec.Image{
Platform: ispec.Platform{
OS: "linux",
Architecture: "amd64",
},
},
[][]byte{
{10, 20, 30},
{11, 21, 31},
})
So(err, ShouldBeNil)
baseLinuxAMD64Digest := imageAMD64.Digest()
imageSomeArch, err := deprecated.GetImageWithComponents( //nolint:staticcheck
ispec.Image{
Platform: ispec.Platform{
OS: "linux",
Architecture: "someArch",
},
}, [][]byte{
{18, 28, 38},
{12, 22, 32},
})
So(err, ShouldBeNil)
baseLinuxSomeArchDigest := imageSomeArch.Digest()
multiImage := deprecated.GetMultiarchImageForImages([]Image{imageAMD64, //nolint:staticcheck
imageSomeArch})
err = UploadMultiarchImage(multiImage, baseURL, "test-repo", "index")
So(err, ShouldBeNil)
// ---------------- BASE IMAGE FOR LINUX AMD64 -------------------
image, err := deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
[][]byte{imageAMD64.Layers[0]},
)
So(err, ShouldBeNil)
err = UploadImage(image, baseURL, "test-repo", "less-layers-linux-amd64")
So(err, ShouldBeNil)
// ---------------- BASE IMAGE FOR LINUX SOMEARCH -------------------
image, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
[][]byte{imageSomeArch.Layers[0]},
)
So(err, ShouldBeNil)
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, err := deprecated.GetImageWithComponents( //nolint:staticcheck
ispec.Image{
Platform: ispec.Platform{
OS: "linux",
Architecture: "amd64",
},
},
[][]byte{
{10, 20, 30},
{11, 21, 31},
})
So(err, ShouldBeNil)
imageSomeArch, err := deprecated.GetImageWithComponents( //nolint:staticcheck
ispec.Image{
Platform: ispec.Platform{
OS: "linux",
Architecture: "someArch",
},
}, [][]byte{
{18, 28, 38},
{12, 22, 32},
})
So(err, ShouldBeNil)
multiImage := deprecated.GetMultiarchImageForImages([]Image{ //nolint:staticcheck
imageAMD64, imageSomeArch,
})
err = UploadMultiarchImage(multiImage, baseURL, "test-repo", "latest")
So(err, ShouldBeNil)
// ---------------- BASE IMAGE -------------------
// ---------------- SAME LAYERS -------------------
image1, err := deprecated.GetImageWithComponents( //nolint:staticcheck
imageSomeArch.Config,
[][]byte{
{0, 0, 2},
},
)
So(err, ShouldBeNil)
image2, err := deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
imageAMD64.Layers,
)
So(err, ShouldBeNil)
multiImage = deprecated.GetMultiarchImageForImages([]Image{ //nolint:staticcheck
image1, image2,
})
err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-same-layers", "index-one-arch-same-layers")
So(err, ShouldBeNil)
// ---------------- SAME LAYERS -------------------
// ---------------- LESS LAYERS -------------------
image1, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageSomeArch.Config,
[][]byte{
{3, 2, 2},
{5, 2, 5},
},
)
So(err, ShouldBeNil)
image2, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
[][]byte{imageAMD64.Layers[0]},
)
So(err, ShouldBeNil)
multiImage = deprecated.GetMultiarchImageForImages([]Image{ //nolint:staticcheck
image1, image2,
})
err = UploadMultiarchImage(multiImage, baseURL, "index-one-arch-less-layers", "index-one-arch-less-layers")
So(err, ShouldBeNil)
// ---------------- LESS LAYERS -------------------
// ---------------- LESS LAYERS FALSE -------------------
image1, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageSomeArch.Config,
[][]byte{
{3, 2, 2},
{5, 2, 5},
},
)
So(err, ShouldBeNil)
image2, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
[][]byte{{99, 100, 102}},
)
So(err, ShouldBeNil)
multiImage = deprecated.GetMultiarchImageForImages([]Image{ //nolint:staticcheck
image1, image2,
})
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, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageSomeArch.Config,
[][]byte{
{0, 0, 2},
{3, 0, 2},
},
)
So(err, ShouldBeNil)
image2, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
[][]byte{
imageAMD64.Layers[0],
imageAMD64.Layers[1],
{1, 3, 55},
},
)
So(err, ShouldBeNil)
multiImage = deprecated.GetMultiarchImageForImages([]Image{ //nolint:staticcheck
image1, image2,
})
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, err := deprecated.GetImageWithComponents( //nolint:staticcheck
ispec.Image{
Platform: ispec.Platform{
OS: "linux",
Architecture: "amd64",
},
},
[][]byte{
{10, 20, 30},
{11, 21, 31},
})
So(err, ShouldBeNil)
baseLinuxAMD64Digest := imageAMD64.Digest()
imageSomeArch, err := deprecated.GetImageWithComponents( //nolint:staticcheck
ispec.Image{
Platform: ispec.Platform{
OS: "linux",
Architecture: "someArch",
},
}, [][]byte{
{18, 28, 38},
{12, 22, 32},
})
So(err, ShouldBeNil)
baseLinuxSomeArchDigest := imageSomeArch.Digest()
multiImage := deprecated.GetMultiarchImageForImages([]Image{ //nolint:staticcheck
imageAMD64, imageSomeArch,
})
err = UploadMultiarchImage(multiImage, baseURL, "test-repo", "index")
So(err, ShouldBeNil)
// ---------------- BASE IMAGE FOR LINUX AMD64 -------------------
image, err := deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
[][]byte{
imageAMD64.Layers[0],
imageAMD64.Layers[1],
{0, 0, 0, 0},
{1, 1, 1, 1},
},
)
So(err, ShouldBeNil)
err = UploadImage(image, baseURL, "test-repo", "more-layers-linux-amd64")
So(err, ShouldBeNil)
// ---------------- BASE IMAGE FOR LINUX SOMEARCH -------------------
image, err = deprecated.GetImageWithComponents( //nolint:staticcheck
imageAMD64.Config,
[][]byte{
imageSomeArch.Layers[0],
imageSomeArch.Layers[1],
{3, 3, 3, 3},
{2, 2, 2, 2},
},
)
So(err, ShouldBeNil)
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()
config1, layers1, manifest1, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
err = UploadImage(
Image{
Manifest: manifest1,
Config: config1,
Layers: layers1,
}, 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{
IncrementImageDownloadsFn: 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, err := deprecated.GetRandomImage() //nolint:staticcheck
So(err, ShouldBeNil)
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, err := deprecated.GetImageWithConfig(ispec.Image{ //nolint:staticcheck
Created: &createdTime2,
History: []ispec.History{
{
Created: &createdTime2,
},
},
})
So(err, ShouldBeNil)
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)
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]
cosignTagRule := regexp.MustCompile(`sha256\-.+\.sig`)
if cosignTagRule.MatchString(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)
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() {
referredImageDigest := image1.Digest()
referrerImage, err := deprecated.GetImageWithSubject(referredImageDigest, //nolint:staticcheck
ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
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
}
}`, referredImageDigest.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 updateManifestConfig(manifest ispec.Manifest, config ispec.Image) (ispec.Manifest, error) {
configBlob, err := json.Marshal(config)
configDigest := godigest.FromBytes(configBlob)
configSize := len(configBlob)
manifest.Config.Digest = configDigest
manifest.Config.Size = int64(configSize)
return manifest, err
}
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"
config, layers, manifest, err := deprecated.GetImageComponents(10000) //nolint:staticcheck
So(err, ShouldBeNil)
configBlob, err := json.Marshal(config)
So(err, ShouldBeNil)
configSize := len(configBlob)
layersSize := 0
for _, l := range layers {
layersSize += len(l)
}
manifestBlob, err := json.Marshal(manifest)
So(err, ShouldBeNil)
manifestSize := len(manifestBlob)
err = UploadImage(
Image{
Manifest: manifest,
Config: config,
Layers: layers,
}, 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(
Image{
Manifest: manifest,
Config: config,
Layers: layers,
}, 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 MaxSeverity }
History {
HistoryDescription { Created }
Layer { Digest Size }
}
}
LastUpdated
Size
Vulnerabilities { Count 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 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, err := deprecated.GetImageWithConfig( //nolint:staticcheck
ispec.Image{
History: []ispec.History{{Created: &createdTime}},
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
},
)
So(err, ShouldBeNil)
manifestDigest := image.Digest()
err = UploadImage(image, baseURL, repoName, tagTarget)
So(err, ShouldBeNil)
// ------ Add a referrer
referrerImage, err := deprecated.GetImageWithConfig(ispec.Image{}) //nolint:staticcheck
So(err, ShouldBeNil)
referrerImage.Manifest.Subject = &ispec.Descriptor{
Digest: manifestDigest,
MediaType: ispec.MediaTypeImageManifest,
}
referrerImage.Manifest.Config.MediaType = "application/test.artifact.type"
referrerImage.Manifest.Annotations = map[string]string{"testAnnotationKey": "testAnnotationValue"}
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.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, "metadb: 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 MaxSeverity }
History {
HistoryDescription { Created }
Layer { Digest Size }
}
}
LastUpdated
Size
Vulnerabilities { Count MaxSeverity }
}
}`
gqlEndpoint := fmt.Sprintf("%s%s?query=", baseURL, graphqlQueryPrefix)
config, layers, manifest, err := deprecated.GetImageComponents(100) //nolint:staticcheck
So(err, ShouldBeNil)
createdTime := time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC)
config.History = append(config.History, ispec.History{Created: &createdTime})
manifest, err = updateManifestConfig(manifest, config)
So(err, ShouldBeNil)
configBlob, errConfig := json.Marshal(config)
configDigest := godigest.FromBytes(configBlob)
So(errConfig, ShouldBeNil) // marshall success, config is valid JSON
ctx := context.Background()
if err := ctlr.Init(ctx); err != nil {
panic(err)
}
ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB)
go func() {
if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}()
defer ctlr.Shutdown()
WaitTillServerReady(baseURL)
manifestBlob, errMarshal := json.Marshal(manifest)
So(errMarshal, ShouldBeNil)
So(manifestBlob, ShouldNotBeNil)
manifestDigest := godigest.FromBytes(manifestBlob)
repoName := "test-repo" //nolint:goconst
tagTarget := "latest"
err = UploadImage(
Image{
Manifest: manifest,
Config: config,
Layers: layers,
}, 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, 1)
So(imgSummary.Manifests[0].Layers[0].Digest, ShouldContainSubstring,
godigest.FromBytes(layers[0]).Encoded())
So(imgSummary.LastUpdated, ShouldEqual, createdTime)
So(imgSummary.IsSigned, ShouldEqual, false)
So(imgSummary.Manifests[0].Platform.Os, ShouldEqual, "linux")
So(imgSummary.Manifests[0].Platform.Arch, ShouldEqual, "amd64")
So(len(imgSummary.Manifests[0].History), ShouldEqual, 1)
So(imgSummary.Manifests[0].History[0].HistoryDescription.Created, ShouldEqual, createdTime)
So(imgSummary.Vulnerabilities.Count, ShouldEqual, 4)
// There are 0 vulnerabilities this data used in tests
So(imgSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL")
})
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, err := deprecated.GetRandomImage() //nolint:staticcheck
So(err, ShouldBeNil)
img1.Manifest.Config = ispec.DescriptorEmptyJSON
img1.Manifest.ArtifactType = artType1
digest1 := img1.Digest()
err = UploadImage(img1, baseURL, "repo", "art1")
So(err, ShouldBeNil)
img2, err := deprecated.GetRandomImage() //nolint:staticcheck
So(err, ShouldBeNil)
img2.Manifest.Config.MediaType = artType2
digest2 := img2.Digest()
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 digest1.String():
So(imgSum.Manifests[0].ArtifactType, ShouldResemble, artType1)
case digest2.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)
manifestData, err := ctlr.MetaDB.GetManifestData(imageWithIncompatibleConfig.ManifestDescriptor.Digest)
So(err, ShouldBeNil)
So(manifestData.ConfigBlob, ShouldEqual, imageWithIncompatibleConfig.ConfigDescriptor.Data)
So(manifestData.ManifestBlob, ShouldEqual, imageWithIncompatibleConfig.ManifestDescriptor.Data)
manifestData, err = ctlr.MetaDB.GetManifestData(defaultImage.ManifestDescriptor.Digest)
So(err, ShouldBeNil)
So(manifestData.ConfigBlob, ShouldEqual, defaultImage.ConfigDescriptor.Data)
So(manifestData.ManifestBlob, ShouldEqual, defaultImage.ManifestDescriptor.Data)
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())
})
}