From 7b1e24c99ea535d0d8a920a7cd1b3958db076e88 Mon Sep 17 00:00:00 2001 From: LaurentiuNiculae Date: Fri, 8 Sep 2023 15:12:47 +0300 Subject: [PATCH] refactor(cli): remove old cli commands (#1756) Signed-off-by: Laurentiu Niculae --- errors/errors.go | 1 + pkg/cli/cli.go | 4 +- pkg/cli/client.go | 30 +- pkg/cli/client_elevated_test.go | 2 +- pkg/cli/client_test.go | 10 +- pkg/cli/client_utils_test.go | 10 +- pkg/cli/cve_cmd.go | 196 --- pkg/cli/cve_cmd_test.go | 535 ++------ pkg/cli/cves_cmd.go | 10 +- pkg/cli/cves_sub_cmd.go | 4 +- pkg/cli/discover.go | 86 +- pkg/cli/gql_queries_test.go | 8 +- pkg/cli/image_cmd.go | 161 +-- pkg/cli/image_cmd_test.go | 822 +++--------- .../{images_sub_cmd.go => image_sub_cmd.go} | 128 +- pkg/cli/images_cmd.go | 33 - pkg/cli/repo_cmd.go | 34 + pkg/cli/{repos_sub_cmd.go => repo_sub_cmd.go} | 4 +- pkg/cli/{repos_test.go => repo_test.go} | 4 +- pkg/cli/repos_cmd.go | 116 -- pkg/cli/root.go | 8 +- pkg/cli/search_cmd.go | 148 +-- pkg/cli/search_cmd_referrers_test.go | 596 --------- pkg/cli/search_cmd_test.go | 494 +++++-- pkg/cli/search_functions.go | 48 +- pkg/cli/search_functions_test.go | 35 +- pkg/cli/search_sub_cmd.go | 14 - pkg/cli/searcher.go | 1169 ----------------- pkg/cli/service.go | 376 +----- pkg/cli/utils.go | 481 +++++++ test/blackbox/cve.bats | 6 +- 31 files changed, 1455 insertions(+), 4118 deletions(-) delete mode 100644 pkg/cli/cve_cmd.go rename pkg/cli/{images_sub_cmd.go => image_sub_cmd.go} (58%) delete mode 100644 pkg/cli/images_cmd.go create mode 100644 pkg/cli/repo_cmd.go rename pkg/cli/{repos_sub_cmd.go => repo_sub_cmd.go} (92%) rename pkg/cli/{repos_test.go => repo_test.go} (89%) delete mode 100644 pkg/cli/repos_cmd.go delete mode 100644 pkg/cli/search_cmd_referrers_test.go delete mode 100644 pkg/cli/searcher.go create mode 100644 pkg/cli/utils.go diff --git a/errors/errors.go b/errors/errors.go index 47985604..bc535fb7 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -159,4 +159,5 @@ var ( ErrFileAlreadyCancelled = errors.New("storageDriver: file already cancelled") ErrFileAlreadyClosed = errors.New("storageDriver: file already closed") ErrFileAlreadyCommitted = errors.New("storageDriver: file already committed") + ErrInvalidOutputFormat = errors.New("cli: invalid output format") ) diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 6edfdbe6..db5b6de2 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -8,9 +8,7 @@ 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(NewCVECommand(NewSearchService())) rootCmd.AddCommand(NewRepoCommand(NewSearchService())) rootCmd.AddCommand(NewSearchCommand(NewSearchService())) } diff --git a/pkg/cli/client.go b/pkg/cli/client.go index 969d067a..e896b728 100644 --- a/pkg/cli/client.go +++ b/pkg/cli/client.go @@ -207,8 +207,8 @@ func (p *requestsPool) doJob(ctx context.Context, job *httpJob) { defer p.wtgrp.Done() // Check manifest media type - header, err := makeHEADRequest(ctx, job.url, job.username, job.password, *job.config.verifyTLS, - *job.config.debug) + header, err := makeHEADRequest(ctx, job.url, job.username, job.password, job.config.verifyTLS, + job.config.debug) if err != nil { if isContextDone(ctx) { return @@ -216,7 +216,7 @@ func (p *requestsPool) doJob(ctx context.Context, job *httpJob) { p.outputCh <- stringResult{"", err} } - verbose := *job.config.verbose + verbose := job.config.verbose switch header.Get("Content-Type") { case ispec.MediaTypeImageManifest: @@ -231,7 +231,7 @@ func (p *requestsPool) doJob(ctx context.Context, job *httpJob) { } platformStr := getPlatformStr(image.Manifests[0].Platform) - str, err := image.string(*job.config.outputFormat, len(job.imageName), len(job.tagName), len(platformStr), verbose) + str, err := image.string(job.config.outputFormat, len(job.imageName), len(job.tagName), len(platformStr), verbose) if err != nil { if isContextDone(ctx) { return @@ -259,7 +259,7 @@ func (p *requestsPool) doJob(ctx context.Context, job *httpJob) { platformStr := getPlatformStr(image.Manifests[0].Platform) - str, err := image.string(*job.config.outputFormat, len(job.imageName), len(job.tagName), len(platformStr), verbose) + str, err := image.string(job.config.outputFormat, len(job.imageName), len(job.tagName), len(platformStr), verbose) if err != nil { if isContextDone(ctx) { return @@ -283,7 +283,7 @@ func fetchImageIndexStruct(ctx context.Context, job *httpJob) (*imageStruct, err var indexContent ispec.Index header, err := makeGETRequest(ctx, job.url, job.username, job.password, - *job.config.verifyTLS, *job.config.debug, &indexContent, job.config.resultWriter) + job.config.verifyTLS, job.config.debug, &indexContent, job.config.resultWriter) if err != nil { if isContextDone(ctx) { return nil, context.Canceled @@ -371,10 +371,10 @@ func fetchManifestStruct(ctx context.Context, repo, manifestReference string, se manifestResp := ispec.Manifest{} URL := fmt.Sprintf("%s/v2/%s/manifests/%s", - *searchConf.servURL, repo, manifestReference) + searchConf.servURL, repo, manifestReference) header, err := makeGETRequest(ctx, URL, username, password, - *searchConf.verifyTLS, *searchConf.debug, &manifestResp, searchConf.resultWriter) + searchConf.verifyTLS, searchConf.debug, &manifestResp, searchConf.resultWriter) if err != nil { if isContextDone(ctx) { return common.ManifestSummary{}, context.Canceled @@ -460,10 +460,10 @@ func fetchConfig(ctx context.Context, repo, configDigest string, searchConf sear configContent := ispec.Image{} URL := fmt.Sprintf("%s/v2/%s/blobs/%s", - *searchConf.servURL, repo, configDigest) + searchConf.servURL, repo, configDigest) _, err := makeGETRequest(ctx, URL, username, password, - *searchConf.verifyTLS, *searchConf.debug, &configContent, searchConf.resultWriter) + searchConf.verifyTLS, searchConf.debug, &configContent, searchConf.resultWriter) if err != nil { if isContextDone(ctx) { return ispec.Image{}, context.Canceled @@ -481,10 +481,10 @@ func isNotationSigned(ctx context.Context, repo, digestStr string, searchConf se var referrers ispec.Index URL := fmt.Sprintf("%s/v2/%s/referrers/%s?artifactType=%s", - *searchConf.servURL, repo, digestStr, common.ArtifactTypeNotation) + searchConf.servURL, repo, digestStr, common.ArtifactTypeNotation) _, err := makeGETRequest(ctx, URL, username, password, - *searchConf.verifyTLS, *searchConf.debug, &referrers, searchConf.resultWriter) + searchConf.verifyTLS, searchConf.debug, &referrers, searchConf.resultWriter) if err != nil { return false } @@ -502,10 +502,10 @@ func isCosignSigned(ctx context.Context, repo, digestStr string, searchConf sear var result interface{} cosignTag := strings.Replace(digestStr, ":", "-", 1) + "." + remote.SignatureTagSuffix - URL := fmt.Sprintf("%s/v2/%s/manifests/%s", *searchConf.servURL, repo, cosignTag) + URL := fmt.Sprintf("%s/v2/%s/manifests/%s", searchConf.servURL, repo, cosignTag) - _, err := makeGETRequest(ctx, URL, username, password, *searchConf.verifyTLS, - *searchConf.debug, &result, searchConf.resultWriter) + _, err := makeGETRequest(ctx, URL, username, password, searchConf.verifyTLS, + searchConf.debug, &result, searchConf.resultWriter) return err == nil } diff --git a/pkg/cli/client_elevated_test.go b/pkg/cli/client_elevated_test.go index dfa208ad..8d867bf1 100644 --- a/pkg/cli/client_elevated_test.go +++ b/pkg/cli/client_elevated_test.go @@ -93,7 +93,7 @@ func TestElevatedPrivilegesTLSNewControllerPrivilegedCert(t *testing.T) { BaseSecureURL2, constants.RoutePrefix, constants.ExtCatalogPrefix)) defer os.Remove(configPath) - args := []string{"imagetest"} + args := []string{"list", "--config", "imagetest"} imageCmd := NewImageCommand(new(searchService)) imageBuff := bytes.NewBufferString("") imageCmd.SetOut(imageBuff) diff --git a/pkg/cli/client_test.go b/pkg/cli/client_test.go index 028ffbc5..d2bd6f92 100644 --- a/pkg/cli/client_test.go +++ b/pkg/cli/client_test.go @@ -90,7 +90,7 @@ func TestTLSWithAuth(t *testing.T) { defer os.RemoveAll(destCertsDir) - args := []string{"imagetest", "--name", "dummyImageName", "--url", HOST1} + args := []string{"name", "dummyImageName", "--url", HOST1} imageCmd := NewImageCommand(new(searchService)) imageBuff := bytes.NewBufferString("") imageCmd.SetOut(imageBuff) @@ -100,7 +100,7 @@ func TestTLSWithAuth(t *testing.T) { So(err, ShouldNotBeNil) So(imageBuff.String(), ShouldContainSubstring, "invalid URL format") - args = []string{"imagetest"} + args = []string{"list", "--config", "imagetest"} configPath = makeConfigFile( fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s%s%s","showspinner":false}]}`, BaseSecureURL1, constants.RoutePrefix, constants.ExtCatalogPrefix)) @@ -115,7 +115,7 @@ func TestTLSWithAuth(t *testing.T) { So(imageBuff.String(), ShouldContainSubstring, "check credentials") user := fmt.Sprintf("%s:%s", username, passphrase) - args = []string{"imagetest", "-u", user} + args = []string{"-u", user, "--config", "imagetest"} configPath = makeConfigFile( fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s%s%s","showspinner":false}]}`, BaseSecureURL1, constants.RoutePrefix, constants.ExtCatalogPrefix)) @@ -170,7 +170,7 @@ func TestTLSWithoutAuth(t *testing.T) { test.CopyTestFiles(sourceCertsDir, destCertsDir) defer os.RemoveAll(destCertsDir) - args := []string{"imagetest"} + args := []string{"list", "--config", "imagetest"} imageCmd := NewImageCommand(new(searchService)) imageBuff := bytes.NewBufferString("") imageCmd.SetOut(imageBuff) @@ -211,7 +211,7 @@ func TestTLSBadCerts(t *testing.T) { BaseSecureURL3, constants.RoutePrefix, constants.ExtCatalogPrefix)) defer os.Remove(configPath) - args := []string{"imagetest"} + args := []string{"list", "--config", "imagetest"} imageCmd := NewImageCommand(new(searchService)) imageBuff := bytes.NewBufferString("") imageCmd.SetOut(imageBuff) diff --git a/pkg/cli/client_utils_test.go b/pkg/cli/client_utils_test.go index 20bf2fb7..63c28966 100644 --- a/pkg/cli/client_utils_test.go +++ b/pkg/cli/client_utils_test.go @@ -26,12 +26,12 @@ func getDefaultSearchConf(baseURL string) searchConfig { outputFormat := "text" return searchConfig{ - servURL: &baseURL, + servURL: baseURL, resultWriter: io.Discard, - verifyTLS: &verifyTLS, - debug: &debug, - verbose: &verbose, - outputFormat: &outputFormat, + verifyTLS: verifyTLS, + debug: debug, + verbose: verbose, + outputFormat: outputFormat, } } diff --git a/pkg/cli/cve_cmd.go b/pkg/cli/cve_cmd.go deleted file mode 100644 index 046d1db8..00000000 --- a/pkg/cli/cve_cmd.go +++ /dev/null @@ -1,196 +0,0 @@ -//go:build search -// +build search - -package cli - -import ( - "fmt" - "os" - "path" - "strings" - "time" - - "github.com/briandowns/spinner" - "github.com/spf13/cobra" - - zerr "zotregistry.io/zot/errors" -) - -const ( - cveDBRetryInterval = 3 -) - -func NewCveCommand(searchService SearchService) *cobra.Command { - searchCveParams := make(map[string]*string) - - var servURL, user, outputFormat string - - var isSpinner, verifyTLS, fixedFlag, verbose, debug bool - - cveCmd := &cobra.Command{ - Use: "cve [config-name]", - 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") - if servURL == "" { - if len(args) > 0 { - urlFromConfig, err := getConfigValue(configPath, args[0], "url") - if err != nil { - cmd.SilenceUsage = true - - return err - } - - if urlFromConfig == "" { - return zerr.ErrNoURLProvided - } - - servURL = urlFromConfig - } else { - return zerr.ErrNoURLProvided - } - } - - if len(args) > 0 { - var err error - isSpinner, err = parseBooleanConfig(configPath, args[0], showspinnerConfig) - if err != nil { - cmd.SilenceUsage = true - - return err - } - - verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig) - if err != nil { - cmd.SilenceUsage = true - - return err - } - } - - spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr())) - spin.Prefix = fmt.Sprintf("Fetching from %s..", servURL) - spin.Suffix = "\n\b" - - verbose = false - - searchConfig := searchConfig{ - params: searchCveParams, - searchService: searchService, - servURL: &servURL, - user: &user, - outputFormat: &outputFormat, - fixedFlag: &fixedFlag, - verifyTLS: &verifyTLS, - verbose: &verbose, - debug: &debug, - resultWriter: cmd.OutOrStdout(), - spinner: spinnerState{spin, isSpinner}, - } - - err = searchCve(searchConfig) - - if err != nil { - cmd.SilenceUsage = true - - return err - } - - return nil - }, - } - - vars := cveFlagVariables{ - searchCveParams: searchCveParams, - servURL: &servURL, - user: &user, - outputFormat: &outputFormat, - fixedFlag: &fixedFlag, - debug: &debug, - } - - setupCveFlags(cveCmd, vars) - - return cveCmd -} - -func setupCveFlags(cveCmd *cobra.Command, variables cveFlagVariables) { - variables.searchCveParams["imageName"] = cveCmd.Flags().StringP("image", "I", "", "List CVEs by IMAGENAME[:TAG]") - variables.searchCveParams["cveID"] = cveCmd.Flags().StringP("cve-id", "i", "", "List images affected by a CVE") - variables.searchCveParams["searchedCVE"] = cveCmd.Flags().StringP("search", "s", "", "Search specific CVEs by name/id") - - cveCmd.Flags().StringVar(variables.servURL, "url", "", "Specify zot server URL if config-name is not mentioned") - cveCmd.Flags().StringVarP(variables.user, "user", "u", "", `User Credentials of `+ - `zot server in USERNAME:PASSWORD format`) - cveCmd.Flags().StringVarP(variables.outputFormat, "output", "o", "", "Specify output format [text/json/yaml]."+ - " JSON and YAML format return all info for CVEs") - - cveCmd.Flags().BoolVar(variables.fixedFlag, "fixed", false, "List tags which have fixed a CVE") - cveCmd.Flags().BoolVar(variables.debug, "debug", false, "Show debug output") -} - -type cveFlagVariables struct { - searchCveParams map[string]*string - servURL *string - user *string - outputFormat *string - fixedFlag *bool - debug *bool -} - -func searchCve(searchConfig searchConfig) error { - var searchers []searcher - - if checkExtEndPoint(searchConfig) { - searchers = getCveSearchersGQL() - } else { - searchers = getCveSearchers() - } - - for _, searcher := range searchers { - // there can be CVE DB readiness issues on the server side - // we need a retry mechanism for that specific type of errors - maxAttempts := 20 - - for i := 0; i < maxAttempts; i++ { - found, err := searcher.search(searchConfig) - if !found { - // searcher does not support this searchConfig - // exit the attempts loop and try a different searcher - break - } - - if err == nil { - // searcher matcher search config and results are already printed - return nil - } - - if i+1 >= maxAttempts { - // searcher matches search config but there are errors - // this is the last attempt and we cannot retry - return err - } - - 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, - "[warning] CVE DB is not ready [", i, "] - retry in ", cveDBRetryInterval, " seconds") - time.Sleep(cveDBRetryInterval * time.Second) - - continue - } - - // an unrecoverable error occurred - return err - } - } - - return zerr.ErrInvalidFlagsCombination -} diff --git a/pkg/cli/cve_cmd_test.go b/pkg/cli/cve_cmd_test.go index bc9057cf..765125c2 100644 --- a/pkg/cli/cve_cmd_test.go +++ b/pkg/cli/cve_cmd_test.go @@ -16,7 +16,6 @@ import ( "regexp" "strconv" "strings" - "sync" "testing" "time" @@ -24,12 +23,10 @@ import ( 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/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" @@ -44,11 +41,31 @@ import ( ) func TestSearchCVECmd(t *testing.T) { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + rootDir := t.TempDir() + conf.Storage.RootDirectory = rootDir + + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + }, + } + + ctlr := api.NewController(conf) + cm := test.NewControllerManager(ctlr) + + cm.StartServer() + defer cm.StopServer() + Convey("Test CVE help", t, func() { args := []string{"--help"} configPath := makeConfigFile("") defer os.Remove(configPath) - cmd := NewCveCommand(new(mockService)) + cmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -62,7 +79,7 @@ func TestSearchCVECmd(t *testing.T) { args := []string{"-h"} configPath := makeConfigFile("") defer os.Remove(configPath) - cmd := NewCveCommand(new(mockService)) + cmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -73,52 +90,39 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test CVE no url", t, func() { - args := []string{"cvetest", "-i", "cveIdRandom"} + args := []string{"affected", "CVE-cveIdRandom", "--config", "cvetest"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cmd := NewCveCommand(new(mockService)) + cmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) - So(err, ShouldEqual, zerr.ErrNoURLProvided) - }) - - Convey("Test CVE no params", t, func() { - args := []string{"cvetest", "--url", "someUrl"} - configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) - defer os.Remove(configPath) - cmd := NewCveCommand(new(mockService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() - So(err, ShouldEqual, zerr.ErrInvalidFlagsCombination) + So(errors.Is(err, zerr.ErrNoURLProvided), ShouldBeTrue) }) Convey("Test CVE invalid url", t, func() { - args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "invalidUrl"} + args := []string{"list", "dummyImageName:tag", "--url", "invalidUrl"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cmd := NewCveCommand(new(searchService)) + cmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) - So(err, ShouldEqual, zerr.ErrInvalidURL) + So(errors.Is(err, zerr.ErrInvalidURL), ShouldBeTrue) So(buff.String(), ShouldContainSubstring, "invalid URL format") }) Convey("Test CVE invalid url port", t, func() { - args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "http://localhost:99999"} + args := []string{"list", "dummyImageName:tag", "--url", "http://localhost:99999"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cmd := NewCveCommand(new(searchService)) + cmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -129,10 +133,10 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test CVE unreachable", t, func() { - args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "http://localhost:9999"} + args := []string{"list", "dummyImageName:tag", "--url", "http://localhost:9999"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cmd := NewCveCommand(new(searchService)) + cmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -142,10 +146,10 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test CVE url from config", t, func() { - args := []string{"cvetest", "--image", "dummyImageName:tag"} - configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","url":"https://test-url.com","showspinner":false}]}`) + args := []string{"list", "dummyImageName:tag", "--config", "cvetest"} + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, baseURL)) defer os.Remove(configPath) - cmd := NewCveCommand(new(mockService)) + cmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -158,10 +162,10 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test debug flag", t, func() { - args := []string{"cvetest", "--image", "dummyImageName:tag", "--debug"} - configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","url":"https://test-url.com","showspinner":false}]}`) + args := []string{"list", "dummyImageName:tag", "--debug", "--config", "cvetest"} + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, baseURL)) defer os.Remove(configPath) - cmd := NewCveCommand(new(searchService)) + cmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -174,10 +178,10 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test CVE by name and CVE ID - long option", t, func() { - args := []string{"cvetest", "--image", "dummyImageName", "--cve-id", "aCVEID", "--url", "someURL"} + args := []string{"affected", "CVE-CVEID", "--repo", "dummyImageName", "--url", baseURL} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(mockService)) + cveCmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -191,11 +195,11 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test CVE by name and CVE ID - using shorthand", t, func() { - args := []string{"cvetest", "-I", "dummyImageName", "--cve-id", "aCVEID", "--url", "someURL"} + args := []string{"affected", "CVE-CVEID", "--repo", "dummyImageName", "--url", baseURL} buff := bytes.NewBufferString("") configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(mockService)) + cveCmd := NewCVECommand(new(mockService)) cveCmd.SetOut(buff) cveCmd.SetErr(buff) cveCmd.SetArgs(args) @@ -208,10 +212,10 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test CVE by image name - in text format", t, func() { - args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "someURL"} + args := []string{"list", "dummyImageName:tag", "--url", baseURL} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(mockService)) + cveCmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -224,10 +228,10 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test CVE by image name - in json format", t, func() { - args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "someURL", "-o", "json"} + args := []string{"list", "dummyImageName:tag", "--url", baseURL, "-f", "json"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(mockService)) + cveCmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -242,10 +246,10 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test CVE by image name - in yaml format", t, func() { - args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "someURL", "-o", "yaml"} + args := []string{"list", "dummyImageName:tag", "--url", baseURL, "-f", "yaml"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(mockService)) + cveCmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -259,10 +263,10 @@ func TestSearchCVECmd(t *testing.T) { So(err, ShouldBeNil) }) Convey("Test CVE by image name - invalid format", t, func() { - args := []string{"cvetest", "--image", "dummyImageName:tag", "--url", "someURL", "-o", "random"} + args := []string{"list", "dummyImageName:tag", "--url", baseURL, "-f", "random"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(mockService)) + cveCmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -271,14 +275,14 @@ func TestSearchCVECmd(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(err, ShouldNotBeNil) - So(strings.TrimSpace(str), ShouldEqual, "Error: invalid output format") + So(strings.TrimSpace(str), ShouldContainSubstring, zerr.ErrInvalidOutputFormat.Error()) }) Convey("Test images by CVE ID - positive", t, func() { - args := []string{"cvetest", "--cve-id", "aCVEID", "--url", "someURL"} + args := []string{"affected", "CVE-CVEID", "--repo", "anImage", "--url", baseURL} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(mockService)) + cveCmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -291,11 +295,11 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test images by CVE ID - positive with retries", t, func() { - args := []string{"cvetest", "--cve-id", "aCVEID", "--url", "someURL"} + args := []string{"affected", "CVE-CVEID", "--repo", "anImage", "--url", baseURL} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) mockService := mockServiceForRetry{succeedOn: 2} // CVE info will be provided in 2nd attempt - cveCmd := NewCveCommand(&mockService) + cveCmd := NewCVECommand(&mockService) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -305,18 +309,18 @@ func TestSearchCVECmd(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") t.Logf("Output: %s", str) So(strings.TrimSpace(str), ShouldContainSubstring, - "[warning] CVE DB is not ready [ 0 ] - retry in "+strconv.Itoa(cveDBRetryInterval)+" seconds") + "[warning] CVE DB is not ready [1] - retry in "+strconv.Itoa(CveDBRetryInterval)+" seconds") So(strings.TrimSpace(str), ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE anImage tag os/arch 6e2f80bf false 123kB") So(err, ShouldBeNil) }) Convey("Test images by CVE ID - failed after retries", t, func() { - args := []string{"cvetest", "--cve-id", "aCVEID", "--url", "someURL"} + args := []string{"affected", "CVE-CVEID", "--url", baseURL} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) mockService := mockServiceForRetry{succeedOn: -1} // CVE info will be unavailable on all retries - cveCmd := NewCveCommand(&mockService) + cveCmd := NewCVECommand(&mockService) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -326,17 +330,17 @@ func TestSearchCVECmd(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") t.Logf("Output: %s", str) So(strings.TrimSpace(str), ShouldContainSubstring, - "[warning] CVE DB is not ready [ 0 ] - retry in "+strconv.Itoa(cveDBRetryInterval)+" seconds") + "[warning] CVE DB is not ready [1] - retry in "+strconv.Itoa(CveDBRetryInterval)+" seconds") So(strings.TrimSpace(str), ShouldNotContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE anImage tag os/arch 6e2f80bf false 123kB") So(err, ShouldNotBeNil) }) Convey("Test images by CVE ID - invalid CVE ID", t, func() { - args := []string{"cvetest", "--cve-id", "invalidCVEID"} + args := []string{"affected", "CVE-invalidCVEID", "--config", "cvetest"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(mockService)) + cveCmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -346,25 +350,25 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test images by CVE ID - invalid url", t, func() { - args := []string{"cvetest", "--cve-id", "aCVEID", "--url", "invalidURL"} + args := []string{"affected", "CVE-CVEID", "--url", "invalidURL"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(NewSearchService()) + cveCmd := NewCVECommand(NewSearchService()) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) cveCmd.SetArgs(args) err := cveCmd.Execute() So(err, ShouldNotBeNil) - So(err, ShouldEqual, zerr.ErrInvalidURL) + So(errors.Is(err, zerr.ErrInvalidURL), ShouldBeTrue) So(buff.String(), ShouldContainSubstring, "invalid URL format") }) Convey("Test fixed tags by and image name CVE ID - positive", t, func() { - args := []string{"cvetest", "--cve-id", "aCVEID", "--image", "fixedImage", "--url", "someURL", "--fixed"} + args := []string{"fixed", "fixedImage", "CVE-CVEID", "--url", baseURL} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(mockService)) + cveCmd := NewCVECommand(new(mockService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -377,10 +381,10 @@ func TestSearchCVECmd(t *testing.T) { }) Convey("Test fixed tags by and image name CVE ID - invalid image name", t, func() { - args := []string{"cvetest", "--cve-id", "aCVEID", "--image", "invalidImageName"} + args := []string{"affected", "CVE-CVEID", "--image", "invalidImageName", "--config", "cvetest"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) defer os.Remove(configPath) - cveCmd := NewCveCommand(NewSearchService()) + cveCmd := NewCVECommand(NewSearchService()) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -443,20 +447,17 @@ func TestNegativeServerResponse(t *testing.T) { } Convey("Status Code Not Found", func() { - args := []string{"cvetest", "--image", "zot-cve-test:0.0.1"} + args := []string{"list", "zot-cve-test:0.0.1", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) cveCmd.SetArgs(args) err = cveCmd.Execute() - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - str = strings.TrimSpace(str) So(err, ShouldNotBeNil) - So(str, ShouldContainSubstring, "404 page not found") + So(err.Error(), ShouldContainSubstring, zerr.ErrExtensionNotEnabled.Error()) }) }) @@ -546,10 +547,10 @@ func TestNegativeServerResponse(t *testing.T) { panic(err) } - args := []string{"cvetest", "--cve-id", "CVE-2019-9923", "--image", "zot-cve-test", "--fixed"} + args := []string{"fixed", "zot-cve-test", "CVE-2019-9923", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -633,10 +634,10 @@ func TestServerCVEResponse(t *testing.T) { } Convey("Test CVE by image name - GQL - positive", t, func() { - args := []string{"cvetest", "--image", "zot-cve-test:0.0.1"} + args := []string{"list", "zot-cve-test:0.0.1", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -651,10 +652,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test CVE by image name - GQL - search CVE by title in results", t, func() { - args := []string{"cvetest", "--image", "zot-cve-test:0.0.1", "--search", "CVE-C1"} + args := []string{"list", "zot-cve-test:0.0.1", "--cve-id", "CVE-C1", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -670,10 +671,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test CVE by image name - GQL - search CVE by id in results", t, func() { - args := []string{"cvetest", "--image", "zot-cve-test:0.0.1", "--search", "CVE-2"} + args := []string{"list", "zot-cve-test:0.0.1", "--cve-id", "CVE-2", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -689,10 +690,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test CVE by image name - GQL - search nonexistent CVE", t, func() { - args := []string{"cvetest", "--image", "zot-cve-test:0.0.1", "--search", "CVE-100"} + args := []string{"list", "zot-cve-test:0.0.1", "--cve-id", "CVE-100", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -706,10 +707,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test CVE by image name - GQL - invalid image", t, func() { - args := []string{"cvetest", "--image", "invalid:0.0.1"} + args := []string{"list", "invalid:0.0.1", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -719,10 +720,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test CVE by image name - GQL - invalid image name and tag", t, func() { - args := []string{"cvetest", "--image", "invalid:"} + args := []string{"list", "invalid:", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -732,10 +733,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test CVE by image name - GQL - invalid output format", t, func() { - args := []string{"cvetest", "--image", "zot-cve-test:0.0.1", "-o", "random"} + args := []string{"list", "zot-cve-test:0.0.1", "-f", "random", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -746,10 +747,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test images by CVE ID - GQL - positive", t, func() { - args := []string{"cvetest", "--cve-id", "CVE-2019-9923"} + args := []string{"affected", "CVE-2019-9923", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -763,10 +764,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test images by CVE ID - GQL - invalid CVE ID", t, func() { - args := []string{"cvetest", "--cve-id", "invalid"} + args := []string{"affected", "CVE-invalid", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -780,10 +781,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test images by CVE ID - GQL - invalid output format", t, func() { - args := []string{"cvetest", "--cve-id", "CVE-2019-9923", "-o", "random"} + args := []string{"affected", "CVE-2019-9923", "-f", "random", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -794,10 +795,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test fixed tags by image name and CVE ID - GQL - positive", t, func() { - args := []string{"cvetest", "--cve-id", "CVE-2019-9923", "--image", "zot-cve-test", "--fixed"} + args := []string{"fixed", "zot-cve-test", "CVE-2019-9923", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -811,10 +812,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test fixed tags by image name and CVE ID - GQL - random cve", t, func() { - args := []string{"cvetest", "--cve-id", "random", "--image", "zot-cve-test", "--fixed"} + args := []string{"fixed", "zot-cve-test", "random", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -828,10 +829,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test fixed tags by image name and CVE ID - GQL - random image", t, func() { - args := []string{"cvetest", "--cve-id", "CVE-2019-20807", "--image", "zot-cv-test", "--fixed"} + args := []string{"fixed", "zot-cv-test", "CVE-2019-20807", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -845,10 +846,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test fixed tags by image name and CVE ID - GQL - invalid image", t, func() { - args := []string{"cvetest", "--cve-id", "CVE-2019-20807", "--image", "zot-cv-test:tag", "--fixed"} + args := []string{"fixed", "zot-cv-test:tag", "CVE-2019-20807", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -862,10 +863,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test CVE by name and CVE ID - GQL - positive", t, func() { - args := []string{"cvetest", "--image", "zot-cve-test", "--cve-id", "CVE-2019-9923"} + args := []string{"affected", "CVE-2019-9923", "--repo", "zot-cve-test", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -879,10 +880,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test CVE by name and CVE ID - GQL - invalid name and CVE ID", t, func() { - args := []string{"cvetest", "--image", "test", "--cve-id", "CVE-20807"} + args := []string{"affected", "CVE-20807", "--repo", "test", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -895,10 +896,10 @@ func TestServerCVEResponse(t *testing.T) { }) Convey("Test CVE by name and CVE ID - GQL - invalid output format", t, func() { - args := []string{"cvetest", "--image", "zot-cve-test", "--cve-id", "CVE-2019-9923", "-o", "random"} + args := []string{"affected", "CVE-2019-9923", "--repo", "zot-cve-test", "-f", "random", "--config", "cvetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) - cveCmd := NewCveCommand(new(searchService)) + cveCmd := NewCVECommand(new(searchService)) buff := bytes.NewBufferString("") cveCmd.SetOut(buff) cveCmd.SetErr(buff) @@ -907,156 +908,6 @@ func TestServerCVEResponse(t *testing.T) { So(err, ShouldNotBeNil) So(buff.String(), ShouldContainSubstring, "invalid output format") }) - - Convey("Test CVE by image name - positive", t, func() { - args := []string{"cvetest", "--image", "zot-cve-test:0.0.1"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cveCmd := MockNewCveCommand(new(searchService)) - buff := bytes.NewBufferString("") - cveCmd.SetOut(buff) - cveCmd.SetErr(buff) - cveCmd.SetArgs(args) - err = cveCmd.Execute() - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - str = strings.TrimSpace(str) - So(err, ShouldBeNil) - So(str, ShouldContainSubstring, "ID SEVERITY TITLE") - So(str, ShouldContainSubstring, "CVE") - }) - - Convey("Test CVE by image name - invalid image", t, func() { - args := []string{"cvetest", "--image", "invalid:0.0.1"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cveCmd := MockNewCveCommand(new(searchService)) - buff := bytes.NewBufferString("") - cveCmd.SetOut(buff) - cveCmd.SetErr(buff) - cveCmd.SetArgs(args) - err = cveCmd.Execute() - So(err, ShouldNotBeNil) - }) - - Convey("Test images by CVE ID - positive", t, func() { - args := []string{"cvetest", "--cve-id", "CVE-2019-9923"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cveCmd := MockNewCveCommand(new(searchService)) - buff := bytes.NewBufferString("") - cveCmd.SetOut(buff) - cveCmd.SetErr(buff) - cveCmd.SetArgs(args) - err := cveCmd.Execute() - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - str = strings.TrimSpace(str) - So(err, ShouldBeNil) - So(str, ShouldEqual, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE zot-cve-test 0.0.1 linux/amd64 40d1f749 false 605B") - }) - - Convey("Test images by CVE ID - invalid CVE ID", t, func() { - args := []string{"cvetest", "--cve-id", "invalid"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cveCmd := MockNewCveCommand(new(searchService)) - buff := bytes.NewBufferString("") - cveCmd.SetOut(buff) - cveCmd.SetErr(buff) - cveCmd.SetArgs(args) - err := cveCmd.Execute() - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - str = strings.TrimSpace(str) - So(err, ShouldBeNil) - So(str, ShouldNotContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") - }) - - Convey("Test fixed tags by and image name CVE ID - positive", t, func() { - args := []string{"cvetest", "--cve-id", "CVE-2019-9923", "--image", "zot-cve-test", "--fixed"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cveCmd := MockNewCveCommand(new(searchService)) - buff := bytes.NewBufferString("") - cveCmd.SetOut(buff) - cveCmd.SetErr(buff) - cveCmd.SetArgs(args) - err := cveCmd.Execute() - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - str = strings.TrimSpace(str) - So(err, ShouldBeNil) - So(str, ShouldEqual, "") - }) - - Convey("Test fixed tags by and image name CVE ID - random cve", t, func() { - args := []string{"cvetest", "--cve-id", "random", "--image", "zot-cve-test", "--fixed"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cveCmd := MockNewCveCommand(new(searchService)) - buff := bytes.NewBufferString("") - cveCmd.SetOut(buff) - cveCmd.SetErr(buff) - cveCmd.SetArgs(args) - err := cveCmd.Execute() - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - str = strings.TrimSpace(str) - So(err, ShouldBeNil) - So(strings.TrimSpace(str), ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") - }) - - Convey("Test fixed tags by and image name CVE ID - invalid image", t, func() { - args := []string{"cvetest", "--cve-id", "CVE-2019-20807", "--image", "zot-cv-test", "--fixed"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cveCmd := MockNewCveCommand(new(searchService)) - buff := bytes.NewBufferString("") - cveCmd.SetOut(buff) - cveCmd.SetErr(buff) - cveCmd.SetArgs(args) - err := cveCmd.Execute() - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - str = strings.TrimSpace(str) - So(err, ShouldNotBeNil) - So(strings.TrimSpace(str), ShouldNotContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") - }) - - Convey("Test CVE by name and CVE ID - positive", t, func() { - args := []string{"cvetest", "--image", "zot-cve-test", "--cve-id", "CVE-2019-9923"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cveCmd := MockNewCveCommand(new(searchService)) - buff := bytes.NewBufferString("") - cveCmd.SetOut(buff) - cveCmd.SetErr(buff) - cveCmd.SetArgs(args) - err := cveCmd.Execute() - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - So(err, ShouldBeNil) - So(strings.TrimSpace(str), ShouldResemble, - "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE zot-cve-test 0.0.1 linux/amd64 40d1f749 false 605B") - }) - - Convey("Test CVE by name and CVE ID - invalid name and CVE ID", t, func() { - args := []string{"cvetest", "--image", "test", "--cve-id", "CVE-20807"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cveCmd := MockNewCveCommand(new(searchService)) - buff := bytes.NewBufferString("") - cveCmd.SetOut(buff) - cveCmd.SetErr(buff) - cveCmd.SetArgs(args) - err := cveCmd.Execute() - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - So(err, ShouldBeNil) - So(strings.TrimSpace(str), ShouldNotContainSubstring, - "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") - }) } func TestCVECommandGQL(t *testing.T) { @@ -1084,9 +935,8 @@ func TestCVECommandGQL(t *testing.T) { defer os.Remove(configPath) Convey("cveid", func() { - args := []string{"cveid", "CVE-1942"} - cmd := NewCVESCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + args := []string{"affected", "CVE-1942", "--config", "cvetest"} + cmd := NewCVECommand(mockService{}) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1096,16 +946,16 @@ func TestCVECommandGQL(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "image-name tag 6e2f80bf false 123kB") + So(actual, ShouldContainSubstring, "image-name tag os/arch 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"} + args := []string{"affected", "CVE-12345", "--config", "cvetest"} defer os.Remove(configPath) - cmd := NewCVESCommand(mockService{ + cmd := NewCVECommand(mockService{ getTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName, cveID string) (*zcommon.ImagesForCve, error, ) { @@ -1119,7 +969,6 @@ func TestCVECommandGQL(t *testing.T) { return &zcommon.ImagesForCve{}, zerr.ErrInjected }, }) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1133,9 +982,8 @@ func TestCVECommandGQL(t *testing.T) { }) Convey("fixed", func() { - args := []string{"fixed", "image-name", "CVE-123"} - cmd := NewCVESCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + args := []string{"fixed", "image-name", "CVE-123", "--config", "cvetest"} + cmd := NewCVECommand(mockService{}) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1145,16 +993,16 @@ func TestCVECommandGQL(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "image-name tag 6e2f80bf false 123kB") + So(actual, ShouldContainSubstring, "image-name tag os/arch 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"} + args := []string{"fixed", "repo", "CVE-2222", "--config", "cvetest"} defer os.Remove(configPath) - cmd := NewCVESCommand(mockService{ + cmd := NewCVECommand(mockService{ getFixedTagsForCVEGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName, cveID string) (*zcommon.ImageListWithCVEFixedResponse, error, ) { @@ -1168,7 +1016,6 @@ func TestCVECommandGQL(t *testing.T) { return &zcommon.ImageListWithCVEFixedResponse{}, zerr.ErrInjected }, }) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1182,9 +1029,8 @@ func TestCVECommandGQL(t *testing.T) { }) Convey("image", func() { - args := []string{"image", "repo:tag"} - cmd := NewCVESCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + args := []string{"list", "repo:tag", "--config", "cvetest"} + cmd := NewCVECommand(mockService{}) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1201,9 +1047,9 @@ func TestCVECommandGQL(t *testing.T) { count := 0 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, baseURL)) - args := []string{"image", "repo:vuln"} + args := []string{"list", "repo:vuln", "--config", "cvetest"} defer os.Remove(configPath) - cmd := NewCVESCommand(mockService{ + cmd := NewCVECommand(mockService{ getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName, searchedCVE string) (*cveResult, error, ) { @@ -1217,7 +1063,6 @@ func TestCVECommandGQL(t *testing.T) { return &cveResult{}, zerr.ErrInjected }, }) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1232,12 +1077,18 @@ func TestCVECommandGQL(t *testing.T) { }) } -func TestCVECommandREST(t *testing.T) { +func TestCVECommandErrors(t *testing.T) { port := test.GetFreePort() baseURL := test.GetBaseURL(port) conf := config.New() conf.HTTP.Port = port + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: ref(true)}, + }, + } + ctlr := api.NewController(conf) ctlr.Config.Storage.RootDirectory = t.TempDir() cm := test.NewControllerManager(ctlr) @@ -1250,9 +1101,8 @@ func TestCVECommandREST(t *testing.T) { defer os.Remove(configPath) Convey("cveid", func() { - args := []string{"cveid", "CVE-1942"} - cmd := NewCVESCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + args := []string{"affected", "CVE-1942"} + cmd := NewCVECommand(mockService{}) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1295,8 +1145,7 @@ func TestCVECommandREST(t *testing.T) { Convey("fixed command", func() { args := []string{"fixed", "image-name", "CVE-123"} - cmd := NewCVESCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + cmd := NewCVECommand(mockService{}) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1338,9 +1187,8 @@ func TestCVECommandREST(t *testing.T) { }) Convey("image", func() { - args := []string{"image", "repo:tag"} - cmd := NewCVESCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "cvetest", "") + args := []string{"list", "repo:tag"} + cmd := NewCVECommand(mockService{}) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1383,108 +1231,6 @@ func TestCVECommandREST(t *testing.T) { }) } -func MockNewCveCommand(searchService SearchService) *cobra.Command { - searchCveParams := make(map[string]*string) - - var servURL, user, outputFormat string - - var verifyTLS, fixedFlag, verbose, debug bool - - cveCmd := &cobra.Command{ - RunE: func(cmd *cobra.Command, args []string) error { - home, err := os.UserHomeDir() - if err != nil { - panic(err) - } - - configPath := path.Join(home, "/.zot") - if len(args) > 0 { - urlFromConfig, err := getConfigValue(configPath, args[0], "url") - if err != nil { - cmd.SilenceUsage = true - - return err - } - - if urlFromConfig == "" { - return zerr.ErrNoURLProvided - } - - servURL = urlFromConfig - } else { - return zerr.ErrNoURLProvided - } - - if len(args) > 0 { - var err error - - verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig) - if err != nil { - cmd.SilenceUsage = true - - return err - } - } - - verbose = false - debug = false - - searchConfig := searchConfig{ - params: searchCveParams, - searchService: searchService, - servURL: &servURL, - user: &user, - outputFormat: &outputFormat, - fixedFlag: &fixedFlag, - verifyTLS: &verifyTLS, - verbose: &verbose, - debug: &debug, - resultWriter: cmd.OutOrStdout(), - } - - err = MockSearchCve(searchConfig) - - if err != nil { - cmd.SilenceUsage = true - - return err - } - - return nil - }, - } - - vars := cveFlagVariables{ - searchCveParams: searchCveParams, - servURL: &servURL, - user: &user, - outputFormat: &outputFormat, - fixedFlag: &fixedFlag, - debug: &debug, - } - - setupCveFlags(cveCmd, vars) - - return cveCmd -} - -func MockSearchCve(searchConfig searchConfig) error { - searchers := getCveSearchers() - - for _, searcher := range searchers { - found, err := searcher.search(searchConfig) - if found { - if err != nil { - return err - } - - return nil - } - } - - return zerr.ErrInvalidFlagsCombination -} - func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo { // MetaDB loaded with initial data, mock the scanner severities := map[string]int{ @@ -1608,19 +1354,14 @@ type mockServiceForRetry struct { succeedOn int } -func (service *mockServiceForRetry) getImagesByCveID(ctx context.Context, config searchConfig, - username, password, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, -) { +func (service *mockServiceForRetry) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, repo, + cveID string, +) (*zcommon.ImagesForCve, error) { service.retryCounter += 1 if service.retryCounter < service.succeedOn || service.succeedOn < 0 { - rch <- stringResult{"", zerr.ErrCVEDBNotFound} - close(rch) - - wtgrp.Done() - - return + return &zcommon.ImagesForCve{}, zerr.ErrCVEDBNotFound } - service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp) + return service.mockService.getTagsForCVEGQL(ctx, config, username, password, repo, cveID) } diff --git a/pkg/cli/cves_cmd.go b/pkg/cli/cves_cmd.go index f3ff3498..f5db8e1e 100644 --- a/pkg/cli/cves_cmd.go +++ b/pkg/cli/cves_cmd.go @@ -9,15 +9,21 @@ import ( "zotregistry.io/zot/pkg/cli/cmdflags" ) -func NewCVESCommand(searchService SearchService) *cobra.Command { +func NewCVECommand(searchService SearchService) *cobra.Command { cvesCmd := &cobra.Command{ - Use: "cves [command]", + Use: "cve [command]", Short: "Lookup CVEs in images hosted on the zot registry", Long: `List CVEs (Common Vulnerabilities and Exposures) of images hosted on the zot registry`, } cvesCmd.SetUsageTemplate(cvesCmd.UsageTemplate() + usageFooter) + cvesCmd.PersistentFlags().String(cmdflags.URLFlag, "", + "Specify zot server URL if config-name is not mentioned") + cvesCmd.PersistentFlags().String(cmdflags.ConfigFlag, "", + "Specify the registry configuration to use for connection") + cvesCmd.PersistentFlags().StringP(cmdflags.UserFlag, "u", "", + `User Credentials of zot server in "username:password" format`) 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") diff --git a/pkg/cli/cves_sub_cmd.go b/pkg/cli/cves_sub_cmd.go index 9da7e185..9220cc69 100644 --- a/pkg/cli/cves_sub_cmd.go +++ b/pkg/cli/cves_sub_cmd.go @@ -22,7 +22,7 @@ func NewCveForImageCommand(searchService SearchService) *cobra.Command { var searchedCVEID string cveForImageCmd := &cobra.Command{ - Use: "image [repo:tag]|[repo@digest]", + Use: "list [repo:tag]|[repo@digest]", Short: "List CVEs by REPO:TAG or REPO@DIGEST", Long: `List CVEs by REPO:TAG or REPO@DIGEST`, Args: OneImageWithRefArg, @@ -52,7 +52,7 @@ func NewImagesByCVEIDCommand(searchService SearchService) *cobra.Command { var repo string imagesByCVEIDCmd := &cobra.Command{ - Use: "cveid [cveId]", + Use: "affected [cveId]", Short: "List images affected by a CVE", Long: `List images affected by a CVE`, Args: func(cmd *cobra.Command, args []string) error { diff --git a/pkg/cli/discover.go b/pkg/cli/discover.go index b6e6cc04..e922fd1c 100644 --- a/pkg/cli/discover.go +++ b/pkg/cli/discover.go @@ -42,16 +42,6 @@ type typeField struct { Name string `json:"name"` } -func containsGQLQuery(queryList []field, query string) bool { - for _, q := range queryList { - if q.Name == query { - return true - } - } - - return false -} - func containsGQLQueryWithParams(queryList []field, serverGQLTypesList []typeInfo, requiredQueries ...GQLQuery) error { serverGQLTypes := map[string][]typeField{} @@ -100,73 +90,11 @@ func haveSameArgs(query field, reqQuery GQLQuery) bool { return true } -func checkExtEndPoint(config searchConfig) bool { - 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 false - } - - discoverResponse := &distext.ExtensionList{} - - _, err = makeGETRequest(ctx, discoverEndPoint, username, password, *config.verifyTLS, - *config.debug, &discoverResponse, config.resultWriter) - if err != nil { - return false - } - - 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 false - } - - searchEndPoint, _ := combineServerAndEndpointURL(*config.servURL, constants.FullSearchPrefix) - - query := ` - { - __schema() { - queryType { - fields { - name - } - } - } - }` - - queryResponse := &schemaList{} - - err = makeGraphQLRequest(ctx, searchEndPoint, query, username, password, *config.verifyTLS, - *config.debug, queryResponse, config.resultWriter) - if err != nil { - return false - } - - if err = checkResultGraphQLQuery(ctx, err, queryResponse.Errors); err != nil { - return false - } - - return containsGQLQuery(queryResponse.Data.Schema.QueryType.Fields, "ImageList") -} - func CheckExtEndPointQuery(config searchConfig, requiredQueries ...GQLQuery) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) ctx := context.Background() - discoverEndPoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("%s%s", + discoverEndPoint, err := combineServerAndEndpointURL(config.servURL, fmt.Sprintf("%s%s", constants.RoutePrefix, constants.ExtOciDiscoverPrefix)) if err != nil { return err @@ -174,8 +102,8 @@ func CheckExtEndPointQuery(config searchConfig, requiredQueries ...GQLQuery) err discoverResponse := &distext.ExtensionList{} - _, err = makeGETRequest(ctx, discoverEndPoint, username, password, *config.verifyTLS, - *config.debug, &discoverResponse, config.resultWriter) + _, err = makeGETRequest(ctx, discoverEndPoint, username, password, config.verifyTLS, + config.debug, &discoverResponse, config.resultWriter) if err != nil { return err } @@ -196,7 +124,7 @@ func CheckExtEndPointQuery(config searchConfig, requiredQueries ...GQLQuery) err return fmt.Errorf("%w: search extension gql endpoints not found", zerr.ErrExtensionNotEnabled) } - searchEndPoint, _ := combineServerAndEndpointURL(*config.servURL, constants.FullSearchPrefix) + searchEndPoint, _ := combineServerAndEndpointURL(config.servURL, constants.FullSearchPrefix) schemaQuery := ` { @@ -225,8 +153,8 @@ func CheckExtEndPointQuery(config searchConfig, requiredQueries ...GQLQuery) err queryResponse := &schemaList{} - err = makeGraphQLRequest(ctx, searchEndPoint, schemaQuery, username, password, *config.verifyTLS, - *config.debug, queryResponse, config.resultWriter) + 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) } diff --git a/pkg/cli/gql_queries_test.go b/pkg/cli/gql_queries_test.go index af98f4e2..a9ae1861 100644 --- a/pkg/cli/gql_queries_test.go +++ b/pkg/cli/gql_queries_test.go @@ -37,10 +37,10 @@ func TestGQLQueries(t *testing.T) { defer cm.StopServer() searchConfig := searchConfig{ - servURL: &baseURL, - user: ref(""), - verifyTLS: ref(false), - debug: ref(false), + servURL: baseURL, + user: "", + verifyTLS: false, + debug: false, resultWriter: io.Discard, } diff --git a/pkg/cli/image_cmd.go b/pkg/cli/image_cmd.go index f6679d55..9fcfc007 100644 --- a/pkg/cli/image_cmd.go +++ b/pkg/cli/image_cmd.go @@ -4,103 +4,47 @@ package cli import ( - "os" - "path" "strconv" "time" - "github.com/briandowns/spinner" "github.com/spf13/cobra" - zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/cli/cmdflags" +) + +const ( + spinnerDuration = 150 * time.Millisecond + usageFooter = ` +Run 'zli config -h' for details on [config-name] argument +` ) -//nolint:dupl func NewImageCommand(searchService SearchService) *cobra.Command { - searchImageParams := make(map[string]*string) - - var servURL, user, outputFormat string - - var isSpinner, verifyTLS, verbose, debug bool - imageCmd := &cobra.Command{ - 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") - if servURL == "" { - if len(args) > 0 { - urlFromConfig, err := getConfigValue(configPath, args[0], "url") - if err != nil { - cmd.SilenceUsage = true - - return err - } - - if urlFromConfig == "" { - return zerr.ErrNoURLProvided - } - - servURL = urlFromConfig - } else { - return zerr.ErrNoURLProvided - } - } - - if len(args) > 0 { - var err error - isSpinner, err = parseBooleanConfig(configPath, args[0], showspinnerConfig) - if err != nil { - cmd.SilenceUsage = true - - return err - } - - verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig) - if err != nil { - cmd.SilenceUsage = true - - return err - } - } - - spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr())) - spin.Prefix = prefix - - searchConfig := searchConfig{ - params: searchImageParams, - searchService: searchService, - servURL: &servURL, - user: &user, - outputFormat: &outputFormat, - verbose: &verbose, - debug: &debug, - spinner: spinnerState{spin, isSpinner}, - verifyTLS: &verifyTLS, - resultWriter: cmd.OutOrStdout(), - } - - err = searchImage(searchConfig) - - if err != nil { - cmd.SilenceUsage = true - - return err - } - - return nil - }, + Use: "image [command]", + Short: "List images hosted on the zot registry", + Long: `List images hosted on the zot registry`, } - setupImageFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose, &debug) imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter) + imageCmd.PersistentFlags().String(cmdflags.URLFlag, "", + "Specify zot server URL if config-name is not mentioned") + imageCmd.PersistentFlags().String(cmdflags.ConfigFlag, "", + "Specify the registry configuration to use for connection") + imageCmd.PersistentFlags().StringP(cmdflags.UserFlag, "u", "", + `User Credentials of zot server in "username:password" format`) + 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 } @@ -117,52 +61,3 @@ func parseBooleanConfig(configPath, configName, configParam string) (bool, error return val, nil } - -func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*string, - servURL, user, outputFormat *string, verbose *bool, debug *bool, -) { - searchImageParams["imageName"] = imageCmd.Flags().StringP("name", "n", "", "List image details by name") - searchImageParams["digest"] = imageCmd.Flags().StringP("digest", "d", "", - "List images containing a specific manifest, config, or layer digest") - searchImageParams["derivedImage"] = imageCmd.Flags().StringP("derived-images", "D", "", - "List images that are derived from given image") - searchImageParams["baseImage"] = imageCmd.Flags().StringP("base-images", "b", "", - "List images that are base for the given image") - - 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 { - var searchers []searcher - - if checkExtEndPoint(searchConfig) { - searchers = getImageSearchersGQL() - } else { - searchers = getImageSearchers() - } - - for _, searcher := range searchers { - found, err := searcher.search(searchConfig) - if found { - if err != nil { - return err - } - - return nil - } - } - - return zerr.ErrInvalidFlagsCombination -} - -const ( - spinnerDuration = 150 * time.Millisecond - usageFooter = ` -Run 'zli config -h' for details on [config-name] argument -` -) diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index 28604584..2e5ea52b 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "log" "os" @@ -23,13 +24,11 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" . "github.com/smartystreets/goconvey/convey" - "github.com/spf13/cobra" "gopkg.in/resty.v1" 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" @@ -65,7 +64,7 @@ func TestSearchImageCmd(t *testing.T) { }) Convey("Test image no url", t, func() { - args := []string{"imagetest", "--name", "dummyIdRandom"} + args := []string{"name", "dummyIdRandom", "--config", "imagetest"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) @@ -75,11 +74,11 @@ func TestSearchImageCmd(t *testing.T) { cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) - So(err, ShouldEqual, zerr.ErrNoURLProvided) + So(errors.Is(err, zerr.ErrNoURLProvided), ShouldBeTrue) }) Convey("Test image invalid home directory", t, func() { - args := []string{"imagetest", "--name", "dummyImageName"} + args := []string{"name", "dummyImageName", "--config", "imagetest"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) @@ -108,7 +107,7 @@ func TestSearchImageCmd(t *testing.T) { }) Convey("Test image no params", t, func() { - args := []string{"imagetest", "--url", "someUrl"} + args := []string{"--url", "someUrl"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) @@ -121,7 +120,7 @@ func TestSearchImageCmd(t *testing.T) { }) Convey("Test image invalid url", t, func() { - args := []string{"imagetest", "--name", "dummyImageName", "--url", "invalidUrl"} + args := []string{"name", "dummyImageName", "--url", "invalidUrl"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -136,7 +135,7 @@ func TestSearchImageCmd(t *testing.T) { }) Convey("Test image invalid url port", t, func() { - args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:99999"} + args := []string{"name", "dummyImageName", "--url", "http://localhost:99999"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -149,7 +148,7 @@ func TestSearchImageCmd(t *testing.T) { So(buff.String(), ShouldContainSubstring, "invalid port") Convey("without flags", func() { - args := []string{"imagetest", "--url", "http://localhost:99999"} + args := []string{"list", "--url", "http://localhost:99999"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -164,7 +163,7 @@ func TestSearchImageCmd(t *testing.T) { }) Convey("Test image unreachable", t, func() { - args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:9999"} + args := []string{"name", "dummyImageName", "--url", "http://localhost:9999"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -177,7 +176,7 @@ func TestSearchImageCmd(t *testing.T) { }) Convey("Test image url from config", t, func() { - args := []string{"imagetest", "--name", "dummyImageName"} + args := []string{"name", "dummyImageName", "--config", "imagetest"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) @@ -194,11 +193,11 @@ func TestSearchImageCmd(t *testing.T) { }) Convey("Test image by name", t, func() { - args := []string{"imagetest", "--name", "dummyImageName", "--url", "someUrlImage"} + args := []string{"name", "dummyImageName", "--url", "http://127.0.0.1:8080"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) imageCmd := NewImageCommand(new(mockService)) - buff := bytes.NewBufferString("") + buff := &bytes.Buffer{} imageCmd.SetOut(buff) imageCmd.SetErr(buff) imageCmd.SetArgs(args) @@ -208,59 +207,26 @@ func TestSearchImageCmd(t *testing.T) { So(strings.TrimSpace(str), ShouldEqual, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE dummyImageName tag os/arch 6e2f80bf false 123kB") So(err, ShouldBeNil) - Convey("using shorthand", func() { - args := []string{"imagetest", "-n", "dummyImageName", "--url", "someUrlImage"} - buff := bytes.NewBufferString("") - configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) - defer os.Remove(configPath) - imageCmd := NewImageCommand(new(mockService)) - imageCmd.SetOut(buff) - imageCmd.SetErr(buff) - imageCmd.SetArgs(args) - err := imageCmd.Execute() - - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, - "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE dummyImageName tag os/arch 6e2f80bf false 123kB") - So(err, ShouldBeNil) - }) }) Convey("Test image by digest", t, func() { - args := []string{"imagetest", "--digest", "6e2f80bf", "--url", "someUrlImage"} - configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) - defer os.Remove(configPath) - imageCmd := NewImageCommand(new(mockService)) - buff := bytes.NewBufferString("") - imageCmd.SetOut(buff) - imageCmd.SetErr(buff) - imageCmd.SetArgs(args) - err := imageCmd.Execute() + searchConfig := getTestSearchConfig("http://127.0.0.1:8080", new(mockService)) + buff := &bytes.Buffer{} + searchConfig.resultWriter = buff + err := SearchImagesByDigest(searchConfig, "6e2f80bf") + So(err, ShouldBeNil) + 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") So(err, ShouldBeNil) - - Convey("invalid URL format", func() { - args := []string{"imagetest", "--digest", "digest", "--url", "invalidURL"} - configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) - defer os.Remove(configPath) - imageCmd := NewImageCommand(NewSearchService()) - buff := bytes.NewBufferString("") - imageCmd.SetOut(buff) - imageCmd.SetErr(buff) - imageCmd.SetArgs(args) - err := imageCmd.Execute() - So(err, ShouldNotBeNil) - So(err, ShouldEqual, zerr.ErrInvalidURL) - So(buff.String(), ShouldContainSubstring, "invalid URL format") - }) }) } func TestSignature(t *testing.T) { + space := regexp.MustCompile(`\s+`) + Convey("Test from real server", t, func() { currentWorkingDir, err := os.Getwd() So(err, ShouldBeNil) @@ -283,22 +249,11 @@ func TestSignature(t *testing.T) { cm.StartAndWait(conf.HTTP.Port) defer cm.StopServer() - cfg, layers, manifest, err := test.GetImageComponents(1) //nolint:staticcheck - So(err, ShouldBeNil) - repoName := "repo7" - err = test.UploadImage( - test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - }, url, repoName, "test:1.0") + image := test.CreateDefaultImage() + err = test.UploadImage(image, url, repoName, "1.0") So(err, ShouldBeNil) - content, err := json.Marshal(manifest) - So(err, ShouldBeNil) - digest := godigest.FromBytes(content) - // generate a keypair if _, err := os.Stat(path.Join(currentDir, "cosign.key")); err != nil { os.Setenv("COSIGN_PASSWORD", "") @@ -317,38 +272,32 @@ func TestSignature(t *testing.T) { AnnotationOptions: options.AnnotationOptions{Annotations: []string{"tag=test:1.0"}}, Upload: true, }, - []string{fmt.Sprintf("localhost:%s/%s@%s", port, "repo7", digest.String())}) + []string{fmt.Sprintf("localhost:%s/%s@%s", port, "repo7", image.DigestStr())}) So(err, ShouldBeNil) + searchConfig := getTestSearchConfig(url, new(searchService)) + t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := NewImageCommand(new(searchService)) + buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() + searchConfig.resultWriter = buff + err = SearchAllImagesGQL(searchConfig) So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - actual := strings.TrimSpace(str) + + actual := strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) So(actual, ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:1.0 linux/amd64 8e59ed3b true 504B") + So(actual, ShouldContainSubstring, "repo7 1.0 linux/amd64 db573b01 true 854B") t.Log("Test getting all images using rest calls to get catalog and individual manifests") - cmd = MockNewImageCommand(new(searchService)) + buff = &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() + searchConfig.resultWriter = buff + err = SearchAllImages(searchConfig) So(err, ShouldBeNil) - str = space.ReplaceAllString(buff.String(), " ") - actual = strings.TrimSpace(str) + + actual = strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) So(actual, ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:1.0 linux/amd64 8e59ed3b true 504B") + So(actual, ShouldContainSubstring, "repo7 1.0 linux/amd64 db573b01 true 854B") err = os.Chdir(currentWorkingDir) So(err, ShouldBeNil) @@ -376,55 +325,35 @@ func TestSignature(t *testing.T) { cm.StartAndWait(conf.HTTP.Port) defer cm.StopServer() - cfg, layers, manifest, err := test.GetImageComponents(1) //nolint:staticcheck - So(err, ShouldBeNil) - repoName := "repo7" - err = test.UploadImage( - test.Image{ - Config: cfg, - Layers: layers, - Manifest: manifest, - }, url, repoName, "0.0.1") + err = test.UploadImage(test.CreateDefaultImage(), url, repoName, "0.0.1") So(err, ShouldBeNil) - content, err := json.Marshal(manifest) - So(err, ShouldBeNil) - digest := godigest.FromBytes(content) - So(digest, ShouldNotBeNil) - err = test.SignImageUsingNotary("repo7:0.0.1", port) So(err, ShouldBeNil) + searchConfig := getTestSearchConfig(url, new(searchService)) + t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := NewImageCommand(new(searchService)) + buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() + searchConfig.resultWriter = buff + err = SearchAllImagesGQL(searchConfig) So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - actual := strings.TrimSpace(str) + + actual := strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) So(actual, ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 0.0.1 linux/amd64 8e59ed3b true 504B") + So(actual, ShouldContainSubstring, "repo7 0.0.1 linux/amd64 db573b01 true 854B") t.Log("Test getting all images using rest calls to get catalog and individual manifests") - cmd = MockNewImageCommand(new(searchService)) buff = &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() + searchConfig.resultWriter = buff + err = SearchAllImages(searchConfig) So(err, ShouldBeNil) - str = space.ReplaceAllString(buff.String(), " ") - actual = strings.TrimSpace(str) + + actual = strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) So(actual, ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 0.0.1 linux/amd64 8e59ed3b true 504B") + So(actual, ShouldContainSubstring, "repo7 0.0.1 linux/amd64 db573b01 true 854B") err = os.Chdir(currentWorkingDir) So(err, ShouldBeNil) @@ -454,52 +383,37 @@ func TestDerivedImageList(t *testing.T) { panic(err) } + space := regexp.MustCompile(`\s+`) + searchConfig := getTestSearchConfig(url, new(searchService)) + t.Logf("rootDir: %s", ctlr.Config.Storage.RootDirectory) Convey("Test from real server", t, func() { Convey("Test derived images list working", func() { t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest", "--derived-images", "repo7:test:2.0"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := NewImageCommand(new(searchService)) + buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + searchConfig.resultWriter = buff + err := SearchDerivedImageListGQL(searchConfig, "repo7:test:2.0") + actual := strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") So(actual, ShouldContainSubstring, "repo7 test:1.0 linux/amd64 9d9461ed false 860B") }) Convey("Test derived images list fails", func() { - args := []string{"imagetest", "--derived-images", "repo7:test:missing"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := NewImageCommand(new(searchService)) buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() + searchConfig.resultWriter = buff + err := SearchDerivedImageListGQL(searchConfig, "repo7:test:missing") So(err, ShouldNotBeNil) }) Convey("Test derived images list cannot print", func() { - t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest", "--derived-images", "repo7:test:2.0", "-o", "random"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := NewImageCommand(new(searchService)) buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + searchConfig.resultWriter = buff + searchConfig.outputFormat = "random" + err := SearchDerivedImageListGQL(searchConfig, "repo7:test:2.0") So(err, ShouldNotBeNil) }) }) @@ -527,73 +441,54 @@ func TestBaseImageList(t *testing.T) { panic(err) } + space := regexp.MustCompile(`\s+`) + searchConfig := getTestSearchConfig(url, new(searchService)) + t.Logf("rootDir: %s", ctlr.Config.Storage.RootDirectory) Convey("Test from real server", t, func() { Convey("Test base images list working", func() { t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest", "--base-images", "repo7:test:1.0"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := NewImageCommand(new(searchService)) + buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + searchConfig.resultWriter = buff + err := SearchBaseImageListGQL(searchConfig, "repo7:test:1.0") So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - actual := strings.TrimSpace(str) + actual := strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) So(actual, ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") So(actual, ShouldContainSubstring, "repo7 test:2.0 linux/amd64 214e4bed false 530B") }) Convey("Test base images list fail", func() { - args := []string{"imagetest", "--base-images", "repo7:test:missing"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := NewImageCommand(new(searchService)) buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() + searchConfig.resultWriter = buff + err := SearchBaseImageListGQL(searchConfig, "repo7:test:missing") So(err, ShouldNotBeNil) }) Convey("Test base images list cannot print", func() { t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest", "--base-images", "repo7:test:1.0", "-o", "random"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := NewImageCommand(new(searchService)) buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + searchConfig.outputFormat = "random" + searchConfig.resultWriter = buff + err := SearchBaseImageListGQL(searchConfig, "repo7:test:missing") So(err, ShouldNotBeNil) }) }) } func TestListRepos(t *testing.T) { + searchConfig := getTestSearchConfig("https://test-url.com", new(mockService)) + Convey("Test listing repositories", t, func() { - args := []string{"config-test"} - configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`) - defer os.Remove(configPath) - cmd := NewRepoCommand(new(mockService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + buff := &bytes.Buffer{} + searchConfig.resultWriter = buff + err := SearchRepos(searchConfig) So(err, ShouldBeNil) }) Convey("Test listing repositories with debug flag", t, func() { - args := []string{"config-test", "--debug"} + args := []string{"list", "--config", "config-test", "--debug"} configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewRepoCommand(new(searchService)) @@ -611,7 +506,7 @@ func TestListRepos(t *testing.T) { }) Convey("Test error on home directory", t, func() { - args := []string{"config-test"} + args := []string{"list", "--config", "config-test"} configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) @@ -640,7 +535,7 @@ func TestListRepos(t *testing.T) { }) Convey("Test listing repositories error", t, func() { - args := []string{"config-test"} + args := []string{"list", "--config", "config-test"} configPath := makeConfigFile(`{"configs":[{"_name":"config-test", "url":"https://invalid.invalid","showspinner":false}]}`) defer os.Remove(configPath) @@ -654,7 +549,7 @@ func TestListRepos(t *testing.T) { }) Convey("Test unable to get config value", t, func() { - args := []string{"config-test-nonexistent"} + args := []string{"list", "--config", "config-test-nonexistent"} configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewRepoCommand(new(mockService)) @@ -667,20 +562,7 @@ func TestListRepos(t *testing.T) { }) Convey("Test error - no url provided", t, func() { - args := []string{"config-test"} - configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"","showspinner":false}]}`) - defer os.Remove(configPath) - cmd := NewRepoCommand(new(mockService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() - So(err, ShouldNotBeNil) - }) - - Convey("Test error - no args provided", t, func() { - var args []string + args := []string{"list", "--config", "config-test"} configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewRepoCommand(new(mockService)) @@ -693,7 +575,7 @@ func TestListRepos(t *testing.T) { }) Convey("Test error - spinner config invalid", t, func() { - args := []string{"config-test"} + args := []string{"list", "--config", "config-test"} configPath := makeConfigFile(`{"configs":[{"_name":"config-test", "url":"https://test-url.com","showspinner":invalid}]}`) defer os.Remove(configPath) @@ -707,7 +589,7 @@ func TestListRepos(t *testing.T) { }) Convey("Test error - verifyTLSConfig fails", t, func() { - args := []string{"config-test"} + args := []string{"list", "--config", "config-test"} configPath := makeConfigFile(`{"configs":[{"_name":"config-test", "verify-tls":"invalid", "url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) @@ -723,7 +605,7 @@ func TestListRepos(t *testing.T) { func TestOutputFormat(t *testing.T) { Convey("Test text", t, func() { - args := []string{"imagetest", "--name", "dummyImageName", "-o", "text"} + args := []string{"name", "dummyImageName", "--config", "imagetest", "-f", "text"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) @@ -740,7 +622,7 @@ func TestOutputFormat(t *testing.T) { }) Convey("Test json", t, func() { - args := []string{"imagetest", "--name", "dummyImageName", "-o", "json"} + args := []string{"name", "dummyImageName", "--config", "imagetest", "-f", "json"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) @@ -767,7 +649,7 @@ func TestOutputFormat(t *testing.T) { }) Convey("Test yaml", t, func() { - args := []string{"imagetest", "--name", "dummyImageName", "-o", "yaml"} + args := []string{"name", "dummyImageName", "--config", "imagetest", "-f", "yaml"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) @@ -798,7 +680,7 @@ func TestOutputFormat(t *testing.T) { So(err, ShouldBeNil) Convey("Test yml", func() { - args := []string{"imagetest", "--name", "dummyImageName", "-o", "yml"} + args := []string{"name", "dummyImageName", "--config", "imagetest", "-f", "yml"} configPath := makeConfigFile( `{"configs":[{"_name":"imagetest",` + `"url":"https://test-url.com","showspinner":false}]}`, @@ -834,7 +716,7 @@ func TestOutputFormat(t *testing.T) { }) Convey("Test invalid", t, func() { - args := []string{"imagetest", "--name", "dummyImageName", "-o", "random"} + args := []string{"name", "dummyImageName", "--config", "imagetest", "-f", "random"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) @@ -870,7 +752,7 @@ func TestOutputFormatGQL(t *testing.T) { Convey("Test json", func() { t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest", "--name", "repo7", "-o", "json"} + args := []string{"name", "repo7", "--config", "imagetest", "-f", "json"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -912,7 +794,7 @@ func TestOutputFormatGQL(t *testing.T) { }) Convey("Test yaml", func() { - args := []string{"imagetest", "--name", "repo7", "-o", "yaml"} + args := []string{"name", "repo7", "--config", "imagetest", "-f", "yaml"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -955,7 +837,7 @@ func TestOutputFormatGQL(t *testing.T) { }) Convey("Test yml", func() { - args := []string{"imagetest", "--name", "repo7", "-o", "yml"} + args := []string{"name", "repo7", "--config", "imagetest", "-f", "yml"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -999,7 +881,7 @@ func TestOutputFormatGQL(t *testing.T) { }) Convey("Test invalid", func() { - args := []string{"imagetest", "--name", "repo7", "-o", "random"} + args := []string{"name", "repo7", "--config", "imagetest", "-f", "random"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1036,7 +918,7 @@ func TestServerResponseGQL(t *testing.T) { Convey("Test all images config url", func() { t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest"} + args := []string{"list", "--config", "imagetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1053,7 +935,7 @@ func TestServerResponseGQL(t *testing.T) { So(actual, ShouldContainSubstring, "repo7 test:2.0 linux/amd64 51e18f50 false 528B") So(actual, ShouldContainSubstring, "repo7 test:1.0 linux/amd64 51e18f50 false 528B") Convey("Test all images invalid output format", func() { - args := []string{"imagetest", "-o", "random"} + args := []string{"list", "--config", "imagetest", "-f", "random"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1068,7 +950,7 @@ func TestServerResponseGQL(t *testing.T) { }) Convey("Test all images verbose", func() { - args := []string{"imagetest", "--verbose"} + args := []string{"list", "--config", "imagetest", "--verbose"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1093,7 +975,7 @@ func TestServerResponseGQL(t *testing.T) { }) Convey("Test all images with debug flag", func() { - args := []string{"imagetest", "--debug"} + args := []string{"list", "--config", "imagetest", "--debug"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1113,7 +995,7 @@ func TestServerResponseGQL(t *testing.T) { }) Convey("Test image by name config url", func() { - args := []string{"imagetest", "--name", "repo7"} + args := []string{"name", "repo7", "--config", "imagetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1130,27 +1012,8 @@ func TestServerResponseGQL(t *testing.T) { So(actual, ShouldContainSubstring, "repo7 test:2.0 linux/amd64 51e18f50 false 528B") So(actual, ShouldContainSubstring, "repo7 test:1.0 linux/amd64 51e18f50 false 528B") - Convey("with shorthand", func() { - args := []string{"imagetest", "-n", "repo7"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := NewImageCommand(new(searchService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() - So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 linux/amd64 51e18f50 false 528B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 linux/amd64 51e18f50 false 528B") - }) - Convey("invalid output format", func() { - args := []string{"imagetest", "--name", "repo7", "-o", "random"} + args := []string{"name", "repo7", "--config", "imagetest", "-f", "random"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1165,7 +1028,7 @@ func TestServerResponseGQL(t *testing.T) { }) Convey("Test image by digest", func() { - args := []string{"imagetest", "--digest", "51e18f50"} + args := []string{"digest", "51e18f50", "--config", "imagetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1186,27 +1049,8 @@ func TestServerResponseGQL(t *testing.T) { So(actual, ShouldContainSubstring, "repo7 test:2.0 linux/amd64 51e18f50 false 528B") So(actual, ShouldContainSubstring, "repo7 test:1.0 linux/amd64 51e18f50 false 528B") - Convey("with shorthand", func() { - args := []string{"imagetest", "-d", "51e18f50"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := NewImageCommand(new(searchService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() - So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") - So(actual, ShouldContainSubstring, "repo7 test:2.0 linux/amd64 51e18f50 false 528B") - So(actual, ShouldContainSubstring, "repo7 test:1.0 linux/amd64 51e18f50 false 528B") - }) - Convey("nonexistent digest", func() { - args := []string{"imagetest", "--digest", "d1g35t"} + args := []string{"digest", "d1g35t", "--config", "imagetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1220,7 +1064,7 @@ func TestServerResponseGQL(t *testing.T) { }) Convey("invalid output format", func() { - args := []string{"imagetest", "--digest", "51e18f50", "-o", "random"} + args := []string{"digest", "51e18f50", "--config", "imagetest", "-f", "random"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1235,7 +1079,7 @@ func TestServerResponseGQL(t *testing.T) { }) Convey("Test image by name nonexistent name", func() { - args := []string{"imagetest", "--name", "repo777"} + args := []string{"name", "repo777", "--config", "imagetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) @@ -1249,7 +1093,7 @@ func TestServerResponseGQL(t *testing.T) { }) Convey("Test list repos error", func() { - args := []string{"config-test"} + args := []string{"list", "--config", "config-test"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"config-test", "url":"%s","showspinner":false}]}`, url)) @@ -1292,22 +1136,17 @@ func TestServerResponse(t *testing.T) { panic(err) } - t.Logf("%s", ctlr.Config.Storage.RootDirectory) + space := regexp.MustCompile(`\s+`) Convey("Test from real server", t, func() { + searchConfig := getTestSearchConfig(url, new(searchService)) + Convey("Test all images", func() { - t.Logf("%s", ctlr.Config.Storage.RootDirectory) - args := []string{"imagetest"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := MockNewImageCommand(new(searchService)) buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + searchConfig.resultWriter = buff + err := SearchAllImages(searchConfig) So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") @@ -1316,17 +1155,13 @@ func TestServerResponse(t *testing.T) { }) Convey("Test all images verbose", func() { - args := []string{"imagetest", "--verbose"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := MockNewImageCommand(new(searchService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + buff := &bytes.Buffer{} + searchConfig.resultWriter = buff + searchConfig.verbose = true + defer func() { searchConfig.verbose = false }() + err := SearchAllImages(searchConfig) So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) // Actual cli output should be something similar to (order of images may differ): @@ -1341,17 +1176,11 @@ func TestServerResponse(t *testing.T) { }) Convey("Test image by name", func() { - args := []string{"imagetest", "--name", "repo7"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := MockNewImageCommand(new(searchService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + buff := &bytes.Buffer{} + searchConfig.resultWriter = buff + err := SearchImageByName(searchConfig, "repo7") So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") @@ -1360,17 +1189,11 @@ func TestServerResponse(t *testing.T) { }) Convey("Test image by digest", func() { - args := []string{"imagetest", "--digest", "51e18f50"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := MockNewImageCommand(new(searchService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + buff := &bytes.Buffer{} + searchConfig.resultWriter = buff + err := SearchImagesByDigest(searchConfig, "51e18f50") So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) // Actual cli output should be something similar to (order of images may differ): @@ -1382,33 +1205,20 @@ func TestServerResponse(t *testing.T) { So(actual, ShouldContainSubstring, "repo7 test:1.0 linux/amd64 51e18f50 false 528B") Convey("nonexistent digest", func() { - args := []string{"imagetest", "--digest", "d1g35t"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := MockNewImageCommand(new(searchService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + buff := &bytes.Buffer{} + searchConfig.resultWriter = buff + err := SearchImagesByDigest(searchConfig, "d1g35t") So(err, ShouldBeNil) + So(len(buff.String()), ShouldEqual, 0) }) }) Convey("Test image by name nonexistent name", func() { - args := []string{"imagetest", "--name", "repo777"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) - defer os.Remove(configPath) - cmd := MockNewImageCommand(new(searchService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + err := SearchImageByName(searchConfig, "repo777") So(err, ShouldNotBeNil) - actual := buff.String() - So(actual, ShouldContainSubstring, "unknown") + + So(err.Error(), ShouldContainSubstring, "no repository found") }) }) } @@ -1493,21 +1303,17 @@ func TestDisplayIndex(t *testing.T) { } func runDisplayIndexTests(baseURL string) { + space := regexp.MustCompile(`\s+`) + searchConfig := getTestSearchConfig(baseURL, new(searchService)) + Convey("Test Image Index", func() { uploadTestMultiarch(baseURL) - args := []string{"imagetest"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, - baseURL)) - defer os.Remove(configPath) - cmd := MockNewImageCommand(new(searchService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + buff := &bytes.Buffer{} + searchConfig.resultWriter = buff + err := SearchAllImages(searchConfig) So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) // Actual cli output should be something similar to (order of images may differ): @@ -1524,18 +1330,12 @@ func runDisplayIndexTests(baseURL string) { Convey("Test Image Index Verbose", func() { uploadTestMultiarch(baseURL) - args := []string{"imagetest", "--verbose"} - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, - baseURL)) - defer os.Remove(configPath) - cmd := MockNewImageCommand(new(searchService)) - buff := bytes.NewBufferString("") - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() + buff := &bytes.Buffer{} + searchConfig.resultWriter = buff + searchConfig.verbose = true + err := SearchAllImages(searchConfig) So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) // Actual cli output should be something similar to (order of images may differ): @@ -1597,10 +1397,9 @@ func TestImagesCommandGQL(t *testing.T) { configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, baseURL)) defer os.Remove(configPath) + args := []string{"base", "repo:derived", "--config", "imagetest"} - args := []string{"base", "repo:derived"} - cmd := NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + cmd := NewImageCommand(NewSearchService()) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1611,10 +1410,9 @@ func TestImagesCommandGQL(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldContainSubstring, "repo base linux/amd64 df554ddd false 699B") + args = []string{"derived", "repo:base", "--config", "imagetest"} - args = []string{"derived", "repo:base"} - cmd = NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + cmd = NewImageCommand(NewSearchService()) buff = bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1629,7 +1427,7 @@ func TestImagesCommandGQL(t *testing.T) { Convey("base and derived command errors", func() { // too many parameters buff := bytes.NewBufferString("") - args := []string{"too", "many", "args"} + args := []string{"too", "many", "args", "--config", "imagetest"} cmd := NewImageBaseCommand(NewSearchService()) cmd.SetOut(buff) cmd.SetErr(buff) @@ -1688,10 +1486,9 @@ func TestImagesCommandGQL(t *testing.T) { configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, baseURL)) defer os.Remove(configPath) + args := []string{"digest", image.DigestStr(), "--config", "imagetest"} - args := []string{"digest", image.DigestStr()} - cmd := NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + cmd := NewImageCommand(NewSearchService()) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1708,7 +1505,7 @@ func TestImagesCommandGQL(t *testing.T) { Convey("digest command errors", func() { // too many parameters buff := bytes.NewBufferString("") - args := []string{"too", "many", "args"} + args := []string{"too", "many", "args", "--config", "imagetest"} cmd := NewImageDigestCommand(NewSearchService()) cmd.SetOut(buff) cmd.SetErr(buff) @@ -1746,10 +1543,9 @@ func TestImagesCommandGQL(t *testing.T) { configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, baseURL)) defer os.Remove(configPath) + args := []string{"list", "--config", "imagetest"} - args := []string{"list"} - cmd := NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + cmd := NewImageCommand(NewSearchService()) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1768,7 +1564,7 @@ func TestImagesCommandGQL(t *testing.T) { Convey("list command errors", func() { // too many parameters buff := bytes.NewBufferString("") - args := []string{"repo:img", "arg"} + args := []string{"repo:img", "arg", "--config", "imagetest"} cmd := NewImageListCommand(NewSearchService()) cmd.SetOut(buff) cmd.SetErr(buff) @@ -1799,10 +1595,9 @@ func TestImagesCommandGQL(t *testing.T) { configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, baseURL)) defer os.Remove(configPath) + args := []string{"name", "repo:img", "--config", "imagetest"} - args := []string{"name", "repo:img"} - cmd := NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + cmd := NewImageCommand(NewSearchService()) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1821,7 +1616,7 @@ func TestImagesCommandGQL(t *testing.T) { Convey("name command errors", func() { // too many parameters buff := bytes.NewBufferString("") - args := []string{"repo:img", "arg"} + args := []string{"repo:img", "arg", "--config", "imagetest"} cmd := NewImageNameCommand(NewSearchService()) cmd.SetOut(buff) cmd.SetErr(buff) @@ -1857,10 +1652,10 @@ func TestImagesCommandGQL(t *testing.T) { 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", "") + + args := []string{"cve", "repo:vuln", "--config", "imagetest"} + cmd := NewImageCommand(mockService{}) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1877,9 +1672,9 @@ func TestImagesCommandGQL(t *testing.T) { 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{ + args := []string{"cve", "repo:vuln", "--config", "imagetest"} + cmd := NewImageCommand(mockService{ getCveByImageGQLFn: func(ctx context.Context, config searchConfig, username, password, imageName, searchedCVE string) (*cveResult, error, ) { @@ -1893,7 +1688,6 @@ func TestImagesCommandGQL(t *testing.T) { return &cveResult{}, zerr.ErrInjected }, }) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1908,9 +1702,8 @@ func TestImagesCommandGQL(t *testing.T) { }) Convey("Config error", t, func() { - args := []string{"base", "repo:derived"} - cmd := NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + args := []string{"base", "repo:derived", "--config", "imagetest"} + cmd := NewImageCommand(NewSearchService()) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1920,7 +1713,7 @@ func TestImagesCommandGQL(t *testing.T) { So(err, ShouldNotBeNil) args = []string{"derived", "repo:base"} - cmd = NewImagesCommand(NewSearchService()) + cmd = NewImageCommand(NewSearchService()) buff = bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1929,7 +1722,7 @@ func TestImagesCommandGQL(t *testing.T) { So(err, ShouldNotBeNil) args = []string{"digest", ispec.DescriptorEmptyJSON.Digest.String()} - cmd = NewImagesCommand(NewSearchService()) + cmd = NewImageCommand(NewSearchService()) buff = bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1938,7 +1731,7 @@ func TestImagesCommandGQL(t *testing.T) { So(err, ShouldNotBeNil) args = []string{"list"} - cmd = NewImagesCommand(NewSearchService()) + cmd = NewImageCommand(NewSearchService()) buff = bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1947,7 +1740,7 @@ func TestImagesCommandGQL(t *testing.T) { So(err, ShouldNotBeNil) args = []string{"name", "repo:img"} - cmd = NewImagesCommand(NewSearchService()) + cmd = NewImageCommand(NewSearchService()) buff = bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -1956,7 +1749,7 @@ func TestImagesCommandGQL(t *testing.T) { So(err, ShouldNotBeNil) args = []string{"cve", "repo:vuln"} - cmd = NewImagesCommand(mockService{}) + cmd = NewImageCommand(mockService{}) buff = bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -2002,9 +1795,8 @@ func TestImageCommandREST(t *testing.T) { baseURL)) defer os.Remove(configPath) - args := []string{"base", "repo:derived"} - cmd := NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + args := []string{"base", "repo:derived", "--config", "imagetest"} + cmd := NewImageCommand(NewSearchService()) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -2013,8 +1805,7 @@ func TestImageCommandREST(t *testing.T) { So(err, ShouldNotBeNil) args = []string{"derived", "repo:base"} - cmd = NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + cmd = NewImageCommand(NewSearchService()) buff = bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -2033,9 +1824,8 @@ func TestImageCommandREST(t *testing.T) { baseURL)) defer os.Remove(configPath) - args := []string{"digest", image.DigestStr()} - cmd := NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + args := []string{"digest", image.DigestStr(), "--config", "imagetest"} + cmd := NewImageCommand(NewSearchService()) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -2054,9 +1844,8 @@ func TestImageCommandREST(t *testing.T) { baseURL)) defer os.Remove(configPath) - args := []string{"list"} - cmd := NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + args := []string{"list", "--config", "imagetest"} + cmd := NewImageCommand(NewSearchService()) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -2080,9 +1869,8 @@ func TestImageCommandREST(t *testing.T) { baseURL)) defer os.Remove(configPath) - args := []string{"name", "repo:img"} - cmd := NewImagesCommand(NewSearchService()) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + args := []string{"name", "repo:img", "--config", "imagetest"} + cmd := NewImageCommand(NewSearchService()) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -2100,10 +1888,9 @@ func TestImageCommandREST(t *testing.T) { configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, baseURL)) - args := []string{"cve", "repo:vuln"} + args := []string{"cve", "repo:vuln", "--config", "imagetest"} defer os.Remove(configPath) - cmd := NewImagesCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "imagetest", "") + cmd := NewImageCommand(mockService{}) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -2151,96 +1938,6 @@ func uploadTestMultiarch(baseURL string) { So(err, ShouldBeNil) } -func MockNewImageCommand(searchService SearchService) *cobra.Command { - searchImageParams := make(map[string]*string) - - var servURL, user, outputFormat string - - var verifyTLS, verbose, debug bool - - imageCmd := &cobra.Command{ - RunE: func(cmd *cobra.Command, args []string) error { - home, err := os.UserHomeDir() - if err != nil { - panic(err) - } - - configPath := path.Join(home, "/.zot") - if len(args) > 0 { - urlFromConfig, err := getConfigValue(configPath, args[0], "url") - if err != nil { - cmd.SilenceUsage = true - - return err - } - - if urlFromConfig == "" { - return zerr.ErrNoURLProvided - } - - servURL = urlFromConfig - } else { - return zerr.ErrNoURLProvided - } - - if len(args) > 0 { - var err error - - verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig) - if err != nil { - cmd.SilenceUsage = true - - return err - } - } - - searchConfig := searchConfig{ - params: searchImageParams, - searchService: searchService, - servURL: &servURL, - user: &user, - outputFormat: &outputFormat, - verbose: &verbose, - debug: &debug, - verifyTLS: &verifyTLS, - resultWriter: cmd.OutOrStdout(), - } - - err = MockSearchImage(searchConfig) - - if err != nil { - cmd.SilenceUsage = true - - return err - } - - return nil - }, - } - - setupImageFlags(imageCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose, &debug) - imageCmd.SetUsageTemplate(imageCmd.UsageTemplate() + usageFooter) - - return imageCmd -} - -func MockSearchImage(searchConfig searchConfig) error { - searchers := getImageSearchers() - - for _, searcher := range searchers { - found, err := searcher.search(searchConfig) - if found { - if err != nil { - return err - } - - return nil - } - } - - return zerr.ErrInvalidFlagsCombination -} - func uploadManifest(url string) error { // create and upload a blob/layer resp, _ := resty.R().Post(url + "/v2/repo7/blobs/uploads/") @@ -2465,18 +2162,6 @@ type mockService struct { 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, ) @@ -2509,10 +2194,6 @@ type mockService struct { 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) @@ -2622,6 +2303,7 @@ func (service mockService) getDerivedImageListGQL(ctx context.Context, config se ConfigDigest: godigest.FromString("ConfigDigest").String(), Size: "123445", Layers: []common.LayerSummary{{Digest: godigest.FromString("LayerDigest").String()}}, + Platform: common.Platform{Os: "os", Arch: "arch"}, }, }, Size: "123445", @@ -2649,6 +2331,7 @@ func (service mockService) getBaseImageListGQL(ctx context.Context, config searc ConfigDigest: godigest.FromString("ConfigDigest").String(), Size: "123445", Layers: []common.LayerSummary{{Digest: godigest.FromString("LayerDigest").String()}}, + Platform: common.Platform{Os: "os", Arch: "arch"}, }, }, Size: "123445", @@ -2678,6 +2361,7 @@ func (service mockService) getImagesGQL(ctx context.Context, config searchConfig ConfigDigest: godigest.FromString("ConfigDigest").String(), Size: "123445", Layers: []common.LayerSummary{{Digest: godigest.FromString("LayerDigest").String()}}, + Platform: common.Platform{Os: "os", Arch: "arch"}, }, }, Size: "123445", @@ -2707,6 +2391,7 @@ func (service mockService) getImagesForDigestGQL(ctx context.Context, config sea ConfigDigest: godigest.FromString("ConfigDigest").String(), Layers: []common.LayerSummary{{Digest: godigest.FromString("LayerDigest").String()}}, Size: "123445", + Platform: common.Platform{Os: "os", Arch: "arch"}, }, }, Size: "123445", @@ -2716,28 +2401,6 @@ func (service mockService) getImagesForDigestGQL(ctx context.Context, config sea return imageListGQLResponse, nil } -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 { - common.PaginatedImagesResult `json:"ImageListForCVE"` //nolint:tagliatelle - }{}, - } - - imagesForCve.Errors = nil - - mockedImage := service.getMockedImageByName("anImage") - imagesForCve.Results = []common.ImageSummary{common.ImageSummary(mockedImage)} - - return imagesForCve, nil -} - func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string, ) (*common.ImagesForCve, error) { @@ -2829,6 +2492,7 @@ func (service mockService) getMockedImageByName(imageName string) imageStruct { ConfigDigest: godigest.FromString("ConfigDigest").String(), Layers: []common.LayerSummary{{Digest: godigest.FromString("LayerDigest").String()}}, Size: "123445", + Platform: common.Platform{Os: "os", Arch: "arch"}, }, } image.Size = "123445" @@ -2864,7 +2528,7 @@ func (service mockService) getAllImages(ctx context.Context, config searchConfig } image.Size = "123445" - str, err := image.string(*config.outputFormat, len(image.RepoName), len(image.Tag), len("os/Arch"), *config.verbose) + str, err := image.string(config.outputFormat, len(image.RepoName), len(image.Tag), len("os/Arch"), config.verbose) if err != nil { channel <- stringResult{"", err} @@ -2902,7 +2566,7 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf } image.Size = "123445" - str, err := image.string(*config.outputFormat, len(image.RepoName), len(image.Tag), len("os/Arch"), *config.verbose) + str, err := image.string(config.outputFormat, len(image.RepoName), len(image.Tag), len("os/Arch"), config.verbose) if err != nil { channel <- stringResult{"", err} @@ -2912,89 +2576,6 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf channel <- stringResult{str, nil} } -func (service mockService) getCveByImage(ctx context.Context, config searchConfig, username, password, - imageName, searchedCVE string, rch chan stringResult, wtgrp *sync.WaitGroup, -) { - defer wtgrp.Done() - defer close(rch) - - cveRes := &cveResult{} - cveRes.Data = cveData{ - CVEListForImage: cveListForImage{ - Tag: imageName, - CVEList: []cve{ - { - ID: "dummyCVEID", - Description: "Description of the CVE", - Title: "Title of that CVE", - Severity: "HIGH", - PackageList: []packageList{ - { - Name: "packagename", - FixedVersion: "fixedver", - InstalledVersion: "installedver", - }, - }, - }, - }, - }, - } - - str, err := cveRes.string(*config.outputFormat) - if err != nil { - rch <- stringResult{"", err} - - return - } - - rch <- stringResult{str, nil} -} - -func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig, - 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, 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, 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, ) { @@ -3026,3 +2607,24 @@ func makeConfigFile(content string) string { return configPath } + +func getTestSearchConfig(url string, searchService SearchService) searchConfig { + var ( + user string = "" + outputFormat string = "" + verbose bool = false + debug bool = false + verifyTLS bool = false + ) + + return searchConfig{ + searchService: searchService, + servURL: url, + user: user, + outputFormat: outputFormat, + verbose: verbose, + debug: debug, + verifyTLS: verifyTLS, + resultWriter: nil, + } +} diff --git a/pkg/cli/images_sub_cmd.go b/pkg/cli/image_sub_cmd.go similarity index 58% rename from pkg/cli/images_sub_cmd.go rename to pkg/cli/image_sub_cmd.go index 9ba4a786..71656de9 100644 --- a/pkg/cli/images_sub_cmd.go +++ b/pkg/cli/image_sub_cmd.go @@ -4,11 +4,6 @@ package cli import ( - "fmt" - "os" - "path" - - "github.com/briandowns/spinner" "github.com/spf13/cobra" zerr "zotregistry.io/zot/errors" @@ -41,7 +36,7 @@ func NewImageCVEListCommand(searchService SearchService) *cobra.Command { var searchedCVEID string cmd := &cobra.Command{ - Use: "cve [repo-name:tag][repo-name@digest]", + Use: "cve [repo]|[repo-name:tag]|[repo-name@digest]", Short: "List all CVE's of the image", Long: "List all CVE's of the image", Args: OneImageWithRefArg, @@ -68,7 +63,7 @@ func NewImageCVEListCommand(searchService SearchService) *cobra.Command { func NewImageDerivedCommand(searchService SearchService) *cobra.Command { cmd := &cobra.Command{ - Use: "derived [repo-name:tag][repo-name@digest]", + 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, @@ -91,7 +86,7 @@ func NewImageDerivedCommand(searchService SearchService) *cobra.Command { func NewImageBaseCommand(searchService SearchService) *cobra.Command { cmd := &cobra.Command{ - Use: "base [repo-name:tag][repo-name@digest]", + 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, @@ -117,7 +112,9 @@ func NewImageDigestCommand(searchService SearchService) *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, + Example: `zli image digest 8a1930f0 +zli image digest sha256:8a1930f0...`, + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) if err != nil { @@ -169,116 +166,3 @@ func NewImageNameCommand(searchService SearchService) *cobra.Command { 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/images_cmd.go b/pkg/cli/images_cmd.go deleted file mode 100644 index 218b6c20..00000000 --- a/pkg/cli/images_cmd.go +++ /dev/null @@ -1,33 +0,0 @@ -//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/repo_cmd.go b/pkg/cli/repo_cmd.go new file mode 100644 index 00000000..7567d8f4 --- /dev/null +++ b/pkg/cli/repo_cmd.go @@ -0,0 +1,34 @@ +//go:build search +// +build search + +package cli + +import ( + "github.com/spf13/cobra" + + "zotregistry.io/zot/pkg/cli/cmdflags" +) + +const prefix = "Searching... " + +func NewRepoCommand(searchService SearchService) *cobra.Command { + repoCmd := &cobra.Command{ + Use: "repo [config-name]", + Short: "List all repositories", + Long: `List all repositories`, + } + + repoCmd.SetUsageTemplate(repoCmd.UsageTemplate() + usageFooter) + + repoCmd.PersistentFlags().String(cmdflags.URLFlag, "", + "Specify zot server URL if config-name is not mentioned") + repoCmd.PersistentFlags().String(cmdflags.ConfigFlag, "", + "Specify the registry configuration to use for connection") + repoCmd.PersistentFlags().StringP(cmdflags.UserFlag, "u", "", + `User Credentials of zot server in "username:password" format`) + repoCmd.PersistentFlags().Bool(cmdflags.DebugFlag, false, "Show debug output") + + repoCmd.AddCommand(NewListReposCommand(searchService)) + + return repoCmd +} diff --git a/pkg/cli/repos_sub_cmd.go b/pkg/cli/repo_sub_cmd.go similarity index 92% rename from pkg/cli/repos_sub_cmd.go rename to pkg/cli/repo_sub_cmd.go index daef084c..f0b9ef9b 100644 --- a/pkg/cli/repos_sub_cmd.go +++ b/pkg/cli/repo_sub_cmd.go @@ -3,7 +3,9 @@ package cli -import "github.com/spf13/cobra" +import ( + "github.com/spf13/cobra" +) func NewListReposCommand(searchService SearchService) *cobra.Command { cmd := &cobra.Command{ diff --git a/pkg/cli/repos_test.go b/pkg/cli/repo_test.go similarity index 89% rename from pkg/cli/repos_test.go rename to pkg/cli/repo_test.go index 6af55084..6d2966bb 100644 --- a/pkg/cli/repos_test.go +++ b/pkg/cli/repo_test.go @@ -15,7 +15,6 @@ import ( "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" - "zotregistry.io/zot/pkg/cli/cmdflags" "zotregistry.io/zot/pkg/test" ) @@ -37,9 +36,8 @@ func TestReposCommand(t *testing.T) { baseURL)) defer os.Remove(configPath) - args := []string{"list"} + args := []string{"list", "--config", "repostest"} cmd := NewRepoCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "repostest", "") buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) diff --git a/pkg/cli/repos_cmd.go b/pkg/cli/repos_cmd.go deleted file mode 100644 index 96ca7adc..00000000 --- a/pkg/cli/repos_cmd.go +++ /dev/null @@ -1,116 +0,0 @@ -//go:build search -// +build search - -package cli - -import ( - "os" - "path" - - "github.com/briandowns/spinner" - "github.com/spf13/cobra" - - zerr "zotregistry.io/zot/errors" - "zotregistry.io/zot/pkg/cli/cmdflags" -) - -const prefix = "Searching... " - -func NewRepoCommand(searchService SearchService) *cobra.Command { - var servURL, user, outputFormat string - - var isSpinner, verifyTLS, verbose, debug bool - - repoCmd := &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") - if servURL == "" { - if len(args) > 0 { - urlFromConfig, err := getConfigValue(configPath, args[0], "url") - if err != nil { - cmd.SilenceUsage = true - - return err - } - - if urlFromConfig == "" { - return zerr.ErrNoURLProvided - } - - servURL = urlFromConfig - } else { - return zerr.ErrNoURLProvided - } - } - - if len(args) > 0 { - var err error - isSpinner, err = parseBooleanConfig(configPath, args[0], showspinnerConfig) - if err != nil { - cmd.SilenceUsage = true - - return err - } - - verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig) - if err != nil { - cmd.SilenceUsage = true - - return err - } - } - - spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr())) - spin.Prefix = prefix - - searchConfig := searchConfig{ - searchService: searchService, - servURL: &servURL, - user: &user, - outputFormat: &outputFormat, - verbose: &verbose, - debug: &debug, - spinner: spinnerState{spin, isSpinner}, - verifyTLS: &verifyTLS, - resultWriter: cmd.OutOrStdout(), - } - - err = listRepos(searchConfig) - - if err != nil { - cmd.SilenceUsage = true - - return err - } - - return nil - }, - } - - repoCmd.SetUsageTemplate(repoCmd.UsageTemplate() + usageFooter) - - 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 -} - -func listRepos(searchConfig searchConfig) error { - searcher := new(repoSearcher) - err := searcher.searchRepos(searchConfig) - - return err -} diff --git a/pkg/cli/root.go b/pkg/cli/root.go index b17ce6b3..762b3fdf 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -210,16 +210,12 @@ func NewCliRootCmd() *cobra.Command { }, } + rootCmd.SilenceUsage = true + // additional cmds enableCli(rootCmd) // "version" 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 } diff --git a/pkg/cli/search_cmd.go b/pkg/cli/search_cmd.go index d1571dd0..66a91c7f 100644 --- a/pkg/cli/search_cmd.go +++ b/pkg/cli/search_cmd.go @@ -4,158 +4,32 @@ package cli import ( - "os" - "path" - - "github.com/briandowns/spinner" "github.com/spf13/cobra" - zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/cli/cmdflags" ) -//nolint:dupl func NewSearchCommand(searchService SearchService) *cobra.Command { - searchImageParams := make(map[string]*string) - - var servURL, user, outputFormat string - - var isSpinner, verifyTLS, verbose, debug bool - searchCmd := &cobra.Command{ Use: "search [config-name]", Short: "Search images and their tags", - Long: `Search repos or images -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. - - # 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: 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") - if servURL == "" { - if len(args) > 0 { - urlFromConfig, err := getConfigValue(configPath, args[0], "url") - if err != nil { - cmd.SilenceUsage = true - - return err - } - - if urlFromConfig == "" { - return zerr.ErrNoURLProvided - } - - servURL = urlFromConfig - } else { - return zerr.ErrNoURLProvided - } - } - - if len(args) > 0 { - var err error - isSpinner, err = parseBooleanConfig(configPath, args[0], showspinnerConfig) - if err != nil { - cmd.SilenceUsage = true - - return err - } - - verifyTLS, err = parseBooleanConfig(configPath, args[0], verifyTLSConfig) - if err != nil { - cmd.SilenceUsage = true - - return err - } - } - - spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr())) - spin.Prefix = prefix - - searchConfig := searchConfig{ - params: searchImageParams, - searchService: searchService, - servURL: &servURL, - user: &user, - outputFormat: &outputFormat, - verbose: &verbose, - debug: &debug, - spinner: spinnerState{spin, isSpinner}, - verifyTLS: &verifyTLS, - resultWriter: cmd.OutOrStdout(), - } - - err = globalSearch(searchConfig) - - if err != nil { - cmd.SilenceUsage = true - - return err - } - - return nil - }, + Long: `Search repos or images`, } - setupSearchFlags(searchCmd, searchImageParams, &servURL, &user, &outputFormat, &verbose, &debug) searchCmd.SetUsageTemplate(searchCmd.UsageTemplate() + usageFooter) + searchCmd.PersistentFlags().String(cmdflags.URLFlag, "", + "Specify zot server URL if config-name is not mentioned") + searchCmd.PersistentFlags().String(cmdflags.ConfigFlag, "", + "Specify the registry configuration to use for connection") + searchCmd.PersistentFlags().StringP(cmdflags.UserFlag, "u", "", + `User Credentials of zot server in "username:password" format`) + searchCmd.PersistentFlags().StringP(cmdflags.OutputFormatFlag, "f", "", "Specify output format [text/json/yaml]") + searchCmd.PersistentFlags().Bool(cmdflags.VerboseFlag, false, "Show verbose output") + searchCmd.PersistentFlags().Bool(cmdflags.DebugFlag, false, "Show debug output") + searchCmd.AddCommand(NewSearchQueryCommand(searchService)) searchCmd.AddCommand(NewSearchSubjectCommand(searchService)) return searchCmd } - -func setupSearchFlags(searchCmd *cobra.Command, searchImageParams map[string]*string, - servURL, user, outputFormat *string, verbose *bool, debug *bool, -) { - searchImageParams["query"] = searchCmd.Flags().StringP("query", "q", "", - "Specify what repo or image(repo:tag) to be searched") - - 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)") - - 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 { - var searchers []searcher - - if checkExtEndPoint(searchConfig) { - searchers = getGlobalSearchersGQL() - } else { - searchers = getGlobalSearchersREST() - } - - for _, searcher := range searchers { - found, err := searcher.search(searchConfig) - if found { - if err != nil { - return err - } - - return nil - } - } - - return zerr.ErrInvalidFlagsCombination -} diff --git a/pkg/cli/search_cmd_referrers_test.go b/pkg/cli/search_cmd_referrers_test.go deleted file mode 100644 index 5b6af38c..00000000 --- a/pkg/cli/search_cmd_referrers_test.go +++ /dev/null @@ -1,596 +0,0 @@ -//go:build search -// +build search - -package cli //nolint:testpackage - -import ( - "bytes" - "fmt" - "net/http" - "os" - "regexp" - "strings" - "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" -) - -const ( - customArtTypeV1 = "application/custom.art.type.v1" - customArtTypeV2 = "application/custom.art.type.v2" - repoName = "repo" -) - -func TestReferrersSearchers(t *testing.T) { - refSearcherGQL := referrerSearcherGQL{} - refSearcher := referrerSearcher{} - - Convey("GQL Searcher", t, func() { - Convey("Bad parameters", func() { - ok, err := refSearcherGQL.search(searchConfig{params: map[string]*string{ - "badParam": ref("badParam"), - }}) - - So(err, ShouldBeNil) - So(ok, ShouldBeFalse) - }) - - Convey("GetRepoReference fails", func() { - conf := searchConfig{ - params: map[string]*string{ - "subject": ref("bad-subject"), - }, - user: ref("test:pass"), - } - - ok, err := refSearcherGQL.search(conf) - - So(err, ShouldNotBeNil) - So(ok, ShouldBeTrue) - }) - - Convey("fetchImageDigest for tags fails", func() { - conf := searchConfig{ - params: map[string]*string{ - "subject": ref("repo:tag"), - }, - user: ref("test:pass"), - servURL: ref("127.0.0.1:8080"), - } - - ok, err := refSearcherGQL.search(conf) - - So(err, ShouldNotBeNil) - So(ok, ShouldBeTrue) - }) - - Convey("search service fails", func() { - port := test.GetFreePort() - - conf := searchConfig{ - params: map[string]*string{ - "subject": ref("repo:tag"), - }, - searchService: NewSearchService(), - user: ref("test:pass"), - servURL: ref("http://127.0.0.1:" + port), - verifyTLS: ref(false), - debug: ref(false), - verbose: ref(false), - } - - server := test.StartTestHTTPServer(test.HTTPRoutes{ - test.RouteHandler{ - Route: "/v2/{repo}/manifests/{ref}", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }, - AllowedMethods: []string{"HEAD"}, - }, - }, port) - - defer server.Close() - - ok, err := refSearcherGQL.search(conf) - - So(err, ShouldNotBeNil) - So(ok, ShouldBeTrue) - }) - }) - - Convey("REST searcher", t, func() { - Convey("Bad parameters", func() { - ok, err := refSearcher.search(searchConfig{params: map[string]*string{ - "badParam": ref("badParam"), - }}) - - So(err, ShouldBeNil) - So(ok, ShouldBeFalse) - }) - - Convey("GetRepoReference fails", func() { - conf := searchConfig{ - params: map[string]*string{ - "subject": ref("bad-subject"), - }, - user: ref("test:pass"), - } - - ok, err := refSearcher.search(conf) - - So(err, ShouldNotBeNil) - So(ok, ShouldBeTrue) - }) - - Convey("fetchImageDigest for tags fails", func() { - conf := searchConfig{ - params: map[string]*string{ - "subject": ref("repo:tag"), - }, - user: ref("test:pass"), - servURL: ref("127.0.0.1:1000"), - } - - ok, err := refSearcher.search(conf) - - So(err, ShouldNotBeNil) - So(ok, ShouldBeTrue) - }) - - Convey("search service fails", func() { - port := test.GetFreePort() - - conf := searchConfig{ - params: map[string]*string{ - "subject": ref("repo:tag"), - }, - searchService: NewSearchService(), - user: ref("test:pass"), - servURL: ref("http://127.0.0.1:" + port), - verifyTLS: ref(false), - debug: ref(false), - verbose: ref(false), - fixedFlag: ref(false), - } - - server := test.StartTestHTTPServer(test.HTTPRoutes{ - test.RouteHandler{ - Route: "/v2/{repo}/manifests/{ref}", - HandlerFunc: func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }, - AllowedMethods: []string{"HEAD"}, - }, - }, port) - - defer server.Close() - - ok, err := refSearcher.search(conf) - - So(err, ShouldNotBeNil) - So(ok, ShouldBeTrue) - }) - }) -} - -func TestReferrerCLI(t *testing.T) { - Convey("Test GQL", t, func() { - rootDir := t.TempDir() - - port := test.GetFreePort() - baseURL := test.GetBaseURL(port) - conf := config.New() - conf.HTTP.Port = port - conf.Storage.GC = false - defaultVal := true - conf.Extensions = &extconf.ExtensionConfig{ - Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, - } - ctlr := api.NewController(conf) - ctlr.Config.Storage.RootDirectory = rootDir - cm := test.NewControllerManager(ctlr) - cm.StartAndWait(conf.HTTP.Port) - defer cm.StopServer() - - repo := repoName - image := test.CreateRandomImage() - - err := test.UploadImage(image, baseURL, repo, "tag") - So(err, ShouldBeNil) - - ref1 := test.CreateImageWith(). - RandomLayers(1, 10). - RandomConfig(). - Subject(image.DescriptorRef()).Build() - - ref2 := test.CreateImageWith(). - RandomLayers(1, 10). - ArtifactConfig(customArtTypeV1). - Subject(image.DescriptorRef()).Build() - - ref3 := test.CreateImageWith(). - RandomLayers(1, 10). - RandomConfig(). - ArtifactType(customArtTypeV2). - Subject(image.DescriptorRef()).Build() - - err = test.UploadImage(ref1, baseURL, repo, ref1.DigestStr()) - So(err, ShouldBeNil) - - err = test.UploadImage(ref2, baseURL, repo, ref2.DigestStr()) - So(err, ShouldBeNil) - - err = test.UploadImage(ref3, baseURL, repo, ref3.DigestStr()) - So(err, ShouldBeNil) - - args := []string{"reftest", "--subject", repo + "@" + image.DigestStr()} - - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, - baseURL)) - defer os.Remove(configPath) - - cmd := NewSearchCommand(new(searchService)) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() - So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) - str := strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) - So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST") - So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 563 B "+ref1.DigestStr()) - So(str, ShouldContainSubstring, "custom.art.type.v1 551 B "+ref2.DigestStr()) - So(str, ShouldContainSubstring, "custom.art.type.v2 611 B "+ref3.DigestStr()) - - fmt.Println(buff.String()) - - os.Remove(configPath) - - args = []string{"reftest", "--subject", repo + ":" + "tag"} - - configPath = makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, - baseURL)) - defer os.Remove(configPath) - - cmd = NewSearchCommand(new(searchService)) - - buff = &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() - So(err, ShouldBeNil) - str = strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) - So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST") - So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 563 B "+ref1.DigestStr()) - So(str, ShouldContainSubstring, "custom.art.type.v1 551 B "+ref2.DigestStr()) - So(str, ShouldContainSubstring, "custom.art.type.v2 611 B "+ref3.DigestStr()) - - fmt.Println(buff.String()) - }) - - Convey("Test REST", t, func() { - rootDir := t.TempDir() - - port := test.GetFreePort() - baseURL := test.GetBaseURL(port) - conf := config.New() - conf.HTTP.Port = port - conf.Storage.GC = false - defaultVal := false - conf.Extensions = &extconf.ExtensionConfig{ - Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, - } - ctlr := api.NewController(conf) - ctlr.Config.Storage.RootDirectory = rootDir - cm := test.NewControllerManager(ctlr) - cm.StartAndWait(conf.HTTP.Port) - defer cm.StopServer() - - repo := repoName - image := test.CreateRandomImage() - - err := test.UploadImage(image, baseURL, repo, "tag") - So(err, ShouldBeNil) - - ref1 := test.CreateImageWith(). - RandomLayers(1, 10). - RandomConfig(). - Subject(image.DescriptorRef()).Build() - - ref2 := test.CreateImageWith(). - RandomLayers(1, 10). - ArtifactConfig(customArtTypeV1). - Subject(image.DescriptorRef()).Build() - - ref3 := test.CreateImageWith(). - RandomLayers(1, 10). - RandomConfig(). - ArtifactType(customArtTypeV2). - Subject(image.DescriptorRef()).Build() - - err = test.UploadImage(ref1, baseURL, repo, ref1.DigestStr()) - So(err, ShouldBeNil) - - err = test.UploadImage(ref2, baseURL, repo, ref2.DigestStr()) - So(err, ShouldBeNil) - - err = test.UploadImage(ref3, baseURL, repo, ref3.DigestStr()) - So(err, ShouldBeNil) - - // get referrers by digest - args := []string{"reftest", "--subject", repo + "@" + image.DigestStr()} - - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, - baseURL)) - - cmd := NewSearchCommand(new(searchService)) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() - So(err, ShouldBeNil) - space := regexp.MustCompile(`\s+`) - str := strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) - So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST") - So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 563 B "+ref1.DigestStr()) - So(str, ShouldContainSubstring, "custom.art.type.v1 551 B "+ref2.DigestStr()) - So(str, ShouldContainSubstring, "custom.art.type.v2 611 B "+ref3.DigestStr()) - fmt.Println(buff.String()) - - os.Remove(configPath) - - args = []string{"reftest", "--subject", repo + ":" + "tag"} - - configPath = makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, - baseURL)) - defer os.Remove(configPath) - - buff = &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() - So(err, ShouldBeNil) - str = strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) - So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST") - So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 563 B "+ref1.DigestStr()) - So(str, ShouldContainSubstring, "custom.art.type.v1 551 B "+ref2.DigestStr()) - So(str, ShouldContainSubstring, "custom.art.type.v2 611 B "+ref3.DigestStr()) - fmt.Println(buff.String()) - }) -} - -func TestFormatsReferrersCLI(t *testing.T) { - Convey("Create server", t, func() { - rootDir := t.TempDir() - - port := test.GetFreePort() - baseURL := test.GetBaseURL(port) - conf := config.New() - conf.HTTP.Port = port - conf.Storage.GC = false - defaultVal := false - conf.Extensions = &extconf.ExtensionConfig{ - Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, - } - ctlr := api.NewController(conf) - ctlr.Config.Storage.RootDirectory = rootDir - cm := test.NewControllerManager(ctlr) - cm.StartAndWait(conf.HTTP.Port) - defer cm.StopServer() - - repo := repoName - image := test.CreateRandomImage() - - err := test.UploadImage(image, baseURL, repo, "tag") - So(err, ShouldBeNil) - - // add referrers - ref1 := test.CreateImageWith(). - RandomLayers(1, 10). - RandomConfig(). - Subject(image.DescriptorRef()).Build() - - ref2 := test.CreateImageWith(). - RandomLayers(1, 10). - ArtifactConfig(customArtTypeV1). - Subject(image.DescriptorRef()).Build() - - ref3 := test.CreateImageWith(). - RandomLayers(1, 10). - RandomConfig(). - ArtifactType(customArtTypeV2). - Subject(image.DescriptorRef()).Build() - - err = test.UploadImage(ref1, baseURL, repo, ref1.DigestStr()) - So(err, ShouldBeNil) - - err = test.UploadImage(ref2, baseURL, repo, ref2.DigestStr()) - So(err, ShouldBeNil) - - err = test.UploadImage(ref3, baseURL, repo, ref3.DigestStr()) - So(err, ShouldBeNil) - - Convey("JSON format", func() { - args := []string{"reftest", "--format", "json", "--subject", repo + "@" + image.DigestStr()} - - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, - baseURL)) - - defer os.Remove(configPath) - - cmd := NewSearchCommand(new(searchService)) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() - So(err, ShouldBeNil) - fmt.Println(buff.String()) - }) - Convey("YAML format", func() { - args := []string{"reftest", "--format", "yaml", "--subject", repo + "@" + image.DigestStr()} - - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, - baseURL)) - - defer os.Remove(configPath) - - cmd := NewSearchCommand(new(searchService)) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() - So(err, ShouldBeNil) - fmt.Println(buff.String()) - }) - Convey("Invalid format", func() { - args := []string{"reftest", "--format", "invalid_format", "--subject", repo + "@" + image.DigestStr()} - - configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, - baseURL)) - - defer os.Remove(configPath) - - cmd := NewSearchCommand(new(searchService)) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err = cmd.Execute() - So(err, ShouldNotBeNil) - }) - }) -} - -func TestReferrersCLIErrors(t *testing.T) { - Convey("Errors", t, func() { - cmd := NewSearchCommand(new(searchService)) - - Convey("no url provided", func() { - args := []string{"reftest", "--format", "invalid", "--query", "repo/alpine"} - - configPath := makeConfigFile(`{"configs":[{"_name":"reftest","showspinner":false}]}`) - - defer os.Remove(configPath) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() - So(err, ShouldNotBeNil) - }) - - Convey("getConfigValue", func() { - args := []string{"reftest", "--subject", "repo/alpine"} - - configPath := makeConfigFile(`bad-json`) - - defer os.Remove(configPath) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() - So(err, ShouldNotBeNil) - }) - - Convey("bad showspinnerConfig ", func() { - args := []string{"reftest"} - - configPath := makeConfigFile(`{"configs":[{"_name":"reftest", "url":"http://127.0.0.1:8080", "showspinner":"bad"}]}`) - - defer os.Remove(configPath) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() - So(err, ShouldNotBeNil) - }) - - Convey("bad verifyTLSConfig ", func() { - args := []string{"reftest"} - - configPath := makeConfigFile( - `{"configs":[{"_name":"reftest", "url":"http://127.0.0.1:8080", "showspinner":false, "verify-tls": "bad"}]}`) - - defer os.Remove(configPath) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() - So(err, ShouldNotBeNil) - }) - - Convey("url from config is empty", func() { - args := []string{"reftest", "--subject", "repo/alpine"} - - configPath := makeConfigFile(`{"configs":[{"_name":"reftest", "url":"", "showspinner":false}]}`) - - defer os.Remove(configPath) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() - So(err, ShouldNotBeNil) - }) - - Convey("bad params combination", func() { - args := []string{"reftest"} - - configPath := makeConfigFile(`{"configs":[{"_name":"reftest", "url":"http://127.0.0.1:8080", "showspinner":false}]}`) - - defer os.Remove(configPath) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() - So(err, ShouldNotBeNil) - }) - - Convey("no url provided error", func() { - args := []string{} - - configPath := makeConfigFile(`bad-json`) - - defer os.Remove(configPath) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() - So(err, ShouldNotBeNil) - }) - }) -} - -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 9a4efbf6..7f0e122b 100644 --- a/pkg/cli/search_cmd_test.go +++ b/pkg/cli/search_cmd_test.go @@ -6,7 +6,6 @@ package cli //nolint:testpackage import ( "bytes" "fmt" - "io" "os" "regexp" "strings" @@ -17,58 +16,408 @@ 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" ) -func TestGlobalSearchers(t *testing.T) { - globalSearcher := globalSearcherGQL{} +const ( + customArtTypeV1 = "application/custom.art.type.v1" + customArtTypeV2 = "application/custom.art.type.v2" + repoName = "repo" +) - Convey("GQL Searcher", t, func() { - Convey("Bad parameters", func() { - ok, err := globalSearcher.search(searchConfig{params: map[string]*string{ - "badParam": ref("badParam"), - }}) +func TestReferrerCLI(t *testing.T) { + Convey("Test GQL", t, func() { + rootDir := t.TempDir() + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.GC = false + defaultVal := true + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, + } + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = rootDir + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + repo := repoName + image := test.CreateRandomImage() + + err := test.UploadImage(image, baseURL, repo, "tag") + So(err, ShouldBeNil) + + ref1 := test.CreateImageWith(). + RandomLayers(1, 10). + RandomConfig(). + Subject(image.DescriptorRef()).Build() + + ref2 := test.CreateImageWith(). + RandomLayers(1, 10). + ArtifactConfig(customArtTypeV1). + Subject(image.DescriptorRef()).Build() + + ref3 := test.CreateImageWith(). + RandomLayers(1, 10). + RandomConfig(). + ArtifactType(customArtTypeV2). + Subject(image.DescriptorRef()).Build() + + err = test.UploadImage(ref1, baseURL, repo, ref1.DigestStr()) + So(err, ShouldBeNil) + + err = test.UploadImage(ref2, baseURL, repo, ref2.DigestStr()) + So(err, ShouldBeNil) + + err = test.UploadImage(ref3, baseURL, repo, ref3.DigestStr()) + So(err, ShouldBeNil) + + args := []string{"subject", repo + "@" + image.DigestStr(), "--config", "reftest"} + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + cmd := NewSearchCommand(new(searchService)) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) + So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST") + So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 563 B "+ref1.DigestStr()) + So(str, ShouldContainSubstring, "custom.art.type.v1 551 B "+ref2.DigestStr()) + So(str, ShouldContainSubstring, "custom.art.type.v2 611 B "+ref3.DigestStr()) + + fmt.Println(buff.String()) + + os.Remove(configPath) + + args = []string{"subject", repo + ":" + "tag", "--config", "reftest"} + + configPath = makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + cmd = NewSearchCommand(new(searchService)) + + buff = &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + str = strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) + So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST") + So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 563 B "+ref1.DigestStr()) + So(str, ShouldContainSubstring, "custom.art.type.v1 551 B "+ref2.DigestStr()) + So(str, ShouldContainSubstring, "custom.art.type.v2 611 B "+ref3.DigestStr()) + + fmt.Println(buff.String()) + }) + + Convey("Test REST", t, func() { + rootDir := t.TempDir() + + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.GC = false + defaultVal := false + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, + } + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = rootDir + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + repo := repoName + image := test.CreateRandomImage() + + err := test.UploadImage(image, baseURL, repo, "tag") + So(err, ShouldBeNil) + + ref1 := test.CreateImageWith(). + RandomLayers(1, 10). + RandomConfig(). + Subject(image.DescriptorRef()).Build() + + ref2 := test.CreateImageWith(). + RandomLayers(1, 10). + ArtifactConfig(customArtTypeV1). + Subject(image.DescriptorRef()).Build() + + ref3 := test.CreateImageWith(). + RandomLayers(1, 10). + RandomConfig(). + ArtifactType(customArtTypeV2). + Subject(image.DescriptorRef()).Build() + + err = test.UploadImage(ref1, baseURL, repo, ref1.DigestStr()) + So(err, ShouldBeNil) + + err = test.UploadImage(ref2, baseURL, repo, ref2.DigestStr()) + So(err, ShouldBeNil) + + err = test.UploadImage(ref3, baseURL, repo, ref3.DigestStr()) + So(err, ShouldBeNil) + + // get referrers by digest + args := []string{"subject", repo + "@" + image.DigestStr(), "--config", "reftest"} + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, + baseURL)) + + cmd := NewSearchCommand(new(searchService)) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) + So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST") + So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 563 B "+ref1.DigestStr()) + So(str, ShouldContainSubstring, "custom.art.type.v1 551 B "+ref2.DigestStr()) + So(str, ShouldContainSubstring, "custom.art.type.v2 611 B "+ref3.DigestStr()) + fmt.Println(buff.String()) + + os.Remove(configPath) + + args = []string{"subject", repo + ":" + "tag", "--config", "reftest"} + + configPath = makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, + baseURL)) + defer os.Remove(configPath) + + buff = &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + str = strings.TrimSpace(space.ReplaceAllString(buff.String(), " ")) + So(str, ShouldContainSubstring, "ARTIFACT TYPE SIZE DIGEST") + So(str, ShouldContainSubstring, "application/vnd.oci.image.config.v1+json 563 B "+ref1.DigestStr()) + So(str, ShouldContainSubstring, "custom.art.type.v1 551 B "+ref2.DigestStr()) + So(str, ShouldContainSubstring, "custom.art.type.v2 611 B "+ref3.DigestStr()) + fmt.Println(buff.String()) + }) +} + +func TestFormatsReferrersCLI(t *testing.T) { + Convey("Create server", t, func() { + rootDir := t.TempDir() + + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.GC = false + defaultVal := false + conf.Extensions = &extconf.ExtensionConfig{ + Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}}, + } + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = rootDir + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(conf.HTTP.Port) + defer cm.StopServer() + + repo := repoName + image := test.CreateRandomImage() + + err := test.UploadImage(image, baseURL, repo, "tag") + So(err, ShouldBeNil) + + // add referrers + ref1 := test.CreateImageWith(). + RandomLayers(1, 10). + RandomConfig(). + Subject(image.DescriptorRef()).Build() + + ref2 := test.CreateImageWith(). + RandomLayers(1, 10). + ArtifactConfig(customArtTypeV1). + Subject(image.DescriptorRef()).Build() + + ref3 := test.CreateImageWith(). + RandomLayers(1, 10). + RandomConfig(). + ArtifactType(customArtTypeV2). + Subject(image.DescriptorRef()).Build() + + err = test.UploadImage(ref1, baseURL, repo, ref1.DigestStr()) + So(err, ShouldBeNil) + + err = test.UploadImage(ref2, baseURL, repo, ref2.DigestStr()) + So(err, ShouldBeNil) + + err = test.UploadImage(ref3, baseURL, repo, ref3.DigestStr()) + So(err, ShouldBeNil) + + Convey("JSON format", func() { + args := []string{"subject", repo + "@" + image.DigestStr(), "--format", "json", "--config", "reftest"} + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, + baseURL)) + + defer os.Remove(configPath) + + cmd := NewSearchCommand(new(searchService)) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() So(err, ShouldBeNil) - So(ok, ShouldBeFalse) + fmt.Println(buff.String()) + }) + Convey("YAML format", func() { + args := []string{"subject", repo + "@" + image.DigestStr(), "--format", "yaml", "--config", "reftest"} + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, + baseURL)) + + defer os.Remove(configPath) + + cmd := NewSearchCommand(new(searchService)) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + fmt.Println(buff.String()) + }) + Convey("Invalid format", func() { + args := []string{"subject", repo + "@" + image.DigestStr(), "--format", "invalid_format", "--config", "reftest"} + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"reftest","url":"%s","showspinner":false}]}`, + baseURL)) + + defer os.Remove(configPath) + + cmd := NewSearchCommand(new(searchService)) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + }) + }) +} + +func TestReferrersCLIErrors(t *testing.T) { + Convey("Errors", t, func() { + cmd := NewSearchCommand(new(searchService)) + + Convey("no url provided", func() { + args := []string{"query", "repo/alpine", "--format", "invalid", "--config", "reftest"} + + configPath := makeConfigFile(`{"configs":[{"_name":"reftest","showspinner":false}]}`) + + defer os.Remove(configPath) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) }) - Convey("global searcher service fail", func() { - conf := searchConfig{ - params: map[string]*string{ - "query": ref("repo"), - }, - searchService: NewSearchService(), - user: ref("test:pass"), - servURL: ref("127.0.0.1:8080"), - verifyTLS: ref(false), - debug: ref(false), - verbose: ref(false), - fixedFlag: ref(false), - } - ok, err := globalSearcher.search(conf) + Convey("getConfigValue", func() { + args := []string{"subject", "repo/alpine", "--config", "reftest"} + configPath := makeConfigFile(`bad-json`) + + defer os.Remove(configPath) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() So(err, ShouldNotBeNil) - So(ok, ShouldBeTrue) }) - Convey("print images fail", func() { - conf := searchConfig{ - params: map[string]*string{ - "query": ref("repo"), - }, - user: ref("user:pass"), - outputFormat: ref("bad-format"), - searchService: mockService{}, - resultWriter: io.Discard, - verbose: ref(false), - } - ok, err := globalSearcher.search(conf) + Convey("bad showspinnerConfig ", func() { + args := []string{"query", "repo", "--config", "reftest"} + configPath := makeConfigFile(`{"configs":[{"_name":"reftest", "url":"http://127.0.0.1:8080", "showspinner":"bad"}]}`) + + defer os.Remove(configPath) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("bad verifyTLSConfig ", func() { + args := []string{"query", "repo", "reftest"} + + configPath := makeConfigFile( + `{"configs":[{"_name":"reftest", "url":"http://127.0.0.1:8080", "showspinner":false, "verify-tls": "bad"}]}`) + + defer os.Remove(configPath) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("url from config is empty", func() { + args := []string{"subject", "repo/alpine", "--config", "reftest"} + + configPath := makeConfigFile(`{"configs":[{"_name":"reftest", "url":"", "showspinner":false}]}`) + + defer os.Remove(configPath) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("bad params combination", func() { + args := []string{"query", "repo", "reftest"} + + configPath := makeConfigFile(`{"configs":[{"_name":"reftest", "url":"http://127.0.0.1:8080", "showspinner":false}]}`) + + defer os.Remove(configPath) + + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() So(err, ShouldNotBeNil) - So(ok, ShouldBeTrue) }) }) } @@ -138,7 +487,7 @@ func TestSearchCLI(t *testing.T) { // search by repos - args := []string{"searchtest", "--query", "test/alpin", "--verbose"} + args := []string{"query", "test/alpin", "--verbose", "--config", "searchtest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, baseURL)) @@ -165,7 +514,7 @@ func TestSearchCLI(t *testing.T) { cmd = NewSearchCommand(new(searchService)) - args = []string{"searchtest", "--query", "repo/alpine:"} + args = []string{"query", "repo/alpine:", "--config", "searchtest"} configPath = makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, baseURL)) @@ -241,7 +590,7 @@ func TestFormatsSearchCLI(t *testing.T) { cmd := NewSearchCommand(new(searchService)) Convey("JSON format", func() { - args := []string{"searchtest", "--format", "json", "--query", "repo/alpine"} + args := []string{"query", "repo/alpine", "--format", "json", "--config", "searchtest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, baseURL)) @@ -258,7 +607,7 @@ func TestFormatsSearchCLI(t *testing.T) { }) Convey("YAML format", func() { - args := []string{"searchtest", "--format", "yaml", "--query", "repo/alpine"} + args := []string{"query", "repo/alpine", "--format", "yaml", "--config", "searchtest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, baseURL)) @@ -275,7 +624,7 @@ func TestFormatsSearchCLI(t *testing.T) { }) Convey("Invalid format", func() { - args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"} + args := []string{"query", "repo/alpine", "--format", "invalid", "--config", "searchtest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"searchtest","url":"%s","showspinner":false}]}`, baseURL)) @@ -297,7 +646,7 @@ func TestSearchCLIErrors(t *testing.T) { cmd := NewSearchCommand(new(searchService)) Convey("no url provided", func() { - args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"} + args := []string{"query", "repo/alpine", "--format", "invalid", "--config", "searchtest"} configPath := makeConfigFile(`{"configs":[{"_name":"searchtest","showspinner":false}]}`) @@ -312,7 +661,7 @@ func TestSearchCLIErrors(t *testing.T) { }) Convey("getConfigValue", func() { - args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"} + args := []string{"query", "repo/alpine", "--format", "invalid", "--config", "searchtest"} configPath := makeConfigFile(`bad-json`) @@ -327,7 +676,7 @@ func TestSearchCLIErrors(t *testing.T) { }) Convey("bad showspinnerConfig ", func() { - args := []string{"searchtest"} + args := []string{"query", "repo/alpine", "--config", "searchtest"} configPath := makeConfigFile( `{"configs":[{"_name":"searchtest", "url":"http://127.0.0.1:8080", "showspinner":"bad"}]}`) @@ -343,7 +692,7 @@ func TestSearchCLIErrors(t *testing.T) { }) Convey("bad verifyTLSConfig ", func() { - args := []string{"searchtest"} + args := []string{"query", "repo/alpine", "--config", "searchtest"} configPath := makeConfigFile( `{"configs":[{"_name":"searchtest", "url":"http://127.0.0.1:8080", "showspinner":false, "verify-tls": "bad"}]}`) @@ -359,7 +708,7 @@ func TestSearchCLIErrors(t *testing.T) { }) Convey("url from config is empty", func() { - args := []string{"searchtest", "--format", "invalid", "--query", "repo/alpine"} + args := []string{"query", "repo/alpine", "--format", "invalid", "--config", "searchtest"} configPath := makeConfigFile(`{"configs":[{"_name":"searchtest", "url":"", "showspinner":false}]}`) @@ -372,35 +721,6 @@ func TestSearchCLIErrors(t *testing.T) { err := cmd.Execute() So(err, ShouldNotBeNil) }) - - Convey("no url provided error", func() { - args := []string{} - - configPath := makeConfigFile(`bad-json`) - - defer os.Remove(configPath) - - buff := &bytes.Buffer{} - cmd.SetOut(buff) - cmd.SetErr(buff) - cmd.SetArgs(args) - err := cmd.Execute() - So(err, ShouldNotBeNil) - }) - - Convey("globalSearch without gql active", func() { - err := globalSearch(searchConfig{ - user: ref("t"), - servURL: ref("t"), - verifyTLS: ref(false), - debug: ref(false), - params: map[string]*string{ - "query": ref("t"), - }, - resultWriter: io.Discard, - }) - So(err, ShouldNotBeNil) - }) }) } @@ -430,9 +750,9 @@ func TestSearchCommandGQL(t *testing.T) { defer os.Remove(configPath) Convey("query", func() { - args := []string{"query", "repo/al"} + args := []string{"query", "repo/al", "--config", "searchtest"} cmd := NewSearchCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "") + buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -448,7 +768,7 @@ func TestSearchCommandGQL(t *testing.T) { Convey("query command errors", func() { // no url - args := []string{"repo/al"} + args := []string{"repo/al", "--config", "searchtest"} cmd := NewSearchQueryCommand(mockService{}) buff := bytes.NewBufferString("") cmd.SetOut(buff) @@ -462,9 +782,9 @@ func TestSearchCommandGQL(t *testing.T) { err := test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "tag") So(err, ShouldBeNil) - args := []string{"subject", "repo:tag"} + args := []string{"subject", "repo:tag", "--config", "searchtest"} cmd := NewSearchCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "") + buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -479,7 +799,7 @@ func TestSearchCommandGQL(t *testing.T) { Convey("subject command errors", func() { // no url - args := []string{"repo:tag"} + args := []string{"repo:tag", "--config", "searchtest"} cmd := NewSearchSubjectCommand(mockService{}) buff := bytes.NewBufferString("") cmd.SetOut(buff) @@ -510,9 +830,9 @@ func TestSearchCommandREST(t *testing.T) { defer os.Remove(configPath) Convey("query", func() { - args := []string{"query", "repo/al"} + args := []string{"query", "repo/al", "--config", "searchtest"} cmd := NewSearchCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "") + buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) @@ -525,9 +845,9 @@ func TestSearchCommandREST(t *testing.T) { err := test.UploadImage(test.CreateRandomImage(), baseURL, "repo", "tag") So(err, ShouldBeNil) - args := []string{"subject", "repo:tag"} + args := []string{"subject", "repo:tag", "--config", "searchtest"} cmd := NewSearchCommand(mockService{}) - cmd.PersistentFlags().String(cmdflags.ConfigFlag, "searchtest", "") + buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) diff --git a/pkg/cli/search_functions.go b/pkg/cli/search_functions.go index 9792635e..feb51ea9 100644 --- a/pkg/cli/search_functions.go +++ b/pkg/cli/search_functions.go @@ -15,8 +15,10 @@ import ( zcommon "zotregistry.io/zot/pkg/common" ) +const CveDBRetryInterval = 3 + func SearchAllImages(config searchConfig) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) imageErr := make(chan stringResult) ctx, cancel := context.WithCancel(context.Background()) @@ -40,7 +42,7 @@ func SearchAllImages(config searchConfig) error { } func SearchAllImagesGQL(config searchConfig) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -60,7 +62,7 @@ func SearchAllImagesGQL(config searchConfig) error { } func SearchImageByName(config searchConfig, image string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) imageErr := make(chan stringResult) ctx, cancel := context.WithCancel(context.Background()) @@ -79,6 +81,10 @@ func SearchImageByName(config searchConfig, image string) error { select { case err := <-errCh: + if strings.Contains(err.Error(), "NAME_UNKNOWN") { + return zerr.ErrEmptyRepoList + } + return err default: return nil @@ -86,7 +92,7 @@ func SearchImageByName(config searchConfig, image string) error { } func SearchImageByNameGQL(config searchConfig, imageName string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -110,7 +116,7 @@ func SearchImageByNameGQL(config searchConfig, imageName string) error { } func SearchImagesByDigest(config searchConfig, digest string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) imageErr := make(chan stringResult) ctx, cancel := context.WithCancel(context.Background()) @@ -136,7 +142,7 @@ func SearchImagesByDigest(config searchConfig, digest string) error { } func SearchDerivedImageListGQL(config searchConfig, derivedImage string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -157,7 +163,7 @@ func SearchDerivedImageListGQL(config searchConfig, derivedImage string) error { } func SearchBaseImageListGQL(config searchConfig, baseImage string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -178,7 +184,7 @@ func SearchBaseImageListGQL(config searchConfig, baseImage string) error { } func SearchImagesForDigestGQL(config searchConfig, digest string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -202,7 +208,7 @@ func SearchImagesForDigestGQL(config searchConfig, digest string) error { } func SearchCVEForImageGQL(config searchConfig, image, searchedCveID string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -225,7 +231,7 @@ func SearchCVEForImageGQL(config searchConfig, image, searchedCveID string) erro } return err - }, maxRetries, cveDBRetryInterval*time.Second) + }, maxRetries, CveDBRetryInterval*time.Second) if err != nil { return err } @@ -238,12 +244,12 @@ func SearchCVEForImageGQL(config searchConfig, image, searchedCveID string) erro var builder strings.Builder - if *config.outputFormat == defaultOutputFormat || *config.outputFormat == "" { - printCVETableHeader(&builder, *config.verbose, 0, 0, 0) + if config.outputFormat == defaultOutputFormat || config.outputFormat == "" { + printCVETableHeader(&builder) fmt.Fprint(config.resultWriter, builder.String()) } - out, err := cveList.string(*config.outputFormat) + out, err := cveList.string(config.outputFormat) if err != nil { return err } @@ -254,7 +260,7 @@ func SearchCVEForImageGQL(config searchConfig, image, searchedCveID string) erro } func SearchImagesByCVEIDGQL(config searchConfig, repo, cveid string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -278,7 +284,7 @@ func SearchImagesByCVEIDGQL(config searchConfig, repo, cveid string) error { } return err - }, maxRetries, cveDBRetryInterval*time.Second) + }, maxRetries, CveDBRetryInterval*time.Second) if err != nil { return err } @@ -293,7 +299,7 @@ func SearchImagesByCVEIDGQL(config searchConfig, repo, cveid string) error { } func SearchFixedTagsGQL(config searchConfig, repo, cveid string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -317,7 +323,7 @@ func SearchFixedTagsGQL(config searchConfig, repo, cveid string) error { } return err - }, maxRetries, cveDBRetryInterval*time.Second) + }, maxRetries, CveDBRetryInterval*time.Second) if err != nil { return err } @@ -332,7 +338,7 @@ func SearchFixedTagsGQL(config searchConfig, repo, cveid string) error { } func GlobalSearchGQL(config searchConfig, query string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -362,7 +368,7 @@ func GlobalSearchGQL(config searchConfig, query string) error { } func SearchReferrersGQL(config searchConfig, subject string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) repo, ref, refIsTag, err := zcommon.GetRepoReference(subject) if err != nil { @@ -399,7 +405,7 @@ func SearchReferrersGQL(config searchConfig, subject string) error { } func SearchReferrers(config searchConfig, subject string) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) repo, ref, refIsTag, err := zcommon.GetRepoReference(subject) if err != nil { @@ -435,7 +441,7 @@ func SearchReferrers(config searchConfig, subject string) error { } func SearchRepos(config searchConfig) error { - username, password := getUsernameAndPassword(*config.user) + username, password := getUsernameAndPassword(config.user) repoErr := make(chan stringResult) ctx, cancel := context.WithCancel(context.Background()) diff --git a/pkg/cli/search_functions_test.go b/pkg/cli/search_functions_test.go index 0a63c65d..286318ce 100644 --- a/pkg/cli/search_functions_test.go +++ b/pkg/cli/search_functions_test.go @@ -611,14 +611,14 @@ func TestSearchRepos(t *testing.T) { func getMockSearchConfig(buff *bytes.Buffer, mockService mockService) searchConfig { return searchConfig{ resultWriter: buff, - user: ref(""), + user: "", searchService: mockService, - servURL: ref("http://127.0.0.1:8000"), - outputFormat: ref(""), - verifyTLS: ref(false), - fixedFlag: ref(false), - verbose: ref(false), - debug: ref(false), + servURL: "http://127.0.0.1:8000", + outputFormat: "", + verifyTLS: false, + fixedFlag: false, + verbose: false, + debug: false, } } @@ -685,7 +685,8 @@ func TestUtils(t *testing.T) { Convey("GetConfigOptions", t, func() { // no flags cmd := &cobra.Command{} - isSpinner, verifyTLS := GetCliConfigOptions(cmd) + isSpinner, verifyTLS, err := GetCliConfigOptions(cmd) + So(err, ShouldNotBeNil) So(isSpinner, ShouldBeFalse) So(verifyTLS, ShouldBeFalse) @@ -693,7 +694,8 @@ func TestUtils(t *testing.T) { configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":"bad", "verify-tls": false}]}`) cmd = &cobra.Command{} cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "") - isSpinner, verifyTLS = GetCliConfigOptions(cmd) + isSpinner, verifyTLS, err = GetCliConfigOptions(cmd) + So(err, ShouldNotBeNil) So(isSpinner, ShouldBeFalse) So(verifyTLS, ShouldBeFalse) os.Remove(configPath) @@ -702,7 +704,8 @@ func TestUtils(t *testing.T) { configPath = makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false, "verify-tls": "bad"}]}`) cmd = &cobra.Command{} cmd.Flags().String(cmdflags.ConfigFlag, "imagetest", "") - isSpinner, verifyTLS = GetCliConfigOptions(cmd) + isSpinner, verifyTLS, err = GetCliConfigOptions(cmd) + So(err, ShouldNotBeNil) So(isSpinner, ShouldBeFalse) So(verifyTLS, ShouldBeFalse) os.Remove(configPath) @@ -743,17 +746,17 @@ func TestUtils(t *testing.T) { Convey("CheckExtEndPointQuery", t, func() { // invalid url err := CheckExtEndPointQuery(searchConfig{ - user: ref(""), - servURL: ref("bad-url"), + user: "", + servURL: "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), + user: "", + servURL: "http://127.0.0.1:5000", + verifyTLS: false, + debug: false, resultWriter: io.Discard, }) So(err, ShouldNotBeNil) diff --git a/pkg/cli/search_sub_cmd.go b/pkg/cli/search_sub_cmd.go index bd370a73..1861fa06 100644 --- a/pkg/cli/search_sub_cmd.go +++ b/pkg/cli/search_sub_cmd.go @@ -6,7 +6,6 @@ package cli import ( "fmt" - godigest "github.com/opencontainers/go-digest" "github.com/spf13/cobra" zerr "zotregistry.io/zot/errors" @@ -94,16 +93,3 @@ func OneImageWithRefArg(cmd *cobra.Command, args []string) error { 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 deleted file mode 100644 index 3e652bf1..00000000 --- a/pkg/cli/searcher.go +++ /dev/null @@ -1,1169 +0,0 @@ -//go:build search -// +build search - -package cli - -import ( - "context" - "errors" - "fmt" - "io" - "math" - "strings" - "sync" - "time" - - "github.com/briandowns/spinner" - - zerr "zotregistry.io/zot/errors" - "zotregistry.io/zot/pkg/api/constants" - zcommon "zotregistry.io/zot/pkg/common" -) - -func getImageSearchers() []searcher { - searchers := []searcher{ - new(allImagesSearcher), - new(imageByNameSearcher), - new(imagesByDigestSearcher), - } - - return searchers -} - -func getCveSearchers() []searcher { - searchers := []searcher{ - new(cveByImageSearcher), - new(imagesByCVEIDSearcher), - new(tagsByImageNameAndCVEIDSearcher), - new(fixedTagsSearcher), - } - - return searchers -} - -func getImageSearchersGQL() []searcher { - searchers := []searcher{ - new(allImagesSearcherGQL), - new(imageByNameSearcherGQL), - new(imagesByDigestSearcherGQL), - new(derivedImageListSearcherGQL), - new(baseImageListSearcherGQL), - } - - return searchers -} - -func getCveSearchersGQL() []searcher { - searchers := []searcher{ - new(cveByImageSearcherGQL), - new(imagesByCVEIDSearcherGQL), - new(tagsByImageNameAndCVEIDSearcherGQL), - new(fixedTagsSearcherGQL), - } - - return searchers -} - -func getGlobalSearchersGQL() []searcher { - searchers := []searcher{ - new(globalSearcherGQL), - new(referrerSearcherGQL), - } - - return searchers -} - -func getGlobalSearchersREST() []searcher { - searchers := []searcher{ - new(referrerSearcher), - new(globalSearcherREST), - } - - return searchers -} - -type searcher interface { - search(searchConfig searchConfig) (bool, error) -} - -func canSearch(params map[string]*string, requiredParams *set) bool { - for key, value := range params { - if requiredParams.contains(key) && *value == "" { - return false - } else if !requiredParams.contains(key) && *value != "" { - return false - } - } - - return true -} - -type searchConfig struct { - params map[string]*string - searchService SearchService - servURL *string - user *string - outputFormat *string - verifyTLS *bool - fixedFlag *bool - verbose *bool - debug *bool - resultWriter io.Writer - spinner spinnerState -} - -type allImagesSearcher struct{} - -func (search allImagesSearcher) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("")) { - return false, nil - } - - 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 true, err - default: - return true, nil - } -} - -type allImagesSearcherGQL struct{} - -func (search allImagesSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("")) { - return false, nil - } - - err := getImages(config) - - return true, err -} - -type imageByNameSearcher struct{} - -func (search imageByNameSearcher) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("imageName")) { - return false, nil - } - - 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, - *config.params["imageName"], 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 true, err - default: - return true, nil - } -} - -type imageByNameSearcherGQL struct{} - -func (search imageByNameSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("imageName")) { - return false, nil - } - - err := getImages(config) - - return true, err -} - -func getImages(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, *config.params["imageName"]) - if err != nil { - return err - } - - imageListData := []imageStruct{} - - for _, image := range imageList.Results { - imageListData = append(imageListData, imageStruct(image)) - } - - return printImageResult(config, imageListData) -} - -type imagesByDigestSearcher struct{} - -func (search imagesByDigestSearcher) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("digest")) { - return false, nil - } - - 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, - *config.params["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 true, err - default: - return true, nil - } -} - -type derivedImageListSearcherGQL struct{} - -func (search derivedImageListSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("derivedImage")) { - return false, nil - } - - username, password := getUsernameAndPassword(*config.user) - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - imageList, err := config.searchService.getDerivedImageListGQL(ctx, config, username, - password, *config.params["derivedImage"]) - if err != nil { - return true, err - } - - imageListData := []imageStruct{} - - for _, image := range imageList.DerivedImageList.Results { - imageListData = append(imageListData, imageStruct(image)) - } - - if err := printImageResult(config, imageListData); err != nil { - return true, err - } - - return true, nil -} - -type baseImageListSearcherGQL struct{} - -func (search baseImageListSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("baseImage")) { - return false, nil - } - - username, password := getUsernameAndPassword(*config.user) - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - imageList, err := config.searchService.getBaseImageListGQL(ctx, config, username, - password, *config.params["baseImage"]) - if err != nil { - return true, err - } - - imageListData := []imageStruct{} - - for _, image := range imageList.BaseImageList.Results { - imageListData = append(imageListData, imageStruct(image)) - } - - if err := printImageResult(config, imageListData); err != nil { - return true, err - } - - return true, nil -} - -type imagesByDigestSearcherGQL struct{} - -func (search imagesByDigestSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("digest")) { - return false, nil - } - - // var builder strings.Builder - - username, password := getUsernameAndPassword(*config.user) - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - imageList, err := config.searchService.getImagesForDigestGQL(ctx, config, username, password, *config.params["digest"]) - if err != nil { - return true, err - } - - imageListData := []imageStruct{} - - for _, image := range imageList.Results { - imageListData = append(imageListData, imageStruct(image)) - } - - if err := printImageResult(config, imageListData); err != nil { - return true, err - } - - return true, nil -} - -type cveByImageSearcher struct{} - -func (search cveByImageSearcher) search(config searchConfig) (bool, error) { - if (!canSearch(config.params, newSet("imageName")) && - !canSearch(config.params, newSet("imageName", "searchedCVE"))) || *config.fixedFlag { - return false, nil - } - - if !validateImageNameTag(*config.params["imageName"]) { - return true, errInvalidImageNameAndTag - } - - username, password := getUsernameAndPassword(*config.user) - strErr := make(chan stringResult) - ctx, cancel := context.WithCancel(context.Background()) - - var wg sync.WaitGroup - - wg.Add(1) - - go config.searchService.getCveByImage(ctx, config, username, password, *config.params["imageName"], - *config.params["searchedCVE"], strErr, &wg) - wg.Add(1) - - errCh := make(chan error, 1) - go collectResults(config, &wg, strErr, cancel, printCVETableHeader, errCh) - - wg.Wait() - - select { - case err := <-errCh: - return true, err - default: - return true, nil - } -} - -type cveByImageSearcherGQL struct{} - -func (search cveByImageSearcherGQL) search(config searchConfig) (bool, error) { - if (!canSearch(config.params, newSet("imageName")) && - !canSearch(config.params, newSet("imageName", "searchedCVE"))) || *config.fixedFlag { - return false, nil - } - - if !validateImageNameTag(*config.params["imageName"]) { - return true, errInvalidImageNameAndTag - } - - var builder strings.Builder - - username, password := getUsernameAndPassword(*config.user) - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - cveList, err := config.searchService.getCveByImageGQL(ctx, config, username, password, - *config.params["imageName"], *config.params["searchedCVE"]) - if err != nil { - return true, err - } - - if len(cveList.Data.CVEListForImage.CVEList) > 0 && - (*config.outputFormat == defaultOutputFormat || *config.outputFormat == "") { - printCVETableHeader(&builder, *config.verbose, 0, 0, 0) - fmt.Fprint(config.resultWriter, builder.String()) - } - - if len(cveList.Data.CVEListForImage.CVEList) == 0 { - fmt.Fprint(config.resultWriter, "No CVEs found for image\n") - - return true, nil - } - - out, err := cveList.string(*config.outputFormat) - if err != nil { - return true, err - } - - fmt.Fprint(config.resultWriter, out) - - return true, nil -} - -type imagesByCVEIDSearcher struct{} - -func (search imagesByCVEIDSearcher) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("cveID")) || *config.fixedFlag { - return false, nil - } - - username, password := getUsernameAndPassword(*config.user) - strErr := make(chan stringResult) - ctx, cancel := context.WithCancel(context.Background()) - - var wg sync.WaitGroup - - wg.Add(1) - - go config.searchService.getImagesByCveID(ctx, config, username, password, *config.params["cveID"], strErr, &wg) - wg.Add(1) - - errCh := make(chan error, 1) - go collectResults(config, &wg, strErr, cancel, printImageTableHeader, errCh) - - wg.Wait() - - select { - case err := <-errCh: - return true, err - default: - return true, nil - } -} - -type imagesByCVEIDSearcherGQL struct{} - -func (search imagesByCVEIDSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("cveID")) || *config.fixedFlag { - return false, nil - } - - username, password := getUsernameAndPassword(*config.user) - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - imageList, err := config.searchService.getImagesByCveIDGQL(ctx, config, username, password, *config.params["cveID"]) - if err != nil { - return true, err - } - - imageListData := []imageStruct{} - - for _, image := range imageList.Results { - imageListData = append(imageListData, imageStruct(image)) - } - - if err := printImageResult(config, imageListData); err != nil { - return true, err - } - - return true, nil -} - -type tagsByImageNameAndCVEIDSearcher struct{} - -func (search tagsByImageNameAndCVEIDSearcher) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("cveID", "imageName")) || *config.fixedFlag { - return false, nil - } - - if strings.Contains(*config.params["imageName"], ":") { - return true, errInvalidImageName - } - - username, password := getUsernameAndPassword(*config.user) - strErr := make(chan stringResult) - ctx, cancel := context.WithCancel(context.Background()) - - var wg sync.WaitGroup - - wg.Add(1) - - go config.searchService.getImageByNameAndCVEID(ctx, config, username, password, *config.params["imageName"], - *config.params["cveID"], strErr, &wg) - wg.Add(1) - - errCh := make(chan error, 1) - go collectResults(config, &wg, strErr, cancel, printImageTableHeader, errCh) - - wg.Wait() - - select { - case err := <-errCh: - return true, err - default: - return true, nil - } -} - -type tagsByImageNameAndCVEIDSearcherGQL struct{} - -func (search tagsByImageNameAndCVEIDSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("cveID", "imageName")) || *config.fixedFlag { - return false, nil - } - - if strings.Contains(*config.params["imageName"], ":") { - return true, errInvalidImageName - } - - err := getTagsByCVE(config) - - return true, err -} - -type fixedTagsSearcherGQL struct{} - -func (search fixedTagsSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("cveID", "imageName")) || !*config.fixedFlag { - return false, nil - } - - err := getTagsByCVE(config) - - return true, err -} - -type fixedTagsSearcher struct{} - -func (search fixedTagsSearcher) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("cveID", "imageName")) || !*config.fixedFlag { - return false, nil - } - - if strings.Contains(*config.params["imageName"], ":") { - return true, errInvalidImageName - } - - username, password := getUsernameAndPassword(*config.user) - strErr := make(chan stringResult) - ctx, cancel := context.WithCancel(context.Background()) - - var wg sync.WaitGroup - - wg.Add(1) - - go config.searchService.getFixedTagsForCVE(ctx, config, username, password, *config.params["imageName"], - *config.params["cveID"], strErr, &wg) - wg.Add(1) - - errCh := make(chan error, 1) - go collectResults(config, &wg, strErr, cancel, printImageTableHeader, errCh) - - wg.Wait() - - select { - case err := <-errCh: - return true, err - default: - return true, nil - } -} - -func getTagsByCVE(config searchConfig) error { - if strings.Contains(*config.params["imageName"], ":") { - return errInvalidImageName - } - - username, password := getUsernameAndPassword(*config.user) - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - var imageList []imageStruct - - if *config.fixedFlag { - fixedTags, err := config.searchService.getFixedTagsForCVEGQL(ctx, config, username, password, - *config.params["imageName"], *config.params["cveID"]) - if err != nil { - return err - } - - for _, image := range fixedTags.Results { - imageList = append(imageList, imageStruct(image)) - } - } else { - tags, err := config.searchService.getTagsForCVEGQL(ctx, config, username, password, - *config.params["imageName"], *config.params["cveID"]) - if err != nil { - return err - } - - imageList = nil - for _, image := range tags.Results { - imageList = append(imageList, imageStruct(image)) - } - } - - return printImageResult(config, imageList) -} - -type referrerSearcherGQL struct{} - -func (search referrerSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("subject")) { - return false, nil - } - - username, password := getUsernameAndPassword(*config.user) - - repo, ref, refIsTag, err := zcommon.GetRepoReference(*config.params["subject"]) - if err != nil { - return true, err - } - - digest := ref - - if refIsTag { - digest, err = fetchImageDigest(repo, ref, username, password, config) - if err != nil { - return true, err - } - } - - response, err := config.searchService.getReferrersGQL(context.Background(), config, username, password, repo, digest) - if err != nil { - return true, 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 true, printReferrersResult(config, referrersList, maxArtifactTypeLen) -} - -func fetchImageDigest(repo, ref, username, password string, config searchConfig) (string, error) { - url, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/manifests/%s", repo, ref)) - if err != nil { - return "", err - } - - res, err := makeHEADRequest(context.Background(), url, username, password, *config.verifyTLS, false) - - digestStr := res.Get(constants.DistContentDigestKey) - - return digestStr, err -} - -type referrerSearcher struct{} - -func (search referrerSearcher) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("subject")) { - return false, nil - } - - username, password := getUsernameAndPassword(*config.user) - - repo, ref, refIsTag, err := zcommon.GetRepoReference(*config.params["subject"]) - if err != nil { - return true, err - } - - digest := ref - - if refIsTag { - digest, err = fetchImageDigest(repo, ref, username, password, config) - if err != nil { - return true, err - } - } - - referrersList, err := config.searchService.getReferrers(context.Background(), config, username, password, - repo, digest) - if err != nil { - return true, err - } - - maxArtifactTypeLen := math.MinInt - - for _, referrer := range referrersList { - if maxArtifactTypeLen < len(referrer.ArtifactType) { - maxArtifactTypeLen = len(referrer.ArtifactType) - } - } - - printReferrersTableHeader(config, config.resultWriter, maxArtifactTypeLen) - - return true, printReferrersResult(config, referrersList, maxArtifactTypeLen) -} - -type globalSearcherGQL struct{} - -func (search globalSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("query")) { - return false, nil - } - - username, password := getUsernameAndPassword(*config.user) - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - query := *config.params["query"] - - globalSearchResult, err := config.searchService.globalSearchGQL(ctx, config, username, password, query) - if err != nil { - return true, 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 true, err - } - - return true, printRepoResults(config, reposList) -} - -type globalSearcherREST struct{} - -func (search globalSearcherREST) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("query")) { - return false, nil - } - - return true, fmt.Errorf("search extension is not enabled: %w", zerr.ErrExtensionNotEnabled) -} - -func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult, - cancel context.CancelFunc, printHeader printHeader, errCh chan error, -) { - var foundResult bool - - defer wg.Done() - config.spinner.startSpinner() - - for { - select { - case result, ok := <-imageErr: - config.spinner.stopSpinner() - - if !ok { - cancel() - - return - } - - if result.Err != nil { - cancel() - errCh <- result.Err - - return - } - - if !foundResult && (*config.outputFormat == defaultOutputFormat || *config.outputFormat == "") { - var builder strings.Builder - - printHeader(&builder, *config.verbose, 0, 0, 0) - fmt.Fprint(config.resultWriter, builder.String()) - } - - foundResult = true - - fmt.Fprint(config.resultWriter, result.StrValue) - case <-time.After(waitTimeout): - config.spinner.stopSpinner() - cancel() - - errCh <- zerr.ErrCLITimeout - - return - } - } -} - -func getUsernameAndPassword(user string) (string, string) { - if strings.Contains(user, ":") { - split := strings.Split(user, ":") - - return split[0], split[1] - } - - return "", "" -} - -func validateImageNameTag(input string) bool { - if !strings.Contains(input, ":") { - return false - } - - split := strings.Split(input, ":") - name := strings.TrimSpace(split[0]) - tag := strings.TrimSpace(split[1]) - - if name == "" || tag == "" { - return false - } - - return true -} - -type spinnerState struct { - spinner *spinner.Spinner - enabled bool -} - -func (spinner *spinnerState) startSpinner() { - if spinner.enabled { - spinner.spinner.Start() - } -} - -func (spinner *spinnerState) stopSpinner() { - if spinner.enabled && spinner.spinner.Active() { - spinner.spinner.Stop() - } -} - -type set struct { - m map[string]struct{} -} - -func getEmptyStruct() struct{} { - return struct{}{} -} - -func newSet(initialValues ...string) *set { - setValues := &set{} - setValues.m = make(map[string]struct{}) - - for _, val := range initialValues { - setValues.m[val] = getEmptyStruct() - } - - return setValues -} - -func (s *set) contains(value string) bool { - _, c := s.m[value] - - return c -} - -const ( - waitTimeout = httpTimeout + 5*time.Second -) - -var ( - ErrCannotSearch = errors.New("cannot search with these parameters") - ErrInvalidOutputFormat = errors.New("invalid output format") -) - -type stringResult struct { - StrValue string - Err error -} - -type printHeader func(writer io.Writer, verbose bool, maxImageNameLen, maxTagLen, maxPlatformLen int) - -func printImageTableHeader(writer io.Writer, verbose bool, maxImageNameLen, maxTagLen, maxPlatformLen int) { - table := getImageTableWriter(writer) - - table.SetColMinWidth(colImageNameIndex, imageNameWidth) - table.SetColMinWidth(colTagIndex, tagWidth) - table.SetColMinWidth(colPlatformIndex, platformWidth) - table.SetColMinWidth(colDigestIndex, digestWidth) - table.SetColMinWidth(colSizeIndex, sizeWidth) - table.SetColMinWidth(colIsSignedIndex, isSignedWidth) - - if verbose { - table.SetColMinWidth(colConfigIndex, configWidth) - table.SetColMinWidth(colLayersIndex, layersWidth) - } - - row := make([]string, 8) //nolint:gomnd - - // adding spaces so that repository and tag columns are aligned - // in case the name/tag are fully shown and too long - var offset string - if maxImageNameLen > len("REPOSITORY") { - offset = strings.Repeat(" ", maxImageNameLen-len("REPOSITORY")) - row[colImageNameIndex] = "REPOSITORY" + offset - } else { - row[colImageNameIndex] = "REPOSITORY" - } - - if maxTagLen > len("TAG") { - offset = strings.Repeat(" ", maxTagLen-len("TAG")) - row[colTagIndex] = "TAG" + offset - } else { - row[colTagIndex] = "TAG" - } - - if maxPlatformLen > len("OS/ARCH") { - offset = strings.Repeat(" ", maxPlatformLen-len("OS/ARCH")) - row[colPlatformIndex] = "OS/ARCH" + offset - } else { - row[colPlatformIndex] = "OS/ARCH" - } - - row[colDigestIndex] = "DIGEST" - row[colSizeIndex] = sizeColumn - row[colIsSignedIndex] = "SIGNED" - - if verbose { - row[colConfigIndex] = "CONFIG" - row[colLayersIndex] = "LAYERS" - } - - table.Append(row) - table.Render() -} - -func printCVETableHeader(writer io.Writer, verbose bool, maxImgLen, maxTagLen, maxPlatformLen int) { - table := getCVETableWriter(writer) - row := make([]string, 3) //nolint:gomnd - row[colCVEIDIndex] = "ID" - row[colCVESeverityIndex] = "SEVERITY" - row[colCVETitleIndex] = "TITLE" - - table.Append(row) - table.Render() -} - -func printReferrersTableHeader(config searchConfig, writer io.Writer, maxArtifactTypeLen int) { - if *config.outputFormat != "" && *config.outputFormat != defaultOutputFormat { - return - } - - table := getReferrersTableWriter(writer) - - table.SetColMinWidth(refArtifactTypeIndex, maxArtifactTypeLen) - table.SetColMinWidth(refDigestIndex, digestWidth) - table.SetColMinWidth(refSizeIndex, sizeWidth) - - row := make([]string, refRowWidth) - - // adding spaces so that repository and tag columns are aligned - // in case the name/tag are fully shown and too long - var offset string - - if maxArtifactTypeLen > len("ARTIFACT TYPE") { - offset = strings.Repeat(" ", maxArtifactTypeLen-len("ARTIFACT TYPE")) - row[refArtifactTypeIndex] = "ARTIFACT TYPE" + offset - } else { - row[refArtifactTypeIndex] = "ARTIFACT TYPE" - } - - row[refDigestIndex] = "DIGEST" - row[refSizeIndex] = sizeColumn - - table.Append(row) - table.Render() -} - -func printRepoTableHeader(writer io.Writer, repoMaxLen, maxTimeLen int, verbose bool) { - table := getRepoTableWriter(writer) - - table.SetColMinWidth(repoNameIndex, repoMaxLen) - table.SetColMinWidth(repoSizeIndex, sizeWidth) - table.SetColMinWidth(repoLastUpdatedIndex, maxTimeLen) - table.SetColMinWidth(repoDownloadsIndex, sizeWidth) - table.SetColMinWidth(repoStarsIndex, sizeWidth) - - if verbose { - table.SetColMinWidth(repoPlatformsIndex, platformWidth) - } - - row := make([]string, repoRowWidth) - - // adding spaces so that repository and tag columns are aligned - // in case the name/tag are fully shown and too long - var offset string - - if repoMaxLen > len("NAME") { - offset = strings.Repeat(" ", repoMaxLen-len("NAME")) - row[repoNameIndex] = "NAME" + offset - } else { - row[repoNameIndex] = "NAME" - } - - if repoMaxLen > len("LAST UPDATED") { - offset = strings.Repeat(" ", repoMaxLen-len("LAST UPDATED")) - row[repoLastUpdatedIndex] = "LAST UPDATED" + offset - } else { - row[repoLastUpdatedIndex] = "LAST UPDATED" - } - - row[repoSizeIndex] = sizeColumn - row[repoDownloadsIndex] = "DOWNLOADS" - row[repoStarsIndex] = "STARS" - - if verbose { - row[repoPlatformsIndex] = "PLATFORMS" - } - - table.Append(row) - table.Render() -} - -func printReferrersResult(config searchConfig, referrersList referrersResult, maxArtifactTypeLen int) error { - out, err := referrersList.string(*config.outputFormat, maxArtifactTypeLen) - if err != nil { - return err - } - - fmt.Fprint(config.resultWriter, out) - - return nil -} - -func printImageResult(config searchConfig, imageList []imageStruct) error { - var builder strings.Builder - maxImgNameLen := 0 - maxTagLen := 0 - maxPlatformLen := 0 - - if len(imageList) > 0 { - for i := range imageList { - if maxImgNameLen < len(imageList[i].RepoName) { - maxImgNameLen = len(imageList[i].RepoName) - } - - if maxTagLen < len(imageList[i].Tag) { - maxTagLen = len(imageList[i].Tag) - } - - for j := range imageList[i].Manifests { - platform := imageList[i].Manifests[j].Platform.Os + "/" + imageList[i].Manifests[j].Platform.Arch - - if maxPlatformLen < len(platform) { - maxPlatformLen = len(platform) - } - } - } - - if *config.outputFormat == defaultOutputFormat || *config.outputFormat == "" { - printImageTableHeader(&builder, *config.verbose, maxImgNameLen, maxTagLen, maxPlatformLen) - } - - fmt.Fprint(config.resultWriter, builder.String()) - } - - for i := range imageList { - img := imageList[i] - verbose := *config.verbose - - out, err := img.string(*config.outputFormat, maxImgNameLen, maxTagLen, maxPlatformLen, verbose) - if err != nil { - return err - } - - fmt.Fprint(config.resultWriter, out) - } - - return nil -} - -func printRepoResults(config searchConfig, repoList []repoStruct) error { - maxRepoNameLen := 0 - maxTimeLen := 0 - - for _, repo := range repoList { - if maxRepoNameLen < len(repo.Name) { - maxRepoNameLen = len(repo.Name) - } - - if maxTimeLen < len(repo.LastUpdated.String()) { - maxTimeLen = len(repo.LastUpdated.String()) - } - } - - if len(repoList) > 0 && (*config.outputFormat == defaultOutputFormat || *config.outputFormat == "") { - printRepoTableHeader(config.resultWriter, maxRepoNameLen, maxTimeLen, *config.verbose) - } - - for _, repo := range repoList { - out, err := repo.string(*config.outputFormat, maxRepoNameLen, maxTimeLen, *config.verbose) - if err != nil { - return err - } - - fmt.Fprint(config.resultWriter, out) - } - - return nil -} - -var ( - errInvalidImageNameAndTag = errors.New("cli: Invalid input format. Expected IMAGENAME:TAG") - errInvalidImageName = errors.New("cli: Invalid input format. Expected IMAGENAME without :TAG") -) - -type repoSearcher struct{} - -func (search repoSearcher) 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 - } -} - -const ( - sizeColumn = "SIZE" -) diff --git a/pkg/cli/service.go b/pkg/cli/service.go index 2567191d..0d99fea7 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -38,8 +38,6 @@ type SearchService interface { //nolint:interfacebloat 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, repo, cveID string) (*common.ImagesForCve, error) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, @@ -55,24 +53,29 @@ type SearchService interface { //nolint:interfacebloat getAllImages(ctx context.Context, config searchConfig, username, password string, channel chan stringResult, wtgrp *sync.WaitGroup) - getCveByImage(ctx context.Context, config searchConfig, username, password, imageName, searchedCVE string, - channel chan stringResult, wtgrp *sync.WaitGroup) - getImagesByCveID(ctx context.Context, config searchConfig, username, password, cveid string, - channel chan stringResult, wtgrp *sync.WaitGroup) getImagesByDigest(ctx context.Context, config searchConfig, username, password, digest string, channel chan stringResult, wtgrp *sync.WaitGroup) - getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cveid string, - channel chan stringResult, wtgrp *sync.WaitGroup) getRepos(ctx context.Context, config searchConfig, username, password string, channel chan stringResult, wtgrp *sync.WaitGroup) getImageByName(ctx context.Context, config searchConfig, username, password, imageName string, channel chan stringResult, wtgrp *sync.WaitGroup) - getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cveid string, - channel chan stringResult, wtgrp *sync.WaitGroup) getReferrers(ctx context.Context, config searchConfig, username, password string, repo, digest string, ) (referrersResult, error) } +type searchConfig struct { + searchService SearchService + servURL string + user string + outputFormat string + verifyTLS bool + fixedFlag bool + verbose bool + debug bool + resultWriter io.Writer + spinner spinnerState +} + type searchService struct{} func NewSearchService() SearchService { @@ -297,43 +300,6 @@ func (service searchService) getImagesForDigestGQL(ctx context.Context, config s return result, nil } -func (service searchService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, - password, cveID string, -) (*common.ImagesForCve, error) { - query := fmt.Sprintf(` - { - ImageListForCVE(id: "%s", requestedPage: {sortBy: ALPHABETIC_ASC}) { - Results { - RepoName Tag - Digest - MediaType - Manifests { - Digest - ConfigDigest - Size - Platform {Os Arch} - IsSigned - Layers {Size Digest} - LastUpdated - } - LastUpdated - Size - IsSigned - } - } - }`, - cveID) - result := &common.ImagesForCve{} - - err := service.makeGraphQLQuery(ctx, config, username, password, query, result) - - if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { - return nil, errResult - } - - return result, nil -} - func (service searchService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password, imageName, searchedCVE string, ) (*cveResult, error) { @@ -443,7 +409,7 @@ func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config s func (service searchService) getReferrers(ctx context.Context, config searchConfig, username, password string, repo, digest string, ) (referrersResult, error) { - referrersEndpoint, err := combineServerAndEndpointURL(*config.servURL, + referrersEndpoint, err := combineServerAndEndpointURL(config.servURL, fmt.Sprintf("/v2/%s/referrers/%s", repo, digest)) if err != nil { if isContextDone(ctx) { @@ -454,8 +420,8 @@ func (service searchService) getReferrers(ctx context.Context, config searchConf } referrerResp := &ispec.Index{} - _, err = makeGETRequest(ctx, referrersEndpoint, username, password, *config.verifyTLS, - *config.debug, &referrerResp, config.resultWriter) + _, err = makeGETRequest(ctx, referrersEndpoint, username, password, config.verifyTLS, + config.debug, &referrerResp, config.resultWriter) if err != nil { if isContextDone(ctx) { @@ -505,7 +471,7 @@ func (service searchService) getAllImages(ctx context.Context, config searchConf catalog := &catalogResponse{} - catalogEndPoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("%s%s", + catalogEndPoint, err := combineServerAndEndpointURL(config.servURL, fmt.Sprintf("%s%s", constants.RoutePrefix, constants.ExtCatalogPrefix)) if err != nil { if isContextDone(ctx) { @@ -516,8 +482,8 @@ func (service searchService) getAllImages(ctx context.Context, config searchConf return } - _, err = makeGETRequest(ctx, catalogEndPoint, username, password, *config.verifyTLS, - *config.debug, catalog, config.resultWriter) + _, err = makeGETRequest(ctx, catalogEndPoint, username, password, config.verifyTLS, + config.debug, catalog, config.resultWriter) if err != nil { if isContextDone(ctx) { return @@ -551,7 +517,7 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag repo, imageTag := common.GetImageDirAndTag(imageName) - tagListEndpoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/tags/list", repo)) + tagListEndpoint, err := combineServerAndEndpointURL(config.servURL, fmt.Sprintf("/v2/%s/tags/list", repo)) if err != nil { if isContextDone(ctx) { return @@ -562,8 +528,8 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag } tagList := &tagListResp{} - _, err = makeGETRequest(ctx, tagListEndpoint, username, password, *config.verifyTLS, - *config.debug, &tagList, config.resultWriter) + _, err = makeGETRequest(ctx, tagListEndpoint, username, password, config.verifyTLS, + config.debug, &tagList, config.resultWriter) if err != nil { if isContextDone(ctx) { @@ -598,79 +564,6 @@ func getImage(ctx context.Context, config searchConfig, username, password, imag } } -func (service searchService) getImagesByCveID(ctx context.Context, config searchConfig, username, - password, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, -) { - defer wtgrp.Done() - defer close(rch) - - query := fmt.Sprintf( - `{ - ImageListForCVE(id: "%s") { - Results { - RepoName Tag - Digest - MediaType - Manifests { - Digest - ConfigDigest - Size - Platform {Os Arch} - IsSigned - Layers {Size Digest} - LastUpdated - } - LastUpdated - Size - IsSigned - } - } - }`, - cveid) - - result := &common.ImagesForCve{} - - err := service.makeGraphQLQuery(ctx, config, username, password, query, result) - if err != nil { - if isContextDone(ctx) { - return - } - rch <- stringResult{"", err} - - return - } - - if result.Errors != nil || err != nil { - var errBuilder strings.Builder - - for _, err := range result.Errors { - fmt.Fprintln(&errBuilder, err.Message) - } - - if isContextDone(ctx) { - return - } - rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 - - return - } - - var localWg sync.WaitGroup - - rlim := newSmoothRateLimiter(&localWg, rch) - localWg.Add(1) - - go rlim.startRateLimiter(ctx) - - for _, image := range result.Results { - localWg.Add(1) - - go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg) - } - - localWg.Wait() -} - func (service searchService) getImagesByDigest(ctx context.Context, config searchConfig, username, password string, digest string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { @@ -744,209 +637,6 @@ func (service searchService) getImagesByDigest(ctx context.Context, config searc localWg.Wait() } -func (service searchService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, - password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, -) { - defer wtgrp.Done() - defer close(rch) - - query := fmt.Sprintf( - `{ - ImageListForCVE(id: "%s") { - Results { - RepoName Tag - Digest - MediaType - Manifests { - Digest - ConfigDigest - Size - Platform {Os Arch} - IsSigned - Layers {Size Digest} - LastUpdated - } - LastUpdated - Size - IsSigned - } - } - }`, - cveid) - - result := &common.ImagesForCve{} - - err := service.makeGraphQLQuery(ctx, config, username, password, query, result) - if err != nil { - if isContextDone(ctx) { - return - } - rch <- stringResult{"", err} - - return - } - - if result.Errors != nil { - var errBuilder strings.Builder - - for _, err := range result.Errors { - fmt.Fprintln(&errBuilder, err.Message) - } - - if isContextDone(ctx) { - return - } - rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 - - return - } - - var localWg sync.WaitGroup - - rlim := newSmoothRateLimiter(&localWg, rch) - localWg.Add(1) - - go rlim.startRateLimiter(ctx) - - for _, image := range result.Results { - if imageName != "" && !strings.EqualFold(imageName, image.RepoName) { - continue - } - - localWg.Add(1) - - go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg) - } - - localWg.Wait() -} - -func (service searchService) getCveByImage(ctx context.Context, config searchConfig, username, password, - imageName, searchedCVE string, rch chan stringResult, wtgrp *sync.WaitGroup, -) { - defer wtgrp.Done() - defer close(rch) - - query := fmt.Sprintf(`{ CVEListForImage (image:"%s", searchedCVE:"%s")`+ - ` { Tag CVEList { Id Title Severity Description `+ - `PackageList {Name InstalledVersion FixedVersion}} } }`, imageName, searchedCVE) - result := &cveResult{} - - err := service.makeGraphQLQuery(ctx, config, username, password, query, result) - if err != nil { - if isContextDone(ctx) { - return - } - rch <- stringResult{"", err} - - return - } - - if result.Errors != nil { - var errBuilder strings.Builder - - for _, err := range result.Errors { - fmt.Fprintln(&errBuilder, err.Message) - } - - if isContextDone(ctx) { - return - } - rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 - - return - } - - result.Data.CVEListForImage.CVEList = groupCVEsBySeverity(result.Data.CVEListForImage.CVEList) - - str, err := result.string(*config.outputFormat) - if err != nil { - if isContextDone(ctx) { - return - } - rch <- stringResult{"", err} - - return - } - - if isContextDone(ctx) { - return - } - rch <- stringResult{str, nil} -} - -func (service searchService) getFixedTagsForCVE(ctx context.Context, config searchConfig, - username, password, imageName, cveid string, rch chan stringResult, wtgrp *sync.WaitGroup, -) { - defer wtgrp.Done() - defer close(rch) - - query := fmt.Sprintf(` - { - ImageListWithCVEFixed (id: "%s", image: "%s") { - Results { - RepoName Tag - Digest - MediaType - Manifests { - Digest - ConfigDigest - Size - Platform {Os Arch} - IsSigned - Layers {Size Digest} - LastUpdated - } - LastUpdated - Size - IsSigned - } - } - }`, cveid, imageName) - - result := &common.ImageListWithCVEFixedResponse{} - - err := service.makeGraphQLQuery(ctx, config, username, password, query, result) - if err != nil { - if isContextDone(ctx) { - return - } - rch <- stringResult{"", err} - - return - } - - if result.Errors != nil { - var errBuilder strings.Builder - - for _, err := range result.Errors { - fmt.Fprintln(&errBuilder, err.Message) - } - - if isContextDone(ctx) { - return - } - rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 - - return - } - - var localWg sync.WaitGroup - - rlim := newSmoothRateLimiter(&localWg, rch) - localWg.Add(1) - - go rlim.startRateLimiter(ctx) - - for _, img := range result.Results { - localWg.Add(1) - - go addManifestCallToPool(ctx, config, rlim, username, password, imageName, img.Tag, rch, &localWg) - } - - localWg.Wait() -} - func groupCVEsBySeverity(cveList []cve) []cve { var ( unknown = make([]cve, 0) @@ -1006,13 +696,13 @@ func (service searchService) makeGraphQLQuery(ctx context.Context, config searchConfig, username, password, query string, resultPtr interface{}, ) error { - endPoint, err := combineServerAndEndpointURL(*config.servURL, constants.FullSearchPrefix) + endPoint, err := combineServerAndEndpointURL(config.servURL, constants.FullSearchPrefix) if err != nil { return err } - err = makeGraphQLRequest(ctx, endPoint, query, username, password, *config.verifyTLS, - *config.debug, resultPtr, config.resultWriter) + err = makeGraphQLRequest(ctx, endPoint, query, username, password, config.verifyTLS, + config.debug, resultPtr, config.resultWriter) if err != nil { return err } @@ -1053,7 +743,7 @@ func addManifestCallToPool(ctx context.Context, config searchConfig, pool *reque ) { defer wtgrp.Done() - manifestEndpoint, err := combineServerAndEndpointURL(*config.servURL, + manifestEndpoint, err := combineServerAndEndpointURL(config.servURL, fmt.Sprintf("/v2/%s/manifests/%s", imageName, tagName)) if err != nil { if isContextDone(ctx) { @@ -1121,7 +811,7 @@ func (cve cveResult) string(format string) (string, error) { case ymlFormat, yamlFormat: return cve.stringYAML() default: - return "", ErrInvalidOutputFormat + return "", zerr.ErrInvalidOutputFormat } } @@ -1180,7 +870,7 @@ func (ref referrersResult) string(format string, maxArtifactTypeLen int) (string case ymlFormat, yamlFormat: return ref.stringYAML() default: - return "", ErrInvalidOutputFormat + return "", zerr.ErrInvalidOutputFormat } } @@ -1244,7 +934,7 @@ func (repo repoStruct) string(format string, maxImgNameLen, maxTimeLen int, verb case ymlFormat, yamlFormat: return repo.stringYAML() default: - return "", ErrInvalidOutputFormat + return "", zerr.ErrInvalidOutputFormat } } @@ -1336,7 +1026,7 @@ func (img imageStruct) string(format string, maxImgNameLen, maxTagLen, maxPlatfo case ymlFormat, yamlFormat: return img.stringYAML() default: - return "", ErrInvalidOutputFormat + return "", zerr.ErrInvalidOutputFormat } } @@ -1663,7 +1353,7 @@ func (service searchService) getRepos(ctx context.Context, config searchConfig, catalog := &catalogResponse{} - catalogEndPoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("%s%s", + catalogEndPoint, err := combineServerAndEndpointURL(config.servURL, fmt.Sprintf("%s%s", constants.RoutePrefix, constants.ExtCatalogPrefix)) if err != nil { if isContextDone(ctx) { @@ -1674,8 +1364,8 @@ func (service searchService) getRepos(ctx context.Context, config searchConfig, return } - _, err = makeGETRequest(ctx, catalogEndPoint, username, password, *config.verifyTLS, - *config.debug, catalog, config.resultWriter) + _, err = makeGETRequest(ctx, catalogEndPoint, username, password, config.verifyTLS, + config.debug, catalog, config.resultWriter) if err != nil { if isContextDone(ctx) { return diff --git a/pkg/cli/utils.go b/pkg/cli/utils.go new file mode 100644 index 00000000..785a51aa --- /dev/null +++ b/pkg/cli/utils.go @@ -0,0 +1,481 @@ +//go:build search +// +build search + +package cli + +import ( + "context" + "fmt" + "io" + "net/url" + "os" + "path" + "strings" + "sync" + "time" + + "github.com/briandowns/spinner" + "github.com/spf13/cobra" + + zerr "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/api/constants" + "zotregistry.io/zot/pkg/cli/cmdflags" +) + +const ( + sizeColumn = "SIZE" +) + +func ref[T any](input T) *T { + ref := input + + return &ref +} + +func fetchImageDigest(repo, ref, username, password string, config searchConfig) (string, error) { + url, err := combineServerAndEndpointURL(config.servURL, fmt.Sprintf("/v2/%s/manifests/%s", repo, ref)) + if err != nil { + return "", err + } + + res, err := makeHEADRequest(context.Background(), url, username, password, config.verifyTLS, false) + + digestStr := res.Get(constants.DistContentDigestKey) + + return digestStr, err +} + +func collectResults(config searchConfig, wg *sync.WaitGroup, imageErr chan stringResult, + cancel context.CancelFunc, printHeader printHeader, errCh chan error, +) { + var foundResult bool + + defer wg.Done() + config.spinner.startSpinner() + + for { + select { + case result, ok := <-imageErr: + config.spinner.stopSpinner() + + if !ok { + cancel() + + return + } + + if result.Err != nil { + cancel() + errCh <- result.Err + + return + } + + if !foundResult && (config.outputFormat == defaultOutputFormat || config.outputFormat == "") { + var builder strings.Builder + + printHeader(&builder, config.verbose, 0, 0, 0) + fmt.Fprint(config.resultWriter, builder.String()) + } + + foundResult = true + + fmt.Fprint(config.resultWriter, result.StrValue) + case <-time.After(waitTimeout): + config.spinner.stopSpinner() + cancel() + + errCh <- zerr.ErrCLITimeout + + return + } + } +} + +func getUsernameAndPassword(user string) (string, string) { + if strings.Contains(user, ":") { + split := strings.Split(user, ":") + + return split[0], split[1] + } + + return "", "" +} + +type spinnerState struct { + spinner *spinner.Spinner + enabled bool +} + +func (spinner *spinnerState) startSpinner() { + if spinner.enabled { + spinner.spinner.Start() + } +} + +func (spinner *spinnerState) stopSpinner() { + if spinner.enabled && spinner.spinner.Active() { + spinner.spinner.Stop() + } +} + +const ( + waitTimeout = httpTimeout + 5*time.Second +) + +type stringResult struct { + StrValue string + Err error +} + +type printHeader func(writer io.Writer, verbose bool, maxImageNameLen, maxTagLen, maxPlatformLen int) + +func printImageTableHeader(writer io.Writer, verbose bool, maxImageNameLen, maxTagLen, maxPlatformLen int) { + table := getImageTableWriter(writer) + + table.SetColMinWidth(colImageNameIndex, imageNameWidth) + table.SetColMinWidth(colTagIndex, tagWidth) + table.SetColMinWidth(colPlatformIndex, platformWidth) + table.SetColMinWidth(colDigestIndex, digestWidth) + table.SetColMinWidth(colSizeIndex, sizeWidth) + table.SetColMinWidth(colIsSignedIndex, isSignedWidth) + + if verbose { + table.SetColMinWidth(colConfigIndex, configWidth) + table.SetColMinWidth(colLayersIndex, layersWidth) + } + + row := make([]string, 8) //nolint:gomnd + + // adding spaces so that repository and tag columns are aligned + // in case the name/tag are fully shown and too long + var offset string + if maxImageNameLen > len("REPOSITORY") { + offset = strings.Repeat(" ", maxImageNameLen-len("REPOSITORY")) + row[colImageNameIndex] = "REPOSITORY" + offset + } else { + row[colImageNameIndex] = "REPOSITORY" + } + + if maxTagLen > len("TAG") { + offset = strings.Repeat(" ", maxTagLen-len("TAG")) + row[colTagIndex] = "TAG" + offset + } else { + row[colTagIndex] = "TAG" + } + + if maxPlatformLen > len("OS/ARCH") { + offset = strings.Repeat(" ", maxPlatformLen-len("OS/ARCH")) + row[colPlatformIndex] = "OS/ARCH" + offset + } else { + row[colPlatformIndex] = "OS/ARCH" + } + + row[colDigestIndex] = "DIGEST" + row[colSizeIndex] = sizeColumn + row[colIsSignedIndex] = "SIGNED" + + if verbose { + row[colConfigIndex] = "CONFIG" + row[colLayersIndex] = "LAYERS" + } + + table.Append(row) + table.Render() +} + +func printCVETableHeader(writer io.Writer) { + table := getCVETableWriter(writer) + row := make([]string, 3) //nolint:gomnd + row[colCVEIDIndex] = "ID" + row[colCVESeverityIndex] = "SEVERITY" + row[colCVETitleIndex] = "TITLE" + + table.Append(row) + table.Render() +} + +func printReferrersTableHeader(config searchConfig, writer io.Writer, maxArtifactTypeLen int) { + if config.outputFormat != "" && config.outputFormat != defaultOutputFormat { + return + } + + table := getReferrersTableWriter(writer) + + table.SetColMinWidth(refArtifactTypeIndex, maxArtifactTypeLen) + table.SetColMinWidth(refDigestIndex, digestWidth) + table.SetColMinWidth(refSizeIndex, sizeWidth) + + row := make([]string, refRowWidth) + + // adding spaces so that repository and tag columns are aligned + // in case the name/tag are fully shown and too long + var offset string + + if maxArtifactTypeLen > len("ARTIFACT TYPE") { + offset = strings.Repeat(" ", maxArtifactTypeLen-len("ARTIFACT TYPE")) + row[refArtifactTypeIndex] = "ARTIFACT TYPE" + offset + } else { + row[refArtifactTypeIndex] = "ARTIFACT TYPE" + } + + row[refDigestIndex] = "DIGEST" + row[refSizeIndex] = sizeColumn + + table.Append(row) + table.Render() +} + +func printRepoTableHeader(writer io.Writer, repoMaxLen, maxTimeLen int, verbose bool) { + table := getRepoTableWriter(writer) + + table.SetColMinWidth(repoNameIndex, repoMaxLen) + table.SetColMinWidth(repoSizeIndex, sizeWidth) + table.SetColMinWidth(repoLastUpdatedIndex, maxTimeLen) + table.SetColMinWidth(repoDownloadsIndex, sizeWidth) + table.SetColMinWidth(repoStarsIndex, sizeWidth) + + if verbose { + table.SetColMinWidth(repoPlatformsIndex, platformWidth) + } + + row := make([]string, repoRowWidth) + + // adding spaces so that repository and tag columns are aligned + // in case the name/tag are fully shown and too long + var offset string + + if repoMaxLen > len("NAME") { + offset = strings.Repeat(" ", repoMaxLen-len("NAME")) + row[repoNameIndex] = "NAME" + offset + } else { + row[repoNameIndex] = "NAME" + } + + if repoMaxLen > len("LAST UPDATED") { + offset = strings.Repeat(" ", repoMaxLen-len("LAST UPDATED")) + row[repoLastUpdatedIndex] = "LAST UPDATED" + offset + } else { + row[repoLastUpdatedIndex] = "LAST UPDATED" + } + + row[repoSizeIndex] = sizeColumn + row[repoDownloadsIndex] = "DOWNLOADS" + row[repoStarsIndex] = "STARS" + + if verbose { + row[repoPlatformsIndex] = "PLATFORMS" + } + + table.Append(row) + table.Render() +} + +func printReferrersResult(config searchConfig, referrersList referrersResult, maxArtifactTypeLen int) error { + out, err := referrersList.string(config.outputFormat, maxArtifactTypeLen) + if err != nil { + return err + } + + fmt.Fprint(config.resultWriter, out) + + return nil +} + +func printImageResult(config searchConfig, imageList []imageStruct) error { + var builder strings.Builder + maxImgNameLen := 0 + maxTagLen := 0 + maxPlatformLen := 0 + + if len(imageList) > 0 { + for i := range imageList { + if maxImgNameLen < len(imageList[i].RepoName) { + maxImgNameLen = len(imageList[i].RepoName) + } + + if maxTagLen < len(imageList[i].Tag) { + maxTagLen = len(imageList[i].Tag) + } + + for j := range imageList[i].Manifests { + platform := imageList[i].Manifests[j].Platform.Os + "/" + imageList[i].Manifests[j].Platform.Arch + + if maxPlatformLen < len(platform) { + maxPlatformLen = len(platform) + } + } + } + + if config.outputFormat == defaultOutputFormat || config.outputFormat == "" { + printImageTableHeader(&builder, config.verbose, maxImgNameLen, maxTagLen, maxPlatformLen) + } + + fmt.Fprint(config.resultWriter, builder.String()) + } + + for i := range imageList { + img := imageList[i] + verbose := config.verbose + + out, err := img.string(config.outputFormat, maxImgNameLen, maxTagLen, maxPlatformLen, verbose) + if err != nil { + return err + } + + fmt.Fprint(config.resultWriter, out) + } + + return nil +} + +func printRepoResults(config searchConfig, repoList []repoStruct) error { + maxRepoNameLen := 0 + maxTimeLen := 0 + + for _, repo := range repoList { + if maxRepoNameLen < len(repo.Name) { + maxRepoNameLen = len(repo.Name) + } + + if maxTimeLen < len(repo.LastUpdated.String()) { + maxTimeLen = len(repo.LastUpdated.String()) + } + } + + if len(repoList) > 0 && (config.outputFormat == defaultOutputFormat || config.outputFormat == "") { + printRepoTableHeader(config.resultWriter, maxRepoNameLen, maxTimeLen, config.verbose) + } + + for _, repo := range repoList { + out, err := repo.string(config.outputFormat, maxRepoNameLen, maxTimeLen, config.verbose) + if err != nil { + return err + } + + fmt.Fprint(config.resultWriter, out) + } + + return nil +} + +func GetSearchConfigFromFlags(cmd *cobra.Command, searchService SearchService) (searchConfig, error) { + serverURL, err := GetServerURLFromFlags(cmd) + if err != nil { + return searchConfig{}, err + } + + isSpinner, verifyTLS, err := GetCliConfigOptions(cmd) + if err != nil { + return searchConfig{}, err + } + + 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{ + 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, error) { + configName, err := cmd.Flags().GetString(cmdflags.ConfigFlag) + if err != nil { + return false, false, err + } + + if configName == "" { + return false, false, nil + } + + home, err := os.UserHomeDir() + if err != nil { + return false, false, err + } + + configDir := path.Join(home, "/.zot") + + isSpinner, err := parseBooleanConfig(configDir, configName, showspinnerConfig) + if err != nil { + return false, false, err + } + + verifyTLS, err := parseBooleanConfig(configDir, configName, verifyTLSConfig) + if err != nil { + return false, false, err + } + + return isSpinner, verifyTLS, nil +} + +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) + } + + _, err = url.Parse(serverURL) + + return serverURL, err +} + +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/test/blackbox/cve.bats b/test/blackbox/cve.bats index 326179ac..79ead772 100644 --- a/test/blackbox/cve.bats +++ b/test/blackbox/cve.bats @@ -84,10 +84,12 @@ function teardown_file() { run curl http://127.0.0.1:8080/v2/golang/tags/list [ "$status" -eq 0 ] [ $(echo "${lines[-1]}" | jq '.tags[]') = '"1.20"' ] - run ${ZLI_PATH} cve ${REGISTRY_NAME} -I golang:1.20 + run ${ZLI_PATH} cve list golang:1.20 --config ${REGISTRY_NAME} [ "$status" -eq 0 ] - found=0 + echo ${lines[@]} + + found=0 for i in "${lines[@]}" do