mirror of
https://github.com/89luca89/distrobox.git
synced 2026-08-19 01:14:49 -05:00
refactor(config): read config into struct
The current implementation uses dotenv to load the configuration into the environment, and then delegate the cli layer to handle the configuration coming from the DBX_ variables. With this new implementation, defaults, config files and environment variables are handled in a single point and treated as values.
This commit is contained in:
committed by
Alessio Biancalana
parent
4b22fa130d
commit
e626b63bf2
@@ -1,5 +1,13 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type Values struct {
|
||||
ContainerManagerType string
|
||||
SudoProgram string
|
||||
@@ -23,6 +31,28 @@ func DefaultValues() *Values {
|
||||
return toStruct(defaultsMap())
|
||||
}
|
||||
|
||||
func LoadValues() (*Values, error) {
|
||||
files, err := getConfigFilePaths()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config file paths: %w", err)
|
||||
}
|
||||
|
||||
configMaps := []map[string]string{defaultsMap()}
|
||||
|
||||
for _, file := range files {
|
||||
config, err := readConfigFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file %q: %w", file, err)
|
||||
}
|
||||
configMaps = append(configMaps, config)
|
||||
}
|
||||
|
||||
configMaps = append(configMaps, readEnv())
|
||||
|
||||
merged := mergeConfigMaps(configMaps...)
|
||||
return toStruct(merged), nil
|
||||
}
|
||||
|
||||
func toStruct(configMap map[string]string) *Values {
|
||||
return &Values{
|
||||
ContainerManagerType: configMap["container_manager"],
|
||||
@@ -36,3 +66,88 @@ func toStruct(configMap map[string]string) *Values {
|
||||
func toBool(value string) bool {
|
||||
return value == "true"
|
||||
}
|
||||
|
||||
// getConfigFilePaths returns a list of configuration file paths in order of priority.
|
||||
func getConfigFilePaths() ([]string, error) {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
execPath, err = filepath.EvalSymlinks(execPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to evaluate symlinks for executable path: %w", err)
|
||||
}
|
||||
|
||||
selfDir := filepath.Dir(execPath)
|
||||
|
||||
xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgConfigHome == "" {
|
||||
xdgConfigHome = filepath.Join(os.Getenv("HOME"), ".config")
|
||||
}
|
||||
|
||||
home := os.Getenv("HOME")
|
||||
|
||||
// Source configuration files, this is done in an hierarchy so local files have
|
||||
// priority over system defaults
|
||||
// leave priority to environment variables.
|
||||
//
|
||||
// On NixOS, for the distrobox derivation to pick up a static config file shipped
|
||||
// by the package maintainer the path must be relative to the script itself.
|
||||
return []string{
|
||||
filepath.Join(selfDir, "..", "share", "distrobox", "distrobox.conf"), // for NixOS
|
||||
"/usr/share/distrobox/distrobox.conf",
|
||||
"/usr/share/defaults/distrobox/distrobox.conf",
|
||||
"/usr/etc/distrobox/distrobox.conf",
|
||||
"/usr/local/share/distrobox/distrobox.conf",
|
||||
"/etc/distrobox/distrobox.conf",
|
||||
filepath.Join(xdgConfigHome, "distrobox", "distrobox.conf"),
|
||||
filepath.Join(home, ".distroboxrc"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mergeConfigMaps(maps ...map[string]string) map[string]string {
|
||||
merged := make(map[string]string)
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
merged[k] = v
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func readConfigFile(filePath string) (map[string]string, error) {
|
||||
cfg, err := ini.Load(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return make(map[string]string), nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to load config file %q: %w", filePath, err)
|
||||
}
|
||||
|
||||
config := make(map[string]string)
|
||||
for _, key := range cfg.Section("").Keys() {
|
||||
config[key.Name()] = key.String()
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func readEnv() map[string]string {
|
||||
envConfig := make(map[string]string)
|
||||
|
||||
if value, exists := os.LookupEnv("DBX_CONTAINER_MANAGER"); exists {
|
||||
envConfig["container_manager"] = value
|
||||
}
|
||||
if value, exists := os.LookupEnv("DBX_SUDO_COMMAND"); exists {
|
||||
envConfig["sudo_program"] = value
|
||||
}
|
||||
if value, exists := os.LookupEnv("DBX_SUDO_PROGRAM"); exists {
|
||||
envConfig["sudo_program"] = value
|
||||
}
|
||||
if value, exists := os.LookupEnv("DBX_VERBOSE"); exists {
|
||||
envConfig["verbose"] = value
|
||||
}
|
||||
|
||||
return envConfig
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToStruct(t *testing.T) {
|
||||
input := map[string]string{
|
||||
"container_manager": "docker",
|
||||
"sudo_program": "doas",
|
||||
"verbose": "true",
|
||||
"container_image": "ubuntu:latest",
|
||||
"container_name": "mybox",
|
||||
}
|
||||
|
||||
cfg := toStruct(input)
|
||||
|
||||
assert.Equal(t, "docker", cfg.ContainerManagerType)
|
||||
assert.Equal(t, "doas", cfg.SudoProgram)
|
||||
assert.True(t, cfg.Verbose)
|
||||
assert.Equal(t, "ubuntu:latest", cfg.DefaultContainerImage)
|
||||
assert.Equal(t, "mybox", cfg.DefaultContainerName)
|
||||
}
|
||||
|
||||
func TestToStruct_MissingKeys(t *testing.T) {
|
||||
cfg := toStruct(map[string]string{})
|
||||
|
||||
assert.Empty(t, cfg.ContainerManagerType)
|
||||
assert.Empty(t, cfg.SudoProgram)
|
||||
assert.False(t, cfg.Verbose)
|
||||
assert.Empty(t, cfg.DefaultContainerImage)
|
||||
assert.Empty(t, cfg.DefaultContainerName)
|
||||
}
|
||||
|
||||
func TestMergeConfigMaps(t *testing.T) {
|
||||
base := map[string]string{
|
||||
"container_manager": "podman",
|
||||
"verbose": "false",
|
||||
}
|
||||
override := map[string]string{
|
||||
"container_manager": "docker",
|
||||
"container_name": "mybox",
|
||||
}
|
||||
|
||||
merged := mergeConfigMaps(base, override)
|
||||
|
||||
assert.Equal(t, "docker", merged["container_manager"])
|
||||
assert.Equal(t, "false", merged["verbose"])
|
||||
assert.Equal(t, "mybox", merged["container_name"])
|
||||
}
|
||||
|
||||
func TestMergeConfigMaps_Empty(t *testing.T) {
|
||||
merged := mergeConfigMaps()
|
||||
assert.Empty(t, merged)
|
||||
}
|
||||
|
||||
func TestReadConfigFile_KeyValues(t *testing.T) {
|
||||
tmpFile := filepath.Join(t.TempDir(), "config.conf")
|
||||
content := `
|
||||
container_manager=docker
|
||||
container_name=mybox
|
||||
verbose=true
|
||||
`
|
||||
require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0o644))
|
||||
|
||||
result, err := readConfigFile(tmpFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "docker", result["container_manager"])
|
||||
assert.Equal(t, "mybox", result["container_name"])
|
||||
assert.Equal(t, "true", result["verbose"])
|
||||
}
|
||||
|
||||
func TestReadConfigFile_WithComments(t *testing.T) {
|
||||
tmpFile := filepath.Join(t.TempDir(), "config.conf")
|
||||
content := `
|
||||
# This is a comment
|
||||
container_manager=podman # Inline comment
|
||||
# Another comment
|
||||
verbose=false
|
||||
`
|
||||
require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0o644))
|
||||
|
||||
result, err := readConfigFile(tmpFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, result, 2)
|
||||
assert.Equal(t, "podman", result["container_manager"])
|
||||
assert.Equal(t, "false", result["verbose"])
|
||||
}
|
||||
|
||||
func TestReadConfigFile_NotFound(t *testing.T) {
|
||||
result, err := readConfigFile(filepath.Join(t.TempDir(), "nonexistent.conf"))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
Reference in New Issue
Block a user