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:
parent
4ddfd059b6
commit
37d150e32f
7 changed files with 942 additions and 29 deletions
2
go.mod
2
go.mod
|
@ -41,6 +41,7 @@ require (
|
|||
github.com/opencontainers/umoci v0.4.8-0.20210922062158-e60a0cc726e6
|
||||
github.com/oras-project/artifacts-spec v1.0.0-draft.1
|
||||
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_model v0.2.0
|
||||
github.com/rs/zerolog v1.26.0
|
||||
|
@ -271,7 +272,6 @@ require (
|
|||
github.com/owenrumney/squealer v0.2.28 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // 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/prometheus/common v0.31.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
|
|
|
@ -36,6 +36,15 @@ type ImgResponsWithLatestTag struct {
|
|||
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
|
||||
type ImgListWithLatestTag struct {
|
||||
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) {
|
||||
Convey("Test utils", t, func() {
|
||||
// Test GetRepo method
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
goerrors "errors"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -17,12 +18,30 @@ import (
|
|||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
const cosignedAnnotation = "dev.cosign.signature.baseimage"
|
||||
|
||||
// OciLayoutInfo ...
|
||||
type OciLayoutUtils struct {
|
||||
Log log.Logger
|
||||
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.
|
||||
func NewOciLayoutUtils(storeController storage.StoreController, log log.Logger) *OciLayoutUtils {
|
||||
return &OciLayoutUtils{Log: log, StoreController: storeController}
|
||||
|
@ -183,6 +202,65 @@ func (olu OciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]TagInfo, err
|
|||
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) {
|
||||
var imageDir string
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -44,12 +44,28 @@ type ImgResultForFixedCve struct {
|
|||
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 {
|
||||
Name *string `json:"Name"`
|
||||
InstalledVersion *string `json:"InstalledVersion"`
|
||||
FixedVersion *string `json:"FixedVersion"`
|
||||
}
|
||||
|
||||
type RepoInfo struct {
|
||||
Manifests []*ManifestInfo `json:"Manifests"`
|
||||
}
|
||||
|
||||
type TagInfo struct {
|
||||
Name *string `json:"Name"`
|
||||
Digest *string `json:"Digest"`
|
||||
|
|
|
@ -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) {
|
||||
trivyCtx := r.cveInfo.GetTrivyContext(image)
|
||||
|
||||
|
|
|
@ -50,10 +50,27 @@ type ImageInfo {
|
|||
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 {
|
||||
CVEListForImage(image: String!) :CVEResultForImage
|
||||
ImageListForCVE(id: String!) :[ImgResultForCVE]
|
||||
ImageListWithCVEFixed(id: String!, image: String!) :ImgResultForFixedCVE
|
||||
ImageListForDigest(id: String!) :[ImgResultForDigest]
|
||||
ImageListWithLatestTag:[ImageInfo]
|
||||
ExpandedRepoInfo(repo: String!):RepoInfo
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue