Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ require (
github.com/schollz/progressbar/v3 v3.19.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
golang.org/x/sync v0.19.0
google.golang.org/grpc v1.72.3
google.golang.org/protobuf v1.36.8
Expand Down Expand Up @@ -49,14 +48,12 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.0.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
Expand All @@ -78,7 +75,6 @@ require (
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand All @@ -88,12 +84,7 @@ require (
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
Expand Down
20 changes: 0 additions & 20 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,6 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
Expand All @@ -92,8 +88,6 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
Expand Down Expand Up @@ -168,8 +162,6 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
Expand All @@ -192,27 +184,17 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand All @@ -227,8 +209,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
Expand Down
33 changes: 10 additions & 23 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package config

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sync"

"github.com/spf13/viper"
)

const (
Expand All @@ -18,9 +19,6 @@ const (
defaultLogDir = "/var/log/aks-flex-node"
defaultLogLevel = "info"
defaultAzureCloud = "AzurePublicCloud"

// Environment variable prefix
envPrefix = "AKS_NODE_CONTROLLER"
)

// Singleton instance for configuration
Expand All @@ -38,38 +36,27 @@ func GetConfig() *Config {
return configInstance
}

// LoadConfig loads configuration from a JSON file and environment variables.
// LoadConfig loads configuration from a JSON file.
// The configPath parameter is required and cannot be empty.
// Environment variables can override config file values using the AKS_NODE_CONTROLLER_ prefix.
// For example: AKS_NODE_CONTROLLER_AZURE_LOCATION=westus2
func LoadConfig(configPath string) (*Config, error) {
// Require config path to be specified
if configPath == "" {
return nil, fmt.Errorf("config file path is required")
}

// Set up viper
v := viper.New()
v.SetConfigType("json")
v.AutomaticEnv()
v.SetEnvPrefix(envPrefix)

// Load the specified config file
v.SetConfigFile(configPath)
if err := v.ReadInConfig(); err != nil {
data, err := os.ReadFile(filepath.Clean(configPath))
if err != nil {
return nil, fmt.Errorf("failed to read config file at %s: %w", configPath, err)
}

// Unmarshal config
config := &Config{}
if err := v.Unmarshal(config); err != nil {
if err := json.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("error unmarshaling config: %w", err)
}
Comment on lines +47 to 55
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.Unmarshal silently ignores unknown fields, which can allow config typos to go unnoticed (potentially leading to confusing runtime behavior). Consider decoding via json.Decoder with DisallowUnknownFields() and returning an error that includes the configPath to make misconfigurations easier to diagnose.

Copilot uses AI. Check for mistakes.

// Track if managedIdentity was explicitly set in config
// This is necessary because viper unmarshals empty JSON objects {} as nil pointers
// Using viper.IsSet() correctly detects if the key was present in the config file
config.isMIExplicitlySet = v.IsSet("azure.managedIdentity")
// encoding/json deserializes "managedIdentity": {} into a non-nil pointer, so
// presence can be detected directly — unlike Viper which decoded it as nil.
config.isMIExplicitlySet = config.Azure.ManagedIdentity != nil

// Set defaults for any missing values
config.SetDefaults()
Expand Down
124 changes: 124 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package config

import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"testing"
)
Expand Down Expand Up @@ -1069,6 +1071,128 @@ func TestAuthenticationMethodValidation(t *testing.T) {
}
}

// baseAzureJSON is the minimal valid azure config block shared across label tests.
const baseAzureJSON = `{
"azure": {
"subscriptionId": "12345678-1234-1234-1234-123456789012",
"tenantId": "12345678-1234-1234-1234-123456789012",
"cloud": "AzurePublicCloud",
"bootstrapToken": { "token": "abcdef.0123456789abcdef" },
"targetCluster": {
"resourceId": "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster",
"location": "eastus"
}
},
"node": {
"kubelet": {
"serverURL": "https://test-cluster.hcp.eastus.azmk8s.io:443",
"caCertData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t"
},
"labels": %s
}
}`

// TestLoadConfigNodeLabels verifies that node label keys are preserved exactly
// as written in the config file. This is the root motivation: Kubernetes label
// keys such as "cleanroom.azure.com/flexnode" or "topology.kubernetes.io/zone"
// contain dots, which Viper's default "." key-path delimiter misinterpreted as
// nested JSON keys, silently dropping the labels on load.
func TestLoadConfigNodeLabels(t *testing.T) {
tests := []struct {
name string
labelsJSON string
expectedLabels map[string]string // only the user-supplied labels; defaults are checked separately
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comment says “defaults are checked separately”, but this test function doesn’t appear to verify defaults (and there’s no pointer here to where that happens). Either add an explicit assertion for expected default labels in this test, or adjust/remove the comment to avoid misleading future maintainers.

Suggested change
expectedLabels map[string]string // only the user-supplied labels; defaults are checked separately
expectedLabels map[string]string // labels expected to be preserved from the config file

Copilot uses AI. Check for mistakes.
}{
{
name: "plain labels without dots",
labelsJSON: `{"env": "production", "team": "platform"}`,
expectedLabels: map[string]string{
"env": "production",
"team": "platform",
},
},
{
name: "label key with a single dot segment (kubernetes.io prefix)",
labelsJSON: `{"kubernetes.io/nodeReady": "true"}`,
expectedLabels: map[string]string{
"kubernetes.io/nodeReady": "true",
},
},
{
name: "label key with multiple dot segments (topology prefix)",
labelsJSON: `{"topology.kubernetes.io/zone": "eastus-1"}`,
expectedLabels: map[string]string{
"topology.kubernetes.io/zone": "eastus-1",
},
},
{
name: "label key with three dot segments (org.example.com prefix)",
labelsJSON: `{"org.example.com/myLabel": "true"}`,
expectedLabels: map[string]string{
"org.example.com/myLabel": "true",
},
},
{
name: "mixed dotted and plain labels all preserved",
labelsJSON: `{
"env": "staging",
"topology.kubernetes.io/zone": "eastus-1",
"cleanroom.azure.com/flexnode": "true",
"disktype": "ssd"
}`,
expectedLabels: map[string]string{
"env": "staging",
"topology.kubernetes.io/zone": "eastus-1",
"cleanroom.azure.com/flexnode": "true",
"disktype": "ssd",
},
},
{
name: "label value containing dots is preserved",
labelsJSON: `{"version": "1.2.3"}`,
expectedLabels: map[string]string{
"version": "1.2.3",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

configJSON := fmt.Sprintf(baseAzureJSON, tt.labelsJSON)
Comment on lines +1159 to +1163
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t.Parallel() inside a range loop subtest captures the loop variable tt. On Go versions prior to 1.22 this can cause all parallel subtests to read the final tt value (flaky/incorrect assertions). Rebind tt := tt (or index into the slice) before t.Run to make the tests correct across Go versions.

Copilot uses AI. Check for mistakes.
configFile := filepath.Join(t.TempDir(), "config.json")
if err := os.WriteFile(configFile, []byte(configJSON), 0o600); err != nil {
t.Fatalf("os.WriteFile: %v", err)
}

config, err := LoadConfig(configFile)
if err != nil {
t.Fatalf("LoadConfig() unexpected error: %v", err)
}

for key, want := range tt.expectedLabels {
got, ok := config.Node.Labels[key]
if !ok {
t.Errorf("label %q not found; got keys: %v", key, labelKeys(config.Node.Labels))
} else if got != want {
t.Errorf("label %q = %q, want %q", key, got, want)
}
}
})
}
}

// labelKeys returns sorted keys of a map for use in test diagnostics.
func labelKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}

func TestIsBootstrapTokenConfigured(t *testing.T) {
tests := []struct {
name string
Expand Down
1 change: 0 additions & 1 deletion pkg/config/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ func (cfg *Config) IsSPConfigured() bool {
}

// IsMIConfigured checks if managed identity configuration is provided in the configuration
// Uses internal flag set during config loading to handle viper's empty object behavior
func (cfg *Config) IsMIConfigured() bool {
return cfg.isMIExplicitlySet
}
Expand Down
Loading