Files
zot/pkg/cli/client/config_cmd.go
T
Andrei Aaron c7ddbe2e36 feat(zli): add config list/show/get/set/reset and isolate deprecated syntax (#4037)
* feat(zli): add config list/show/get/set/reset and isolate deprecated syntax

Introduce first-class subcommands for listing profiles, showing a profile,
getting and setting keys, and resetting optional keys (alongside existing add/remove).
The parent command now resolves ~/.zot via zliUserConfigPath(),
documents that profile names must not clash with subcommand names,
and states that positional/--list/--reset usage is deprecated and will be removed soon.

Legacy behavior is delegated to config_cmd_deprecated.go with stderr warnings for old flags and positional get/set.
Examples and inline help point users at the new commands.
FormatNames/FormatListedVars comments reference config list/show.

Tests are split so config_cmd_test.go exercises the supported subcommands
while config_cmd_deprecated_test.go retains coverage for the deprecated
paths under renamed TestConfigCmdDeprecated* entries.

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>

* test: stabilize retention check tests

See https://github.com/project-zot/zot/actions/runs/25361779632/job/74362802944?pr=4037

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>

---------

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
2026-05-08 20:19:26 +03:00

459 lines
10 KiB
Go

//go:build search
package client
import (
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"github.com/spf13/cobra"
zerr "zotregistry.dev/zot/v2/errors"
)
func NewConfigCommand() *cobra.Command {
var isListing bool
var isReset bool
configCmd := &cobra.Command{
Use: "config",
Example: examples,
Short: "Configure zot registry parameters for CLI",
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
return runLegacyConfig(cmd, args, configPath, isListing, isReset)
},
}
configCmd.Flags().BoolVarP(&isListing, "list", "l", false,
"[deprecated: use \"config list\" or \"config show <name>\"] List configurations")
configCmd.Flags().BoolVar(&isReset, "reset", false,
"[deprecated: use \"config reset\"] Reset a variable value")
configCmd.SetUsageTemplate(configCmd.UsageTemplate() + supportedOptions)
configCmd.AddCommand(NewConfigAddCommand())
configCmd.AddCommand(NewConfigRemoveCommand())
configCmd.AddCommand(NewConfigListCommand())
configCmd.AddCommand(NewConfigShowCommand())
configCmd.AddCommand(NewConfigGetCommand())
configCmd.AddCommand(NewConfigSetCommand())
configCmd.AddCommand(NewConfigResetCommand())
// Build this from actual subcommands to avoid drift.
reserved := strings.Join(reservedProfileNames(configCmd), ", ")
configCmd.Long = fmt.Sprintf(`Configure zot registry parameters for CLI.
Use the list, show, get, set, and reset subcommands for inspecting and editing profiles.
Profile names must not collide with subcommand names (%s).
Older positional syntax on this command is deprecated and will soon be removed.`, reserved)
return configCmd
}
func zliUserConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".zot"), nil
}
// validateProfileNameForCreation prevents creating profiles that shadow subcommand names.
// We intentionally allow interacting with pre-existing profiles that collide with subcommand names
// so users can migrate/rename/remove them without editing ~/.zot by hand.
func validateProfileNameForCreation(configCmd *cobra.Command, name string) error {
if slices.Contains(reservedProfileNames(configCmd), name) {
return fmt.Errorf("%w: %q", zerr.ErrReservedConfigName, name)
}
return nil
}
func reservedProfileNames(configCmd *cobra.Command) []string {
seen := make(map[string]struct{})
for _, sub := range configCmd.Commands() {
name := sub.Name()
if name == "" {
continue
}
seen[name] = struct{}{}
}
reserved := make([]string, 0, len(seen))
for name := range seen {
reserved = append(reserved, name)
}
sort.Strings(reserved)
return reserved
}
func NewConfigAddCommand() *cobra.Command {
configAddCmd := &cobra.Command{
Use: "add <config-name> <url>",
Example: " zli config add main https://zot-foo.com:8080",
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 {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
configRoot := cmd.Parent()
if configRoot == nil {
configRoot = cmd
}
if err := validateProfileNameForCreation(configRoot, args[0]); err != nil {
return err
}
err = addConfig(configPath, args[0], args[1])
if err != nil {
return err
}
return nil
},
}
// Prevent parent template from overwriting default template
configAddCmd.SetUsageTemplate(configAddCmd.UsageTemplate())
return configAddCmd
}
func NewConfigRemoveCommand() *cobra.Command {
configRemoveCmd := &cobra.Command{
Use: "remove <config-name>",
Example: " zli config remove main",
Short: "Remove configuration for a zot registry",
Long: "Remove configuration for a zot registry",
Args: cobra.ExactArgs(oneArg),
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
err = removeConfig(configPath, args[0])
if err != nil {
return err
}
return nil
},
}
// Prevent parent template from overwriting default template
configRemoveCmd.SetUsageTemplate(configRemoveCmd.UsageTemplate())
return configRemoveCmd
}
func NewConfigListCommand() *cobra.Command {
listCmd := &cobra.Command{
Use: "list",
Example: " zli config list",
Short: "List all configuration profile names",
Long: "Print every configured CLI profile name (and URLs where applicable).",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
res, err := getConfigNames(configPath)
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), res)
return nil
},
}
listCmd.SetUsageTemplate(listCmd.UsageTemplate())
return listCmd
}
func NewConfigShowCommand() *cobra.Command {
showCmd := &cobra.Command{
Use: "show <name>",
Example: " zli config show main",
Short: "Show all variables for one profile",
Long: "Print every variable set for the named CLI profile.",
Args: cobra.ExactArgs(oneArg),
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
res, err := getAllConfig(configPath, args[0])
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), res)
return nil
},
}
showCmd.SetUsageTemplate(showCmd.UsageTemplate())
return showCmd
}
func NewConfigGetCommand() *cobra.Command {
getCmd := &cobra.Command{
Use: "get <name> <key>",
Example: " zli config get main url",
Short: "Print one configuration variable",
Long: "Print the value of a single key for the named profile.",
Args: cobra.ExactArgs(twoArgs),
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
res, err := getConfigValue(configPath, args[0], args[1])
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), res)
return nil
},
}
getCmd.SetUsageTemplate(getCmd.UsageTemplate())
return getCmd
}
func NewConfigSetCommand() *cobra.Command {
setCmd := &cobra.Command{
Use: "set <name> <key> <value>",
Example: " zli config set main showspinner false",
Short: "Set a configuration variable",
Long: "Set a single key for the named profile and persist ~/.zot.",
Args: cobra.ExactArgs(threeArgs),
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
return setConfigValue(configPath, args[0], args[1], args[2])
},
}
setCmd.SetUsageTemplate(setCmd.UsageTemplate())
return setCmd
}
func NewConfigResetCommand() *cobra.Command {
resetCmd := &cobra.Command{
Use: "reset <name> <key>",
Example: " zli config reset main showspinner",
Short: "Reset a configuration variable to its default",
Long: "Remove a non-default key from the named profile (URL and profile name cannot be reset).",
Args: cobra.ExactArgs(twoArgs),
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
return resetConfigValue(configPath, args[0], args[1])
},
}
resetCmd.SetUsageTemplate(resetCmd.UsageTemplate())
return resetCmd
}
func getConfigNames(configPath string) (string, error) {
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
if isConfigUnavailable(err) {
return "", nil
}
return "", err
}
return cfg.FormatNames()
}
func addConfig(configPath, configName, url string) error {
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
if !isConfigUnavailable(err) {
return err
}
cfg = &ZliConfigFile{}
}
if err := cfg.AddEntry(configName, url); err != nil {
return err
}
return cfg.WriteFile(configPath)
}
func removeConfig(configPath, configName string) error {
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
if isConfigUnavailable(err) {
return zerr.ErrConfigNotFound
}
return err
}
if err := cfg.RemoveEntry(configName); err != nil {
return err
}
return cfg.WriteFile(configPath)
}
func getConfigValue(configPath, configName, key string) (string, error) {
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
if isConfigUnavailable(err) {
return "", zerr.ErrConfigNotFound
}
return "", err
}
c, err := cfg.Find(configName)
if err != nil {
return "", err
}
return c.GetVar(key)
}
func resetConfigValue(configPath, configName, key string) error {
if key == URLFlag || key == nameKey {
return zerr.ErrCannotResetConfigKey
}
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
if isConfigUnavailable(err) {
return zerr.ErrConfigNotFound
}
return err
}
c, err := cfg.Find(configName)
if err != nil {
return err
}
if err := c.ResetVar(key); err != nil {
return err
}
return cfg.WriteFile(configPath)
}
func setConfigValue(configPath, configName, key, value string) error {
if key == nameKey {
return zerr.ErrIllegalConfigKey
}
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
if isConfigUnavailable(err) {
return zerr.ErrConfigNotFound
}
return err
}
c, err := cfg.Find(configName)
if err != nil {
return err
}
if err := c.SetVar(key, value); err != nil {
return err
}
return cfg.WriteFile(configPath)
}
func getAllConfig(configPath, configName string) (string, error) {
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
if isConfigUnavailable(err) {
return "", nil
}
return "", err
}
c, err := cfg.Find(configName)
if err != nil {
return "", err
}
return c.FormatListedVars(), nil
}
const (
examples = ` zli config add main https://zot-foo.com:8080
zli config list
zli config show main
zli config get main url
zli config set main showspinner false
zli config reset main showspinner
zli config remove main`
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]
`
noArgs = 0
oneArg = 1
twoArgs = 2
threeArgs = 3
)