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

list all images that have are base images for the given image + zli command

Signed-off-by: Lisca Ana-Roberta <ana.kagome@yahoo.com>
This commit is contained in:
Lisca Ana-Roberta 2022-08-08 10:40:30 +03:00 committed by Ramkumar Chinchani
parent 220de354b2
commit dbe96efa00
10 changed files with 880 additions and 3 deletions

View file

@ -121,6 +121,8 @@ func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*stri
searchImageParams["imageName"] = imageCmd.Flags().StringP("name", "n", "", "List image details by name")
searchImageParams["digest"] = imageCmd.Flags().StringP("digest", "d", "",
"List images containing a specific manifest, config, or layer digest")
searchImageParams["derivedImage"] = imageCmd.Flags().StringP("derived-images", "D", "",
"List images that are derived from given image")
imageCmd.Flags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
imageCmd.Flags().StringVarP(user, "user", "u", "", `User Credentials of zot server in "username:password" format`)

View file

@ -250,6 +250,100 @@ func TestSearchImageCmd(t *testing.T) {
})
}
func TestDerivedImageList(t *testing.T) {
Convey("Test from real server", t, func() {
port := test.GetFreePort()
url := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{Enable: &defaultVal},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
go func(controller *api.Controller) {
// this blocks
if err := controller.Run(context.Background()); err != nil {
return
}
}(ctlr)
// wait till ready
for {
_, err := resty.R().Get(url)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
defer func(controller *api.Controller) {
ctx := context.Background()
_ = controller.Server.Shutdown(ctx)
}(ctlr)
err := uploadManifest(url)
So(err, ShouldBeNil)
t.Logf("rootDir: %s", ctlr.Config.Storage.RootDirectory)
Convey("Test derived images list working", func() {
t.Logf("%s", ctlr.Config.Storage.RootDirectory)
args := []string{"imagetest", "--derived-images", "repo7:test:1.0"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := NewImageCommand(new(searchService))
buff := &bytes.Buffer{}
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
So(actual, ShouldContainSubstring, "repo7 test:2.0 883fc0c5 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 883fc0c5 15B")
})
Convey("Test derived images fail", func() {
t.Logf("%s", ctlr.Config.Storage.RootDirectory)
err = os.Chmod(ctlr.Config.Storage.RootDirectory, 0o000)
So(err, ShouldBeNil)
defer func() {
err := os.Chmod(ctlr.Config.Storage.RootDirectory, 0o755)
So(err, ShouldBeNil)
}()
args := []string{"imagetest", "--derived-images", "repo7:test:1.0"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := NewImageCommand(new(searchService))
buff := &bytes.Buffer{}
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("Test derived images list cannot print", func() {
t.Logf("%s", ctlr.Config.Storage.RootDirectory)
args := []string{"imagetest", "--derived-images", "repo7:test:1.0", "-o", "random"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := NewImageCommand(new(searchService))
buff := &bytes.Buffer{}
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
})
}
func TestListRepos(t *testing.T) {
Convey("Test listing repositories", t, func() {
args := []string{"config-test"}
@ -1172,6 +1266,22 @@ func (service mockService) getRepos(ctx context.Context, config searchConfig, us
channel <- stringResult{"", nil}
}
func (service mockService) getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string,
derivedImage string,
) (*imageListStructForDerivedImagesGQL, error) {
imageListGQLResponse := &imageListStructForDerivedImagesGQL{}
imageListGQLResponse.Data.ImageList = []imageStruct{
{
RepoName: "dummyImageName",
Tag: "tag",
Digest: "DigestsAreReallyLong",
Size: "123445",
},
}
return imageListGQLResponse, nil
}
func (service mockService) getImagesGQL(ctx context.Context, config searchConfig, username, password string,
imageName string,
) (*imageListStructGQL, error) {

View file

@ -42,6 +42,7 @@ func getImageSearchersGQL() []searcher {
new(allImagesSearcherGQL),
new(imageByNameSearcherGQL),
new(imagesByDigestSearcherGQL),
new(derivedImageListSearcherGQL),
}
return searchers
@ -219,6 +220,31 @@ func (search imagesByDigestSearcher) search(config searchConfig) (bool, error) {
}
}
type derivedImageListSearcherGQL struct{}
func (search derivedImageListSearcherGQL) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("derivedImage")) {
return false, nil
}
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getDerivedImageListGQL(ctx, config, username,
password, *config.params["derivedImage"])
if err != nil {
return true, err
}
if err := printResult(config, imageList.Data.ImageList); err != nil {
return true, err
}
return true, nil
}
type imagesByDigestSearcherGQL struct{}
func (search imagesByDigestSearcherGQL) search(config searchConfig) (bool, error) {

View file

@ -34,6 +34,8 @@ type SearchService interface {
cveID string) (*imagesForCve, error)
getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
cveID string) (*fixedTags, error)
getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string,
derivedImage string) (*imageListStructForDerivedImagesGQL, error)
getAllImages(ctx context.Context, config searchConfig, username, password string,
channel chan stringResult, wtgrp *sync.WaitGroup)
@ -59,6 +61,32 @@ func NewSearchService() SearchService {
return searchService{}
}
func (service searchService) getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string,
derivedImage string,
) (*imageListStructForDerivedImagesGQL, error) {
query := fmt.Sprintf(`
{
DerivedImageList(image:"%s"){
RepoName,
Tag,
Digest,
ConfigDigest,
LastUpdated,
IsSigned,
Size
}
}`, derivedImage)
result := &imageListStructForDerivedImagesGQL{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
return nil, errResult
}
return result, nil
}
func (service searchService) getImagesGQL(ctx context.Context, config searchConfig, username, password string,
imageName string,
) (*imageListStructGQL, error) {
@ -803,6 +831,13 @@ type imageListStructForDigestGQL struct {
} `json:"data"`
}
type imageListStructForDerivedImagesGQL struct {
Errors []errorGraphQL `json:"errors"`
Data struct {
ImageList []imageStruct `json:"DerivedImageList"` // nolint:tagliatelle
} `json:"data"`
}
type imagesForDigest struct {
Errors []errorGraphQL `json:"errors"`
Data struct {

View file

@ -14,10 +14,12 @@ import (
"os/exec"
"path"
"strconv"
"strings"
"testing"
"time"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sigstore/cosign/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/cmd/cosign/cli/options"
@ -982,6 +984,450 @@ func TestUtilsMethod(t *testing.T) {
})
}
func TestGetImageManifest(t *testing.T) {
Convey("Test inexistent image", t, func() {
mockImageStore := mocks.MockedImageStore{}
storeController := storage.StoreController{
DefaultStore: mockImageStore,
}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
_, err := olu.GetImageManifest("inexistent-repo", "latest")
So(err, ShouldNotBeNil)
})
Convey("Test inexistent image", t, func() {
mockImageStore := mocks.MockedImageStore{
GetImageManifestFn: func(repo string, reference string) ([]byte, string, string, error) {
return []byte{}, "", "", ErrTestError
},
}
storeController := storage.StoreController{
DefaultStore: mockImageStore,
}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
_, err := olu.GetImageManifest("test-repo", "latest")
So(err, ShouldNotBeNil)
})
}
func TestDerivedImageList(t *testing.T) {
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)
}()
Convey("Test dependency list for image working", t, func() {
// create test images
config := ispec.Image{
Architecture: "amd64",
OS: "linux",
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []digest.Digest{},
},
Author: "ZotUser",
}
configBlob, err := json.Marshal(config)
So(err, ShouldBeNil)
configDigest := digest.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: digest.FromBytes(layers[0]),
Size: int64(len(layers[0])),
},
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest.FromBytes(layers[1]),
Size: int64(len(layers[1])),
},
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest.FromBytes(layers[2]),
Size: int64(len(layers[2])),
},
},
}
repoName := "test-repo"
err = UploadImage(
Image{
Manifest: manifest,
Config: config,
Layers: layers,
Tag: "latest",
},
baseURL,
repoName,
)
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: digest.FromBytes(layers[0]),
Size: int64(len(layers[0])),
},
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest.FromBytes(layers[1]),
Size: int64(len(layers[1])),
},
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest.FromBytes(layers[2]),
Size: int64(len(layers[2])),
},
},
}
repoName = "same-layers"
err = UploadImage(
Image{
Manifest: manifest,
Config: config,
Layers: layers,
Tag: "latest",
},
baseURL,
repoName,
)
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: digest.FromBytes(layers[0]),
Size: int64(len(layers[0])),
},
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest.FromBytes(layers[1]),
Size: int64(len(layers[1])),
},
},
}
repoName = "missing-layer"
err = UploadImage(
Image{
Manifest: manifest,
Config: config,
Layers: layers,
Tag: "latest",
},
baseURL,
repoName,
)
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: digest.FromBytes(layers[0]),
Size: int64(len(layers[0])),
},
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest.FromBytes(layers[1]),
Size: int64(len(layers[1])),
},
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest.FromBytes(layers[2]),
Size: int64(len(layers[2])),
},
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest.FromBytes(layers[3]),
Size: int64(len(layers[3])),
},
{
MediaType: "application/vnd.oci.image.layer.v1.tar",
Digest: digest.FromBytes(layers[4]),
Size: int64(len(layers[4])),
},
},
}
repoName = "more-layers"
err = UploadImage(
Image{
Manifest: manifest,
Config: config,
Layers: layers,
Tag: "latest",
},
baseURL,
repoName,
)
So(err, ShouldBeNil)
query := `
{
DerivedImageList(image:"test-repo"){
RepoName,
Tag,
Digest,
ConfigDigest,
LastUpdated,
IsSigned,
Size
}
}`
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
So(resp, ShouldNotBeNil)
So(strings.Contains(string(resp.Body()), "same-layers"), ShouldBeTrue)
So(strings.Contains(string(resp.Body()), "missing-layers"), ShouldBeFalse)
So(strings.Contains(string(resp.Body()), "more-layers"), ShouldBeTrue)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
})
Convey("Inexistent repository", t, func() {
query := `
{
DerivedImageList(image:"inexistent-image"){
RepoName,
Tag,
Digest,
ConfigDigest,
LastUpdated,
IsSigned,
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("Failed to get manifest", t, func() {
err := os.Mkdir(path.Join(rootDir, "fail-image"), 0o000)
So(err, ShouldBeNil)
query := `
{
DerivedImageList(image:"fail-image"){
RepoName,
Tag,
Digest,
ConfigDigest,
LastUpdated,
IsSigned,
Size
}
}`
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
So(strings.Contains(string(resp.Body()), "permission denied"), ShouldBeTrue)
So(err, ShouldBeNil)
})
}
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{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 := `
{
DerivedImageList(image:"test-image"){
RepoName,
Tag,
Digest,
ConfigDigest,
LastUpdated,
IsSigned,
Size
}
}`
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
So(strings.Contains(string(resp.Body()), "{\"data\":{\"DerivedImageList\":[]}}"), 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]storage.ImageStore{"test": mockImageStore},
}
olu := common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
repoList, err := olu.GetRepositories()
So(repoList, ShouldBeEmpty)
So(err, ShouldNotBeNil)
storeController = storage.StoreController{
DefaultStore: mocks.MockedImageStore{},
SubStore: map[string]storage.ImageStore{"test": mockImageStore},
}
olu = common.NewBaseOciLayoutUtils(storeController, log.NewLogger("debug", ""))
repoList, err = olu.GetRepositories()
So(repoList, ShouldBeEmpty)
So(err, ShouldNotBeNil)
})
}
func TestGlobalSearch(t *testing.T) {
Convey("Test utils", t, func() {
subpath := "/a"

View file

@ -34,6 +34,7 @@ type OciLayoutUtils interface {
GetExpandedRepoInfo(name string) (RepoInfo, error)
GetImageConfigInfo(repo string, manifestDigest godigest.Digest) (ispec.Image, error)
CheckManifestSignature(name string, digest godigest.Digest) bool
GetRepositories() ([]string, error)
}
// OciLayoutInfo ...
@ -78,6 +79,49 @@ func NewBaseOciLayoutUtils(storeController storage.StoreController, log log.Logg
return &BaseOciLayoutUtils{Log: log, StoreController: storeController}
}
func (olu BaseOciLayoutUtils) GetImageManifest(repo string, reference string) (ispec.Manifest, error) {
imageStore := olu.StoreController.GetImageStore(repo)
if reference == "" {
reference = "latest"
}
buf, _, _, err := imageStore.GetImageManifest(repo, reference)
if err != nil {
return ispec.Manifest{}, err
}
var manifest ispec.Manifest
err = json.Unmarshal(buf, &manifest)
if err != nil {
return ispec.Manifest{}, err
}
return manifest, nil
}
func (olu BaseOciLayoutUtils) GetRepositories() ([]string, error) {
defaultStore := olu.StoreController.DefaultStore
substores := olu.StoreController.SubStore
repoList, err := defaultStore.GetRepositories()
if err != nil {
return []string{}, err
}
for _, sub := range substores {
repoListForSubstore, err := sub.GetRepositories()
if err != nil {
return []string{}, err
}
repoList = append(repoList, repoListForSubstore...)
}
return repoList, nil
}
// Below method will return image path including root dir, root dir is determined by splitting.
func (olu BaseOciLayoutUtils) GetImageManifests(image string) ([]ispec.Descriptor, error) {
imageStore := olu.StoreController.GetImageStore(image)
@ -476,9 +520,7 @@ func GetImageDirAndTag(imageName string) (string, string) {
var imageTag string
if strings.Contains(imageName, ":") {
splitImageName := strings.Split(imageName, ":")
imageDir = splitImageName[0]
imageTag = splitImageName[1]
imageDir, imageTag, _ = strings.Cut(imageName, ":")
} else {
imageDir = imageName
}

View file

@ -99,6 +99,7 @@ type ComplexityRoot struct {
Query struct {
CVEListForImage func(childComplexity int, image string) int
DerivedImageList func(childComplexity int, image string) int
ExpandedRepoInfo func(childComplexity int, repo string) int
GlobalSearch func(childComplexity int, query string) int
ImageList func(childComplexity int, repo string) int
@ -136,6 +137,7 @@ type QueryResolver interface {
ImageList(ctx context.Context, repo string) ([]*ImageSummary, error)
ExpandedRepoInfo(ctx context.Context, repo string) (*RepoInfo, error)
GlobalSearch(ctx context.Context, query string) (*GlobalSearchResult, error)
DerivedImageList(ctx context.Context, image string) ([]*ImageSummary, error)
}
type executableSchema struct {
@ -396,6 +398,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.CVEListForImage(childComplexity, args["image"].(string)), true
case "Query.DerivedImageList":
if e.complexity.Query.DerivedImageList == nil {
break
}
args, err := ec.field_Query_DerivedImageList_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.DerivedImageList(childComplexity, args["image"].(string)), true
case "Query.ExpandedRepoInfo":
if e.complexity.Query.ExpandedRepoInfo == nil {
break
@ -701,6 +715,7 @@ type Query {
ImageList(repo: String!): [ImageSummary!]
ExpandedRepoInfo(repo: String!): RepoInfo!
GlobalSearch(query: String!): GlobalSearchResult!
DerivedImageList(image: String!): [ImageSummary!]
}
`, BuiltIn: false},
}
@ -725,6 +740,21 @@ func (ec *executionContext) field_Query_CVEListForImage_args(ctx context.Context
return args, nil
}
func (ec *executionContext) field_Query_DerivedImageList_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 string
if tmp, ok := rawArgs["image"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("image"))
arg0, err = ec.unmarshalNString2string(ctx, tmp)
if err != nil {
return nil, err
}
}
args["image"] = arg0
return args, nil
}
func (ec *executionContext) field_Query_ExpandedRepoInfo_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -2913,6 +2943,90 @@ func (ec *executionContext) fieldContext_Query_GlobalSearch(ctx context.Context,
return fc, nil
}
func (ec *executionContext) _Query_DerivedImageList(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_DerivedImageList(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().DerivedImageList(rctx, fc.Args["image"].(string))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.([]*ImageSummary)
fc.Result = res
return ec.marshalOImageSummary2ᚕᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageSummaryᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Query_DerivedImageList(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Query",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "RepoName":
return ec.fieldContext_ImageSummary_RepoName(ctx, field)
case "Tag":
return ec.fieldContext_ImageSummary_Tag(ctx, field)
case "Digest":
return ec.fieldContext_ImageSummary_Digest(ctx, field)
case "ConfigDigest":
return ec.fieldContext_ImageSummary_ConfigDigest(ctx, field)
case "LastUpdated":
return ec.fieldContext_ImageSummary_LastUpdated(ctx, field)
case "IsSigned":
return ec.fieldContext_ImageSummary_IsSigned(ctx, field)
case "Size":
return ec.fieldContext_ImageSummary_Size(ctx, field)
case "Platform":
return ec.fieldContext_ImageSummary_Platform(ctx, field)
case "Vendor":
return ec.fieldContext_ImageSummary_Vendor(ctx, field)
case "Score":
return ec.fieldContext_ImageSummary_Score(ctx, field)
case "DownloadCount":
return ec.fieldContext_ImageSummary_DownloadCount(ctx, field)
case "Layers":
return ec.fieldContext_ImageSummary_Layers(ctx, field)
case "Description":
return ec.fieldContext_ImageSummary_Description(ctx, field)
case "Licenses":
return ec.fieldContext_ImageSummary_Licenses(ctx, field)
case "Labels":
return ec.fieldContext_ImageSummary_Labels(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Query_DerivedImageList_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return
}
return fc, nil
}
func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query___type(ctx, field)
if err != nil {
@ -5874,6 +5988,26 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc)
}
out.Concurrently(i, func() graphql.Marshaler {
return rrm(innerCtx)
})
case "DerivedImageList":
field := field
innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_DerivedImageList(ctx, field)
return res
}
rrm := func(ctx context.Context) graphql.Marshaler {
return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc)
}
out.Concurrently(i, func() graphql.Marshaler {
return rrm(innerCtx)
})

