From 111b80625d823b4cd947d1312e9ed83e2a1938a6 Mon Sep 17 00:00:00 2001 From: Lisca Ana-Roberta Date: Thu, 2 Jun 2022 17:14:21 +0300 Subject: [PATCH] added repos command to list repositories Signed-off-by: Lisca Ana-Roberta --- pkg/cli/cli.go | 1 + pkg/cli/config_cmd_test.go | 57 +++++++++++ pkg/cli/image_cmd_test.go | 204 +++++++++++++++++++++++++++++++++++++ pkg/cli/repo_cmd.go | 106 +++++++++++++++++++ pkg/cli/searcher.go | 26 +++++ pkg/cli/service.go | 38 +++++++ 6 files changed, 432 insertions(+) create mode 100644 pkg/cli/repo_cmd.go diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 1ed23284..7980aa8b 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -9,4 +9,5 @@ func enableCli(rootCmd *cobra.Command) { rootCmd.AddCommand(NewConfigCommand()) rootCmd.AddCommand(NewImageCommand(NewSearchService())) rootCmd.AddCommand(NewCveCommand(NewSearchService())) + rootCmd.AddCommand(NewRepoCommand(NewSearchService())) } diff --git a/pkg/cli/config_cmd_test.go b/pkg/cli/config_cmd_test.go index c98e6fc7..a72a61c0 100644 --- a/pkg/cli/config_cmd_test.go +++ b/pkg/cli/config_cmd_test.go @@ -6,6 +6,7 @@ package cli //nolint:testpackage import ( "bytes" "io/ioutil" + "log" "os" "strings" "testing" @@ -78,6 +79,62 @@ func TestConfigCmdMain(t *testing.T) { So(actualStr, ShouldContainSubstring, "https://test-url.com") }) + Convey("Test error on home directory", t, func() { + args := []string{"add", "configtest1", "https://test-url.com"} + file := makeConfigFile("") + defer os.Remove(file) + + err := os.Setenv("HOME", "nonExistentDirectory") + if err != nil { + panic(err) + } + + cmd := NewConfigCommand() + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + err = os.Setenv("HOME", home) + if err != nil { + log.Fatal(err) + } + }) + + Convey("Test error on home directory at new add config", t, func() { + args := []string{"add", "configtest1", "https://test-url.com"} + file := makeConfigFile("") + defer os.Remove(file) + + err := os.Setenv("HOME", "nonExistentDirectory") + if err != nil { + panic(err) + } + + cmd := NewConfigAddCommand() + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + err = os.Setenv("HOME", home) + if err != nil { + log.Fatal(err) + } + }) + Convey("Test add config with invalid format", t, func() { args := []string{"--list"} configPath := makeConfigFile(`{"configs":{"_name":"configtest","url":"https://test-url.com","showspinner":false}}`) diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index cf09ca12..ad3a74a1 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "log" "os" "path" "regexp" @@ -69,6 +70,35 @@ func TestSearchImageCmd(t *testing.T) { So(err, ShouldEqual, zotErrors.ErrNoURLProvided) }) + Convey("Test image invalid home directory", t, func() { + args := []string{"imagetest", "--name", "dummyImageName"} + + configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) + defer os.Remove(configPath) + + err := os.Setenv("HOME", "nonExistentDirectory") + if err != nil { + panic(err) + } + + cmd := NewImageCommand(new(mockService)) + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + err = os.Setenv("HOME", home) + if err != nil { + log.Fatal(err) + } + }) + Convey("Test image no params", t, func() { args := []string{"imagetest", "--url", "someUrl"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) @@ -187,6 +217,145 @@ func TestSearchImageCmd(t *testing.T) { }) } +func TestListRepos(t *testing.T) { + 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() + So(err, ShouldBeNil) + }) + + Convey("Test error on home directory", t, func() { + args := []string{"config-test"} + + configPath := makeConfigFile(`{"configs":[{"_name":"config-test","url":"https://test-url.com","showspinner":false}]}`) + defer os.Remove(configPath) + + err := os.Setenv("HOME", "nonExistentDirectory") + if err != nil { + panic(err) + } + + cmd := NewRepoCommand(new(mockService)) + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldNotBeNil) + + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + err = os.Setenv("HOME", home) + if err != nil { + log.Fatal(err) + } + }) + + Convey("Test listing repositories error", t, func() { + args := []string{"config-test"} + + configPath := makeConfigFile(`{"configs":[{"_name":"config-test", + "url":"https://invalid.invalid","showspinner":false}]}`) + defer os.Remove(configPath) + + cmd := NewRepoCommand(new(searchService)) + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err := cmd.Execute() + So(err, ShouldNotBeNil) + }) + + Convey("Test unable to get config value", t, func() { + args := []string{"config-test-inexistent"} + + 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() + So(err, ShouldNotBeNil) + }) + + 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 + + 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 - spinner config invalid", t, func() { + args := []string{"config-test"} + + configPath := makeConfigFile(`{"configs":[{"_name":"config-test", + "url":"https://test-url.com","showspinner":invalid}]}`) + 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 - verifyTLSConfig fails", t, func() { + args := []string{"config-test"} + + configPath := makeConfigFile(`{"configs":[{"_name":"config-test", + "verify-tls":"invalid", "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() + So(err, ShouldNotBeNil) + }) +} + func TestOutputFormat(t *testing.T) { Convey("Test text", t, func() { args := []string{"imagetest", "--name", "dummyImageName", "-o", "text"} @@ -455,6 +624,27 @@ func TestServerResponse(t *testing.T) { actual := buff.String() So(actual, ShouldContainSubstring, "unknown") }) + + Convey("Test list repos error", func() { + args := []string{"config-test"} + + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"config-test", + "url":"%s","showspinner":false}]}`, url)) + defer os.Remove(configPath) + + cmd := NewRepoCommand(new(searchService)) + buff := &bytes.Buffer{} + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + + So(actual, ShouldContainSubstring, "REPOSITORY NAME") + So(actual, ShouldContainSubstring, "repo7") + }) }) } @@ -537,6 +727,20 @@ func uploadManifest(url string) error { type mockService struct{} +func (service mockService) getRepos(ctx context.Context, config searchConfig, username, + password string, channel chan stringResult, wtgrp *sync.WaitGroup, +) { + defer wtgrp.Done() + defer close(channel) + + var catalog [3]string + catalog[0] = "python" + catalog[1] = "busybox" + catalog[2] = "hello-world" + + channel <- stringResult{"", nil} +} + func (service mockService) getAllImages(ctx context.Context, config searchConfig, username, password string, channel chan stringResult, wtgrp *sync.WaitGroup, ) { diff --git a/pkg/cli/repo_cmd.go b/pkg/cli/repo_cmd.go new file mode 100644 index 00000000..e8bd8093 --- /dev/null +++ b/pkg/cli/repo_cmd.go @@ -0,0 +1,106 @@ +//go:build extended +// +build extended + +package cli + +import ( + "os" + "path" + + "github.com/briandowns/spinner" + "github.com/spf13/cobra" + zotErrors "zotregistry.io/zot/errors" +) + +func NewRepoCommand(searchService SearchService) *cobra.Command { + var servURL, user, outputFormat string + + var isSpinner, verifyTLS, verbose bool + + repoCmd := &cobra.Command{ + Use: "repos [config-name]", + Short: "List all repositories", + Long: `List all repositories`, + 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 zotErrors.ErrNoURLProvided + } + + servURL = urlFromConfig + } else { + return zotErrors.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 = "Searching... " + + searchConfig := searchConfig{ + searchService: searchService, + servURL: &servURL, + user: &user, + outputFormat: &outputFormat, + verbose: &verbose, + 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.Flags().StringVar(&servURL, "url", "", "Specify zot server URL if config-name is not mentioned") + repoCmd.Flags().StringVarP(&user, "user", "u", "", `User Credentials of zot server in "username:password" format`) + + return repoCmd +} + +func listRepos(searchConfig searchConfig) error { + searcher := new(repoSearcher) + err := searcher.searchRepos(searchConfig) + + return err +} diff --git a/pkg/cli/searcher.go b/pkg/cli/searcher.go index 32f3a7ff..a4870507 100644 --- a/pkg/cli/searcher.go +++ b/pkg/cli/searcher.go @@ -473,3 +473,29 @@ 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 + } +} diff --git a/pkg/cli/service.go b/pkg/cli/service.go index 78000f40..c2a6862e 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -36,6 +36,8 @@ type SearchService interface { channel chan stringResult, wtgrp *sync.WaitGroup) getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cvid string, channel chan stringResult, wtgrp *sync.WaitGroup) + getRepos(ctx context.Context, config searchConfig, username, password string, + channel chan stringResult, wtgrp *sync.WaitGroup) } type searchService struct{} @@ -841,6 +843,42 @@ func getCVETableWriter(writer io.Writer) *tablewriter.Table { return table } +func (service searchService) getRepos(ctx context.Context, config searchConfig, username, password string, + rch chan stringResult, wtgrp *sync.WaitGroup, +) { + defer wtgrp.Done() + defer close(rch) + + catalog := &catalogResponse{} + + catalogEndPoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("%s%s", + constants.RoutePrefix, constants.ExtCatalogPrefix)) + if err != nil { + if isContextDone(ctx) { + return + } + rch <- stringResult{"", err} + + return + } + + _, err = makeGETRequest(ctx, catalogEndPoint, username, password, *config.verifyTLS, catalog) + if err != nil { + if isContextDone(ctx) { + return + } + rch <- stringResult{"", err} + + return + } + + fmt.Fprintln(config.resultWriter, "\n\nREPOSITORY NAME") + + for _, repo := range catalog.Repositories { + fmt.Fprintln(config.resultWriter, repo) + } +} + const ( imageNameWidth = 32 tagWidth = 24