mirror of
https://github.com/project-zot/zot.git
synced 2026-06-16 04:17:55 +08:00
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:
committed by
Ramkumar Chinchani
parent
97628e69c9
commit
519ea75d9a
@@ -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`)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user