diff --git a/errors/errors.go b/errors/errors.go index 3e4c65e98e..ae2d1aaefd 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -33,7 +33,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 in current server version") 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") @@ -74,7 +74,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") @@ -110,4 +110,7 @@ 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") ) diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 47e7dcf968..6edfdbe6b6 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -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())) } diff --git a/pkg/cli/client.go b/pkg/cli/client.go index a9bcea7ced..6d1f43a4c3 100644 --- a/pkg/cli/client.go +++ b/pkg/cli/client.go @@ -124,7 +124,7 @@ 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() @@ -136,7 +136,7 @@ func doHTTPRequest(req *http.Request, verifyTLS bool, debug bool, bodyBytes, _ := io.ReadAll(resp.Body) - return nil, errors.New(string(bodyBytes)) //nolint: goerr113 + return nil, errors.New(resp.Status + " " + string(bodyBytes)) //nolint: goerr113 } if resultsPtr == nil { diff --git a/pkg/cli/cmdflags/flags.go b/pkg/cli/cmdflags/flags.go new file mode 100644 index 0000000000..72fcd9f58a --- /dev/null +++ b/pkg/cli/cmdflags/flags.go @@ -0,0 +1,13 @@ +package cmdflags + +const ( + URLFlag = "url" + ConfigFlag = "config" + UserFlag = "user" + OutputFormatFlag = "output" + FixedFlag = "fixed" + VerboseFlag = "verbose" + VersionFlag = "version" + DebugFlag = "debug" + SearchedCVEID = "id" +) diff --git a/pkg/cli/cve_cmd.go b/pkg/cli/cve_cmd.go index 875eebc17a..d4dedfe4c7 100644 --- a/pkg/cli/cve_cmd.go +++ b/pkg/cli/cve_cmd.go @@ -17,7 +17,7 @@ import ( ) const ( - cveDBRetryInterval = 3 + cveDBRetryInterval = 1 ) func NewCveCommand(searchService SearchService) *cobra.Command { diff --git a/pkg/cli/cve_cmd_test.go b/pkg/cli/cve_cmd_test.go index 44e4c4b7a1..3e1aa57e08 100644 --- a/pkg/cli/cve_cmd_test.go +++ b/pkg/cli/cve_cmd_test.go @@ -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") }) @@ -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") }) @@ -1056,6 +1057,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) @@ -1080,12 +1405,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 { @@ -1155,7 +1480,7 @@ func MockSearchCve(searchConfig searchConfig) error { } } - return zotErrors.ErrInvalidFlagsCombination + return zerr.ErrInvalidFlagsCombination } func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo { @@ -1230,7 +1555,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 @@ -1250,7 +1575,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 { @@ -1260,7 +1585,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo { return true, nil default: - return false, zotErrors.ErrScanNotSupported + return false, zerr.ErrScanNotSupported } } @@ -1282,12 +1607,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() diff --git a/pkg/cli/cves_cmd.go b/pkg/cli/cves_cmd.go new file mode 100644 index 0000000000..5eaeba6da4 --- /dev/null +++ b/pkg/cli/cves_cmd.go @@ -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, "o", "", "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 +} diff --git a/pkg/cli/cves_sub_cmd.go b/pkg/cli/cves_sub_cmd.go new file mode 100644 index 0000000000..d36603cac8 --- /dev/null +++ b/pkg/cli/cves_sub_cmd.go @@ -0,0 +1,205 @@ +//go:build search +// +build search + +package cli + +import ( + "context" + "fmt" + "strings" + "time" + + "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", + Long: `List CVEs by REPO:TAG`, + 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] + ctx, cancel := context.WithCancel(context.Background()) + retryCount := 1 + + return RetryWithContext(ctx, func() error { + err := SearchCVEForImageGQL(searchConfig, image, searchedCVEID) + if err != nil { + if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) { + cancel() + + return err + } + + fmt.Fprintln(searchConfig.resultWriter, + "[warning] CVE DB is not ready [", retryCount, "] - retry in ", cveDBRetryInterval, " seconds") + + retryCount += 1 + } + + return err + }, maxRetries, cveDBRetryInterval*time.Second) + }, + } + + 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] + ctx, cancel := context.WithCancel(context.Background()) + retryCount := 1 + + return RetryWithContext(ctx, func() error { + err := SearchImagesByCVEIDGQL(searchConfig, repo, searchedCVEID) + if err != nil { + if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) { + cancel() + + return err + } + + fmt.Fprintln(searchConfig.resultWriter, + "[warning] CVE DB is not ready [", retryCount, "] - retry in ", cveDBRetryInterval, " seconds") + + retryCount += 1 + } + + return err + }, maxRetries, cveDBRetryInterval*time.Second) + }, + } + + 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 which have fixed a CVE", + Long: `List tags which have fixed a CVE`, + Args: func(cmd *cobra.Command, args []string) error { + const argCount = 2 + + if err := cobra.ExactArgs(argCount)(cmd, args); err != nil { + return err + } + + if !zcommon.CheckIsRepoName(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] + ctx, cancel := context.WithCancel(context.Background()) + retryCount := 1 + + return RetryWithContext(ctx, func() error { + err := SearchFixedTagsGQL(searchConfig, repo, searchedCVEID) + if err != nil { + if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) { + cancel() + + return err + } + + fmt.Fprintln(searchConfig.resultWriter, + "[warning] CVE DB is not ready [", retryCount, "] - retry in ", cveDBRetryInterval, " seconds") + + retryCount += 1 + } + + return err + }, maxRetries, cveDBRetryInterval*time.Second) + }, + } + + return fixedTagsCmd +} + +func RetryWithContext(ctx context.Context, operation func() error, maxRetries int, + delay time.Duration, +) error { + err := operation() + + for attempt := 0; err != nil && attempt < maxRetries; attempt++ { + timer := time.NewTimer(delay) + + select { + case <-timer.C: + break + case <-ctx.Done(): + return err + } + + err = operation() + } + + return err +} diff --git a/pkg/cli/discover.go b/pkg/cli/discover.go index 2141533234..32e8b181c1 100644 --- a/pkg/cli/discover.go +++ b/pkg/cli/discover.go @@ -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 { @@ -25,7 +29,7 @@ type schemaList struct { } `json:"queryType"` //nolint:tagliatelle // graphQL schema } `json:"__schema"` //nolint:tagliatelle // graphQL schema } `json:"data"` - Errors []common.ErrorGQL `json:"errors"` + Errors []zcommon.ErrorGQL `json:"errors"` } func containsGQLQuery(queryList []field, query string) bool { @@ -38,6 +42,38 @@ func containsGQLQuery(queryList []field, query string) bool { return false } +func containsGQLQueryWithParams(queryList []field, requiredQueries ...GQLQuery) error { + 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) + } + } + + 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() @@ -99,3 +135,68 @@ 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 == "_zot" { + for _, endpoint := range extension.Endpoints { + if endpoint == constants.FullSearchPrefix { + searchEnabled = true + } + } + } + } + + if !searchEnabled { + return fmt.Errorf("%w: search extension gql endpoints", zerr.ErrExtensionNotEnabled) + } + + searchEndPoint, _ := combineServerAndEndpointURL(*config.servURL, constants.FullSearchPrefix) + + schemaQuery := ` + { + __schema() { + queryType { + fields { + name + args { + 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, requiredQueries...) +} diff --git a/pkg/cli/gql_queries.go b/pkg/cli/gql_queries.go new file mode 100644 index 0000000000..b032d734bb --- /dev/null +++ b/pkg/cli/gql_queries.go @@ -0,0 +1,45 @@ +//go:build search +// +build search + +package cli + +type GQLQuery struct { + Name string + Args []string +} + +func ImageListQuery() GQLQuery { + return GQLQuery{Name: "ImageList", Args: []string{"repo", "requestedPage"}} +} + +func ImageListForDigestQuery() GQLQuery { + return GQLQuery{Name: "ImageListForDigest", Args: []string{"id", "requestedPage"}} +} + +func BaseImageListQuery() GQLQuery { + return GQLQuery{Name: "BaseImageList", Args: []string{"image", "digest", "requestedPage"}} +} + +func DerivedImageListQuery() GQLQuery { + return GQLQuery{Name: "DerivedImageList", Args: []string{"image", "digest", "requestedPage"}} +} + +func CVEListForImageQuery() GQLQuery { + return GQLQuery{Name: "CVEListForImage", Args: []string{"image", "requestedPage", "searchedCVE"}} +} + +func ImageListForCVEQuery() GQLQuery { + return GQLQuery{Name: "ImageListForCVE", Args: []string{"id", "filter", "requestedPage"}} +} + +func ImageListWithCVEFixedQuery() GQLQuery { + return GQLQuery{Name: "ImageListWithCVEFixed", Args: []string{"id", "image", "filter", "requestedPage"}} +} + +func ReferrersQuery() GQLQuery { + return GQLQuery{Name: "Referrers", Args: []string{"repo", "digest", "type"}} +} + +func GlobalSearchQuery() GQLQuery { + return GQLQuery{Name: "GlobalSearch", Args: []string{"query", "filter", "requestedPage"}} +} diff --git a/pkg/cli/image_cmd.go b/pkg/cli/image_cmd.go index 8661733cb1..65c63a7566 100644 --- a/pkg/cli/image_cmd.go +++ b/pkg/cli/image_cmd.go @@ -24,7 +24,7 @@ func NewImageCommand(searchService SearchService) *cobra.Command { var isSpinner, verifyTLS, verbose, debug bool imageCmd := &cobra.Command{ - Use: "images [config-name]", + Use: "image [config-name]", Short: "List images hosted on the zot registry", Long: `List images hosted on the zot registry`, RunE: func(cmd *cobra.Command, args []string) error { @@ -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 { diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index 4b26b4d160..d60cb246ad 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -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" "zotregistry.io/zot/pkg/test" @@ -73,7 +74,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() { @@ -129,7 +130,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") }) @@ -252,7 +253,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") }) }) @@ -1552,6 +1553,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.CreateVulnerableImage() + 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.CreateVulnerableImage() + 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} @@ -1613,12 +2172,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 { @@ -1676,7 +2235,7 @@ func MockSearchImage(searchConfig searchConfig) error { } } - return zotErrors.ErrInvalidFlagsCombination + return zerr.ErrInvalidFlagsCombination } func uploadManifest(url string) error { @@ -1892,7 +2451,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, @@ -1900,39 +2525,56 @@ 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", + Digest: godigest.FromString("str").String(), + Size: "100", + ConfigDigest: ispec.DescriptorEmptyJSON.Digest.String(), }, }, }, }, Repos: []common.RepoSummary{ { - Name: "repo", + Name: "repo", + Size: "100", + LastUpdated: time.Date(2010, 1, 1, 1, 1, 1, 0, time.UTC), }, }, }, nil @@ -1941,6 +2583,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{ @@ -1958,6 +2604,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{ { @@ -1979,8 +2629,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{ { @@ -2004,6 +2658,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{ { @@ -2026,9 +2684,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{ { @@ -2054,6 +2716,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 { @@ -2072,6 +2738,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 { @@ -2079,6 +2749,10 @@ func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchCo }{}, } + if imageName == "" { + imageName = "image-name" + } + images.Errors = nil mockedImage := service.getMockedImageByName(imageName) @@ -2090,6 +2764,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 { @@ -2108,6 +2786,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{ @@ -2138,6 +2819,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(), @@ -2157,6 +2839,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" @@ -2189,6 +2877,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" @@ -2254,26 +2948,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) } diff --git a/pkg/cli/images_cmd.go b/pkg/cli/images_cmd.go new file mode 100644 index 0000000000..b9625f856d --- /dev/null +++ b/pkg/cli/images_cmd.go @@ -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, "o", "", "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 +} diff --git a/pkg/cli/images_sub_cmd.go b/pkg/cli/images_sub_cmd.go new file mode 100644 index 0000000000..6514b70fed --- /dev/null +++ b/pkg/cli/images_sub_cmd.go @@ -0,0 +1,300 @@ +//go:build search +// +build search + +package cli + +import ( + "context" + "fmt" + "os" + "path" + "strings" + "time" + + "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 { + ctx, cancel := context.WithCancel(context.Background()) + retryCount := 1 + + return RetryWithContext(ctx, func() error { + err = SearchCVEForImageGQL(searchConfig, args[0], searchedCVEID) + + if err != nil { + if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) { + cancel() + + return err + } + + fmt.Fprintln(searchConfig.resultWriter, + "[warning] CVE DB is not ready [", retryCount, "] - retry in ", cveDBRetryInterval, " seconds") + + retryCount += 1 + } + + return err + }, maxRetries, cveDBRetryInterval*time.Second) + } 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 := GetConfigOptions(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.OutOrStderr(), + }, nil +} + +func defaultIfError[T any](out T, err error) T { + var defaultVal T + + if err != nil { + return defaultVal + } + + return out +} + +func GetConfigOptions(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 "", zerr.ErrNoURLProvided + } + + 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 +} diff --git a/pkg/cli/repo_cmd.go b/pkg/cli/repos_cmd.go similarity index 82% rename from pkg/cli/repo_cmd.go rename to pkg/cli/repos_cmd.go index 784de164bf..f51edc8856 100644 --- a/pkg/cli/repo_cmd.go +++ b/pkg/cli/repos_cmd.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" zotErrors "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/cli/cmdflags" ) const prefix = "Searching... " @@ -24,6 +25,7 @@ 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 { @@ -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 } diff --git a/pkg/cli/repos_sub_cmd.go b/pkg/cli/repos_sub_cmd.go new file mode 100644 index 0000000000..daef084c94 --- /dev/null +++ b/pkg/cli/repos_sub_cmd.go @@ -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 +} diff --git a/pkg/cli/repos_test.go b/pkg/cli/repos_test.go new file mode 100644 index 0000000000..6af5508404 --- /dev/null +++ b/pkg/cli/repos_test.go @@ -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") + }) +} diff --git a/pkg/cli/root.go b/pkg/cli/root.go index cd4f1be769..a6f2609eb8 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -22,6 +22,7 @@ import ( "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" storageConstants "zotregistry.io/zot/pkg/storage/constants" @@ -186,6 +187,11 @@ func NewServerRootCmd() *cobra.Command { return rootCmd } +const ( + TestURL = iota + TestConfigPath +) + // "zli" - client-side cli. func NewCliRootCmd() *cobra.Command { showVersion := false @@ -208,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 } @@ -717,7 +729,7 @@ func LoadConfiguration(config *config.Config, configPath string) error { metaData := &mapstructure.Metadata{} if err := viperInstance.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") return err } diff --git a/pkg/cli/root_test.go b/pkg/cli/root_test.go index 65aed63fe0..4b4b306602 100644 --- a/pkg/cli/root_test.go +++ b/pkg/cli/root_test.go @@ -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": { diff --git a/pkg/cli/search_cmd.go b/pkg/cli/search_cmd.go index 3433418e6d..8abb1278f0 100644 --- a/pkg/cli/search_cmd.go +++ b/pkg/cli/search_cmd.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" zotErrors "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,6 +37,7 @@ 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 { @@ -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, "o", "", + "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 { diff --git a/pkg/cli/search_cmd_referrers_test.go b/pkg/cli/search_cmd_referrers_test.go index 851f1ed85b..59910e34a1 100644 --- a/pkg/cli/search_cmd_referrers_test.go +++ b/pkg/cli/search_cmd_referrers_test.go @@ -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" @@ -594,3 +588,9 @@ func TestReferrersCLIErrors(t *testing.T) { }) }) } + +func ref[T any](input T) *T { + ref := input + + return &ref +} diff --git a/pkg/cli/search_cmd_test.go b/pkg/cli/search_cmd_test.go index 1c276b332c..63f7685c21 100644 --- a/pkg/cli/search_cmd_test.go +++ b/pkg/cli/search_cmd_test.go @@ -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" ) @@ -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") + }) + }) +} diff --git a/pkg/cli/search_functions.go b/pkg/cli/search_functions.go new file mode 100644 index 0000000000..d43910e97b --- /dev/null +++ b/pkg/cli/search_functions.go @@ -0,0 +1,403 @@ +//go:build search +// +build search + +package cli + +import ( + "context" + "fmt" + "math" + "strings" + "sync" + + 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() + + cveList, err := config.searchService.getCveByImageGQL(ctx, config, username, password, image, searchedCveID) + 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() + + imageList, err := config.searchService.getTagsForCVEGQL(ctx, config, username, password, + repo, cveid) + 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() + + fixedTags, err := config.searchService.getFixedTagsForCVEGQL(ctx, config, username, password, + repo, cveid) + 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 + } +} diff --git a/pkg/cli/search_functions_test.go b/pkg/cli/search_functions_test.go new file mode 100644 index 0000000000..75ee8c5122 --- /dev/null +++ b/pkg/cli/search_functions_test.go @@ -0,0 +1,760 @@ +//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"}, + }, + GQLQuery{Name: "other-name"}, + ) + So(err, ShouldNotBeNil) + }) + + Convey("GetConfigOptions", t, func() { + // no flags + cmd := &cobra.Command{} + isSpinner, verifyTLS := GetConfigOptions(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 = GetConfigOptions(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 = GetConfigOptions(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) + }) +} diff --git a/pkg/cli/search_sub.cmd.go b/pkg/cli/search_sub.cmd.go new file mode 100644 index 0000000000..4f6af25421 --- /dev/null +++ b/pkg/cli/search_sub.cmd.go @@ -0,0 +1,94 @@ +//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: "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)", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + if err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery()); err == nil { + return GlobalSearchGQL(searchConfig, args[0]) + } else { + return fmt.Errorf("%w: '%s'", err, CVEListForImageQuery().Name) + } + }, + } + + 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 +} diff --git a/pkg/cli/searcher.go b/pkg/cli/searcher.go index aa8ad9cb0f..3e652bf152 100644 --- a/pkg/cli/searcher.go +++ b/pkg/cli/searcher.go @@ -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 } @@ -407,7 +407,7 @@ func (search cveByImageSearcherGQL) search(config searchConfig) (bool, error) { } if len(cveList.Data.CVEListForImage.CVEList) > 0 && - (*config.outputFormat == defaultOutoutFormat || *config.outputFormat == "") { + (*config.outputFormat == defaultOutputFormat || *config.outputFormat == "") { printCVETableHeader(&builder, *config.verbose, 0, 0, 0) fmt.Fprint(config.resultWriter, builder.String()) } @@ -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, @@ -799,7 +799,7 @@ func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan strin return } - if !foundResult && (*config.outputFormat == defaultOutoutFormat || *config.outputFormat == "") { + if !foundResult && (*config.outputFormat == defaultOutputFormat || *config.outputFormat == "") { var builder strings.Builder printHeader(&builder, *config.verbose, 0, 0, 0) @@ -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 } @@ -970,7 +970,7 @@ func printCVETableHeader(writer io.Writer, verbose bool, maxImgLen, maxTagLen, m } func printReferrersTableHeader(config searchConfig, writer io.Writer, maxArtifactTypeLen int) { - if *config.outputFormat != "" && *config.outputFormat != defaultOutoutFormat { + if *config.outputFormat != "" && *config.outputFormat != defaultOutputFormat { return } @@ -1081,7 +1081,7 @@ func printImageResult(config searchConfig, imageList []imageStruct) error { } } - if *config.outputFormat == defaultOutoutFormat || *config.outputFormat == "" { + if *config.outputFormat == defaultOutputFormat || *config.outputFormat == "" { printImageTableHeader(&builder, *config.verbose, maxImgNameLen, maxTagLen, maxPlatformLen) } @@ -1117,7 +1117,7 @@ func printRepoResults(config searchConfig, repoList []repoStruct) error { } } - if len(repoList) > 0 && (*config.outputFormat == defaultOutoutFormat || *config.outputFormat == "") { + if len(repoList) > 0 && (*config.outputFormat == defaultOutputFormat || *config.outputFormat == "") { printRepoTableHeader(config.resultWriter, maxRepoNameLen, maxTimeLen, *config.verbose) } diff --git a/pkg/cli/service.go b/pkg/cli/service.go index e0a19baf29..7eec53b93d 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -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) @@ -57,17 +57,17 @@ type SearchService interface { //nolint:interfacebloat channel chan stringResult, wtgrp *sync.WaitGroup) getCveByImage(ctx context.Context, config searchConfig, username, password, imageName, searchedCVE string, channel chan stringResult, wtgrp *sync.WaitGroup) - getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string, + getImagesByCveID(ctx context.Context, config searchConfig, username, password, cveid string, channel chan stringResult, wtgrp *sync.WaitGroup) getImagesByDigest(ctx context.Context, config searchConfig, username, password, digest string, channel chan stringResult, wtgrp *sync.WaitGroup) - getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cvid string, + getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cveid string, channel chan stringResult, wtgrp *sync.WaitGroup) getRepos(ctx context.Context, config searchConfig, username, password string, channel chan stringResult, wtgrp *sync.WaitGroup) getImageByName(ctx context.Context, config searchConfig, username, password, imageName string, channel chan stringResult, wtgrp *sync.WaitGroup) - getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cvid string, + getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cveid string, channel chan stringResult, wtgrp *sync.WaitGroup) getReferrers(ctx context.Context, config searchConfig, username, password string, repo, digest string, ) (referrersResult, 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,7 +387,19 @@ func (service searchService) getTagsForCVEGQL(ctx context.Context, config search return nil, errResult } - return result, nil + 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, @@ -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,14 +584,22 @@ 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) } } func (service searchService) getImagesByCveID(ctx context.Context, config searchConfig, username, - password, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup, + password, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) @@ -604,7 +626,7 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search } } }`, - cvid) + cveid) result := &common.ImagesForCve{} @@ -723,7 +745,7 @@ func (service searchService) getImagesByDigest(ctx context.Context, config searc } func (service searchService) 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, ) { defer wtgrp.Done() defer close(rch) @@ -750,7 +772,7 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config } } }`, - cvid) + cveid) result := &common.ImagesForCve{} @@ -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 } @@ -854,7 +876,7 @@ func (service searchService) getCveByImage(ctx context.Context, config searchCon } func (service searchService) 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, ) { defer wtgrp.Done() defer close(rch) @@ -880,7 +902,7 @@ func (service searchService) getFixedTagsForCVE(ctx context.Context, config sear IsSigned } } - }`, cvid, imageName) + }`, cveid, imageName) result := &common.ImageListWithCVEFixedResponse{} @@ -1092,7 +1114,7 @@ type cveData struct { func (cve cveResult) string(format string) (string, error) { switch strings.ToLower(format) { - case "", defaultOutoutFormat: + case "", defaultOutputFormat: return cve.stringPlainText() case jsonFormat: return cve.stringJSON() @@ -1151,7 +1173,7 @@ type referrersResult []common.Referrer func (ref referrersResult) string(format string, maxArtifactTypeLen int) (string, error) { switch strings.ToLower(format) { - case "", defaultOutoutFormat: + case "", defaultOutputFormat: return ref.stringPlainText(maxArtifactTypeLen) case jsonFormat: return ref.stringJSON() @@ -1215,7 +1237,7 @@ type repoStruct common.RepoSummary func (repo repoStruct) string(format string, maxImgNameLen, maxTimeLen int, verbose bool) (string, error) { //nolint: lll switch strings.ToLower(format) { - case "", defaultOutoutFormat: + case "", defaultOutputFormat: return repo.stringPlainText(maxImgNameLen, maxTimeLen, verbose) case jsonFormat: return repo.stringJSON() @@ -1234,7 +1256,7 @@ func (repo repoStruct) stringPlainText(repoMaxLen, maxTimeLen int, verbose bool) table.SetColMinWidth(repoNameIndex, repoMaxLen) table.SetColMinWidth(repoSizeIndex, sizeWidth) table.SetColMinWidth(repoLastUpdatedIndex, maxTimeLen) - table.SetColMinWidth(repoDownloadsIndex, dounloadsWidth) + table.SetColMinWidth(repoDownloadsIndex, downloadsWidth) table.SetColMinWidth(repoStarsIndex, signedWidth) if verbose { @@ -1307,7 +1329,7 @@ type imageStruct common.ImageSummary func (img imageStruct) string(format string, maxImgNameLen, maxTagLen, maxPlatformLen int, verbose bool) (string, error) { //nolint: lll switch strings.ToLower(format) { - case "", defaultOutoutFormat: + case "", defaultOutputFormat: return img.stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen, verbose) case jsonFormat: return img.stringJSON() @@ -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) { @@ -1677,7 +1699,7 @@ const ( platformWidth = 14 sizeWidth = 10 isSignedWidth = 8 - dounloadsWidth = 10 + downloadsWidth = 10 signedWidth = 10 lastUpdatedWidth = 14 configWidth = 8 @@ -1692,7 +1714,7 @@ const ( colCVESeverityIndex = 1 colCVETitleIndex = 2 - defaultOutoutFormat = "text" + defaultOutputFormat = "text" ) const ( diff --git a/pkg/common/oci.go b/pkg/common/oci.go index 97ddc900e4..061ff2010b 100644 --- a/pkg/common/oci.go +++ b/pkg/common/oci.go @@ -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 CheckIsRepoName(repo string) bool { + return !strings.ContainsAny(repo, ":@") +} diff --git a/pkg/exporter/cli/cli.go b/pkg/exporter/cli/cli.go index 24285556e6..385a250cbb 100644 --- a/pkg/exporter/cli/cli.go +++ b/pkg/exporter/cli/cli.go @@ -66,7 +66,7 @@ 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) } diff --git a/pkg/extensions/search/convert/annotations.go b/pkg/extensions/search/convert/annotations.go index 99c83c8d07..2b15aebfff 100644 --- a/pkg/extensions/search/convert/annotations.go +++ b/pkg/extensions/search/convert/annotations.go @@ -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 { diff --git a/pkg/extensions/search/convert/convert_test.go b/pkg/extensions/search/convert/convert_test.go index 8532663a87..8d45d6cedf 100644 --- a/pkg/extensions/search/convert/convert_test.go +++ b/pkg/extensions/search/convert/convert_test.go @@ -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) diff --git a/pkg/extensions/search/convert/metadb.go b/pkg/extensions/search/convert/metadb.go index 4b126c2d36..e581784613 100644 --- a/pkg/extensions/search/convert/metadb.go +++ b/pkg/extensions/search/convert/metadb.go @@ -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 } diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index e3a71997ae..a05b7f0fab 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -592,9 +592,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) @@ -603,7 +603,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) @@ -611,7 +611,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) @@ -628,7 +628,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) @@ -636,7 +636,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) @@ -644,11 +644,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) @@ -656,7 +656,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) @@ -725,7 +725,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) }) @@ -1405,7 +1405,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) diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index 41229225f9..47684947d9 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -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 diff --git a/pkg/extensions/search/search_test.go b/pkg/extensions/search/search_test.go index a096c10f88..4af69febe8 100644 --- a/pkg/extensions/search/search_test.go +++ b/pkg/extensions/search/search_test.go @@ -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) diff --git a/pkg/meta/boltdb/boltdb.go b/pkg/meta/boltdb/boltdb.go index a0a6aaaf35..468b00217c 100644 --- a/pkg/meta/boltdb/boltdb.go +++ b/pkg/meta/boltdb/boltdb.go @@ -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: diff --git a/pkg/test/common.go b/pkg/test/common.go index 1e3fcbf8fd..1e03453e37 100644 --- a/pkg/test/common.go +++ b/pkg/test/common.go @@ -51,6 +51,7 @@ import ( "zotregistry.io/zot/pkg/storage" storageCommon "zotregistry.io/zot/pkg/storage/common" + stypes "zotregistry.io/zot/pkg/storage/types" "zotregistry.io/zot/pkg/test/inject" ) @@ -1400,12 +1401,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) } @@ -2029,3 +2030,20 @@ func GetDefaultLayersBlobs() [][]byte { []byte("xyz"), } } + +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 +} diff --git a/pkg/test/images.go b/pkg/test/images.go index 69efd566ec..a160a6f422 100644 --- a/pkg/test/images.go +++ b/pkg/test/images.go @@ -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