Implement a way to search for an image by manifest, config or layer digest

```
Usage:
  zot images [config-name] [flags]

Flags:
  -d, --digest string   List images containing a specific manifest, config, or layer digest
[...]
```
This commit is contained in:
Andrei Aaron
2021-05-26 20:22:31 +03:00
committed by Ramkumar Chinchani
parent 97628e69c9
commit 519ea75d9a
14 changed files with 1043 additions and 175 deletions
+2
View File
@@ -109,6 +109,8 @@ func parseBooleanConfig(configPath, configName, configParam string) (bool, error
func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*string,
servURL, user, outputFormat *string) {
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")
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`)
+50
View File
@@ -22,6 +22,7 @@ import (
zotErrors "github.com/anuvu/zot/errors"
"github.com/anuvu/zot/pkg/api"
"github.com/anuvu/zot/pkg/compliance/v1_0_0"
"github.com/anuvu/zot/pkg/extensions"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
@@ -285,6 +286,9 @@ func TestServerResponse(t *testing.T) {
url := "http://127.0.0.1:8080"
config := api.NewConfig()
config.HTTP.Port = port
config.Extensions = &extensions.ExtensionConfig{
Search: &extensions.SearchConfig{},
}
c := api.NewController(config)
dir, err := ioutil.TempDir("", "oci-repo-test")
if err != nil {
@@ -372,6 +376,47 @@ func TestServerResponse(t *testing.T) {
})
})
Convey("Test image by digest", func() {
args := []string{"imagetest", "--digest", "a0ca253b"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := NewImageCommand(new(searchService))
buff := bytes.NewBufferString("")
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)
// Actual cli output should be something similar to (order of images may differ):
// IMAGE NAME TAG DIGEST SIZE
// repo7 test:2.0 a0ca253b 15B
// repo7 test:1.0 a0ca253b 15B
So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE")
So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B")
Convey("with shorthand", func() {
args := []string{"imagetest", "-d", "a0ca253b"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cmd := NewImageCommand(new(searchService))
buff := bytes.NewBufferString("")
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 a0ca253b 15B")
So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B")
})
})
Convey("Test image by name invalid name", func() {
args := []string{"imagetest", "--name", "repo777"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url))
@@ -528,6 +573,11 @@ func (service mockService) getImagesByCveID(ctx context.Context, config searchCo
service.getImageByName(ctx, config, username, password, "anImage", c, wg)
}
func (service mockService) getImagesByDigest(ctx context.Context, config searchConfig, username,
password, digest string, c chan stringResult, wg *sync.WaitGroup) {
service.getImageByName(ctx, config, username, password, "anImage", c, wg)
}
func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username,
password, imageName, cveID string, c chan stringResult, wg *sync.WaitGroup) {
service.getImageByName(ctx, config, username, password, imageName, c, wg)
+33
View File
@@ -19,6 +19,7 @@ func getImageSearchers() []searcher {
searchers := []searcher{
new(allImagesSearcher),
new(imageByNameSearcher),
new(imagesByDigestSearcher),
}
return searchers
@@ -125,6 +126,38 @@ func (search imageByNameSearcher) search(config searchConfig) (bool, error) {
}
}
type imagesByDigestSearcher struct{}
func (search imagesByDigestSearcher) search(config searchConfig) (bool, error) {
if !canSearch(config.params, newSet("digest")) {
return false, nil
}
username, password := getUsernameAndPassword(*config.user)
imageErr := make(chan stringResult)
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go config.searchService.getImagesByDigest(ctx, config, username, password,
*config.params["digest"], imageErr, &wg)
wg.Add(1)
var errCh chan error = make(chan error, 1)
go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh)
wg.Wait()
select {
case err := <-errCh:
return true, err
default:
return true, nil
}
}
type cveByImageSearcher struct{}
func (search cveByImageSearcher) search(config searchConfig) (bool, error) {
+87 -42
View File
@@ -29,6 +29,8 @@ type SearchService interface {
channel chan stringResult, wg *sync.WaitGroup)
getImagesByCveID(ctx context.Context, config searchConfig, username, password, cveID string,
channel chan stringResult, wg *sync.WaitGroup)
getImagesByDigest(ctx context.Context, config searchConfig, username, password, digest string,
channel chan stringResult, wg *sync.WaitGroup)
getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cveID string,
channel chan stringResult, wg *sync.WaitGroup)
getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cveID string,
@@ -147,7 +149,8 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search
cveID)
result := &imagesForCve{}
endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query")
err := service.makeGraphQLQuery(config, username, password, query, result)
if err != nil {
if isContextDone(ctx) {
return
@@ -157,17 +160,7 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search
return
}
err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result)
if err != nil {
if isContextDone(ctx) {
return
}
c <- stringResult{"", err}
return
}
if result.Errors != nil {
if result.Errors != nil || err != nil {
var errBuilder strings.Builder
for _, err := range result.Errors {
@@ -200,6 +193,61 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search
localWg.Wait()
}
func (service searchService) getImagesByDigest(ctx context.Context, config searchConfig, username,
password string, digest string, c chan stringResult, wg *sync.WaitGroup) {
defer wg.Done()
defer close(c)
query := fmt.Sprintf(`{ImageListForDigest(id: "%s") {`+`
Name Tags }
}`,
digest)
result := &imagesForDigest{}
err := service.makeGraphQLQuery(config, username, password, query, result)
if err != nil {
if isContextDone(ctx) {
return
}
c <- stringResult{"", err}
return
}
if result.Errors != nil {
var errBuilder strings.Builder
for _, err := range result.Errors {
fmt.Fprintln(&errBuilder, err.Message)
}
if isContextDone(ctx) {
return
}
c <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113
return
}
var localWg sync.WaitGroup
p := newSmoothRateLimiter(ctx, &localWg, c)
localWg.Add(1)
go p.startRateLimiter()
for _, image := range result.Data.ImageListForDigest {
for _, tag := range image.Tags {
localWg.Add(1)
go addManifestCallToPool(ctx, config, p, username, password, image.Name, tag, c, &localWg)
}
}
localWg.Wait()
}
func (service searchService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username,
password, imageName, cveID string, c chan stringResult, wg *sync.WaitGroup) {
defer wg.Done()
@@ -211,17 +259,8 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config
cveID)
result := &imagesForCve{}
endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query")
if err != nil {
if isContextDone(ctx) {
return
}
c <- stringResult{"", err}
err := service.makeGraphQLQuery(config, username, password, query, result)
return
}
err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result)
if err != nil {
if isContextDone(ctx) {
return
@@ -278,17 +317,8 @@ func (service searchService) getCveByImage(ctx context.Context, config searchCon
`PackageList {Name InstalledVersion FixedVersion}} } }`, imageName)
result := &cveResult{}
endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query")
if err != nil {
if isContextDone(ctx) {
return
}
c <- stringResult{"", err}
err := service.makeGraphQLQuery(config, username, password, query, result)
return
}
err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result)
if err != nil {
if isContextDone(ctx) {
return
@@ -372,17 +402,8 @@ func (service searchService) getFixedTagsForCVE(ctx context.Context, config sear
cveID, imageName)
result := &fixedTags{}
endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query")
if err != nil {
if isContextDone(ctx) {
return
}
c <- stringResult{"", err}
err := service.makeGraphQLQuery(config, username, password, query, result)
return
}
err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result)
if err != nil {
if isContextDone(ctx) {
return
@@ -423,6 +444,23 @@ func (service searchService) getFixedTagsForCVE(ctx context.Context, config sear
localWg.Wait()
}
// Query using JQL, the query string is passed as a parameter
// errors are returned in the stringResult channel, the unmarshalled payload is in resultPtr.
func (service searchService) makeGraphQLQuery(config searchConfig, username, password, query string,
resultPtr interface{}) error {
endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query")
if err != nil {
return err
}
err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, resultPtr)
if err != nil {
return err
}
return nil
}
func addManifestCallToPool(ctx context.Context, config searchConfig, p *requestsPool, username, password, imageName,
tagName string, c chan stringResult, wg *sync.WaitGroup) {
defer wg.Done()
@@ -555,6 +593,13 @@ type imagesForCve struct {
} `json:"data"`
}
type imagesForDigest struct {
Errors []errorGraphQL `json:"errors"`
Data struct {
ImageListForDigest []tagListResp `json:"ImageListForDigest"`
} `json:"data"`
}
type tagListResp struct {
Name string `json:"name"`
Tags []string `json:"tags"`