diff --git a/cmd/zb/helper.go b/cmd/zb/helper.go index 5ad669aa..19c9b765 100644 --- a/cmd/zb/helper.go +++ b/cmd/zb/helper.go @@ -3,7 +3,6 @@ package main import ( "bytes" "encoding/json" - "errors" "fmt" "io" "log" @@ -19,6 +18,7 @@ import ( ispec "github.com/opencontainers/image-spec/specs-go/v1" "gopkg.in/resty.v1" + zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/test" ) @@ -32,7 +32,8 @@ func makeHTTPGetRequest(url string, resultPtr interface{}, client *resty.Client) if resp.StatusCode() != http.StatusOK { log.Printf("unable to make GET request on %s, response status code: %d", url, resp.StatusCode()) - return errors.New(string(resp.Body())) //nolint: goerr113 + return fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusOK, + resp.StatusCode(), string(resp.Body())) } err = json.Unmarshal(resp.Body(), resultPtr) @@ -52,7 +53,8 @@ func makeHTTPDeleteRequest(url string, client *resty.Client) error { if resp.StatusCode() != http.StatusAccepted { log.Printf("unable to make DELETE request on %s, response status code: %d", url, resp.StatusCode()) - return errors.New(string(resp.Body())) //nolint: goerr113 + return fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted, + resp.StatusCode(), string(resp.Body())) } return nil @@ -336,7 +338,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo // request specific check statusCode = resp.StatusCode() if statusCode != http.StatusAccepted { - return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113 + return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted, + resp.StatusCode(), string(resp.Body())) //nolint: goerr113 } loc := test.Location(url, resp) @@ -374,7 +377,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo // request specific check statusCode = resp.StatusCode() if statusCode != http.StatusCreated { - return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113 + return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated, + resp.StatusCode(), string(resp.Body())) } // upload image config blob @@ -388,7 +392,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo // request specific check statusCode = resp.StatusCode() if statusCode != http.StatusAccepted { - return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113 + return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusAccepted, + resp.StatusCode(), string(resp.Body())) } loc = test.Location(url, resp) @@ -408,7 +413,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo // request specific check statusCode = resp.StatusCode() if statusCode != http.StatusCreated { - return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113 + return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated, + resp.StatusCode(), string(resp.Body())) } // create a manifest @@ -451,7 +457,8 @@ func pushMonolithImage(workdir, url, trepo string, repos []string, config testCo // request specific check statusCode = resp.StatusCode() if statusCode != http.StatusCreated { - return nil, repos, errors.New(string(resp.Body())) //nolint: goerr113 + return nil, repos, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusCreated, + resp.StatusCode(), string(resp.Body())) } manifestHash[repo] = manifestTag diff --git a/errors/errors.go b/errors/errors.go index b3cf4bfd..41e19590 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -74,7 +74,7 @@ var ( ErrInvalidArgs = errors.New("cli: Invalid Arguments") ErrInvalidFlagsCombination = errors.New("cli: Invalid combination of flags") ErrInvalidURL = errors.New("cli: invalid URL format") - ErrExtensionNotEnabled = errors.New("cli: functionality is not built in current version") + ErrExtensionNotEnabled = errors.New("cli: functionality is not built/configured in the current server") ErrUnauthorizedAccess = errors.New("auth: unauthorized access. check credentials") ErrCannotResetConfigKey = errors.New("cli: cannot reset given config key") ErrConfigNotFound = errors.New("cli: config with the given name does not exist") @@ -115,7 +115,7 @@ var ( ErrEmptyRepoName = errors.New("metadb: repo name can't be empty string") ErrEmptyTag = errors.New("metadb: tag can't be empty string") ErrEmptyDigest = errors.New("metadb: digest can't be empty string") - ErrInvalidRepoRefFormat = errors.New("invalid image reference format") + ErrInvalidRepoRefFormat = errors.New("invalid image reference format [repo:tag] or [repo@digest]") ErrLimitIsNegative = errors.New("pageturner: limit has negative value") ErrOffsetIsNegative = errors.New("pageturner: offset has negative value") ErrSortCriteriaNotSupported = errors.New("pageturner: the sort criteria is not supported") @@ -151,4 +151,8 @@ var ( ErrInvalidPublicKeyContent = errors.New("signatures: invalid public key content") ErrInvalidStateCookie = errors.New("auth: state cookie not present or differs from original state") ErrSyncNoURLsLeft = errors.New("sync: no valid registry urls left after filtering local ones") + ErrInvalidCLIParameter = errors.New("cli: invalid parameter") + ErrGQLEndpointNotFound = errors.New("cli: the server doesn't have a gql endpoint") + ErrGQLQueryNotSupported = errors.New("cli: query is not supported or has different arguments") + ErrBadHTTPStatusCode = errors.New("cli: the response doesn't contain the expected status code") ) diff --git a/pkg/api/constants/extensions.go b/pkg/api/constants/extensions.go index 1fa02461..188c4f55 100644 --- a/pkg/api/constants/extensions.go +++ b/pkg/api/constants/extensions.go @@ -5,6 +5,8 @@ const ( ExtCatalogPrefix = "/_catalog" ExtOciDiscoverPrefix = "/_oci/ext/discover" + BaseExtension = "_zot" + // zot specific extensions. BasePrefix = "/_zot" ExtPrefix = BasePrefix + "/ext" diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 341c13e7..f71ce857 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -8491,7 +8491,7 @@ func TestDistSpecExtensions(t *testing.T) { t.Log(extensionList.Extensions) So(len(extensionList.Extensions), ShouldEqual, 1) So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 5) - So(extensionList.Extensions[0].Name, ShouldEqual, "_zot") + So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension) So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md") So(extensionList.Extensions[0].Description, ShouldNotBeEmpty) // Verify the endpoints below are enabled by search @@ -8539,7 +8539,7 @@ func TestDistSpecExtensions(t *testing.T) { t.Log(extensionList.Extensions) So(len(extensionList.Extensions), ShouldEqual, 1) So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 2) - So(extensionList.Extensions[0].Name, ShouldEqual, "_zot") + So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension) So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md") So(extensionList.Extensions[0].Description, ShouldNotBeEmpty) // Verify the endpoints below are enabled by search diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 1bfdb4ca..fcccc1e9 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -717,7 +717,7 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht rh.c.Log.Error().Err(err).Msg("unexpected error: performing cleanup") if err = imgStore.DeleteImageManifest(name, reference, false); err != nil { - // deletion of image manifest is important, but not critical for image repo consistancy + // deletion of image manifest is important, but not critical for image repo consistency // in the worst scenario a partial manifest file written to disk will not affect the repo because // the new manifest was not added to "index.json" file (it is possible that GC will take care of it) rh.c.Log.Error().Err(err).Str("repository", name).Str("reference", reference). diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 47e7dcf9..6edfdbe6 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 a9bcea7c..3acd546b 100644 --- a/pkg/cli/client.go +++ b/pkg/cli/client.go @@ -7,7 +7,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "io" "net/http" @@ -21,7 +20,7 @@ import ( ispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sigstore/cosign/v2/pkg/oci/remote" - zotErrors "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/common" ) @@ -124,19 +123,20 @@ func doHTTPRequest(req *http.Request, verifyTLS bool, debug bool, if debug { fmt.Fprintln(configWriter, "[debug] ", req.Method, req.URL, "[status] ", - resp.StatusCode, " ", "[respoonse header] ", resp.Header) + resp.StatusCode, " ", "[response header] ", resp.Header) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { if resp.StatusCode == http.StatusUnauthorized { - return nil, zotErrors.ErrUnauthorizedAccess + return nil, zerr.ErrUnauthorizedAccess } bodyBytes, _ := io.ReadAll(resp.Body) - return nil, errors.New(string(bodyBytes)) //nolint: goerr113 + return nil, fmt.Errorf("%w: Expected: %d, Got: %d, Body: '%s'", zerr.ErrBadHTTPStatusCode, http.StatusOK, + resp.StatusCode, string(bodyBytes)) } if resultsPtr == nil { diff --git a/pkg/cli/cmdflags/flags.go b/pkg/cli/cmdflags/flags.go new file mode 100644 index 00000000..9d6ef640 --- /dev/null +++ b/pkg/cli/cmdflags/flags.go @@ -0,0 +1,13 @@ +package cmdflags + +const ( + URLFlag = "url" + ConfigFlag = "config" + UserFlag = "user" + OutputFormatFlag = "format" + FixedFlag = "fixed" + VerboseFlag = "verbose" + VersionFlag = "version" + DebugFlag = "debug" + SearchedCVEID = "cve-id" +) diff --git a/pkg/cli/config_cmd.go b/pkg/cli/config_cmd.go index 0460cffd..128bca2a 100644 --- a/pkg/cli/config_cmd.go +++ b/pkg/cli/config_cmd.go @@ -40,7 +40,7 @@ func NewConfigCommand() *cobra.Command { panic(err) } - configPath := path.Join(home + "/.zot") + configPath := path.Join(home, "/.zot") switch len(args) { case noArgs: if isListing { // zot config -l @@ -115,7 +115,7 @@ func NewConfigAddCommand() *cobra.Command { panic(err) } - configPath := path.Join(home + "/.zot") + configPath := path.Join(home, "/.zot") // zot config add err = addConfig(configPath, args[0], args[1]) if err != nil { @@ -145,7 +145,7 @@ func NewConfigRemoveCommand() *cobra.Command { panic(err) } - configPath := path.Join(home + "/.zot") + configPath := path.Join(home, "/.zot") // zot config add err = removeConfig(configPath, args[0]) if err != nil { diff --git a/pkg/cli/config_cmd_test.go b/pkg/cli/config_cmd_test.go index 1ad0c7ba..3decfbbb 100644 --- a/pkg/cli/config_cmd_test.go +++ b/pkg/cli/config_cmd_test.go @@ -13,7 +13,7 @@ import ( . "github.com/smartystreets/goconvey/convey" - zotErrors "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" ) func TestConfigCmdBasics(t *testing.T) { @@ -146,7 +146,7 @@ func TestConfigCmdMain(t *testing.T) { cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() - So(err, ShouldEqual, zotErrors.ErrCliBadConfig) + So(err, ShouldEqual, zerr.ErrCliBadConfig) }) Convey("Test add config with invalid URL", t, func() { @@ -160,7 +160,7 @@ func TestConfigCmdMain(t *testing.T) { cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) - So(err, ShouldEqual, zotErrors.ErrInvalidURL) + So(err, ShouldEqual, zerr.ErrInvalidURL) }) Convey("Test remove config entry successfully", t, func() { diff --git a/pkg/cli/cve_cmd.go b/pkg/cli/cve_cmd.go index 875eebc1..046d1db8 100644 --- a/pkg/cli/cve_cmd.go +++ b/pkg/cli/cve_cmd.go @@ -13,7 +13,7 @@ import ( "github.com/briandowns/spinner" "github.com/spf13/cobra" - zotErrors "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" ) const ( @@ -29,15 +29,15 @@ func NewCveCommand(searchService SearchService) *cobra.Command { cveCmd := &cobra.Command{ Use: "cve [config-name]", - Short: "Lookup CVEs in images hosted on the zot registry", - Long: `List CVEs (Common Vulnerabilities and Exposures) of images hosted on the zot registry`, + Short: "DEPRECATED (see cves)", + Long: `DEPRECATED (see cves)! List CVEs (Common Vulnerabilities and Exposures) of images hosted on the zot registry`, RunE: func(cmd *cobra.Command, args []string) error { home, err := os.UserHomeDir() if err != nil { panic(err) } - configPath := path.Join(home + "/.zot") + configPath := path.Join(home, "/.zot") if servURL == "" { if len(args) > 0 { urlFromConfig, err := getConfigValue(configPath, args[0], "url") @@ -48,12 +48,12 @@ func NewCveCommand(searchService SearchService) *cobra.Command { } if urlFromConfig == "" { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } servURL = urlFromConfig } else { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } } @@ -177,7 +177,7 @@ func searchCve(searchConfig searchConfig) error { return err } - if strings.Contains(err.Error(), zotErrors.ErrCVEDBNotFound.Error()) { + if strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) { // searches matches search config but CVE DB is not ready server side // wait and retry a few more times fmt.Fprintln(searchConfig.resultWriter, @@ -192,5 +192,5 @@ func searchCve(searchConfig searchConfig) error { } } - return zotErrors.ErrInvalidFlagsCombination + return zerr.ErrInvalidFlagsCombination } diff --git a/pkg/cli/cve_cmd_test.go b/pkg/cli/cve_cmd_test.go index 3d602ae7..7860543b 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") }) @@ -285,7 +286,7 @@ func TestSearchCVECmd(t *testing.T) { err := cveCmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE anImage tag os/arch 6e2f80bf false 123kB") //nolint:lll + So(strings.TrimSpace(str), ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE anImage tag os/arch 6e2f80bf false 123kB") //nolint:lll So(err, ShouldBeNil) }) @@ -355,7 +356,7 @@ func TestSearchCVECmd(t *testing.T) { cveCmd.SetArgs(args) err := cveCmd.Execute() So(err, ShouldNotBeNil) - So(err, ShouldEqual, zotErrors.ErrInvalidURL) + So(err, ShouldEqual, zerr.ErrInvalidURL) So(buff.String(), ShouldContainSubstring, "invalid URL format") }) @@ -1058,6 +1059,330 @@ func TestServerCVEResponse(t *testing.T) { }) } +func TestCVECommandGQL(t *testing.T) { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + }, + } + + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + cm := test.NewControllerManager(ctlr) + + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + Convey("commands without gql", t, func() { + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, baseURL)) + defer os.Remove(configPath) + + Convey("cveid", func() { + args := []string{"cveid", "CVE-1942"} + cmd := NewCVESCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "image-name tag 6e2f80bf false 123kB") + }) + + Convey("cveid db download wait", func() { + count := 0 + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, + baseURL)) + args := []string{"cveid", "CVE-12345"} + defer os.Remove(configPath) + cmd := NewCVESCommand(mockService{ + getTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password, + imageName, cveID string) (*zcommon.ImagesForCve, error, + ) { + if count == 0 { + count++ + fmt.Println("Count:", count) + + return &zcommon.ImagesForCve{}, zerr.ErrCVEDBNotFound + } + + return &zcommon.ImagesForCve{}, zerr.ErrInjected + }, + }) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready") + }) + + Convey("fixed", func() { + args := []string{"fixed", "image-name", "CVE-123"} + cmd := NewCVESCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "image-name tag 6e2f80bf false 123kB") + }) + + Convey("fixed db download wait", func() { + count := 0 + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, + baseURL)) + args := []string{"fixed", "repo", "CVE-2222"} + defer os.Remove(configPath) + cmd := NewCVESCommand(mockService{ + getFixedTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password, + imageName, cveID string) (*zcommon.ImageListWithCVEFixedResponse, error, + ) { + if count == 0 { + count++ + fmt.Println("Count:", count) + + return &zcommon.ImageListWithCVEFixedResponse{}, zerr.ErrCVEDBNotFound + } + + return &zcommon.ImageListWithCVEFixedResponse{}, zerr.ErrInjected + }, + }) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready") + }) + + Convey("image", func() { + args := []string{"image", "repo:tag"} + cmd := NewCVESCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "dummyCVEID HIGH Title of that CVE") + }) + + Convey("image db download wait", func() { + count := 0 + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, + baseURL)) + args := []string{"image", "repo:vuln"} + defer os.Remove(configPath) + cmd := NewCVESCommand(mockService{ + getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username, password, + imageName, searchedCVE string) (*cveResult, error, + ) { + if count == 0 { + count++ + fmt.Println("Count:", count) + + return &cveResult{}, zerr.ErrCVEDBNotFound + } + + return &cveResult{}, zerr.ErrInjected + }, + }) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready") + }) + }) +} + +func TestCVECommandREST(t *testing.T) { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + cm := test.NewControllerManager(ctlr) + + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + Convey("commands without gql", t, func() { + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, baseURL)) + defer os.Remove(configPath) + + Convey("cveid", func() { + args := []string{"cveid", "CVE-1942"} + cmd := NewCVESCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("cveid error", func() { + // too many args + args := []string{"too", "many", "args"} + cmd := NewImagesByCVEIDCommand(mockService{}) + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + + // bad args + args = []string{"not-a-cve-id"} + cmd = NewImagesByCVEIDCommand(mockService{}) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + // no URL + args = []string{"CVE-1942"} + cmd = NewImagesByCVEIDCommand(mockService{}) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("fixed command", func() { + args := []string{"fixed", "image-name", "CVE-123"} + cmd := NewCVESCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("fixed command error", func() { + // too many args + args := []string{"too", "many", "args", "args"} + cmd := NewFixedTagsCommand(mockService{}) + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + + // bad args + args = []string{"repo-tag-instead-of-just-repo:fail-here", "CVE-123"} + cmd = NewFixedTagsCommand(mockService{}) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + // no URL + args = []string{"CVE-1942"} + cmd = NewFixedTagsCommand(mockService{}) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("image", func() { + args := []string{"image", "repo:tag"} + cmd := NewCVESCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("image command error", func() { + // too many args + args := []string{"too", "many", "args", "args"} + cmd := NewCveForImageCommand(mockService{}) + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + + // bad args + args = []string{"repo-tag-instead-of-just-repo:fail-here", "CVE-123"} + cmd = NewCveForImageCommand(mockService{}) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + // no URL + args = []string{"CVE-1942"} + cmd = NewCveForImageCommand(mockService{}) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + }) +} + func MockNewCveCommand(searchService SearchService) *cobra.Command { searchCveParams := make(map[string]*string) @@ -1072,7 +1397,7 @@ func MockNewCveCommand(searchService SearchService) *cobra.Command { panic(err) } - configPath := path.Join(home + "/.zot") + configPath := path.Join(home, "/.zot") if len(args) > 0 { urlFromConfig, err := getConfigValue(configPath, args[0], "url") if err != nil { @@ -1082,12 +1407,12 @@ func MockNewCveCommand(searchService SearchService) *cobra.Command { } if urlFromConfig == "" { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } servURL = urlFromConfig } else { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } if len(args) > 0 { @@ -1157,7 +1482,7 @@ func MockSearchCve(searchConfig searchConfig) error { } } - return zotErrors.ErrInvalidFlagsCombination + return zerr.ErrInvalidFlagsCombination } func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo { @@ -1232,7 +1557,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo { descriptor, ok := repoMeta.Tags[inputTag] if !ok { - return false, zotErrors.ErrTagMetaNotFound + return false, zerr.ErrTagMetaNotFound } manifestDigestStr = descriptor.Digest @@ -1252,7 +1577,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo { err = json.Unmarshal(manifestData.ManifestBlob, &manifestContent) if err != nil { - return false, zotErrors.ErrScanNotSupported + return false, zerr.ErrScanNotSupported } for _, imageLayer := range manifestContent.Layers { @@ -1262,7 +1587,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo { return true, nil default: - return false, zotErrors.ErrScanNotSupported + return false, zerr.ErrScanNotSupported } } @@ -1284,12 +1609,12 @@ type mockServiceForRetry struct { } func (service *mockServiceForRetry) getImagesByCveID(ctx context.Context, config searchConfig, - username, password, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup, + username, password, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { service.retryCounter += 1 if service.retryCounter < service.succeedOn || service.succeedOn < 0 { - rch <- stringResult{"", zotErrors.ErrCVEDBNotFound} + rch <- stringResult{"", zerr.ErrCVEDBNotFound} close(rch) wtgrp.Done() diff --git a/pkg/cli/cves_cmd.go b/pkg/cli/cves_cmd.go new file mode 100644 index 00000000..f3ff3498 --- /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, "f", "", "Specify output format [text/json/yaml]") + cvesCmd.PersistentFlags().Bool(cmdflags.VerboseFlag, false, "Show verbose output") + cvesCmd.PersistentFlags().Bool(cmdflags.DebugFlag, false, "Show debug output") + + cvesCmd.AddCommand(NewCveForImageCommand(searchService)) + cvesCmd.AddCommand(NewImagesByCVEIDCommand(searchService)) + cvesCmd.AddCommand(NewFixedTagsCommand(searchService)) + + return cvesCmd +} diff --git a/pkg/cli/cves_sub_cmd.go b/pkg/cli/cves_sub_cmd.go new file mode 100644 index 00000000..9da7e185 --- /dev/null +++ b/pkg/cli/cves_sub_cmd.go @@ -0,0 +1,128 @@ +//go:build search +// +build search + +package cli + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/cli/cmdflags" + zcommon "zotregistry.io/zot/pkg/common" +) + +const ( + maxRetries = 20 +) + +func NewCveForImageCommand(searchService SearchService) *cobra.Command { + var searchedCVEID string + + cveForImageCmd := &cobra.Command{ + Use: "image [repo:tag]|[repo@digest]", + Short: "List CVEs by REPO:TAG or REPO@DIGEST", + Long: `List CVEs by REPO:TAG or REPO@DIGEST`, + Args: OneImageWithRefArg, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + err = CheckExtEndPointQuery(searchConfig, CVEListForImageQuery()) + if err != nil { + return fmt.Errorf("%w: '%s'", err, CVEListForImageQuery().Name) + } + + image := args[0] + + return SearchCVEForImageGQL(searchConfig, image, searchedCVEID) + }, + } + + cveForImageCmd.Flags().StringVar(&searchedCVEID, cmdflags.SearchedCVEID, "", "Search for a specific CVE by name/id") + + return cveForImageCmd +} + +func NewImagesByCVEIDCommand(searchService SearchService) *cobra.Command { + var repo string + + imagesByCVEIDCmd := &cobra.Command{ + Use: "cveid [cveId]", + Short: "List images affected by a CVE", + Long: `List images affected by a CVE`, + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + + if !strings.HasPrefix(args[0], "CVE") { + return fmt.Errorf("%w: expected a cveid 'CVE-...' got '%s'", zerr.ErrInvalidCLIParameter, args[0]) + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + err = CheckExtEndPointQuery(searchConfig, ImageListForCVEQuery()) + if err != nil { + return fmt.Errorf("%w: '%s'", err, ImageListForCVEQuery().Name) + } + + searchedCVEID := args[0] + + return SearchImagesByCVEIDGQL(searchConfig, repo, searchedCVEID) + }, + } + + imagesByCVEIDCmd.Flags().StringVar(&repo, "repo", "", "Search for a specific CVE by name/id") + + return imagesByCVEIDCmd +} + +func NewFixedTagsCommand(searchService SearchService) *cobra.Command { + fixedTagsCmd := &cobra.Command{ + Use: "fixed [repo] [cveId]", + Short: "List tags where a CVE is fixedRetryWithContext", + Long: `List tags where a CVE is fixedRetryWithContext`, + Args: func(cmd *cobra.Command, args []string) error { + const argCount = 2 + + if err := cobra.ExactArgs(argCount)(cmd, args); err != nil { + return err + } + + if !zcommon.CheckIsCorrectRepoNameFormat(args[0]) { + return fmt.Errorf("%w: expected a valid repo name for first argument '%s'", zerr.ErrInvalidCLIParameter, args[0]) + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + err = CheckExtEndPointQuery(searchConfig, ImageListWithCVEFixedQuery()) + if err != nil { + return fmt.Errorf("%w: '%s'", err, ImageListWithCVEFixedQuery().Name) + } + + repo := args[0] + searchedCVEID := args[1] + + return SearchFixedTagsGQL(searchConfig, repo, searchedCVEID) + }, + } + + return fixedTagsCmd +} diff --git a/pkg/cli/discover.go b/pkg/cli/discover.go index 21415332..b6e6cc04 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 { @@ -23,9 +27,19 @@ type schemaList struct { QueryType struct { Fields []field `json:"fields"` } `json:"queryType"` //nolint:tagliatelle // graphQL schema + Types []typeInfo `json:"types"` } `json:"__schema"` //nolint:tagliatelle // graphQL schema } `json:"data"` - Errors []common.ErrorGQL `json:"errors"` + Errors []zcommon.ErrorGQL `json:"errors"` +} + +type typeInfo struct { + Name string `json:"name"` + Fields []typeField `json:"fields"` +} + +type typeField struct { + Name string `json:"name"` } func containsGQLQuery(queryList []field, query string) bool { @@ -38,6 +52,54 @@ func containsGQLQuery(queryList []field, query string) bool { return false } +func containsGQLQueryWithParams(queryList []field, serverGQLTypesList []typeInfo, requiredQueries ...GQLQuery) error { + serverGQLTypes := map[string][]typeField{} + + for _, typeInfo := range serverGQLTypesList { + serverGQLTypes[typeInfo.Name] = typeInfo.Fields + } + + for _, reqQuery := range requiredQueries { + foundQuery := false + + for _, query := range queryList { + if query.Name == reqQuery.Name && haveSameArgs(query, reqQuery) { + foundQuery = true + } + } + + if !foundQuery { + return fmt.Errorf("%w: %s", zerr.ErrGQLQueryNotSupported, reqQuery.Name) + } + + // let's check just the name of the returned type + returnType := reqQuery.ReturnType.Name + + // we can next define fields of the returned types and check them recursively + // for now we will just check the name of the returned type to be known by the server + _, ok := serverGQLTypes[returnType] + if !ok { + return fmt.Errorf("%w: server doesn't support needed type '%s'", zerr.ErrGQLQueryNotSupported, returnType) + } + } + + return nil +} + +func haveSameArgs(query field, reqQuery GQLQuery) bool { + if len(query.Args) != len(reqQuery.Args) { + return false + } + + for i := range query.Args { + if query.Args[i].Name != reqQuery.Args[i] { + return false + } + } + + return true +} + func checkExtEndPoint(config searchConfig) bool { username, password := getUsernameAndPassword(*config.user) ctx := context.Background() @@ -59,7 +121,7 @@ func checkExtEndPoint(config searchConfig) bool { searchEnabled := false for _, extension := range discoverResponse.Extensions { - if extension.Name == "_zot" { + if extension.Name == constants.BaseExtension { for _, endpoint := range extension.Endpoints { if endpoint == constants.FullSearchPrefix { searchEnabled = true @@ -99,3 +161,80 @@ func checkExtEndPoint(config searchConfig) bool { return containsGQLQuery(queryResponse.Data.Schema.QueryType.Fields, "ImageList") } + +func CheckExtEndPointQuery(config searchConfig, requiredQueries ...GQLQuery) error { + username, password := getUsernameAndPassword(*config.user) + ctx := context.Background() + + discoverEndPoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("%s%s", + constants.RoutePrefix, constants.ExtOciDiscoverPrefix)) + if err != nil { + return err + } + + discoverResponse := &distext.ExtensionList{} + + _, err = makeGETRequest(ctx, discoverEndPoint, username, password, *config.verifyTLS, + *config.debug, &discoverResponse, config.resultWriter) + if err != nil { + return err + } + + searchEnabled := false + + for _, extension := range discoverResponse.Extensions { + if extension.Name == constants.BaseExtension { + for _, endpoint := range extension.Endpoints { + if endpoint == constants.FullSearchPrefix { + searchEnabled = true + } + } + } + } + + if !searchEnabled { + return fmt.Errorf("%w: search extension gql endpoints not found", zerr.ErrExtensionNotEnabled) + } + + searchEndPoint, _ := combineServerAndEndpointURL(*config.servURL, constants.FullSearchPrefix) + + schemaQuery := ` + { + __schema() { + queryType { + fields { + name + args { + name + } + type { + name + kind + } + } + __typename + } + types { + name + fields { + name + } + } + } + }` + + queryResponse := &schemaList{} + + err = makeGraphQLRequest(ctx, searchEndPoint, schemaQuery, username, password, *config.verifyTLS, + *config.debug, queryResponse, config.resultWriter) + if err != nil { + return fmt.Errorf("gql query failed: %w", err) + } + + if err = checkResultGraphQLQuery(ctx, err, queryResponse.Errors); err != nil { + return fmt.Errorf("gql query failed: %w", err) + } + + return containsGQLQueryWithParams(queryResponse.Data.Schema.QueryType.Fields, + queryResponse.Data.Schema.Types, requiredQueries...) +} diff --git a/pkg/cli/gql_queries.go b/pkg/cli/gql_queries.go new file mode 100644 index 00000000..e006e3a5 --- /dev/null +++ b/pkg/cli/gql_queries.go @@ -0,0 +1,116 @@ +//go:build search +// +build search + +package cli + +type GQLField struct { + Name string + Type GQLType +} + +type GQLType struct { + Name string + Fields []GQLField +} + +type GQLQuery struct { + Name string + Args []string + ReturnType GQLType +} + +func CVEResultForImage() GQLType { + return GQLType{ + Name: "CVEResultForImage", + } +} + +func PaginatedImagesResult() GQLType { + return GQLType{ + Name: "PaginatedImagesResult", + } +} + +func Referrer() GQLType { + return GQLType{ + Name: "Referrer", + } +} + +func GlobalSearchResult() GQLType { + return GQLType{ + Name: "GlobalSearchResult", + } +} + +func ImageListQuery() GQLQuery { + return GQLQuery{ + Name: "ImageList", + Args: []string{"repo", "requestedPage"}, + ReturnType: PaginatedImagesResult(), + } +} + +func ImageListForDigestQuery() GQLQuery { + return GQLQuery{ + Name: "ImageListForDigest", + Args: []string{"id", "requestedPage"}, + ReturnType: PaginatedImagesResult(), + } +} + +func BaseImageListQuery() GQLQuery { + return GQLQuery{ + Name: "BaseImageList", + Args: []string{"image", "digest", "requestedPage"}, + ReturnType: PaginatedImagesResult(), + } +} + +func DerivedImageListQuery() GQLQuery { + return GQLQuery{ + Name: "DerivedImageList", + Args: []string{"image", "digest", "requestedPage"}, + ReturnType: PaginatedImagesResult(), + } +} + +func CVEListForImageQuery() GQLQuery { + return GQLQuery{ + Name: "CVEListForImage", + Args: []string{"image", "requestedPage", "searchedCVE"}, + ReturnType: CVEResultForImage(), + } +} + +func ImageListForCVEQuery() GQLQuery { + return GQLQuery{ + Name: "ImageListForCVE", + Args: []string{"id", "filter", "requestedPage"}, + ReturnType: PaginatedImagesResult(), + } +} + +func ImageListWithCVEFixedQuery() GQLQuery { + return GQLQuery{ + Name: "ImageListWithCVEFixed", + Args: []string{"id", "image", "filter", "requestedPage"}, + ReturnType: PaginatedImagesResult(), + } +} + +func ReferrersQuery() GQLQuery { + return GQLQuery{ + Name: "Referrers", + Args: []string{"repo", "digest", "type"}, + ReturnType: Referrer(), + } +} + +func GlobalSearchQuery() GQLQuery { + return GQLQuery{ + Name: "GlobalSearch", + Args: []string{"query", "filter", "requestedPage"}, + ReturnType: GlobalSearchResult(), + } +} diff --git a/pkg/cli/gql_queries_test.go b/pkg/cli/gql_queries_test.go new file mode 100644 index 00000000..af98f4e2 --- /dev/null +++ b/pkg/cli/gql_queries_test.go @@ -0,0 +1,93 @@ +//go:build search +// +build search + +package cli //nolint:testpackage + +import ( + "io" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "zotregistry.io/zot/pkg/api" + "zotregistry.io/zot/pkg/api/config" + extconf "zotregistry.io/zot/pkg/extensions/config" + "zotregistry.io/zot/pkg/test" +) + +func TestGQLQueries(t *testing.T) { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + dir := t.TempDir() + conf.Storage.RootDirectory = dir + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + }, + } + + ctlr := api.NewController(conf) + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(conf.HTTP.Port) + + defer cm.StopServer() + + searchConfig := searchConfig{ + servURL: &baseURL, + user: ref(""), + verifyTLS: ref(false), + debug: ref(false), + resultWriter: io.Discard, + } + + Convey("Make sure the current CLI used the right queries in case they change", t, func() { + Convey("ImageList", func() { + err := CheckExtEndPointQuery(searchConfig, ImageListQuery()) + So(err, ShouldBeNil) + }) + + Convey("ImageListForDigest", func() { + err := CheckExtEndPointQuery(searchConfig, ImageListForDigestQuery()) + So(err, ShouldBeNil) + }) + + Convey("BaseImageList", func() { + err := CheckExtEndPointQuery(searchConfig, BaseImageListQuery()) + So(err, ShouldBeNil) + }) + + Convey("DerivedImageList", func() { + err := CheckExtEndPointQuery(searchConfig, DerivedImageListQuery()) + So(err, ShouldBeNil) + }) + + Convey("CVEListForImage", func() { + err := CheckExtEndPointQuery(searchConfig, CVEListForImageQuery()) + So(err, ShouldBeNil) + }) + + Convey("ImageListForCVE", func() { + err := CheckExtEndPointQuery(searchConfig, ImageListForCVEQuery()) + So(err, ShouldBeNil) + }) + + Convey("ImageListWithCVEFixed", func() { + err := CheckExtEndPointQuery(searchConfig, ImageListWithCVEFixedQuery()) + So(err, ShouldBeNil) + }) + + Convey("Referrers", func() { + err := CheckExtEndPointQuery(searchConfig, ReferrersQuery()) + So(err, ShouldBeNil) + }) + + Convey("GlobalSearch", func() { + err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery()) + So(err, ShouldBeNil) + }) + }) +} diff --git a/pkg/cli/image_cmd.go b/pkg/cli/image_cmd.go index 8661733c..f6679d55 100644 --- a/pkg/cli/image_cmd.go +++ b/pkg/cli/image_cmd.go @@ -12,7 +12,7 @@ import ( "github.com/briandowns/spinner" "github.com/spf13/cobra" - zotErrors "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" ) //nolint:dupl @@ -24,16 +24,16 @@ func NewImageCommand(searchService SearchService) *cobra.Command { var isSpinner, verifyTLS, verbose, debug bool imageCmd := &cobra.Command{ - Use: "images [config-name]", - Short: "List images hosted on the zot registry", - Long: `List images hosted on the zot registry`, + Use: "image [config-name]", + Short: "DEPRECATED (see images)", + Long: `DEPRECATED (see images)! List images hosted on the zot registry`, RunE: func(cmd *cobra.Command, args []string) error { home, err := os.UserHomeDir() if err != nil { panic(err) } - configPath := path.Join(home + "/.zot") + configPath := path.Join(home, "/.zot") if servURL == "" { if len(args) > 0 { urlFromConfig, err := getConfigValue(configPath, args[0], "url") @@ -44,12 +44,12 @@ func NewImageCommand(searchService SearchService) *cobra.Command { } if urlFromConfig == "" { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } servURL = urlFromConfig } else { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } } @@ -129,11 +129,12 @@ func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*stri searchImageParams["baseImage"] = imageCmd.Flags().StringP("base-images", "b", "", "List images that are base for the given image") - imageCmd.Flags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned") - imageCmd.Flags().StringVarP(user, "user", "u", "", `User Credentials of zot server in "username:password" format`) - imageCmd.Flags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]") - imageCmd.Flags().BoolVar(verbose, "verbose", false, "Show verbose output") - imageCmd.Flags().BoolVar(debug, "debug", false, "Show debug output") + imageCmd.PersistentFlags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned") + imageCmd.PersistentFlags().StringVarP(user, "user", "u", "", + `User Credentials of zot server in "username:password" format`) + imageCmd.PersistentFlags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]") + imageCmd.PersistentFlags().BoolVar(verbose, "verbose", false, "Show verbose output") + imageCmd.PersistentFlags().BoolVar(debug, "debug", false, "Show debug output") } func searchImage(searchConfig searchConfig) error { @@ -156,7 +157,7 @@ func searchImage(searchConfig searchConfig) error { } } - return zotErrors.ErrInvalidFlagsCombination + return zerr.ErrInvalidFlagsCombination } const ( diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index b1b20d92..28604584 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" zlog "zotregistry.io/zot/pkg/log" @@ -74,7 +75,7 @@ func TestSearchImageCmd(t *testing.T) { cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) - So(err, ShouldEqual, zotErrors.ErrNoURLProvided) + So(err, ShouldEqual, zerr.ErrNoURLProvided) }) Convey("Test image invalid home directory", t, func() { @@ -130,7 +131,7 @@ func TestSearchImageCmd(t *testing.T) { cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) - So(err, ShouldEqual, zotErrors.ErrInvalidURL) + So(err, ShouldEqual, zerr.ErrInvalidURL) So(buff.String(), ShouldContainSubstring, "invalid URL format") }) @@ -253,7 +254,7 @@ func TestSearchImageCmd(t *testing.T) { imageCmd.SetArgs(args) err := imageCmd.Execute() So(err, ShouldNotBeNil) - So(err, ShouldEqual, zotErrors.ErrInvalidURL) + So(err, ShouldEqual, zerr.ErrInvalidURL) So(buff.String(), ShouldContainSubstring, "invalid URL format") }) }) @@ -1555,6 +1556,564 @@ func runDisplayIndexTests(baseURL string) { }) } +func TestImagesCommandGQL(t *testing.T) { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + }, + } + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + cm := test.NewControllerManager(ctlr) + + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + Convey("commands with gql", t, func() { + err := test.RemoveLocalStorageContents(ctlr.StoreController.DefaultStore) + So(err, ShouldBeNil) + + Convey("base and derived command", func() { + baseImage := test.CreateImageWith().LayerBlobs( + [][]byte{{1, 2, 3}, {11, 22, 33}}, + ).DefaultConfig().Build() + + derivedImage := test.CreateImageWith().LayerBlobs( + [][]byte{{1, 2, 3}, {11, 22, 33}, {44, 55, 66}}, + ).DefaultConfig().Build() + + err := test.UploadImage(baseImage, baseURL, "repo", "base") + So(err, ShouldBeNil) + + err = test.UploadImage(derivedImage, baseURL, "repo", "derived") + So(err, ShouldBeNil) + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + args := []string{"base", "repo:derived"} + cmd := NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo base linux/amd64 df554ddd false 699B") + + args = []string{"derived", "repo:base"} + cmd = NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + str = space.ReplaceAllString(buff.String(), " ") + actual = strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo derived linux/amd64 79f4b82e false 854B") + }) + + Convey("base and derived command errors", func() { + // too many parameters + buff := bytes.NewBufferString("") + args := []string{"too", "many", "args"} + cmd := NewImageBaseCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + + cmd = NewImageDerivedCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + // bad input + buff = bytes.NewBufferString("") + args = []string{"only-repo"} + cmd = NewImageBaseCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + cmd = NewImageDerivedCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + // no url + buff = bytes.NewBufferString("") + args = []string{"repo:tag"} + cmd = NewImageBaseCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + cmd = NewImageDerivedCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("digest command", func() { + image := test.CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build() + + err := test.UploadImage(image, baseURL, "repo", "img") + So(err, ShouldBeNil) + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + args := []string{"digest", image.DigestStr()} + cmd := NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, fmt.Sprintf("repo img linux/amd64 %s false 552B", + image.DigestStr()[7:7+8])) + }) + + Convey("digest command errors", func() { + // too many parameters + buff := bytes.NewBufferString("") + args := []string{"too", "many", "args"} + cmd := NewImageDigestCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + + // bad input + buff = bytes.NewBufferString("") + args = []string{"bad-digest"} + cmd = NewImageDigestCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + // no url + buff = bytes.NewBufferString("") + args = []string{godigest.FromString("str").String()} + cmd = NewImageDigestCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("list command", func() { + image := test.CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build() + + err := test.UploadImage(image, baseURL, "repo", "img") + So(err, ShouldBeNil) + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + args := []string{"list"} + cmd := NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + fmt.Println(actual) + So(actual, ShouldContainSubstring, fmt.Sprintf("repo img linux/amd64 %s false 552B", + image.DigestStr()[7:7+8])) + fmt.Println(actual) + }) + + Convey("list command errors", func() { + // too many parameters + buff := bytes.NewBufferString("") + args := []string{"repo:img", "arg"} + cmd := NewImageListCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + + // no url + buff = bytes.NewBufferString("") + args = []string{} + cmd = NewImageListCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("name command", func() { + image := test.CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build() + + err := test.UploadImage(image, baseURL, "repo", "img") + So(err, ShouldBeNil) + + err = test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "img2") + So(err, ShouldBeNil) + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + args := []string{"name", "repo:img"} + cmd := NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + fmt.Println(actual) + So(actual, ShouldContainSubstring, fmt.Sprintf("repo img linux/amd64 %s false 552B", + image.DigestStr()[7:7+8])) + fmt.Println(actual) + }) + + Convey("name command errors", func() { + // too many parameters + buff := bytes.NewBufferString("") + args := []string{"repo:img", "arg"} + cmd := NewImageNameCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + + // bad input + buff = bytes.NewBufferString("") + args = []string{":tag"} + cmd = NewImageNameCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + // no url + buff = bytes.NewBufferString("") + args = []string{"repo:tag"} + cmd = NewImageNameCommand(NewSearchService()) + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("CVE", func() { + vulnImage := test.CreateDefaultVulnerableImage() + err := test.UploadImage(vulnImage, baseURL, "repo", "vuln") + So(err, ShouldBeNil) + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + args := []string{"cve", "repo:vuln"} + defer os.Remove(configPath) + cmd := NewImagesCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "dummyCVEID HIGH Title of that CVE") + }) + + Convey("CVE errors", func() { + count := 0 + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + args := []string{"cve", "repo:vuln"} + defer os.Remove(configPath) + cmd := NewImagesCommand(mockService{ + getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username, password, + imageName, searchedCVE string) (*cveResult, error, + ) { + if count == 0 { + count++ + fmt.Println("Count:", count) + + return &cveResult{}, zerr.ErrCVEDBNotFound + } + + return &cveResult{}, zerr.ErrInjected + }, + }) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "[warning] CVE DB is not ready") + }) + }) + + Convey("Config error", t, func() { + args := []string{"base", "repo:derived"} + cmd := NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + So(err, ShouldNotBeNil) + + args = []string{"derived", "repo:base"} + cmd = NewImagesCommand(NewSearchService()) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + args = []string{"digest", ispec.DescriptorEmptyJSON.Digest.String()} + cmd = NewImagesCommand(NewSearchService()) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + args = []string{"list"} + cmd = NewImagesCommand(NewSearchService()) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + args = []string{"name", "repo:img"} + cmd = NewImagesCommand(NewSearchService()) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + args = []string{"cve", "repo:vuln"} + cmd = NewImagesCommand(mockService{}) + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) +} + +func TestImageCommandREST(t *testing.T) { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + cm := test.NewControllerManager(ctlr) + + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + Convey("commands without gql", t, func() { + err := test.RemoveLocalStorageContents(ctlr.StoreController.DefaultStore) + So(err, ShouldBeNil) + + Convey("base and derived command", func() { + baseImage := test.CreateImageWith().LayerBlobs( + [][]byte{{1, 2, 3}, {11, 22, 33}}, + ).DefaultConfig().Build() + + derivedImage := test.CreateImageWith().LayerBlobs( + [][]byte{{1, 2, 3}, {11, 22, 33}, {44, 55, 66}}, + ).DefaultConfig().Build() + + err := test.UploadImage(baseImage, baseURL, "repo", "base") + So(err, ShouldBeNil) + + err = test.UploadImage(derivedImage, baseURL, "repo", "derived") + So(err, ShouldBeNil) + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + args := []string{"base", "repo:derived"} + cmd := NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + args = []string{"derived", "repo:base"} + cmd = NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff = bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("digest command", func() { + image := test.CreateRandomImage() + + err := test.UploadImage(image, baseURL, "repo", "img") + So(err, ShouldBeNil) + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + args := []string{"digest", image.DigestStr()} + cmd := NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("list command", func() { + image := test.CreateRandomImage() + + err := test.UploadImage(image, baseURL, "repo", "img") + So(err, ShouldBeNil) + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + args := []string{"list"} + cmd := NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + fmt.Println(buff.String()) + fmt.Println() + }) + + Convey("name command", func() { + image := test.CreateRandomImage() + + err := test.UploadImage(image, baseURL, "repo", "img") + So(err, ShouldBeNil) + + err = test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "img2") + So(err, ShouldBeNil) + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + args := []string{"name", "repo:img"} + cmd := NewImagesCommand(NewSearchService()) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + fmt.Println(buff.String()) + fmt.Println() + }) + + Convey("CVE", func() { + vulnImage := test.CreateDefaultVulnerableImage() + err := test.UploadImage(vulnImage, baseURL, "repo", "vuln") + So(err, ShouldBeNil) + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, + baseURL)) + args := []string{"cve", "repo:vuln"} + defer os.Remove(configPath) + cmd := NewImagesCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + }) +} + func uploadTestMultiarch(baseURL string) { // ------- Define Image1 layer11 := []byte{11, 12, 13, 14} @@ -1606,7 +2165,7 @@ func MockNewImageCommand(searchService SearchService) *cobra.Command { panic(err) } - configPath := path.Join(home + "/.zot") + configPath := path.Join(home, "/.zot") if len(args) > 0 { urlFromConfig, err := getConfigValue(configPath, args[0], "url") if err != nil { @@ -1616,12 +2175,12 @@ func MockNewImageCommand(searchService SearchService) *cobra.Command { } if urlFromConfig == "" { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } servURL = urlFromConfig } else { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } if len(args) > 0 { @@ -1679,7 +2238,7 @@ func MockSearchImage(searchConfig searchConfig) error { } } - return zotErrors.ErrInvalidFlagsCombination + return zerr.ErrInvalidFlagsCombination } func uploadManifest(url string) error { @@ -1895,7 +2454,73 @@ func uploadManifestDerivedBase(url string) error { return nil } -type mockService struct{} +type mockService struct { + getAllImagesFn func(ctx context.Context, config searchConfig, username, password string, + channel chan stringResult, wtgrp *sync.WaitGroup) + + getImagesGQLFn func(ctx context.Context, config searchConfig, username, password string, + imageName string) (*common.ImageListResponse, error) + + getImageByNameFn func(ctx context.Context, config searchConfig, + username, password, imageName string, channel chan stringResult, wtgrp *sync.WaitGroup, + ) + + getFixedTagsForCVEFn func(ctx context.Context, config searchConfig, + username, password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, + ) + + getImageByNameAndCVEIDFn func(ctx context.Context, config searchConfig, username, + password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, + ) + + getImagesByCveIDFn func(ctx context.Context, config searchConfig, username, password, cveid string, + rch chan stringResult, wtgrp *sync.WaitGroup, + ) + + getImagesByDigestFn func(ctx context.Context, config searchConfig, username, + password, digest string, rch chan stringResult, wtgrp *sync.WaitGroup, + ) + + getReferrersFn func(ctx context.Context, config searchConfig, username, password string, + repo, digest string, + ) (referrersResult, error) + + globalSearchGQLFn func(ctx context.Context, config searchConfig, username, password string, + query string, + ) (*common.GlobalSearch, error) + + getReferrersGQLFn func(ctx context.Context, config searchConfig, username, password string, + repo, digest string, + ) (*common.ReferrersResp, error) + + getDerivedImageListGQLFn func(ctx context.Context, config searchConfig, username, password string, + derivedImage string, + ) (*common.DerivedImageListResponse, error) + + getBaseImageListGQLFn func(ctx context.Context, config searchConfig, username, password string, + derivedImage string, + ) (*common.BaseImageListResponse, error) + + getImagesForDigestGQLFn func(ctx context.Context, config searchConfig, username, password string, + digest string, + ) (*common.ImagesForDigest, error) + + getCveByImageGQLFn func(ctx context.Context, config searchConfig, username, password, + imageName, searchedCVE string, + ) (*cveResult, error) + + getImagesByCveIDGQLFn func(ctx context.Context, config searchConfig, username, password string, + digest string, + ) (*common.ImagesForCve, error) + + getTagsForCVEGQLFn func(ctx context.Context, config searchConfig, username, password, + imageName, cveID string, + ) (*common.ImagesForCve, error) + + getFixedTagsForCVEGQLFn func(ctx context.Context, config searchConfig, username, password, + imageName, cveID string, + ) (*common.ImageListWithCVEFixedResponse, error) +} func (service mockService) getRepos(ctx context.Context, config searchConfig, username, password string, channel chan stringResult, wtgrp *sync.WaitGroup, @@ -1903,39 +2528,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 @@ -1944,6 +2586,10 @@ func (service mockService) globalSearchGQL(ctx context.Context, config searchCon func (service mockService) getReferrersGQL(ctx context.Context, config searchConfig, username, password string, repo, digest string, ) (*common.ReferrersResp, error) { + if service.getReferrersGQLFn != nil { + return service.getReferrersGQLFn(ctx, config, username, password, repo, digest) + } + return &common.ReferrersResp{ ReferrersResult: common.ReferrersResult{ Referrers: []common.Referrer{ @@ -1961,6 +2607,10 @@ func (service mockService) getReferrersGQL(ctx context.Context, config searchCon func (service mockService) getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string, derivedImage string, ) (*common.DerivedImageListResponse, error) { + if service.getDerivedImageListGQLFn != nil { + return service.getDerivedImageListGQLFn(ctx, config, username, password, derivedImage) + } + imageListGQLResponse := &common.DerivedImageListResponse{} imageListGQLResponse.DerivedImageList.Results = []common.ImageSummary{ { @@ -1982,8 +2632,12 @@ func (service mockService) getDerivedImageListGQL(ctx context.Context, config se } func (service mockService) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string, - derivedImage string, + baseImage string, ) (*common.BaseImageListResponse, error) { + if service.getBaseImageListGQLFn != nil { + return service.getBaseImageListGQLFn(ctx, config, username, password, baseImage) + } + imageListGQLResponse := &common.BaseImageListResponse{} imageListGQLResponse.BaseImageList.Results = []common.ImageSummary{ { @@ -2007,6 +2661,10 @@ func (service mockService) getBaseImageListGQL(ctx context.Context, config searc func (service mockService) getImagesGQL(ctx context.Context, config searchConfig, username, password string, imageName string, ) (*common.ImageListResponse, error) { + if service.getImagesGQLFn != nil { + return service.getImagesGQLFn(ctx, config, username, password, imageName) + } + imageListGQLResponse := &common.ImageListResponse{} imageListGQLResponse.PaginatedImagesResult.Results = []common.ImageSummary{ { @@ -2029,9 +2687,13 @@ func (service mockService) getImagesGQL(ctx context.Context, config searchConfig return imageListGQLResponse, nil } -func (service mockService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string, +func (service mockService) getImagesForDigestGQL(ctx context.Context, config searchConfig, username, password string, digest string, ) (*common.ImagesForDigest, error) { + if service.getImagesForDigestGQLFn != nil { + return service.getImagesForDigestGQLFn(ctx, config, username, password, digest) + } + imageListGQLResponse := &common.ImagesForDigest{} imageListGQLResponse.Results = []common.ImageSummary{ { @@ -2057,6 +2719,10 @@ func (service mockService) getImagesByDigestGQL(ctx context.Context, config sear func (service mockService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string, digest string, ) (*common.ImagesForCve, error) { + if service.getImagesByCveIDGQLFn != nil { + return service.getImagesByCveIDGQLFn(ctx, config, username, password, digest) + } + imagesForCve := &common.ImagesForCve{ Errors: nil, ImagesForCVEList: struct { @@ -2075,6 +2741,10 @@ func (service mockService) getImagesByCveIDGQL(ctx context.Context, config searc func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string, ) (*common.ImagesForCve, error) { + if service.getTagsForCVEGQLFn != nil { + return service.getTagsForCVEGQLFn(ctx, config, username, password, imageName, cveID) + } + images := &common.ImagesForCve{ Errors: nil, ImagesForCVEList: struct { @@ -2082,6 +2752,10 @@ func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchCo }{}, } + if imageName == "" { + imageName = "image-name" + } + images.Errors = nil mockedImage := service.getMockedImageByName(imageName) @@ -2093,6 +2767,10 @@ func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchCo func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string, ) (*common.ImageListWithCVEFixedResponse, error) { + if service.getFixedTagsForCVEGQLFn != nil { + return service.getFixedTagsForCVEGQLFn(ctx, config, username, password, imageName, cveID) + } + fixedTags := &common.ImageListWithCVEFixedResponse{ Errors: nil, ImageListWithCVEFixed: struct { @@ -2111,6 +2789,9 @@ func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config sea func (service mockService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password, imageName, searchedCVE string, ) (*cveResult, error) { + if service.getCveByImageGQLFn != nil { + return service.getCveByImageGQLFn(ctx, config, username, password, imageName, searchedCVE) + } cveRes := &cveResult{} cveRes.Data = cveData{ CVEListForImage: cveListForImage{ @@ -2141,6 +2822,7 @@ func (service mockService) getMockedImageByName(imageName string) imageStruct { image := imageStruct{} image.RepoName = imageName image.Tag = "tag" + image.MediaType = ispec.MediaTypeImageManifest image.Manifests = []common.ManifestSummary{ { Digest: godigest.FromString("Digest").String(), @@ -2160,6 +2842,12 @@ func (service mockService) getAllImages(ctx context.Context, config searchConfig defer wtgrp.Done() defer close(channel) + if service.getAllImagesFn != nil { + service.getAllImagesFn(ctx, config, username, password, channel, wtgrp) + + return + } + image := &imageStruct{} image.RepoName = "randomimageName" image.Tag = "tag" @@ -2192,6 +2880,12 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf defer wtgrp.Done() defer close(channel) + if service.getImageByNameFn != nil { + service.getImageByNameFn(ctx, config, username, password, imageName, channel, wtgrp) + + return + } + image := &imageStruct{} image.RepoName = imageName image.Tag = "tag" @@ -2257,26 +2951,62 @@ func (service mockService) getCveByImage(ctx context.Context, config searchConfi } func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig, - username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup, + username, password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { + if service.getFixedTagsForCVEFn != nil { + defer wtgrp.Done() + defer close(rch) + + service.getFixedTagsForCVEFn(ctx, config, username, password, imageName, cveid, rch, wtgrp) + + return + } + service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp) } func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, - password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup, + password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { + if service.getImageByNameAndCVEIDFn != nil { + defer wtgrp.Done() + defer close(rch) + + service.getImageByNameAndCVEIDFn(ctx, config, username, password, imageName, cveid, rch, wtgrp) + + return + } + service.getImageByName(ctx, config, username, password, imageName, rch, wtgrp) } -func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string, +func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { + if service.getImagesByCveIDFn != nil { + defer wtgrp.Done() + defer close(rch) + + service.getImagesByCveIDFn(ctx, config, username, password, cveid, rch, wtgrp) + + return + } + service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp) } func (service mockService) getImagesByDigest(ctx context.Context, config searchConfig, username, password, digest string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { + if service.getImagesByDigestFn != nil { + defer wtgrp.Done() + defer close(rch) + + service.getImagesByDigestFn(ctx, config, username, password, digest, rch, wtgrp) + + return + } + service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp) } @@ -2288,7 +3018,7 @@ func makeConfigFile(content string) string { panic(err) } - configPath := path.Join(home + "/.zot") + configPath := path.Join(home, "/.zot") if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil { panic(err) diff --git a/pkg/cli/images_cmd.go b/pkg/cli/images_cmd.go new file mode 100644 index 00000000..218b6c20 --- /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, "f", "", "Specify output format [text/json/yaml]") + imageCmd.PersistentFlags().Bool(cmdflags.VerboseFlag, false, "Show verbose output") + imageCmd.PersistentFlags().Bool(cmdflags.DebugFlag, false, "Show debug output") + + imageCmd.AddCommand(NewImageListCommand(searchService)) + imageCmd.AddCommand(NewImageCVEListCommand(searchService)) + imageCmd.AddCommand(NewImageBaseCommand(searchService)) + imageCmd.AddCommand(NewImageDerivedCommand(searchService)) + imageCmd.AddCommand(NewImageDigestCommand(searchService)) + imageCmd.AddCommand(NewImageNameCommand(searchService)) + + return imageCmd +} diff --git a/pkg/cli/images_sub_cmd.go b/pkg/cli/images_sub_cmd.go new file mode 100644 index 00000000..9ba4a786 --- /dev/null +++ b/pkg/cli/images_sub_cmd.go @@ -0,0 +1,284 @@ +//go:build search +// +build search + +package cli + +import ( + "fmt" + "os" + "path" + + "github.com/briandowns/spinner" + "github.com/spf13/cobra" + + zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/cli/cmdflags" + zcommon "zotregistry.io/zot/pkg/common" +) + +func NewImageListCommand(searchService SearchService) *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all images", + Long: "List all images", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + if err := CheckExtEndPointQuery(searchConfig, ImageListQuery()); err == nil { + return SearchAllImagesGQL(searchConfig) + } + + return SearchAllImages(searchConfig) + }, + } +} + +func NewImageCVEListCommand(searchService SearchService) *cobra.Command { + var searchedCVEID string + + cmd := &cobra.Command{ + Use: "cve [repo-name:tag][repo-name@digest]", + Short: "List all CVE's of the image", + Long: "List all CVE's of the image", + Args: OneImageWithRefArg, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + if err := CheckExtEndPointQuery(searchConfig, CVEListForImageQuery()); err == nil { + image := args[0] + + return SearchCVEForImageGQL(searchConfig, image, searchedCVEID) + } else { + return err + } + }, + } + + cmd.Flags().StringVar(&searchedCVEID, cmdflags.SearchedCVEID, "", "Search for a specific CVE by name/id") + + return cmd +} + +func NewImageDerivedCommand(searchService SearchService) *cobra.Command { + cmd := &cobra.Command{ + Use: "derived [repo-name:tag][repo-name@digest]", + Short: "List images that are derived from given image", + Long: "List images that are derived from given image", + Args: OneImageWithRefArg, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + if err := CheckExtEndPointQuery(searchConfig, DerivedImageListQuery()); err == nil { + return SearchDerivedImageListGQL(searchConfig, args[0]) + } else { + return err + } + }, + } + + return cmd +} + +func NewImageBaseCommand(searchService SearchService) *cobra.Command { + cmd := &cobra.Command{ + Use: "base [repo-name:tag][repo-name@digest]", + Short: "List images that are base for the given image", + Long: "List images that are base for the given image", + Args: OneImageWithRefArg, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + if err := CheckExtEndPointQuery(searchConfig, BaseImageListQuery()); err == nil { + return SearchBaseImageListGQL(searchConfig, args[0]) + } else { + return err + } + }, + } + + return cmd +} + +func NewImageDigestCommand(searchService SearchService) *cobra.Command { + cmd := &cobra.Command{ + Use: "digest [digest]", + Short: "List images that contain a blob(manifest, config or layer) with the given digest", + Long: "List images that contain a blob(manifest, config or layer) with the given digest", + Args: OneDigestArg, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + if err := CheckExtEndPointQuery(searchConfig, ImageListForDigestQuery()); err == nil { + return SearchImagesForDigestGQL(searchConfig, args[0]) + } else { + return err + } + }, + } + + return cmd +} + +func NewImageNameCommand(searchService SearchService) *cobra.Command { + cmd := &cobra.Command{ + Use: "name [repo:tag]", + Short: "List image details by name", + Long: "List image details by name", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + + image := args[0] + + if dir, _ := zcommon.GetImageDirAndTag(image); dir == "" { + return zerr.ErrInvalidRepoRefFormat + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + if err := CheckExtEndPointQuery(searchConfig, ImageListQuery()); err == nil { + return SearchImageByNameGQL(searchConfig, args[0]) + } + + return SearchImageByName(searchConfig, args[0]) + }, + } + + return cmd +} + +func GetSearchConfigFromFlags(cmd *cobra.Command, searchService SearchService) (searchConfig, error) { + serverURL, err := GetServerURLFromFlags(cmd) + if err != nil { + return searchConfig{}, err + } + + isSpinner, verifyTLS := GetCliConfigOptions(cmd) + + flags := cmd.Flags() + user := defaultIfError(flags.GetString(cmdflags.UserFlag)) + fixed := defaultIfError(flags.GetBool(cmdflags.FixedFlag)) + debug := defaultIfError(flags.GetBool(cmdflags.DebugFlag)) + verbose := defaultIfError(flags.GetBool(cmdflags.VerboseFlag)) + outputFormat := defaultIfError(flags.GetString(cmdflags.OutputFormatFlag)) + + spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr())) + spin.Prefix = prefix + + return searchConfig{ + params: map[string]*string{}, + searchService: searchService, + servURL: &serverURL, + user: &user, + outputFormat: &outputFormat, + verifyTLS: &verifyTLS, + fixedFlag: &fixed, + verbose: &verbose, + debug: &debug, + spinner: spinnerState{spin, isSpinner}, + resultWriter: cmd.OutOrStdout(), + }, nil +} + +func defaultIfError[T any](out T, err error) T { + var defaultVal T + + if err != nil { + return defaultVal + } + + return out +} + +func GetCliConfigOptions(cmd *cobra.Command) (bool, bool) { + configName, err := cmd.Flags().GetString(cmdflags.ConfigFlag) + if err != nil { + return false, false + } + + home, err := os.UserHomeDir() + if err != nil { + return false, false + } + + configDir := path.Join(home, "/.zot") + + isSpinner, err := parseBooleanConfig(configDir, configName, showspinnerConfig) + if err != nil { + return false, false + } + + verifyTLS, err := parseBooleanConfig(configDir, configName, verifyTLSConfig) + if err != nil { + return false, false + } + + return isSpinner, verifyTLS +} + +func GetServerURLFromFlags(cmd *cobra.Command) (string, error) { + serverURL, err := cmd.Flags().GetString(cmdflags.URLFlag) + if err == nil && serverURL != "" { + return serverURL, nil + } + + configName, err := cmd.Flags().GetString(cmdflags.ConfigFlag) + if err != nil { + return "", err + } + + if configName == "" { + return "", fmt.Errorf("%w: specify either '--%s' or '--%s' flags", zerr.ErrNoURLProvided, cmdflags.URLFlag, + cmdflags.ConfigFlag) + } + + serverURL, err = ReadServerURLFromConfig(configName) + if err != nil { + return serverURL, fmt.Errorf("reading url from config failed: %w", err) + } + + if serverURL == "" { + return "", fmt.Errorf("%w: url field from config is empty", zerr.ErrNoURLProvided) + } + + return serverURL, nil +} + +func ReadServerURLFromConfig(configName string) (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + + configDir := path.Join(home, "/.zot") + + urlFromConfig, err := getConfigValue(configDir, configName, "url") + if err != nil { + return "", err + } + + return urlFromConfig, nil +} diff --git a/pkg/cli/repo_cmd.go b/pkg/cli/repos_cmd.go similarity index 77% rename from pkg/cli/repo_cmd.go rename to pkg/cli/repos_cmd.go index 784de164..96ca7adc 100644 --- a/pkg/cli/repo_cmd.go +++ b/pkg/cli/repos_cmd.go @@ -10,7 +10,8 @@ import ( "github.com/briandowns/spinner" "github.com/spf13/cobra" - zotErrors "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/cli/cmdflags" ) const prefix = "Searching... " @@ -24,13 +25,14 @@ func NewRepoCommand(searchService SearchService) *cobra.Command { Use: "repos [config-name]", Short: "List all repositories", Long: `List all repositories`, + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { home, err := os.UserHomeDir() if err != nil { panic(err) } - configPath := path.Join(home + "/.zot") + configPath := path.Join(home, "/.zot") if servURL == "" { if len(args) > 0 { urlFromConfig, err := getConfigValue(configPath, args[0], "url") @@ -41,12 +43,12 @@ func NewRepoCommand(searchService SearchService) *cobra.Command { } if urlFromConfig == "" { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } servURL = urlFromConfig } else { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } } @@ -96,9 +98,12 @@ func NewRepoCommand(searchService SearchService) *cobra.Command { repoCmd.SetUsageTemplate(repoCmd.UsageTemplate() + usageFooter) - repoCmd.Flags().StringVar(&servURL, "url", "", "Specify zot server URL if config-name is not mentioned") - repoCmd.Flags().StringVarP(&user, "user", "u", "", `User Credentials of zot server in "username:password" format`) - repoCmd.Flags().BoolVar(&debug, "debug", false, "Show debug output") + repoCmd.AddCommand(NewListReposCommand(searchService)) + + repoCmd.Flags().StringVar(&servURL, cmdflags.URLFlag, "", "Specify zot server URL if config-name is not mentioned") + repoCmd.Flags().StringVarP(&user, cmdflags.UserFlag, "u", "", + `User Credentials of zot server in "username:password" format`) + repoCmd.Flags().BoolVar(&debug, cmdflags.DebugFlag, false, "Show debug output") return repoCmd } diff --git a/pkg/cli/repos_sub_cmd.go b/pkg/cli/repos_sub_cmd.go new file mode 100644 index 00000000..daef084c --- /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 00000000..6af55084 --- /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 dc5d2db5..2d4876a8 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -18,10 +18,11 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" + "zotregistry.io/zot/pkg/cli/cmdflags" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/monitoring" zlog "zotregistry.io/zot/pkg/log" @@ -213,7 +214,13 @@ func NewCliRootCmd() *cobra.Command { // additional cmds enableCli(rootCmd) // "version" - rootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit") + rootCmd.Flags().BoolVarP(&showVersion, cmdflags.VersionFlag, "v", false, "show the version and exit") + rootCmd.PersistentFlags().String(cmdflags.URLFlag, "", + "Specify zot server URL if config-name is not mentioned") + rootCmd.PersistentFlags().String(cmdflags.ConfigFlag, "", + "Specify the repository where to connect") + rootCmd.PersistentFlags().StringP(cmdflags.UserFlag, "u", "", + `User Credentials of zot server in "username:password" format`) return rootCmd } @@ -225,18 +232,18 @@ func validateStorageConfig(cfg *config.Config, log zlog.Logger) error { for _, storageConfig := range cfg.Storage.SubPaths { if strings.EqualFold(defaultRootDir, storageConfig.RootDirectory) { - log.Error().Err(errors.ErrBadConfig).Msg("storage subpaths cannot use default storage root directory") + log.Error().Err(zerr.ErrBadConfig).Msg("storage subpaths cannot use default storage root directory") - return errors.ErrBadConfig + return zerr.ErrBadConfig } expConfig, ok := expConfigMap[storageConfig.RootDirectory] if ok { equal := expConfig.ParamsEqual(storageConfig) if !equal { - log.Error().Err(errors.ErrBadConfig).Msg("storage config with same root directory should have same parameters") + log.Error().Err(zerr.ErrBadConfig).Msg("storage config with same root directory should have same parameters") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } else { expConfigMap[storageConfig.RootDirectory] = storageConfig @@ -251,31 +258,31 @@ func validateCacheConfig(cfg *config.Config, log zlog.Logger) error { // dedupe true, remote storage, remoteCache true, but no cacheDriver (remote) //nolint: lll if cfg.Storage.Dedupe && cfg.Storage.StorageDriver != nil && cfg.Storage.RemoteCache && cfg.Storage.CacheDriver == nil { - log.Error().Err(errors.ErrBadConfig).Msg( + log.Error().Err(zerr.ErrBadConfig).Msg( "dedupe set to true with remote storage and caching, but no remote cache configured!") - return errors.ErrBadConfig + return zerr.ErrBadConfig } if cfg.Storage.CacheDriver != nil && cfg.Storage.RemoteCache { // local storage with remote caching if cfg.Storage.StorageDriver == nil { - log.Error().Err(errors.ErrBadConfig).Msg("cannot have local storage driver with remote caching!") + log.Error().Err(zerr.ErrBadConfig).Msg("cannot have local storage driver with remote caching!") - return errors.ErrBadConfig + return zerr.ErrBadConfig } // unsupported cache driver if cfg.Storage.CacheDriver["name"] != storageConstants.DynamoDBDriverName { - log.Error().Err(errors.ErrBadConfig). + log.Error().Err(zerr.ErrBadConfig). Interface("cacheDriver", cfg.Storage.CacheDriver["name"]).Msg("unsupported cache driver") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } if !cfg.Storage.RemoteCache && cfg.Storage.CacheDriver != nil { - log.Warn().Err(errors.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory). + log.Warn().Err(zerr.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory). Msg("remoteCache set to false but cacheDriver config (remote caching) provided for directory" + "will ignore and use local caching") } @@ -285,30 +292,30 @@ func validateCacheConfig(cfg *config.Config, log zlog.Logger) error { // dedupe true, remote storage, remoteCache true, but no cacheDriver (remote) //nolint: lll if subPath.Dedupe && subPath.StorageDriver != nil && subPath.RemoteCache && subPath.CacheDriver == nil { - log.Error().Err(errors.ErrBadConfig).Msg("dedupe set to true with remote storage and caching, but no remote cache configured!") + log.Error().Err(zerr.ErrBadConfig).Msg("dedupe set to true with remote storage and caching, but no remote cache configured!") - return errors.ErrBadConfig + return zerr.ErrBadConfig } if subPath.CacheDriver != nil && subPath.RemoteCache { // local storage with remote caching if subPath.StorageDriver == nil { - log.Error().Err(errors.ErrBadConfig).Msg("cannot have local storage driver with remote caching!") + log.Error().Err(zerr.ErrBadConfig).Msg("cannot have local storage driver with remote caching!") - return errors.ErrBadConfig + return zerr.ErrBadConfig } // unsupported cache driver if subPath.CacheDriver["name"] != storageConstants.DynamoDBDriverName { - log.Error().Err(errors.ErrBadConfig).Interface("cacheDriver", cfg.Storage.CacheDriver["name"]). + log.Error().Err(zerr.ErrBadConfig).Interface("cacheDriver", cfg.Storage.CacheDriver["name"]). Msg("unsupported cache driver") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } if !subPath.RemoteCache && subPath.CacheDriver != nil { - log.Warn().Err(errors.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory). + log.Warn().Err(zerr.ErrBadConfig).Str("directory", cfg.Storage.RootDirectory). Msg("remoteCache set to false but cacheDriver config (remote caching) provided for directory," + "will ignore and use local caching") } @@ -331,27 +338,27 @@ func validateExtensionsConfig(cfg *config.Config, log zlog.Logger) error { // it would make sense to also check for mgmt and user prefs to be enabled, // but those are both enabled by having the search and ui extensions enabled if cfg.Extensions.Search == nil || !*cfg.Extensions.Search.Enable { - log.Warn().Err(errors.ErrBadConfig).Msg("UI functionality can't be used without search extension.") + log.Warn().Err(zerr.ErrBadConfig).Msg("UI functionality can't be used without search extension.") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } //nolint:lll if cfg.Storage.StorageDriver != nil && cfg.Extensions != nil && cfg.Extensions.Search != nil && cfg.Extensions.Search.Enable != nil && *cfg.Extensions.Search.Enable && cfg.Extensions.Search.CVE != nil { - log.Warn().Err(errors.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE") + log.Warn().Err(zerr.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE") - return errors.ErrBadConfig + return zerr.ErrBadConfig } for _, subPath := range cfg.Storage.SubPaths { //nolint:lll if subPath.StorageDriver != nil && cfg.Extensions != nil && cfg.Extensions.Search != nil && cfg.Extensions.Search.Enable != nil && *cfg.Extensions.Search.Enable && cfg.Extensions.Search.CVE != nil { - log.Warn().Err(errors.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE") + log.Warn().Err(zerr.ErrBadConfig).Msg("CVE functionality can't be used with remote storage. Please disable CVE") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } @@ -402,17 +409,17 @@ func validateConfiguration(config *config.Config, log zlog.Logger) error { if len(config.Storage.StorageDriver) != 0 { // enforce s3 driver in case of using storage driver if config.Storage.StorageDriver["name"] != storageConstants.S3StorageDriverName { - log.Error().Err(errors.ErrBadConfig).Interface("cacheDriver", config.Storage.StorageDriver["name"]). + log.Error().Err(zerr.ErrBadConfig).Interface("cacheDriver", config.Storage.StorageDriver["name"]). Msg("unsupported storage driver") - return errors.ErrBadConfig + return zerr.ErrBadConfig } // enforce filesystem storage in case sync feature is enabled if config.Extensions != nil && config.Extensions.Sync != nil { - log.Error().Err(errors.ErrBadConfig).Msg("sync supports only filesystem storage") + log.Error().Err(zerr.ErrBadConfig).Msg("sync supports only filesystem storage") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } @@ -424,10 +431,10 @@ func validateConfiguration(config *config.Config, log zlog.Logger) error { for route, storageConfig := range subPaths { if len(storageConfig.StorageDriver) != 0 { if storageConfig.StorageDriver["name"] != storageConstants.S3StorageDriverName { - log.Error().Err(errors.ErrBadConfig).Str("subpath", route).Interface("storageDriver", + log.Error().Err(zerr.ErrBadConfig).Str("subpath", route).Interface("storageDriver", storageConfig.StorageDriver["name"]).Msg("unsupported storage driver") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } } @@ -456,23 +463,23 @@ func validateOpenIDConfig(cfg *config.Config, log zlog.Logger) error { if config.IsOpenIDSupported(provider) { if providerConfig.ClientID == "" || providerConfig.Issuer == "" || len(providerConfig.Scopes) == 0 { - log.Error().Err(errors.ErrBadConfig). + log.Error().Err(zerr.ErrBadConfig). Msg("OpenID provider config requires clientid, issuer and scopes parameters") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } else if config.IsOauth2Supported(provider) { if providerConfig.ClientID == "" || len(providerConfig.Scopes) == 0 { - log.Error().Err(errors.ErrBadConfig). + log.Error().Err(zerr.ErrBadConfig). Msg("OAuth2 provider config requires clientid and scopes parameters") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } else { - log.Error().Err(errors.ErrBadConfig). + log.Error().Err(zerr.ErrBadConfig). Msg("unsupported openid/oauth2 provider") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } } @@ -483,11 +490,11 @@ func validateOpenIDConfig(cfg *config.Config, log zlog.Logger) error { func validateAuthzPolicies(config *config.Config, log zlog.Logger) error { if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil && config.HTTP.Auth.OpenID == nil)) && !authzContainsOnlyAnonymousPolicy(config) { - log.Error().Err(errors.ErrBadConfig). + log.Error().Err(zerr.ErrBadConfig). Msg("access control config requires one of httpasswd, ldap or openid authentication " + "or using only 'anonymousPolicy' policies") - return errors.ErrBadConfig + return zerr.ErrBadConfig } return nil @@ -730,15 +737,15 @@ func LoadConfiguration(config *config.Config, configPath string) error { log := zlog.NewLogger(config.Log.Level, config.Log.Output) if len(metaData.Keys) == 0 { - log.Error().Err(errors.ErrBadConfig).Msg("config doesn't contain any key:value pair") + log.Error().Err(zerr.ErrBadConfig).Msg("config doesn't contain any key:value pair") - return errors.ErrBadConfig + return zerr.ErrBadConfig } if len(metaData.Unused) > 0 { - log.Error().Err(errors.ErrBadConfig).Strs("keys", metaData.Unused).Msg("unknown keys") + log.Error().Err(zerr.ErrBadConfig).Strs("keys", metaData.Unused).Msg("unknown keys") - return errors.ErrBadConfig + return zerr.ErrBadConfig } // defaults @@ -803,21 +810,21 @@ func validateLDAP(config *config.Config, log zlog.Logger) error { log.Error().Str("userAttribute", ldap.UserAttribute). Msg("invalid LDAP configuration, missing mandatory key: userAttribute") - return errors.ErrLDAPConfig + return zerr.ErrLDAPConfig } if ldap.Address == "" { log.Error().Str("address", ldap.Address). Msg("invalid LDAP configuration, missing mandatory key: address") - return errors.ErrLDAPConfig + return zerr.ErrLDAPConfig } if ldap.BaseDN == "" { log.Error().Str("basedn", ldap.BaseDN). Msg("invalid LDAP configuration, missing mandatory key: basedn") - return errors.ErrLDAPConfig + return zerr.ErrLDAPConfig } } @@ -830,7 +837,7 @@ func validateHTTP(config *config.Config, log zlog.Logger) error { if err != nil || (port < 0 || port > 65535) { log.Error().Str("port", config.HTTP.Port).Msg("invalid port") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } @@ -840,27 +847,27 @@ func validateHTTP(config *config.Config, log zlog.Logger) error { func validateGC(config *config.Config, log zlog.Logger) error { // enforce GC params if config.Storage.GCDelay < 0 { - log.Error().Err(errors.ErrBadConfig).Dur("delay", config.Storage.GCDelay). + log.Error().Err(zerr.ErrBadConfig).Dur("delay", config.Storage.GCDelay). Msg("invalid garbage-collect delay specified") - return errors.ErrBadConfig + return zerr.ErrBadConfig } if config.Storage.GCInterval < 0 { - log.Error().Err(errors.ErrBadConfig).Dur("interval", config.Storage.GCInterval). + log.Error().Err(zerr.ErrBadConfig).Dur("interval", config.Storage.GCInterval). Msg("invalid garbage-collect interval specified") - return errors.ErrBadConfig + return zerr.ErrBadConfig } if !config.Storage.GC { if config.Storage.GCDelay != 0 { - log.Warn().Err(errors.ErrBadConfig). + log.Warn().Err(zerr.ErrBadConfig). Msg("garbage-collect delay specified without enabling garbage-collect, will be ignored") } if config.Storage.GCInterval != 0 { - log.Warn().Err(errors.ErrBadConfig). + log.Warn().Err(zerr.ErrBadConfig). Msg("periodic garbage-collect interval specified without enabling garbage-collect, will be ignored") } } @@ -868,12 +875,12 @@ func validateGC(config *config.Config, log zlog.Logger) error { // subpaths for name, subPath := range config.Storage.SubPaths { if subPath.GC && subPath.GCDelay <= 0 { - log.Error().Err(errors.ErrBadConfig). + log.Error().Err(zerr.ErrBadConfig). Str("subPath", name). Interface("gcDelay", subPath.GCDelay). Msg("invalid GC delay configuration - cannot be negative or zero") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } @@ -886,10 +893,10 @@ func validateSync(config *config.Config, log zlog.Logger) error { for id, regCfg := range config.Extensions.Sync.Registries { // check retry options are configured for sync if regCfg.MaxRetries != nil && regCfg.RetryDelay == nil { - log.Error().Err(errors.ErrBadConfig).Int("id", id).Interface("extensions.sync.registries[id]", + log.Error().Err(zerr.ErrBadConfig).Int("id", id).Interface("extensions.sync.registries[id]", config.Extensions.Sync.Registries[id]).Msg("retryDelay is required when using maxRetries") - return errors.ErrBadConfig + return zerr.ErrBadConfig } if regCfg.Content != nil { @@ -902,11 +909,11 @@ func validateSync(config *config.Config, log zlog.Logger) error { } if content.StripPrefix && !strings.Contains(content.Prefix, "/*") && content.Destination == "/" { - log.Error().Err(errors.ErrBadConfig). + log.Error().Err(zerr.ErrBadConfig). Interface("sync content", content). Msg("sync config: can not use stripPrefix true and destination '/' without using glob patterns in prefix") - return errors.ErrBadConfig + return zerr.ErrBadConfig } } } diff --git a/pkg/cli/root_test.go b/pkg/cli/root_test.go index 41a1f37e..d0359276 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 3433418e..d1571dd0 100644 --- a/pkg/cli/search_cmd.go +++ b/pkg/cli/search_cmd.go @@ -10,7 +10,8 @@ import ( "github.com/briandowns/spinner" "github.com/spf13/cobra" - zotErrors "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/cli/cmdflags" ) //nolint:dupl @@ -21,7 +22,7 @@ func NewSearchCommand(searchService SearchService) *cobra.Command { var isSpinner, verifyTLS, verbose, debug bool - imageCmd := &cobra.Command{ + searchCmd := &cobra.Command{ Use: "search [config-name]", Short: "Search images and their tags", Long: `Search repos or images @@ -36,13 +37,14 @@ Example: zli search --subject repo@sha256:f9a0981... zli search --subject repo:tag `, + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { home, err := os.UserHomeDir() if err != nil { panic(err) } - configPath := path.Join(home + "/.zot") + configPath := path.Join(home, "/.zot") if servURL == "" { if len(args) > 0 { urlFromConfig, err := getConfigValue(configPath, args[0], "url") @@ -53,12 +55,12 @@ Example: } if urlFromConfig == "" { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } servURL = urlFromConfig } else { - return zotErrors.ErrNoURLProvided + return zerr.ErrNoURLProvided } } @@ -107,27 +109,32 @@ Example: }, } - setupSearchFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose, &debug) - imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter) + setupSearchFlags(searchCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose, &debug) + searchCmd.SetUsageTemplate(searchCmd.UsageTemplate() + usageFooter) - return imageCmd + searchCmd.AddCommand(NewSearchQueryCommand(searchService)) + searchCmd.AddCommand(NewSearchSubjectCommand(searchService)) + + return searchCmd } -func setupSearchFlags(imageCmd *cobra.Command, searchImageParams map[string]*string, +func setupSearchFlags(searchCmd *cobra.Command, searchImageParams map[string]*string, servURL, user, outputFormat *string, verbose *bool, debug *bool, ) { - searchImageParams["query"] = imageCmd.Flags().StringP("query", "q", "", + searchImageParams["query"] = searchCmd.Flags().StringP("query", "q", "", "Specify what repo or image(repo:tag) to be searched") - searchImageParams["subject"] = imageCmd.Flags().StringP("subject", "s", "", + searchImageParams["subject"] = searchCmd.Flags().StringP("subject", "s", "", "List all referrers for this subject. The subject can be specified by tag(repo:tag) or by digest"+ "(repo@digest)") - imageCmd.Flags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned") - imageCmd.Flags().StringVarP(user, "user", "u", "", `User Credentials of zot server in "username:password" format`) - imageCmd.Flags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]") - imageCmd.Flags().BoolVar(verbose, "verbose", false, "Show verbose output") - imageCmd.Flags().BoolVar(debug, "debug", false, "Show debug output") + searchCmd.Flags().StringVar(servURL, cmdflags.URLFlag, "", "Specify zot server URL if config-name is not mentioned") + searchCmd.Flags().StringVarP(user, cmdflags.UserFlag, "u", "", + `User Credentials of zot server in "username:password" format`) + searchCmd.PersistentFlags().StringVarP(outputFormat, cmdflags.OutputFormatFlag, "f", "", + "Specify output format [text/json/yaml]") + searchCmd.PersistentFlags().BoolVar(verbose, cmdflags.VerboseFlag, false, "Show verbose output") + searchCmd.PersistentFlags().BoolVar(debug, cmdflags.DebugFlag, false, "Show debug output") } func globalSearch(searchConfig searchConfig) error { @@ -150,5 +157,5 @@ func globalSearch(searchConfig searchConfig) error { } } - return zotErrors.ErrInvalidFlagsCombination + return zerr.ErrInvalidFlagsCombination } diff --git a/pkg/cli/search_cmd_referrers_test.go b/pkg/cli/search_cmd_referrers_test.go index 851f1ed8..5b6af38c 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" @@ -428,7 +422,7 @@ func TestFormatsReferrersCLI(t *testing.T) { So(err, ShouldBeNil) Convey("JSON format", func() { - args := []string{"reftest", "--output", "json", "--subject", repo + "@" + image.DigestStr()} + args := []string{"reftest", "--format", "json", "--subject", repo + "@" + image.DigestStr()} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, baseURL)) @@ -446,7 +440,7 @@ func TestFormatsReferrersCLI(t *testing.T) { fmt.Println(buff.String()) }) Convey("YAML format", func() { - args := []string{"reftest", "--output", "yaml", "--subject", repo + "@" + image.DigestStr()} + args := []string{"reftest", "--format", "yaml", "--subject", repo + "@" + image.DigestStr()} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, baseURL)) @@ -464,7 +458,7 @@ func TestFormatsReferrersCLI(t *testing.T) { fmt.Println(buff.String()) }) Convey("Invalid format", func() { - args := []string{"reftest", "--output", "invalid_format", "--subject", repo + "@" + image.DigestStr()} + args := []string{"reftest", "--format", "invalid_format", "--subject", repo + "@" + image.DigestStr()} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, baseURL)) @@ -488,7 +482,7 @@ func TestReferrersCLIErrors(t *testing.T) { cmd := NewSearchCommand(new(searchService)) Convey("no url provided", func() { - args := []string{"reftest", "--output", "invalid", "--query", "repo/alpine"} + args := []string{"reftest", "--format", "invalid", "--query", "repo/alpine"} configPath := makeConfigFile(`{"configs":[{"_name":"reftest","showspinner":false}]}`) @@ -594,3 +588,9 @@ func TestReferrersCLIErrors(t *testing.T) { }) }) } + +func ref[T any](input T) *T { + ref := input + + return &ref +} diff --git a/pkg/cli/search_cmd_test.go b/pkg/cli/search_cmd_test.go index 1c276b33..9a4efbf6 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" ) @@ -240,7 +241,7 @@ func TestFormatsSearchCLI(t *testing.T) { cmd := NewSearchCommand(new(searchService)) Convey("JSON format", func() { - args := []string{"searchtest", "--output", "json", "--query", "repo/alpine"} + args := []string{"searchtest", "--format", "json", "--query", "repo/alpine"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, baseURL)) @@ -257,7 +258,7 @@ func TestFormatsSearchCLI(t *testing.T) { }) Convey("YAML format", func() { - args := []string{"searchtest", "--output", "yaml", "--query", "repo/alpine"} + args := []string{"searchtest", "--format", "yaml", "--query", "repo/alpine"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, baseURL)) @@ -274,7 +275,7 @@ func TestFormatsSearchCLI(t *testing.T) { }) Convey("Invalid format", func() { - args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"} + args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, baseURL)) @@ -296,7 +297,7 @@ func TestSearchCLIErrors(t *testing.T) { cmd := NewSearchCommand(new(searchService)) Convey("no url provided", func() { - args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"} + args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"} configPath := makeConfigFile(`{"configs":[{"_name":"searchtest","showspinner":false}]}`) @@ -311,7 +312,7 @@ func TestSearchCLIErrors(t *testing.T) { }) Convey("getConfigValue", func() { - args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"} + args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"} configPath := makeConfigFile(`bad-json`) @@ -358,7 +359,7 @@ func TestSearchCLIErrors(t *testing.T) { }) Convey("url from config is empty", func() { - args := []string{"searchtest", "--output", "invalid", "--query", "repo/alpine"} + args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"} configPath := makeConfigFile(`{"configs":[{"_name":"searchtest", "url":"", "showspinner":false}]}`) @@ -402,3 +403,142 @@ func TestSearchCLIErrors(t *testing.T) { }) }) } + +func TestSearchCommandGQL(t *testing.T) { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + }, + } + + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + cm := test.NewControllerManager(ctlr) + + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + Convey("commands without gql", t, func() { + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + Convey("query", func() { + args := []string{"query", "repo/al"} + cmd := NewSearchCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo 8c25cb36 false 100B") + So(actual, ShouldContainSubstring, "repo 100B 2010-01-01 01:01:01 +0000 UTC 0 0") + }) + + Convey("query command errors", func() { + // no url + args := []string{"repo/al"} + cmd := NewSearchQueryCommand(mockService{}) + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("subject", func() { + err := test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "tag") + So(err, ShouldBeNil) + + args := []string{"subject", "repo:tag"} + cmd := NewSearchCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "ArtifactType 100 B Digest") + }) + + Convey("subject command errors", func() { + // no url + args := []string{"repo:tag"} + cmd := NewSearchSubjectCommand(mockService{}) + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + }) + }) +} + +func TestSearchCommandREST(t *testing.T) { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + cm := test.NewControllerManager(ctlr) + + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + Convey("commands without gql", t, func() { + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + Convey("query", func() { + args := []string{"query", "repo/al"} + cmd := NewSearchCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("subject", func() { + err := test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "tag") + So(err, ShouldBeNil) + + args := []string{"subject", "repo:tag"} + cmd := NewSearchCommand(mockService{}) + cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "") + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, + "art.type 100 B sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a") + }) + }) +} diff --git a/pkg/cli/search_functions.go b/pkg/cli/search_functions.go new file mode 100644 index 00000000..9792635e --- /dev/null +++ b/pkg/cli/search_functions.go @@ -0,0 +1,459 @@ +//go:build search +// +build search + +package cli + +import ( + "context" + "fmt" + "math" + "strings" + "sync" + "time" + + zerr "zotregistry.io/zot/errors" + zcommon "zotregistry.io/zot/pkg/common" +) + +func SearchAllImages(config searchConfig) error { + username, password := getUsernameAndPassword(*config.user) + imageErr := make(chan stringResult) + ctx, cancel := context.WithCancel(context.Background()) + + var wg sync.WaitGroup + + wg.Add(1) + + go config.searchService.getAllImages(ctx, config, username, password, imageErr, &wg) + wg.Add(1) + + errCh := make(chan error, 1) + + go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh) + wg.Wait() + select { + case err := <-errCh: + return err + default: + return nil + } +} + +func SearchAllImagesGQL(config searchConfig) error { + username, password := getUsernameAndPassword(*config.user) + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + imageList, err := config.searchService.getImagesGQL(ctx, config, username, password, "") + if err != nil { + return err + } + + imageListData := []imageStruct{} + + for _, image := range imageList.Results { + imageListData = append(imageListData, imageStruct(image)) + } + + return printImageResult(config, imageListData) +} + +func SearchImageByName(config searchConfig, image string) error { + username, password := getUsernameAndPassword(*config.user) + imageErr := make(chan stringResult) + ctx, cancel := context.WithCancel(context.Background()) + + var wg sync.WaitGroup + + wg.Add(1) + + go config.searchService.getImageByName(ctx, config, username, password, + image, imageErr, &wg) + wg.Add(1) + + errCh := make(chan error, 1) + go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh) + + wg.Wait() + + select { + case err := <-errCh: + return err + default: + return nil + } +} + +func SearchImageByNameGQL(config searchConfig, imageName string) error { + username, password := getUsernameAndPassword(*config.user) + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + repo, tag := zcommon.GetImageDirAndTag(imageName) + + imageList, err := config.searchService.getImagesGQL(ctx, config, username, password, repo) + if err != nil { + return err + } + + imageListData := []imageStruct{} + + for _, image := range imageList.Results { + if tag == "" || image.Tag == tag { + imageListData = append(imageListData, imageStruct(image)) + } + } + + return printImageResult(config, imageListData) +} + +func SearchImagesByDigest(config searchConfig, digest string) error { + username, password := getUsernameAndPassword(*config.user) + imageErr := make(chan stringResult) + ctx, cancel := context.WithCancel(context.Background()) + + var wg sync.WaitGroup + + wg.Add(1) + + go config.searchService.getImagesByDigest(ctx, config, username, password, + digest, imageErr, &wg) + wg.Add(1) + + errCh := make(chan error, 1) + go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh) + + wg.Wait() + + select { + case err := <-errCh: + return err + default: + return nil + } +} + +func SearchDerivedImageListGQL(config searchConfig, derivedImage string) error { + username, password := getUsernameAndPassword(*config.user) + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + imageList, err := config.searchService.getDerivedImageListGQL(ctx, config, username, + password, derivedImage) + if err != nil { + return err + } + + imageListData := []imageStruct{} + + for _, image := range imageList.DerivedImageList.Results { + imageListData = append(imageListData, imageStruct(image)) + } + + return printImageResult(config, imageListData) +} + +func SearchBaseImageListGQL(config searchConfig, baseImage string) error { + username, password := getUsernameAndPassword(*config.user) + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + imageList, err := config.searchService.getBaseImageListGQL(ctx, config, username, + password, baseImage) + if err != nil { + return err + } + + imageListData := []imageStruct{} + + for _, image := range imageList.BaseImageList.Results { + imageListData = append(imageListData, imageStruct(image)) + } + + return printImageResult(config, imageListData) +} + +func SearchImagesForDigestGQL(config searchConfig, digest string) error { + username, password := getUsernameAndPassword(*config.user) + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + imageList, err := config.searchService.getImagesForDigestGQL(ctx, config, username, password, digest) + if err != nil { + return err + } + + imageListData := []imageStruct{} + + for _, image := range imageList.Results { + imageListData = append(imageListData, imageStruct(image)) + } + + if err := printImageResult(config, imageListData); err != nil { + return err + } + + return nil +} + +func SearchCVEForImageGQL(config searchConfig, image, searchedCveID string) error { + username, password := getUsernameAndPassword(*config.user) + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + var cveList *cveResult + + err := zcommon.RetryWithContext(ctx, func(attempt int, retryIn time.Duration) error { + var err error + + cveList, err = config.searchService.getCveByImageGQL(ctx, config, username, password, image, searchedCveID) + if err != nil { + if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) { + cancel() + + return err + } + + fmt.Fprintf(config.resultWriter, + "[warning] CVE DB is not ready [%d] - retry in %d seconds\n", attempt, int(retryIn.Seconds())) + } + + return err + }, maxRetries, cveDBRetryInterval*time.Second) + if err != nil { + return err + } + + if len(cveList.Data.CVEListForImage.CVEList) == 0 { + fmt.Fprint(config.resultWriter, "No CVEs found for image\n") + + return nil + } + + var builder strings.Builder + + if *config.outputFormat == defaultOutputFormat || *config.outputFormat == "" { + printCVETableHeader(&builder, *config.verbose, 0, 0, 0) + fmt.Fprint(config.resultWriter, builder.String()) + } + + out, err := cveList.string(*config.outputFormat) + if err != nil { + return err + } + + fmt.Fprint(config.resultWriter, out) + + return nil +} + +func SearchImagesByCVEIDGQL(config searchConfig, repo, cveid string) error { + username, password := getUsernameAndPassword(*config.user) + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + var imageList *zcommon.ImagesForCve + + err := zcommon.RetryWithContext(ctx, func(attempt int, retryIn time.Duration) error { + var err error + + imageList, err = config.searchService.getTagsForCVEGQL(ctx, config, username, password, + repo, cveid) + if err != nil { + if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) { + cancel() + + return err + } + + fmt.Fprintf(config.resultWriter, + "[warning] CVE DB is not ready [%d] - retry in %d seconds\n", attempt, int(retryIn.Seconds())) + } + + return err + }, maxRetries, cveDBRetryInterval*time.Second) + if err != nil { + return err + } + + imageListData := []imageStruct{} + + for _, image := range imageList.Results { + imageListData = append(imageListData, imageStruct(image)) + } + + return printImageResult(config, imageListData) +} + +func SearchFixedTagsGQL(config searchConfig, repo, cveid string) error { + username, password := getUsernameAndPassword(*config.user) + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + var fixedTags *zcommon.ImageListWithCVEFixedResponse + + err := zcommon.RetryWithContext(ctx, func(attempt int, retryIn time.Duration) error { + var err error + + fixedTags, err = config.searchService.getFixedTagsForCVEGQL(ctx, config, username, password, + repo, cveid) + if err != nil { + if !strings.Contains(err.Error(), zerr.ErrCVEDBNotFound.Error()) { + cancel() + + return err + } + + fmt.Fprintf(config.resultWriter, + "[warning] CVE DB is not ready [%d] - retry in %d seconds\n", attempt, int(retryIn.Seconds())) + } + + return err + }, maxRetries, cveDBRetryInterval*time.Second) + if err != nil { + return err + } + + imageList := make([]imageStruct, 0, len(fixedTags.Results)) + + for _, image := range fixedTags.Results { + imageList = append(imageList, imageStruct(image)) + } + + return printImageResult(config, imageList) +} + +func GlobalSearchGQL(config searchConfig, query string) error { + username, password := getUsernameAndPassword(*config.user) + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + globalSearchResult, err := config.searchService.globalSearchGQL(ctx, config, username, password, query) + if err != nil { + return err + } + + imagesList := []imageStruct{} + + for _, image := range globalSearchResult.Images { + imagesList = append(imagesList, imageStruct(image)) + } + + reposList := []repoStruct{} + + for _, repo := range globalSearchResult.Repos { + reposList = append(reposList, repoStruct(repo)) + } + + if err := printImageResult(config, imagesList); err != nil { + return err + } + + return printRepoResults(config, reposList) +} + +func SearchReferrersGQL(config searchConfig, subject string) error { + username, password := getUsernameAndPassword(*config.user) + + repo, ref, refIsTag, err := zcommon.GetRepoReference(subject) + if err != nil { + return err + } + + digest := ref + + if refIsTag { + digest, err = fetchImageDigest(repo, ref, username, password, config) + if err != nil { + return err + } + } + + response, err := config.searchService.getReferrersGQL(context.Background(), config, username, password, repo, digest) + if err != nil { + return err + } + + referrersList := referrersResult(response.Referrers) + + maxArtifactTypeLen := math.MinInt + + for _, referrer := range referrersList { + if maxArtifactTypeLen < len(referrer.ArtifactType) { + maxArtifactTypeLen = len(referrer.ArtifactType) + } + } + + printReferrersTableHeader(config, config.resultWriter, maxArtifactTypeLen) + + return printReferrersResult(config, referrersList, maxArtifactTypeLen) +} + +func SearchReferrers(config searchConfig, subject string) error { + username, password := getUsernameAndPassword(*config.user) + + repo, ref, refIsTag, err := zcommon.GetRepoReference(subject) + if err != nil { + return err + } + + digest := ref + + if refIsTag { + digest, err = fetchImageDigest(repo, ref, username, password, config) + if err != nil { + return err + } + } + + referrersList, err := config.searchService.getReferrers(context.Background(), config, username, password, + repo, digest) + if err != nil { + return err + } + + maxArtifactTypeLen := math.MinInt + + for _, referrer := range referrersList { + if maxArtifactTypeLen < len(referrer.ArtifactType) { + maxArtifactTypeLen = len(referrer.ArtifactType) + } + } + + printReferrersTableHeader(config, config.resultWriter, maxArtifactTypeLen) + + return printReferrersResult(config, referrersList, maxArtifactTypeLen) +} + +func SearchRepos(config searchConfig) error { + username, password := getUsernameAndPassword(*config.user) + repoErr := make(chan stringResult) + ctx, cancel := context.WithCancel(context.Background()) + + var wg sync.WaitGroup + + wg.Add(1) + + go config.searchService.getRepos(ctx, config, username, password, repoErr, &wg) + wg.Add(1) + + errCh := make(chan error, 1) + + go collectResults(config, &wg, repoErr, cancel, printImageTableHeader, errCh) + wg.Wait() + select { + case err := <-errCh: + return err + default: + return nil + } +} diff --git a/pkg/cli/search_functions_test.go b/pkg/cli/search_functions_test.go new file mode 100644 index 00000000..0a63c65d --- /dev/null +++ b/pkg/cli/search_functions_test.go @@ -0,0 +1,761 @@ +//go:build search +// +build search + +// +//nolint:dupl +package cli //nolint:testpackage + +import ( + "bytes" + "context" + "io" + "os" + "regexp" + "strings" + "sync" + "testing" + "time" + + godigest "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + . "github.com/smartystreets/goconvey/convey" + "github.com/spf13/cobra" + + zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/cli/cmdflags" + "zotregistry.io/zot/pkg/common" +) + +func TestSearchAllImages(t *testing.T) { + Convey("SearchAllImages", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getAllImagesFn: func(ctx context.Context, config searchConfig, username, password string, + channel chan stringResult, wtgrp *sync.WaitGroup, + ) { + str, err := getMockImageStruct().stringPlainText(10, 10, 10, false) + + channel <- stringResult{StrValue: str, Err: err} + }, + }) + + err := SearchAllImages(searchConfig) + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B") + }) +} + +func TestSearchAllImagesGQL(t *testing.T) { + Convey("SearchAllImagesGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string, + ) (*common.ImageListResponse, error) { + return &common.ImageListResponse{ImageList: common.ImageList{ + PaginatedImagesResult: common.PaginatedImagesResult{ + Results: []common.ImageSummary{getMockImageSummary()}, + }, + }}, nil + }, + }) + + err := SearchAllImagesGQL(searchConfig) + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B") + }) + + Convey("SearchAllImagesGQL error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string, + ) (*common.ImageListResponse, error) { + return &common.ImageListResponse{ImageList: common.ImageList{ + PaginatedImagesResult: common.PaginatedImagesResult{ + Results: []common.ImageSummary{getMockImageSummary()}, + }, + }}, zerr.ErrInjected + }, + }) + + err := SearchAllImagesGQL(searchConfig) + So(err, ShouldNotBeNil) + }) +} + +func TestSearchImageByName(t *testing.T) { + Convey("SearchImageByName", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getImageByNameFn: func(ctx context.Context, config searchConfig, username string, password string, imageName string, + channel chan stringResult, wtgrp *sync.WaitGroup, + ) { + str, err := getMockImageStruct().stringPlainText(10, 10, 10, false) + + channel <- stringResult{StrValue: str, Err: err} + }, + }) + + err := SearchImageByName(searchConfig, "repo") + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B") + }) + + Convey("SearchImageByName error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getImageByNameFn: func(ctx context.Context, config searchConfig, username string, password string, imageName string, + channel chan stringResult, wtgrp *sync.WaitGroup, + ) { + channel <- stringResult{StrValue: "", Err: zerr.ErrInjected} + }, + }) + + err := SearchImageByName(searchConfig, "repo") + So(err, ShouldNotBeNil) + }) +} + +func TestSearchImageByNameGQL(t *testing.T) { + Convey("SearchImageByNameGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string, + ) (*common.ImageListResponse, error) { + return &common.ImageListResponse{ImageList: common.ImageList{ + PaginatedImagesResult: common.PaginatedImagesResult{ + Results: []common.ImageSummary{getMockImageSummary()}, + }, + }}, nil + }, + }) + + err := SearchImageByNameGQL(searchConfig, "repo") + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B") + }) + + Convey("SearchImageByNameGQL error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getImagesGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName string, + ) (*common.ImageListResponse, error) { + return &common.ImageListResponse{ImageList: common.ImageList{ + PaginatedImagesResult: common.PaginatedImagesResult{ + Results: []common.ImageSummary{getMockImageSummary()}, + }, + }}, zerr.ErrInjected + }, + }) + + err := SearchImageByNameGQL(searchConfig, "repo") + So(err, ShouldNotBeNil) + }) +} + +func TestSearchImagesByDigest(t *testing.T) { + Convey("SearchImagesByDigest", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getImagesByDigestFn: func(ctx context.Context, config searchConfig, username string, password string, digest string, + rch chan stringResult, wtgrp *sync.WaitGroup, + ) { + str, err := getMockImageStruct().stringPlainText(10, 10, 10, false) + + rch <- stringResult{StrValue: str, Err: err} + }, + }) + + err := SearchImagesByDigest(searchConfig, godigest.FromString("str").String()) + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B") + }) + + Convey("SearchImagesByDigest error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getImagesByDigestFn: func(ctx context.Context, config searchConfig, username string, password string, digest string, + rch chan stringResult, wtgrp *sync.WaitGroup, + ) { + rch <- stringResult{StrValue: "", Err: zerr.ErrInjected} + }, + }) + + err := SearchImagesByDigest(searchConfig, godigest.FromString("str").String()) + So(err, ShouldNotBeNil) + }) +} + +func TestSearchDerivedImageListGQL(t *testing.T) { + Convey("SearchDerivedImageListGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getDerivedImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string, + derivedImage string) (*common.DerivedImageListResponse, error, + ) { + return &common.DerivedImageListResponse{DerivedImageList: common.DerivedImageList{ + PaginatedImagesResult: common.PaginatedImagesResult{ + Results: []common.ImageSummary{ + getMockImageSummary(), + }, + }, + }}, nil + }, + }) + + err := SearchDerivedImageListGQL(searchConfig, "repo:tag") + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B") + }) + + Convey("SearchDerivedImageListGQL error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getDerivedImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string, + derivedImage string) (*common.DerivedImageListResponse, error, + ) { + return &common.DerivedImageListResponse{DerivedImageList: common.DerivedImageList{ + PaginatedImagesResult: common.PaginatedImagesResult{Results: []common.ImageSummary{}}, + }}, zerr.ErrInjected + }, + }) + + err := SearchDerivedImageListGQL(searchConfig, "repo:tag") + So(err, ShouldNotBeNil) + }) +} + +func TestSearchBaseImageListGQL(t *testing.T) { + Convey("SearchBaseImageListGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getBaseImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string, + derivedImage string) (*common.BaseImageListResponse, error, + ) { + return &common.BaseImageListResponse{BaseImageList: common.BaseImageList{ + PaginatedImagesResult: common.PaginatedImagesResult{Results: []common.ImageSummary{ + getMockImageSummary(), + }}, + }}, nil + }, + }) + + err := SearchBaseImageListGQL(searchConfig, "repo:tag") + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B") + }) + + Convey("SearchBaseImageListGQL error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getBaseImageListGQLFn: func(ctx context.Context, config searchConfig, username string, password string, + derivedImage string) (*common.BaseImageListResponse, error, + ) { + return &common.BaseImageListResponse{BaseImageList: common.BaseImageList{ + PaginatedImagesResult: common.PaginatedImagesResult{Results: []common.ImageSummary{}}, + }}, zerr.ErrInjected + }, + }) + + err := SearchBaseImageListGQL(searchConfig, "repo:tag") + So(err, ShouldNotBeNil) + }) +} + +func TestSearchImagesForDigestGQL(t *testing.T) { + Convey("SearchImagesForDigestGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getImagesForDigestGQLFn: func(ctx context.Context, config searchConfig, username string, + password string, digest string) (*common.ImagesForDigest, error, + ) { + return &common.ImagesForDigest{ImagesForDigestList: common.ImagesForDigestList{ + PaginatedImagesResult: common.PaginatedImagesResult{ + Results: []common.ImageSummary{getMockImageSummary()}, + }, + }}, nil + }, + }) + + err := SearchImagesForDigestGQL(searchConfig, "digest") + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B") + }) + + Convey("SearchImagesForDigestGQL error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getImagesForDigestGQLFn: func(ctx context.Context, config searchConfig, username string, + password string, digest string) (*common.ImagesForDigest, error, + ) { + return &common.ImagesForDigest{ImagesForDigestList: common.ImagesForDigestList{ + PaginatedImagesResult: common.PaginatedImagesResult{}, + }}, zerr.ErrInjected + }, + }) + + err := SearchImagesForDigestGQL(searchConfig, "digest") + So(err, ShouldNotBeNil) + }) +} + +func TestSearchCVEForImageGQL(t *testing.T) { + Convey("SearchCVEForImageGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username string, password string, + imageName string, searchedCVE string) (*cveResult, error, + ) { + return &cveResult{ + Data: cveData{ + CVEListForImage: cveListForImage{ + CVEList: []cve{ + { + ID: "dummyCVEID", + Description: "Description of the CVE", + Title: "Title of that CVE", + Severity: "HIGH", + PackageList: []packageList{ + { + Name: "packagename", + FixedVersion: "fixedver", + InstalledVersion: "installedver", + }, + }, + }, + }, + }, + }, + }, nil + }, + }) + + err := SearchCVEForImageGQL(searchConfig, "repo-test", "dummyCVEID") + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "dummyCVEID HIGH Title of that CVE") + }) + + Convey("SearchCVEForImageGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username string, password string, + imageName string, searchedCVE string) (*cveResult, error, + ) { + return &cveResult{}, zerr.ErrInjected + }, + }) + + err := SearchCVEForImageGQL(searchConfig, "repo-test", "dummyCVEID") + So(err, ShouldNotBeNil) + }) +} + +func TestSearchImagesByCVEIDGQL(t *testing.T) { + Convey("SearchImagesByCVEIDGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password, + imageName, cveID string) (*common.ImagesForCve, error, + ) { + return &common.ImagesForCve{ + ImagesForCVEList: common.ImagesForCVEList{ + PaginatedImagesResult: common.PaginatedImagesResult{ + Results: []common.ImageSummary{ + getMockImageSummary(), + }, + }, + }, + }, nil + }, + }) + + err := SearchImagesByCVEIDGQL(searchConfig, "repo", "CVE-12345") + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B") + }) + + Convey("SearchImagesByCVEIDGQL error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password, + imageName, cveID string) (*common.ImagesForCve, error, + ) { + return &common.ImagesForCve{ + ImagesForCVEList: common.ImagesForCVEList{ + PaginatedImagesResult: common.PaginatedImagesResult{}, + }, + }, zerr.ErrInjected + }, + }) + + err := SearchImagesByCVEIDGQL(searchConfig, "repo", "CVE-12345") + So(err, ShouldNotBeNil) + }) +} + +func TestSearchFixedTagsGQL(t *testing.T) { + Convey("SearchFixedTagsGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getFixedTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password, + imageName, cveID string) (*common.ImageListWithCVEFixedResponse, error, + ) { + return &common.ImageListWithCVEFixedResponse{ + ImageListWithCVEFixed: common.ImageListWithCVEFixed{ + PaginatedImagesResult: common.PaginatedImagesResult{ + Results: []common.ImageSummary{getMockImageSummary()}, + }, + }, + }, nil + }, + }) + + err := SearchFixedTagsGQL(searchConfig, "repo", "CVE-12345") + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo tag os/arch 8c25cb36 false 100B") + }) + + Convey("SearchFixedTagsGQL error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getFixedTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password, + imageName, cveID string) (*common.ImageListWithCVEFixedResponse, error, + ) { + return &common.ImageListWithCVEFixedResponse{ + ImageListWithCVEFixed: common.ImageListWithCVEFixed{ + PaginatedImagesResult: common.PaginatedImagesResult{}, + }, + }, zerr.ErrInjected + }, + }) + + err := SearchFixedTagsGQL(searchConfig, "repo", "CVE-12345") + So(err, ShouldNotBeNil) + }) +} + +func TestSearchReferrersGQL(t *testing.T) { + Convey("SearchReferrersGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getReferrersGQLFn: func(ctx context.Context, config searchConfig, username, password, + repo, digest string) (*common.ReferrersResp, error, + ) { + return &common.ReferrersResp{ + ReferrersResult: common.ReferrersResult{ + Referrers: []common.Referrer{{ + MediaType: ispec.MediaTypeImageManifest, + Size: 100, + ArtifactType: "art.type", + Digest: godigest.FromString("123").String(), + }}, + }, + }, nil + }, + }) + + err := SearchReferrersGQL(searchConfig, "repo@"+godigest.FromString("str").String()) + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, + "art.type 100 B sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3") + }) + + Convey("SearchReferrersGQL error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getReferrersGQLFn: func(ctx context.Context, config searchConfig, username, password, + repo, digest string) (*common.ReferrersResp, error, + ) { + return &common.ReferrersResp{}, zerr.ErrInjected + }, + }) + + err := SearchReferrersGQL(searchConfig, "repo@"+godigest.FromString("str").String()) + So(err, ShouldNotBeNil) + }) +} + +func TestGlobalSearchGQL(t *testing.T) { + Convey("GlobalSearchGQL", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + globalSearchGQLFn: func(ctx context.Context, config searchConfig, username, password, + query string) (*common.GlobalSearch, error, + ) { + return &common.GlobalSearch{ + Repos: []common.RepoSummary{{ + Name: "repo", + Size: "100", + LastUpdated: time.Date(2010, 1, 1, 1, 1, 1, 0, time.UTC), + }}, + }, nil + }, + }) + + err := GlobalSearchGQL(searchConfig, "repo") + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, + "repo ") + }) + + Convey("GlobalSearchGQL error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + globalSearchGQLFn: func(ctx context.Context, config searchConfig, username, password, + query string) (*common.GlobalSearch, error, + ) { + return &common.GlobalSearch{}, zerr.ErrInjected + }, + }) + + err := GlobalSearchGQL(searchConfig, "repo") + So(err, ShouldNotBeNil) + }) +} + +func TestSearchReferrers(t *testing.T) { + Convey("SearchReferrers", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getReferrersFn: func(ctx context.Context, config searchConfig, username string, password string, + repo string, digest string) (referrersResult, error, + ) { + return referrersResult([]common.Referrer{ + { + MediaType: ispec.MediaTypeImageManifest, + Size: 100, + ArtifactType: "art.type", + Digest: godigest.FromString("123").String(), + }, + }), nil + }, + }) + + err := SearchReferrers(searchConfig, "repo@"+godigest.FromString("str").String()) + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, + "art.type 100 B sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3") + }) + + Convey("SearchReferrers error", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getReferrersFn: func(ctx context.Context, config searchConfig, username string, password string, + repo string, digest string) (referrersResult, error, + ) { + return referrersResult{}, zerr.ErrInjected + }, + }) + + err := SearchReferrers(searchConfig, "repo@"+godigest.FromString("str").String()) + So(err, ShouldNotBeNil) + }) +} + +func TestSearchRepos(t *testing.T) { + Convey("SearchRepos", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{}) + + err := SearchRepos(searchConfig) + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "repo1") + So(actual, ShouldContainSubstring, "repo2") + }) +} + +func getMockSearchConfig(buff *bytes.Buffer, mockService mockService) searchConfig { + return searchConfig{ + resultWriter: buff, + user: ref(""), + searchService: mockService, + servURL: ref("http://127.0.0.1:8000"), + outputFormat: ref(""), + verifyTLS: ref(false), + fixedFlag: ref(false), + verbose: ref(false), + debug: ref(false), + } +} + +func getMockImageStruct() imageStruct { + return imageStruct(common.ImageSummary{ + RepoName: "repo", Tag: "tag", + MediaType: ispec.MediaTypeImageManifest, + Digest: godigest.FromString("str").String(), + Size: "100", + Manifests: []common.ManifestSummary{{ + Size: "100", + Platform: common.Platform{Os: "os", Arch: "arch"}, + Digest: godigest.FromString("str").String(), + ConfigDigest: godigest.FromString("str").String(), + }}, + }) +} + +func getMockImageSummary() common.ImageSummary { + return common.ImageSummary{ + RepoName: "repo", Tag: "tag", + MediaType: ispec.MediaTypeImageManifest, + Digest: godigest.FromString("str").String(), + Size: "100", + Manifests: []common.ManifestSummary{{ + Size: "100", + Platform: common.Platform{Os: "os", Arch: "arch"}, + Digest: godigest.FromString("str").String(), + ConfigDigest: godigest.FromString("str").String(), + }}, + } +} + +func TestUtils(t *testing.T) { + Convey("Utils", t, func() { + ok := haveSameArgs(field{"query", []struct { + Name string "json:\"name\"" + }{ + {Name: "arg1"}, {Name: "arg2"}, + }}, GQLQuery{ + Name: "query", Args: []string{"arg1"}, + }) + So(ok, ShouldBeFalse) + + ok = haveSameArgs(field{"query", []struct { + Name string "json:\"name\"" + }{ + {Name: "arg1"}, {Name: "arg2"}, + }}, GQLQuery{ + Name: "query", Args: []string{"arg1", "arg3"}, + }) + So(ok, ShouldBeFalse) + + err := containsGQLQueryWithParams( + []field{ + {Name: "query"}, + }, + []typeInfo{}, + GQLQuery{Name: "other-name"}, + ) + So(err, ShouldNotBeNil) + }) + + Convey("GetConfigOptions", t, func() { + // no flags + cmd := &cobra.Command{} + isSpinner, verifyTLS := GetCliConfigOptions(cmd) + So(isSpinner, ShouldBeFalse) + So(verifyTLS, ShouldBeFalse) + + // bad showspinner + configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":"bad", "verify-tls": false}]}`) + cmd = &cobra.Command{} + cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "") + isSpinner, verifyTLS = GetCliConfigOptions(cmd) + So(isSpinner, ShouldBeFalse) + So(verifyTLS, ShouldBeFalse) + os.Remove(configPath) + + // bad verify-tls + configPath = makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false, "verify-tls": "bad"}]}`) + cmd = &cobra.Command{} + cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "") + isSpinner, verifyTLS = GetCliConfigOptions(cmd) + So(isSpinner, ShouldBeFalse) + So(verifyTLS, ShouldBeFalse) + os.Remove(configPath) + }) + + Convey("GetServerURLFromFlags", t, func() { + cmd := &cobra.Command{} + cmd.Flags().String(cmdflags.URLFlag, "url", "") + url, err := GetServerURLFromFlags(cmd) + So(url, ShouldResemble, "url") + So(err, ShouldBeNil) + + // err no config or url + cmd = &cobra.Command{} + url, err = GetServerURLFromFlags(cmd) + So(url, ShouldResemble, "") + So(err, ShouldNotBeNil) + + // err ulr from config is empty + configPath := makeConfigFile(`{"configs":[{"_name":"imagetest"}]}`) + cmd = &cobra.Command{} + cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "") + url, err = GetServerURLFromFlags(cmd) + So(url, ShouldResemble, "") + So(err, ShouldNotBeNil) + os.Remove(configPath) + + // err reading the server url from config + configPath = makeConfigFile("{}") + cmd = &cobra.Command{} + cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "") + url, err = GetServerURLFromFlags(cmd) + So(url, ShouldResemble, "") + So(err, ShouldNotBeNil) + os.Remove(configPath) + }) + + Convey("CheckExtEndPointQuery", t, func() { + // invalid url + err := CheckExtEndPointQuery(searchConfig{ + user: ref(""), + servURL: ref("bad-url"), + }) + So(err, ShouldNotBeNil) + + // good url but no connection + err = CheckExtEndPointQuery(searchConfig{ + user: ref(""), + servURL: ref("http://127.0.0.1:5000"), + verifyTLS: ref(false), + debug: ref(false), + resultWriter: io.Discard, + }) + So(err, ShouldNotBeNil) + }) +} diff --git a/pkg/cli/search_sub_cmd.go b/pkg/cli/search_sub_cmd.go new file mode 100644 index 00000000..bd370a73 --- /dev/null +++ b/pkg/cli/search_sub_cmd.go @@ -0,0 +1,109 @@ +//go:build search +// +build search + +package cli + +import ( + "fmt" + + godigest "github.com/opencontainers/go-digest" + "github.com/spf13/cobra" + + zerr "zotregistry.io/zot/errors" + zcommon "zotregistry.io/zot/pkg/common" +) + +func NewSearchSubjectCommand(searchService SearchService) *cobra.Command { + imageCmd := &cobra.Command{ + Use: "subject [repo:tag]|[repo@digest]", + Short: "List all referrers for this subject.", + Long: `List all referrers for this subject. The subject can be specified by tag(repo:tag) or by digest" + + "(repo@digest)`, + Example: `# For referrers search specify the referred subject using it's full digest or tag: + zli search subject "repo@sha256:f9a0981..." + zli search subject "repo:tag"`, + Args: OneImageWithRefArg, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + if err := CheckExtEndPointQuery(searchConfig, ReferrersQuery()); err == nil { + return SearchReferrersGQL(searchConfig, args[0]) + } else { + return SearchReferrers(searchConfig, args[0]) + } + }, + } + + return imageCmd +} + +func NewSearchQueryCommand(searchService SearchService) *cobra.Command { + imageCmd := &cobra.Command{ + Use: "query", + Short: "Fuzzy search for repos and their tags.", + Long: "Fuzzy search for repos and their tags.", + Example: `# For repo search specify a substring of the repo name without the tag + zli search query "test/repo" + +# For image search specify the full repo name followed by the tag or a prefix of the tag. + zli search query "test/repo:2.1." + +# To search all tags in all repos. + zli search query ":"`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + // special format for searching all images and tags + if args[0] == ":" { + err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery()) + if err != nil { + return fmt.Errorf("%w: '%s'", err, ImageListQuery().Name) + } + + return SearchAllImagesGQL(searchConfig) + } + + if err := CheckExtEndPointQuery(searchConfig, GlobalSearchQuery()); err != nil { + return fmt.Errorf("%w: '%s'", err, CVEListForImageQuery().Name) + } + + return GlobalSearchGQL(searchConfig, args[0]) + }, + } + + return imageCmd +} + +func OneImageWithRefArg(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + + image := args[0] + + if dir, ref, _ := zcommon.GetImageDirAndReference(image); dir == "" || ref == "" { + return zerr.ErrInvalidRepoRefFormat + } + + return nil +} + +func OneDigestArg(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + + digest := args[0] + if _, err := godigest.Parse(digest); err != nil { + return err + } + + return nil +} diff --git a/pkg/cli/searcher.go b/pkg/cli/searcher.go index 5f70058d..3e652bf1 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 } @@ -770,7 +770,7 @@ func (search globalSearcherREST) search(config searchConfig) (bool, error) { return false, nil } - return true, fmt.Errorf("search extension is not enabled: %w", zotErrors.ErrExtensionNotEnabled) + return true, fmt.Errorf("search extension is not enabled: %w", zerr.ErrExtensionNotEnabled) } func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult, @@ -813,7 +813,7 @@ func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan strin config.spinner.stopSpinner() cancel() - errCh <- zotErrors.ErrCLITimeout + errCh <- zerr.ErrCLITimeout return } diff --git a/pkg/cli/service.go b/pkg/cli/service.go index 6d5e1a6d..2567191d 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -20,7 +20,7 @@ import ( ispec "github.com/opencontainers/image-spec/specs-go/v1" "gopkg.in/yaml.v2" - zotErrors "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/common" ) @@ -34,13 +34,13 @@ const ( type SearchService interface { //nolint:interfacebloat getImagesGQL(ctx context.Context, config searchConfig, username, password string, imageName string) (*common.ImageListResponse, error) - getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string, + getImagesForDigestGQL(ctx context.Context, config searchConfig, username, password string, digest string) (*common.ImagesForDigest, error) getCveByImageGQL(ctx context.Context, config searchConfig, username, password, imageName string, searchedCVE string) (*cveResult, error) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string, digest string) (*common.ImagesForCve, error) - getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, + getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, repo, cveID string) (*common.ImagesForCve, error) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string) (*common.ImageListWithCVEFixedResponse, error) @@ -260,7 +260,7 @@ func (service searchService) getImagesGQL(ctx context.Context, config searchConf return result, nil } -func (service searchService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string, +func (service searchService) getImagesForDigestGQL(ctx context.Context, config searchConfig, username, password string, digest string, ) (*common.ImagesForDigest, error) { query := fmt.Sprintf(` @@ -354,7 +354,7 @@ func (service searchService) getCveByImageGQL(ctx context.Context, config search } func (service searchService) getTagsForCVEGQL(ctx context.Context, config searchConfig, - username, password, imageName, cveID string, + username, password, repo, cveID string, ) (*common.ImagesForCve, error) { query := fmt.Sprintf(` { @@ -387,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,9 +584,17 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag continue } + shouldMatchTag := imageTag != "" + matchesTag := tag == imageTag + + // when the tag is empty we match everything + if shouldMatchTag && !matchesTag { + continue + } + wtgrp.Add(1) - go addManifestCallToPool(ctx, config, pool, username, password, imageName, tag, rch, wtgrp) + go addManifestCallToPool(ctx, config, pool, username, password, repo, tag, rch, wtgrp) } } @@ -787,7 +809,7 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config go rlim.startRateLimiter(ctx) for _, image := range result.Results { - if !strings.EqualFold(imageName, image.RepoName) { + if imageName != "" && !strings.EqualFold(imageName, image.RepoName) { continue } @@ -1438,7 +1460,7 @@ func addManifestToTable(table *tablewriter.Table, imageName, tagName string, man platform += offset } - minifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "") + manifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "") configDigestStr := ellipsize(configDigest.Encoded(), configWidth, "") imgSize, _ := strconv.ParseUint(manifest.Size, 10, 64) size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis) @@ -1447,7 +1469,7 @@ func addManifestToTable(table *tablewriter.Table, imageName, tagName string, man row[colImageNameIndex] = imageName row[colTagIndex] = tagName - row[colDigestIndex] = minifestDigestStr + row[colDigestIndex] = manifestDigestStr row[colPlatformIndex] = platform row[colSizeIndex] = size row[colIsSignedIndex] = strconv.FormatBool(isSigned) @@ -1487,23 +1509,23 @@ func addManifestToTable(table *tablewriter.Table, imageName, tagName string, man return nil } -func getPlatformStr(platf common.Platform) string { - if platf.Arch == "" && platf.Os == "" { +func getPlatformStr(platform common.Platform) string { + if platform.Arch == "" && platform.Os == "" { return "" } - platform := platf.Os + fullPlatform := platform.Os - if platf.Arch != "" { - platform = platform + "/" + platf.Arch - platform = strings.Trim(platform, "/") + if platform.Arch != "" { + fullPlatform = fullPlatform + "/" + platform.Arch + fullPlatform = strings.Trim(fullPlatform, "/") - if platf.Variant != "" { - platform = platform + "/" + platf.Variant + if platform.Variant != "" { + fullPlatform = fullPlatform + "/" + platform.Variant } } - return platform + return fullPlatform } func (img imageStruct) stringJSON() (string, error) { @@ -1534,12 +1556,12 @@ type catalogResponse struct { func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) { if !isURL(serverURL) { - return "", zotErrors.ErrInvalidURL + return "", zerr.ErrInvalidURL } newURL, err := url.Parse(serverURL) if err != nil { - return "", zotErrors.ErrInvalidURL + return "", zerr.ErrInvalidURL } newURL, _ = newURL.Parse(endPoint) diff --git a/pkg/common/oci.go b/pkg/common/oci.go index 97ddc900..f4baf303 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 CheckIsCorrectRepoNameFormat(repo string) bool { + return !strings.ContainsAny(repo, ":@") +} diff --git a/pkg/common/retry.go b/pkg/common/retry.go new file mode 100644 index 00000000..a6875564 --- /dev/null +++ b/pkg/common/retry.go @@ -0,0 +1,24 @@ +package common + +import ( + "context" + "time" +) + +func RetryWithContext(ctx context.Context, operation func(attempt int, retryIn time.Duration) error, maxRetries int, + delay time.Duration, +) error { + err := operation(1, delay) + + for attempt := 1; err != nil && attempt < maxRetries; attempt++ { + select { + case <-time.After(delay): + case <-ctx.Done(): + return err + } + + err = operation(attempt+1, delay) + } + + return err +} diff --git a/pkg/exporter/cli/cli.go b/pkg/exporter/cli/cli.go index 24285556..27a54719 100644 --- a/pkg/exporter/cli/cli.go +++ b/pkg/exporter/cli/cli.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/exporter/api" ) @@ -66,12 +66,12 @@ func loadConfiguration(config *api.Config, configPath string) { metaData := &mapstructure.Metadata{} if err := viper.Unmarshal(&config, metadataConfig(metaData)); err != nil { - log.Error().Err(err).Msg("Error while unmarshalling new config") + log.Error().Err(err).Msg("Error while unmarshaling new config") panic(err) } if len(metaData.Keys) == 0 || len(metaData.Unused) > 0 { - log.Error().Err(errors.ErrBadConfig).Msg("Bad configuration, retry writing it") - panic(errors.ErrBadConfig) + log.Error().Err(zerr.ErrBadConfig).Msg("Bad configuration, retry writing it") + panic(zerr.ErrBadConfig) } } diff --git a/pkg/extensions/get_extensions.go b/pkg/extensions/get_extensions.go index 74a414ee..2bd8ea32 100644 --- a/pkg/extensions/get_extensions.go +++ b/pkg/extensions/get_extensions.go @@ -38,7 +38,7 @@ func GetExtensions(config *config.Config) distext.ExtensionList { if len(endpoints) > 0 { extensions = append(extensions, distext.Extension{ - Name: "_zot", + Name: constants.BaseExtension, URL: "https://github.com/project-zot/zot/blob/" + config.ReleaseTag + "/pkg/extensions/_zot.md", Description: "zot registry extensions", Endpoints: endpoints, diff --git a/pkg/extensions/search/convert/annotations.go b/pkg/extensions/search/convert/annotations.go index 99c83c8d..2b15aebf 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 8532663a..8d45d6ce 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 4b126c2d..e5817846 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 6c73be9e..9dc8c855 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -599,9 +599,9 @@ func TestCVESearch(t *testing.T) { So(err, ShouldBeNil) So(len(cveResult.ImgList.CVEResultForImage.CVEList), ShouldNotBeZeroValue) - cvid := cveResult.ImgList.CVEResultForImage.CVEList[0].ID + cveid := cveResult.ImgList.CVEResultForImage.CVEList[0].ID - resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}") + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -610,7 +610,7 @@ func TestCVESearch(t *testing.T) { So(err, ShouldBeNil) So(len(imgListWithCVEFixed.Images), ShouldEqual, 0) - resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-cve-test\"){Results{RepoName%20LastUpdated}}}") + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-cve-test\"){Results{RepoName%20LastUpdated}}}") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -618,7 +618,7 @@ func TestCVESearch(t *testing.T) { So(err, ShouldBeNil) So(len(imgListWithCVEFixed.Images), ShouldEqual, 0) - resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}") + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-test\"){Results{RepoName%20LastUpdated}}}") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -635,7 +635,7 @@ func TestCVESearch(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) - resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noindex\"){Results{RepoName%20LastUpdated}}}") + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-noindex\"){Results{RepoName%20LastUpdated}}}") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -643,7 +643,7 @@ func TestCVESearch(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) - resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-index\"){Results{RepoName%20LastUpdated}}}") + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-invalid-index\"){Results{RepoName%20LastUpdated}}}") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -651,11 +651,11 @@ func TestCVESearch(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) - resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-noblob\"){Results{RepoName%20LastUpdated}}}") + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-noblob\"){Results{RepoName%20LastUpdated}}}") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) - resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-test\"){Results{RepoName%20LastUpdated}}}") + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-test\"){Results{RepoName%20LastUpdated}}}") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -663,7 +663,7 @@ func TestCVESearch(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) - resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cvid + "\",image:\"zot-squashfs-invalid-blob\"){Results{RepoName%20LastUpdated}}}") + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListWithCVEFixed(id:\"" + cveid + "\",image:\"zot-squashfs-invalid-blob\"){Results{RepoName%20LastUpdated}}}") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) @@ -732,7 +732,7 @@ func TestCVESearch(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 422) - resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListForCVE(id:\"" + cvid + "\"){Results{RepoName%20Tag}}}") + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + constants.FullSearchPrefix + "?query={ImageListForCVE(id:\"" + cveid + "\"){Results{RepoName%20Tag}}}") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) @@ -1412,7 +1412,7 @@ func TestCVEStruct(t *testing.T) { So(err, ShouldBeNil) So(len(tagList), ShouldEqual, 0) - // Repo is not found, assume it is affetected by the CVE + // Repo is not found, assume it is affected by the CVE // But we don't have enough of it's data to actually return it tagList, err = cveInfo.GetImageListForCVE("repo100", "CVE100") So(err, ShouldEqual, zerr.ErrRepoMetaNotFound) diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index 41229225..47684947 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 a096c10f..4af69feb 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/extensions/sync/sync_internal_test.go b/pkg/extensions/sync/sync_internal_test.go index 630aee4a..80415a5b 100644 --- a/pkg/extensions/sync/sync_internal_test.go +++ b/pkg/extensions/sync/sync_internal_test.go @@ -20,7 +20,7 @@ import ( "github.com/rs/zerolog" . "github.com/smartystreets/goconvey/convey" - "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/extensions/config" syncconf "zotregistry.io/zot/pkg/extensions/config/sync" "zotregistry.io/zot/pkg/extensions/lint" @@ -92,7 +92,7 @@ func TestSyncInternal(t *testing.T) { repositoryReference = fmt.Sprintf("%s/%s:tagged", host, testImage) _, err = parseRepositoryReference(repositoryReference) - So(err, ShouldEqual, errors.ErrInvalidRepositoryName) + So(err, ShouldEqual, zerr.ErrInvalidRepositoryName) repositoryReference = fmt.Sprintf("http://%s/%s", host, testImage) _, err = parseRepositoryReference(repositoryReference) @@ -341,7 +341,7 @@ func TestLocalRegistry(t *testing.T) { registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, mocks.MetaDBMock{ SetRepoReferenceFn: func(repo, Reference string, manifestDigest godigest.Digest, mediaType string) error { if Reference == "1.0" { - return errors.ErrRepoMetaNotFound + return zerr.ErrRepoMetaNotFound } return nil @@ -355,7 +355,7 @@ func TestLocalRegistry(t *testing.T) { Convey("trigger metaDB error on image manifest in CommitImage()", func() { registry := NewLocalRegistry(storage.StoreController{DefaultStore: syncImgStore}, mocks.MetaDBMock{ SetRepoReferenceFn: func(repo, Reference string, manifestDigest godigest.Digest, mediaType string) error { - return errors.ErrRepoMetaNotFound + return zerr.ErrRepoMetaNotFound }, }, log) diff --git a/pkg/meta/boltdb/boltdb.go b/pkg/meta/boltdb/boltdb.go index 24d14d7a..bc79da21 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/requestcontext/context.go b/pkg/requestcontext/context.go index 685f6cec..e27a3e5c 100644 --- a/pkg/requestcontext/context.go +++ b/pkg/requestcontext/context.go @@ -5,7 +5,7 @@ import ( glob "github.com/bmatcuk/doublestar/v4" //nolint:gci - "zotregistry.io/zot/errors" + zerr "zotregistry.io/zot/errors" ) type Key int @@ -41,7 +41,7 @@ func GetAccessControlContext(ctx context.Context) (*AccessControlContext, error) if authCtx := ctx.Value(authzCtxKey); authCtx != nil { acCtx, ok := authCtx.(AccessControlContext) if !ok { - return nil, errors.ErrBadType + return nil, zerr.ErrBadType } return &acCtx, nil @@ -110,7 +110,7 @@ func GetAuthnMiddlewareContext(ctx context.Context) (*AuthnMiddlewareContext, er if authnMiddlewareCtx := ctx.Value(authnMiddlewareCtxKey); authnMiddlewareCtx != nil { amCtx, ok := authnMiddlewareCtx.(AuthnMiddlewareContext) if !ok { - return nil, errors.ErrBadType + return nil, zerr.ErrBadType } return &amCtx, nil diff --git a/pkg/test/common.go b/pkg/test/common.go index aa27ad2e..5525101a 100644 --- a/pkg/test/common.go +++ b/pkg/test/common.go @@ -54,7 +54,7 @@ import ( "zotregistry.io/zot/pkg/storage" storageCommon "zotregistry.io/zot/pkg/storage/common" "zotregistry.io/zot/pkg/storage/local" - "zotregistry.io/zot/pkg/storage/types" + stypes "zotregistry.io/zot/pkg/storage/types" "zotregistry.io/zot/pkg/test/inject" "zotregistry.io/zot/pkg/test/mocks" ) @@ -1437,12 +1437,12 @@ func ListNotarySignatures(reference string, tdir string) ([]godigest.Digest, err sigRepo := notreg.NewRepository(remoteRepo) - artifectDesc, err := sigRepo.Resolve(ctx, reference) + artifactDesc, err := sigRepo.Resolve(ctx, reference) if err != nil { return signatures, err } - err = sigRepo.ListSignatures(ctx, artifectDesc, func(signatureManifests []ispec.Descriptor) error { + err = sigRepo.ListSignatures(ctx, artifactDesc, func(signatureManifests []ispec.Descriptor) error { for _, sigManifestDesc := range signatureManifests { signatures = append(signatures, sigManifestDesc.Digest) } @@ -2067,11 +2067,11 @@ func GetDefaultLayersBlobs() [][]byte { } } -func GetDefaultImageStore(rootDir string, log zLog.Logger) types.ImageStore { +func GetDefaultImageStore(rootDir string, log zLog.Logger) stypes.ImageStore { return local.NewImageStore(rootDir, false, time.Hour, false, false, log, monitoring.NewMetricsServer(false, log), mocks.MockedLint{ - LintFn: func(repo string, manifestDigest godigest.Digest, imageStore types.ImageStore) (bool, error) { + LintFn: func(repo string, manifestDigest godigest.Digest, imageStore stypes.ImageStore) (bool, error) { return true, nil }, }, @@ -2084,3 +2084,20 @@ func GetDefaultStoreController(rootDir string, log zLog.Logger) storage.StoreCon DefaultStore: GetDefaultImageStore(rootDir, log), } } + +func RemoveLocalStorageContents(imageStore stypes.ImageStore) error { + repos, err := imageStore.GetRepositories() + if err != nil { + return err + } + + for _, repo := range repos { + // take just the first path + err = os.RemoveAll(filepath.Join(imageStore.RootDir(), filepath.SplitList(repo)[0])) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/test/images.go b/pkg/test/images.go index 9f4afc76..ab56ec21 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