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. # 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 # PURL specification: https://github.com/package-url/purl-spec
trusted_packages: trusted_packages: []
- purl: pkg:npm/@safedep/pmg
reason: "PMG is a trusted package for PMG"
# Sandbox configuration (EXPERIMENTAL) # Sandbox configuration (EXPERIMENTAL)
# When enabled, package managers run in sandbox environments with restricted # 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.Paranoid, "expected Paranoid false")
assert.False(t, false, cfg.SkipEventLogging, "expected SkipEventLogging false") assert.False(t, false, cfg.SkipEventLogging, "expected SkipEventLogging false")
assert.Equal(t, 7, cfg.EventLogRetentionDays, "expected EventLogRetentionDays 7") 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) { 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.EventLogRetentionDays, parsed.EventLogRetentionDays, "event_log_retention_days mismatch")
assert.Equal(t, def.Verbosity, parsed.Verbosity, "verbosity mismatch") assert.Equal(t, def.Verbosity, parsed.Verbosity, "verbosity mismatch")
assert.NotEmpty(t, parsed.TrustedPackages, "expected at least one trusted_packages entry") assert.Equal(t, def.TrustedPackages, parsed.TrustedPackages, "trusted_packages mismatch")
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.DependencyCooldown.Enabled, parsed.DependencyCooldown.Enabled, "dependency_cooldown.enabled 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") assert.Equal(t, def.DependencyCooldown.Days, parsed.DependencyCooldown.Days, "dependency_cooldown.days mismatch")
+7 -48
View File
@@ -3,7 +3,6 @@ package config
import ( import (
"fmt" "fmt"
"os" "os"
"reflect"
"strings" "strings"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -24,13 +23,14 @@ func loadViperConfig() error {
v.AutomaticEnv() v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
// Register all config struct fields as Viper defaults so that env vars work // Load the embedded template as the base so Viper knows all keys and their
// for any key Viper wouldn't otherwise know about — either because there is no // defaults. This is required for AutomaticEnv to resolve PMG_* env vars for
// config file, or because the key is absent from the file (e.g. commented out, // keys that are absent from or newer than the user's config file.
// or a new key added after the user last ran "pmg setup install"). if err := v.ReadConfig(strings.NewReader(templateConfig)); err != nil {
registerViperDefaults(v, globalConfig.Config, "") 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 { if _, statErr := os.Stat(configPath); statErr == nil {
v.SetConfigFile(configPath) v.SetConfigFile(configPath)
if err := v.MergeInConfig(); err != nil { 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 merged := globalConfig.Config
if err := v.Unmarshal(&merged); err != nil { if err := v.Unmarshal(&merged); err != nil {
return fmt.Errorf("failed to unmarshal config: %w", err) return fmt.Errorf("failed to unmarshal config: %w", err)
@@ -48,42 +46,3 @@ func loadViperConfig() error {
globalConfig.Config = merged globalConfig.Config = merged
return nil 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())
}
}
}