//go:build search // +build search package cli import ( "errors" "fmt" "os" "path" "strconv" "strings" "text/tabwriter" jsoniter "github.com/json-iterator/go" "github.com/spf13/cobra" zerr "zotregistry.io/zot/errors" ) const ( defaultConfigPerms = 0o644 defaultFilePerms = 0o600 ) func NewConfigCommand() *cobra.Command { var isListing bool var isReset bool configCmd := &cobra.Command{ Use: "config [variable] [value]", Example: examples, Short: "Configure zot registry parameters for CLI", Long: `Configure zot registry parameters for CLI`, Args: cobra.ArbitraryArgs, RunE: func(cmd *cobra.Command, args []string) error { home, err := os.UserHomeDir() if err != nil { panic(err) } configPath := path.Join(home + "/.zot") switch len(args) { case noArgs: if isListing { // zot config -l res, err := getConfigNames(configPath) if err != nil { return err } fmt.Fprint(cmd.OutOrStdout(), res) return nil } return zerr.ErrInvalidArgs case oneArg: // zot config -l if isListing { res, err := getAllConfig(configPath, args[0]) if err != nil { return err } fmt.Fprint(cmd.OutOrStdout(), res) return nil } return zerr.ErrInvalidArgs case twoArgs: if isReset { // zot config --reset return resetConfigValue(configPath, args[0], args[1]) } // zot config res, err := getConfigValue(configPath, args[0], args[1]) if err != nil { return err } fmt.Fprintln(cmd.OutOrStdout(), res) case threeArgs: // zot config if err := setConfigValue(configPath, args[0], args[1], args[2]); err != nil { return err } default: return zerr.ErrInvalidArgs } return nil }, } configCmd.Flags().BoolVarP(&isListing, "list", "l", false, "List configurations") configCmd.Flags().BoolVar(&isReset, "reset", false, "Reset a variable value") configCmd.SetUsageTemplate(configCmd.UsageTemplate() + supportedOptions) configCmd.AddCommand(NewConfigAddCommand()) return configCmd } func NewConfigAddCommand() *cobra.Command { configAddCmd := &cobra.Command{ Use: "add ", Short: "Add configuration for a zot registry", Long: "Add configuration for a zot registry", Args: cobra.ExactArgs(twoArgs), RunE: func(cmd *cobra.Command, args []string) error { home, err := os.UserHomeDir() if err != nil { panic(err) } configPath := path.Join(home + "/.zot") // zot config add err = addConfig(configPath, args[0], args[1]) if err != nil { return err } return nil }, } return configAddCmd } func getConfigMapFromFile(filePath string) ([]interface{}, error) { file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, defaultConfigPerms) if err != nil { return nil, err } file.Close() data, err := os.ReadFile(filePath) if err != nil { return nil, err } var jsonMap map[string]interface{} json := jsoniter.ConfigCompatibleWithStandardLibrary _ = json.Unmarshal(data, &jsonMap) if jsonMap["configs"] == nil { return nil, zerr.ErrEmptyJSON } configs, ok := jsonMap["configs"].([]interface{}) if !ok { return nil, zerr.ErrCliBadConfig } return configs, nil } func saveConfigMapToFile(filePath string, configMap []interface{}) error { json := jsoniter.ConfigCompatibleWithStandardLibrary listMap := make(map[string]interface{}) listMap["configs"] = configMap marshalled, err := json.Marshal(&listMap) if err != nil { return err } if err := os.WriteFile(filePath, marshalled, defaultFilePerms); err != nil { return err } return nil } func getConfigNames(configPath string) (string, error) { configs, err := getConfigMapFromFile(configPath) if err != nil { if errors.Is(err, zerr.ErrEmptyJSON) { return "", nil } return "", err } var builder strings.Builder writer := tabwriter.NewWriter(&builder, 0, 8, 1, '\t', tabwriter.AlignRight) //nolint:gomnd for _, val := range configs { configMap, ok := val.(map[string]interface{}) if !ok { return "", zerr.ErrBadConfig } fmt.Fprintf(writer, "%s\t%s\n", configMap[nameKey], configMap["url"]) } err = writer.Flush() if err != nil { return "", err } return builder.String(), nil } func addConfig(configPath, configName, url string) error { configs, err := getConfigMapFromFile(configPath) if err != nil && !errors.Is(err, zerr.ErrEmptyJSON) { return err } if !isURL(url) { return zerr.ErrInvalidURL } if configNameExists(configs, configName) { return zerr.ErrDuplicateConfigName } configMap := make(map[string]interface{}) configMap["url"] = url configMap[nameKey] = configName addDefaultConfigs(configMap) configs = append(configs, configMap) err = saveConfigMapToFile(configPath, configs) if err != nil { return err } 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 { if errors.Is(err, zerr.ErrEmptyJSON) { return "", zerr.ErrConfigNotFound } return "", err } for _, val := range configs { configMap, ok := val.(map[string]interface{}) if !ok { return "", zerr.ErrBadConfig } addDefaultConfigs(configMap) name := configMap[nameKey] if name == configName { if configMap[key] == nil { return "", nil } return fmt.Sprintf("%v", configMap[key]), nil } } return "", zerr.ErrConfigNotFound } func resetConfigValue(configPath, configName, key string) error { if key == "url" || key == nameKey { return zerr.ErrCannotResetConfigKey } configs, err := getConfigMapFromFile(configPath) if err != nil { if errors.Is(err, zerr.ErrEmptyJSON) { return zerr.ErrConfigNotFound } return err } for _, val := range configs { configMap, ok := val.(map[string]interface{}) if !ok { return zerr.ErrBadConfig } addDefaultConfigs(configMap) name := configMap[nameKey] if name == configName { delete(configMap, key) err = saveConfigMapToFile(configPath, configs) if err != nil { return err } return nil } } return zerr.ErrConfigNotFound } func setConfigValue(configPath, configName, key, value string) error { if key == nameKey { return zerr.ErrIllegalConfigKey } configs, err := getConfigMapFromFile(configPath) if err != nil { if errors.Is(err, zerr.ErrEmptyJSON) { return zerr.ErrConfigNotFound } return err } for _, val := range configs { configMap, ok := val.(map[string]interface{}) if !ok { return zerr.ErrBadConfig } addDefaultConfigs(configMap) name := configMap[nameKey] if name == configName { boolVal, err := strconv.ParseBool(value) if err == nil { configMap[key] = boolVal } else { configMap[key] = value } err = saveConfigMapToFile(configPath, configs) if err != nil { return err } return nil } } return zerr.ErrConfigNotFound } func getAllConfig(configPath, configName string) (string, error) { configs, err := getConfigMapFromFile(configPath) if err != nil { if errors.Is(err, zerr.ErrEmptyJSON) { return "", nil } return "", err } var builder strings.Builder for _, value := range configs { configMap, ok := value.(map[string]interface{}) if !ok { return "", zerr.ErrBadConfig } addDefaultConfigs(configMap) name := configMap[nameKey] if name == configName { for key, val := range configMap { if key == nameKey { continue } fmt.Fprintf(&builder, "%s = %v\n", key, val) } return builder.String(), nil } } return "", zerr.ErrConfigNotFound } func configNameExists(configs []interface{}, configName string) bool { for _, val := range configs { configMap, ok := val.(map[string]interface{}) if !ok { return false } if configMap[nameKey] == configName { return true } } return false } const ( examples = ` zli config add main https://zot-foo.com:8080 zli config main url zli config main --list zli config --list` supportedOptions = ` Useful variables: url zot server URL showspinner show spinner while loading data [true/false] verify-tls enable TLS certificate verification of the server [default: true]` nameKey = "_name" noArgs = 0 oneArg = 1 twoArgs = 2 threeArgs = 3 showspinnerConfig = "showspinner" verifyTLSConfig = "verify-tls" )