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

feat(cli): add sort-by flag to sub commands (#1768)

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
LaurentiuNiculae 2023-09-14 20:51:17 +03:00 committed by GitHub
parent c210e3f377
commit aae8b7b4e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 714 additions and 98 deletions

View file

@ -160,4 +160,6 @@ var (
ErrFileAlreadyClosed = errors.New("storageDriver: file already closed")
ErrFileAlreadyCommitted = errors.New("storageDriver: file already committed")
ErrInvalidOutputFormat = errors.New("cli: invalid output format")
ErrFlagValueUnsupported = errors.New("supported values ")
ErrUnknownSubcommand = errors.New("cli: unknown subcommand")
)

View file

@ -149,11 +149,22 @@ func doHTTPRequest(req *http.Request, verifyTLS bool, debug bool,
return resp.Header, nil
}
func isURL(str string) bool {
u, err := url.Parse(str)
func validateURL(str string) error {
parsedURL, err := url.Parse(str)
if err != nil {
if strings.Contains(err.Error(), "first path segment in URL cannot contain colon") {
return fmt.Errorf("%w: scheme not provided (ex: https://)", zerr.ErrInvalidURL)
}
return err == nil && u.Scheme != "" && u.Host != ""
} // from https://stackoverflow.com/a/55551215
return err
}
if parsedURL.Scheme == "" || parsedURL.Host == "" {
return fmt.Errorf("%w: scheme not provided (ex: https://)", zerr.ErrInvalidURL)
}
return nil
}
type requestsPool struct {
jobs chan *httpJob

View file

@ -98,7 +98,7 @@ func TestTLSWithAuth(t *testing.T) {
imageCmd.SetArgs(args)
err = imageCmd.Execute()
So(err, ShouldNotBeNil)
So(imageBuff.String(), ShouldContainSubstring, "invalid URL format")
So(imageBuff.String(), ShouldContainSubstring, "scheme not provided")
args = []string{"list", "--config", "imagetest"}
configPath = makeConfigFile(

View file

@ -1,5 +1,13 @@
package cmdflags
import (
"fmt"
"strings"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/common"
)
const (
URLFlag = "url"
ConfigFlag = "config"
@ -10,4 +18,144 @@ const (
VersionFlag = "version"
DebugFlag = "debug"
SearchedCVEID = "cve-id"
SortByFlag = "sort-by"
)
const (
SortByRelevance = "relevance"
SortByUpdateTime = "update-time"
SortByAlphabeticAsc = "alpha-asc"
SortByAlphabeticDsc = "alpha-dsc"
SortBySeverity = "severity"
)
const stringType = "string"
func ImageListSortOptions() []string {
return []string{SortByUpdateTime, SortByAlphabeticAsc, SortByAlphabeticDsc}
}
func ImageListSortOptionsStr() string {
return strings.Join(ImageListSortOptions(), ", ")
}
func ImageSearchSortOptions() []string {
return []string{SortByRelevance, SortByUpdateTime, SortByAlphabeticAsc, SortByAlphabeticDsc}
}
func ImageSearchSortOptionsStr() string {
return strings.Join(ImageSearchSortOptions(), ", ")
}
func CVEListSortOptions() []string {
return []string{SortByAlphabeticAsc, SortByAlphabeticDsc, SortBySeverity}
}
func CVEListSortOptionsStr() string {
return strings.Join(CVEListSortOptions(), ", ")
}
func RepoListSortOptions() []string {
return []string{SortByAlphabeticAsc, SortByAlphabeticDsc}
}
func RepoListSortOptionsStr() string {
return strings.Join(RepoListSortOptions(), ", ")
}
func Flag2SortCriteria(sortBy string) string {
switch sortBy {
case SortByRelevance:
return "RELEVANCE"
case SortByUpdateTime:
return "UPDATE_TIME"
case SortByAlphabeticAsc:
return "ALPHABETIC_ASC"
case SortByAlphabeticDsc:
return "ALPHABETIC_DSC"
case SortBySeverity:
return "SEVERITY"
default:
return "BAD_SORT_CRITERIA"
}
}
type CVEListSortFlag string
func (e *CVEListSortFlag) String() string {
return string(*e)
}
func (e *CVEListSortFlag) Set(val string) error {
if !common.Contains(CVEListSortOptions(), val) {
return fmt.Errorf("%w %s", zerr.ErrFlagValueUnsupported, CVEListSortOptionsStr())
}
*e = CVEListSortFlag(val)
return nil
}
func (e *CVEListSortFlag) Type() string {
return stringType
}
type ImageListSortFlag string
func (e *ImageListSortFlag) String() string {
return string(*e)
}
func (e *ImageListSortFlag) Set(val string) error {
if !common.Contains(ImageListSortOptions(), val) {
return fmt.Errorf("%w %s", zerr.ErrFlagValueUnsupported, ImageListSortOptionsStr())
}
*e = ImageListSortFlag(val)
return nil
}
func (e *ImageListSortFlag) Type() string {
return stringType
}
type ImageSearchSortFlag string
func (e *ImageSearchSortFlag) String() string {
return string(*e)
}
func (e *ImageSearchSortFlag) Set(val string) error {
if !common.Contains(ImageSearchSortOptions(), val) {
return fmt.Errorf("%w %s", zerr.ErrFlagValueUnsupported, ImageSearchSortOptionsStr())
}
*e = ImageSearchSortFlag(val)
return nil
}
func (e *ImageSearchSortFlag) Type() string {
return stringType
}
type RepoListSortFlag string
func (e *RepoListSortFlag) String() string {
return string(*e)
}
func (e *RepoListSortFlag) Set(val string) error {
if !common.Contains(RepoListSortOptions(), val) {
return fmt.Errorf("%w %s", zerr.ErrFlagValueUnsupported, RepoListSortOptionsStr())
}
*e = RepoListSortFlag(val)
return nil
}
func (e *RepoListSortFlag) Type() string {
return stringType
}

View file

@ -0,0 +1,45 @@
package cmdflags_test
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
. "zotregistry.io/zot/pkg/cli/cmdflags"
gql_gen "zotregistry.io/zot/pkg/extensions/search/gql_generated"
)
func TestSortFlagsMapping(t *testing.T) {
// We do this to not import the whole gql_gen in the CLI
Convey("Make sure the sort-by values map correctly to the gql enum type", t, func() {
So(Flag2SortCriteria(SortByRelevance), ShouldResemble, string(gql_gen.SortCriteriaRelevance))
So(Flag2SortCriteria(SortByUpdateTime), ShouldResemble, string(gql_gen.SortCriteriaUpdateTime))
So(Flag2SortCriteria(SortByAlphabeticAsc), ShouldResemble, string(gql_gen.SortCriteriaAlphabeticAsc))
So(Flag2SortCriteria(SortByAlphabeticDsc), ShouldResemble, string(gql_gen.SortCriteriaAlphabeticDsc))
So(Flag2SortCriteria(SortBySeverity), ShouldResemble, string(gql_gen.SortCriteriaSeverity))
})
}
func TestSortFlags(t *testing.T) {
Convey("Flags", t, func() {
cveSortFlag := CVEListSortFlag("")
err := cveSortFlag.Set("bad-flag")
So(err, ShouldNotBeNil)
imageListSortFlag := ImageListSortFlag("")
err = imageListSortFlag.Set("bad-flag")
So(err, ShouldNotBeNil)
imageSearchSortFlag := ImageSearchSortFlag("")
err = imageSearchSortFlag.Set("bad-flag")
So(err, ShouldNotBeNil)
repoListSearchFlag := RepoListSortFlag("")
err = repoListSearchFlag.Set("bad-flag")
So(err, ShouldNotBeNil)
})
Convey("Flag2SortCriteria", t, func() {
So(Flag2SortCriteria("bad-flag"), ShouldResemble, "BAD_SORT_CRITERIA")
})
}

View file

@ -248,8 +248,8 @@ func addConfig(configPath, configName, url string) error {
return err
}
if !isURL(url) {
return zerr.ErrInvalidURL
if err := validateURL(url); err != nil {
return err
}
if configNameExists(configs, configName) {

View file

@ -160,7 +160,7 @@ func TestConfigCmdMain(t *testing.T) {
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zerr.ErrInvalidURL)
So(strings.Contains(err.Error(), zerr.ErrInvalidURL.Error()), ShouldBeTrue)
})
Convey("Test remove config entry successfully", t, func() {

View file

@ -910,6 +910,159 @@ func TestServerCVEResponse(t *testing.T) {
})
}
func TestCVESort(t *testing.T) {
rootDir := t.TempDir()
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},
CVE: &extconf.CVEConfig{
UpdateInterval: 2,
Trivy: &extconf.TrivyConfig{
DBRepository: "ghcr.io/project-zot/trivy-db",
},
},
},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = rootDir
image1 := test.CreateRandomImage()
storeController := test.GetDefaultStoreController(rootDir, ctlr.Log)
err := test.WriteImageToFileSystem(image1, "repo", "tag", storeController)
if err != nil {
t.FailNow()
}
ctx := context.Background()
if err := ctlr.Init(ctx); err != nil {
panic(err)
}
severities := map[string]int{
"UNKNOWN": 0,
"LOW": 1,
"MEDIUM": 2,
"HIGH": 3,
"CRITICAL": 4,
}
ctlr.CveInfo = cveinfo.BaseCveInfo{
Log: ctlr.Log,
MetaDB: mocks.MetaDBMock{},
Scanner: mocks.CveScannerMock{
CompareSeveritiesFn: func(severity1, severity2 string) int {
return severities[severity2] - severities[severity1]
},
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
return map[string]cvemodel.CVE{
"CVE-2023-1255": {
ID: "CVE-2023-1255",
Severity: "LOW",
Title: "Input buffer over-read in AES-XTS implementation and testing",
},
"CVE-2023-2650": {
ID: "CVE-2023-2650",
Severity: "MEDIUM",
Title: "Possible DoS translating ASN.1 object identifier and executer",
},
"CVE-2023-2975": {
ID: "CVE-2023-2975",
Severity: "HIGH",
Title: "AES-SIV cipher implementation contains a bug that can break",
},
"CVE-2023-3446": {
ID: "CVE-2023-3446",
Severity: "CRITICAL",
Title: "Excessive time spent checking DH keys and parenthesis",
},
"CVE-2023-3817": {
ID: "CVE-2023-3817",
Severity: "MEDIUM",
Title: "Excessive time spent checking DH q parameter and arguments",
},
}, nil
},
},
}
go func() {
if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}()
defer ctlr.Shutdown()
test.WaitTillServerReady(baseURL)
space := regexp.MustCompile(`\s+`)
Convey("test sorting", t, func() {
args := []string{"list", "repo:tag", "--sort-by", "severity", "--url", baseURL}
cmd := NewCVECommand(new(searchService))
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, ShouldResemble,
"ID SEVERITY TITLE "+
"CVE-2023-3446 CRITICAL Excessive time spent checking DH keys and par... "+
"CVE-2023-2975 HIGH AES-SIV cipher implementation contains a bug ... "+
"CVE-2023-2650 MEDIUM Possible DoS translating ASN.1 object identif... "+
"CVE-2023-3817 MEDIUM Excessive time spent checking DH q parameter ... "+
"CVE-2023-1255 LOW Input buffer over-read in AES-XTS implementat...")
args = []string{"list", "repo:tag", "--sort-by", "alpha-asc", "--url", baseURL}
cmd = NewCVECommand(new(searchService))
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, ShouldResemble,
"ID SEVERITY TITLE "+
"CVE-2023-1255 LOW Input buffer over-read in AES-XTS implementat... "+
"CVE-2023-2650 MEDIUM Possible DoS translating ASN.1 object identif... "+
"CVE-2023-2975 HIGH AES-SIV cipher implementation contains a bug ... "+
"CVE-2023-3446 CRITICAL Excessive time spent checking DH keys and par... "+
"CVE-2023-3817 MEDIUM Excessive time spent checking DH q parameter ...")
args = []string{"list", "repo:tag", "--sort-by", "alpha-dsc", "--url", baseURL}
cmd = NewCVECommand(new(searchService))
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, ShouldResemble,
"ID SEVERITY TITLE "+
"CVE-2023-3817 MEDIUM Excessive time spent checking DH q parameter ... "+
"CVE-2023-3446 CRITICAL Excessive time spent checking DH keys and par... "+
"CVE-2023-2975 HIGH AES-SIV cipher implementation contains a bug ... "+
"CVE-2023-2650 MEDIUM Possible DoS translating ASN.1 object identif... "+
"CVE-2023-1255 LOW Input buffer over-read in AES-XTS implementat...")
})
}
func TestCVECommandGQL(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)

