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 )