View file

@ -88,4 +88,5 @@ type Query {
ImageList(repo: String!): [ImageSummary!]
ExpandedRepoInfo(repo: String!): RepoInfo!
GlobalSearch(query: String!): GlobalSearchResult!
DerivedImageList(image: String!): [ImageSummary!]
}

View file

@ -6,6 +6,7 @@ package search
import (
"context"
"fmt"
"strconv"
"strings"
godigest "github.com/opencontainers/go-digest"
@ -470,6 +471,77 @@ func (r *queryResolver) GlobalSearch(ctx context.Context, query string) (*gql_ge
}, nil
}
// DependencyListForImage is the resolver for the DependencyListForImage field.
func (r *queryResolver) DerivedImageList(ctx context.Context, image string) ([]*gql_generated.ImageSummary, error) {
layoutUtils := common.NewBaseOciLayoutUtils(r.storeController, r.log)
imageList := make([]*gql_generated.ImageSummary, 0)
repoList, err := layoutUtils.GetRepositories()
if err != nil {
r.log.Error().Err(err).Msg("unable to get repositories list")
return nil, err
}
if len(repoList) == 0 {
r.log.Info().Msg("no repositories found")
return imageList, nil
}
imageDir, imageTag := common.GetImageDirAndTag(image)
imageManifest, err := layoutUtils.GetImageManifest(imageDir, imageTag)
if err != nil {
r.log.Info().Str("image", image).Msg("image not found")
return imageList, err
}
imageLayers := imageManifest.Layers
for _, repo := range repoList {
repoInfo, err := r.ExpandedRepoInfo(ctx, repo)
if err != nil {
r.log.Error().Err(err).Msg("unable to get image list")
return nil, err
}
manifests := repoInfo.Images
// verify every image
for _, manifest := range manifests {
layers := manifest.Layers
sameLayer := 0
imageSize := 0
for _, l := range imageLayers {
for _, k := range layers {
if *k.Digest == l.Digest.Encoded() {
sameLayer++
layerSize, _ := strconv.Atoi(*k.Size)
imageSize += layerSize
}
}
}
// if all layers are the same
if sameLayer == len(imageLayers) {
// add to returned list
name := repo
manifest.RepoName = &name
size := strconv.Itoa(imageSize)
manifest.Size = &size
imageList = append(imageList, manifest)
}
}
}
return imageList, nil
}
// Query returns gql_generated.QueryResolver implementation.
func (r *Resolver) Query() gql_generated.QueryResolver { return &queryResolver{r} }

View file

@ -24,6 +24,15 @@ type OciLayoutUtilsMock struct {
GetExpandedRepoInfoFn func(name string) (common.RepoInfo, error)
GetImageConfigInfoFn func(repo string, manifestDigest godigest.Digest) (ispec.Image, error)
CheckManifestSignatureFn func(name string, digest godigest.Digest) bool
GetRepositoriesFn func() ([]string, error)
}
func (olum OciLayoutUtilsMock) GetRepositories() ([]string, error) {
if olum.GetImageManifestsFn != nil {
return olum.GetRepositoriesFn()
}
return []string{}, nil
}
func (olum OciLayoutUtilsMock) GetImageManifests(image string) ([]ispec.Descriptor, error) {