zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/cli/client/config_cmd.go (about)

     1  //go:build search
     2  // +build search
     3  
     4  package client
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"path"
    11  	"strconv"
    12  	"strings"
    13  	"text/tabwriter"
    14  
    15  	jsoniter "github.com/json-iterator/go"
    16  	"github.com/spf13/cobra"
    17  
    18  	zerr "zotregistry.dev/zot/errors"
    19  )
    20  
    21  const (
    22  	defaultConfigPerms = 0o644
    23  	defaultFilePerms   = 0o600
    24  )
    25  
    26  func NewConfigCommand() *cobra.Command {
    27  	var isListing bool
    28  
    29  	var isReset bool
    30  
    31  	configCmd := &cobra.Command{
    32  		Use:     "config <config-name> [variable] [value]",
    33  		Example: examples,
    34  		Short:   "Configure zot registry parameters for CLI",
    35  		Long:    `Configure zot registry parameters for CLI`,
    36  		Args:    cobra.ArbitraryArgs,
    37  		RunE: func(cmd *cobra.Command, args []string) error {
    38  			home, err := os.UserHomeDir()
    39  			if err != nil {
    40  				return err
    41  			}
    42  
    43  			configPath := path.Join(home, "/.zot")
    44  			switch len(args) {
    45  			case noArgs:
    46  				if isListing { // zot config -l
    47  					res, err := getConfigNames(configPath)
    48  					if err != nil {
    49  						return err
    50  					}
    51  
    52  					fmt.Fprint(cmd.OutOrStdout(), res)
    53  
    54  					return nil
    55  				}
    56  
    57  				return zerr.ErrInvalidArgs
    58  			case oneArg:
    59  				// zot config <name> -l
    60  				if isListing {
    61  					res, err := getAllConfig(configPath, args[0])
    62  					if err != nil {
    63  						return err
    64  					}
    65  
    66  					fmt.Fprint(cmd.OutOrStdout(), res)
    67  
    68  					return nil
    69  				}
    70  
    71  				return zerr.ErrInvalidArgs
    72  			case twoArgs:
    73  				if isReset { // zot config <name> <key> --reset
    74  					return resetConfigValue(configPath, args[0], args[1])
    75  				}
    76  				// zot config <name> <key>
    77  				res, err := getConfigValue(configPath, args[0], args[1])
    78  				if err != nil {
    79  					return err
    80  				}
    81  				fmt.Fprintln(cmd.OutOrStdout(), res)
    82  			case threeArgs:
    83  				// zot config <name> <key> <value>
    84  				if err := setConfigValue(configPath, args[0], args[1], args[2]); err != nil {
    85  					return err
    86  				}
    87  
    88  			default:
    89  				return zerr.ErrInvalidArgs
    90  			}
    91  
    92  			return nil
    93  		},
    94  	}
    95  
    96  	configCmd.Flags().BoolVarP(&isListing, "list", "l", false, "List configurations")
    97  	configCmd.Flags().BoolVar(&isReset, "reset", false, "Reset a variable value")
    98  	configCmd.SetUsageTemplate(configCmd.UsageTemplate() + supportedOptions)
    99  	configCmd.AddCommand(NewConfigAddCommand())
   100  	configCmd.AddCommand(NewConfigRemoveCommand())
   101  
   102  	return configCmd
   103  }
   104  
   105  func NewConfigAddCommand() *cobra.Command {
   106  	configAddCmd := &cobra.Command{
   107  		Use:     "add <config-name> <url>",
   108  		Example: "  zli config add main https://zot-foo.com:8080",
   109  		Short:   "Add configuration for a zot registry",
   110  		Long:    "Add configuration for a zot registry",
   111  		Args:    cobra.ExactArgs(twoArgs),
   112  		RunE: func(cmd *cobra.Command, args []string) error {
   113  			home, err := os.UserHomeDir()
   114  			if err != nil {
   115  				return err
   116  			}
   117  
   118  			configPath := path.Join(home, "/.zot")
   119  			// zot config add <config-name> <url>
   120  			err = addConfig(configPath, args[0], args[1])
   121  			if err != nil {
   122  				return err
   123  			}
   124  
   125  			return nil
   126  		},
   127  	}
   128  
   129  	// Prevent parent template from overwriting default template
   130  	configAddCmd.SetUsageTemplate(configAddCmd.UsageTemplate())
   131  
   132  	return configAddCmd
   133  }
   134  
   135  func NewConfigRemoveCommand() *cobra.Command {
   136  	configRemoveCmd := &cobra.Command{
   137  		Use:     "remove <config-name>",
   138  		Example: "  zli config remove main",
   139  		Short:   "Remove configuration for a zot registry",
   140  		Long:    "Remove configuration for a zot registry",
   141  		Args:    cobra.ExactArgs(oneArg),
   142  		RunE: func(cmd *cobra.Command, args []string) error {
   143  			home, err := os.UserHomeDir()
   144  			if err != nil {
   145  				return err
   146  			}
   147  
   148  			configPath := path.Join(home, "/.zot")
   149  			// zot config add <config-name> <url>
   150  			err = removeConfig(configPath, args[0])
   151  			if err != nil {
   152  				return err
   153  			}
   154  
   155  			return nil
   156  		},
   157  	}
   158  
   159  	// Prevent parent template from overwriting default template
   160  	configRemoveCmd.SetUsageTemplate(configRemoveCmd.UsageTemplate())
   161  
   162  	return configRemoveCmd
   163  }
   164  
   165  func getConfigMapFromFile(filePath string) ([]interface{}, error) {
   166  	file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, defaultConfigPerms)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	file.Close()
   172  
   173  	data, err := os.ReadFile(filePath)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	var jsonMap map[string]interface{}
   179  
   180  	json := jsoniter.ConfigCompatibleWithStandardLibrary
   181  
   182  	_ = json.Unmarshal(data, &jsonMap)
   183  
   184  	if jsonMap["configs"] == nil {
   185  		return nil, zerr.ErrEmptyJSON
   186  	}
   187  
   188  	configs, ok := jsonMap["configs"].([]interface{})
   189  	if !ok {
   190  		return nil, zerr.ErrCliBadConfig
   191  	}
   192  
   193  	return configs, nil
   194  }
   195  
   196  func saveConfigMapToFile(filePath string, configMap []interface{}) error {
   197  	json := jsoniter.ConfigCompatibleWithStandardLibrary
   198  
   199  	listMap := make(map[string]interface{})
   200  	listMap["configs"] = configMap
   201  
   202  	marshalled, err := json.MarshalIndent(&listMap, "", "  ")
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	if err := os.WriteFile(filePath, marshalled, defaultFilePerms); err != nil {
   208  		return err
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func getConfigNames(configPath string) (string, error) {
   215  	configs, err := getConfigMapFromFile(configPath)
   216  	if err != nil {
   217  		if errors.Is(err, zerr.ErrEmptyJSON) {
   218  			return "", nil
   219  		}
   220  
   221  		return "", err
   222  	}
   223  
   224  	var builder strings.Builder
   225  
   226  	writer := tabwriter.NewWriter(&builder, 0, 8, 1, '\t', tabwriter.AlignRight) //nolint:gomnd
   227  
   228  	for _, val := range configs {
   229  		configMap, ok := val.(map[string]interface{})
   230  		if !ok {
   231  			return "", zerr.ErrBadConfig
   232  		}
   233  
   234  		fmt.Fprintf(writer, "%s\t%s\n", configMap[nameKey], configMap["url"])
   235  	}
   236  
   237  	err = writer.Flush()
   238  	if err != nil {
   239  		return "", err
   240  	}
   241  
   242  	return builder.String(), nil
   243  }
   244  
   245  func addConfig(configPath, configName, url string) error {
   246  	configs, err := getConfigMapFromFile(configPath)
   247  	if err != nil && !errors.Is(err, zerr.ErrEmptyJSON) {
   248  		return err
   249  	}
   250  
   251  	if err := validateURL(url); err != nil {
   252  		return err
   253  	}
   254  
   255  	if configNameExists(configs, configName) {
   256  		return zerr.ErrDuplicateConfigName
   257  	}
   258  
   259  	configMap := make(map[string]interface{})
   260  	configMap["url"] = url
   261  	configMap[nameKey] = configName
   262  	addDefaultConfigs(configMap)
   263  	configs = append(configs, configMap)
   264  
   265  	err = saveConfigMapToFile(configPath, configs)
   266  	if err != nil {
   267  		return err
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  func removeConfig(configPath, configName string) error {
   274  	configs, err := getConfigMapFromFile(configPath)
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	for i, val := range configs {
   280  		configMap, ok := val.(map[string]interface{})
   281  		if !ok {
   282  			return zerr.ErrBadConfig
   283  		}
   284  
   285  		name := configMap[nameKey]
   286  		if name != configName {
   287  			continue
   288  		}
   289  
   290  		// Remove config from the config list before saving
   291  		newConfigs := configs[:i]
   292  		newConfigs = append(newConfigs, configs[i+1:]...)
   293  
   294  		err = saveConfigMapToFile(configPath, newConfigs)
   295  		if err != nil {
   296  			return err
   297  		}
   298  
   299  		return nil
   300  	}
   301  
   302  	return zerr.ErrConfigNotFound
   303  }
   304  
   305  func addDefaultConfigs(config map[string]interface{}) {
   306  	if _, ok := config[showspinnerConfig]; !ok {
   307  		config[showspinnerConfig] = true
   308  	}
   309  
   310  	if _, ok := config[verifyTLSConfig]; !ok {
   311  		config[verifyTLSConfig] = true
   312  	}
   313  }
   314  
   315  func getConfigValue(configPath, configName, key string) (string, error) {
   316  	configs, err := getConfigMapFromFile(configPath)
   317  	if err != nil {
   318  		if errors.Is(err, zerr.ErrEmptyJSON) {
   319  			return "", zerr.ErrConfigNotFound
   320  		}
   321  
   322  		return "", err
   323  	}
   324  
   325  	for _, val := range configs {
   326  		configMap, ok := val.(map[string]interface{})
   327  		if !ok {
   328  			return "", zerr.ErrBadConfig
   329  		}
   330  
   331  		addDefaultConfigs(configMap)
   332  
   333  		name := configMap[nameKey]
   334  		if name == configName {
   335  			if configMap[key] == nil {
   336  				return "", nil
   337  			}
   338  
   339  			return fmt.Sprintf("%v", configMap[key]), nil
   340  		}
   341  	}
   342  
   343  	return "", zerr.ErrConfigNotFound
   344  }
   345  
   346  func resetConfigValue(configPath, configName, key string) error {
   347  	if key == "url" || key == nameKey {
   348  		return zerr.ErrCannotResetConfigKey
   349  	}
   350  
   351  	configs, err := getConfigMapFromFile(configPath)
   352  	if err != nil {
   353  		if errors.Is(err, zerr.ErrEmptyJSON) {
   354  			return zerr.ErrConfigNotFound
   355  		}
   356  
   357  		return err
   358  	}
   359  
   360  	for _, val := range configs {
   361  		configMap, ok := val.(map[string]interface{})
   362  		if !ok {
   363  			return zerr.ErrBadConfig
   364  		}
   365  
   366  		addDefaultConfigs(configMap)
   367  
   368  		name := configMap[nameKey]
   369  		if name == configName {
   370  			delete(configMap, key)
   371  
   372  			err = saveConfigMapToFile(configPath, configs)
   373  			if err != nil {
   374  				return err
   375  			}
   376  
   377  			return nil
   378  		}
   379  	}
   380  
   381  	return zerr.ErrConfigNotFound
   382  }
   383  
   384  func setConfigValue(configPath, configName, key, value string) error {
   385  	if key == nameKey {
   386  		return zerr.ErrIllegalConfigKey
   387  	}
   388  
   389  	configs, err := getConfigMapFromFile(configPath)
   390  	if err != nil {
   391  		if errors.Is(err, zerr.ErrEmptyJSON) {
   392  			return zerr.ErrConfigNotFound
   393  		}
   394  
   395  		return err
   396  	}
   397  
   398  	for _, val := range configs {
   399  		configMap, ok := val.(map[string]interface{})
   400  		if !ok {
   401  			return zerr.ErrBadConfig
   402  		}
   403  
   404  		addDefaultConfigs(configMap)
   405  
   406  		name := configMap[nameKey]
   407  		if name == configName {
   408  			boolVal, err := strconv.ParseBool(value)
   409  			if err == nil {
   410  				configMap[key] = boolVal
   411  			} else {
   412  				configMap[key] = value
   413  			}
   414  
   415  			err = saveConfigMapToFile(configPath, configs)
   416  			if err != nil {
   417  				return err
   418  			}
   419  
   420  			return nil
   421  		}
   422  	}
   423  
   424  	return zerr.ErrConfigNotFound
   425  }
   426  
   427  func getAllConfig(configPath, configName string) (string, error) {
   428  	configs, err := getConfigMapFromFile(configPath)
   429  	if err != nil {
   430  		if errors.Is(err, zerr.ErrEmptyJSON) {
   431  			return "", nil
   432  		}
   433  
   434  		return "", err
   435  	}
   436  
   437  	var builder strings.Builder
   438  
   439  	for _, value := range configs {
   440  		configMap, ok := value.(map[string]interface{})
   441  		if !ok {
   442  			return "", zerr.ErrBadConfig
   443  		}
   444  
   445  		addDefaultConfigs(configMap)
   446  
   447  		name := configMap[nameKey]
   448  		if name == configName {
   449  			for key, val := range configMap {
   450  				if key == nameKey {
   451  					continue
   452  				}
   453  
   454  				fmt.Fprintf(&builder, "%s = %v\n", key, val)
   455  			}
   456  
   457  			return builder.String(), nil
   458  		}
   459  	}
   460  
   461  	return "", zerr.ErrConfigNotFound
   462  }
   463  
   464  func configNameExists(configs []interface{}, configName string) bool {
   465  	for _, val := range configs {
   466  		configMap, ok := val.(map[string]interface{})
   467  		if !ok {
   468  			return false
   469  		}
   470  
   471  		if configMap[nameKey] == configName {
   472  			return true
   473  		}
   474  	}
   475  
   476  	return false
   477  }
   478  
   479  const (
   480  	examples = `  zli config add main https://zot-foo.com:8080
   481    zli config --list
   482    zli config main url
   483    zli config main --list
   484    zli config remove main`
   485  
   486  	supportedOptions = `
   487  Useful variables:
   488    url		zot server URL
   489    showspinner	show spinner while loading data [true/false]
   490    verify-tls	enable TLS certificate verification of the server [default: true]
   491  `
   492  
   493  	nameKey = "_name"
   494  
   495  	noArgs    = 0
   496  	oneArg    = 1
   497  	twoArgs   = 2
   498  	threeArgs = 3
   499  
   500  	showspinnerConfig = "showspinner"
   501  	verifyTLSConfig   = "verify-tls"
   502  )