mirror of
https://github.com/project-zot/zot.git
synced 2026-06-16 04:17:55 +08:00
list all images that have all layers of the base image included (2) (#813)
* list all images that are base images for the given image + zli command Signed-off-by: Lisca Ana-Roberta <ana.kagome@yahoo.com> * Fix a failing test The test expected the image size to be the size of the layer, not the manifest+config+layer Signed-off-by: Andrei Aaron <andaaron@cisco.com> Signed-off-by: Lisca Ana-Roberta <ana.kagome@yahoo.com> Signed-off-by: Andrei Aaron <andaaron@cisco.com> Co-authored-by: Lisca Ana-Roberta <ana.kagome@yahoo.com>
This commit is contained in:
@@ -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["baseImage"] = imageCmd.Flags().StringP("base-images", "b", "",
|
||||
"List images that are base for the 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`)
|
||||
|
||||
+110
-1
@@ -250,6 +250,99 @@ func TestSearchImageCmd(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBaseImageList(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 base images list working", func() {
|
||||
t.Logf("%s", ctlr.Config.Storage.RootDirectory)
|
||||
args := []string{"imagetest", "--base-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 base 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", "--base-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 base images list cannot print", func() {
|
||||
t.Logf("%s", ctlr.Config.Storage.RootDirectory)
|
||||
args := []string{"imagetest", "--base-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"}
|
||||
@@ -308,7 +401,7 @@ func TestListRepos(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Test unable to get config value", t, func() {
|
||||
args := []string{"config-test-inexistent"}
|
||||
args := []string{"config-test-nonexistent"}
|
||||
configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`)
|
||||
defer os.Remove(configPath)
|
||||
cmd := NewRepoCommand(new(mockService))
|
||||
@@ -1172,6 +1265,22 @@ func (service mockService) getRepos(ctx context.Context, config searchConfig, us
|
||||
channel <- stringResult{"", nil}
|
||||
}
|
||||
|
||||
func (service mockService) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
derivedImage string,
|
||||
) (*imageListStructForBaseImagesGQL, error) {
|
||||
imageListGQLResponse := &imageListStructForBaseImagesGQL{}
|
||||
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) {
|
||||
|
||||
@@ -42,6 +42,7 @@ func getImageSearchersGQL() []searcher {
|
||||
new(allImagesSearcherGQL),
|
||||
new(imageByNameSearcherGQL),
|
||||
new(imagesByDigestSearcherGQL),
|
||||
new(baseImageListSearcherGQL),
|
||||
}
|
||||
|
||||
return searchers
|
||||
@@ -219,6 +220,31 @@ func (search imagesByDigestSearcher) search(config searchConfig) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
type baseImageListSearcherGQL struct{}
|
||||
|
||||
func (search baseImageListSearcherGQL) search(config searchConfig) (bool, error) {
|
||||
if !canSearch(config.params, newSet("baseImage")) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
username, password := getUsernameAndPassword(*config.user)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
defer cancel()
|
||||
|
||||
imageList, err := config.searchService.getBaseImageListGQL(ctx, config, username,
|
||||
password, *config.params["baseImage"])
|
||||
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) {
|
||||
|
||||
@@ -34,6 +34,8 @@ type SearchService interface {
|
||||
cveID string) (*imagesForCve, error)
|
||||
getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
|
||||
cveID string) (*fixedTags, error)
|
||||
getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
baseImage string) (*imageListStructForBaseImagesGQL, 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) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
|
||||
baseImage string,
|
||||
) (*imageListStructForBaseImagesGQL, error) {
|
||||
query := fmt.Sprintf(`
|
||||
{
|
||||
BaseImageList(image:"%s"){
|
||||
RepoName,
|
||||
Tag,
|
||||
Digest,
|
||||
ConfigDigest,
|
||||
LastUpdated,
|
||||
IsSigned,
|
||||
Size
|
||||
}
|
||||
}`, baseImage)
|
||||
|
||||
result := &imageListStructForBaseImagesGQL{}
|
||||
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 imageListStructForBaseImagesGQL struct {
|
||||
Errors []errorGraphQL `json:"errors"`
|
||||
Data struct {
|
||||
ImageList []imageStruct `json:"BaseImageList"` // nolint:tagliatelle
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type imagesForDigest struct {
|
||||
Errors []errorGraphQL `json:"errors"`
|
||||
Data struct {
|
||||
|
||||
Reference in New Issue
Block a user