refactor: Replace reflection-based Viper defaults with embedded template

Load the embedded config template as the Viper base so all keys are
registered upfront, enabling PMG_* env vars to work regardless of
whether a key exists in the user's config file.
This commit is contained in:
Sahilb315
2026-04-15 20:06:53 +05:30
parent 7821c40312
commit f117627e3c
3 changed files with 10 additions and 57 deletions
+1 -3
View File
@@ -55,9 +55,7 @@ proxy_install_only: false
#
# The purl is the package identifier and the reason is the reason for trusting the package.
# PURL specification: https://github.com/package-url/purl-spec
trusted_packages:
- purl: pkg:npm/@safedep/pmg
reason: "PMG is a trusted package for PMG"
trusted_packages: []
# Sandbox configuration (EXPERIMENTAL)
# When enabled, package managers run in sandbox environments with restricted
+2 -6
View File
@@ -32,7 +32,7 @@ func TestTemplateParsesAsYAML(t *testing.T) {
assert.False(t, false, cfg.Paranoid, "expected Paranoid false")
assert.False(t, false, cfg.SkipEventLogging, "expected SkipEventLogging false")
assert.Equal(t, 7, cfg.EventLogRetentionDays, "expected EventLogRetentionDays 7")
assert.Len(t, cfg.TrustedPackages, 1)
assert.Empty(t, cfg.TrustedPackages)
}
func TestTemplateMatchesDefaults(t *testing.T) {
@@ -56,11 +56,7 @@ func TestTemplateMatchesDefaults(t *testing.T) {
assert.Equal(t, def.EventLogRetentionDays, parsed.EventLogRetentionDays, "event_log_retention_days mismatch")
assert.Equal(t, def.Verbosity, parsed.Verbosity, "verbosity mismatch")
assert.NotEmpty(t, parsed.TrustedPackages, "expected at least one trusted_packages entry")
first := parsed.TrustedPackages[0]
assert.NotEmpty(t, first.Purl, "first trusted package has empty purl")
assert.NotEmpty(t, first.Reason, "first trusted package has empty reason")
assert.Equal(t, def.TrustedPackages, parsed.TrustedPackages, "trusted_packages mismatch")
assert.Equal(t, def.DependencyCooldown.Enabled, parsed.DependencyCooldown.Enabled, "dependency_cooldown.enabled mismatch")
assert.Equal(t, def.DependencyCooldown.Days, parsed.DependencyCooldown.Days, "dependency_cooldown.days mismatch")
+7 -48
View File
@@ -3,7 +3,6 @@ package config
import (
"fmt"
"os"
"reflect"
"strings"
"github.com/spf13/viper"
@@ -24,13 +23,14 @@ func loadViperConfig() error {
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
// Register all config struct fields as Viper defaults so that env vars work
// for any key Viper wouldn't otherwise know about — either because there is no
// config file, or because the key is absent from the file (e.g. commented out,
// or a new key added after the user last ran "pmg setup install").
registerViperDefaults(v, globalConfig.Config, "")
// Load the embedded template as the base so Viper knows all keys and their
// defaults. This is required for AutomaticEnv to resolve PMG_* env vars for
// keys that are absent from or newer than the user's config file.
if err := v.ReadConfig(strings.NewReader(templateConfig)); err != nil {
return fmt.Errorf("failed to load default config: %w", err)
}
// Merge the user config file on top if it exists.
// Merge user config on top if it exists.
if _, statErr := os.Stat(configPath); statErr == nil {
v.SetConfigFile(configPath)
if err := v.MergeInConfig(); err != nil {
@@ -38,8 +38,6 @@ func loadViperConfig() error {
}
}
// Unmarshal into a copy of the current defaults so that keys absent from
// both the env and the user config file retain their Go defaults.
merged := globalConfig.Config
if err := v.Unmarshal(&merged); err != nil {
return fmt.Errorf("failed to unmarshal config: %w", err)
@@ -48,42 +46,3 @@ func loadViperConfig() error {
globalConfig.Config = merged
return nil
}
// registerViperDefaults walks cfg (a struct) recursively via reflection and registers
// each field as a Viper default using its mapstructure tag as the key. This is the
// minimum required for AutomaticEnv to resolve env vars for those keys.
func registerViperDefaults(v *viper.Viper, cfg any, prefix string) {
t := reflect.TypeOf(cfg)
val := reflect.ValueOf(cfg)
if t.Kind() == reflect.Pointer {
t = t.Elem()
val = val.Elem()
}
if t.Kind() != reflect.Struct {
return
}
for i := range t.NumField() {
field := t.Field(i)
fieldVal := val.Field(i)
tag := field.Tag.Get("mapstructure")
if tag == "" || tag == "-" {
continue
}
// Strip options like ",squash" or ",omitempty"
key := strings.SplitN(tag, ",", 2)[0]
if prefix != "" {
key = prefix + "." + key
}
if field.Type.Kind() == reflect.Struct {
registerViperDefaults(v, fieldVal.Interface(), key)
} else {
v.SetDefault(key, fieldVal.Interface())
}
}
}