View file

@ -14,6 +14,7 @@ func NewCVECommand(searchService SearchService) *cobra.Command {
Use: "cve [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`,
RunE: ShowSuggestionsIfUnknownCommand,
}
cvesCmd.SetUsageTemplate(cvesCmd.UsageTemplate() + usageFooter)

View file

@ -19,7 +19,10 @@ const (
)
func NewCveForImageCommand(searchService SearchService) *cobra.Command {
var searchedCVEID string
var (
searchedCVEID string
cveListSortFlag = cmdflags.CVEListSortFlag(cmdflags.SortBySeverity)
)
cveForImageCmd := &cobra.Command{
Use: "list [repo:tag]|[repo@digest]",
@ -44,12 +47,17 @@ func NewCveForImageCommand(searchService SearchService) *cobra.Command {
}
cveForImageCmd.Flags().StringVar(&searchedCVEID, cmdflags.SearchedCVEID, "", "Search for a specific CVE by name/id")
cveForImageCmd.Flags().Var(&cveListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.CVEListSortOptionsStr()))
return cveForImageCmd
}
func NewImagesByCVEIDCommand(searchService SearchService) *cobra.Command {
var repo string
var (
repo string
imageListSortFlag = cmdflags.ImageListSortFlag(cmdflags.SortByAlphabeticAsc)
)
imagesByCVEIDCmd := &cobra.Command{
Use: "affected [cveId]",
@ -84,15 +92,19 @@ func NewImagesByCVEIDCommand(searchService SearchService) *cobra.Command {
}
imagesByCVEIDCmd.Flags().StringVar(&repo, "repo", "", "Search for a specific CVE by name/id")
imagesByCVEIDCmd.Flags().Var(&imageListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.ImageListSortOptionsStr()))
return imagesByCVEIDCmd
}
func NewFixedTagsCommand(searchService SearchService) *cobra.Command {
imageListSortFlag := cmdflags.ImageListSortFlag(cmdflags.SortByAlphabeticAsc)
fixedTagsCmd := &cobra.Command{
Use: "fixed [repo] [cveId]",
Short: "List tags where a CVE is fixedRetryWithContext",
Long: `List tags where a CVE is fixedRetryWithContext`,
Short: "List tags where a CVE is fixed",
Long: `List tags where a CVE is fixed`,
Args: func(cmd *cobra.Command, args []string) error {
const argCount = 2
@ -124,5 +136,8 @@ func NewFixedTagsCommand(searchService SearchService) *cobra.Command {
},
}
fixedTagsCmd.Flags().Var(&imageListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.ImageListSortOptionsStr()))
return fixedTagsCmd
}

