freeform querry api

Signed-off-by: Laurentiu Niculae <themelopeus@gmail.com>
This commit is contained in:
Laurentiu Niculae
2022-07-12 15:58:04 +03:00
committed by Ramkumar Chinchani
parent a31869f270
commit 7e3d063319
20 changed files with 3597 additions and 562 deletions
+451 -1
View File
@@ -6,8 +6,10 @@ package common_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"path"
@@ -30,12 +32,15 @@ import (
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
. "zotregistry.io/zot/pkg/test"
"zotregistry.io/zot/pkg/test/mocks"
)
const (
graphqlQueryPrefix = constants.ExtSearchPrefix
)
var ErrTestError = errors.New("test error")
// nolint:gochecknoglobals
var (
rootDir string
@@ -52,6 +57,50 @@ type ExpandedRepoInfoResp struct {
Errors []ErrorGQL `json:"errors"`
}
type GlobalSearchResultResp struct {
GlobalSearchResult GlobalSearchResult `json:"data"`
Errors []ErrorGQL `json:"errors"`
}
type GlobalSearchResult struct {
GlobalSearch GlobalSearch `json:"globalSearch"`
}
type GlobalSearch struct {
Images []ImageSummary `json:"images"`
Repos []RepoSummary `json:"repos"`
Layers []LayerSummary `json:"layers"`
}
type ImageSummary struct {
RepoName string `json:"repoName"`
Tag string `json:"tag"`
LastUpdated time.Time `json:"lastUpdated"`
Size string `json:"size"`
Platform OsArch `json:"platform"`
Vendor string `json:"vendor"`
Score int `json:"score"`
}
type RepoSummary struct {
Name string `json:"name"`
LastUpdated time.Time `json:"lastUpdated"`
Size string `json:"size"`
Platforms []OsArch `json:"platforms"`
Vendors []string `json:"vendors"`
Score int `json:"score"`
}
type LayerSummary struct {
Size string `json:"size"`
Digest string `json:"digest"`
Score int `json:"score"`
}
type OsArch struct {
Os string `json:"os"`
Arch string `json:"arch"`
}
type ExpandedRepoInfo struct {
RepoInfo common.RepoInfo `json:"expandedRepoInfo"`
}
@@ -210,7 +259,7 @@ func TestImageFormat(t *testing.T) {
metrics := monitoring.NewMetricsServer(false, log)
defaultStore := storage.NewImageStore(dbDir, false, storage.DefaultGCDelay, false, false, log, metrics)
storeController := storage.StoreController{DefaultStore: defaultStore}
olu := common.NewOciLayoutUtils(storeController, log)
olu := common.NewBaseOciLayoutUtils(storeController, log)
isValidImage, err := olu.IsValidImageFormat("zot-test")
So(err, ShouldBeNil)
@@ -671,3 +720,404 @@ func TestUtilsMethod(t *testing.T) {
So(dir, ShouldEqual, subRootDir)
})
}
func TestGlobalSearch(t *testing.T) {
Convey("Test utils", t, func() {
subpath := "/a"
err := testSetup(t, subpath)
if err != nil {
panic(err)
}
port := GetFreePort()
baseURL := GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
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{Enable: &defaultVal},
}
conf.Extensions.Search.CVE = nil
ctlr := api.NewController(conf)
go func() {
// this blocks
if err := ctlr.Run(context.Background()); err != nil {
return
}
}()
// wait till ready
for {
_, err := resty.R().Get(baseURL)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
// shut down server
defer func() {
ctx := context.Background()
_ = ctlr.Server.Shutdown(ctx)
}()
query := `
{
GlobalSearch(query:""){
Images {
RepoName
Tag
LastUpdated
Size
Score
}
Repos {
Name
LastUpdated
Size
Platforms {
Os
Arch
}
Vendors
Score
}
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 := &GlobalSearchResultResp{}
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(responseStruct.GlobalSearchResult.GlobalSearch.Images, ShouldNotBeNil)
So(len(responseStruct.GlobalSearchResult.GlobalSearch.Images), ShouldNotBeEmpty)
So(len(responseStruct.GlobalSearchResult.GlobalSearch.Repos), ShouldNotBeEmpty)
So(len(responseStruct.GlobalSearchResult.GlobalSearch.Layers), ShouldNotBeEmpty)
// GetRepositories fail
err = os.Chmod(rootDir, 0o333)
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)
responseStruct = &GlobalSearchResultResp{}
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(responseStruct.Errors, ShouldNotBeEmpty)
err = os.Chmod(rootDir, 0o777)
So(err, ShouldBeNil)
})
}
func TestBaseOciLayoutUtils(t *testing.T) {
manifestDigest := "sha256:adf3bb6cc81f8bd6a9d5233be5f0c1a4f1e3ed1cf5bbdfad7708cc8d4099b741"
Convey("GetImageManifestSize fail", t, func() {
mockStoreController := mocks.MockedImageStore{
GetBlobContentFn: func(repo, digest string) ([]byte, error) {
return []byte{}, ErrTestError
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
size := olu.GetImageManifestSize("", "")
So(size, ShouldBeZeroValue)
})
Convey("GetImageConfigSize: fail GetImageBlobManifest", t, func() {
mockStoreController := mocks.MockedImageStore{
GetBlobContentFn: func(repo, digest string) ([]byte, error) {
return []byte{}, ErrTestError
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
size := olu.GetImageConfigSize("", "")
So(size, ShouldBeZeroValue)
})
Convey("GetImageConfigSize: config GetBlobContent fail", t, func() {
mockStoreController := mocks.MockedImageStore{
GetBlobContentFn: func(repo, digest string) ([]byte, error) {
if digest == manifestDigest {
return []byte{}, ErrTestError
}
return []byte(
`
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": manifestDigest,
"size": 1476
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:2d473b07cdd5f0912cd6f1a703352c82b512407db6b05b43f2553732b55df3bc",
"size": 76097157
}
]
}`), nil
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
size := olu.GetImageConfigSize("", "")
So(size, ShouldBeZeroValue)
})
Convey("GetRepoLastUpdated: config GetBlobContent fail", t, func() {
mockStoreController := mocks.MockedImageStore{
GetIndexContentFn: func(repo string) ([]byte, error) {
return []byte{}, ErrTestError
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
_, err := olu.GetRepoLastUpdated("")
So(err, ShouldNotBeNil)
})
Convey("GetImageLastUpdated: GetImageBlobManifest fails", t, func() {
mockStoreController := mocks.MockedImageStore{
GetBlobContentFn: func(repo, digest string) ([]byte, error) {
return []byte{}, ErrTestError
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
time := olu.GetImageLastUpdated("", "")
So(time, ShouldBeZeroValue)
})
Convey("GetImageLastUpdated: GetImageInfo fails", t, func() {
mockStoreController := mocks.MockedImageStore{
GetBlobContentFn: func(repo, digest string) ([]byte, error) {
if digest == manifestDigest {
return []byte{}, ErrTestError
}
return []byte(
`
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": manifestDigest,
"size": 1476
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:2d473b07cdd5f0912cd6f1a703352c82b512407db6b05b43f2553732b55df3bc",
"size": 76097157
}
]
}`), nil
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
time := olu.GetImageLastUpdated("", "")
So(time, ShouldBeZeroValue)
})
Convey("GetImageLastUpdated: GetImageInfo history is empty", t, func() {
mockStoreController := mocks.MockedImageStore{
GetBlobContentFn: func(repo, digest string) ([]byte, error) {
if digest == manifestDigest {
return []byte(
`
{
"created": "2020-11-14T00:20:04.644613188Z",
"architecture": "amd64",
"os": "linux",
"config": {
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/bash"
],
"Labels": {
}
},
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02"
]
},
"history": [
]
}
`), nil
}
return []byte(
`
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": manifestDigest,
"size": 1476
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:2d473b07cdd5f0912cd6f1a703352c82b512407db6b05b43f2553732b55df3bc",
"size": 76097157
}
]
}`), nil
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
time := olu.GetImageLastUpdated("", "")
So(time, ShouldBeZeroValue)
})
Convey("GetImagePlatform: GetImageBlobManifest fails", t, func() {
mockStoreController := mocks.MockedImageStore{
GetBlobContentFn: func(repo, digest string) ([]byte, error) {
return []byte{}, ErrTestError
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
os, arch := olu.GetImagePlatform("", "")
So(os, ShouldBeZeroValue)
So(arch, ShouldBeZeroValue)
})
Convey("GetImagePlatform: GetImageInfo fails", t, func() {
mockStoreController := mocks.MockedImageStore{
GetBlobContentFn: func(repo, digest string) ([]byte, error) {
if digest == manifestDigest {
return []byte{}, ErrTestError
}
return []byte(
`
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": manifestDigest,
"size": 1476
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:2d473b07cdd5f0912cd6f1a703352c82b512407db6b05b43f2553732b55df3bc",
"size": 76097157
}
]
}`), nil
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
os, arch := olu.GetImagePlatform("", "")
So(os, ShouldBeZeroValue)
So(arch, ShouldBeZeroValue)
})
Convey("GetImageVendor: GetImageBlobManifest fails", t, func() {
mockStoreController := mocks.MockedImageStore{
GetBlobContentFn: func(repo, digest string) ([]byte, error) {
return []byte{}, ErrTestError
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
vendor := olu.GetImageVendor("", "")
So(vendor, ShouldBeZeroValue)
})
Convey("GetImageVendor: GetImageInfo fails", t, func() {
mockStoreController := mocks.MockedImageStore{
GetBlobContentFn: func(repo, digest string) ([]byte, error) {
if digest == manifestDigest {
return []byte{}, ErrTestError
}
return []byte(
`
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": manifestDigest,
"size": 1476
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:2d473b07cdd5f0912cd6f1a703352c82b512407db6b05b43f2553732b55df3bc",
"size": 76097157
}
]
}`), nil
},
}
storeController := storage.StoreController{DefaultStore: mockStoreController}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
vendor := olu.GetImageVendor("", "")
So(vendor, ShouldBeZeroValue)
})
}
+133 -13
View File
@@ -20,8 +20,23 @@ import (
"zotregistry.io/zot/pkg/storage"
)
type OciLayoutUtils interface {
GetImageManifests(image string) ([]ispec.Descriptor, error)
GetImageBlobManifest(imageDir string, digest godigest.Digest) (v1.Manifest, error)
GetImageInfo(imageDir string, hash v1.Hash) (ispec.Image, error)
IsValidImageFormat(image string) (bool, error)
GetImageTagsWithTimestamp(repo string) ([]TagInfo, error)
GetImageLastUpdated(repo string, manifestDigest godigest.Digest) time.Time
GetImagePlatform(repo string, manifestDigest godigest.Digest) (string, string)
GetImageVendor(repo string, manifestDigest godigest.Digest) string
GetImageManifestSize(repo string, manifestDigest godigest.Digest) int64
GetImageConfigSize(repo string, manifestDigest godigest.Digest) int64
GetRepoLastUpdated(repo string) (time.Time, error)
GetExpandedRepoInfo(name string) (RepoInfo, error)
}
// OciLayoutInfo ...
type OciLayoutUtils struct {
type BaseOciLayoutUtils struct {
Log log.Logger
StoreController storage.StoreController
}
@@ -42,13 +57,13 @@ type Layer struct {
Digest string `json:"digest"`
}
// NewOciLayoutUtils initializes a new OciLayoutUtils object.
func NewOciLayoutUtils(storeController storage.StoreController, log log.Logger) *OciLayoutUtils {
return &OciLayoutUtils{Log: log, StoreController: storeController}
// NewBaseOciLayoutUtils initializes a new OciLayoutUtils object.
func NewBaseOciLayoutUtils(storeController storage.StoreController, log log.Logger) *BaseOciLayoutUtils {
return &BaseOciLayoutUtils{Log: log, StoreController: storeController}
}
// Below method will return image path including root dir, root dir is determined by splitting.
func (olu OciLayoutUtils) GetImageManifests(image string) ([]ispec.Descriptor, error) {
func (olu BaseOciLayoutUtils) GetImageManifests(image string) ([]ispec.Descriptor, error) {
imageStore := olu.StoreController.GetImageStore(image)
buf, err := imageStore.GetIndexContent(image)
@@ -76,7 +91,7 @@ func (olu OciLayoutUtils) GetImageManifests(image string) ([]ispec.Descriptor, e
}
//nolint: interfacer
func (olu OciLayoutUtils) GetImageBlobManifest(imageDir string, digest godigest.Digest) (v1.Manifest, error) {
func (olu BaseOciLayoutUtils) GetImageBlobManifest(imageDir string, digest godigest.Digest) (v1.Manifest, error) {
var blobIndex v1.Manifest
imageStore := olu.StoreController.GetImageStore(imageDir)
@@ -98,7 +113,7 @@ func (olu OciLayoutUtils) GetImageBlobManifest(imageDir string, digest godigest.
}
//nolint: interfacer
func (olu OciLayoutUtils) GetImageInfo(imageDir string, hash v1.Hash) (ispec.Image, error) {
func (olu BaseOciLayoutUtils) GetImageInfo(imageDir string, hash v1.Hash) (ispec.Image, error) {
var imageInfo ispec.Image
imageStore := olu.StoreController.GetImageStore(imageDir)
@@ -119,7 +134,7 @@ func (olu OciLayoutUtils) GetImageInfo(imageDir string, hash v1.Hash) (ispec.Ima
return imageInfo, err
}
func (olu OciLayoutUtils) IsValidImageFormat(image string) (bool, error) {
func (olu BaseOciLayoutUtils) IsValidImageFormat(image string) (bool, error) {
imageDir, inputTag := GetImageDirAndTag(image)
manifests, err := olu.GetImageManifests(imageDir)
@@ -158,7 +173,7 @@ func (olu OciLayoutUtils) IsValidImageFormat(image string) (bool, error) {
}
// GetImageTagsWithTimestamp returns a list of image tags with timestamp available in the specified repository.
func (olu OciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]TagInfo, error) {
func (olu BaseOciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]TagInfo, error) {
tagsInfo := make([]TagInfo, 0)
manifests, err := olu.GetImageManifests(repo)
@@ -203,7 +218,7 @@ func (olu OciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]TagInfo, err
}
// check notary signature corresponding to repo name, manifest digest and mediatype.
func (olu OciLayoutUtils) checkNotarySignature(name string, digest godigest.Digest) bool {
func (olu BaseOciLayoutUtils) checkNotarySignature(name string, digest godigest.Digest) bool {
imageStore := olu.StoreController.GetImageStore(name)
mediaType := notreg.ArtifactTypeNotation
@@ -219,7 +234,7 @@ func (olu OciLayoutUtils) checkNotarySignature(name string, digest godigest.Dige
}
// check cosign signature corresponding to manifest.
func (olu OciLayoutUtils) checkCosignSignature(name string, digest godigest.Digest) bool {
func (olu BaseOciLayoutUtils) checkCosignSignature(name string, digest godigest.Digest) bool {
imageStore := olu.StoreController.GetImageStore(name)
// if manifest is signed using cosign mechanism, cosign adds a new manifest.
@@ -240,7 +255,7 @@ func (olu OciLayoutUtils) checkCosignSignature(name string, digest godigest.Dige
// checks if manifest is signed or not
// checks for notary or cosign signature
// if cosign signature found it does not looks for notary signature.
func (olu OciLayoutUtils) checkManifestSignature(name string, digest godigest.Digest) bool {
func (olu BaseOciLayoutUtils) checkManifestSignature(name string, digest godigest.Digest) bool {
if !olu.checkCosignSignature(name, digest) {
return olu.checkNotarySignature(name, digest)
}
@@ -248,7 +263,112 @@ func (olu OciLayoutUtils) checkManifestSignature(name string, digest godigest.Di
return true
}
func (olu OciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) {
func (olu BaseOciLayoutUtils) GetImageLastUpdated(repo string, manifestDigest godigest.Digest) time.Time {
imageBlobManifest, err := olu.GetImageBlobManifest(repo, manifestDigest)
if err != nil {
olu.Log.Error().Err(err).Msg("unable to read image blob")
return time.Time{}
}
imageInfo, err := olu.GetImageInfo(repo, imageBlobManifest.Config.Digest)
if err != nil {
olu.Log.Error().Err(err).Msg("unable to read image info")
return time.Time{}
}
var timeStamp time.Time
if len(imageInfo.History) != 0 {
timeStamp = *imageInfo.History[0].Created
} else {
timeStamp = time.Time{}
}
return timeStamp
}
func (olu BaseOciLayoutUtils) GetImagePlatform(repo string, manifestDigest godigest.Digest) (
string, string,
) {
imageBlobManifest, err := olu.GetImageBlobManifest(repo, manifestDigest)
if err != nil {
olu.Log.Error().Err(err).Msg("can't get image blob manifest")
return "", ""
}
imageConfig, err := olu.GetImageInfo(repo, imageBlobManifest.Config.Digest)
if err != nil {
olu.Log.Error().Err(err).Msg("extension api: error reading image config")
return "", ""
}
return imageConfig.OS, imageConfig.Architecture
}
func (olu BaseOciLayoutUtils) GetImageVendor(repo string, manifestDigest godigest.Digest) string {
imageBlobManifest, err := olu.GetImageBlobManifest(repo, manifestDigest)
if err != nil {
olu.Log.Error().Err(err).Msg("can't get image blob manifest")
return ""
}
imageConfig, err := olu.GetImageInfo(repo, imageBlobManifest.Config.Digest)
if err != nil {
olu.Log.Error().Err(err).Msg("extension api: error reading image config")
return ""
}
return imageConfig.Config.Labels["vendor"]
}
func (olu BaseOciLayoutUtils) GetImageManifestSize(repo string, manifestDigest godigest.Digest) int64 {
imageBlobManifest, err := olu.GetImageBlobManifest(repo, manifestDigest)
if err != nil {
olu.Log.Error().Err(err).Msg("can't get image blob manifest")
return 0
}
return imageBlobManifest.Config.Size
}
func (olu BaseOciLayoutUtils) GetImageConfigSize(repo string, manifestDigest godigest.Digest) int64 {
imageBlobManifest, err := olu.GetImageBlobManifest(repo, manifestDigest)
if err != nil {
olu.Log.Error().Err(err).Msg("can't get image blob manifest")
return 0
}
imageStore := olu.StoreController.GetImageStore(repo)
buf, err := imageStore.GetBlobContent(repo, imageBlobManifest.Config.Digest.String())
if err != nil {
olu.Log.Error().Err(err).Msg("error when getting blob content")
return int64(len(buf))
}
return int64(len(buf))
}
func (olu BaseOciLayoutUtils) GetRepoLastUpdated(repo string) (time.Time, error) {
tagsInfo, err := olu.GetImageTagsWithTimestamp(repo)
if err != nil || len(tagsInfo) == 0 {
return time.Time{}, err
}
latestTag := GetLatestTag(tagsInfo)
return latestTag.Timestamp, nil
}
func (olu BaseOciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) {
repo := RepoInfo{}
manifests := make([]Manifest, 0)
+1 -1
View File
@@ -78,7 +78,7 @@ func ScanImage(ctx *cli.Context) (report.Report, error) {
func GetCVEInfo(storeController storage.StoreController, log log.Logger) (*CveInfo, error) {
cveController := CveTrivyController{}
layoutUtils := common.NewOciLayoutUtils(storeController, log)
layoutUtils := common.NewBaseOciLayoutUtils(storeController, log)
subCveConfig := make(map[string]*TrivyCtx)
+1 -1
View File
@@ -94,7 +94,7 @@ func testSetup() error {
storeController := storage.StoreController{DefaultStore: storage.NewImageStore(dir, false, storage.DefaultGCDelay, false, false, log, metrics)}
layoutUtils := common.NewOciLayoutUtils(storeController, log)
layoutUtils := common.NewBaseOciLayoutUtils(storeController, log)
cve = &cveinfo.CveInfo{Log: log, StoreController: storeController, LayoutUtils: layoutUtils}
+1 -1
View File
@@ -13,7 +13,7 @@ type CveInfo struct {
Log log.Logger
CveTrivyController CveTrivyController
StoreController storage.StoreController
LayoutUtils *common.OciLayoutUtils
LayoutUtils *common.BaseOciLayoutUtils
}
type CveTrivyController struct {
+2 -2
View File
@@ -12,12 +12,12 @@ import (
// DigestInfo implements searching by manifes/config/layer digest.
type DigestInfo struct {
Log log.Logger
LayoutUtils *common.OciLayoutUtils
LayoutUtils *common.BaseOciLayoutUtils
}
// NewDigestInfo initializes a new DigestInfo object.
func NewDigestInfo(storeController storage.StoreController, log log.Logger) *DigestInfo {
layoutUtils := common.NewOciLayoutUtils(storeController, log)
layoutUtils := common.NewBaseOciLayoutUtils(storeController, log)
return &DigestInfo{Log: log, LayoutUtils: layoutUtils}
}
File diff suppressed because it is too large Load Diff
@@ -19,6 +19,12 @@ type CVEResultForImage struct {
CVEList []*Cve `json:"CVEList"`
}
type GlobalSearchResult struct {
Images []*ImageSummary `json:"Images"`
Repos []*RepoSummary `json:"Repos"`
Layers []*LayerSummary `json:"Layers"`
}
type ImageInfo struct {
Name *string `json:"Name"`
Latest *string `json:"Latest"`
@@ -30,6 +36,17 @@ type ImageInfo struct {
Labels *string `json:"Labels"`
}
type ImageSummary struct {
RepoName *string `json:"RepoName"`
Tag *string `json:"Tag"`
LastUpdated *time.Time `json:"LastUpdated"`
IsSigned *bool `json:"IsSigned"`
Size *string `json:"Size"`
Platform *OsArch `json:"Platform"`
Vendor *string `json:"Vendor"`
Score *int `json:"Score"`
}
type ImgResultForCve struct {
Name *string `json:"Name"`
Tags []*string `json:"Tags"`
@@ -49,6 +66,12 @@ type LayerInfo struct {
Digest *string `json:"Digest"`
}
type LayerSummary struct {
Size *string `json:"Size"`
Digest *string `json:"Digest"`
Score *int `json:"Score"`
}
type ManifestInfo struct {
Digest *string `json:"Digest"`
Tag *string `json:"Tag"`
@@ -56,6 +79,11 @@ type ManifestInfo struct {
Layers []*LayerInfo `json:"Layers"`
}
type OsArch struct {
Os *string `json:"Os"`
Arch *string `json:"Arch"`
}
type PackageInfo struct {
Name *string `json:"Name"`
InstalledVersion *string `json:"InstalledVersion"`
@@ -66,6 +94,15 @@ type RepoInfo struct {
Manifests []*ManifestInfo `json:"Manifests"`
}
type RepoSummary struct {
Name *string `json:"Name"`
LastUpdated *time.Time `json:"LastUpdated"`
Size *string `json:"Size"`
Platforms []*OsArch `json:"Platforms"`
Vendors []*string `json:"Vendors"`
Score *int `json:"Score"`
}
type TagInfo struct {
Name *string `json:"Name"`
Digest *string `json:"Digest"`
+177 -1
View File
@@ -5,7 +5,9 @@ package search
// It serves as dependency injection for your app, add any dependencies you require here.
import (
"sort"
"strconv"
"strings"
godigest "github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/log" // nolint: gci
@@ -123,7 +125,7 @@ func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*
r.log.Info().Msg("no repositories found")
}
layoutUtils := common.NewOciLayoutUtils(r.storeController, r.log)
layoutUtils := common.NewBaseOciLayoutUtils(r.storeController, r.log)
for _, repo := range repoList {
tagsInfo, err := layoutUtils.GetImageTagsWithTimestamp(repo)
@@ -186,6 +188,180 @@ func (r *queryResolver) getImageListWithLatestTag(store storage.ImageStore) ([]*
return results, nil
}
func cleanQuerry(query string) string {
query = strings.ToLower(query)
query = strings.Replace(query, ":", " ", 1)
return query
}
func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils, log log.Logger) (
[]*gql_generated.RepoSummary, []*gql_generated.ImageSummary, []*gql_generated.LayerSummary,
) {
repos := []*gql_generated.RepoSummary{}
images := []*gql_generated.ImageSummary{}
layers := []*gql_generated.LayerSummary{}
for _, repo := range repoList {
repo := repo
// map used for dedube if 2 images reference the same blob
repoLayerBlob2Size := make(map[string]int64, 10)
// made up of all manifests, configs and image layers
repoSize := int64(0)
lastUpdate, err := olu.GetRepoLastUpdated(repo)
if err != nil {
log.Error().Err(err).Msgf("can't find latest update timestamp for repo: %s", repo)
}
tagsInfo, err := olu.GetImageTagsWithTimestamp(repo)
if err != nil {
log.Error().Err(err).Msgf("can't get tags info for repo: %s", repo)
continue
}
repoInfo, err := olu.GetExpandedRepoInfo(repo)
if err != nil {
log.Error().Err(err).Msgf("can't get repo info for repo: %s", repo)
continue
}
repoPlatforms := make([]*gql_generated.OsArch, 0, len(tagsInfo))
repoVendors := make([]*string, 0, len(repoInfo.Manifests))
for i, manifest := range repoInfo.Manifests {
imageLayersSize := int64(0)
manifestSize := olu.GetImageManifestSize(repo, godigest.Digest(tagsInfo[i].Digest))
configSize := olu.GetImageConfigSize(repo, godigest.Digest(tagsInfo[i].Digest))
for _, layer := range manifest.Layers {
layer := layer
layerSize, err := strconv.ParseInt(layer.Size, 10, 64)
if err != nil {
log.Error().Err(err).Msg("invalid layer size")
continue
}
repoLayerBlob2Size[layer.Digest] = layerSize
imageLayersSize += layerSize
// if we have a tag we won't match a layer
if tag != "" {
continue
}
if index := strings.Index(layer.Digest, name); index != -1 {
layers = append(layers, &gql_generated.LayerSummary{
Digest: &layer.Digest,
Size: &layer.Size,
Score: &index,
})
}
}
imageSize := imageLayersSize + manifestSize + configSize
repoSize += manifestSize + configSize
index := strings.Index(repo, name)
matchesTag := strings.HasPrefix(manifest.Tag, tag)
if index != -1 {
tag := manifest.Tag
size := strconv.Itoa(int(imageSize))
vendor := olu.GetImageVendor(repo, godigest.Digest(tagsInfo[i].Digest))
lastUpdated := olu.GetImageLastUpdated(repo, godigest.Digest(tagsInfo[i].Digest))
isSigned := manifest.IsSigned
// update matching score
score := calculateImageMatchingScore(repo, index, matchesTag)
os, arch := olu.GetImagePlatform(repo, godigest.Digest(tagsInfo[i].Digest))
osArch := &gql_generated.OsArch{
Os: &os,
Arch: &arch,
}
repoPlatforms = append(repoPlatforms, osArch)
repoVendors = append(repoVendors, &vendor)
images = append(images, &gql_generated.ImageSummary{
RepoName: &repo,
Tag: &tag,
LastUpdated: &lastUpdated,
IsSigned: &isSigned,
Size: &size,
Platform: osArch,
Vendor: &vendor,
Score: &score,
})
}
}
for layerBlob := range repoLayerBlob2Size {
repoSize += repoLayerBlob2Size[layerBlob]
}
if index := strings.Index(repo, name); index != -1 {
repoSize := strconv.FormatInt(repoSize, 10)
repos = append(repos, &gql_generated.RepoSummary{
Name: &repo,
LastUpdated: &lastUpdate,
Size: &repoSize,
Platforms: repoPlatforms,
Vendors: repoVendors,
Score: &index,
})
}
}
sort.Slice(repos, func(i, j int) bool {
return *repos[i].Score < *repos[j].Score
})
sort.Slice(images, func(i, j int) bool {
return *images[i].Score < *images[j].Score
})
sort.Slice(layers, func(i, j int) bool {
return *layers[i].Score < *layers[j].Score
})
return repos, images, layers
}
// calcalculateImageMatchingScore iterated from the index of the matched string in the
// artifact name until the beginning of the string or until delimitator "/".
// The distance represents the score of the match.
//
// Example:
// query: image
// repos: repo/test/myimage
// Score will be 2.
func calculateImageMatchingScore(artefactName string, index int, matchesTag bool) int {
score := 0
for index >= 1 {
if artefactName[index-1] == '/' {
break
}
index--
score++
}
if !matchesTag {
score += 10
}
return score
}
func getGraphqlCompatibleTags(fixedTags []common.TagInfo) []*gql_generated.TagInfo {
finalTagList := make([]*gql_generated.TagInfo, 0)
+154
View File
@@ -0,0 +1,154 @@
package search //nolint
import (
"errors"
"strings"
"testing"
"time"
godigest "github.com/opencontainers/go-digest"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/extensions/search/common"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/test/mocks"
)
var ErrTestError = errors.New("TestError")
func TestGlobalSearch(t *testing.T) {
Convey("globalSearch", t, func() {
Convey("GetRepoLastUpdated fail", func() {
mockOlum := mocks.OciLayoutUtilsMock{
GetRepoLastUpdatedFn: func(repo string) (time.Time, error) {
return time.Time{}, ErrTestError
},
}
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
})
Convey("GetImageTagsWithTimestamp fail", func() {
mockOlum := mocks.OciLayoutUtilsMock{
GetImageTagsWithTimestampFn: func(repo string) ([]common.TagInfo, error) {
return []common.TagInfo{}, ErrTestError
},
}
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
})
Convey("GetExpandedRepoInfo fail", func() {
mockOlum := mocks.OciLayoutUtilsMock{
GetExpandedRepoInfoFn: func(name string) (common.RepoInfo, error) {
return common.RepoInfo{}, ErrTestError
},
}
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
})
Convey("Bad layer digest in manifest", func() {
mockOlum := mocks.OciLayoutUtilsMock{
GetExpandedRepoInfoFn: func(name string) (common.RepoInfo, error) {
return common.RepoInfo{
Manifests: []common.Manifest{
{
Tag: "latest",
Layers: []common.Layer{
{
Size: "this is a bad size format",
Digest: "digest",
},
},
},
},
}, nil
},
GetImageManifestSizeFn: func(repo string, manifestDigest godigest.Digest) int64 {
return 100
},
GetImageConfigSizeFn: func(repo string, manifestDigest godigest.Digest) int64 {
return 100
},
GetImageTagsWithTimestampFn: func(repo string) ([]common.TagInfo, error) {
return []common.TagInfo{
{
Name: "test",
Digest: "test",
},
}, nil
},
}
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
})
Convey("Tag given, no layer match", func() {
mockOlum := mocks.OciLayoutUtilsMock{
GetExpandedRepoInfoFn: func(name string) (common.RepoInfo, error) {
return common.RepoInfo{
Manifests: []common.Manifest{
{
Tag: "latest",
Layers: []common.Layer{
{
Size: "100",
Digest: "sha256:855b1556a45637abf05c63407437f6f305b4627c4361fb965a78e5731999c0c7",
},
},
},
},
}, nil
},
GetImageManifestSizeFn: func(repo string, manifestDigest godigest.Digest) int64 {
return 100
},
GetImageConfigSizeFn: func(repo string, manifestDigest godigest.Digest) int64 {
return 100
},
GetImageTagsWithTimestampFn: func(repo string) ([]common.TagInfo, error) {
return []common.TagInfo{
{
Name: "test",
Digest: "test",
},
}, nil
},
}
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
})
})
}
func TestMatching(t *testing.T) {
pine := "pine"
Convey("Perfect Matching", t, func() {
query := "alpine"
score := calculateImageMatchingScore("alpine", strings.Index("alpine", query), true)
So(score, ShouldEqual, 0)
})
Convey("Partial Matching", t, func() {
query := pine
score := calculateImageMatchingScore("alpine", strings.Index("alpine", query), true)
So(score, ShouldEqual, 2)
})
Convey("Complex Partial Matching", t, func() {
query := pine
score := calculateImageMatchingScore("repo/test/alpine", strings.Index("alpine", query), true)
So(score, ShouldEqual, 2)
query = pine
score = calculateImageMatchingScore("repo/alpine/test", strings.Index("alpine", query), true)
So(score, ShouldEqual, 2)
query = pine
score = calculateImageMatchingScore("alpine/repo/test", strings.Index("alpine", query), true)
So(score, ShouldEqual, 2)
query = pine
score = calculateImageMatchingScore("alpine/repo/test", strings.Index("alpine", query), false)
So(score, ShouldEqual, 12)
})
}
+45
View File
@@ -66,6 +66,50 @@ type LayerInfo {
Digest: String
}
# Search results in all repos/images/layers
# There will be other more structures for more detailed information
type GlobalSearchResult {
Images: [ImageSummary]
Repos: [RepoSummary]
Layers: [LayerSummary]
}
# Brief on a specific image to be used in queries returning a list of images
# We define an image as a pairing or a repo and a tag belonging to that repo
type ImageSummary {
RepoName: String
Tag: String
LastUpdated: Time
IsSigned: Boolean
Size: String
Platform: OsArch
Vendor: String
Score: Int
}
# Brief on a specific repo to be used in queries returning a list of repos
type RepoSummary {
Name: String
LastUpdated: Time
Size: String
Platforms: [OsArch]
Vendors: [String]
Score: Int
}
# Currently the same as LayerInfo, we can refactor later
# For detailed information on the layer a ImageListForDigest call can be made
type LayerSummary {
Size: String # Int64 is not supported.
Digest: String
Score: Int
}
type OsArch {
Os: String
Arch: String
}
type Query {
CVEListForImage(image: String!) :CVEResultForImage
ImageListForCVE(id: String!) :[ImgResultForCVE]
@@ -73,4 +117,5 @@ type Query {
ImageListForDigest(id: String!) :[ImgResultForDigest]
ImageListWithLatestTag:[ImageInfo]
ExpandedRepoInfo(repo: String!):RepoInfo
GlobalSearch(query: String!): GlobalSearchResult
}
+30 -1
View File
@@ -319,7 +319,7 @@ func (r *queryResolver) ImageListWithLatestTag(ctx context.Context) ([]*gql_gene
// ExpandedRepoInfo is the resolver for the ExpandedRepoInfo field.
func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql_generated.RepoInfo, error) {
olu := common.NewOciLayoutUtils(r.storeController, r.log)
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
origRepoInfo, err := olu.GetExpandedRepoInfo(repo)
if err != nil {
@@ -364,6 +364,35 @@ func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, repo string) (*gql
return repoInfo, nil
}
// GlobalSearch is the resolver for the GlobalSearch field.
func (r *queryResolver) GlobalSearch(ctx context.Context, query string) (*gql_generated.GlobalSearchResult, error) {
query = cleanQuerry(query)
defaultStore := r.storeController.DefaultStore
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
var name, tag string
_, err := fmt.Sscanf(query, "%s %s", &name, &tag)
if err != nil {
name = query
}
repoList, err := defaultStore.GetRepositories()
if err != nil {
r.log.Error().Err(err).Msg("unable to search repositories")
return &gql_generated.GlobalSearchResult{}, err
}
repos, images, layers := globalSearch(repoList, name, tag, olu, r.log)
return &gql_generated.GlobalSearchResult{
Images: images,
Repos: repos,
Layers: layers,
}, nil
}
// Query returns gql_generated.QueryResolver implementation.
func (r *Resolver) Query() gql_generated.QueryResolver { return &queryResolver{r} }