0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

search: graphql api to give detailed repo info

DetailedRepoInfo graphql api returns detailed repo info given repo name
repo contains its manifests info
Each manifest entry contains digest,signed, tag and layers info
Each layer info containes digest, size

Signed-off-by: Shivam Mishra <shimish2@cisco.com>
This commit is contained in:
Shivam Mishra 2022-02-02 02:02:05 +00:00 committed by Ramkumar Chinchani
parent 4ddfd059b6
commit 37d150e32f
7 changed files with 942 additions and 29 deletions

2
go.mod
View file

@ -41,6 +41,7 @@ require (
github.com/opencontainers/umoci v0.4.8-0.20210922062158-e60a0cc726e6 github.com/opencontainers/umoci v0.4.8-0.20210922062158-e60a0cc726e6
github.com/oras-project/artifacts-spec v1.0.0-draft.1 github.com/oras-project/artifacts-spec v1.0.0-draft.1
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_golang v1.11.0
github.com/prometheus/client_model v0.2.0 github.com/prometheus/client_model v0.2.0
github.com/rs/zerolog v1.26.0 github.com/rs/zerolog v1.26.0
@ -271,7 +272,6 @@ require (
github.com/owenrumney/squealer v0.2.28 // indirect github.com/owenrumney/squealer v0.2.28 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.31.1 // indirect github.com/prometheus/common v0.31.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.7.3 // indirect

View file

@ -36,6 +36,15 @@ type ImgResponsWithLatestTag struct {
Errors []ErrorGQL `json:"errors"` Errors []ErrorGQL `json:"errors"`
} }
type ExpandedRepoInfoResp struct {
ExpandedRepoInfo ExpandedRepoInfo `json:"data"`
Errors []ErrorGQL `json:"errors"`
}
type ExpandedRepoInfo struct {
RepoInfo common.RepoInfo `json:"expandedRepoInfo"`
}
//nolint:tagliatelle // graphQL schema //nolint:tagliatelle // graphQL schema
type ImgListWithLatestTag struct { type ImgListWithLatestTag struct {
Images []ImageInfo `json:"ImageListWithLatestTag"` Images []ImageInfo `json:"ImageListWithLatestTag"`
@ -311,6 +320,110 @@ func TestLatestTagSearchHTTP(t *testing.T) {
}) })
} }
func TestExpandedRepoInfo(t *testing.T) {
Convey("Test expanded repo info", t, func() {
err := testSetup()
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["/a"] = 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(); 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)
}()
resp, err := resty.R().Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, err = resty.R().Get(baseURL + "/query")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
query := "{ExpandedRepoInfo(repo:\"zot-test\"){Manifests%20{Digest%20IsSigned%20Tag%20Layers%20{Size%20Digest}}}}"
resp, err = resty.R().Get(baseURL + "/query?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := &ExpandedRepoInfoResp{}
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
query = "{ExpandedRepoInfo(repo:\"\"){Manifests%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}"
resp, err = resty.R().Get(baseURL + "/query?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
query = "{ExpandedRepoInfo(repo:\"a/zot-test\"){Manifests%20{Digest%20Tag%20IsSigned%20%Layers%20{Size%20Digest}}}}"
resp, err = resty.R().Get(baseURL + "/query?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
err = os.Remove(path.Join(rootDir, "zot-test/blobs/sha256",
"2bacca16b9df395fc855c14ccf50b12b58d35d468b8e7f25758aff90f89bf396"))
if err != nil {
panic(err)
}
query = "{ExpandedRepoInfo(repo:\"zot-test\"){Manifests%20{Digest%20Tag%20IsSigned%20%Layers%20{Size%20Digest}}}}"
resp, err = resty.R().Get(baseURL + "/query?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
})
}
func TestUtilsMethod(t *testing.T) { func TestUtilsMethod(t *testing.T) {
Convey("Test utils", t, func() { Convey("Test utils", t, func() {
// Test GetRepo method // Test GetRepo method

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
goerrors "errors" goerrors "errors"
"path" "path"
"strconv"
"strings" "strings"
"time" "time"
@ -17,12 +18,30 @@ import (
"zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage"
) )
const cosignedAnnotation = "dev.cosign.signature.baseimage"
// OciLayoutInfo ... // OciLayoutInfo ...
type OciLayoutUtils struct { type OciLayoutUtils struct {
Log log.Logger Log log.Logger
StoreController storage.StoreController StoreController storage.StoreController
} }
type RepoInfo struct {
Manifests []Manifest `json:"manifests"`
}
type Manifest struct {
Tag string `json:"tag"`
Digest string `json:"digest"`
IsSigned bool `json:"isSigned"`
Layers []Layer `json:"layers"`
}
type Layer struct {
Size string `json:"size"`
Digest string `json:"digest"`
}
// NewOciLayoutUtils initializes a new OciLayoutUtils object. // NewOciLayoutUtils initializes a new OciLayoutUtils object.
func NewOciLayoutUtils(storeController storage.StoreController, log log.Logger) *OciLayoutUtils { func NewOciLayoutUtils(storeController storage.StoreController, log log.Logger) *OciLayoutUtils {
return &OciLayoutUtils{Log: log, StoreController: storeController} return &OciLayoutUtils{Log: log, StoreController: storeController}
@ -183,6 +202,65 @@ func (olu OciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]TagInfo, err
return tagsInfo, nil return tagsInfo, nil
} }
func (olu OciLayoutUtils) GetExpandedRepoInfo(name string) (RepoInfo, error) {
repo := RepoInfo{}
manifests := make([]Manifest, 0)
manifestList, err := olu.GetImageManifests(name)
if err != nil {
olu.Log.Error().Err(err).Msg("error getting image manifests")
return RepoInfo{}, err
}
for _, manifest := range manifestList {
manifestInfo := Manifest{}
manifestInfo.Digest = manifest.Digest.Encoded()
manifestInfo.IsSigned = false
tag, ok := manifest.Annotations[ispec.AnnotationRefName]
if !ok {
tag = "latest"
}
manifestInfo.Tag = tag
manifest, err := olu.GetImageBlobManifest(name, manifest.Digest)
if err != nil {
olu.Log.Error().Err(err).Msg("error getting image manifest blob")
return RepoInfo{}, err
}
layers := make([]Layer, 0)
for _, layer := range manifest.Layers {
layerInfo := Layer{}
layerInfo.Digest = layer.Digest.Hex
layerInfo.Size = strconv.FormatInt(layer.Size, 10)
layers = append(layers, layerInfo)
if _, ok := layer.Annotations[cosignedAnnotation]; ok {
manifestInfo.IsSigned = true
}
}
manifestInfo.Layers = layers
manifests = append(manifests, manifestInfo)
}
repo.Manifests = manifests
return repo, nil
}
func GetImageDirAndTag(imageName string) (string, string) { func GetImageDirAndTag(imageName string) (string, string) {
var imageDir string var imageDir string

File diff suppressed because it is too large Load diff

View file

@ -44,12 +44,28 @@ type ImgResultForFixedCve struct {
Tags []*TagInfo `json:"Tags"` Tags []*TagInfo `json:"Tags"`
} }
type LayerInfo struct {
Size *string `json:"Size"`
Digest *string `json:"Digest"`
}
type ManifestInfo struct {
Digest *string `json:"Digest"`
Tag *string `json:"Tag"`
IsSigned *bool `json:"IsSigned"`
Layers []*LayerInfo `json:"Layers"`
}
type PackageInfo struct { type PackageInfo struct {
Name *string `json:"Name"` Name *string `json:"Name"`
InstalledVersion *string `json:"InstalledVersion"` InstalledVersion *string `json:"InstalledVersion"`
FixedVersion *string `json:"FixedVersion"` FixedVersion *string `json:"FixedVersion"`
} }
type RepoInfo struct {
Manifests []*ManifestInfo `json:"Manifests"`
}
type TagInfo struct { type TagInfo struct {
Name *string `json:"Name"` Name *string `json:"Name"`
Digest *string `json:"Digest"` Digest *string `json:"Digest"`

View file

@ -62,6 +62,52 @@ func GetResolverConfig(log log.Logger, storeController storage.StoreController,
} }
} }
func (r *queryResolver) ExpandedRepoInfo(ctx context.Context, name string) (*RepoInfo, error) {
olu := common.NewOciLayoutUtils(r.storeController, r.log)
repo, err := olu.GetExpandedRepoInfo(name)
if err != nil {
r.log.Error().Err(err).Msg("error getting repos")
return &RepoInfo{}, err
}
// repos type is of common deep copy this to search
repoInfo := &RepoInfo{}
manifests := make([]*ManifestInfo, 0)
for _, manifest := range repo.Manifests {
tag := manifest.Tag
digest := manifest.Digest
isSigned := manifest.IsSigned
manifestInfo := &ManifestInfo{Tag: &tag, Digest: &digest, IsSigned: &isSigned}
layers := make([]*LayerInfo, 0)
for _, l := range manifest.Layers {
size := l.Size
digest := l.Digest
layerInfo := &LayerInfo{Digest: &digest, Size: &size}
layers = append(layers, layerInfo)
}
manifestInfo.Layers = layers
manifests = append(manifests, manifestInfo)
}
repoInfo.Manifests = manifests
return repoInfo, nil
}
func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*CVEResultForImage, error) { func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*CVEResultForImage, error) {
trivyCtx := r.cveInfo.GetTrivyContext(image) trivyCtx := r.cveInfo.GetTrivyContext(image)

View file

@ -50,10 +50,27 @@ type ImageInfo {
Labels: String Labels: String
} }
type RepoInfo {
Manifests: [ManifestInfo]
}
type ManifestInfo {
Digest: String
Tag: String
IsSigned: Boolean
Layers: [LayerInfo]
}
type LayerInfo {
Size: String # Int64 is not supported.
Digest: String
}
type Query { type Query {
CVEListForImage(image: String!) :CVEResultForImage CVEListForImage(image: String!) :CVEResultForImage
ImageListForCVE(id: String!) :[ImgResultForCVE] ImageListForCVE(id: String!) :[ImgResultForCVE]
ImageListWithCVEFixed(id: String!, image: String!) :ImgResultForFixedCVE ImageListWithCVEFixed(id: String!, image: String!) :ImgResultForFixedCVE
ImageListForDigest(id: String!) :[ImgResultForDigest] ImageListForDigest(id: String!) :[ImgResultForDigest]
ImageListWithLatestTag:[ImageInfo] ImageListWithLatestTag:[ImageInfo]
ExpandedRepoInfo(repo: String!):RepoInfo
} }