View file

@ -24,6 +24,7 @@ func NewImageCommand(searchService SearchService) *cobra.Command {
Use: "image [command]",
Short: "List images hosted on the zot registry",
Long: `List images hosted on the zot registry`,
RunE: ShowSuggestionsIfUnknownCommand,
}
imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter)

View file

@ -131,7 +131,7 @@ func TestSearchImageCmd(t *testing.T) {
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(err, ShouldEqual, zerr.ErrInvalidURL)
So(strings.Contains(err.Error(), zerr.ErrInvalidURL.Error()), ShouldBeTrue)
So(buff.String(), ShouldContainSubstring, "invalid URL format")
})
@ -1357,6 +1357,80 @@ func runDisplayIndexTests(baseURL string) {
})
}
func TestImagesSortFlag(t *testing.T) {
rootDir := t.TempDir()
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},
CVE: nil,
},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = rootDir
image1 := test.CreateImageWith().DefaultLayers().
ImageConfig(ispec.Image{Created: test.DateRef(2010, 1, 1, 1, 1, 1, 0, time.UTC)}).Build()
image2 := test.CreateImageWith().DefaultLayers().
ImageConfig(ispec.Image{Created: test.DateRef(2020, 1, 1, 1, 1, 1, 0, time.UTC)}).Build()
storeController := test.GetDefaultStoreController(rootDir, ctlr.Log)
err := test.WriteImageToFileSystem(image1, "a-repo", "tag1", storeController)
if err != nil {
t.FailNow()
}
err = test.WriteImageToFileSystem(image2, "b-repo", "tag2", storeController)
if err != nil {
t.FailNow()
}
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("Sorting", t, func() {
args := []string{"list", "--sort-by", "alpha-asc", "--url", baseURL}
cmd := NewImageCommand(new(searchService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
str := buff.String()
So(strings.Index(str, "a-repo"), ShouldBeLessThan, strings.Index(str, "b-repo"))
args = []string{"list", "--sort-by", "alpha-dsc", "--url", baseURL}
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
str = buff.String()
So(strings.Index(str, "b-repo"), ShouldBeLessThan, strings.Index(str, "a-repo"))
args = []string{"list", "--sort-by", "update-time", "--url", baseURL}
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
str = buff.String()
So(err, ShouldBeNil)
So(strings.Index(str, "b-repo"), ShouldBeLessThan, strings.Index(str, "a-repo"))
})
}
func TestImagesCommandGQL(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
@ -2620,6 +2694,7 @@ func getTestSearchConfig(url string, searchService SearchService) searchConfig {
return searchConfig{
searchService: searchService,
sortBy: "alpha-asc",
servURL: url,
user: user,
outputFormat: outputFormat,

View file

@ -4,6 +4,8 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
zerr "zotregistry.io/zot/errors"
@ -12,7 +14,9 @@ import (
)
func NewImageListCommand(searchService SearchService) *cobra.Command {
return &cobra.Command{
imageListSortFlag := cmdflags.ImageListSortFlag(cmdflags.SortByAlphabeticAsc)
cmd := &cobra.Command{
Use: "list",
Short: "List all images",
Long: "List all images",
@ -30,10 +34,18 @@ func NewImageListCommand(searchService SearchService) *cobra.Command {
return SearchAllImages(searchConfig)
},
}
cmd.Flags().Var(&imageListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.ImageListSortOptionsStr()))
return cmd
}
func NewImageCVEListCommand(searchService SearchService) *cobra.Command {
var searchedCVEID string
var (
searchedCVEID string
cveListSortFlag = cmdflags.CVEListSortFlag(cmdflags.SortBySeverity)
)
cmd := &cobra.Command{
Use: "cve [repo]|[repo-name:tag]|[repo-name@digest]",
@ -57,11 +69,15 @@ func NewImageCVEListCommand(searchService SearchService) *cobra.Command {
}
cmd.Flags().StringVar(&searchedCVEID, cmdflags.SearchedCVEID, "", "Search for a specific CVE by name/id")
cmd.Flags().Var(&cveListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.CVEListSortOptionsStr()))
return cmd
}
func NewImageDerivedCommand(searchService SearchService) *cobra.Command {
imageListSortFlag := cmdflags.ImageListSortFlag(cmdflags.SortByAlphabeticAsc)
cmd := &cobra.Command{
Use: "derived [repo-name:tag]|[repo-name@digest]",
Short: "List images that are derived from given image",
@ -81,10 +97,15 @@ func NewImageDerivedCommand(searchService SearchService) *cobra.Command {
},
}
cmd.Flags().Var(&imageListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.ImageListSortOptionsStr()))
return cmd
}
func NewImageBaseCommand(searchService SearchService) *cobra.Command {
imageListSortFlag := cmdflags.ImageListSortFlag(cmdflags.SortByAlphabeticAsc)
cmd := &cobra.Command{
Use: "base [repo-name:tag]|[repo-name@digest]",
Short: "List images that are base for the given image",
@ -104,10 +125,15 @@ func NewImageBaseCommand(searchService SearchService) *cobra.Command {
},
}
cmd.Flags().Var(&imageListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.ImageListSortOptionsStr()))
return cmd
}
func NewImageDigestCommand(searchService SearchService) *cobra.Command {
imageListSortFlag := cmdflags.ImageListSortFlag(cmdflags.SortByAlphabeticAsc)
cmd := &cobra.Command{
Use: "digest [digest]",
Short: "List images that contain a blob(manifest, config or layer) with the given digest",
@ -129,10 +155,15 @@ zli image digest sha256:8a1930f0...`,
},
}
cmd.Flags().Var(&imageListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.ImageListSortOptionsStr()))
return cmd
}
func NewImageNameCommand(searchService SearchService) *cobra.Command {
imageListSortFlag := cmdflags.ImageListSortFlag(cmdflags.SortByAlphabeticAsc)
cmd := &cobra.Command{
Use: "name [repo:tag]",
Short: "List image details by name",
@ -164,5 +195,8 @@ func NewImageNameCommand(searchService SearchService) *cobra.Command {
},
}
cmd.Flags().Var(&imageListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.ImageListSortOptionsStr()))
return cmd
}

View file

@ -16,6 +16,7 @@ func NewRepoCommand(searchService SearchService) *cobra.Command {
Use: "repo [config-name]",
Short: "List all repositories",
Long: `List all repositories`,
RunE: ShowSuggestionsIfUnknownCommand,
}
repoCmd.SetUsageTemplate(repoCmd.UsageTemplate() + usageFooter)

View file

@ -4,10 +4,16 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"zotregistry.io/zot/pkg/cli/cmdflags"
)
func NewListReposCommand(searchService SearchService) *cobra.Command {
repoListSortFlag := cmdflags.RepoListSortFlag(cmdflags.SortByAlphabeticAsc)
cmd := &cobra.Command{
Use: "list",
Short: "List all repositories",
@ -23,5 +29,8 @@ func NewListReposCommand(searchService SearchService) *cobra.Command {
},
}
cmd.Flags().Var(&repoListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.RepoListSortOptionsStr()))
return cmd
}

View file

@ -32,6 +32,11 @@ func TestReposCommand(t *testing.T) {
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
err := test.UploadImage(test.CreateRandomImage(), baseURL, "repo1", "tag1")
So(err, ShouldBeNil)
err = test.UploadImage(test.CreateRandomImage(), baseURL, "repo2", "tag2")
So(err, ShouldBeNil)
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"repostest","url":"%s","showspinner":false}]}`,
baseURL))
defer os.Remove(configPath)
@ -42,12 +47,55 @@ func TestReposCommand(t *testing.T) {
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
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")
args = []string{"list", "--sort-by", "alpha-dsc", "--config", "repostest"}
cmd = NewRepoCommand(new(searchService))
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
space = regexp.MustCompile(`\s+`)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo1")
So(actual, ShouldContainSubstring, "repo2")
So(strings.Index(actual, "repo2"), ShouldBeLessThan, strings.Index(actual, "repo1"))
args = []string{"list", "--sort-by", "alpha-asc", "--config", "repostest"}
cmd = NewRepoCommand(new(searchService))
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
space = regexp.MustCompile(`\s+`)
str = space.ReplaceAllString(buff.String(), " ")
actual = strings.TrimSpace(str)
So(actual, ShouldContainSubstring, "repo1")
So(actual, ShouldContainSubstring, "repo2")
So(strings.Index(actual, "repo1"), ShouldBeLessThan, strings.Index(actual, "repo2"))
})
}
func TestSuggestions(t *testing.T) {
Convey("Suggestions", t, func() {
space := regexp.MustCompile(`\s+`)
suggestion := ShowSuggestionsIfUnknownCommand(NewRepoCommand(mockService{}), []string{"bad-command"})
str := space.ReplaceAllString(suggestion.Error(), " ")
So(str, ShouldContainSubstring, "unknown subcommand")
suggestion = ShowSuggestionsIfUnknownCommand(NewRepoCommand(mockService{}), []string{"listt"})
str = space.ReplaceAllString(suggestion.Error(), " ")
So(str, ShouldContainSubstring, "Did you mean this? list")
})
}

