From 6285a730a1cdf745480ab303d06c51d93c69111a Mon Sep 17 00:00:00 2001 From: Tanmay Naik Date: Fri, 17 Jul 2020 15:42:22 -0400 Subject: [PATCH] cli: add option to ignore TLS verification adds a property in config : "verify-tls" --- pkg/cli/client.go | 25 ++++++++++----- pkg/cli/config_cmd.go | 21 ++++++++++++- pkg/cli/image_cmd.go | 54 +++++++++++++++++--------------- pkg/cli/image_cmd_test.go | 12 ++++---- pkg/cli/searcher.go | 65 +++++++++++++++++++++++---------------- pkg/cli/service.go | 61 +++++++++++++++++++++--------------- 6 files changed, 148 insertions(+), 90 deletions(-) diff --git a/pkg/cli/client.go b/pkg/cli/client.go index 9812e738..06394574 100644 --- a/pkg/cli/client.go +++ b/pkg/cli/client.go @@ -2,6 +2,7 @@ package cli import ( "context" + "crypto/tls" "encoding/json" "errors" "io/ioutil" @@ -14,17 +15,23 @@ import ( zotErrors "github.com/anuvu/zot/errors" ) -var httpClient *http.Client = createHTTPClient() //nolint: gochecknoglobals +var httpClient *http.Client //nolint: gochecknoglobals const httpTimeout = 5 * time.Second -func createHTTPClient() *http.Client { +func createHTTPClient(verifyTLS bool) *http.Client { + var tr = http.DefaultTransport.(*http.Transport).Clone() + if !verifyTLS { + tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint: gosec + } + return &http.Client{ - Timeout: httpTimeout, + Timeout: httpTimeout, + Transport: tr, } } -func makeGETRequest(url, username, password string, resultsPtr interface{}) (http.Header, error) { +func makeGETRequest(url, username, password string, verifyTLS bool, resultsPtr interface{}) (http.Header, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { @@ -33,6 +40,10 @@ func makeGETRequest(url, username, password string, resultsPtr interface{}) (htt req.SetBasicAuth(username, password) + if httpClient == nil { + httpClient = createHTTPClient(verifyTLS) + } + resp, err := httpClient.Do(req) if err != nil { return nil, err @@ -74,9 +85,9 @@ type manifestJob struct { url string username string password string - outputFormat string imageName string tagName string + config searchConfig manifestResp manifestResponse } @@ -116,7 +127,7 @@ func (p *requestsPool) startRateLimiter() { func (p *requestsPool) doJob(job *manifestJob) { defer p.waitGroup.Done() - header, err := makeGETRequest(job.url, job.username, job.password, &job.manifestResp) + header, err := makeGETRequest(job.url, job.username, job.password, *job.config.verifyTLS, &job.manifestResp) if err != nil { if isContextDone(p.context) { return @@ -143,7 +154,7 @@ func (p *requestsPool) doJob(job *manifestJob) { }, } - str, err := image.string(job.outputFormat) + str, err := image.string(*job.config.outputFormat) if err != nil { if isContextDone(p.context) { return diff --git a/pkg/cli/config_cmd.go b/pkg/cli/config_cmd.go index 501c8551..b7a53a15 100644 --- a/pkg/cli/config_cmd.go +++ b/pkg/cli/config_cmd.go @@ -205,6 +205,7 @@ func addConfig(configPath, configName, url string) error { configMap := make(map[string]interface{}) configMap["url"] = url configMap[nameKey] = configName + addDefaultConfigs(configMap) configs = append(configs, configMap) err = saveConfigMapToFile(configPath, configs) @@ -215,6 +216,16 @@ func addConfig(configPath, configName, url string) error { return nil } +func addDefaultConfigs(config map[string]interface{}) { + if _, ok := config[showspinnerConfig]; !ok { + config[showspinnerConfig] = true + } + + if _, ok := config[verifyTLSConfig]; !ok { + config[verifyTLSConfig] = true + } +} + func getConfigValue(configPath, configName, key string) (string, error) { configs, err := getConfigMapFromFile(configPath) if err != nil { @@ -227,6 +238,7 @@ func getConfigValue(configPath, configName, key string) (string, error) { for _, val := range configs { configMap := val.(map[string]interface{}) + addDefaultConfigs(configMap) name := configMap[nameKey] if name == configName { @@ -257,6 +269,7 @@ func resetConfigValue(configPath, configName, key string) error { for _, val := range configs { configMap := val.(map[string]interface{}) + addDefaultConfigs(configMap) name := configMap[nameKey] if name == configName { @@ -290,6 +303,7 @@ func setConfigValue(configPath, configName, key, value string) error { for _, val := range configs { configMap := val.(map[string]interface{}) + addDefaultConfigs(configMap) name := configMap[nameKey] if name == configName { @@ -326,6 +340,7 @@ func getAllConfig(configPath, configName string) (string, error) { for _, value := range configs { configMap := value.(map[string]interface{}) + addDefaultConfigs(configMap) name := configMap[nameKey] if name == configName { @@ -353,7 +368,8 @@ const ( supportedOptions = ` Useful variables: url zot server URL - showspinner show spinner while loading data [true/false]` + showspinner show spinner while loading data [true/false] + verify-tls verify TLS Certificate verification of the server [default: true]` nameKey = "_name" @@ -361,6 +377,9 @@ Useful variables: oneArg = 1 twoArgs = 2 threeArgs = 3 + + showspinnerConfig = "showspinner" + verifyTLSConfig = "verify-tls" ) var ( diff --git a/pkg/cli/image_cmd.go b/pkg/cli/image_cmd.go index 4edf7c3a..9e527747 100644 --- a/pkg/cli/image_cmd.go +++ b/pkg/cli/image_cmd.go @@ -14,11 +14,9 @@ import ( func NewImageCommand(searchService ImageSearchService) *cobra.Command { searchImageParams := make(map[string]*string) - var servURL string + var servURL, user, outputFormat string - var user string - - var outputFormat string + var isSpinner, verifyTLS bool var imageCmd = &cobra.Command{ Use: "images [config-name]", @@ -47,20 +45,35 @@ func NewImageCommand(searchService ImageSearchService) *cobra.Command { } } - var isSpinner bool - if len(args) > 0 { var err error - isSpinner, err = isSpinnerEnabled(configPath, args[0]) + 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 } - } else { - isSpinner = true } - err = searchImage(cmd, searchImageParams, searchService, &servURL, &user, &outputFormat, isSpinner) + spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr())) + spin.Prefix = "Searching... " + + searchConfig := searchConfig{ + params: searchImageParams, + searchService: searchService, + servURL: &servURL, + user: &user, + outputFormat: &outputFormat, + spinner: spinnerState{spin, isSpinner}, + verifyTLS: &verifyTLS, + resultWriter: cmd.OutOrStdout(), + } + + err = searchImage(searchConfig) if err != nil { cmd.SilenceUsage = true @@ -77,22 +90,18 @@ func NewImageCommand(searchService ImageSearchService) *cobra.Command { return imageCmd } -func isSpinnerEnabled(configPath, configName string) (bool, error) { - spinnerConfig, err := getConfigValue(configPath, configName, "showspinner") +func parseBooleanConfig(configPath, configName, configParam string) (bool, error) { + config, err := getConfigValue(configPath, configName, configParam) if err != nil { return false, err } - if spinnerConfig == "" { - return true, nil // spinner is enabled by default - } - - isSpinner, err := strconv.ParseBool(spinnerConfig) + val, err := strconv.ParseBool(config) if err != nil { return false, err } - return isSpinner, nil + return val, nil } func setupCmdFlags(imageCmd *cobra.Command, searchImageParams map[string]*string, servURL, user, outputFormat *string) { @@ -103,14 +112,9 @@ func setupCmdFlags(imageCmd *cobra.Command, searchImageParams map[string]*string imageCmd.Flags().StringVarP(outputFormat, "output", "o", "", "Specify output format [text/json/yaml]") } -func searchImage(cmd *cobra.Command, params map[string]*string, - service ImageSearchService, servURL, user, outputFormat *string, isSpinner bool) error { - spin := spinner.New(spinner.CharSets[39], spinnerDuration, spinner.WithWriter(cmd.ErrOrStderr())) - spin.Prefix = "Searching... " - +func searchImage(searchConfig searchConfig) error { for _, searcher := range getSearchers() { - found, err := searcher.search(params, service, servURL, user, outputFormat, - cmd.OutOrStdout(), spinnerState{spin, isSpinner}) + found, err := searcher.search(searchConfig) if found { if err != nil { return err diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index 3096390b..6c92df8b 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -439,8 +439,8 @@ func uploadManifest(url string) { type mockService struct{} -func (service mockService) getAllImages(ctx context.Context, serverURL, username, password, - outputFormat string, channel chan imageListResult, wg *sync.WaitGroup) { +func (service mockService) getAllImages(ctx context.Context, config searchConfig, username, password string, + channel chan imageListResult, wg *sync.WaitGroup) { defer wg.Done() image := &imageStruct{} @@ -453,7 +453,7 @@ func (service mockService) getAllImages(ctx context.Context, serverURL, username }, } - str, err := image.string(outputFormat) + str, err := image.string(*config.outputFormat) if err != nil { channel <- imageListResult{"", err} return @@ -461,8 +461,8 @@ func (service mockService) getAllImages(ctx context.Context, serverURL, username channel <- imageListResult{str, nil} } -func (service mockService) getImageByName(ctx context.Context, serverURL, username, password, - imageName, outputFormat string, channel chan imageListResult, wg *sync.WaitGroup) { +func (service mockService) getImageByName(ctx context.Context, config searchConfig, + username, password, imageName string, channel chan imageListResult, wg *sync.WaitGroup) { defer wg.Done() image := &imageStruct{} @@ -475,7 +475,7 @@ func (service mockService) getImageByName(ctx context.Context, serverURL, userna }, } - str, err := image.string(outputFormat) + str, err := image.string(*config.outputFormat) if err != nil { channel <- imageListResult{"", err} return diff --git a/pkg/cli/searcher.go b/pkg/cli/searcher.go index 68fb69a2..ae01d988 100644 --- a/pkg/cli/searcher.go +++ b/pkg/cli/searcher.go @@ -22,8 +22,7 @@ func getSearchers() []searcher { } type searcher interface { - search(params map[string]*string, searchService ImageSearchService, - servURL, user, outputFormat *string, stdWriter io.Writer, spinner spinnerState) (bool, error) + search(searchConfig searchConfig) (bool, error) } func canSearch(params map[string]*string, requiredParams *set) bool { @@ -38,15 +37,25 @@ func canSearch(params map[string]*string, requiredParams *set) bool { return true } +type searchConfig struct { + params map[string]*string + searchService ImageSearchService + servURL *string + user *string + outputFormat *string + verifyTLS *bool + resultWriter io.Writer + spinner spinnerState +} + type allImagesSearcher struct{} -func (search allImagesSearcher) search(params map[string]*string, searchService ImageSearchService, - servURL, user, outputFormat *string, stdWriter io.Writer, spinner spinnerState) (bool, error) { - if !canSearch(params, newSet("")) { +func (search allImagesSearcher) search(config searchConfig) (bool, error) { + if !canSearch(config.params, newSet("")) { return false, nil } - username, password := getUsernameAndPassword(*user) + username, password := getUsernameAndPassword(*config.user) imageErr := make(chan imageListResult) ctx, cancel := context.WithCancel(context.Background()) @@ -54,12 +63,12 @@ func (search allImagesSearcher) search(params map[string]*string, searchService wg.Add(1) - go searchService.getAllImages(ctx, *servURL, username, password, *outputFormat, imageErr, &wg) + go config.searchService.getAllImages(ctx, config, username, password, imageErr, &wg) wg.Add(1) var errCh chan error = make(chan error, 1) - go collectImages(outputFormat, stdWriter, &wg, imageErr, cancel, spinner, errCh) + go collectImages(config, &wg, imageErr, cancel, errCh) wg.Wait() select { case err := <-errCh: @@ -71,14 +80,12 @@ func (search allImagesSearcher) search(params map[string]*string, searchService type imageByNameSearcher struct{} -func (search imageByNameSearcher) search(params map[string]*string, - searchService ImageSearchService, servURL, user, outputFormat *string, - stdWriter io.Writer, spinner spinnerState) (bool, error) { - if !canSearch(params, newSet("imageName")) { +func (search imageByNameSearcher) search(config searchConfig) (bool, error) { + if !canSearch(config.params, newSet("imageName")) { return false, nil } - username, password := getUsernameAndPassword(*user) + username, password := getUsernameAndPassword(*config.user) imageErr := make(chan imageListResult) ctx, cancel := context.WithCancel(context.Background()) @@ -86,11 +93,11 @@ func (search imageByNameSearcher) search(params map[string]*string, wg.Add(1) - go searchService.getImageByName(ctx, *servURL, username, password, *params["imageName"], *outputFormat, imageErr, &wg) + go config.searchService.getImageByName(ctx, config, username, password, *config.params["imageName"], imageErr, &wg) wg.Add(1) var errCh chan error = make(chan error, 1) - go collectImages(outputFormat, stdWriter, &wg, imageErr, cancel, spinner, errCh) + go collectImages(config, &wg, imageErr, cancel, errCh) wg.Wait() @@ -102,38 +109,44 @@ func (search imageByNameSearcher) search(params map[string]*string, } } -func collectImages(outputFormat *string, stdWriter io.Writer, wg *sync.WaitGroup, - imageErr chan imageListResult, cancel context.CancelFunc, spinner spinnerState, errCh chan error) { +func collectImages(config searchConfig, wg *sync.WaitGroup, imageErr chan imageListResult, + cancel context.CancelFunc, errCh chan error) { var foundResult bool defer wg.Done() - spinner.startSpinner() + config.spinner.startSpinner() for { select { - case result := <-imageErr: + case result, ok := <-imageErr: + config.spinner.stopSpinner() + + if !ok { + cancel() + return + } + if result.Err != nil { - spinner.stopSpinner() cancel() errCh <- result.Err return } - spinner.stopSpinner() - - if !foundResult && (*outputFormat == "text" || *outputFormat == "") { + if !foundResult && (*config.outputFormat == "text" || *config.outputFormat == "") { var builder strings.Builder printImageTableHeader(&builder) - fmt.Fprint(stdWriter, builder.String()) + fmt.Fprint(config.resultWriter, builder.String()) } foundResult = true - fmt.Fprint(stdWriter, result.StrValue) + fmt.Fprint(config.resultWriter, result.StrValue) case <-time.After(waitTimeout): cancel() + config.spinner.stopSpinner() + return } } @@ -211,5 +224,5 @@ func printImageTableHeader(writer io.Writer) { } const ( - waitTimeout = 2 * time.Second + waitTimeout = 6 * time.Second ) diff --git a/pkg/cli/service.go b/pkg/cli/service.go index d8b883d6..6dc841ab 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -17,9 +17,9 @@ import ( ) type ImageSearchService interface { - getAllImages(ctx context.Context, serverURL, username, password, - outputFormat string, channel chan imageListResult, wg *sync.WaitGroup) - getImageByName(ctx context.Context, serverURL, username, password, imageName, outputFormat string, + getAllImages(ctx context.Context, config searchConfig, username, password string, + channel chan imageListResult, wg *sync.WaitGroup) + getImageByName(ctx context.Context, config searchConfig, username, password, imageName string, channel chan imageListResult, wg *sync.WaitGroup) } type searchService struct{} @@ -28,27 +28,32 @@ func NewImageSearchService() ImageSearchService { return searchService{} } -func (service searchService) getImageByName(ctx context.Context, url, username, password, - imageName, outputFormat string, c chan imageListResult, wg *sync.WaitGroup) { +func (service searchService) getImageByName(ctx context.Context, config searchConfig, + username, password, imageName string, c chan imageListResult, wg *sync.WaitGroup) { defer wg.Done() + defer close(c) - p := newSmoothRateLimiter(ctx, wg, c) + var localWg sync.WaitGroup + p := newSmoothRateLimiter(ctx, &localWg, c) - wg.Add(1) + localWg.Add(1) go p.startRateLimiter() - wg.Add(1) + localWg.Add(1) - go getImage(ctx, url, username, password, imageName, outputFormat, c, wg, p) + go getImage(ctx, config, username, password, imageName, c, &localWg, p) + + localWg.Wait() } -func (service searchService) getAllImages(ctx context.Context, url, username, password, - outputFormat string, c chan imageListResult, wg *sync.WaitGroup) { +func (service searchService) getAllImages(ctx context.Context, config searchConfig, username, password string, + c chan imageListResult, wg *sync.WaitGroup) { defer wg.Done() + defer close(c) catalog := &catalogResponse{} - catalogEndPoint, err := combineServerAndEndpointURL(url, "/v2/_catalog") + catalogEndPoint, err := combineServerAndEndpointURL(*config.servURL, "/v2/_catalog") if err != nil { if isContextDone(ctx) { return @@ -58,7 +63,7 @@ func (service searchService) getAllImages(ctx context.Context, url, username, pa return } - _, err = makeGETRequest(catalogEndPoint, username, password, catalog) + _, err = makeGETRequest(catalogEndPoint, username, password, *config.verifyTLS, catalog) if err != nil { if isContextDone(ctx) { return @@ -68,23 +73,28 @@ func (service searchService) getAllImages(ctx context.Context, url, username, pa return } - p := newSmoothRateLimiter(ctx, wg, c) + var localWg sync.WaitGroup - wg.Add(1) + p := newSmoothRateLimiter(ctx, &localWg, c) + + localWg.Add(1) go p.startRateLimiter() for _, repo := range catalog.Repositories { - wg.Add(1) + localWg.Add(1) - go getImage(ctx, url, username, password, repo, outputFormat, c, wg, p) + go getImage(ctx, config, username, password, repo, c, &localWg, p) } + + localWg.Wait() } -func getImage(ctx context.Context, url, username, password, imageName, outputFormat string, + +func getImage(ctx context.Context, config searchConfig, username, password, imageName string, c chan imageListResult, wg *sync.WaitGroup, pool *requestsPool) { defer wg.Done() - tagListEndpoint, err := combineServerAndEndpointURL(url, fmt.Sprintf("/v2/%s/tags/list", imageName)) + tagListEndpoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/tags/list", imageName)) if err != nil { if isContextDone(ctx) { return @@ -95,7 +105,7 @@ func getImage(ctx context.Context, url, username, password, imageName, outputFor } tagsList := &tagListResp{} - _, err = makeGETRequest(tagListEndpoint, username, password, &tagsList) + _, err = makeGETRequest(tagListEndpoint, username, password, *config.verifyTLS, &tagsList) if err != nil { if isContextDone(ctx) { @@ -109,7 +119,7 @@ func getImage(ctx context.Context, url, username, password, imageName, outputFor for _, tag := range tagsList.Tags { wg.Add(1) - go addManifestCallToPool(ctx, pool, url, username, password, imageName, tag, outputFormat, c, wg) + go addManifestCallToPool(ctx, config, pool, username, password, imageName, tag, c, wg) } } @@ -122,13 +132,14 @@ func isContextDone(ctx context.Context) bool { } } -func addManifestCallToPool(ctx context.Context, p *requestsPool, url, username, password, imageName, - tagName, outputFormat string, c chan imageListResult, wg *sync.WaitGroup) { +func addManifestCallToPool(ctx context.Context, config searchConfig, p *requestsPool, username, password, imageName, + tagName string, c chan imageListResult, wg *sync.WaitGroup) { defer wg.Done() resultManifest := manifestResponse{} - manifestEndpoint, err := combineServerAndEndpointURL(url, fmt.Sprintf("/v2/%s/manifests/%s", imageName, tagName)) + manifestEndpoint, err := combineServerAndEndpointURL(*config.servURL, + fmt.Sprintf("/v2/%s/manifests/%s", imageName, tagName)) if err != nil { if isContextDone(ctx) { return @@ -143,7 +154,7 @@ func addManifestCallToPool(ctx context.Context, p *requestsPool, url, username, password: password, tagName: tagName, manifestResp: resultManifest, - outputFormat: outputFormat, + config: config, } wg.Add(1)