From 0f7b174fc06df46107338b1d7bcbe9cef502c86a Mon Sep 17 00:00:00 2001 From: Lisca Ana-Roberta <55219463+aokirisaki@users.noreply.github.com> Date: Fri, 23 Sep 2022 19:23:31 +0300 Subject: [PATCH] Signed-off-by: Lisca Ana-Roberta (#713) list all images that have are base images for the given image + zli command Signed-off-by: Lisca Ana-Roberta --- pkg/cli/image_cmd.go | 2 + pkg/cli/image_cmd_test.go | 111 ++++ pkg/cli/searcher.go | 26 + pkg/cli/service.go | 35 ++ pkg/extensions/search/common/common_test.go | 474 ++++++++++++++++-- .../search/gql_generated/generated.go | 142 ++++++ pkg/extensions/search/schema.graphql | 1 + pkg/extensions/search/schema.resolvers.go | 68 +++ 8 files changed, 815 insertions(+), 44 deletions(-) diff --git a/pkg/cli/image_cmd.go b/pkg/cli/image_cmd.go index 73c15e56..82d0e97c 100644 --- a/pkg/cli/image_cmd.go +++ b/pkg/cli/image_cmd.go @@ -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") searchImageParams["baseImage"] = imageCmd.Flags().StringP("base-images", "b", "", "List images that are base for the given image") diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index 4ef36a49..96328f7c 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -250,6 +250,101 @@ func TestSearchImageCmd(t *testing.T) { }) } +// nolint: dupl +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 492B") + }) + + 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) + }) + }) +} + +// nolint: dupl func TestBaseImageList(t *testing.T) { Convey("Test from real server", t, func() { port := test.GetFreePort() @@ -1265,6 +1360,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) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string, derivedImage string, ) (*imageListStructForBaseImagesGQL, error) { diff --git a/pkg/cli/searcher.go b/pkg/cli/searcher.go index 6eed4aea..e07df57d 100644 --- a/pkg/cli/searcher.go +++ b/pkg/cli/searcher.go @@ -42,6 +42,7 @@ func getImageSearchersGQL() []searcher { new(allImagesSearcherGQL), new(imageByNameSearcherGQL), new(imagesByDigestSearcherGQL), + new(derivedImageListSearcherGQL), new(baseImageListSearcherGQL), } @@ -220,6 +221,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 baseImageListSearcherGQL struct{} func (search baseImageListSearcherGQL) search(config searchConfig) (bool, error) { diff --git a/pkg/cli/service.go b/pkg/cli/service.go index 3367c7cb..cde7bb6b 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -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) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string, baseImage string) (*imageListStructForBaseImagesGQL, error) @@ -61,6 +63,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) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string, baseImage string, ) (*imageListStructForBaseImagesGQL, error) { @@ -831,6 +859,13 @@ type imageListStructForDigestGQL struct { } `json:"data"` } +type imageListStructForDerivedImagesGQL struct { + Errors []errorGraphQL `json:"errors"` + Data struct { + ImageList []imageStruct `json:"DerivedImageList"` // nolint:tagliatelle + } `json:"data"` +} + type imageListStructForBaseImagesGQL struct { Errors []errorGraphQL `json:"errors"` Data struct { diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index db3837ca..590c8eaa 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -1006,6 +1006,391 @@ func TestUtilsMethod(t *testing.T) { }) } +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) + }) +} + +// nolint:dupl +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 TestGetImageManifest(t *testing.T) { Convey("Test nonexistent image", t, func() { mockImageStore := mocks.MockedImageStore{} @@ -1394,17 +1779,17 @@ func TestBaseImageList(t *testing.T) { So(err, ShouldBeNil) query := ` - { - BaseImageList(image:"test-repo"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size - } - }` + { + BaseImageList(image:"test-repo"){ + RepoName, + Tag, + Digest, + ConfigDigest, + LastUpdated, + IsSigned, + Size + } + }` resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) So(resp, ShouldNotBeNil) @@ -1420,17 +1805,17 @@ func TestBaseImageList(t *testing.T) { Convey("Nonexistent repository", t, func() { query := ` - { - BaseImageList(image:"nonexistent-image"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size - } - }` + { + BaseImageList(image:"nonexistent-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) @@ -1442,17 +1827,17 @@ func TestBaseImageList(t *testing.T) { So(err, ShouldBeNil) query := ` - { - BaseImageList(image:"fail-image"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size - } - }` + { + BaseImageList(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) @@ -1460,6 +1845,7 @@ func TestBaseImageList(t *testing.T) { }) } +// nolint:dupl func TestBaseImageListNoRepos(t *testing.T) { Convey("No repositories found", t, func() { port := GetFreePort() @@ -1502,17 +1888,17 @@ func TestBaseImageListNoRepos(t *testing.T) { }() query := ` - { - BaseImageList(image:"test-image"){ - RepoName, - Tag, - Digest, - ConfigDigest, - LastUpdated, - IsSigned, - Size - } - }` + { + BaseImageList(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\":{\"BaseImageList\":[]}}"), ShouldBeTrue) diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 2ea67b3e..4363bcfb 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -117,6 +117,7 @@ type ComplexityRoot struct { Query struct { BaseImageList func(childComplexity int, image string) int 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 @@ -154,6 +155,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) BaseImageList(ctx context.Context, image string) ([]*ImageSummary, error) } @@ -504,6 +506,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 @@ -838,6 +852,7 @@ type Query { ImageList(repo: String!): [ImageSummary!] ExpandedRepoInfo(repo: String!): RepoInfo! GlobalSearch(query: String!): GlobalSearchResult! + DerivedImageList(image: String!): [ImageSummary!] BaseImageList(image: String!): [ImageSummary!] }`, BuiltIn: false}, } @@ -877,6 +892,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{}{} @@ -3582,6 +3612,98 @@ 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) + case "Title": + return ec.fieldContext_ImageSummary_Title(ctx, field) + case "Source": + return ec.fieldContext_ImageSummary_Source(ctx, field) + case "Documentation": + return ec.fieldContext_ImageSummary_Documentation(ctx, field) + case "History": + return ec.fieldContext_ImageSummary_History(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_BaseImageList(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_BaseImageList(ctx, field) if err != nil { @@ -6737,6 +6859,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) }) diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index e909607c..c3e8bb64 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -117,5 +117,6 @@ type Query { ImageList(repo: String!): [ImageSummary!] ExpandedRepoInfo(repo: String!): RepoInfo! GlobalSearch(query: String!): GlobalSearchResult! + DerivedImageList(image: String!): [ImageSummary!] BaseImageList(image: String!): [ImageSummary!] } \ No newline at end of file diff --git a/pkg/extensions/search/schema.resolvers.go b/pkg/extensions/search/schema.resolvers.go index 07a961e1..e37250d8 100644 --- a/pkg/extensions/search/schema.resolvers.go +++ b/pkg/extensions/search/schema.resolvers.go @@ -501,6 +501,74 @@ 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 + } + + imageSummaries := repoInfo.Images + + // verify every image + for _, imageSummary := range imageSummaries { + if imageTag == *imageSummary.Tag && imageDir == repo { + continue + } + + layers := imageSummary.Layers + + sameLayer := 0 + + for _, l := range imageLayers { + for _, k := range layers { + if *k.Digest == l.Digest.Encoded() { + sameLayer++ + } + } + } + + // if all layers are the same + if sameLayer == len(imageLayers) { + // add to returned list + imageList = append(imageList, imageSummary) + } + } + } + + return imageList, nil +} + // BaseImageList is the resolver for the BaseImageList field. func (r *queryResolver) BaseImageList(ctx context.Context, image string) ([]*gql_generated.ImageSummary, error) { layoutUtils := common.NewBaseOciLayoutUtils(r.storeController, r.log)