diff --git a/errors/errors.go b/errors/errors.go index bc535fb7..5bcca71e 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -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") ) diff --git a/pkg/cli/client.go b/pkg/cli/client.go index e896b728..3e6775ee 100644 --- a/pkg/cli/client.go +++ b/pkg/cli/client.go @@ -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 diff --git a/pkg/cli/client_test.go b/pkg/cli/client_test.go index d2bd6f92..a5c54efc 100644 --- a/pkg/cli/client_test.go +++ b/pkg/cli/client_test.go @@ -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( diff --git a/pkg/cli/cmdflags/flags.go b/pkg/cli/cmdflags/flags.go index 9d6ef640..8b2e484a 100644 --- a/pkg/cli/cmdflags/flags.go +++ b/pkg/cli/cmdflags/flags.go @@ -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 +} diff --git a/pkg/cli/cmdflags/flags_test.go b/pkg/cli/cmdflags/flags_test.go new file mode 100644 index 00000000..735db023 --- /dev/null +++ b/pkg/cli/cmdflags/flags_test.go @@ -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") + }) +} diff --git a/pkg/cli/config_cmd.go b/pkg/cli/config_cmd.go index 128bca2a..f8391054 100644 --- a/pkg/cli/config_cmd.go +++ b/pkg/cli/config_cmd.go @@ -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) { diff --git a/pkg/cli/config_cmd_test.go b/pkg/cli/config_cmd_test.go index 3decfbbb..dd9df9e2 100644 --- a/pkg/cli/config_cmd_test.go +++ b/pkg/cli/config_cmd_test.go @@ -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() { diff --git a/pkg/cli/cve_cmd_test.go b/pkg/cli/cve_cmd_test.go index 765125c2..8de5b5e2 100644 --- a/pkg/cli/cve_cmd_test.go +++ b/pkg/cli/cve_cmd_test.go @@ -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) diff --git a/pkg/cli/cves_cmd.go b/pkg/cli/cves_cmd.go index f5db8e1e..da592432 100644 --- a/pkg/cli/cves_cmd.go +++ b/pkg/cli/cves_cmd.go @@ -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) diff --git a/pkg/cli/cves_sub_cmd.go b/pkg/cli/cves_sub_cmd.go index 9220cc69..6baf5401 100644 --- a/pkg/cli/cves_sub_cmd.go +++ b/pkg/cli/cves_sub_cmd.go @@ -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 } diff --git a/pkg/cli/image_cmd.go b/pkg/cli/image_cmd.go index 9fcfc007..842de758 100644 --- a/pkg/cli/image_cmd.go +++ b/pkg/cli/image_cmd.go @@ -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) diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index f5f1930b..feb57cc7 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -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, diff --git a/pkg/cli/image_sub_cmd.go b/pkg/cli/image_sub_cmd.go index 71656de9..b96c0a52 100644 --- a/pkg/cli/image_sub_cmd.go +++ b/pkg/cli/image_sub_cmd.go @@ -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 } diff --git a/pkg/cli/repo_cmd.go b/pkg/cli/repo_cmd.go index 7567d8f4..248d04bd 100644 --- a/pkg/cli/repo_cmd.go +++ b/pkg/cli/repo_cmd.go @@ -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) diff --git a/pkg/cli/repo_sub_cmd.go b/pkg/cli/repo_sub_cmd.go index f0b9ef9b..b8ff046f 100644 --- a/pkg/cli/repo_sub_cmd.go +++ b/pkg/cli/repo_sub_cmd.go @@ -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 } diff --git a/pkg/cli/repo_test.go b/pkg/cli/repo_test.go index 6d2966bb..f63f243f 100644 --- a/pkg/cli/repo_test.go +++ b/pkg/cli/repo_test.go @@ -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") }) } diff --git a/pkg/cli/search_cmd.go b/pkg/cli/search_cmd.go index 66a91c7f..d00baeb4 100644 --- a/pkg/cli/search_cmd.go +++ b/pkg/cli/search_cmd.go @@ -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) diff --git a/pkg/cli/search_cmd_test.go b/pkg/cli/search_cmd_test.go index 7f0e122b..7081d20f 100644 --- a/pkg/cli/search_cmd_test.go +++ b/pkg/cli/search_cmd_test.go @@ -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")) + }) +} diff --git a/pkg/cli/search_sub_cmd.go b/pkg/cli/search_sub_cmd.go index 1861fa06..341f55e3 100644 --- a/pkg/cli/search_sub_cmd.go +++ b/pkg/cli/search_sub_cmd.go @@ -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 { diff --git a/pkg/cli/service.go b/pkg/cli/service.go index 0d99fea7..d869c293 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -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]) + } } } diff --git a/pkg/cli/utils.go b/pkg/cli/utils.go index 785a51aa..2ef07fd4 100644 --- a/pkg/cli/utils.go +++ b/pkg/cli/utils.go @@ -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) +} diff --git a/pkg/extensions/search/cve/pagination.go b/pkg/extensions/search/cve/pagination.go index 53017c64..046942f9 100644 --- a/pkg/extensions/search/cve/pagination.go +++ b/pkg/extensions/search/cve/pagination.go @@ -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 } } diff --git a/pkg/test/multiarch.go b/pkg/test/multiarch.go index 12519632..f7b876aa 100644 --- a/pkg/test/multiarch.go +++ b/pkg/test/multiarch.go @@ -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,