f58597ade9
Signed-off-by: Alexei Dodon <adodon@cisco.com>
503 lines
10 KiB
Go
503 lines
10 KiB
Go
//go:build search
|
|
// +build search
|
|
|
|
package client
|
|
|
|
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 <config-name> [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 <name> -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 <name> <key> --reset
|
|
return resetConfigValue(configPath, args[0], args[1])
|
|
}
|
|
// zot config <name> <key>
|
|
res, err := getConfigValue(configPath, args[0], args[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(cmd.OutOrStdout(), res)
|
|
case threeArgs:
|
|
// zot config <name> <key> <value>
|
|
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())
|
|
configCmd.AddCommand(NewConfigRemoveCommand())
|
|
|
|
return configCmd
|
|
}
|
|
|
|
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 {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
configPath := path.Join(home, "/.zot")
|
|
// zot config add <config-name> <url>
|
|
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 {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
configPath := path.Join(home, "/.zot")
|
|
// zot config add <config-name> <url>
|
|
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 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.MarshalIndent(&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 err := validateURL(url); err != nil {
|
|
return err
|
|
}
|
|
|
|
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 removeConfig(configPath, configName string) error {
|
|
configs, err := getConfigMapFromFile(configPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i, val := range configs {
|
|
configMap, ok := val.(map[string]interface{})
|
|
if !ok {
|
|
return zerr.ErrBadConfig
|
|
}
|
|
|
|
name := configMap[nameKey]
|
|
if name != configName {
|
|
continue
|
|
}
|
|
|
|
// Remove config from the config list before saving
|
|
newConfigs := configs[:i]
|
|
newConfigs = append(newConfigs, configs[i+1:]...)
|
|
|
|
err = saveConfigMapToFile(configPath, newConfigs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return zerr.ErrConfigNotFound
|
|
}
|
|
|
|
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 --list
|
|
zli config main url
|
|
zli config main --list
|
|
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]
|
|
`
|
|
|
|
nameKey = "_name"
|
|
|
|
noArgs = 0
|
|
oneArg = 1
|
|
twoArgs = 2
|
|
threeArgs = 3
|
|
|
|
showspinnerConfig = "showspinner"
|
|
verifyTLSConfig = "verify-tls"
|
|
)
|