View file

@ -11,9 +11,10 @@ import (
func NewSearchCommand(searchService SearchService) *cobra.Command {
searchCmd := &cobra.Command{
Use: "search [config-name]",
Use: "search [command]",
Short: "Search images and their tags",
Long: `Search repos or images`,
RunE: ShowSuggestionsIfUnknownCommand,
}
searchCmd.SetUsageTemplate(searchCmd.UsageTemplate() + usageFooter)

View file

@ -10,6 +10,7 @@ import (
"regexp"
"strings"
"testing"
"time"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
@ -862,3 +863,70 @@ func TestSearchCommandREST(t *testing.T) {
})
})
}
func TestSearchSort(t *testing.T) {
rootDir := t.TempDir()
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},
CVE: nil,
},
}
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = rootDir
image1 := test.CreateImageWith().DefaultLayers().
ImageConfig(ispec.Image{Created: test.DateRef(2010, 1, 1, 1, 1, 1, 0, time.UTC)}).
Build()
image2 := test.CreateImageWith().DefaultLayers().
ImageConfig(ispec.Image{Created: test.DateRef(2020, 1, 1, 1, 1, 1, 0, time.UTC)}).
Build()
storeController := test.GetDefaultStoreController(rootDir, ctlr.Log)
err := test.WriteImageToFileSystem(image1, "b-repo", "tag2", storeController)
if err != nil {
t.FailNow()
}
err = test.WriteImageToFileSystem(image2, "a-test-repo", "tag2", storeController)
if err != nil {
t.FailNow()
}
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(conf.HTTP.Port)
defer cm.StopServer()
Convey("test sorting", t, func() {
args := []string{"query", "repo", "--sort-by", "relevance", "--url", baseURL}
cmd := NewSearchCommand(new(searchService))
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err := cmd.Execute()
So(err, ShouldBeNil)
str := buff.String()
So(strings.Index(str, "b-repo"), ShouldBeLessThan, strings.Index(str, "a-test-repo"))
args = []string{"query", "repo", "--sort-by", "alpha-asc", "--url", baseURL}
cmd = NewSearchCommand(new(searchService))
buff = bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs(args)
err = cmd.Execute()
So(err, ShouldBeNil)
str = buff.String()
So(strings.Index(str, "a-test-repo"), ShouldBeLessThan, strings.Index(str, "b-repo"))
})
}

