0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

refactor(cli): added equivalent subcommands for each flag combination under every command (#1674)

- image command is now deprecated in favor of 'images'
- cve command is now deprecated in favor of 'cves'

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
LaurentiuNiculae 2023-08-30 20:12:24 +03:00 committed by GitHub
parent 2bd479edd7
commit 112fbec5b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 3857 additions and 315 deletions

View file

@ -3,7 +3,6 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@ -19,6 +18,7 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"gopkg.in/resty.v1"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/test"
)
@ -32,7 +32,8 @@ func makeHTTPGetRequest(url string, resultPtr interface{}, client *resty.Client)
if resp.StatusCode() != http.StatusOK {
log.Printf("unable to make GET request on %s, response status code: %d", url, resp.StatusCode())
return errors.New(string(resp.Body())) //nolint: goerr113
return fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusOK,
resp.StatusCode(), string(resp.Body()))
}
err = json.Unmarshal(resp.Body(), resultPtr)
@ -52,7 +53,8 @@ func makeHTTPDeleteRequest(url string, client *resty.Client) error {
if resp.StatusCode() != http.StatusAccepted {
log.Printf("unable to make DELETE request on %s, response status code: %d", url, resp.StatusCode())
return errors.New(string(resp.Body())) //nolint: goerr113
return fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted,
resp.StatusCode(), string(resp.Body()))
}
return nil
@ -336,7 +338,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo
// request specific check
statusCode = resp.StatusCode()
if statusCode != http.StatusAccepted {
return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113
return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted,
resp.StatusCode(), string(resp.Body())) //nolint: goerr113
}
loc := test.Location(url, resp)
@ -374,7 +377,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo
// request specific check
statusCode = resp.StatusCode()
if statusCode != http.StatusCreated {
return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113
return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated,
resp.StatusCode(), string(resp.Body()))
}
// upload image config blob
@ -388,7 +392,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo
// request specific check
statusCode = resp.StatusCode()
if statusCode != http.StatusAccepted {
return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113
return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted,
resp.StatusCode(), string(resp.Body()))
}
loc = test.Location(url, resp)
@ -408,7 +413,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo
// request specific check
statusCode = resp.StatusCode()
if statusCode != http.StatusCreated {
return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113
return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated,
resp.StatusCode(), string(resp.Body()))
}
// create a manifest
@ -451,7 +457,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo
// request specific check
statusCode = resp.StatusCode()
if statusCode != http.StatusCreated {
return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113
return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated,
resp.StatusCode(), string(resp.Body()))
}
manifestHash[repo] = manifestTag

View file

