feat(cli): support default config name (#4143)

* feat(cli): support default config name

* fix: lint issues

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

* fix: address copilot comments

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

* test: increase coverage

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

* fix: remove deadcode

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

---------

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
Co-authored-by: Akash Kumar <meakash7902@gmail.com>
This commit is contained in:
Andrei Aaron
2026-06-19 05:42:31 +03:00
committed by GitHub
parent 43a5f155b8
commit 09066cae59
7 changed files with 746 additions and 21 deletions
+67 -8
View File
@@ -21,14 +21,16 @@ const (
defaultConfigPerms = 0o644
defaultFilePerms = 0o600
nameKey = "_name"
showspinnerConfig = "showspinner"
verifyTLSConfig = "verify-tls"
defaultConfigNameKey = "defaultConfigName"
nameKey = "_name"
showspinnerConfig = "showspinner"
verifyTLSConfig = "verify-tls"
)
// ZliConfigFile is the on-disk JSON shape for ~/.zot (zli CLI registry profiles).
type ZliConfigFile struct {
Configs []ZliConfig `json:"configs"`
Configs []ZliConfig `json:"configs"`
DefaultConfigName string `json:"defaultConfigName,omitempty"`
}
// ZliConfig is one named registry profile inside ZliConfigFile.Configs.
@@ -89,6 +91,17 @@ func ReadZliConfigFile(filePath string) (*ZliConfigFile, error) {
out := &ZliConfigFile{Configs: make([]ZliConfig, 0, len(configsAny))}
if defaultNameRaw, ok := jsonMap[defaultConfigNameKey]; ok && defaultNameRaw != nil {
defaultName, ok := defaultNameRaw.(string)
if !ok {
return nil, fmt.Errorf(
`%w: field "%s" must be a string, got %T`,
zerr.ErrCliBadConfig, defaultConfigNameKey, defaultNameRaw)
}
out.DefaultConfigName = defaultName
}
for i, v := range configsAny {
configMap, ok := v.(map[string]any)
if !ok {
@@ -182,6 +195,37 @@ func (f *ZliConfigFile) HasEntry(configName string) bool {
})
}
// SetDefault validates and stores the default profile name.
func (f *ZliConfigFile) SetDefault(configName string) error {
if !f.HasEntry(configName) {
return zerr.ErrConfigNotFound
}
f.DefaultConfigName = configName
return nil
}
// ClearDefault clears the stored default profile name.
func (f *ZliConfigFile) ClearDefault() {
f.DefaultConfigName = ""
}
// DefaultName returns the configured default profile name, validating that it still exists.
func (f *ZliConfigFile) DefaultName() (string, error) {
if f.DefaultConfigName == "" {
return "", nil
}
if !f.HasEntry(f.DefaultConfigName) {
return "", fmt.Errorf(
"%w: %s %q does not match any profile",
zerr.ErrConfigNotFound, defaultConfigNameKey, f.DefaultConfigName)
}
return f.DefaultConfigName, nil
}
// AddEntry appends a new profile after validating URL and duplicate names.
func (f *ZliConfigFile) AddEntry(configName, urlStr string) error {
if err := validateURL(urlStr); err != nil {
@@ -210,6 +254,9 @@ func (f *ZliConfigFile) RemoveEntry(configName string) error {
}
f.Configs = append(f.Configs[:i], f.Configs[i+1:]...)
if f.DefaultConfigName == configName {
f.ClearDefault()
}
return nil
}
@@ -219,12 +266,22 @@ func (f *ZliConfigFile) RemoveEntry(configName string) error {
// FormatNames renders name and URL columns for `zli config list`.
func (f *ZliConfigFile) FormatNames() (string, error) {
defaultName, err := f.DefaultName()
if err != nil {
return "", err
}
var builder strings.Builder
writer := tabwriter.NewWriter(&builder, 0, 8, 1, '\t', tabwriter.AlignRight) //nolint:mnd
for _, c := range f.Configs {
fmt.Fprintf(writer, "%s\t%s\n", c.Name, c.URL)
for _, profile := range f.Configs {
name := profile.Name
if profile.Name == defaultName {
name += " (default)"
}
fmt.Fprintf(writer, "%s\t%s\n", name, profile.URL)
}
if err := writer.Flush(); err != nil {
@@ -317,13 +374,15 @@ func (c *ZliConfig) ResetVar(key string) error {
return nil
}
// FormatListedVars renders lines for `zli config show <name>`.
func (c *ZliConfig) FormatListedVars() string {
func (c *ZliConfig) formatListedVars(isDefault bool) string {
var builder strings.Builder
fmt.Fprintf(&builder, "%s = %v\n", URLFlag, c.URL)
fmt.Fprintf(&builder, "%s = %v\n", showspinnerConfig, c.ShowSpinner)
fmt.Fprintf(&builder, "%s = %v\n", verifyTLSConfig, c.VerifyTLS)
if isDefault {
fmt.Fprintln(&builder, "default = true")
}
return builder.String()
}
+91 -4
View File
@@ -55,12 +55,14 @@ func NewConfigCommand() *cobra.Command {
configCmd.AddCommand(NewConfigGetCommand())
configCmd.AddCommand(NewConfigSetCommand())
configCmd.AddCommand(NewConfigResetCommand())
configCmd.AddCommand(NewConfigSetDefaultCommand())
configCmd.AddCommand(NewConfigClearDefaultCommand())
// 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.
Use the list, show, get, set, reset, set-default, and clear-default 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)
@@ -165,7 +167,7 @@ func NewConfigRemoveCommand() *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",
Long: "Remove configuration for a zot registry. Removing the default profile also clears the default.",
SilenceUsage: true,
Args: exactArgsOrHelp(oneArg),
RunE: func(cmd *cobra.Command, args []string) error {
@@ -324,6 +326,52 @@ func NewConfigResetCommand() *cobra.Command {
return resetCmd
}
func NewConfigSetDefaultCommand() *cobra.Command {
setDefaultCmd := &cobra.Command{
Use: "set-default <name>",
Example: " zli config set-default main",
Short: "Set the default configuration profile",
Long: "Set the default profile used when neither --url nor --config is provided.",
SilenceUsage: true,
Args: exactArgsOrHelp(oneArg),
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
return setDefaultConfig(configPath, args[0])
},
}
setDefaultCmd.SetUsageTemplate(setDefaultCmd.UsageTemplate())
return setDefaultCmd
}
func NewConfigClearDefaultCommand() *cobra.Command {
clearDefaultCmd := &cobra.Command{
Use: "clear-default",
Example: " zli config clear-default",
Short: "Clear the default configuration profile",
Long: "Clear the default profile. Commands will again require --url or --config.",
SilenceUsage: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
configPath, err := zliUserConfigPath()
if err != nil {
return err
}
return clearDefaultConfig(configPath)
},
}
clearDefaultCmd.SetUsageTemplate(clearDefaultCmd.UsageTemplate())
return clearDefaultCmd
}
func getConfigNames(configPath string) (string, error) {
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
@@ -371,6 +419,38 @@ func removeConfig(configPath, configName string) error {
return cfg.WriteFile(configPath)
}
func setDefaultConfig(configPath, configName string) error {
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
if isConfigUnavailable(err) {
return zerr.ErrConfigNotFound
}
return err
}
if err := cfg.SetDefault(configName); err != nil {
return err
}
return cfg.WriteFile(configPath)
}
func clearDefaultConfig(configPath string) error {
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
if isConfigUnavailable(err) {
return nil
}
return err
}
cfg.ClearDefault()
return cfg.WriteFile(configPath)
}
func getConfigValue(configPath, configName, key string) (string, error) {
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
@@ -451,12 +531,17 @@ func getAllConfig(configPath, configName string) (string, error) {
return "", err
}
c, err := cfg.Find(configName)
profile, err := cfg.Find(configName)
if err != nil {
return "", err
}
return c.FormatListedVars(), nil
defaultName, err := cfg.DefaultName()
if err != nil {
return "", err
}
return profile.formatListedVars(defaultName == configName), nil
}
const (
@@ -466,6 +551,8 @@ const (
zli config get main url
zli config set main showspinner false
zli config reset main showspinner
zli config set-default main
zli config clear-default
zli config remove main`
supportedOptions = `
+204
View File
@@ -4,6 +4,7 @@ package client
import (
"errors"
"io"
"os"
"path/filepath"
"testing"
@@ -306,6 +307,131 @@ func TestSetConfigValue(t *testing.T) {
})
}
func TestDefaultConfigValue(t *testing.T) {
t.Parallel()
validProfile := `{"configs":[{"_name":"a","url":"https://example.com"}]}`
t.Run("setDefaultConfig_success", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cfgPath := writeTestZotFile(t, dir, validProfile)
require.NoError(t, setDefaultConfig(cfgPath, "a"))
cfg, err := ReadZliConfigFile(cfgPath)
require.NoError(t, err)
require.Equal(t, "a", cfg.DefaultConfigName)
})
t.Run("setDefaultConfig_missing_profile", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cfgPath := writeTestZotFile(t, dir, validProfile)
err := setDefaultConfig(cfgPath, "missing")
require.ErrorIs(t, err, zerr.ErrConfigNotFound)
})
t.Run("setDefaultConfig_fresh_ErrConfigNotFound", func(t *testing.T) {
t.Parallel()
cfgPath := tempConfigPath(t, false, "")
err := setDefaultConfig(cfgPath, "any")
require.ErrorIs(t, err, zerr.ErrConfigNotFound)
})
t.Run("setDefaultConfig_invalid_config_returns_error", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cfgPath := writeTestZotFile(t, dir, `{"configs":"not-an-array"}`)
err := setDefaultConfig(cfgPath, "any")
require.ErrorIs(t, err, zerr.ErrCliBadConfig)
})
t.Run("removeConfig_fresh_ErrConfigNotFound", func(t *testing.T) {
t.Parallel()
cfgPath := tempConfigPath(t, false, "")
err := removeConfig(cfgPath, "any")
require.ErrorIs(t, err, zerr.ErrConfigNotFound)
})
t.Run("clearDefaultConfig_success", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cfgPath := writeTestZotFile(t, dir,
`{"configs":[{"_name":"a","url":"https://example.com"}],"defaultConfigName":"a"}`)
require.NoError(t, clearDefaultConfig(cfgPath))
cfg, err := ReadZliConfigFile(cfgPath)
require.NoError(t, err)
require.Empty(t, cfg.DefaultConfigName)
})
t.Run("clearDefaultConfig_fresh_returns_nil", func(t *testing.T) {
t.Parallel()
cfgPath := tempConfigPath(t, false, "")
require.NoError(t, clearDefaultConfig(cfgPath))
})
t.Run("getDefaultConfigName_missing_profile", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cfgPath := writeTestZotFile(t, dir,
`{"configs":[{"_name":"a","url":"https://example.com"}],"defaultConfigName":"missing"}`)
_, err := getDefaultConfigName(cfgPath)
require.ErrorIs(t, err, zerr.ErrConfigNotFound)
require.Contains(t, err.Error(), "defaultConfigName")
})
t.Run("getDefaultConfigName_missing_file", func(t *testing.T) {
t.Parallel()
cfgPath := tempConfigPath(t, false, "")
defaultName, err := getDefaultConfigName(cfgPath)
require.NoError(t, err)
require.Empty(t, defaultName)
_, err = os.Stat(cfgPath)
require.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("getDefaultConfigName_empty_file_returns_empty", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cfgPath := writeTestZotFile(t, dir, "")
defaultName, err := getDefaultConfigName(cfgPath)
require.NoError(t, err)
require.Empty(t, defaultName)
})
t.Run("getDefaultConfigName_invalid_config_returns_error", func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cfgPath := writeTestZotFile(t, dir, `{"configs":"not-an-array"}`)
_, err := getDefaultConfigName(cfgPath)
require.ErrorIs(t, err, zerr.ErrCliBadConfig)
})
}
func TestConfigCmd_listFreshAndFindErrors(t *testing.T) {
t.Parallel()
@@ -337,6 +463,28 @@ func TestConfigCmd_listFreshAndFindErrors(t *testing.T) {
name: "getConfigNames_fresh_returns_empty",
wantOut: "",
},
{
name: "getConfigNames_stale_default_ErrConfigNotFound",
writeFile: true,
cfgContents: `{"configs":[{"_name":"main","url":"https://example.com"}],"defaultConfigName":"missing"}`,
wantErrIs: zerr.ErrConfigNotFound,
},
{
name: "getAllConfig_stale_default_ErrConfigNotFound",
writeFile: true,
cfgContents: `{"configs":[{"_name":"main","url":"https://example.com"}],"defaultConfigName":"missing"}`,
runGetAll: true,
configName: "main",
wantErrIs: zerr.ErrConfigNotFound,
},
{
name: "getAllConfig_invalid_config_ErrCliBadConfig",
writeFile: true,
cfgContents: `{"configs":"not-an-array"}`,
runGetAll: true,
configName: "main",
wantErrIs: zerr.ErrCliBadConfig,
},
}
for _, tableCase := range tests {
@@ -353,6 +501,9 @@ func TestConfigCmd_listFreshAndFindErrors(t *testing.T) {
out, err := getAllConfig(cfgPath, "any")
require.NoError(t, err)
require.Equal(t, tableCase.wantOut, out)
case tableCase.wantErrIs != nil:
_, err := getConfigNames(cfgPath)
require.ErrorIs(t, err, tableCase.wantErrIs)
default:
out, err := getConfigNames(cfgPath)
require.NoError(t, err)
@@ -361,3 +512,56 @@ func TestConfigCmd_listFreshAndFindErrors(t *testing.T) {
})
}
}
func TestConfigSubcommands_unavailableHome(t *testing.T) {
tests := []struct {
name string
args []string
}{
{name: "add", args: []string{"add", "main", "https://example.com"}},
{name: "remove", args: []string{"remove", "main"}},
{name: "list", args: []string{"list"}},
{name: "show", args: []string{"show", "main"}},
{name: "get", args: []string{"get", "main", "url"}},
{name: "set", args: []string{"set", "main", "showspinner", "false"}},
{name: "reset", args: []string{"reset", "main", "showspinner"}},
{name: "set-default", args: []string{"set-default", "main"}},
{name: "clear-default", args: []string{"clear-default"}},
}
for _, tableCase := range tests {
t.Run(tableCase.name, func(t *testing.T) {
t.Setenv("HOME", "nonExistentDirectory")
cmd := NewConfigCommand()
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs(tableCase.args)
require.Error(t, cmd.Execute())
})
}
}
func TestConfigCommand_legacyPath_unavailableHome(t *testing.T) {
t.Setenv("HOME", "nonExistentDirectory")
cmd := NewConfigCommand()
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"main", "https://example.com"})
require.Error(t, cmd.Execute())
}
func TestConfigSubcommands_exactArgsOrHelp(t *testing.T) {
t.Parallel()
cmd := NewConfigCommand()
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"show"})
err := cmd.Execute()
require.ErrorIs(t, err, zerr.ErrInvalidArgs)
}
+150
View File
@@ -321,6 +321,140 @@ func TestConfigCmdMain(t *testing.T) {
So(strings.TrimSpace(outBuff.String()), ShouldEqual, "")
So(errBuff.String(), ShouldEqual, "")
})
Convey("rejects stale defaultConfigName", func() {
_ = makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com"}],"defaultConfigName":"missing"}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs([]string{"list"})
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrConfigNotFound), ShouldBeTrue)
So(buff.String(), ShouldContainSubstring, "defaultConfigName")
})
})
Convey("Test config default profile commands", t, func() {
Convey("sets and displays the default profile", func() {
configPath := makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"set-default", "configtest"})
So(cmd.Execute(), ShouldBeNil)
actual, err := os.ReadFile(configPath)
So(err, ShouldBeNil)
So(string(actual), ShouldContainSubstring, `"defaultConfigName": "configtest"`)
listCmd := client.NewConfigCommand()
listOut := bytes.NewBufferString("")
listErr := bytes.NewBufferString("")
listCmd.SetOut(listOut)
listCmd.SetErr(listErr)
listCmd.SetArgs([]string{"list"})
So(listCmd.Execute(), ShouldBeNil)
So(listOut.String(), ShouldContainSubstring, "configtest (default)")
So(listErr.String(), ShouldEqual, "")
showCmd := client.NewConfigCommand()
showOut := bytes.NewBufferString("")
showErr := bytes.NewBufferString("")
showCmd.SetOut(showOut)
showCmd.SetErr(showErr)
showCmd.SetArgs([]string{"show", "configtest"})
So(showCmd.Execute(), ShouldBeNil)
So(showOut.String(), ShouldContainSubstring, "default = true")
So(showErr.String(), ShouldEqual, "")
})
Convey("rejects a missing default profile", func() {
_ = makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com","showspinner":false}]}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs([]string{"set-default", "missing"})
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrConfigNotFound), ShouldBeTrue)
})
Convey("set-default errors when home directory is unavailable", func() {
t.Setenv("HOME", "nonExistentDirectory")
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs([]string{"set-default", "configtest"})
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
Convey("clears the default profile", func() {
configPath := makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com",`+
`"showspinner":false}],"defaultConfigName":"configtest"}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"clear-default"})
So(cmd.Execute(), ShouldBeNil)
So(outBuff.String(), ShouldEqual, "")
So(errBuff.String(), ShouldEqual, "")
actual, err := os.ReadFile(configPath)
So(err, ShouldBeNil)
So(string(actual), ShouldNotContainSubstring, "defaultConfigName")
})
Convey("remove clears the default profile", func() {
configPath := makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com",`+
`"showspinner":false}],"defaultConfigName":"configtest"}`)
cmd := client.NewConfigCommand()
outBuff := bytes.NewBufferString("")
errBuff := bytes.NewBufferString("")
cmd.SetOut(outBuff)
cmd.SetErr(errBuff)
cmd.SetArgs([]string{"remove", "configtest"})
So(cmd.Execute(), ShouldBeNil)
actual, err := os.ReadFile(configPath)
So(err, ShouldBeNil)
So(string(actual), ShouldNotContainSubstring, "defaultConfigName")
})
Convey("clear-default errors when home directory is unavailable", func() {
t.Setenv("HOME", "nonExistentDirectory")
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs([]string{"clear-default"})
err := cmd.Execute()
So(err, ShouldNotBeNil)
})
})
Convey("Test config show", t, func() {
@@ -339,6 +473,22 @@ func TestConfigCmdMain(t *testing.T) {
So(errBuff.String(), ShouldEqual, "")
})
Convey("rejects stale defaultConfigName", func() {
_ = makeConfigFile(t,
`{"configs":[{"_name":"configtest","url":"https://test-url.com"}],"defaultConfigName":"missing"}`)
cmd := client.NewConfigCommand()
buff := bytes.NewBufferString("")
cmd.SetOut(buff)
cmd.SetErr(buff)
cmd.SetArgs([]string{"show", "configtest"})
err := cmd.Execute()
So(err, ShouldNotBeNil)
So(errors.Is(err, zerr.ErrConfigNotFound), ShouldBeTrue)
So(buff.String(), ShouldContainSubstring, "defaultConfigName")
})
Convey("from empty config file", func() {
_ = makeConfigFile(t, ``)
+77
View File
@@ -86,6 +86,12 @@ func TestReadZliConfigFile_errors(t *testing.T) {
wantSubs: "",
wantErrIs: []error{zerr.ErrCliBadConfig, zerr.ErrCliMissingConfigsField},
},
{
name: "default_config_name_not_string",
cfgContents: `{"configs":[],"defaultConfigName":1}`,
wantSubs: `field "defaultConfigName" must be a string`,
wantErrIs: []error{zerr.ErrCliBadConfig},
},
}
for _, tableCase := range tests {
@@ -119,6 +125,77 @@ func TestZliConfigFile_RemoveEntry_NotFound(t *testing.T) {
assert.ErrorIs(t, err, zerr.ErrConfigNotFound)
}
func TestZliConfigFile_DefaultConfigName(t *testing.T) {
t.Parallel()
configFile := client.ZliConfigFile{
Configs: []client.ZliConfig{
{Name: "main", URL: "https://example.com"},
{Name: "other", URL: "https://other.example.com"},
},
}
require.NoError(t, configFile.SetDefault("main"))
assert.Equal(t, "main", configFile.DefaultConfigName)
defaultName, err := configFile.DefaultName()
require.NoError(t, err)
assert.Equal(t, "main", defaultName)
require.ErrorIs(t, configFile.SetDefault("missing"), zerr.ErrConfigNotFound)
require.NoError(t, configFile.RemoveEntry("main"))
assert.Empty(t, configFile.DefaultConfigName)
}
func TestZliConfigFile_DefaultNameMissingProfile(t *testing.T) {
t.Parallel()
configFile := client.ZliConfigFile{
DefaultConfigName: "missing",
Configs: []client.ZliConfig{{Name: "main", URL: "https://example.com"}},
}
_, err := configFile.DefaultName()
require.ErrorIs(t, err, zerr.ErrConfigNotFound)
assert.Contains(t, err.Error(), "defaultConfigName")
}
func TestZliConfigFile_FormatNames(t *testing.T) {
t.Parallel()
t.Run("marks_valid_default", func(t *testing.T) {
t.Parallel()
configFile := client.ZliConfigFile{
DefaultConfigName: "main",
Configs: []client.ZliConfig{
{Name: "main", URL: "https://example.com"},
{Name: "other", URL: "https://other.example.com"},
},
}
out, err := configFile.FormatNames()
require.NoError(t, err)
assert.Contains(t, out, "main (default)")
assert.Contains(t, out, "other")
assert.NotContains(t, out, "other (default)")
})
t.Run("stale_default_errors", func(t *testing.T) {
t.Parallel()
configFile := client.ZliConfigFile{
DefaultConfigName: "missing",
Configs: []client.ZliConfig{{Name: "main", URL: "https://example.com"}},
}
_, err := configFile.FormatNames()
require.ErrorIs(t, err, zerr.ErrConfigNotFound)
assert.Contains(t, err.Error(), "defaultConfigName")
})
}
func TestZliConfig_GetVar(t *testing.T) {
t.Parallel()
@@ -955,6 +955,47 @@ func TestUtils(t *testing.T) {
So(err, ShouldNotBeNil)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
// default profile when --config and --url are absent
_ = makeConfigFile(t,
`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false, "verify-tls": false}],"defaultConfigName":"imagetest"}`)
cmd = &cobra.Command{}
cmd.Flags().String(ConfigFlag, "", "")
cmd.Flags().String(URLFlag, "", "")
isSpinner, verifyTLS, err = GetCliConfigOptions(cmd)
So(err, ShouldBeNil)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
// no default profile and no flags returns defaults without error
_ = makeConfigFile(t, "")
cmd = &cobra.Command{}
cmd.Flags().String(ConfigFlag, "", "")
cmd.Flags().String(URLFlag, "", "")
isSpinner, verifyTLS, err = GetCliConfigOptions(cmd)
So(err, ShouldBeNil)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
// stale default profile returns error
_ = makeConfigFile(t,
`{"configs":[{"_name":"imagetest","url":"https://test-url.com"}],"defaultConfigName":"missing"}`)
cmd = &cobra.Command{}
cmd.Flags().String(ConfigFlag, "", "")
cmd.Flags().String(URLFlag, "", "")
isSpinner, verifyTLS, err = GetCliConfigOptions(cmd)
So(err, ShouldNotBeNil)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
// unreadable config path when --config is set
t.Setenv("HOME", "nonExistentDirectory")
cmd = &cobra.Command{}
cmd.Flags().String(ConfigFlag, "imagetest", "")
isSpinner, verifyTLS, err = GetCliConfigOptions(cmd)
So(err, ShouldNotBeNil)
So(isSpinner, ShouldBeFalse)
So(verifyTLS, ShouldBeFalse)
})
Convey("GetServerURLFromFlags", t, func() {
@@ -985,6 +1026,65 @@ func TestUtils(t *testing.T) {
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "")
So(err, ShouldNotBeNil)
// default profile when --config and --url are absent
_ = makeConfigFile(t,
`{"configs":[{"_name":"main","url":"https://default.example.com"},{"_name":"other","url":"https://other.example.com"}],"defaultConfigName":"main"}`)
cmd = &cobra.Command{}
cmd.Flags().String(URLFlag, "", "")
cmd.Flags().String(ConfigFlag, "", "")
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "https://default.example.com")
So(err, ShouldBeNil)
// explicit --url overrides default
cmd = &cobra.Command{}
cmd.Flags().String(URLFlag, "https://flag.example.com", "")
cmd.Flags().String(ConfigFlag, "", "")
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "https://flag.example.com")
So(err, ShouldBeNil)
// explicit --config overrides default
cmd = &cobra.Command{}
cmd.Flags().String(URLFlag, "", "")
cmd.Flags().String(ConfigFlag, "other", "")
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "https://other.example.com")
So(err, ShouldBeNil)
// invalid default profile has a clear error
_ = makeConfigFile(t,
`{"configs":[{"_name":"main","url":"https://default.example.com"}],"defaultConfigName":"missing"}`)
cmd = &cobra.Command{}
cmd.Flags().String(URLFlag, "", "")
cmd.Flags().String(ConfigFlag, "", "")
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "")
So(err, ShouldNotBeNil)
So(err.Error(), ShouldContainSubstring, "defaultConfigName")
// invalid URL in default profile
_ = makeConfigFile(t,
`{"configs":[{"_name":"main","url":"not-a-url"}],"defaultConfigName":"main"}`)
cmd = &cobra.Command{}
cmd.Flags().String(URLFlag, "", "")
cmd.Flags().String(ConfigFlag, "", "")
url, err = GetServerURLFromFlags(cmd)
So(url, ShouldResemble, "")
So(err, ShouldNotBeNil)
})
Convey("ReadServerURLFromConfig", t, func() {
_ = makeConfigFile(t, `{"configs":[{"_name":"main","url":"https://default.example.com"}]}`)
url, err := ReadServerURLFromConfig("main")
So(url, ShouldResemble, "https://default.example.com")
So(err, ShouldBeNil)
t.Setenv("HOME", "nonExistentDirectory")
url, err = ReadServerURLFromConfig("main")
So(url, ShouldResemble, "")
So(err, ShouldNotBeNil)
})
Convey("CheckExtEndPointQuery", t, func() {
+57 -9
View File
@@ -4,10 +4,10 @@ package client
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -449,16 +449,31 @@ func GetCliConfigOptions(cmd *cobra.Command) (bool, bool, error) {
}
if configName == "" {
return false, false, nil
serverURL, urlErr := cmd.Flags().GetString(URLFlag)
if urlErr == nil && serverURL != "" {
return false, false, nil
}
configPath, err := zliUserConfigPath()
if err != nil {
return false, false, err
}
configName, err = getDefaultConfigName(configPath)
if err != nil {
return false, false, err
}
if configName == "" {
return false, false, nil
}
}
home, err := os.UserHomeDir()
configPath, err := zliUserConfigPath()
if err != nil {
return false, false, err
}
configPath := filepath.Join(home, ".zot")
isSpinner, err := parseBooleanConfig(configPath, configName, showspinnerConfig)
if err != nil {
return false, false, err
@@ -484,7 +499,21 @@ func GetServerURLFromFlags(cmd *cobra.Command) (string, error) {
}
if configName == "" {
return "", fmt.Errorf("%w: specify either '--%s' or '--%s' flags", zerr.ErrNoURLProvided, URLFlag, ConfigFlag)
configPath, err := zliUserConfigPath()
if err != nil {
return "", err
}
configName, err = getDefaultConfigName(configPath)
if err != nil {
return "", err
}
}
if configName == "" {
return "", fmt.Errorf(
"%w: specify either '--%s' or '--%s' flags, or set a default with 'zli config set-default <name>'",
zerr.ErrNoURLProvided, URLFlag, ConfigFlag)
}
serverURL, err = ReadServerURLFromConfig(configName)
@@ -500,13 +529,11 @@ func GetServerURLFromFlags(cmd *cobra.Command) (string, error) {
}
func ReadServerURLFromConfig(configName string) (string, error) {
home, err := os.UserHomeDir()
configPath, err := zliUserConfigPath()
if err != nil {
return "", err
}
configPath := filepath.Join(home, ".zot")
urlFromConfig, err := getConfigValue(configPath, configName, URLFlag)
if err != nil {
return "", err
@@ -515,6 +542,27 @@ func ReadServerURLFromConfig(configName string) (string, error) {
return urlFromConfig, nil
}
func getDefaultConfigName(configPath string) (string, error) {
if _, err := os.Stat(configPath); err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", nil
}
return "", err
}
cfg, err := ReadZliConfigFile(configPath)
if err != nil {
if isConfigUnavailable(err) {
return "", nil
}
return "", err
}
return cfg.DefaultName()
}
func GetSuggestionsString(suggestions []string) string {
if len(suggestions) > 0 {
return "\n\nDid you mean this?\n" + "\t" + strings.Join(suggestions, "\n\t")