View file

@ -9,11 +9,14 @@ import (
"github.com/spf13/cobra"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/cli/cmdflags"
zcommon "zotregistry.io/zot/pkg/common"
)
func NewSearchSubjectCommand(searchService SearchService) *cobra.Command {
imageCmd := &cobra.Command{
imageListSortFlag := cmdflags.ImageListSortFlag(cmdflags.SortByAlphabeticAsc)
cmd := &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" +
@ -36,22 +39,24 @@ func NewSearchSubjectCommand(searchService SearchService) *cobra.Command {
},
}
return imageCmd
cmd.Flags().Var(&imageListSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.ImageListSortOptionsStr()))
return cmd
}
func NewSearchQueryCommand(searchService SearchService) *cobra.Command {
imageCmd := &cobra.Command{
Use: "query",
imageSearchSortFlag := cmdflags.ImageSearchSortFlag(cmdflags.SortByRelevance)
cmd := &cobra.Command{
Use: "query [repo]|[repo:tag]",
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 ":"`,
zli search query "test/repo:2.1."`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
@ -70,14 +75,17 @@ func NewSearchQueryCommand(searchService SearchService) *cobra.Command {
}
if err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery()); err != nil {
return fmt.Errorf("%w: '%s'", err, CVEListForImageQuery().Name)
return fmt.Errorf("%w: '%s'", err, GlobalSearchQuery().Name)
}
return GlobalSearchGQL(searchConfig, args[0])
},
}
return imageCmd
cmd.Flags().Var(&imageSearchSortFlag, cmdflags.SortByFlag,
fmt.Sprintf("Options for sorting the output: [%s]", cmdflags.ImageSearchSortOptionsStr()))
return cmd
}
func OneImageWithRefArg(cmd *cobra.Command, args []string) error {

View file

@ -22,6 +22,7 @@ import (
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/cli/cmdflags"
"zotregistry.io/zot/pkg/common"
)
@ -68,6 +69,7 @@ type searchConfig struct {
servURL string
user string
outputFormat string
sortBy string
verifyTLS bool
fixedFlag bool
verbose bool
@ -87,7 +89,7 @@ func (service searchService) getDerivedImageListGQL(ctx context.Context, config
) (*common.DerivedImageListResponse, error) {
query := fmt.Sprintf(`
{
DerivedImageList(image:"%s", requestedPage: {sortBy: ALPHABETIC_ASC}){
DerivedImageList(image:"%s", requestedPage: {sortBy: %s}){
Results{
RepoName Tag
Digest
@ -106,7 +108,7 @@ func (service searchService) getDerivedImageListGQL(ctx context.Context, config
IsSigned
}
}
}`, derivedImage)
}`, derivedImage, cmdflags.Flag2SortCriteria(config.sortBy))
result := &common.DerivedImageListResponse{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
@ -150,7 +152,7 @@ func (service searchService) globalSearchGQL(ctx context.Context, config searchC
) (*common.GlobalSearch, error) {
GQLQuery := fmt.Sprintf(`
{
GlobalSearch(query:"%s"){
GlobalSearch(query:"%s", requestedPage: {sortBy: %s}){
Images {
RepoName
Tag
@ -178,7 +180,7 @@ func (service searchService) globalSearchGQL(ctx context.Context, config searchC
StarCount
}
}
}`, query)
}`, query, cmdflags.Flag2SortCriteria(config.sortBy))
result := &common.GlobalSearchResultResp{}
@ -195,7 +197,7 @@ func (service searchService) getBaseImageListGQL(ctx context.Context, config sea
) (*common.BaseImageListResponse, error) {
query := fmt.Sprintf(`
{
BaseImageList(image:"%s", requestedPage: {sortBy: ALPHABETIC_ASC}){
BaseImageList(image:"%s", requestedPage: {sortBy: %s}){
Results{
RepoName Tag
Digest
@ -214,7 +216,7 @@ func (service searchService) getBaseImageListGQL(ctx context.Context, config sea
IsSigned
}
}
}`, baseImage)
}`, baseImage, cmdflags.Flag2SortCriteria(config.sortBy))
result := &common.BaseImageListResponse{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
@ -231,7 +233,7 @@ func (service searchService) getImagesGQL(ctx context.Context, config searchConf
) (*common.ImageListResponse, error) {
query := fmt.Sprintf(`
{
ImageList(repo: "%s", requestedPage: {sortBy: ALPHABETIC_ASC}) {
ImageList(repo: "%s", requestedPage: {sortBy: %s}) {
Results {
RepoName Tag
Digest
@ -250,8 +252,7 @@ func (service searchService) getImagesGQL(ctx context.Context, config searchConf
IsSigned
}
}
}`,
imageName)
}`, imageName, cmdflags.Flag2SortCriteria(config.sortBy))
result := &common.ImageListResponse{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
@ -268,7 +269,7 @@ func (service searchService) getImagesForDigestGQL(ctx context.Context, config s
) (*common.ImagesForDigest, error) {
query := fmt.Sprintf(`
{
ImageListForDigest(id: "%s", requestedPage: {sortBy: ALPHABETIC_ASC}) {
ImageListForDigest(id: "%s", requestedPage: {sortBy: %s}) {
Results {
RepoName Tag
Digest
@ -287,8 +288,7 @@ func (service searchService) getImagesForDigestGQL(ctx context.Context, config s
IsSigned
}
}
}`,
digest)
}`, digest, cmdflags.Flag2SortCriteria(config.sortBy))
result := &common.ImagesForDigest{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
@ -303,9 +303,15 @@ func (service searchService) getImagesForDigestGQL(ctx context.Context, config s
func (service searchService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password,
imageName, searchedCVE string,
) (*cveResult, error) {
query := fmt.Sprintf(`{ CVEListForImage (image:"%s", searchedCVE:"%s")`+
` { Tag CVEList { Id Title Severity Description `+
`PackageList {Name InstalledVersion FixedVersion}} } }`, imageName, searchedCVE)
query := fmt.Sprintf(`
{
CVEListForImage (image:"%s", searchedCVE:"%s", requestedPage: {sortBy: %s}) {
Tag CVEList {
Id Title Severity Description
PackageList {Name InstalledVersion FixedVersion}
}
}
}`, imageName, searchedCVE, cmdflags.Flag2SortCriteria(config.sortBy))
result := &cveResult{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
@ -314,8 +320,6 @@ func (service searchService) getCveByImageGQL(ctx context.Context, config search
return nil, errResult
}
result.Data.CVEListForImage.CVEList = groupCVEsBySeverity(result.Data.CVEListForImage.CVEList)
return result, nil
}
@ -324,7 +328,7 @@ func (service searchService) getTagsForCVEGQL(ctx context.Context, config search
) (*common.ImagesForCve, error) {
query := fmt.Sprintf(`
{
ImageListForCVE(id: "%s") {
ImageListForCVE(id: "%s", requestedPage: {sortBy: %s}) {
Results {
RepoName Tag
Digest
@ -344,7 +348,7 @@ func (service searchService) getTagsForCVEGQL(ctx context.Context, config search
}
}
}`,
cveID)
cveID, cmdflags.Flag2SortCriteria(config.sortBy))
result := &common.ImagesForCve{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
@ -637,50 +641,6 @@ func (service searchService) getImagesByDigest(ctx context.Context, config searc
localWg.Wait()
}
func groupCVEsBySeverity(cveList []cve) []cve {
var (
unknown = make([]cve, 0)
none = make([]cve, 0)
high = make([]cve, 0)
med = make([]cve, 0)
low = make([]cve, 0)
critical = make([]cve, 0)
)
for _, cve := range cveList {
switch cve.Severity {
case "NONE":
none = append(none, cve)
case "LOW":
low = append(low, cve)
case "MEDIUM":
med = append(med, cve)
case "HIGH":
high = append(high, cve)
case "CRITICAL":
critical = append(critical, cve)
default:
unknown = append(unknown, cve)
}
}
vulnsCount := len(unknown) + len(none) + len(high) + len(med) + len(low) + len(critical)
vulns := make([]cve, 0, vulnsCount)
vulns = append(vulns, critical...)
vulns = append(vulns, high...)
vulns = append(vulns, med...)
vulns = append(vulns, low...)
vulns = append(vulns, none...)
vulns = append(vulns, unknown...)
return vulns
}
func isContextDone(ctx context.Context) bool {
select {
case <-ctx.Done():
@ -1245,8 +1205,8 @@ type catalogResponse struct {
}
func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) {
if !isURL(serverURL) {
return "", zerr.ErrInvalidURL
if err := validateURL(serverURL); err != nil {
return "", err
}
newURL, err := url.Parse(serverURL)
@ -1375,10 +1335,16 @@ func (service searchService) getRepos(ctx context.Context, config searchConfig,
return
}
fmt.Fprintln(config.resultWriter, "\n\nREPOSITORY NAME")
fmt.Fprintln(config.resultWriter, "\nREPOSITORY NAME")
for _, repo := range catalog.Repositories {
fmt.Fprintln(config.resultWriter, repo)
if config.sortBy == cmdflags.SortByAlphabeticAsc {
for i := 0; i < len(catalog.Repositories); i++ {
fmt.Fprintln(config.resultWriter, catalog.Repositories[i])
}
} else {
for i := len(catalog.Repositories) - 1; i >= 0; i-- {
fmt.Fprintln(config.resultWriter, catalog.Repositories[i])
}
}
}

View file

@ -7,7 +7,6 @@ import (
"context"
"fmt"
"io"
"net/url"
"os"
"path"
"strings"
@ -376,6 +375,7 @@ func GetSearchConfigFromFlags(cmd *cobra.Command, searchService SearchService) (
debug := defaultIfError(flags.GetBool(cmdflags.DebugFlag))
verbose := defaultIfError(flags.GetBool(cmdflags.VerboseFlag))
outputFormat := defaultIfError(flags.GetString(cmdflags.OutputFormatFlag))
sortBy := defaultIfError(flags.GetString(cmdflags.SortByFlag))
spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr()))
spin.Prefix = prefix
@ -389,6 +389,7 @@ func GetSearchConfigFromFlags(cmd *cobra.Command, searchService SearchService) (
fixedFlag: fixed,
verbose: verbose,
debug: debug,
sortBy: sortBy,
spinner: spinnerState{spin, isSpinner},
resultWriter: cmd.OutOrStdout(),
}, nil
@ -459,9 +460,11 @@ func GetServerURLFromFlags(cmd *cobra.Command) (string, error) {
return "", fmt.Errorf("%w: url field from config is empty", zerr.ErrNoURLProvided)
}
_, err = url.Parse(serverURL)
if err := validateURL(serverURL); err != nil {
return "", err
}
return serverURL, err
return serverURL, nil
}
func ReadServerURLFromConfig(configName string) (string, error) {
@ -479,3 +482,22 @@ func ReadServerURLFromConfig(configName string) (string, error) {
return urlFromConfig, nil
}
func GetSuggestionsString(suggestions []string) string {
if len(suggestions) > 0 {
return "\n\nDid you mean this?\n" + "\t" + strings.Join(suggestions, "\n\t")
}
return ""
}
func ShowSuggestionsIfUnknownCommand(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
cmd.SuggestionsMinimumDistance = 2
suggestions := GetSuggestionsString(cmd.SuggestionsFor(args[0]))
return fmt.Errorf("%w '%s' for '%s'%s", zerr.ErrUnknownSubcommand, args[0], cmd.Name(), suggestions)
}

View file

@ -37,6 +37,10 @@ func SortByAlphabeticDsc(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j i
func SortBySeverity(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool {
return func(i, j int) bool {
if cveInfo.CompareSeverities(pageBuffer[i].Severity, pageBuffer[j].Severity) == 0 {
return pageBuffer[i].ID < pageBuffer[j].ID
}
return cveInfo.CompareSeverities(pageBuffer[i].Severity, pageBuffer[j].Severity) < 0
}
}

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
godigest "github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
mTypes "zotregistry.io/zot/pkg/meta/types"
@ -115,7 +116,10 @@ func (mb *BaseMultiarchBuilder) Build() MultiarchImage {
}
}
version := 2
index := ispec.Index{
Versioned: specs.Versioned{SchemaVersion: version},
MediaType: ispec.MediaTypeImageIndex,
Manifests: manifests,
Annotations: mb.annotations,