Signed-off-by: Lisca Ana-Roberta <ana.kagome@yahoo.com> (#713)

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-09-23 19:23:31 +03:00
committed by GitHub
parent 944ae66844
commit 0f7b174fc0
8 changed files with 815 additions and 44 deletions
+430 -44
View File
@@ -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)
@@ -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)
})
+1
View File
@@ -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!]
}
+68
View File
@@ -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)