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/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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
@ -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"`
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue