From b91a983cc0e794ba5f461fd42147aaf574796171 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 6 May 2026 14:26:43 +0200 Subject: [PATCH] fix: work around Viper dropping nil-value keys --- internal/config/config.go | 2 +- internal/config/config_test.go | 46 +++++++++++++++++++++++++++++++ internal/config/presets.go | 22 ++++++++++----- tests/provider-k8s/concierge.yaml | 3 ++ tests/provider-k8s/task.yaml | 11 ++++++-- 5 files changed, 73 insertions(+), 11 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index ff38d8fb..7302eca9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -97,7 +97,7 @@ func parseConfig(configFile string) (*Config, error) { slog.Info("Configuration file found", "path", "concierge.yaml") } - fixNilSnapEntries(viper.GetViper()) + fixNilYAMLEntries(viper.GetViper()) conf := &Config{} err := viper.Unmarshal(conf) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 78e367de..3b3bed71 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -136,6 +136,52 @@ host: } } +func TestK8sFeaturesWithoutConfig(t *testing.T) { + yamlConfig := ` +providers: + k8s: + enable: true + bootstrap: true + features: + dns: + ingress: + local-storage: + network: + load-balancer: + l2-mode: "true" + cidrs: 10.0.0.0/24 +` + + tmpFile, err := os.CreateTemp("", "concierge-test-*.yaml") + if err != nil { + t.Fatal(err) + } + defer func() { _ = os.Remove(tmpFile.Name()) }() + + if _, err := tmpFile.Write([]byte(yamlConfig)); err != nil { + t.Fatal(err) + } + if err := tmpFile.Close(); err != nil { + t.Fatal(err) + } + + cfg, err := parseConfig(tmpFile.Name()) + if err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + for _, name := range []string{"dns", "ingress", "local-storage", "network", "load-balancer"} { + if _, ok := cfg.Providers.K8s.Features[name]; !ok { + t.Fatalf("expected k8s feature %q to be present in map, got: %v", name, cfg.Providers.K8s.Features) + } + } + + lb := cfg.Providers.K8s.Features["load-balancer"] + if lb["l2-mode"] != "true" || lb["cidrs"] != "10.0.0.0/24" { + t.Fatalf("expected load-balancer config preserved, got: %v", lb) + } +} + func TestExtraBootstrapArgsFromYAML(t *testing.T) { yamlConfig := ` juju: diff --git a/internal/config/presets.go b/internal/config/presets.go index 49a21b2d..a1194b78 100644 --- a/internal/config/presets.go +++ b/internal/config/presets.go @@ -42,18 +42,26 @@ func Preset(preset string) (*Config, error) { return loadPreset(data) } -// fixNilSnapEntries replaces nil-valued snap entries with empty maps so that -// Viper's Unmarshal does not silently drop bare YAML keys like "charmcraft:". -func fixNilSnapEntries(v *viper.Viper) { - if snaps, ok := v.Get("host.snaps").(map[string]any); ok { - for name, val := range snaps { +// fixNilMapEntries replaces nil-valued entries within the map at the given +// Viper key with empty maps, so that Unmarshal does not silently drop bare +// YAML keys like "charmcraft:" or "ingress:". +func fixNilMapEntries(v *viper.Viper, key string) { + if entries, ok := v.Get(key).(map[string]any); ok { + for name, val := range entries { if val == nil { - v.Set("host.snaps."+name, map[string]any{}) + v.Set(key+"."+name, map[string]any{}) } } } } +// fixNilYAMLEntries fixes bare YAML keys that Viper would otherwise silently +// drop when unmarshalling into typed maps. +func fixNilYAMLEntries(v *viper.Viper) { + fixNilMapEntries(v, "host.snaps") + fixNilMapEntries(v, "providers.k8s.features") +} + // loadPreset parses YAML data into a Config using a fresh Viper instance. func loadPreset(data []byte) (*Config, error) { v := viper.New() @@ -63,7 +71,7 @@ func loadPreset(data []byte) (*Config, error) { return nil, fmt.Errorf("failed to parse preset: %w", err) } - fixNilSnapEntries(v) + fixNilYAMLEntries(v) conf := &Config{} if err := v.Unmarshal(conf); err != nil { diff --git a/tests/provider-k8s/concierge.yaml b/tests/provider-k8s/concierge.yaml index 07740566..f9970e1e 100644 --- a/tests/provider-k8s/concierge.yaml +++ b/tests/provider-k8s/concierge.yaml @@ -4,7 +4,10 @@ providers: bootstrap: true channel: 1.32-classic/stable features: + dns: + ingress: local-storage: + network: load-balancer: l2-mode: true cidrs: 10.64.140.43/32 diff --git a/tests/provider-k8s/task.yaml b/tests/provider-k8s/task.yaml index e9da8a1a..1cc7c33f 100644 --- a/tests/provider-k8s/task.yaml +++ b/tests/provider-k8s/task.yaml @@ -15,9 +15,14 @@ execute: | echo $list | MATCH juju echo $list | MATCH kubectl - sudo k8s status --output-format yaml | yq '.dns.enabled' | MATCH true - sudo k8s status --output-format yaml | yq '.load-balancer.enabled' | MATCH true - sudo k8s status --output-format yaml | yq '.load-balancer.message' | MATCH "enabled, L2 mode" + status="$(sudo k8s status --output-format yaml)" + # Bare-key features (no inline config) must still be enabled — see issue #189. + echo "$status" | yq '.dns.enabled' | MATCH true + echo "$status" | yq '.ingress.enabled' | MATCH true + echo "$status" | yq '.["local-storage"].enabled' | MATCH true + echo "$status" | yq '.network.enabled' | MATCH true + echo "$status" | yq '.["load-balancer"].enabled' | MATCH true + echo "$status" | yq '.["load-balancer"].message' | MATCH "enabled, L2 mode" sudo k8s get | yq '.load-balancer.cidrs' | MATCH "10.64.140.43/32" kubectl config current-context | MATCH "k8s"