mirror of
https://github.com/project-zot/zot.git
synced 2026-06-19 14:08:01 +08:00
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:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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, ``)
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user