@ -74,7 +74,7 @@ var (
ErrInvalidArgs = errors.New("cli: Invalid Arguments")
ErrInvalidFlagsCombination = errors.New("cli: Invalid combination of flags")
ErrInvalidURL = errors.New("cli: invalid URL format")
ErrExtensionNotEnabled = errors.New("cli: functionality is not built in current version")
ErrExtensionNotEnabled = errors.New("cli: functionality is not built/configured in the current server")
ErrUnauthorizedAccess = errors.New("auth: unauthorized access. check credentials")
ErrCannotResetConfigKey = errors.New("cli: cannot reset given config key")
ErrConfigNotFound = errors.New("cli: config with the given name does not exist")
@ -115,7 +115,7 @@ var (
ErrEmptyRepoName = errors.New("metadb: repo name can't be empty string")
ErrEmptyTag = errors.New("metadb: tag can't be empty string")
ErrEmptyDigest = errors.New("metadb: digest can't be empty string")
ErrInvalidRepoRefFormat = errors.New("invalid image reference format")
ErrInvalidRepoRefFormat = errors.New("invalid image reference format [repo:tag] or [repo@digest]")
ErrLimitIsNegative = errors.New("pageturner: limit has negative value")
ErrOffsetIsNegative = errors.New("pageturner: offset has negative value")
ErrSortCriteriaNotSupported = errors.New("pageturner: the sort criteria is not supported")
@ -151,4 +151,8 @@ var (
ErrInvalidPublicKeyContent = errors.New("signatures: invalid public key content")
ErrInvalidStateCookie = errors.New("auth: state cookie not present or differs from original state")
ErrSyncNoURLsLeft = errors.New("sync: no valid registry urls left after filtering local ones")
ErrInvalidCLIParameter = errors.New("cli: invalid parameter")
ErrGQLEndpointNotFound = errors.New("cli: the server doesn't have a gql endpoint")
ErrGQLQueryNotSupported = errors.New("cli: query is not supported or has different arguments")
ErrBadHTTPStatusCode = errors.New("cli: the response doesn't contain the expected status code")
)

View file

@ -5,6 +5,8 @@ const (
ExtCatalogPrefix = "/_catalog"
ExtOciDiscoverPrefix = "/_oci/ext/discover"
BaseExtension = "_zot"
// zot specific extensions.
BasePrefix = "/_zot"
ExtPrefix = BasePrefix + "/ext"

View file

@ -8491,7 +8491,7 @@ func TestDistSpecExtensions(t *testing.T) {
t.Log(extensionList.Extensions)
So(len(extensionList.Extensions), ShouldEqual, 1)
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 5)
So(extensionList.Extensions[0].Name, ShouldEqual, "_zot")
So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension)
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
// Verify the endpoints below are enabled by search
@ -8539,7 +8539,7 @@ func TestDistSpecExtensions(t *testing.T) {
t.Log(extensionList.Extensions)
So(len(extensionList.Extensions), ShouldEqual, 1)
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 2)
So(extensionList.Extensions[0].Name, ShouldEqual, "_zot")
So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension)
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
// Verify the endpoints below are enabled by search

View file

@ -717,7 +717,7 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht
rh.c.Log.Error().Err(err).Msg("unexpected error: performing cleanup")
if err = imgStore.DeleteImageManifest(name, reference, false); err != nil {
// deletion of image manifest is important, but not critical for image repo consistancy
// deletion of image manifest is important, but not critical for image repo consistency
// in the worst scenario a partial manifest file written to disk will not affect the repo because
// the new manifest was not added to "index.json" file (it is possible that GC will take care of it)
rh.c.Log.Error().Err(err).Str("repository", name).Str("reference", reference).

View file

@ -8,7 +8,9 @@ import "github.com/spf13/cobra"
func enableCli(rootCmd *cobra.Command) {
rootCmd.AddCommand(NewConfigCommand())
rootCmd.AddCommand(NewImageCommand(NewSearchService()))
rootCmd.AddCommand(NewImagesCommand(NewSearchService()))
rootCmd.AddCommand(NewCveCommand(NewSearchService()))
rootCmd.AddCommand(NewCVESCommand(NewSearchService()))
rootCmd.AddCommand(NewRepoCommand(NewSearchService()))
rootCmd.AddCommand(NewSearchCommand(NewSearchService()))
}

View file

@ -7,7 +7,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@ -21,7 +20,7 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sigstore/cosign/v2/pkg/oci/remote"
zotErrors "zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/common"
)
@ -124,19 +123,20 @@ func doHTTPRequest(req *http.Request, verifyTLS bool, debug bool,
if debug {
fmt.Fprintln(configWriter, "[debug] ", req.Method, req.URL, "[status] ",
resp.StatusCode, " ", "[respoonse header] ", resp.Header)
resp.StatusCode, " ", "[response header] ", resp.Header)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusUnauthorized {
return nil, zotErrors.ErrUnauthorizedAccess
return nil, zerr.ErrUnauthorizedAccess
}
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, errors.New(string(bodyBytes)) //nolint: goerr113
return nil, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusOK,
resp.StatusCode, string(bodyBytes))
}
if resultsPtr == nil {

13
pkg/cli/cmdflags/flags.go Normal file
View file

@ -0,0 +1,13 @@
package cmdflags
const (
URLFlag = "url"
ConfigFlag = "config"
UserFlag = "user"
OutputFormatFlag = "format"
FixedFlag = "fixed"
VerboseFlag = "verbose"
VersionFlag = "version"
DebugFlag = "debug"
SearchedCVEID = "cve-id"
)

View file

@ -40,7 +40,7 @@ func NewConfigCommand() *cobra.Command {
panic(err)
}
configPath := path.Join(home + "/.zot")
configPath := path.Join(home, "/.zot")
switch len(args) {
case noArgs:
if isListing { // zot config -l
@ -115,7 +115,7 @@ func NewConfigAddCommand() *cobra.Command {
panic(err)
}
configPath := path.Join(home + "/.zot")
configPath := path.Join(home, "/.zot")
// zot config add <config-name> <url>
err = addConfig(configPath, args[0], args[1])
if err != nil {
@ -145,7 +145,7 @@ func NewConfigRemoveCommand() *cobra.Command {
panic(err)
}
configPath := path.Join(home + "/.zot")
configPath := path.Join(home, "/.zot")
// zot config add <config-name> <url>
err = removeConfig(configPath, args[0])
if err != nil {

View file

@ -13,7 +13,7 @@ import (
. "github.com/smartystreets/goconvey/convey"
zotErrors "zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
)
func TestConfigCmdBasics(t *testing.T) {
@ -146,7 +146,7 @@ func TestConfigCmdMain(t *testing.T) {
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldEqual, zotErrors.ErrCliBadConfig)
So(err, ShouldEqual, zerr.ErrCliBadConfig)
})
Convey("Test add config with invalid URL", t, func() {
@ -160,7 +160,7 @@ func TestConfigCmdMain(t *testing.T) {
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
So(err, ShouldEqual, zerr.ErrInvalidURL)
})
Convey("Test remove config entry successfully", t, func() {

View file

@ -13,7 +13,7 @@ import (
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
zotErrors "zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
)
const (
@ -29,15 +29,15 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
cveCmd := &cobra.Command{
Use: "cve [config-name]",
Short: "Lookup CVEs in images hosted on the zot registry",
Long: `List CVEs (Common Vulnerabilities and Exposures) of images hosted on the zot registry`,
Short: "DEPRECATED (see cves)",
Long: `DEPRECATED (see cves)! List CVEs (Common Vulnerabilities and Exposures) of images hosted on the zot registry`,
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
configPath := path.Join(home + "/.zot")
configPath := path.Join(home, "/.zot")
if servURL == "" {
if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
@ -48,12 +48,12 @@ func NewCveCommand(searchService SearchService) *cobra.Command {
}
if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
servURL = urlFromConfig
} else {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
}
@ -177,7 +177,7 @@ func searchCve(searchConfig searchConfig) error {
return err
}
if strings.Contains(err.Error(), zotErrors.ErrCVEDBNotFound.Error()) {
if strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) {
// searches matches search config but CVE DB is not ready server side
// wait and retry a few more times
fmt.Fprintln(searchConfig.resultWriter,
@ -192,5 +192,5 @@ func searchCve(searchConfig searchConfig) error {
}
}
return zotErrors.ErrInvalidFlagsCombination
return zerr.ErrInvalidFlagsCombination
}

View file

@ -26,9 +26,10 @@ import (
. "github.com/smartystreets/goconvey/convey"
"github.com/spf13/cobra"
zotErrors "zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/cli/cmdflags"
zcommon "zotregistry.io/zot/pkg/common"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
@ -82,7 +83,7 @@ func TestSearchCVECmd(t *testing.T) {
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrNoURLProvided)
So(err, ShouldEqual, zerr.ErrNoURLProvided)
})
Convey("Test CVE no params", t, func() {
@ -95,7 +96,7 @@ func TestSearchCVECmd(t *testing.T) {
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldEqual, zotErrors.ErrInvalidFlagsCombination)
So(err, ShouldEqual, zerr.ErrInvalidFlagsCombination)
})
Convey("Test CVE invalid url", t, func() {
@ -109,7 +110,7 @@ func TestSearchCVECmd(t *testing.T) {
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
So(err, ShouldEqual, zerr.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format")
})
@ -285,7 +286,7 @@ func TestSearchCVECmd(t *testing.T) {
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
So(strings.TrimSpace(str), ShouldEqual, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE anImage tag os/arch 6e2f80bf false 123kB") //nolint:lll
So(strings.TrimSpace(str), ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE anImage tag os/arch 6e2f80bf false 123kB") //nolint:lll
So(err, ShouldBeNil)
})
@ -355,7 +356,7 @@ func TestSearchCVECmd(t *testing.T) {
cveCmd.SetArgs(args)
err := cveCmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
So(err, ShouldEqual, zerr.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format")
})
@ -1058,6 +1059,330 @@ func TestServerCVEResponse(t *testing.T) {
})
}
func TestCVECommandGQL(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands without gql", t, func() {
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, baseURL))
defer os.Remove(configPath)
Convey("cveid", func() {
args := []string{"cveid", "CVE-1942"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
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 6e2f80bf false 123kB")
})
Convey("cveid db download wait", func() {
count := 0
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"cveid", "CVE-12345"}
defer os.Remove(configPath)
cmd := NewCVESCommand(mockService{
getTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*zcommon.ImagesForCve, error,
) {
if count == 0 {
count++
fmt.Println("Count:", count)
return &zcommon.ImagesForCve{}, zerr.ErrCVEDBNotFound
}
return &zcommon.ImagesForCve{}, zerr.ErrInjected
},
})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready")
})
Convey("fixed", func() {
args := []string{"fixed", "image-name", "CVE-123"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
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 6e2f80bf false 123kB")
})
Convey("fixed db download wait", func() {
count := 0
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"fixed", "repo", "CVE-2222"}
defer os.Remove(configPath)
cmd := NewCVESCommand(mockService{
getFixedTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*zcommon.ImageListWithCVEFixedResponse, error,
) {
if count == 0 {
count++
fmt.Println("Count:", count)
return &zcommon.ImageListWithCVEFixedResponse{}, zerr.ErrCVEDBNotFound
}
return &zcommon.ImageListWithCVEFixedResponse{}, zerr.ErrInjected
},
})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready")
})
Convey("image", func() {
args := []string{"image", "repo:tag"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
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, "dummyCVEID HIGH Title of that CVE")
})
Convey("image db download wait", func() {
count := 0
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"image", "repo:vuln"}
defer os.Remove(configPath)
cmd := NewCVESCommand(mockService{
getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, searchedCVE string) (*cveResult, error,
) {
if count == 0 {
count++
fmt.Println("Count:", count)
return &cveResult{}, zerr.ErrCVEDBNotFound
}
return &cveResult{}, zerr.ErrInjected
},
})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready")
})
})
}
func TestCVECommandREST(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands without gql", t, func() {
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, baseURL))
defer os.Remove(configPath)
Convey("cveid", func() {
args := []string{"cveid", "CVE-1942"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("cveid error", func() {
// too many args
args := []string{"too", "many", "args"}
cmd := NewImagesByCVEIDCommand(mockService{})
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// bad args
args = []string{"not-a-cve-id"}
cmd = NewImagesByCVEIDCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no URL
args = []string{"CVE-1942"}
cmd = NewImagesByCVEIDCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("fixed command", func() {
args := []string{"fixed", "image-name", "CVE-123"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("fixed command error", func() {
// too many args
args := []string{"too", "many", "args", "args"}
cmd := NewFixedTagsCommand(mockService{})
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// bad args
args = []string{"repo-tag-instead-of-just-repo:fail-here", "CVE-123"}
cmd = NewFixedTagsCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no URL
args = []string{"CVE-1942"}
cmd = NewFixedTagsCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("image", func() {
args := []string{"image", "repo:tag"}
cmd := NewCVESCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("image command error", func() {
// too many args
args := []string{"too", "many", "args", "args"}
cmd := NewCveForImageCommand(mockService{})
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// bad args
args = []string{"repo-tag-instead-of-just-repo:fail-here", "CVE-123"}
cmd = NewCveForImageCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no URL
args = []string{"CVE-1942"}
cmd = NewCveForImageCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
})
}
func MockNewCveCommand(searchService SearchService) *cobra.Command {
searchCveParams := make(map[string]*string)
@ -1072,7 +1397,7 @@ func MockNewCveCommand(searchService SearchService) *cobra.Command {
panic(err)
}
configPath := path.Join(home + "/.zot")
configPath := path.Join(home, "/.zot")
if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
if err != nil {
@ -1082,12 +1407,12 @@ func MockNewCveCommand(searchService SearchService) *cobra.Command {
}
if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
servURL = urlFromConfig
} else {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
if len(args) > 0 {
@ -1157,7 +1482,7 @@ func MockSearchCve(searchConfig searchConfig) error {
}
}
return zotErrors.ErrInvalidFlagsCombination
return zerr.ErrInvalidFlagsCombination
}
func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo {
@ -1232,7 +1557,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo {
descriptor, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zotErrors.ErrTagMetaNotFound
return false, zerr.ErrTagMetaNotFound
}
manifestDigestStr = descriptor.Digest
@ -1252,7 +1577,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo {
err = json.Unmarshal(manifestData.ManifestBlob, &manifestContent)
if err != nil {
return false, zotErrors.ErrScanNotSupported
return false, zerr.ErrScanNotSupported
}
for _, imageLayer := range manifestContent.Layers {
@ -1262,7 +1587,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo {
return true, nil
default:
return false, zotErrors.ErrScanNotSupported
return false, zerr.ErrScanNotSupported
}
}
@ -1284,12 +1609,12 @@ type mockServiceForRetry struct {
}
func (service *mockServiceForRetry) getImagesByCveID(ctx context.Context, config searchConfig,
username, password, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
username, password, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
service.retryCounter += 1
if service.retryCounter < service.succeedOn || service.succeedOn < 0 {
rch <- stringResult{"", zotErrors.ErrCVEDBNotFound}
rch <- stringResult{"", zerr.ErrCVEDBNotFound}
close(rch)
wtgrp.Done()

30
pkg/cli/cves_cmd.go Normal file
View file

@ -0,0 +1,30 @@
//go:build search
// +build search
package cli
import (
"github.com/spf13/cobra"
"zotregistry.io/zot/pkg/cli/cmdflags"
)
func NewCVESCommand(searchService SearchService) *cobra.Command {
cvesCmd := &cobra.Command{
Use: "cves [command]",
Short: "Lookup CVEs in images hosted on the zot registry",
Long: `List CVEs (Common Vulnerabilities and Exposures) of images hosted on the zot registry`,
}
cvesCmd.SetUsageTemplate(cvesCmd.UsageTemplate() + usageFooter)
cvesCmd.PersistentFlags().StringP(cmdflags.OutputFormatFlag, "f", "", "Specify output format [text/json/yaml]")
cvesCmd.PersistentFlags().Bool(cmdflags.VerboseFlag, false, "Show verbose output")
cvesCmd.PersistentFlags().Bool(cmdflags.DebugFlag, false, "Show debug output")
cvesCmd.AddCommand(NewCveForImageCommand(searchService))
cvesCmd.AddCommand(NewImagesByCVEIDCommand(searchService))
cvesCmd.AddCommand(NewFixedTagsCommand(searchService))
return cvesCmd
}

128
pkg/cli/cves_sub_cmd.go Normal file
View file

@ -0,0 +1,128 @@
//go:build search
// +build search
package cli
import (
"fmt"
"strings"
"github.com/spf13/cobra"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
zcommon "zotregistry.io/zot/pkg/common"
)
const (
maxRetries = 20
)
func NewCveForImageCommand(searchService SearchService) *cobra.Command {
var searchedCVEID string
cveForImageCmd := &cobra.Command{
Use: "image [repo:tag]|[repo@digest]",
Short: "List CVEs by REPO:TAG or REPO@DIGEST",
Long: `List CVEs by REPO:TAG or REPO@DIGEST`,
Args: OneImageWithRefArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
err = CheckExtEndPointQuery(searchConfig, CVEListForImageQuery())
if err != nil {
return fmt.Errorf("%w: '%s'", err, CVEListForImageQuery().Name)
}
image := args[0]
return SearchCVEForImageGQL(searchConfig, image, searchedCVEID)
},
}
cveForImageCmd.Flags().StringVar(&searchedCVEID, cmdflags.SearchedCVEID, "", "Search for a specific CVE by name/id")
return cveForImageCmd
}
func NewImagesByCVEIDCommand(searchService SearchService) *cobra.Command {
var repo string
imagesByCVEIDCmd := &cobra.Command{
Use: "cveid [cveId]",
Short: "List images affected by a CVE",
Long: `List images affected by a CVE`,
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
if !strings.HasPrefix(args[0], "CVE") {
return fmt.Errorf("%w: expected a cveid 'CVE-...' got '%s'", zerr.ErrInvalidCLIParameter, args[0])
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
err = CheckExtEndPointQuery(searchConfig, ImageListForCVEQuery())
if err != nil {
return fmt.Errorf("%w: '%s'", err, ImageListForCVEQuery().Name)
}
searchedCVEID := args[0]
return SearchImagesByCVEIDGQL(searchConfig, repo, searchedCVEID)
},
}
imagesByCVEIDCmd.Flags().StringVar(&repo, "repo", "", "Search for a specific CVE by name/id")
return imagesByCVEIDCmd
}
func NewFixedTagsCommand(searchService SearchService) *cobra.Command {
fixedTagsCmd := &cobra.Command{
Use: "fixed [repo] [cveId]",
Short: "List tags where a CVE is fixedRetryWithContext",
Long: `List tags where a CVE is fixedRetryWithContext`,
Args: func(cmd *cobra.Command, args []string) error {
const argCount = 2
if err := cobra.ExactArgs(argCount)(cmd, args); err != nil {
return err
}
if !zcommon.CheckIsCorrectRepoNameFormat(args[0]) {
return fmt.Errorf("%w: expected a valid repo name for first argument '%s'", zerr.ErrInvalidCLIParameter, args[0])
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
err = CheckExtEndPointQuery(searchConfig, ImageListWithCVEFixedQuery())
if err != nil {
return fmt.Errorf("%w: '%s'", err, ImageListWithCVEFixedQuery().Name)
}
repo := args[0]
searchedCVEID := args[1]
return SearchFixedTagsGQL(searchConfig, repo, searchedCVEID)
},
}
return fixedTagsCmd
}

View file

@ -9,12 +9,16 @@ import (
distext "github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/common"
zcommon "zotregistry.io/zot/pkg/common"
)
type field struct {
Name string `json:"name"`
Args []struct {
Name string `json:"name"`
} `json:"args"`
}
type schemaList struct {
@ -23,9 +27,19 @@ type schemaList struct {
QueryType struct {
Fields []field `json:"fields"`
} `json:"queryType"` //nolint:tagliatelle // graphQL schema
Types []typeInfo `json:"types"`
} `json:"__schema"` //nolint:tagliatelle // graphQL schema
} `json:"data"`
Errors []common.ErrorGQL `json:"errors"`
Errors []zcommon.ErrorGQL `json:"errors"`
}
type typeInfo struct {
Name string `json:"name"`
Fields []typeField `json:"fields"`
}
type typeField struct {
Name string `json:"name"`
}
func containsGQLQuery(queryList []field, query string) bool {
@ -38,6 +52,54 @@ func containsGQLQuery(queryList []field, query string) bool {
return false
}
func containsGQLQueryWithParams(queryList []field, serverGQLTypesList []typeInfo, requiredQueries ...GQLQuery) error {
serverGQLTypes := map[string][]typeField{}
for _, typeInfo := range serverGQLTypesList {
serverGQLTypes[typeInfo.Name] = typeInfo.Fields
}
for _, reqQuery := range requiredQueries {
foundQuery := false
for _, query := range queryList {
if query.Name == reqQuery.Name && haveSameArgs(query, reqQuery) {
foundQuery = true
}
}
if !foundQuery {
return fmt.Errorf("%w: %s", zerr.ErrGQLQueryNotSupported, reqQuery.Name)
}
// let's check just the name of the returned type
returnType := reqQuery.ReturnType.Name
// we can next define fields of the returned types and check them recursively
// for now we will just check the name of the returned type to be known by the server
_, ok := serverGQLTypes[returnType]
if !ok {
return fmt.Errorf("%w: server doesn't support needed type '%s'", zerr.ErrGQLQueryNotSupported, returnType)
}
}
return nil
}
func haveSameArgs(query field, reqQuery GQLQuery) bool {
if len(query.Args) != len(reqQuery.Args) {
return false
}
for i := range query.Args {
if query.Args[i].Name != reqQuery.Args[i] {
return false
}
}
return true
}
func checkExtEndPoint(config searchConfig) bool {
username, password := getUsernameAndPassword(*config.user)
ctx := context.Background()
@ -59,7 +121,7 @@ func checkExtEndPoint(config searchConfig) bool {
searchEnabled := false
for _, extension := range discoverResponse.Extensions {
if extension.Name == "_zot" {
if extension.Name == constants.BaseExtension {
for _, endpoint := range extension.Endpoints {
if endpoint == constants.FullSearchPrefix {
searchEnabled = true
@ -99,3 +161,80 @@ func checkExtEndPoint(config searchConfig) bool {
return containsGQLQuery(queryResponse.Data.Schema.QueryType.Fields, "ImageList")
}
func CheckExtEndPointQuery(config searchConfig, requiredQueries ...GQLQuery) error {
username, password := getUsernameAndPassword(*config.user)
ctx := context.Background()
discoverEndPoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("%s%s",
constants.RoutePrefix, constants.ExtOciDiscoverPrefix))
if err != nil {
return err
}
discoverResponse := &distext.ExtensionList{}
_, err = makeGETRequest(ctx, discoverEndPoint, username, password, *config.verifyTLS,
*config.debug, &discoverResponse, config.resultWriter)
if err != nil {
return err
}
searchEnabled := false
for _, extension := range discoverResponse.Extensions {
if extension.Name == constants.BaseExtension {
for _, endpoint := range extension.Endpoints {
if endpoint == constants.FullSearchPrefix {
searchEnabled = true
}
}
}
}
if !searchEnabled {
return fmt.Errorf("%w: search extension gql endpoints not found", zerr.ErrExtensionNotEnabled)
}
searchEndPoint, _ := combineServerAndEndpointURL(*config.servURL, constants.FullSearchPrefix)
schemaQuery := `
{
__schema() {
queryType {
fields {
name
args {
name
}
type {
name
kind
}
}
__typename
}
types {
name
fields {
name
}
}
}
}`
queryResponse := &schemaList{}
err = makeGraphQLRequest(ctx, searchEndPoint, schemaQuery, username, password, *config.verifyTLS,
*config.debug, queryResponse, config.resultWriter)
if err != nil {
return fmt.Errorf("gql query failed: %w", err)
}
if err = checkResultGraphQLQuery(ctx, err, queryResponse.Errors); err != nil {
return fmt.Errorf("gql query failed: %w", err)
}
return containsGQLQueryWithParams(queryResponse.Data.Schema.QueryType.Fields,
queryResponse.Data.Schema.Types, requiredQueries...)
}

116
pkg/cli/gql_queries.go Normal file
View file

@ -0,0 +1,116 @@
//go:build search
// +build search
package cli
type GQLField struct {
Name string
Type GQLType
}
type GQLType struct {
Name string
Fields []GQLField
}
type GQLQuery struct {
Name string
Args []string
ReturnType GQLType
}
func CVEResultForImage() GQLType {
return GQLType{
Name: "CVEResultForImage",
}
}
func PaginatedImagesResult() GQLType {
return GQLType{
Name: "PaginatedImagesResult",
}
}
func Referrer() GQLType {
return GQLType{
Name: "Referrer",
}
}
func GlobalSearchResult() GQLType {
return GQLType{
Name: "GlobalSearchResult",
}
}
func ImageListQuery() GQLQuery {
return GQLQuery{
Name: "ImageList",
Args: []string{"repo", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func ImageListForDigestQuery() GQLQuery {
return GQLQuery{
Name: "ImageListForDigest",
Args: []string{"id", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func BaseImageListQuery() GQLQuery {
return GQLQuery{
Name: "BaseImageList",
Args: []string{"image", "digest", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func DerivedImageListQuery() GQLQuery {
return GQLQuery{
Name: "DerivedImageList",
Args: []string{"image", "digest", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func CVEListForImageQuery() GQLQuery {
return GQLQuery{
Name: "CVEListForImage",
Args: []string{"image", "requestedPage", "searchedCVE"},
ReturnType: CVEResultForImage(),
}
}
func ImageListForCVEQuery() GQLQuery {
return GQLQuery{
Name: "ImageListForCVE",
Args: []string{"id", "filter", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func ImageListWithCVEFixedQuery() GQLQuery {
return GQLQuery{
Name: "ImageListWithCVEFixed",
Args: []string{"id", "image", "filter", "requestedPage"},
ReturnType: PaginatedImagesResult(),
}
}
func ReferrersQuery() GQLQuery {
return GQLQuery{
Name: "Referrers",
Args: []string{"repo", "digest", "type"},
ReturnType: Referrer(),
}
}
func GlobalSearchQuery() GQLQuery {
return GQLQuery{
Name: "GlobalSearch",
Args: []string{"query", "filter", "requestedPage"},
ReturnType: GlobalSearchResult(),
}
}

View file

@ -0,0 +1,93 @@
//go:build search
// +build search
package cli //nolint:testpackage
import (
"io"
"testing"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/test"
)
func TestGQLQueries(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
dir := t.TempDir()
conf.Storage.RootDirectory = dir
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
searchConfig := searchConfig{
servURL: &baseURL,
user: ref(""),
verifyTLS: ref(false),
debug: ref(false),
resultWriter: io.Discard,
}
Convey("Make sure the current CLI used the right queries in case they change", t, func() {
Convey("ImageList", func() {
err := CheckExtEndPointQuery(searchConfig, ImageListQuery())
So(err, ShouldBeNil)
})
Convey("ImageListForDigest", func() {
err := CheckExtEndPointQuery(searchConfig, ImageListForDigestQuery())
So(err, ShouldBeNil)
})
Convey("BaseImageList", func() {
err := CheckExtEndPointQuery(searchConfig, BaseImageListQuery())
So(err, ShouldBeNil)
})
Convey("DerivedImageList", func() {
err := CheckExtEndPointQuery(searchConfig, DerivedImageListQuery())
So(err, ShouldBeNil)
})
Convey("CVEListForImage", func() {
err := CheckExtEndPointQuery(searchConfig, CVEListForImageQuery())
So(err, ShouldBeNil)
})
Convey("ImageListForCVE", func() {
err := CheckExtEndPointQuery(searchConfig, ImageListForCVEQuery())
So(err, ShouldBeNil)
})
Convey("ImageListWithCVEFixed", func() {
err := CheckExtEndPointQuery(searchConfig, ImageListWithCVEFixedQuery())
So(err, ShouldBeNil)
})
Convey("Referrers", func() {
err := CheckExtEndPointQuery(searchConfig, ReferrersQuery())
So(err, ShouldBeNil)
})
Convey("GlobalSearch", func() {
err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery())
So(err, ShouldBeNil)
})
})
}

View file

@ -12,7 +12,7 @@ import (
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
zotErrors "zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
)
//nolint:dupl
@ -24,16 +24,16 @@ func NewImageCommand(searchService SearchService) *cobra.Command {
var isSpinner, verifyTLS, verbose, debug bool
imageCmd := &cobra.Command{
Use: "images [config-name]",
Short: "List images hosted on the zot registry",
Long: `List images hosted on the zot registry`,
Use: "image [config-name]",
Short: "DEPRECATED (see images)",
Long: `DEPRECATED (see images)! List images hosted on the zot registry`,
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
configPath := path.Join(home + "/.zot")
configPath := path.Join(home, "/.zot")
if servURL == "" {
if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
@ -44,12 +44,12 @@ func NewImageCommand(searchService SearchService) *cobra.Command {
}
if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
servURL = urlFromConfig
} else {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
}
@ -129,11 +129,12 @@ func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*stri
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`)
imageCmd.Flags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]")
imageCmd.Flags().BoolVar(verbose, "verbose", false, "Show verbose output")
imageCmd.Flags().BoolVar(debug, "debug", false, "Show debug output")
imageCmd.PersistentFlags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
imageCmd.PersistentFlags().StringVarP(user, "user", "u", "",
`User Credentials of zot server in "username:password" format`)
imageCmd.PersistentFlags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]")
imageCmd.PersistentFlags().BoolVar(verbose, "verbose", false, "Show verbose output")
imageCmd.PersistentFlags().BoolVar(debug, "debug", false, "Show debug output")
}
func searchImage(searchConfig searchConfig) error {
@ -156,7 +157,7 @@ func searchImage(searchConfig searchConfig) error {
}
}
return zotErrors.ErrInvalidFlagsCombination
return zerr.ErrInvalidFlagsCombination
}
const (

View file

@ -26,9 +26,10 @@ import (
"github.com/spf13/cobra"
"gopkg.in/resty.v1"
zotErrors "zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/cli/cmdflags"
"zotregistry.io/zot/pkg/common"
extconf "zotregistry.io/zot/pkg/extensions/config"
zlog "zotregistry.io/zot/pkg/log"
@ -74,7 +75,7 @@ func TestSearchImageCmd(t *testing.T) {
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrNoURLProvided)
So(err, ShouldEqual, zerr.ErrNoURLProvided)
})
Convey("Test image invalid home directory", t, func() {
@ -130,7 +131,7 @@ func TestSearchImageCmd(t *testing.T) {
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
So(err, ShouldEqual, zerr.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format")
})
@ -253,7 +254,7 @@ func TestSearchImageCmd(t *testing.T) {
imageCmd.SetArgs(args)
err := imageCmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zotErrors.ErrInvalidURL)
So(err, ShouldEqual, zerr.ErrInvalidURL)
So(buff.String(), ShouldContainSubstring, "invalid URL format")
})
})
@ -1555,6 +1556,564 @@ func runDisplayIndexTests(baseURL string) {
})
}
func TestImagesCommandGQL(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands with gql", t, func() {
err := test.RemoveLocalStorageContents(ctlr.StoreController.DefaultStore)
So(err, ShouldBeNil)
Convey("base and derived command", func() {
baseImage := test.CreateImageWith().LayerBlobs(
[][]byte{{1, 2, 3}, {11, 22, 33}},
).DefaultConfig().Build()
derivedImage := test.CreateImageWith().LayerBlobs(
[][]byte{{1, 2, 3}, {11, 22, 33}, {44, 55, 66}},
).DefaultConfig().Build()
err := test.UploadImage(baseImage, baseURL, "repo", "base")
So(err, ShouldBeNil)
err = test.UploadImage(derivedImage, baseURL, "repo", "derived")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"base", "repo:derived"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
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, "repo base linux/amd64 df554ddd false 699B")
args = []string{"derived", "repo:base"}
cmd = NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo derived linux/amd64 79f4b82e false 854B")
})
Convey("base and derived command errors", func() {
// too many parameters
buff := bytes.NewBufferString("")
args := []string{"too", "many", "args"}
cmd := NewImageBaseCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
cmd = NewImageDerivedCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// bad input
buff = bytes.NewBufferString("")
args = []string{"only-repo"}
cmd = NewImageBaseCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
cmd = NewImageDerivedCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no url
buff = bytes.NewBufferString("")
args = []string{"repo:tag"}
cmd = NewImageBaseCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
cmd = NewImageDerivedCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("digest command", func() {
image := test.CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"digest", image.DigestStr()}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
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, fmt.Sprintf("repo img linux/amd64 %s false 552B",
image.DigestStr()[7:7+8]))
})
Convey("digest command errors", func() {
// too many parameters
buff := bytes.NewBufferString("")
args := []string{"too", "many", "args"}
cmd := NewImageDigestCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// bad input
buff = bytes.NewBufferString("")
args = []string{"bad-digest"}
cmd = NewImageDigestCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no url
buff = bytes.NewBufferString("")
args = []string{godigest.FromString("str").String()}
cmd = NewImageDigestCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("list command", func() {
image := test.CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"list"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
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)
fmt.Println(actual)
So(actual, ShouldContainSubstring, fmt.Sprintf("repo img linux/amd64 %s false 552B",
image.DigestStr()[7:7+8]))
fmt.Println(actual)
})
Convey("list command errors", func() {
// too many parameters
buff := bytes.NewBufferString("")
args := []string{"repo:img", "arg"}
cmd := NewImageListCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// no url
buff = bytes.NewBufferString("")
args = []string{}
cmd = NewImageListCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("name command", func() {
image := test.CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
err = test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "img2")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"name", "repo:img"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
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)
fmt.Println(actual)
So(actual, ShouldContainSubstring, fmt.Sprintf("repo img linux/amd64 %s false 552B",
image.DigestStr()[7:7+8]))
fmt.Println(actual)
})
Convey("name command errors", func() {
// too many parameters
buff := bytes.NewBufferString("")
args := []string{"repo:img", "arg"}
cmd := NewImageNameCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
// bad input
buff = bytes.NewBufferString("")
args = []string{":tag"}
cmd = NewImageNameCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
// no url
buff = bytes.NewBufferString("")
args = []string{"repo:tag"}
cmd = NewImageNameCommand(NewSearchService())
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("CVE", func() {
vulnImage := test.CreateDefaultVulnerableImage()
err := test.UploadImage(vulnImage, baseURL, "repo", "vuln")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"cve", "repo:vuln"}
defer os.Remove(configPath)
cmd := NewImagesCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
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, "dummyCVEID HIGH Title of that CVE")
})
Convey("CVE errors", func() {
count := 0
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"cve", "repo:vuln"}
defer os.Remove(configPath)
cmd := NewImagesCommand(mockService{
getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, searchedCVE string) (*cveResult, error,
) {
if count == 0 {
count++
fmt.Println("Count:", count)
return &cveResult{}, zerr.ErrCVEDBNotFound
}
return &cveResult{}, zerr.ErrInjected
},
})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready")
})
})
Convey("Config error", t, func() {
args := []string{"base", "repo:derived"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldNotBeNil)
args = []string{"derived", "repo:base"}
cmd = NewImagesCommand(NewSearchService())
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
args = []string{"digest", ispec.DescriptorEmptyJSON.Digest.String()}
cmd = NewImagesCommand(NewSearchService())
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
args = []string{"list"}
cmd = NewImagesCommand(NewSearchService())
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
args = []string{"name", "repo:img"}
cmd = NewImagesCommand(NewSearchService())
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
args = []string{"cve", "repo:vuln"}
cmd = NewImagesCommand(mockService{})
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
}
func TestImageCommandREST(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands without gql", t, func() {
err := test.RemoveLocalStorageContents(ctlr.StoreController.DefaultStore)
So(err, ShouldBeNil)
Convey("base and derived command", func() {
baseImage := test.CreateImageWith().LayerBlobs(
[][]byte{{1, 2, 3}, {11, 22, 33}},
).DefaultConfig().Build()
derivedImage := test.CreateImageWith().LayerBlobs(
[][]byte{{1, 2, 3}, {11, 22, 33}, {44, 55, 66}},
).DefaultConfig().Build()
err := test.UploadImage(baseImage, baseURL, "repo", "base")
So(err, ShouldBeNil)
err = test.UploadImage(derivedImage, baseURL, "repo", "derived")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"base", "repo:derived"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
args = []string{"derived", "repo:base"}
cmd = NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("digest command", func() {
image := test.CreateRandomImage()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"digest", image.DigestStr()}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("list command", func() {
image := test.CreateRandomImage()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"list"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
fmt.Println(buff.String())
fmt.Println()
})
Convey("name command", func() {
image := test.CreateRandomImage()
err := test.UploadImage(image, baseURL, "repo", "img")
So(err, ShouldBeNil)
err = test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "img2")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"name", "repo:img"}
cmd := NewImagesCommand(NewSearchService())
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
fmt.Println(buff.String())
fmt.Println()
})
Convey("CVE", func() {
vulnImage := test.CreateDefaultVulnerableImage()
err := test.UploadImage(vulnImage, baseURL, "repo", "vuln")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`,
baseURL))
args := []string{"cve", "repo:vuln"}
defer os.Remove(configPath)
cmd := NewImagesCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldNotBeNil)
})
})
}
func uploadTestMultiarch(baseURL string) {
// ------- Define Image1
layer11 := []byte{11, 12, 13, 14}
@ -1606,7 +2165,7 @@ func MockNewImageCommand(searchService SearchService) *cobra.Command {
panic(err)
}
configPath := path.Join(home + "/.zot")
configPath := path.Join(home, "/.zot")
if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
if err != nil {
@ -1616,12 +2175,12 @@ func MockNewImageCommand(searchService SearchService) *cobra.Command {
}
if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
servURL = urlFromConfig
} else {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
if len(args) > 0 {
@ -1679,7 +2238,7 @@ func MockSearchImage(searchConfig searchConfig) error {
}
}
return zotErrors.ErrInvalidFlagsCombination
return zerr.ErrInvalidFlagsCombination
}
func uploadManifest(url string) error {
@ -1895,7 +2454,73 @@ func uploadManifestDerivedBase(url string) error {
return nil
}
type mockService struct{}
type mockService struct {
getAllImagesFn func(ctx context.Context, config searchConfig, username, password string,
channel chan stringResult, wtgrp *sync.WaitGroup)
getImagesGQLFn func(ctx context.Context, config searchConfig, username, password string,
imageName string) (*common.ImageListResponse, error)
getImageByNameFn func(ctx context.Context, config searchConfig,
username, password, imageName string, channel chan stringResult, wtgrp *sync.WaitGroup,
)
getFixedTagsForCVEFn func(ctx context.Context, config searchConfig,
username, password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup,
)
getImageByNameAndCVEIDFn func(ctx context.Context, config searchConfig, username,
password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup,
)
getImagesByCveIDFn func(ctx context.Context, config searchConfig, username, password, cveid string,
rch chan stringResult, wtgrp *sync.WaitGroup,
)
getImagesByDigestFn func(ctx context.Context, config searchConfig, username,
password, digest string, rch chan stringResult, wtgrp *sync.WaitGroup,
)
getReferrersFn func(ctx context.Context, config searchConfig, username, password string,
repo, digest string,
) (referrersResult, error)
globalSearchGQLFn func(ctx context.Context, config searchConfig, username, password string,
query string,
) (*common.GlobalSearch, error)
getReferrersGQLFn func(ctx context.Context, config searchConfig, username, password string,
repo, digest string,
) (*common.ReferrersResp, error)
getDerivedImageListGQLFn func(ctx context.Context, config searchConfig, username, password string,
derivedImage string,
) (*common.DerivedImageListResponse, error)
getBaseImageListGQLFn func(ctx context.Context, config searchConfig, username, password string,
derivedImage string,
) (*common.BaseImageListResponse, error)
getImagesForDigestGQLFn func(ctx context.Context, config searchConfig, username, password string,
digest string,
) (*common.ImagesForDigest, error)
getCveByImageGQLFn func(ctx context.Context, config searchConfig, username, password,
imageName, searchedCVE string,
) (*cveResult, error)
getImagesByCveIDGQLFn func(ctx context.Context, config searchConfig, username, password string,
digest string,
) (*common.ImagesForCve, error)
getTagsForCVEGQLFn func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string,
) (*common.ImagesForCve, error)
getFixedTagsForCVEGQLFn func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string,
) (*common.ImageListWithCVEFixedResponse, error)
}
func (service mockService) getRepos(ctx context.Context, config searchConfig, username,
password string, channel chan stringResult, wtgrp *sync.WaitGroup,
@ -1903,32 +2528,47 @@ func (service mockService) getRepos(ctx context.Context, config searchConfig, us
defer wtgrp.Done()
defer close(channel)
var catalog [3]string
catalog[0] = "python"
catalog[1] = "busybox"
catalog[2] = "hello-world"
fmt.Fprintln(config.resultWriter, "\n\nREPOSITORY NAME")
channel <- stringResult{"", nil}
fmt.Fprintln(config.resultWriter, "repo1")
fmt.Fprintln(config.resultWriter, "repo2")
}
func (service mockService) getReferrers(ctx context.Context, config searchConfig, username, password string,
repo, digest string,
) (referrersResult, error) {
return referrersResult{}, nil
if service.getReferrersFn != nil {
return service.getReferrersFn(ctx, config, username, password, repo, digest)
}
return referrersResult{
common.Referrer{
ArtifactType: "art.type",
Digest: ispec.DescriptorEmptyJSON.Digest.String(),
MediaType: ispec.MediaTypeImageManifest,
Size: 100,
},
}, nil
}
func (service mockService) globalSearchGQL(ctx context.Context, config searchConfig, username, password string,
query string,
) (*common.GlobalSearch, error) {
if service.globalSearchGQLFn != nil {
return service.globalSearchGQLFn(ctx, config, username, password, query)
}
return &common.GlobalSearch{
Images: []common.ImageSummary{
{
RepoName: "repo",
MediaType: ispec.MediaTypeImageManifest,
Size: "100",
Manifests: []common.ManifestSummary{
{
Digest: godigest.FromString("str").String(),
Size: "100",
ConfigDigest: ispec.DescriptorEmptyJSON.Digest.String(),
},
},
},
@ -1936,6 +2576,8 @@ func (service mockService) globalSearchGQL(ctx context.Context, config searchCon
Repos: []common.RepoSummary{
{
Name: "repo",
Size: "100",
LastUpdated: time.Date(2010, 1, 1, 1, 1, 1, 0, time.UTC),
},
},
}, nil
@ -1944,6 +2586,10 @@ func (service mockService) globalSearchGQL(ctx context.Context, config searchCon
func (service mockService) getReferrersGQL(ctx context.Context, config searchConfig, username, password string,
repo, digest string,
) (*common.ReferrersResp, error) {
if service.getReferrersGQLFn != nil {
return service.getReferrersGQLFn(ctx, config, username, password, repo, digest)
}
return &common.ReferrersResp{
ReferrersResult: common.ReferrersResult{
Referrers: []common.Referrer{
@ -1961,6 +2607,10 @@ func (service mockService) getReferrersGQL(ctx context.Context, config searchCon
func (service mockService) getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string,
derivedImage string,
) (*common.DerivedImageListResponse, error) {
if service.getDerivedImageListGQLFn != nil {
return service.getDerivedImageListGQLFn(ctx, config, username, password, derivedImage)
}
imageListGQLResponse := &common.DerivedImageListResponse{}
imageListGQLResponse.DerivedImageList.Results = []common.ImageSummary{
{
@ -1982,8 +2632,12 @@ func (service mockService) getDerivedImageListGQL(ctx context.Context, config se
}
func (service mockService) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
derivedImage string,
baseImage string,
) (*common.BaseImageListResponse, error) {
if service.getBaseImageListGQLFn != nil {
return service.getBaseImageListGQLFn(ctx, config, username, password, baseImage)
}
imageListGQLResponse := &common.BaseImageListResponse{}
imageListGQLResponse.BaseImageList.Results = []common.ImageSummary{
{
@ -2007,6 +2661,10 @@ func (service mockService) getBaseImageListGQL(ctx context.Context, config searc
func (service mockService) getImagesGQL(ctx context.Context, config searchConfig, username, password string,
imageName string,
) (*common.ImageListResponse, error) {
if service.getImagesGQLFn != nil {
return service.getImagesGQLFn(ctx, config, username, password, imageName)
}
imageListGQLResponse := &common.ImageListResponse{}
imageListGQLResponse.PaginatedImagesResult.Results = []common.ImageSummary{
{
@ -2029,9 +2687,13 @@ func (service mockService) getImagesGQL(ctx context.Context, config searchConfig
return imageListGQLResponse, nil
}
func (service mockService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string,
func (service mockService) getImagesForDigestGQL(ctx context.Context, config searchConfig, username, password string,
digest string,
) (*common.ImagesForDigest, error) {
if service.getImagesForDigestGQLFn != nil {
return service.getImagesForDigestGQLFn(ctx, config, username, password, digest)
}
imageListGQLResponse := &common.ImagesForDigest{}
imageListGQLResponse.Results = []common.ImageSummary{
{
@ -2057,6 +2719,10 @@ func (service mockService) getImagesByDigestGQL(ctx context.Context, config sear
func (service mockService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string,
digest string,
) (*common.ImagesForCve, error) {
if service.getImagesByCveIDGQLFn != nil {
return service.getImagesByCveIDGQLFn(ctx, config, username, password, digest)
}
imagesForCve := &common.ImagesForCve{
Errors: nil,
ImagesForCVEList: struct {
@ -2075,6 +2741,10 @@ func (service mockService) getImagesByCveIDGQL(ctx context.Context, config searc
func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password,
imageName, cveID string,
) (*common.ImagesForCve, error) {
if service.getTagsForCVEGQLFn != nil {
return service.getTagsForCVEGQLFn(ctx, config, username, password, imageName, cveID)
}
images := &common.ImagesForCve{
Errors: nil,
ImagesForCVEList: struct {
@ -2082,6 +2752,10 @@ func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchCo
}{},
}
if imageName == "" {
imageName = "image-name"
}
images.Errors = nil
mockedImage := service.getMockedImageByName(imageName)
@ -2093,6 +2767,10 @@ func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchCo
func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password,
imageName, cveID string,
) (*common.ImageListWithCVEFixedResponse, error) {
if service.getFixedTagsForCVEGQLFn != nil {
return service.getFixedTagsForCVEGQLFn(ctx, config, username, password, imageName, cveID)
}
fixedTags := &common.ImageListWithCVEFixedResponse{
Errors: nil,
ImageListWithCVEFixed: struct {
@ -2111,6 +2789,9 @@ func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config sea
func (service mockService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
imageName, searchedCVE string,
) (*cveResult, error) {
if service.getCveByImageGQLFn != nil {
return service.getCveByImageGQLFn(ctx, config, username, password, imageName, searchedCVE)
}
cveRes := &cveResult{}
cveRes.Data = cveData{
CVEListForImage: cveListForImage{
@ -2141,6 +2822,7 @@ func (service mockService) getMockedImageByName(imageName string) imageStruct {
image := imageStruct{}
image.RepoName = imageName
image.Tag = "tag"
image.MediaType = ispec.MediaTypeImageManifest
image.Manifests = []common.ManifestSummary{
{
Digest: godigest.FromString("Digest").String(),
@ -2160,6 +2842,12 @@ func (service mockService) getAllImages(ctx context.Context, config searchConfig
defer wtgrp.Done()
defer close(channel)
if service.getAllImagesFn != nil {
service.getAllImagesFn(ctx, config, username, password, channel, wtgrp)
return
}
image := &imageStruct{}
image.RepoName = "randomimageName"
image.Tag = "tag"
@ -2192,6 +2880,12 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf
defer wtgrp.Done()
defer close(channel)
if service.getImageByNameFn != nil {
service.getImageByNameFn(ctx, config, username, password, imageName, channel, wtgrp)
return
}
image := &imageStruct{}
image.RepoName = imageName
image.Tag = "tag"
@ -2257,26 +2951,62 @@ func (service mockService) getCveByImage(ctx context.Context, config searchConfi
}
func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig,
username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
username, password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
if service.getFixedTagsForCVEFn != nil {
defer wtgrp.Done()
defer close(rch)
service.getFixedTagsForCVEFn(ctx, config, username, password, imageName, cveid, rch, wtgrp)
return
}
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
}
func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username,
password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
if service.getImageByNameAndCVEIDFn != nil {
defer wtgrp.Done()
defer close(rch)
service.getImageByNameAndCVEIDFn(ctx, config, username, password, imageName, cveid, rch, wtgrp)
return
}
service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp)
}
func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string,
func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cveid string,
rch chan stringResult, wtgrp *sync.WaitGroup,
) {
if service.getImagesByCveIDFn != nil {
defer wtgrp.Done()
defer close(rch)
service.getImagesByCveIDFn(ctx, config, username, password, cveid, rch, wtgrp)
return
}
service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp)
}
func (service mockService) getImagesByDigest(ctx context.Context, config searchConfig, username,
password, digest string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
if service.getImagesByDigestFn != nil {
defer wtgrp.Done()
defer close(rch)
service.getImagesByDigestFn(ctx, config, username, password, digest, rch, wtgrp)
return
}
service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp)
}
@ -2288,7 +3018,7 @@ func makeConfigFile(content string) string {
panic(err)
}
configPath := path.Join(home + "/.zot")
configPath := path.Join(home, "/.zot")
if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil {
panic(err)

33
pkg/cli/images_cmd.go Normal file
View file

@ -0,0 +1,33 @@
//go:build search
// +build search
package cli
import (
"github.com/spf13/cobra"
"zotregistry.io/zot/pkg/cli/cmdflags"
)
func NewImagesCommand(searchService SearchService) *cobra.Command {
imageCmd := &cobra.Command{
Use: "images [command]",
Short: "List images hosted on the zot registry",
Long: `List images hosted on the zot registry`,
}
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter)
imageCmd.PersistentFlags().StringP(cmdflags.OutputFormatFlag, "f", "", "Specify output format [text/json/yaml]")
imageCmd.PersistentFlags().Bool(cmdflags.VerboseFlag, false, "Show verbose output")
imageCmd.PersistentFlags().Bool(cmdflags.DebugFlag, false, "Show debug output")
imageCmd.AddCommand(NewImageListCommand(searchService))
imageCmd.AddCommand(NewImageCVEListCommand(searchService))
imageCmd.AddCommand(NewImageBaseCommand(searchService))
imageCmd.AddCommand(NewImageDerivedCommand(searchService))
imageCmd.AddCommand(NewImageDigestCommand(searchService))
imageCmd.AddCommand(NewImageNameCommand(searchService))
return imageCmd
}

284
pkg/cli/images_sub_cmd.go Normal file
View file

@ -0,0 +1,284 @@
//go:build search
// +build search
package cli
import (
"fmt"
"os"
"path"
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
zcommon "zotregistry.io/zot/pkg/common"
)
func NewImageListCommand(searchService SearchService) *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all images",
Long: "List all images",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, ImageListQuery()); err == nil {
return SearchAllImagesGQL(searchConfig)
}
return SearchAllImages(searchConfig)
},
}
}
func NewImageCVEListCommand(searchService SearchService) *cobra.Command {
var searchedCVEID string
cmd := &cobra.Command{
Use: "cve [repo-name:tag][repo-name@digest]",
Short: "List all CVE's of the image",
Long: "List all CVE's of the image",
Args: OneImageWithRefArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, CVEListForImageQuery()); err == nil {
image := args[0]
return SearchCVEForImageGQL(searchConfig, image, searchedCVEID)
} else {
return err
}
},
}
cmd.Flags().StringVar(&searchedCVEID, cmdflags.SearchedCVEID, "", "Search for a specific CVE by name/id")
return cmd
}
func NewImageDerivedCommand(searchService SearchService) *cobra.Command {
cmd := &cobra.Command{
Use: "derived [repo-name:tag][repo-name@digest]",
Short: "List images that are derived from given image",
Long: "List images that are derived from given image",
Args: OneImageWithRefArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, DerivedImageListQuery()); err == nil {
return SearchDerivedImageListGQL(searchConfig, args[0])
} else {
return err
}
},
}
return cmd
}
func NewImageBaseCommand(searchService SearchService) *cobra.Command {
cmd := &cobra.Command{
Use: "base [repo-name:tag][repo-name@digest]",
Short: "List images that are base for the given image",
Long: "List images that are base for the given image",
Args: OneImageWithRefArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, BaseImageListQuery()); err == nil {
return SearchBaseImageListGQL(searchConfig, args[0])
} else {
return err
}
},
}
return cmd
}
func NewImageDigestCommand(searchService SearchService) *cobra.Command {
cmd := &cobra.Command{
Use: "digest [digest]",
Short: "List images that contain a blob(manifest, config or layer) with the given digest",
Long: "List images that contain a blob(manifest, config or layer) with the given digest",
Args: OneDigestArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, ImageListForDigestQuery()); err == nil {
return SearchImagesForDigestGQL(searchConfig, args[0])
} else {
return err
}
},
}
return cmd
}
func NewImageNameCommand(searchService SearchService) *cobra.Command {
cmd := &cobra.Command{
Use: "name [repo:tag]",
Short: "List image details by name",
Long: "List image details by name",
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
image := args[0]
if dir, _ := zcommon.GetImageDirAndTag(image); dir == "" {
return zerr.ErrInvalidRepoRefFormat
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, ImageListQuery()); err == nil {
return SearchImageByNameGQL(searchConfig, args[0])
}
return SearchImageByName(searchConfig, args[0])
},
}
return cmd
}
func GetSearchConfigFromFlags(cmd *cobra.Command, searchService SearchService) (searchConfig, error) {
serverURL, err := GetServerURLFromFlags(cmd)
if err != nil {
return searchConfig{}, err
}
isSpinner, verifyTLS := GetCliConfigOptions(cmd)
flags := cmd.Flags()
user := defaultIfError(flags.GetString(cmdflags.UserFlag))
fixed := defaultIfError(flags.GetBool(cmdflags.FixedFlag))
debug := defaultIfError(flags.GetBool(cmdflags.DebugFlag))
verbose := defaultIfError(flags.GetBool(cmdflags.VerboseFlag))
outputFormat := defaultIfError(flags.GetString(cmdflags.OutputFormatFlag))
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
spin.Prefix = prefix
return searchConfig{
params: map[string]*string{},
searchService: searchService,
servURL: &serverURL,
user: &user,
outputFormat: &outputFormat,
verifyTLS: &verifyTLS,
fixedFlag: &fixed,
verbose: &verbose,
debug: &debug,
spinner: spinnerState{spin, isSpinner},
resultWriter: cmd.OutOrStdout(),
}, nil
}
func defaultIfError[T any](out T, err error) T {
var defaultVal T
if err != nil {
return defaultVal
}
return out
}
func GetCliConfigOptions(cmd *cobra.Command) (bool, bool) {
configName, err := cmd.Flags().GetString(cmdflags.ConfigFlag)
if err != nil {
return false, false
}
home, err := os.UserHomeDir()
if err != nil {
return false, false
}
configDir := path.Join(home, "/.zot")
isSpinner, err := parseBooleanConfig(configDir, configName, showspinnerConfig)
if err != nil {
return false, false
}
verifyTLS, err := parseBooleanConfig(configDir, configName, verifyTLSConfig)
if err != nil {
return false, false
}
return isSpinner, verifyTLS
}
func GetServerURLFromFlags(cmd *cobra.Command) (string, error) {
serverURL, err := cmd.Flags().GetString(cmdflags.URLFlag)
if err == nil && serverURL != "" {
return serverURL, nil
}
configName, err := cmd.Flags().GetString(cmdflags.ConfigFlag)
if err != nil {
return "", err
}
if configName == "" {
return "", fmt.Errorf("%w: specify either '--%s' or '--%s' flags", zerr.ErrNoURLProvided, cmdflags.URLFlag,
cmdflags.ConfigFlag)
}
serverURL, err = ReadServerURLFromConfig(configName)
if err != nil {
return serverURL, fmt.Errorf("reading url from config failed: %w", err)
}
if serverURL == "" {
return "", fmt.Errorf("%w: url field from config is empty", zerr.ErrNoURLProvided)
}
return serverURL, nil
}
func ReadServerURLFromConfig(configName string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
configDir := path.Join(home, "/.zot")
urlFromConfig, err := getConfigValue(configDir, configName, "url")
if err != nil {
return "", err
}
return urlFromConfig, nil
}

View file

@ -10,7 +10,8 @@ import (
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
zotErrors "zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
)
const prefix = "Searching... "
@ -24,13 +25,14 @@ func NewRepoCommand(searchService SearchService) *cobra.Command {
Use: "repos [config-name]",
Short: "List all repositories",
Long: `List all repositories`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
configPath := path.Join(home + "/.zot")
configPath := path.Join(home, "/.zot")
if servURL == "" {
if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
@ -41,12 +43,12 @@ func NewRepoCommand(searchService SearchService) *cobra.Command {
}
if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
servURL = urlFromConfig
} else {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
}
@ -96,9 +98,12 @@ func NewRepoCommand(searchService SearchService) *cobra.Command {
repoCmd.SetUsageTemplate(repoCmd.UsageTemplate() + usageFooter)
repoCmd.Flags().StringVar(&servURL, "url", "", "Specify zot server URL if config-name is not mentioned")
repoCmd.Flags().StringVarP(&user, "user", "u", "", `User Credentials of zot server in "username:password" format`)
repoCmd.Flags().BoolVar(&debug, "debug", false, "Show debug output")
repoCmd.AddCommand(NewListReposCommand(searchService))
repoCmd.Flags().StringVar(&servURL, cmdflags.URLFlag, "", "Specify zot server URL if config-name is not mentioned")
repoCmd.Flags().StringVarP(&user, cmdflags.UserFlag, "u", "",
`User Credentials of zot server in "username:password" format`)
repoCmd.Flags().BoolVar(&debug, cmdflags.DebugFlag, false, "Show debug output")
return repoCmd
}

25
pkg/cli/repos_sub_cmd.go Normal file
View file

@ -0,0 +1,25 @@
//go:build search
// +build search
package cli
import "github.com/spf13/cobra"
func NewListReposCommand(searchService SearchService) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List all repositories",
Long: "List all repositories",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
return SearchRepos(searchConfig)
},
}
return cmd
}

55
pkg/cli/repos_test.go Normal file
View file

@ -0,0 +1,55 @@
//go:build search
// +build search
package cli //nolint:testpackage
import (
"bytes"
"fmt"
"os"
"regexp"
"strings"
"testing"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/cli/cmdflags"
"zotregistry.io/zot/pkg/test"
)
func TestReposCommand(t *testing.T) {
Convey("repos", t, func() {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"repostest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
args := []string{"list"}
cmd := NewRepoCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "repostest", "")
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, "repo1")
So(actual, ShouldContainSubstring, "repo2")
})
}

View file

@ -18,10 +18,11 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/cli/cmdflags"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
zlog "zotregistry.io/zot/pkg/log"
@ -213,7 +214,13 @@ func NewCliRootCmd() *cobra.Command {
// additional cmds
enableCli(rootCmd)
// "version"
rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit")
rootCmd.Flags().BoolVarP(&showVersion, cmdflags.VersionFlag, "v", false, "show the version and exit")
rootCmd.PersistentFlags().String(cmdflags.URLFlag, "",
"Specify zot server URL if config-name is not mentioned")
rootCmd.PersistentFlags().String(cmdflags.ConfigFlag, "",
"Specify the repository where to connect")
rootCmd.PersistentFlags().StringP(cmdflags.UserFlag, "u", "",
`User Credentials of zot server in "username:password" format`)
return rootCmd
}
@ -225,18 +232,18 @@ func validateStorageConfig(cfg *config.Config, log zlog.Logger) error {
for _, storageConfig := range cfg.Storage.SubPaths {
if strings.EqualFold(defaultRootDir, storageConfig.RootDirectory) {
log.Error().Err(errors.ErrBadConfig).Msg("storage subpaths cannot use default storage root directory")
log.Error().Err(zerr.ErrBadConfig).Msg("storage subpaths cannot use default storage root directory")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
expConfig, ok := expConfigMap[storageConfig.RootDirectory]
if ok {
equal := expConfig.ParamsEqual(storageConfig)
if !equal {
log.Error().Err(errors.ErrBadConfig).Msg("storage config with same root directory should have same parameters")
log.Error().Err(zerr.ErrBadConfig).Msg("storage config with same root directory should have same parameters")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
} else {
expConfigMap[storageConfig.RootDirectory] = storageConfig
@ -251,31 +258,31 @@ func validateCacheConfig(cfg *config.Config, log zlog.Logger) error {
// dedupe true, remote storage, remoteCache true, but no cacheDriver (remote)
//nolint: lll
if cfg.Storage.Dedupe && cfg.Storage.StorageDriver != nil && cfg.Storage.RemoteCache && cfg.Storage.CacheDriver == nil {
log.Error().Err(errors.ErrBadConfig).Msg(
log.Error().Err(zerr.ErrBadConfig).Msg(
"dedupe set to true with remote storage and caching, but no remote cache configured!")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
if cfg.Storage.CacheDriver != nil && cfg.Storage.RemoteCache {
// local storage with remote caching
if cfg.Storage.StorageDriver == nil {
log.Error().Err(errors.ErrBadConfig).Msg("cannot have local storage driver with remote caching!")
log.Error().Err(zerr.ErrBadConfig).Msg("cannot have local storage driver with remote caching!")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
// unsupported cache driver
if cfg.Storage.CacheDriver["name"] != storageConstants.DynamoDBDriverName {
log.Error().Err(errors.ErrBadConfig).
log.Error().Err(zerr.ErrBadConfig).
Interface("cacheDriver", cfg.Storage.CacheDriver["name"]).Msg("unsupported cache driver")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
}
if !cfg.Storage.RemoteCache && cfg.Storage.CacheDriver != nil {
log.Warn().Err(errors.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory).
log.Warn().Err(zerr.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory).
Msg("remoteCache set to false but cacheDriver config (remote caching) provided for directory" +
"will ignore and use local caching")
}
@ -285,30 +292,30 @@ func validateCacheConfig(cfg *config.Config, log zlog.Logger) error {
// dedupe true, remote storage, remoteCache true, but no cacheDriver (remote)
//nolint: lll
if subPath.Dedupe && subPath.StorageDriver != nil && subPath.RemoteCache && subPath.CacheDriver == nil {
log.Error().Err(errors.ErrBadConfig).Msg("dedupe set to true with remote storage and caching, but no remote cache configured!")
log.Error().Err(zerr.ErrBadConfig).Msg("dedupe set to true with remote storage and caching, but no remote cache configured!")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
if subPath.CacheDriver != nil && subPath.RemoteCache {
// local storage with remote caching
if subPath.StorageDriver == nil {
log.Error().Err(errors.ErrBadConfig).Msg("cannot have local storage driver with remote caching!")
log.Error().Err(zerr.ErrBadConfig).Msg("cannot have local storage driver with remote caching!")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
// unsupported cache driver
if subPath.CacheDriver["name"] != storageConstants.DynamoDBDriverName {
log.Error().Err(errors.ErrBadConfig).Interface("cacheDriver", cfg.Storage.CacheDriver["name"]).
log.Error().Err(zerr.ErrBadConfig).Interface("cacheDriver", cfg.Storage.CacheDriver["name"]).
Msg("unsupported cache driver")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
}
if !subPath.RemoteCache && subPath.CacheDriver != nil {
log.Warn().Err(errors.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory).
log.Warn().Err(zerr.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory).
Msg("remoteCache set to false but cacheDriver config (remote caching) provided for directory," +
"will ignore and use local caching")
}
@ -331,27 +338,27 @@ func validateExtensionsConfig(cfg *config.Config, log zlog.Logger) error {
// it would make sense to also check for mgmt and user prefs to be enabled,
// but those are both enabled by having the search and ui extensions enabled
if cfg.Extensions.Search == nil || !*cfg.Extensions.Search.Enable {
log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without search extension.")
log.Warn().Err(zerr.ErrBadConfig).Msg("UI functionality can't be used without search extension.")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
}
//nolint:lll
if cfg.Storage.StorageDriver != nil && cfg.Extensions != nil && cfg.Extensions.Search != nil &&
cfg.Extensions.Search.Enable != nil && *cfg.Extensions.Search.Enable && cfg.Extensions.Search.CVE != nil {
log.Warn().Err(errors.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE")
log.Warn().Err(zerr.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
for _, subPath := range cfg.Storage.SubPaths {
//nolint:lll
if subPath.StorageDriver != nil && cfg.Extensions != nil && cfg.Extensions.Search != nil &&
cfg.Extensions.Search.Enable != nil && *cfg.Extensions.Search.Enable && cfg.Extensions.Search.CVE != nil {
log.Warn().Err(errors.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE")
log.Warn().Err(zerr.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
}
@ -402,17 +409,17 @@ func validateConfiguration(config *config.Config, log zlog.Logger) error {
if len(config.Storage.StorageDriver) != 0 {
// enforce s3 driver in case of using storage driver
if config.Storage.StorageDriver["name"] != storageConstants.S3StorageDriverName {
log.Error().Err(errors.ErrBadConfig).Interface("cacheDriver", config.Storage.StorageDriver["name"]).
log.Error().Err(zerr.ErrBadConfig).Interface("cacheDriver", config.Storage.StorageDriver["name"]).
Msg("unsupported storage driver")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
// enforce filesystem storage in case sync feature is enabled
if config.Extensions != nil && config.Extensions.Sync != nil {
log.Error().Err(errors.ErrBadConfig).Msg("sync supports only filesystem storage")
log.Error().Err(zerr.ErrBadConfig).Msg("sync supports only filesystem storage")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
}
@ -424,10 +431,10 @@ func validateConfiguration(config *config.Config, log zlog.Logger) error {
for route, storageConfig := range subPaths {
if len(storageConfig.StorageDriver) != 0 {
if storageConfig.StorageDriver["name"] != storageConstants.S3StorageDriverName {
log.Error().Err(errors.ErrBadConfig).Str("subpath", route).Interface("storageDriver",
log.Error().Err(zerr.ErrBadConfig).Str("subpath", route).Interface("storageDriver",
storageConfig.StorageDriver["name"]).Msg("unsupported storage driver")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
}
}
@ -456,23 +463,23 @@ func validateOpenIDConfig(cfg *config.Config, log zlog.Logger) error {
if config.IsOpenIDSupported(provider) {
if providerConfig.ClientID == "" || providerConfig.Issuer == "" ||
len(providerConfig.Scopes) == 0 {
log.Error().Err(errors.ErrBadConfig).
log.Error().Err(zerr.ErrBadConfig).
Msg("OpenID provider config requires clientid, issuer and scopes parameters")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
} else if config.IsOauth2Supported(provider) {
if providerConfig.ClientID == "" || len(providerConfig.Scopes) == 0 {
log.Error().Err(errors.ErrBadConfig).
log.Error().Err(zerr.ErrBadConfig).
Msg("OAuth2 provider config requires clientid and scopes parameters")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
} else {
log.Error().Err(errors.ErrBadConfig).
log.Error().Err(zerr.ErrBadConfig).
Msg("unsupported openid/oauth2 provider")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
}
}
@ -483,11 +490,11 @@ func validateOpenIDConfig(cfg *config.Config, log zlog.Logger) error {
func validateAuthzPolicies(config *config.Config, log zlog.Logger) error {
if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil &&
config.HTTP.Auth.OpenID == nil)) && !authzContainsOnlyAnonymousPolicy(config) {
log.Error().Err(errors.ErrBadConfig).
log.Error().Err(zerr.ErrBadConfig).
Msg("access control config requires one of httpasswd, ldap or openid authentication " +
"or using only 'anonymousPolicy' policies")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
return nil
@ -730,15 +737,15 @@ func LoadConfiguration(config *config.Config, configPath string) error {
log := zlog.NewLogger(config.Log.Level, config.Log.Output)
if len(metaData.Keys) == 0 {
log.Error().Err(errors.ErrBadConfig).Msg("config doesn't contain any key:value pair")
log.Error().Err(zerr.ErrBadConfig).Msg("config doesn't contain any key:value pair")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
if len(metaData.Unused) > 0 {
log.Error().Err(errors.ErrBadConfig).Strs("keys", metaData.Unused).Msg("unknown keys")
log.Error().Err(zerr.ErrBadConfig).Strs("keys", metaData.Unused).Msg("unknown keys")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
// defaults
@ -803,21 +810,21 @@ func validateLDAP(config *config.Config, log zlog.Logger) error {
log.Error().Str("userAttribute", ldap.UserAttribute).
Msg("invalid LDAP configuration, missing mandatory key: userAttribute")
return errors.ErrLDAPConfig
return zerr.ErrLDAPConfig
}
if ldap.Address == "" {
log.Error().Str("address", ldap.Address).
Msg("invalid LDAP configuration, missing mandatory key: address")
return errors.ErrLDAPConfig
return zerr.ErrLDAPConfig
}
if ldap.BaseDN == "" {
log.Error().Str("basedn", ldap.BaseDN).
Msg("invalid LDAP configuration, missing mandatory key: basedn")
return errors.ErrLDAPConfig
return zerr.ErrLDAPConfig
}
}
@ -830,7 +837,7 @@ func validateHTTP(config *config.Config, log zlog.Logger) error {
if err != nil || (port < 0 || port > 65535) {
log.Error().Str("port", config.HTTP.Port).Msg("invalid port")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
}
@ -840,27 +847,27 @@ func validateHTTP(config *config.Config, log zlog.Logger) error {
func validateGC(config *config.Config, log zlog.Logger) error {
// enforce GC params
if config.Storage.GCDelay < 0 {
log.Error().Err(errors.ErrBadConfig).Dur("delay", config.Storage.GCDelay).
log.Error().Err(zerr.ErrBadConfig).Dur("delay", config.Storage.GCDelay).
Msg("invalid garbage-collect delay specified")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
if config.Storage.GCInterval < 0 {
log.Error().Err(errors.ErrBadConfig).Dur("interval", config.Storage.GCInterval).
log.Error().Err(zerr.ErrBadConfig).Dur("interval", config.Storage.GCInterval).
Msg("invalid garbage-collect interval specified")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
if !config.Storage.GC {
if config.Storage.GCDelay != 0 {
log.Warn().Err(errors.ErrBadConfig).
log.Warn().Err(zerr.ErrBadConfig).
Msg("garbage-collect delay specified without enabling garbage-collect, will be ignored")
}
if config.Storage.GCInterval != 0 {
log.Warn().Err(errors.ErrBadConfig).
log.Warn().Err(zerr.ErrBadConfig).
Msg("periodic garbage-collect interval specified without enabling garbage-collect, will be ignored")
}
}
@ -868,12 +875,12 @@ func validateGC(config *config.Config, log zlog.Logger) error {
// subpaths
for name, subPath := range config.Storage.SubPaths {
if subPath.GC && subPath.GCDelay <= 0 {
log.Error().Err(errors.ErrBadConfig).
log.Error().Err(zerr.ErrBadConfig).
Str("subPath", name).
Interface("gcDelay", subPath.GCDelay).
Msg("invalid GC delay configuration - cannot be negative or zero")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
}
@ -886,10 +893,10 @@ func validateSync(config *config.Config, log zlog.Logger) error {
for id, regCfg := range config.Extensions.Sync.Registries {
// check retry options are configured for sync
if regCfg.MaxRetries != nil && regCfg.RetryDelay == nil {
log.Error().Err(errors.ErrBadConfig).Int("id", id).Interface("extensions.sync.registries[id]",
log.Error().Err(zerr.ErrBadConfig).Int("id", id).Interface("extensions.sync.registries[id]",
config.Extensions.Sync.Registries[id]).Msg("retryDelay is required when using maxRetries")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
if regCfg.Content != nil {
@ -902,11 +909,11 @@ func validateSync(config *config.Config, log zlog.Logger) error {
}
if content.StripPrefix && !strings.Contains(content.Prefix, "/*") && content.Destination == "/" {
log.Error().Err(errors.ErrBadConfig).
log.Error().Err(zerr.ErrBadConfig).
Interface("sync content", content).
Msg("sync config: can not use stripPrefix true and destination '/' without using glob patterns in prefix")
return errors.ErrBadConfig
return zerr.ErrBadConfig
}
}
}

View file

@ -91,7 +91,7 @@ func TestServe(t *testing.T) {
Convey("config with missing rootDir", func(c C) {
rootDir := t.TempDir()
// missing storag config should result in an error in Controller.Init()
// missing storage config should result in an error in Controller.Init()
content := []byte(`{
"distSpecVersion": "1.1.0-dev",
"http": {

View file

@ -10,7 +10,8 @@ import (
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
zotErrors "zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
)
//nolint:dupl
@ -21,7 +22,7 @@ func NewSearchCommand(searchService SearchService) *cobra.Command {
var isSpinner, verifyTLS, verbose, debug bool
imageCmd := &cobra.Command{
searchCmd := &cobra.Command{
Use: "search [config-name]",
Short: "Search images and their tags",
Long: `Search repos or images
@ -36,13 +37,14 @@ Example:
zli search --subject repo@sha256:f9a0981...
zli search --subject repo:tag
`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
configPath := path.Join(home + "/.zot")
configPath := path.Join(home, "/.zot")
if servURL == "" {
if len(args) > 0 {
urlFromConfig, err := getConfigValue(configPath, args[0], "url")
@ -53,12 +55,12 @@ Example:
}
if urlFromConfig == "" {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
servURL = urlFromConfig
} else {
return zotErrors.ErrNoURLProvided
return zerr.ErrNoURLProvided
}
}
@ -107,27 +109,32 @@ Example:
},
}
setupSearchFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose, &debug)
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter)
setupSearchFlags(searchCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose, &debug)
searchCmd.SetUsageTemplate(searchCmd.UsageTemplate() + usageFooter)
return imageCmd
searchCmd.AddCommand(NewSearchQueryCommand(searchService))
searchCmd.AddCommand(NewSearchSubjectCommand(searchService))
return searchCmd
}
func setupSearchFlags(imageCmd *cobra.Command, searchImageParams map[string]*string,
func setupSearchFlags(searchCmd *cobra.Command, searchImageParams map[string]*string,
servURL, user, outputFormat *string, verbose *bool, debug *bool,
) {
searchImageParams["query"] = imageCmd.Flags().StringP("query", "q", "",
searchImageParams["query"] = searchCmd.Flags().StringP("query", "q", "",
"Specify what repo or image(repo:tag) to be searched")
searchImageParams["subject"] = imageCmd.Flags().StringP("subject", "s", "",
searchImageParams["subject"] = searchCmd.Flags().StringP("subject", "s", "",
"List all referrers for this subject. The subject can be specified by tag(repo:tag) or by digest"+
"(repo@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`)
imageCmd.Flags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]")
imageCmd.Flags().BoolVar(verbose, "verbose", false, "Show verbose output")
imageCmd.Flags().BoolVar(debug, "debug", false, "Show debug output")
searchCmd.Flags().StringVar(servURL, cmdflags.URLFlag, "", "Specify zot server URL if config-name is not mentioned")
searchCmd.Flags().StringVarP(user, cmdflags.UserFlag, "u", "",
`User Credentials of zot server in "username:password" format`)
searchCmd.PersistentFlags().StringVarP(outputFormat, cmdflags.OutputFormatFlag, "f", "",
"Specify output format [text/json/yaml]")
searchCmd.PersistentFlags().BoolVar(verbose, cmdflags.VerboseFlag, false, "Show verbose output")
searchCmd.PersistentFlags().BoolVar(debug, cmdflags.DebugFlag, false, "Show debug output")
}
func globalSearch(searchConfig searchConfig) error {
@ -150,5 +157,5 @@ func globalSearch(searchConfig searchConfig) error {
}
}
return zotErrors.ErrInvalidFlagsCombination
return zerr.ErrInvalidFlagsCombination
}

View file

@ -20,12 +20,6 @@ import (
"zotregistry.io/zot/pkg/test"
)
func ref[T any](input T) *T {
ref := input
return &ref
}
const (
customArtTypeV1 = "application/custom.art.type.v1"
customArtTypeV2 = "application/custom.art.type.v2"
@ -428,7 +422,7 @@ func TestFormatsReferrersCLI(t *testing.T) {
So(err, ShouldBeNil)
Convey("JSON format", func() {
args := []string{"reftest", "--output", "json", "--subject", repo + "@" + image.DigestStr()}
args := []string{"reftest", "--format", "json", "--subject", repo + "@" + image.DigestStr()}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
baseURL))
@ -446,7 +440,7 @@ func TestFormatsReferrersCLI(t *testing.T) {
fmt.Println(buff.String())
})
Convey("YAML format", func() {
args := []string{"reftest", "--output", "yaml", "--subject", repo + "@" + image.DigestStr()}
args := []string{"reftest", "--format", "yaml", "--subject", repo + "@" + image.DigestStr()}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
baseURL))
@ -464,7 +458,7 @@ func TestFormatsReferrersCLI(t *testing.T) {
fmt.Println(buff.String())
})
Convey("Invalid format", func() {
args := []string{"reftest", "--output", "invalid_format", "--subject", repo + "@" + image.DigestStr()}
args := []string{"reftest", "--format", "invalid_format", "--subject", repo + "@" + image.DigestStr()}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`,
baseURL))
@ -488,7 +482,7 @@ func TestReferrersCLIErrors(t *testing.T) {
cmd := NewSearchCommand(new(searchService))
Convey("no url provided", func() {
args := []string{"reftest", "--output", "invalid", "--query", "repo/alpine"}
args := []string{"reftest", "--format", "invalid", "--query", "repo/alpine"}
configPath := makeConfigFile(`{"configs":[{"_name":"reftest","showspinner":false}]}`)
@ -594,3 +588,9 @@ func TestReferrersCLIErrors(t *testing.T) {
})
})
}
func ref[T any](input T) *T {
ref := input
return &ref
}

View file

@ -17,6 +17,7 @@ import (
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/cli/cmdflags"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/test"
)
@ -240,7 +241,7 @@ func TestFormatsSearchCLI(t *testing.T) {
cmd := NewSearchCommand(new(searchService))
Convey("JSON format", func() {
args := []string{"searchtest", "--output", "json", "--query", "repo/alpine"}
args := []string{"searchtest", "--format", "json", "--query", "repo/alpine"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
baseURL))
@ -257,7 +258,7 @@ func TestFormatsSearchCLI(t *testing.T) {
})
Convey("YAML format", func() {
args := []string{"searchtest", "--output", "yaml", "--query", "repo/alpine"}
args := []string{"searchtest", "--format", "yaml", "--query", "repo/alpine"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
baseURL))
@ -274,7 +275,7 @@ func TestFormatsSearchCLI(t *testing.T) {
})
Convey("Invalid format", func() {
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"}
args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
baseURL))
@ -296,7 +297,7 @@ func TestSearchCLIErrors(t *testing.T) {
cmd := NewSearchCommand(new(searchService))
Convey("no url provided", func() {
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"}
args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"}
configPath := makeConfigFile(`{"configs":[{"_name":"searchtest","showspinner":false}]}`)
@ -311,7 +312,7 @@ func TestSearchCLIErrors(t *testing.T) {
})
Convey("getConfigValue", func() {
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"}
args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"}
configPath := makeConfigFile(`bad-json`)
@ -358,7 +359,7 @@ func TestSearchCLIErrors(t *testing.T) {
})
Convey("url from config is empty", func() {
args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"}
args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"}
configPath := makeConfigFile(`{"configs":[{"_name":"searchtest", "url":"", "showspinner":false}]}`)
@ -402,3 +403,142 @@ func TestSearchCLIErrors(t *testing.T) {
})
})
}
func TestSearchCommandGQL(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands without gql", t, func() {
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
Convey("query", func() {
args := []string{"query", "repo/al"}
cmd := NewSearchCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "")
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, "repo 8c25cb36 false 100B")
So(actual, ShouldContainSubstring, "repo 100B 2010-01-01 01:01:01 +0000 UTC 0 0")
})
Convey("query command errors", func() {
// no url
args := []string{"repo/al"}
cmd := NewSearchQueryCommand(mockService{})
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("subject", func() {
err := test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "tag")
So(err, ShouldBeNil)
args := []string{"subject", "repo:tag"}
cmd := NewSearchCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "")
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, "ArtifactType 100 B Digest")
})
Convey("subject command errors", func() {
// no url
args := []string{"repo:tag"}
cmd := NewSearchSubjectCommand(mockService{})
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
})
}
func TestSearchCommandREST(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("commands without gql", t, func() {
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
Convey("query", func() {
args := []string{"query", "repo/al"}
cmd := NewSearchCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "")
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("subject", func() {
err := test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "tag")
So(err, ShouldBeNil)
args := []string{"subject", "repo:tag"}
cmd := NewSearchCommand(mockService{})
cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "")
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,
"art.type 100 B sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a")
})
})
}

459
pkg/cli/search_functions.go Normal file
View file

@ -0,0 +1,459 @@
//go:build search
// +build search
package cli
import (
"context"
"fmt"
"math"
"strings"
"sync"
"time"
zerr "zotregistry.io/zot/errors"
zcommon "zotregistry.io/zot/pkg/common"
)
func SearchAllImages(config searchConfig) error {
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.getAllImages(ctx, config, username, password, imageErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh)
wg.Wait()
select {
case err := <-errCh:
return err
default:
return nil
}
}
func SearchAllImagesGQL(config searchConfig) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getImagesGQL(ctx, config, username, password, "")
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.Results {
imageListData = append(imageListData, imageStruct(image))
}
return printImageResult(config, imageListData)
}
func SearchImageByName(config searchConfig, image string) error {
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.getImageByName(ctx, config, username, password,
image, imageErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh)
wg.Wait()
select {
case err := <-errCh:
return err
default:
return nil
}
}
func SearchImageByNameGQL(config searchConfig, imageName string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
repo, tag := zcommon.GetImageDirAndTag(imageName)
imageList, err := config.searchService.getImagesGQL(ctx, config, username, password, repo)
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.Results {
if tag == "" || image.Tag == tag {
imageListData = append(imageListData, imageStruct(image))
}
}
return printImageResult(config, imageListData)
}
func SearchImagesByDigest(config searchConfig, digest string) error {
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,
digest, imageErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh)
wg.Wait()
select {
case err := <-errCh:
return err
default:
return nil
}
}
func SearchDerivedImageListGQL(config searchConfig, derivedImage string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getDerivedImageListGQL(ctx, config, username,
password, derivedImage)
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.DerivedImageList.Results {
imageListData = append(imageListData, imageStruct(image))
}
return printImageResult(config, imageListData)
}
func SearchBaseImageListGQL(config searchConfig, baseImage string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getBaseImageListGQL(ctx, config, username,
password, baseImage)
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.BaseImageList.Results {
imageListData = append(imageListData, imageStruct(image))
}
return printImageResult(config, imageListData)
}
func SearchImagesForDigestGQL(config searchConfig, digest string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
imageList, err := config.searchService.getImagesForDigestGQL(ctx, config, username, password, digest)
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.Results {
imageListData = append(imageListData, imageStruct(image))
}
if err := printImageResult(config, imageListData); err != nil {
return err
}
return nil
}
func SearchCVEForImageGQL(config searchConfig, image, searchedCveID string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var cveList *cveResult
err := zcommon.RetryWithContext(ctx, func(attempt int, retryIn time.Duration) error {
var err error
cveList, err = config.searchService.getCveByImageGQL(ctx, config, username, password, image, searchedCveID)
if err != nil {
if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) {
cancel()
return err
}
fmt.Fprintf(config.resultWriter,
"[warning] CVE DB is not ready [%d] - retry in %d seconds\n", attempt, int(retryIn.Seconds()))
}
return err
}, maxRetries, cveDBRetryInterval*time.Second)
if err != nil {
return err
}
if len(cveList.Data.CVEListForImage.CVEList) == 0 {
fmt.Fprint(config.resultWriter, "No CVEs found for image\n")
return nil
}
var builder strings.Builder
if *config.outputFormat == defaultOutputFormat || *config.outputFormat == "" {
printCVETableHeader(&builder, *config.verbose, 0, 0, 0)
fmt.Fprint(config.resultWriter, builder.String())
}
out, err := cveList.string(*config.outputFormat)
if err != nil {
return err
}
fmt.Fprint(config.resultWriter, out)
return nil
}
func SearchImagesByCVEIDGQL(config searchConfig, repo, cveid string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var imageList *zcommon.ImagesForCve
err := zcommon.RetryWithContext(ctx, func(attempt int, retryIn time.Duration) error {
var err error
imageList, err = config.searchService.getTagsForCVEGQL(ctx, config, username, password,
repo, cveid)
if err != nil {
if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) {
cancel()
return err
}
fmt.Fprintf(config.resultWriter,
"[warning] CVE DB is not ready [%d] - retry in %d seconds\n", attempt, int(retryIn.Seconds()))
}
return err
}, maxRetries, cveDBRetryInterval*time.Second)
if err != nil {
return err
}
imageListData := []imageStruct{}
for _, image := range imageList.Results {
imageListData = append(imageListData, imageStruct(image))
}
return printImageResult(config, imageListData)
}
func SearchFixedTagsGQL(config searchConfig, repo, cveid string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var fixedTags *zcommon.ImageListWithCVEFixedResponse
err := zcommon.RetryWithContext(ctx, func(attempt int, retryIn time.Duration) error {
var err error
fixedTags, err = config.searchService.getFixedTagsForCVEGQL(ctx, config, username, password,
repo, cveid)
if err != nil {
if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) {
cancel()
return err
}
fmt.Fprintf(config.resultWriter,
"[warning] CVE DB is not ready [%d] - retry in %d seconds\n", attempt, int(retryIn.Seconds()))
}
return err
}, maxRetries, cveDBRetryInterval*time.Second)
if err != nil {
return err
}
imageList := make([]imageStruct, 0, len(fixedTags.Results))
for _, image := range fixedTags.Results {
imageList = append(imageList, imageStruct(image))
}
return printImageResult(config, imageList)
}
func GlobalSearchGQL(config searchConfig, query string) error {
username, password := getUsernameAndPassword(*config.user)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
globalSearchResult, err := config.searchService.globalSearchGQL(ctx, config, username, password, query)
if err != nil {
return err
}
imagesList := []imageStruct{}
for _, image := range globalSearchResult.Images {
imagesList = append(imagesList, imageStruct(image))
}
reposList := []repoStruct{}
for _, repo := range globalSearchResult.Repos {
reposList = append(reposList, repoStruct(repo))
}
if err := printImageResult(config, imagesList); err != nil {
return err
}
return printRepoResults(config, reposList)
}
func SearchReferrersGQL(config searchConfig, subject string) error {
username, password := getUsernameAndPassword(*config.user)
repo, ref, refIsTag, err := zcommon.GetRepoReference(subject)
if err != nil {
return err
}
digest := ref
if refIsTag {
digest, err = fetchImageDigest(repo, ref, username, password, config)
if err != nil {
return err
}
}
response, err := config.searchService.getReferrersGQL(context.Background(), config, username, password, repo, digest)
if err != nil {
return err
}
referrersList := referrersResult(response.Referrers)
maxArtifactTypeLen := math.MinInt
for _, referrer := range referrersList {
if maxArtifactTypeLen < len(referrer.ArtifactType) {
maxArtifactTypeLen = len(referrer.ArtifactType)
}
}
printReferrersTableHeader(config, config.resultWriter, maxArtifactTypeLen)
return printReferrersResult(config, referrersList, maxArtifactTypeLen)
}
func SearchReferrers(config searchConfig, subject string) error {
username, password := getUsernameAndPassword(*config.user)
repo, ref, refIsTag, err := zcommon.GetRepoReference(subject)
if err != nil {
return err
}
digest := ref
if refIsTag {
digest, err = fetchImageDigest(repo, ref, username, password, config)
if err != nil {
return err
}
}
referrersList, err := config.searchService.getReferrers(context.Background(), config, username, password,
repo, digest)
if err != nil {
return err
}
maxArtifactTypeLen := math.MinInt
for _, referrer := range referrersList {
if maxArtifactTypeLen < len(referrer.ArtifactType) {
maxArtifactTypeLen = len(referrer.ArtifactType)
}
}
printReferrersTableHeader(config, config.resultWriter, maxArtifactTypeLen)
return printReferrersResult(config, referrersList, maxArtifactTypeLen)
}
func SearchRepos(config searchConfig) error {
username, password := getUsernameAndPassword(*config.user)
repoErr := make(chan stringResult)
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go config.searchService.getRepos(ctx, config, username, password, repoErr, &wg)
wg.Add(1)
errCh := make(chan error, 1)
go collectResults(config, &wg, repoErr, cancel, printImageTableHeader, errCh)
wg.Wait()
select {
case err := <-errCh:
return err
default:
return nil
}
}

View file

@ -0,0 +1,761 @@
//go:build search
// +build search
//
//nolint:dupl
package cli //nolint:testpackage
import (
"bytes"
"context"
"io"
"os"
"regexp"
"strings"
"sync"
"testing"
"time"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
"github.com/spf13/cobra"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
"zotregistry.io/zot/pkg/common"
)
func TestSearchAllImages(t *testing.T) {
Convey("SearchAllImages", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getAllImagesFn: func(ctx context.Context, config searchConfig, username, password string,
channel chan stringResult, wtgrp *sync.WaitGroup,
) {
str, err := getMockImageStruct().stringPlainText(10, 10, 10, false)
channel <- stringResult{StrValue: str, Err: err}
},
})
err := SearchAllImages(searchConfig)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
}
func TestSearchAllImagesGQL(t *testing.T) {
Convey("SearchAllImagesGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string,
) (*common.ImageListResponse, error) {
return &common.ImageListResponse{ImageList: common.ImageList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
}}, nil
},
})
err := SearchAllImagesGQL(searchConfig)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchAllImagesGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string,
) (*common.ImageListResponse, error) {
return &common.ImageListResponse{ImageList: common.ImageList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
}}, zerr.ErrInjected
},
})
err := SearchAllImagesGQL(searchConfig)
So(err, ShouldNotBeNil)
})
}
func TestSearchImageByName(t *testing.T) {
Convey("SearchImageByName", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImageByNameFn: func(ctx context.Context, config searchConfig, username string, password string, imageName string,
channel chan stringResult, wtgrp *sync.WaitGroup,
) {
str, err := getMockImageStruct().stringPlainText(10, 10, 10, false)
channel <- stringResult{StrValue: str, Err: err}
},
})
err := SearchImageByName(searchConfig, "repo")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchImageByName error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImageByNameFn: func(ctx context.Context, config searchConfig, username string, password string, imageName string,
channel chan stringResult, wtgrp *sync.WaitGroup,
) {
channel <- stringResult{StrValue: "", Err: zerr.ErrInjected}
},
})
err := SearchImageByName(searchConfig, "repo")
So(err, ShouldNotBeNil)
})
}
func TestSearchImageByNameGQL(t *testing.T) {
Convey("SearchImageByNameGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string,
) (*common.ImageListResponse, error) {
return &common.ImageListResponse{ImageList: common.ImageList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
}}, nil
},
})
err := SearchImageByNameGQL(searchConfig, "repo")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchImageByNameGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string,
) (*common.ImageListResponse, error) {
return &common.ImageListResponse{ImageList: common.ImageList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
}}, zerr.ErrInjected
},
})
err := SearchImageByNameGQL(searchConfig, "repo")
So(err, ShouldNotBeNil)
})
}
func TestSearchImagesByDigest(t *testing.T) {
Convey("SearchImagesByDigest", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesByDigestFn: func(ctx context.Context, config searchConfig, username string, password string, digest string,
rch chan stringResult, wtgrp *sync.WaitGroup,
) {
str, err := getMockImageStruct().stringPlainText(10, 10, 10, false)
rch <- stringResult{StrValue: str, Err: err}
},
})
err := SearchImagesByDigest(searchConfig, godigest.FromString("str").String())
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchImagesByDigest error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesByDigestFn: func(ctx context.Context, config searchConfig, username string, password string, digest string,
rch chan stringResult, wtgrp *sync.WaitGroup,
) {
rch <- stringResult{StrValue: "", Err: zerr.ErrInjected}
},
})
err := SearchImagesByDigest(searchConfig, godigest.FromString("str").String())
So(err, ShouldNotBeNil)
})
}
func TestSearchDerivedImageListGQL(t *testing.T) {
Convey("SearchDerivedImageListGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getDerivedImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
derivedImage string) (*common.DerivedImageListResponse, error,
) {
return &common.DerivedImageListResponse{DerivedImageList: common.DerivedImageList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{
getMockImageSummary(),
},
},
}}, nil
},
})
err := SearchDerivedImageListGQL(searchConfig, "repo:tag")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchDerivedImageListGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getDerivedImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
derivedImage string) (*common.DerivedImageListResponse, error,
) {
return &common.DerivedImageListResponse{DerivedImageList: common.DerivedImageList{
PaginatedImagesResult: common.PaginatedImagesResult{Results: []common.ImageSummary{}},
}}, zerr.ErrInjected
},
})
err := SearchDerivedImageListGQL(searchConfig, "repo:tag")
So(err, ShouldNotBeNil)
})
}
func TestSearchBaseImageListGQL(t *testing.T) {
Convey("SearchBaseImageListGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getBaseImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
derivedImage string) (*common.BaseImageListResponse, error,
) {
return &common.BaseImageListResponse{BaseImageList: common.BaseImageList{
PaginatedImagesResult: common.PaginatedImagesResult{Results: []common.ImageSummary{
getMockImageSummary(),
}},
}}, nil
},
})
err := SearchBaseImageListGQL(searchConfig, "repo:tag")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchBaseImageListGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getBaseImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
derivedImage string) (*common.BaseImageListResponse, error,
) {
return &common.BaseImageListResponse{BaseImageList: common.BaseImageList{
PaginatedImagesResult: common.PaginatedImagesResult{Results: []common.ImageSummary{}},
}}, zerr.ErrInjected
},
})
err := SearchBaseImageListGQL(searchConfig, "repo:tag")
So(err, ShouldNotBeNil)
})
}
func TestSearchImagesForDigestGQL(t *testing.T) {
Convey("SearchImagesForDigestGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesForDigestGQLFn: func(ctx context.Context, config searchConfig, username string,
password string, digest string) (*common.ImagesForDigest, error,
) {
return &common.ImagesForDigest{ImagesForDigestList: common.ImagesForDigestList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
}}, nil
},
})
err := SearchImagesForDigestGQL(searchConfig, "digest")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchImagesForDigestGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getImagesForDigestGQLFn: func(ctx context.Context, config searchConfig, username string,
password string, digest string) (*common.ImagesForDigest, error,
) {
return &common.ImagesForDigest{ImagesForDigestList: common.ImagesForDigestList{
PaginatedImagesResult: common.PaginatedImagesResult{},
}}, zerr.ErrInjected
},
})
err := SearchImagesForDigestGQL(searchConfig, "digest")
So(err, ShouldNotBeNil)
})
}
func TestSearchCVEForImageGQL(t *testing.T) {
Convey("SearchCVEForImageGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
imageName string, searchedCVE string) (*cveResult, error,
) {
return &cveResult{
Data: cveData{
CVEListForImage: cveListForImage{
CVEList: []cve{
{
ID: "dummyCVEID",
Description: "Description of the CVE",
Title: "Title of that CVE",
Severity: "HIGH",
PackageList: []packageList{
{
Name: "packagename",
FixedVersion: "fixedver",
InstalledVersion: "installedver",
},
},
},
},
},
},
}, nil
},
})
err := SearchCVEForImageGQL(searchConfig, "repo-test", "dummyCVEID")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "dummyCVEID HIGH Title of that CVE")
})
Convey("SearchCVEForImageGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username string, password string,
imageName string, searchedCVE string) (*cveResult, error,
) {
return &cveResult{}, zerr.ErrInjected
},
})
err := SearchCVEForImageGQL(searchConfig, "repo-test", "dummyCVEID")
So(err, ShouldNotBeNil)
})
}
func TestSearchImagesByCVEIDGQL(t *testing.T) {
Convey("SearchImagesByCVEIDGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*common.ImagesForCve, error,
) {
return &common.ImagesForCve{
ImagesForCVEList: common.ImagesForCVEList{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{
getMockImageSummary(),
},
},
},
}, nil
},
})
err := SearchImagesByCVEIDGQL(searchConfig, "repo", "CVE-12345")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchImagesByCVEIDGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*common.ImagesForCve, error,
) {
return &common.ImagesForCve{
ImagesForCVEList: common.ImagesForCVEList{
PaginatedImagesResult: common.PaginatedImagesResult{},
},
}, zerr.ErrInjected
},
})
err := SearchImagesByCVEIDGQL(searchConfig, "repo", "CVE-12345")
So(err, ShouldNotBeNil)
})
}
func TestSearchFixedTagsGQL(t *testing.T) {
Convey("SearchFixedTagsGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getFixedTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*common.ImageListWithCVEFixedResponse, error,
) {
return &common.ImageListWithCVEFixedResponse{
ImageListWithCVEFixed: common.ImageListWithCVEFixed{
PaginatedImagesResult: common.PaginatedImagesResult{
Results: []common.ImageSummary{getMockImageSummary()},
},
},
}, nil
},
})
err := SearchFixedTagsGQL(searchConfig, "repo", "CVE-12345")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B")
})
Convey("SearchFixedTagsGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getFixedTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password,
imageName, cveID string) (*common.ImageListWithCVEFixedResponse, error,
) {
return &common.ImageListWithCVEFixedResponse{
ImageListWithCVEFixed: common.ImageListWithCVEFixed{
PaginatedImagesResult: common.PaginatedImagesResult{},
},
}, zerr.ErrInjected
},
})
err := SearchFixedTagsGQL(searchConfig, "repo", "CVE-12345")
So(err, ShouldNotBeNil)
})
}
func TestSearchReferrersGQL(t *testing.T) {
Convey("SearchReferrersGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getReferrersGQLFn: func(ctx context.Context, config searchConfig, username, password,
repo, digest string) (*common.ReferrersResp, error,
) {
return &common.ReferrersResp{
ReferrersResult: common.ReferrersResult{
Referrers: []common.Referrer{{
MediaType: ispec.MediaTypeImageManifest,
Size: 100,
ArtifactType: "art.type",
Digest: godigest.FromString("123").String(),
}},
},
}, nil
},
})
err := SearchReferrersGQL(searchConfig, "repo@"+godigest.FromString("str").String())
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring,
"art.type 100 B sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3")
})
Convey("SearchReferrersGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getReferrersGQLFn: func(ctx context.Context, config searchConfig, username, password,
repo, digest string) (*common.ReferrersResp, error,
) {
return &common.ReferrersResp{}, zerr.ErrInjected
},
})
err := SearchReferrersGQL(searchConfig, "repo@"+godigest.FromString("str").String())
So(err, ShouldNotBeNil)
})
}
func TestGlobalSearchGQL(t *testing.T) {
Convey("GlobalSearchGQL", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
globalSearchGQLFn: func(ctx context.Context, config searchConfig, username, password,
query string) (*common.GlobalSearch, error,
) {
return &common.GlobalSearch{
Repos: []common.RepoSummary{{
Name: "repo",
Size: "100",
LastUpdated: time.Date(2010, 1, 1, 1, 1, 1, 0, time.UTC),
}},
}, nil
},
})
err := GlobalSearchGQL(searchConfig, "repo")
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring,
"repo ")
})
Convey("GlobalSearchGQL error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
globalSearchGQLFn: func(ctx context.Context, config searchConfig, username, password,
query string) (*common.GlobalSearch, error,
) {
return &common.GlobalSearch{}, zerr.ErrInjected
},
})
err := GlobalSearchGQL(searchConfig, "repo")
So(err, ShouldNotBeNil)
})
}
func TestSearchReferrers(t *testing.T) {
Convey("SearchReferrers", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getReferrersFn: func(ctx context.Context, config searchConfig, username string, password string,
repo string, digest string) (referrersResult, error,
) {
return referrersResult([]common.Referrer{
{
MediaType: ispec.MediaTypeImageManifest,
Size: 100,
ArtifactType: "art.type",
Digest: godigest.FromString("123").String(),
},
}), nil
},
})
err := SearchReferrers(searchConfig, "repo@"+godigest.FromString("str").String())
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring,
"art.type 100 B sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3")
})
Convey("SearchReferrers error", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{
getReferrersFn: func(ctx context.Context, config searchConfig, username string, password string,
repo string, digest string) (referrersResult, error,
) {
return referrersResult{}, zerr.ErrInjected
},
})
err := SearchReferrers(searchConfig, "repo@"+godigest.FromString("str").String())
So(err, ShouldNotBeNil)
})
}
func TestSearchRepos(t *testing.T) {
Convey("SearchRepos", t, func() {
buff := bytes.NewBufferString("")
searchConfig := getMockSearchConfig(buff, mockService{})
err := SearchRepos(searchConfig)
So(err, ShouldBeNil)
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
actual := strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo1")
So(actual, ShouldContainSubstring, "repo2")
})
}
func getMockSearchConfig(buff *bytes.Buffer, mockService mockService) searchConfig {
return searchConfig{
resultWriter: buff,
user: ref(""),
searchService: mockService,
servURL: ref("http://127.0.0.1:8000"),
outputFormat: ref(""),
verifyTLS: ref(false),
fixedFlag: ref(false),
verbose: ref(false),
debug: ref(false),
}
}
func getMockImageStruct() imageStruct {
return imageStruct(common.ImageSummary{
RepoName: "repo", Tag: "tag",
MediaType: ispec.MediaTypeImageManifest,
Digest: godigest.FromString("str").String(),
Size: "100",
Manifests: []common.ManifestSummary{{
Size: "100",
Platform: common.Platform{Os: "os", Arch: "arch"},
Digest: godigest.FromString("str").String(),
ConfigDigest: godigest.FromString("str").String(),
}},
})
}
func getMockImageSummary() common.ImageSummary {
return common.ImageSummary{
RepoName: "repo", Tag: "tag",
MediaType: ispec.MediaTypeImageManifest,
Digest: godigest.FromString("str").String(),
Size: "100",
Manifests: []common.ManifestSummary{{
Size: "100",
Platform: common.Platform{Os: "os", Arch: "arch"},
Digest: godigest.FromString("str").String(),
ConfigDigest: godigest.FromString("str").String(),
}},
}
}
func TestUtils(t *testing.T) {
Convey("Utils", t, func() {
ok := haveSameArgs(field{"query", []struct {
Name string "json:\"name\""
}{
{Name: "arg1"}, {Name: "arg2"},
}}, GQLQuery{
Name: "query", Args: []string{"arg1"},
})
So(ok, ShouldBeFalse)
ok = haveSameArgs(field{"query", []struct {
Name string "json:\"name\""
}{
{Name: "arg1"}, {Name: "arg2"},
}}, GQLQuery{
Name: "query", Args: []string{"arg1", "arg3"},
})
So(ok, ShouldBeFalse)
err := containsGQLQueryWithParams(
[]field{
{Name: "query"},
},
[]typeInfo{},
GQLQuery{Name: "other-name"},
)
So(err, ShouldNotBeNil)
})
Convey("GetConfigOptions", t, func() {
// no flags
cmd := &cobra.Command{}
isSpinner, verifyTLS := GetCliConfigOptions(cmd)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
// bad showspinner
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":"bad", "verify-tls": false}]}`)
cmd = &cobra.Command{}
cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "")
isSpinner, verifyTLS = GetCliConfigOptions(cmd)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
os.Remove(configPath)
// bad verify-tls
configPath = makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false, "verify-tls": "bad"}]}`)
cmd = &cobra.Command{}
cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "")
isSpinner, verifyTLS = GetCliConfigOptions(cmd)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
os.Remove(configPath)
})
Convey("GetServerURLFromFlags", t, func() {
cmd := &cobra.Command{}
cmd.Flags().String(cmdflags.URLFlag, "url", "")
url, err := GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "url")
So(err, ShouldBeNil)
// err no config or url
cmd = &cobra.Command{}
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "")
So(err, ShouldNotBeNil)
// err ulr from config is empty
configPath := makeConfigFile(`{"configs":[{"_name":"imagetest"}]}`)
cmd = &cobra.Command{}
cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "")
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "")
So(err, ShouldNotBeNil)
os.Remove(configPath)
// err reading the server url from config
configPath = makeConfigFile("{}")
cmd = &cobra.Command{}
cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "")
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "")
So(err, ShouldNotBeNil)
os.Remove(configPath)
})
Convey("CheckExtEndPointQuery", t, func() {
// invalid url
err := CheckExtEndPointQuery(searchConfig{
user: ref(""),
servURL: ref("bad-url"),
})
So(err, ShouldNotBeNil)
// good url but no connection
err = CheckExtEndPointQuery(searchConfig{
user: ref(""),
servURL: ref("http://127.0.0.1:5000"),
verifyTLS: ref(false),
debug: ref(false),
resultWriter: io.Discard,
})
So(err, ShouldNotBeNil)
})
}

109
pkg/cli/search_sub_cmd.go Normal file
View file

@ -0,0 +1,109 @@
//go:build search
// +build search
package cli
import (
"fmt"
godigest "github.com/opencontainers/go-digest"
"github.com/spf13/cobra"
zerr "zotregistry.io/zot/errors"
zcommon "zotregistry.io/zot/pkg/common"
)
func NewSearchSubjectCommand(searchService SearchService) *cobra.Command {
imageCmd := &cobra.Command{
Use: "subject [repo:tag]|[repo@digest]",
Short: "List all referrers for this subject.",
Long: `List all referrers for this subject. The subject can be specified by tag(repo:tag) or by digest" +
"(repo@digest)`,
Example: `# For referrers search specify the referred subject using it's full digest or tag:
zli search subject "repo@sha256:f9a0981..."
zli search subject "repo:tag"`,
Args: OneImageWithRefArg,
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
if err := CheckExtEndPointQuery(searchConfig, ReferrersQuery()); err == nil {
return SearchReferrersGQL(searchConfig, args[0])
} else {
return SearchReferrers(searchConfig, args[0])
}
},
}
return imageCmd
}
func NewSearchQueryCommand(searchService SearchService) *cobra.Command {
imageCmd := &cobra.Command{
Use: "query",
Short: "Fuzzy search for repos and their tags.",
Long: "Fuzzy search for repos and their tags.",
Example: `# For repo search specify a substring of the repo name without the tag
zli search query "test/repo"
# For image search specify the full repo name followed by the tag or a prefix of the tag.
zli search query "test/repo:2.1."
# To search all tags in all repos.
zli search query ":"`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
// special format for searching all images and tags
if args[0] == ":" {
err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery())
if err != nil {
return fmt.Errorf("%w: '%s'", err, ImageListQuery().Name)
}
return SearchAllImagesGQL(searchConfig)
}
if err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery()); err != nil {
return fmt.Errorf("%w: '%s'", err, CVEListForImageQuery().Name)
}
return GlobalSearchGQL(searchConfig, args[0])
},
}
return imageCmd
}
func OneImageWithRefArg(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
image := args[0]
if dir, ref, _ := zcommon.GetImageDirAndReference(image); dir == "" || ref == "" {
return zerr.ErrInvalidRepoRefFormat
}
return nil
}
func OneDigestArg(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
digest := args[0]
if _, err := godigest.Parse(digest); err != nil {
return err
}
return nil
}

View file

@ -15,7 +15,7 @@ import (
"github.com/briandowns/spinner"
zotErrors "zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common"
)
@ -326,7 +326,7 @@ func (search imagesByDigestSearcherGQL) search(config searchConfig) (bool, error
defer cancel()
imageList, err := config.searchService.getImagesByDigestGQL(ctx, config, username, password, *config.params["digest"])
imageList, err := config.searchService.getImagesForDigestGQL(ctx, config, username, password, *config.params["digest"])
if err != nil {
return true, err
}
@ -770,7 +770,7 @@ func (search globalSearcherREST) search(config searchConfig) (bool, error) {
return false, nil
}
return true, fmt.Errorf("search extension is not enabled: %w", zotErrors.ErrExtensionNotEnabled)
return true, fmt.Errorf("search extension is not enabled: %w", zerr.ErrExtensionNotEnabled)
}
func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult,
@ -813,7 +813,7 @@ func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan strin
config.spinner.stopSpinner()
cancel()
errCh <- zotErrors.ErrCLITimeout
errCh <- zerr.ErrCLITimeout
return
}

View file

@ -20,7 +20,7 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"gopkg.in/yaml.v2"
zotErrors "zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/common"
)
@ -34,13 +34,13 @@ const (
type SearchService interface { //nolint:interfacebloat
getImagesGQL(ctx context.Context, config searchConfig, username, password string,
imageName string) (*common.ImageListResponse, error)
getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string,
getImagesForDigestGQL(ctx context.Context, config searchConfig, username, password string,
digest string) (*common.ImagesForDigest, error)
getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
imageName string, searchedCVE string) (*cveResult, error)
getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string,
digest string) (*common.ImagesForCve, error)
getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, repo,
cveID string) (*common.ImagesForCve, error)
getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
cveID string) (*common.ImageListWithCVEFixedResponse, error)
@ -260,7 +260,7 @@ func (service searchService) getImagesGQL(ctx context.Context, config searchConf
return result, nil
}
func (service searchService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string,
func (service searchService) getImagesForDigestGQL(ctx context.Context, config searchConfig, username, password string,
digest string,
) (*common.ImagesForDigest, error) {
query := fmt.Sprintf(`
@ -354,7 +354,7 @@ func (service searchService) getCveByImageGQL(ctx context.Context, config search
}
func (service searchService) getTagsForCVEGQL(ctx context.Context, config searchConfig,
username, password, imageName, cveID string,
username, password, repo, cveID string,
) (*common.ImagesForCve, error) {
query := fmt.Sprintf(`
{
@ -387,9 +387,21 @@ func (service searchService) getTagsForCVEGQL(ctx context.Context, config search
return nil, errResult
}
if repo == "" {
return result, nil
}
filteredResults := &common.ImagesForCve{}
for _, image := range result.Results {
if image.RepoName == repo {
filteredResults.Results = append(filteredResults.Results, image)
}
}
return filteredResults, nil
}
func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig,
username, password, imageName, cveID string,
) (*common.ImageListWithCVEFixedResponse, error) {
@ -537,7 +549,9 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
) {
defer wtgrp.Done()
tagListEndpoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/tags/list", imageName))
repo, imageTag := common.GetImageDirAndTag(imageName)
tagListEndpoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/tags/list", repo))
if err != nil {
if isContextDone(ctx) {
return
@ -570,9 +584,17 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag
continue
}
shouldMatchTag := imageTag != ""
matchesTag := tag == imageTag
// when the tag is empty we match everything
if shouldMatchTag && !matchesTag {
continue
}
wtgrp.Add(1)
go addManifestCallToPool(ctx, config, pool, username, password, imageName, tag, rch, wtgrp)
go addManifestCallToPool(ctx, config, pool, username, password, repo, tag, rch, wtgrp)
}
}
@ -787,7 +809,7 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config
go rlim.startRateLimiter(ctx)
for _, image := range result.Results {
if !strings.EqualFold(imageName, image.RepoName) {
if imageName != "" && !strings.EqualFold(imageName, image.RepoName) {
continue
}
@ -1438,7 +1460,7 @@ func addManifestToTable(table *tablewriter.Table, imageName, tagName string, man
platform += offset
}
minifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "")
manifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "")
configDigestStr := ellipsize(configDigest.Encoded(), configWidth, "")
imgSize, _ := strconv.ParseUint(manifest.Size, 10, 64)
size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis)
@ -1447,7 +1469,7 @@ func addManifestToTable(table *tablewriter.Table, imageName, tagName string, man
row[colImageNameIndex] = imageName
row[colTagIndex] = tagName
row[colDigestIndex] = minifestDigestStr
row[colDigestIndex] = manifestDigestStr
row[colPlatformIndex] = platform
row[colSizeIndex] = size
row[colIsSignedIndex] = strconv.FormatBool(isSigned)
@ -1487,23 +1509,23 @@ func addManifestToTable(table *tablewriter.Table, imageName, tagName string, man
return nil
}
func getPlatformStr(platf common.Platform) string {
if platf.Arch == "" && platf.Os == "" {
func getPlatformStr(platform common.Platform) string {
if platform.Arch == "" && platform.Os == "" {
return ""
}
platform := platf.Os
fullPlatform := platform.Os
if platf.Arch != "" {
platform = platform + "/" + platf.Arch
platform = strings.Trim(platform, "/")
if platform.Arch != "" {
fullPlatform = fullPlatform + "/" + platform.Arch
fullPlatform = strings.Trim(fullPlatform, "/")
if platf.Variant != "" {
platform = platform + "/" + platf.Variant
if platform.Variant != "" {
fullPlatform = fullPlatform + "/" + platform.Variant
}
}
return platform
return fullPlatform
}
func (img imageStruct) stringJSON() (string, error) {
@ -1534,12 +1556,12 @@ type catalogResponse struct {
func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) {
if !isURL(serverURL) {
return "", zotErrors.ErrInvalidURL
return "", zerr.ErrInvalidURL
}
newURL, err := url.Parse(serverURL)
if err != nil {
return "", zotErrors.ErrInvalidURL
return "", zerr.ErrInvalidURL
}
newURL, _ = newURL.Parse(endPoint)

View file

@ -111,7 +111,7 @@ func GetRepoReference(repo string) (string, string, bool, error) {
return repoName, digest, false, nil
}
// GetFullImageName returns the formated string for the given repo/tag or repo/digest.
// GetFullImageName returns the formatted string for the given repo/tag or repo/digest.
func GetFullImageName(repo, ref string) string {
if IsTag(ref) {
return repo + ":" + ref
@ -129,3 +129,7 @@ func IsDigest(ref string) bool {
func IsTag(ref string) bool {
return !IsDigest(ref)
}
func CheckIsCorrectRepoNameFormat(repo string) bool {
return !strings.ContainsAny(repo, ":@")
}

24
pkg/common/retry.go Normal file
View file

@ -0,0 +1,24 @@
package common
import (
"context"
"time"
)
func RetryWithContext(ctx context.Context, operation func(attempt int, retryIn time.Duration) error, maxRetries int,
delay time.Duration,
) error {
err := operation(1, delay)
for attempt := 1; err != nil && attempt < maxRetries; attempt++ {
select {
case <-time.After(delay):
case <-ctx.Done():
return err
}
err = operation(attempt+1, delay)
}
return err
}

View file

@ -9,7 +9,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/exporter/api"
)
@ -66,12 +66,12 @@ func loadConfiguration(config *api.Config, configPath string) {
metaData := &mapstructure.Metadata{}
if err := viper.Unmarshal(&config, metadataConfig(metaData)); err != nil {
log.Error().Err(err).Msg("Error while unmarshalling new config")
log.Error().Err(err).Msg("Error while unmarshaling new config")
panic(err)
}
if len(metaData.Keys) == 0 || len(metaData.Unused) > 0 {
log.Error().Err(errors.ErrBadConfig).Msg("Bad configuration, retry writing it")
panic(errors.ErrBadConfig)
log.Error().Err(zerr.ErrBadConfig).Msg("Bad configuration, retry writing it")
panic(zerr.ErrBadConfig)
}
}

View file

@ -38,7 +38,7 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
if len(endpoints) > 0 {
extensions = append(extensions, distext.Extension{
Name: "_zot",
Name: constants.BaseExtension,
URL: "https://github.com/project-zot/zot/blob/" + config.ReleaseTag + "/pkg/extensions/_zot.md",
Description: "zot registry extensions",
Endpoints: endpoints,

View file

@ -30,7 +30,7 @@ type ImageAnnotations struct {
/*
OCI annotation/label with backwards compatibility
arg can be either lables or annotations
arg can be either labels or annotations
https://github.com/opencontainers/image-spec/blob/main/annotations.md.
*/
func GetAnnotationValue(annotations map[string]string, annotationKey, labelKey string) string {

View file

@ -61,7 +61,7 @@ func TestConvertErrors(t *testing.T) {
err = metaDB.SetRepoReference("repo1", "0.1.0", digest11, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
repoMetas, manifestMetaMap, _, err := metaDB.SearchRepos(context.Background(), "")
reposMeta, manifestMetaMap, _, err := metaDB.SearchRepos(context.Background(), "")
So(err, ShouldBeNil)
ctx := graphql.WithResponseContext(context.Background(),
@ -69,7 +69,7 @@ func TestConvertErrors(t *testing.T) {
_ = convert.RepoMeta2RepoSummary(
ctx,
repoMetas[0],
reposMeta[0],
manifestMetaMap,
map[string]mTypes.IndexData{},
convert.SkipQGLField{},
@ -286,7 +286,7 @@ func TestConvertErrors(t *testing.T) {
})
}
func TestUpdateLastUpdatedTimestam(t *testing.T) {
func TestUpdateLastUpdatedTimestamp(t *testing.T) {
Convey("Image summary is the first image checked for the repo", t, func() {
before := time.Time{}
after := time.Date(2023, time.April, 1, 11, 0, 0, 0, time.UTC)

View file

@ -44,7 +44,7 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta mTypes.RepoMetadata,
repoIsUserStarred = repoMeta.IsStarred // value specific to the current user
repoIsUserBookMarked = repoMeta.IsBookmarked // value specific to the current user
// map used to keep track of all blobs of a repo without dublicates as
// map used to keep track of all blobs of a repo without duplicates as
// some images may have the same layers
repoBlob2Size = make(map[string]int64, 10)
@ -140,7 +140,7 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta mTypes.RepoMetadata,
}
}
func PaginatedRepoMeta2RepoSummaries(ctx context.Context, repoMetas []mTypes.RepoMetadata,
func PaginatedRepoMeta2RepoSummaries(ctx context.Context, reposMeta []mTypes.RepoMetadata,
manifestMetaMap map[string]mTypes.ManifestMetadata, indexDataMap map[string]mTypes.IndexData,
skip SkipQGLField, cveInfo cveinfo.CveInfo, filter mTypes.Filter, pageInput pagination.PageInput,
) ([]*gql_generated.RepoSummary, zcommon.PageInfo, error) {
@ -149,7 +149,7 @@ func PaginatedRepoMeta2RepoSummaries(ctx context.Context, repoMetas []mTypes.Rep
return []*gql_generated.RepoSummary{}, zcommon.PageInfo{}, err
}
for _, repoMeta := range repoMetas {
for _, repoMeta := range reposMeta {
repoSummary := RepoMeta2RepoSummary(ctx, repoMeta, manifestMetaMap, indexDataMap, skip, cveInfo)
if RepoSumAcceptedByFilter(repoSummary, filter) {
@ -679,7 +679,7 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMetadata
isStarred = repoMeta.IsStarred // value specific to the current user
isBookmarked = repoMeta.IsBookmarked // value specific to the current user
// map used to keep track of all blobs of a repo without dublicates as
// map used to keep track of all blobs of a repo without duplicates as
// some images may have the same layers
repoBlob2Size = make(map[string]int64, 10)
@ -694,7 +694,7 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta mTypes.RepoMetadata
skip.Vulnerabilities, repoMeta, manifestMetaMap, indexDataMap, cveInfo)
if err != nil {
log.Error().Str("repository", repoName).Str("reference", tag).
Msg("metadb: erorr while converting descriptor for image")
Msg("metadb: error while converting descriptor for image")
continue
}

View file

@ -599,9 +599,9 @@ func TestCVESearch(t *testing.T) {
So(err, ShouldBeNil)
So(len(cveResult.ImgList.CVEResultForImage.CVEList), ShouldNotBeZeroValue)
cvid := cveResult.ImgList.CVEResultForImage.CVEList[0].ID
cveid := cveResult.ImgList.CVEResultForImage.CVEList[0].ID
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -610,7 +610,7 @@ func TestCVESearch(t *testing.T) {
So(err, ShouldBeNil)
So(len(imgListWithCVEFixed.Images), ShouldEqual, 0)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-cve-test\"){Results{RepoName%20LastUpdated}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-cve-test\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -618,7 +618,7 @@ func TestCVESearch(t *testing.T) {
So(err, ShouldBeNil)
So(len(imgListWithCVEFixed.Images), ShouldEqual, 0)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -635,7 +635,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noindex\"){Results{RepoName%20LastUpdated}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-noindex\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -643,7 +643,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-index\"){Results{RepoName%20LastUpdated}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-invalid-index\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -651,11 +651,11 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noblob\"){Results{RepoName%20LastUpdated}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-noblob\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-test\"){Results{RepoName%20LastUpdated}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-test\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -663,7 +663,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-blob\"){Results{RepoName%20LastUpdated}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-invalid-blob\"){Results{RepoName%20LastUpdated}}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
@ -732,7 +732,7 @@ func TestCVESearch(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 422)
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListForCVE(id:\"" + cvid + "\"){Results{RepoName%20Tag}}}")
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListForCVE(id:\"" + cveid + "\"){Results{RepoName%20Tag}}}")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
})
@ -1412,7 +1412,7 @@ func TestCVEStruct(t *testing.T) {
So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 0)
// Repo is not found, assume it is affetected by the CVE
// Repo is not found, assume it is affected by the CVE
// But we don't have enough of it's data to actually return it
tagList, err = cveInfo.GetImageListForCVE("repo100", "CVE100")
So(err, ShouldEqual, zerr.ErrRepoMetaNotFound)

View file

@ -123,7 +123,7 @@ func TestGlobalSearch(t *testing.T) {
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{
manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob1,
@ -134,7 +134,7 @@ func TestGlobalSearch(t *testing.T) {
},
}
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
},
}
@ -189,14 +189,14 @@ func TestGlobalSearch(t *testing.T) {
configBlob, err := json.Marshal(ispec.Image{})
So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{
manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: []byte("bad manifest blob"),
ConfigBlob: configBlob,
},
}
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
},
}
@ -262,14 +262,14 @@ func TestGlobalSearch(t *testing.T) {
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{
manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: []byte("bad config blob"),
},
}
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
},
}
@ -375,7 +375,7 @@ func TestGlobalSearch(t *testing.T) {
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{
manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob1,
@ -386,7 +386,7 @@ func TestGlobalSearch(t *testing.T) {
},
}
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
},
}
@ -505,7 +505,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
})
So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{
manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: []byte("bad manifest blob"),
ConfigBlob: configBlob1,
@ -516,7 +516,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
},
}
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
},
}
@ -601,7 +601,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{
manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob1,
@ -612,7 +612,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
},
}
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
},
}
Convey("MetaDB missing requestedPage", func() {
@ -776,7 +776,7 @@ func TestImageListForDigest(t *testing.T) {
So(err, ShouldBeNil)
manifestBlob := []byte("invalid")
manifestMetaDatas := map[string]mTypes.ManifestMetadata{
manifestsMetaData := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
@ -784,7 +784,7 @@ func TestImageListForDigest(t *testing.T) {
},
}
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
},
}
@ -820,7 +820,7 @@ func TestImageListForDigest(t *testing.T) {
configBlob, err := json.Marshal(ispec.ImageConfig{})
So(err, ShouldBeNil)
manifestMetaDatas := map[string]mTypes.ManifestMetadata{
manifestsMetaData := map[string]mTypes.ManifestMetadata{
manifestDigest: {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
@ -829,9 +829,9 @@ func TestImageListForDigest(t *testing.T) {
}
matchedTags := repos[0].Tags
for tag, manifestDescriptor := range repos[0].Tags {
if !filterFunc(repos[0], manifestMetaDatas[manifestDescriptor.Digest]) {
if !filterFunc(repos[0], manifestsMetaData[manifestDescriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetaDatas, manifestDescriptor.Digest)
delete(manifestsMetaData, manifestDescriptor.Digest)
continue
}
@ -839,7 +839,7 @@ func TestImageListForDigest(t *testing.T) {
repos[0].Tags = matchedTags
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
},
}
@ -898,7 +898,7 @@ func TestImageListForDigest(t *testing.T) {
})
So(err, ShouldBeNil)
manifestMetaDatas := map[string]mTypes.ManifestMetadata{
manifestsMetaData := map[string]mTypes.ManifestMetadata{
manifestDigest: {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
@ -908,9 +908,9 @@ func TestImageListForDigest(t *testing.T) {
matchedTags := repos[0].Tags
for tag, manifestDescriptor := range repos[0].Tags {
if !filterFunc(repos[0], manifestMetaDatas[manifestDescriptor.Digest]) {
if !filterFunc(repos[0], manifestsMetaData[manifestDescriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetaDatas, manifestDescriptor.Digest)
delete(manifestsMetaData, manifestDescriptor.Digest)
continue
}
@ -918,7 +918,7 @@ func TestImageListForDigest(t *testing.T) {
repos[0].Tags = matchedTags
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
},
}
@ -975,7 +975,7 @@ func TestImageListForDigest(t *testing.T) {
})
So(err, ShouldBeNil)
manifestMetaDatas := map[string]mTypes.ManifestMetadata{
manifestsMetaData := map[string]mTypes.ManifestMetadata{
manifestDigest: {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
@ -985,9 +985,9 @@ func TestImageListForDigest(t *testing.T) {
matchedTags := repos[0].Tags
for tag, manifestDescriptor := range repos[0].Tags {
if !filterFunc(repos[0], manifestMetaDatas[manifestDescriptor.Digest]) {
if !filterFunc(repos[0], manifestsMetaData[manifestDescriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetaDatas, manifestDescriptor.Digest)
delete(manifestsMetaData, manifestDescriptor.Digest)
continue
}
@ -995,7 +995,7 @@ func TestImageListForDigest(t *testing.T) {
repos[0].Tags = matchedTags
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
},
}
@ -1042,7 +1042,7 @@ func TestImageListForDigest(t *testing.T) {
},
}
manifestMetaDatas := map[string]mTypes.ManifestMetadata{
manifestsMetaData := map[string]mTypes.ManifestMetadata{
manifestDigest: {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
@ -1054,9 +1054,9 @@ func TestImageListForDigest(t *testing.T) {
matchedTags := repo.Tags
for tag, manifestDescriptor := range repo.Tags {
if !filterFunc(repo, manifestMetaDatas[manifestDescriptor.Digest]) {
if !filterFunc(repo, manifestsMetaData[manifestDescriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetaDatas, manifestDescriptor.Digest)
delete(manifestsMetaData, manifestDescriptor.Digest)
continue
}
@ -1065,7 +1065,7 @@ func TestImageListForDigest(t *testing.T) {
repos[i].Tags = matchedTags
}
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
},
}
@ -1111,7 +1111,7 @@ func TestImageListForDigest(t *testing.T) {
},
}
manifestMetaDatas := map[string]mTypes.ManifestMetadata{
manifestsMetaData := map[string]mTypes.ManifestMetadata{
manifestDigest: {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
@ -1123,9 +1123,9 @@ func TestImageListForDigest(t *testing.T) {
matchedTags := repo.Tags
for tag, manifestDescriptor := range repo.Tags {
if !filterFunc(repo, manifestMetaDatas[manifestDescriptor.Digest]) {
if !filterFunc(repo, manifestsMetaData[manifestDescriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetaDatas, manifestDescriptor.Digest)
delete(manifestsMetaData, manifestDescriptor.Digest)
continue
}
@ -1136,7 +1136,7 @@ func TestImageListForDigest(t *testing.T) {
repos = append(repos, repo)
}
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
},
}
@ -1429,7 +1429,7 @@ func TestImageList(t *testing.T) {
manifestBlob, err := json.Marshal(ispec.Manifest{})
So(err, ShouldBeNil)
manifestMetaDatas := map[string]mTypes.ManifestMetadata{
manifestsMetaData := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
@ -1442,21 +1442,21 @@ func TestImageList(t *testing.T) {
},
}
if !filterFunc(repos[0], manifestMetaDatas["digestTag1.0.1"]) {
if !filterFunc(repos[0], manifestsMetaData["digestTag1.0.1"]) {
return []mTypes.RepoMetadata{}, map[string]mTypes.ManifestMetadata{},
map[string]mTypes.IndexData{}, nil
}
return repos, manifestMetaDatas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMetaData, map[string]mTypes.IndexData{}, nil
},
}
limit := 1
ofset := 0
offset := 0
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := gql_generated.PageInput{
Limit: &limit,
Offset: &ofset,
Offset: &offset,
SortBy: &sortCriteria,
}
@ -1889,7 +1889,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
}
// Create metadb data for scannable image with vulnerabilities
// Create manifets metadata first
// Create manifest metadata first
timeStamp1 := time.Date(2008, 1, 1, 12, 0, 0, 0, time.UTC)
configBlob1, err := json.Marshal(ispec.Image{
@ -2076,7 +2076,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
},
"CVE2": {
ID: "CVE2",
Severity: "MEDIM",
Severity: "MEDIUM",
Title: "Title CVE2",
Description: "Description CVE2",
},
@ -2961,7 +2961,7 @@ func TestDerivedImageList(t *testing.T) {
})
So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{
manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
@ -3019,9 +3019,9 @@ func TestDerivedImageList(t *testing.T) {
matchedTags := repo.Tags
for tag, descriptor := range repo.Tags {
if !filterFunc(repo, manifestMetas[descriptor.Digest]) {
if !filterFunc(repo, manifestsMeta[descriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetas, descriptor.Digest)
delete(manifestsMeta, descriptor.Digest)
continue
}
@ -3030,7 +3030,7 @@ func TestDerivedImageList(t *testing.T) {
repos[i].Tags = matchedTags
}
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
},
}
@ -3249,7 +3249,7 @@ func TestBaseImageList(t *testing.T) {
})
So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{
manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
@ -3301,9 +3301,9 @@ func TestBaseImageList(t *testing.T) {
matchedTags := repo.Tags
for tag, descriptor := range repo.Tags {
if !filterFunc(repo, manifestMetas[descriptor.Digest]) {
if !filterFunc(repo, manifestsMeta[descriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetas, descriptor.Digest)
delete(manifestsMeta, descriptor.Digest)
continue
}
@ -3312,7 +3312,7 @@ func TestBaseImageList(t *testing.T) {
repos[i].Tags = matchedTags
}
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
},
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
@ -3416,7 +3416,7 @@ func TestBaseImageList(t *testing.T) {
})
So(err, ShouldBeNil)
manifestMetas := map[string]mTypes.ManifestMetadata{
manifestsMeta := map[string]mTypes.ManifestMetadata{
"digestTag1.0.1": {
ManifestBlob: manifestBlob,
ConfigBlob: configBlob,
@ -3467,9 +3467,9 @@ func TestBaseImageList(t *testing.T) {
matchedTags := repo.Tags
for tag, descriptor := range repo.Tags {
if !filterFunc(repo, manifestMetas[descriptor.Digest]) {
if !filterFunc(repo, manifestsMeta[descriptor.Digest]) {
delete(matchedTags, tag)
delete(manifestMetas, descriptor.Digest)
delete(manifestsMeta, descriptor.Digest)
continue
}
@ -3478,7 +3478,7 @@ func TestBaseImageList(t *testing.T) {
repos[i].Tags = matchedTags
}
return repos, manifestMetas, map[string]mTypes.IndexData{}, nil
return repos, manifestsMeta, map[string]mTypes.IndexData{}, nil
},
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
@ -3519,12 +3519,12 @@ func TestExpandedRepoInfo(t *testing.T) {
Digest: "goodIndexBadManifests",
MediaType: ispec.MediaTypeImageIndex,
},
"tagGoodIndex1GoodManfest": {
Digest: "goodIndexGoodManfest",
"tagGoodIndex1GoodManifest": {
Digest: "goodIndexGoodManifest",
MediaType: ispec.MediaTypeImageIndex,
},
"tagGoodIndex2GoodManfest": {
Digest: "goodIndexGoodManfest",
"tagGoodIndex2GoodManifest": {
Digest: "goodIndexGoodManifest",
MediaType: ispec.MediaTypeImageIndex,
},
},
@ -3569,7 +3569,7 @@ func TestExpandedRepoInfo(t *testing.T) {
return mTypes.IndexData{
IndexBlob: goodIndexBadManifestsBlob,
}, nil
case "goodIndexGoodManfest":
case "goodIndexGoodManifest":
return mTypes.IndexData{
IndexBlob: goodIndexGoodManifestBlob,
}, nil

View file

@ -6242,8 +6242,8 @@ func TestImageSummary(t *testing.T) {
WaitTillServerReady(baseURL)
manifestBlob, errMarsal := json.Marshal(manifest)
So(errMarsal, ShouldBeNil)
manifestBlob, errMarshal := json.Marshal(manifest)
So(errMarshal, ShouldBeNil)
So(manifestBlob, ShouldNotBeNil)
manifestDigest := godigest.FromBytes(manifestBlob)
repoName := "test-repo" //nolint:goconst
@ -6427,7 +6427,7 @@ func TestImageSummary(t *testing.T) {
})
}
func TestUplodingArtifactsWithDifferentMediaType(t *testing.T) {
func TestUploadingArtifactsWithDifferentMediaType(t *testing.T) {
Convey("", t, func() {
port := GetFreePort()
baseURL := GetBaseURL(port)

View file

@ -20,7 +20,7 @@ import (
"github.com/rs/zerolog"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/extensions/config"
syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
"zotregistry.io/zot/pkg/extensions/lint"
@ -92,7 +92,7 @@ func TestSyncInternal(t *testing.T) {
repositoryReference = fmt.Sprintf("%s/%s:tagged", host, testImage)
_, err = parseRepositoryReference(repositoryReference)
So(err, ShouldEqual, errors.ErrInvalidRepositoryName)
So(err, ShouldEqual, zerr.ErrInvalidRepositoryName)
repositoryReference = fmt.Sprintf("http://%s/%s", host, testImage)
_, err = parseRepositoryReference(repositoryReference)
@ -341,7 +341,7 @@ func TestLocalRegistry(t *testing.T) {
registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, mocks.MetaDBMock{
SetRepoReferenceFn: func(repo, Reference string, manifestDigest godigest.Digest, mediaType string) error {
if Reference == "1.0" {
return errors.ErrRepoMetaNotFound
return zerr.ErrRepoMetaNotFound
}
return nil
@ -355,7 +355,7 @@ func TestLocalRegistry(t *testing.T) {
Convey("trigger metaDB error on image manifest in CommitImage()", func() {
registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, mocks.MetaDBMock{
SetRepoReferenceFn: func(repo, Reference string, manifestDigest godigest.Digest, mediaType string) error {
return errors.ErrRepoMetaNotFound
return zerr.ErrRepoMetaNotFound
},
}, log)

View file

@ -1156,7 +1156,7 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFunc,
repoMeta.IsStarred = zcommon.Contains(userStars, repoMeta.Name)
matchedTags := make(map[string]mTypes.Descriptor)
// take all manifestMetas
// take all manifestsMeta
for tag, descriptor := range repoMeta.Tags {
switch descriptor.MediaType {
case ispec.MediaTypeImageManifest:

View file

@ -5,7 +5,7 @@ import (
glob "github.com/bmatcuk/doublestar/v4" //nolint:gci
"zotregistry.io/zot/errors"
zerr "zotregistry.io/zot/errors"
)
type Key int
@ -41,7 +41,7 @@ func GetAccessControlContext(ctx context.Context) (*AccessControlContext, error)
if authCtx := ctx.Value(authzCtxKey); authCtx != nil {
acCtx, ok := authCtx.(AccessControlContext)
if !ok {
return nil, errors.ErrBadType
return nil, zerr.ErrBadType
}
return &acCtx, nil
@ -110,7 +110,7 @@ func GetAuthnMiddlewareContext(ctx context.Context) (*AuthnMiddlewareContext, er
if authnMiddlewareCtx := ctx.Value(authnMiddlewareCtxKey); authnMiddlewareCtx != nil {
amCtx, ok := authnMiddlewareCtx.(AuthnMiddlewareContext)
if !ok {
return nil, errors.ErrBadType
return nil, zerr.ErrBadType
}
return &amCtx, nil

View file

@ -54,7 +54,7 @@ import (
"zotregistry.io/zot/pkg/storage"
storageCommon "zotregistry.io/zot/pkg/storage/common"
"zotregistry.io/zot/pkg/storage/local"
"zotregistry.io/zot/pkg/storage/types"
stypes "zotregistry.io/zot/pkg/storage/types"
"zotregistry.io/zot/pkg/test/inject"
"zotregistry.io/zot/pkg/test/mocks"
)
@ -1437,12 +1437,12 @@ func ListNotarySignatures(reference string, tdir string) ([]godigest.Digest, err
sigRepo := notreg.NewRepository(remoteRepo)
artifectDesc, err := sigRepo.Resolve(ctx, reference)
artifactDesc, err := sigRepo.Resolve(ctx, reference)
if err != nil {
return signatures, err
}
err = sigRepo.ListSignatures(ctx, artifectDesc, func(signatureManifests []ispec.Descriptor) error {
err = sigRepo.ListSignatures(ctx, artifactDesc, func(signatureManifests []ispec.Descriptor) error {
for _, sigManifestDesc := range signatureManifests {
signatures = append(signatures, sigManifestDesc.Digest)
}
@ -2067,11 +2067,11 @@ func GetDefaultLayersBlobs() [][]byte {
}
}
func GetDefaultImageStore(rootDir string, log zLog.Logger) types.ImageStore {
func GetDefaultImageStore(rootDir string, log zLog.Logger) stypes.ImageStore {
return local.NewImageStore(rootDir, false, time.Hour, false, false, log,
monitoring.NewMetricsServer(false, log),
mocks.MockedLint{
LintFn: func(repo string, manifestDigest godigest.Digest, imageStore types.ImageStore) (bool, error) {
LintFn: func(repo string, manifestDigest godigest.Digest, imageStore stypes.ImageStore) (bool, error) {
return true, nil
},
},
@ -2084,3 +2084,20 @@ func GetDefaultStoreController(rootDir string, log zLog.Logger) storage.StoreCon
DefaultStore: GetDefaultImageStore(rootDir, log),
}
}
func RemoveLocalStorageContents(imageStore stypes.ImageStore) error {
repos, err := imageStore.GetRepositories()
if err != nil {
return err
}
for _, repo := range repos {
// take just the first path
err = os.RemoveAll(filepath.Join(imageStore.RootDir(), filepath.SplitList(repo)[0]))
if err != nil {
return err
}
}
return nil
}

View file

@ -56,7 +56,7 @@ type ConfigBuilder interface {
RandomConfig() ManifestBuilder
}
// VulnerableConfigBuilder abstracts specifying the config of an vulnerage OCI image.
// VulnerableConfigBuilder abstracts specifying the config of an vulnerable OCI image.
// Keeping the RootFS field consistent with the vulnerable layers.
type VulnerableConfigBuilder interface {
// VulnerableConfig sets the given config while keeping the correct RootFS values for the