Repository: open-telemetry/opentelemetry-operator (go)

Issue:
Some file config of OpAMPBridge ALWAYS overriden by CLI
### Component(s)

OpAMP Bridge

### Describe the issue you're reporting

because of this [function](https://github.com/open-telemetry/opentelemetry-operator/blob/main/cmd/operator-opamp-bridge/internal/config/config.go#L254-L292), some values written in config.yaml are always overriden by default values.

listen-addr
kubeconfig-path
heartbeat-interval
name

Since these fields have YAML tags in the Config struct, I believe the values should be injectable via a configuration file.
Is this intentional?


Pull request:
[target allocator] Fix config loading priority
**Description:**

Fix the target allocator's config loading priority to reflect our intent. We had a problem where some values were taken from command-line flag defaults even if the flags weren't set, making it impossible to set them through the config file.

To achieve this, I've:

* Set all the defaults in the defaulting function.
* Use flag values only if they're actually set, for flags where this wasn't the case.
* Add tests for the config loading priority.

**Link to tracking Issue(s):**

Partially fixes #3928.


Patch:
diff --git a/.chloggen/fix_ta-cmdline-values.yaml b/.chloggen/fix_ta-cmdline-values.yaml
new file mode 100755
index 00000000..9734ece9
--- /dev/null
+++ b/.chloggen/fix_ta-cmdline-values.yaml
@@ -0,0 +1,16 @@
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: bug_fix
+
+# The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action)
+component: target allocator
+
+# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Fix config loading priority
+
+# One or more tracking issues related to the change
+issues: [3928]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext:
\ No newline at end of file
diff --git a/cmd/otel-allocator/internal/config/config.go b/cmd/otel-allocator/internal/config/config.go
index f118fc33..bc51a1d6 100644
--- a/cmd/otel-allocator/internal/config/config.go
+++ b/cmd/otel-allocator/internal/config/config.go
@@ -12,6 +12,7 @@ import (
 	"log/slog"
 	"math"
 	"os"
+	"path/filepath"
 	"reflect"
 	"time"
 
@@ -26,12 +27,15 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/util/homedir"
 	"k8s.io/klog/v2"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 )
 
 const (
+	DefaultListenAddr                                  = ":8080"
+	DefaultHttpsListenAddr                             = ":8443"
 	DefaultResyncTime                                  = 5 * time.Minute
 	DefaultConfigFilePath               string         = "/conf/targetallocator.yaml"
 	DefaultCRScrapeInterval             model.Duration = model.Duration(time.Second * 30)
@@ -40,6 +44,10 @@ const (
 	DefaultCollectorNotReadyGracePeriod                = 0 * time.Second
 )
 
+var (
+	DefaultKubeConfigFilePath string = filepath.Join(homedir.HomeDir(), ".kube", "config")
+)
+
 // By default, scrape protocols include PrometheusText1_0_0, which only Prometheus >=3.0 supports.
 // Manually exclude this protocol until several versions of the Otel Collector support it.
 var DefaultScrapeProtocols = []promconfig.ScrapeProtocol{
@@ -227,9 +235,10 @@ func LoadFromCLI(target *Config, flagSet *pflag.FlagSet) error {
 	klog.SetLogger(target.RootLogger)
 	ctrl.SetLogger(target.RootLogger)
 
-	target.KubeConfigFilePath, err = getKubeConfigFilePath(flagSet)
-	if err != nil {
-		return err
+	if kubeConfigFilePath, changed, flagErr := getKubeConfigFilePath(flagSet); flagErr != nil {
+		return flagErr
+	} else if changed {
+		target.KubeConfigFilePath = kubeConfigFilePath
 	}
 	clusterConfig, err := clientcmd.BuildConfigFromFlags("", target.KubeConfigFilePath)
 	if err != nil {
@@ -245,9 +254,10 @@ func LoadFromCLI(target *Config, flagSet *pflag.FlagSet) error {
 	}
 	target.ClusterConfig = clusterConfig
 
-	target.ListenAddr, err = getListenAddr(flagSet)
-	if err != nil {
-		return err
+	if listenAddr, changed, flagErr := getListenAddr(flagSet); flagErr != nil {
+		return flagErr
+	} else if changed {
+		target.ListenAddr = listenAddr
 	}
 
 	if prometheusCREnabled, changed, flagErr := getPrometheusCREnabled(flagSet); flagErr != nil {
@@ -335,6 +345,11 @@ func unmarshal(cfg *Config, configFile string) error {
 
 func CreateDefaultConfig() Config {
 	return Config{
+		ListenAddr:         DefaultListenAddr,
+		KubeConfigFilePath: DefaultKubeConfigFilePath,
+		HTTPS: HTTPSServerConfig{
+			ListenAddr: DefaultHttpsListenAddr,
+		},
 		AllocationStrategy:         DefaultAllocationStrategy,
 		AllocationFallbackStrategy: "",
 		FilterStrategy:             DefaultFilterStrategy,
@@ -349,11 +364,11 @@ func CreateDefaultConfig() Config {
 	}
 }
 
-func Load() (*Config, error) {
+func Load(args []string) (*Config, error) {
 	var err error
 
 	flagSet := getFlagSet(pflag.ExitOnError)
-	err = flagSet.Parse(os.Args)
+	err = flagSet.Parse(args)
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/otel-allocator/internal/config/flags.go b/cmd/otel-allocator/internal/config/flags.go
index 5c812e8f..ae156ff8 100644
--- a/cmd/otel-allocator/internal/config/flags.go
+++ b/cmd/otel-allocator/internal/config/flags.go
@@ -5,10 +5,8 @@ package config
 
 import (
 	"flag"
-	"path/filepath"
 
 	"github.com/spf13/pflag"
-	"k8s.io/client-go/util/homedir"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 )
 
@@ -32,11 +30,11 @@ var zapCmdLineOpts zap.Options
 func getFlagSet(errorHandling pflag.ErrorHandling) *pflag.FlagSet {
 	flagSet := pflag.NewFlagSet(targetAllocatorName, errorHandling)
 	flagSet.String(configFilePathFlagName, DefaultConfigFilePath, "The path to the config file.")
-	flagSet.String(listenAddrFlagName, ":8080", "The address where this service serves.")
+	flagSet.String(listenAddrFlagName, DefaultListenAddr, "The address where this service serves.")
 	flagSet.Bool(prometheusCREnabledFlagName, false, "Enable Prometheus CRs as target sources")
-	flagSet.String(kubeConfigPathFlagName, filepath.Join(homedir.HomeDir(), ".kube", "config"), "absolute path to the KubeconfigPath file")
+	flagSet.String(kubeConfigPathFlagName, DefaultKubeConfigFilePath, "absolute path to the KubeconfigPath file")
 	flagSet.Bool(httpsEnabledFlagName, false, "Enable HTTPS additional server")
-	flagSet.String(listenAddrHttpsFlagName, ":8443", "The address where this service serves over HTTPS.")
+	flagSet.String(listenAddrHttpsFlagName, DefaultHttpsListenAddr, "The address where this service serves over HTTPS.")
 	flagSet.String(httpsCAFilePathFlagName, "", "The path to the HTTPS server TLS CA file.")
 	flagSet.String(httpsTLSCertFilePathFlagName, "", "The path to the HTTPS server TLS certificate file.")
 	flagSet.String(httpsTLSKeyFilePathFlagName, "", "The path to the HTTPS server TLS key file.")
@@ -50,64 +48,54 @@ func getConfigFilePath(flagSet *pflag.FlagSet) (string, error) {
 	return flagSet.GetString(configFilePathFlagName)
 }
 
-func getKubeConfigFilePath(flagSet *pflag.FlagSet) (string, error) {
-	return flagSet.GetString(kubeConfigPathFlagName)
+func getKubeConfigFilePath(flagSet *pflag.FlagSet) (value string, changed bool, err error) {
+	return getFlagValueAndChangedString(flagSet, kubeConfigPathFlagName)
 }
 
-func getListenAddr(flagSet *pflag.FlagSet) (string, error) {
-	return flagSet.GetString(listenAddrFlagName)
+func getListenAddr(flagSet *pflag.FlagSet) (value string, changed bool, err error) {
+	return getFlagValueAndChangedString(flagSet, listenAddrFlagName)
 }
 
 func getPrometheusCREnabled(flagSet *pflag.FlagSet) (value bool, changed bool, err error) {
-	if changed = flagSet.Changed(prometheusCREnabledFlagName); !changed {
-		value, err = false, nil
-		return
-	}
-	value, err = flagSet.GetBool(prometheusCREnabledFlagName)
-	return
+	return getFlagValueAndChangedBool(flagSet, prometheusCREnabledFlagName)
 }
 
 func getHttpsListenAddr(flagSet *pflag.FlagSet) (value string, changed bool, err error) {
-	if changed = flagSet.Changed(listenAddrHttpsFlagName); !changed {
-		value, err = ":8443", nil
-		return
-	}
-	value, err = flagSet.GetString(listenAddrHttpsFlagName)
-	return
+	return getFlagValueAndChangedString(flagSet, listenAddrHttpsFlagName)
 }
 
 func getHttpsEnabled(flagSet *pflag.FlagSet) (value bool, changed bool, err error) {
-	if changed = flagSet.Changed(httpsEnabledFlagName); !changed {
-		value, err = false, nil
-		return
-	}
-	value, err = flagSet.GetBool(httpsEnabledFlagName)
-	return
+	return getFlagValueAndChangedBool(flagSet, httpsEnabledFlagName)
 }
 
 func getHttpsCAFilePath(flagSet *pflag.FlagSet) (value string, changed bool, err error) {
-	if changed = flagSet.Changed(httpsCAFilePathFlagName); !changed {
-		value, err = "", nil
-		return
-	}
-	value, err = flagSet.GetString(httpsCAFilePathFlagName)
-	return
+	return getFlagValueAndChangedString(flagSet, httpsCAFilePathFlagName)
 }
 
 func getHttpsTLSCertFilePath(flagSet *pflag.FlagSet) (value string, changed bool, err error) {
-	if changed = flagSet.Changed(httpsTLSCertFilePathFlagName); !changed {
+	return getFlagValueAndChangedString(flagSet, httpsTLSCertFilePathFlagName)
+}
+
+func getHttpsTLSKeyFilePath(flagSet *pflag.FlagSet) (value string, changed bool, err error) {
+	return getFlagValueAndChangedString(flagSet, httpsTLSKeyFilePathFlagName)
+}
+
+// getFlagValueAndChanged returns the given flag's string value and whether it was changed.
+func getFlagValueAndChangedString(flagSet *pflag.FlagSet, flagName string) (value string, changed bool, err error) {
+	if changed = flagSet.Changed(flagName); !changed {
 		value, err = "", nil
 		return
 	}
-	value, err = flagSet.GetString(httpsTLSCertFilePathFlagName)
+	value, err = flagSet.GetString(flagName)
 	return
 }
 
-func getHttpsTLSKeyFilePath(flagSet *pflag.FlagSet) (value string, changed bool, err error) {
-	if changed = flagSet.Changed(httpsTLSKeyFilePathFlagName); !changed {
-		value, err = "", nil
+// getFlagValueAndChanged returns the given flag's string value and whether it was changed.
+func getFlagValueAndChangedBool(flagSet *pflag.FlagSet, flagName string) (value bool, changed bool, err error) {
+	if changed = flagSet.Changed(flagName); !changed {
+		value, err = false, nil
 		return
 	}
-	value, err = flagSet.GetString(httpsTLSKeyFilePathFlagName)
+	value, err = flagSet.GetBool(flagName)
 	return
 }
diff --git a/cmd/otel-allocator/main.go b/cmd/otel-allocator/main.go
index 1b387585..6035080f 100644
--- a/cmd/otel-allocator/main.go
+++ b/cmd/otel-allocator/main.go
@@ -52,7 +52,7 @@ func main() {
 		interrupts      = make(chan os.Signal, 1)
 		errChan         = make(chan error)
 	)
-	cfg, err := config.Load()
+	cfg, err := config.Load(os.Args)
 	if err != nil {
 		fmt.Printf("Failed to load config: %v", err)
 		os.Exit(1)


Test patch:
diff --git a/cmd/otel-allocator/internal/config/config_test.go b/cmd/otel-allocator/internal/config/config_test.go
index 9df390f4..7593681e 100644
--- a/cmd/otel-allocator/internal/config/config_test.go
+++ b/cmd/otel-allocator/internal/config/config_test.go
@@ -6,6 +6,7 @@ package config
 import (
 	"fmt"
 	"os"
+	"path/filepath"
 	"testing"
 	"time"
 
@@ -20,7 +21,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-func TestLoad(t *testing.T) {
+func TestLoadFromFile(t *testing.T) {
 	type args struct {
 		file string
 	}
@@ -36,6 +37,8 @@ func TestLoad(t *testing.T) {
 				file: "./testdata/config_test.yaml",
 			},
 			want: Config{
+				ListenAddr:         DefaultListenAddr,
+				KubeConfigFilePath: DefaultKubeConfigFilePath,
 				AllocationStrategy: DefaultAllocationStrategy,
 				CollectorNamespace: "default",
 				CollectorSelector: &metav1.LabelSelector{
@@ -122,6 +125,8 @@ func TestLoad(t *testing.T) {
 				file: "./testdata/pod_service_selector_test.yaml",
 			},
 			want: Config{
+				ListenAddr:         DefaultListenAddr,
+				KubeConfigFilePath: DefaultKubeConfigFilePath,
 				AllocationStrategy: DefaultAllocationStrategy,
 				CollectorNamespace: "default",
 				CollectorSelector: &metav1.LabelSelector{
@@ -148,6 +153,9 @@ func TestLoad(t *testing.T) {
 					ProbeNamespaceSelector:          &metav1.LabelSelector{},
 					ScrapeInterval:                  DefaultCRScrapeInterval,
 				},
+				HTTPS: HTTPSServerConfig{
+					ListenAddr: ":8443",
+				},
 				PromConfig: &promconfig.Config{
 					GlobalConfig: promconfig.GlobalConfig{
 						ScrapeInterval:     model.Duration(60 * time.Second),
@@ -197,6 +205,8 @@ func TestLoad(t *testing.T) {
 				file: "./testdata/pod_service_selector_camelcase_test.yaml",
 			},
 			want: Config{
+				ListenAddr:         DefaultListenAddr,
+				KubeConfigFilePath: DefaultKubeConfigFilePath,
 				AllocationStrategy: DefaultAllocationStrategy,
 				CollectorNamespace: "default",
 				CollectorSelector: &metav1.LabelSelector{
@@ -223,6 +233,9 @@ func TestLoad(t *testing.T) {
 					ProbeNamespaceSelector:          &metav1.LabelSelector{},
 					ScrapeInterval:                  DefaultCRScrapeInterval,
 				},
+				HTTPS: HTTPSServerConfig{
+					ListenAddr: ":8443",
+				},
 				PromConfig: &promconfig.Config{
 					GlobalConfig: promconfig.GlobalConfig{
 						ScrapeInterval:     model.Duration(60 * time.Second),
@@ -272,6 +285,8 @@ func TestLoad(t *testing.T) {
 				file: "./testdata/pod_service_selector_expressions_test.yaml",
 			},
 			want: Config{
+				ListenAddr:         DefaultListenAddr,
+				KubeConfigFilePath: DefaultKubeConfigFilePath,
 				AllocationStrategy: DefaultAllocationStrategy,
 				CollectorNamespace: "default",
 				CollectorSelector: &metav1.LabelSelector{
@@ -322,6 +337,9 @@ func TestLoad(t *testing.T) {
 					ProbeNamespaceSelector:          &metav1.LabelSelector{},
 					ScrapeInterval:                  DefaultCRScrapeInterval,
 				},
+				HTTPS: HTTPSServerConfig{
+					ListenAddr: ":8443",
+				},
 				PromConfig: &promconfig.Config{
 					GlobalConfig: promconfig.GlobalConfig{
 						ScrapeInterval:     model.Duration(60 * time.Second),
@@ -371,6 +389,8 @@ func TestLoad(t *testing.T) {
 				file: "./testdata/pod_service_selector_camelcase_expressions_test.yaml",
 			},
 			want: Config{
+				ListenAddr:         DefaultListenAddr,
+				KubeConfigFilePath: DefaultKubeConfigFilePath,
 				AllocationStrategy: DefaultAllocationStrategy,
 				CollectorNamespace: "default",
 				CollectorSelector: &metav1.LabelSelector{
@@ -421,6 +441,9 @@ func TestLoad(t *testing.T) {
 					ProbeNamespaceSelector:          &metav1.LabelSelector{},
 					ScrapeInterval:                  DefaultCRScrapeInterval,
 				},
+				HTTPS: HTTPSServerConfig{
+					ListenAddr: ":8443",
+				},
 				PromConfig: &promconfig.Config{
 					GlobalConfig: promconfig.GlobalConfig{
 						ScrapeInterval:     model.Duration(60 * time.Second),
@@ -599,3 +622,259 @@ func TestGetAllowDenyLists(t *testing.T) {
 		})
 	}
 }
+
+func TestConfigLoadPriority(t *testing.T) {
+	// Helper function to create a dummy kube config for tests
+	createDummyKubeConfig := func(t *testing.T, dir string) string {
+		kubeConfigPath := filepath.Join(dir, "kube.config")
+		kubeConfigContent := `
+apiVersion: v1
+kind: Config
+clusters:
+- cluster:
+    server: https://example.com
+  name: test-cluster
+contexts:
+- context:
+    cluster: test-cluster
+    user: test-user
+  name: test-context
+current-context: test-context
+users:
+- name: test-user
+  user:
+    token: dummy-token
+`
+		err := os.WriteFile(kubeConfigPath, []byte(kubeConfigContent), 0600)
+		require.NoError(t, err)
+		return kubeConfigPath
+	}
+
+	t.Run("default values when nothing is set", func(t *testing.T) {
+		// Setup: create empty config file and dummy kube config
+		tempDir := t.TempDir()
+		emptyConfigPath := filepath.Join(tempDir, "empty.yaml")
+		err := os.WriteFile(emptyConfigPath, []byte("{}"), 0600)
+		require.NoError(t, err)
+
+		kubeConfigPath := createDummyKubeConfig(t, tempDir)
+
+		// Prepare args for Load function
+		args := []string{
+			"--" + configFilePathFlagName + "=" + emptyConfigPath,
+			"--" + kubeConfigPathFlagName + "=" + kubeConfigPath,
+		}
+
+		// Load config using the full Load function
+		config, err := Load(args)
+		require.NoError(t, err)
+
+		// Assert defaults are used
+		assert.Equal(t, DefaultListenAddr, config.ListenAddr)
+		assert.Equal(t, kubeConfigPath, config.KubeConfigFilePath)
+		assert.Equal(t, DefaultHttpsListenAddr, config.HTTPS.ListenAddr)
+		assert.Equal(t, DefaultAllocationStrategy, config.AllocationStrategy)
+		assert.Equal(t, DefaultFilterStrategy, config.FilterStrategy)
+		assert.False(t, config.PrometheusCR.Enabled)
+		assert.False(t, config.HTTPS.Enabled)
+	})
+
+	t.Run("command-line has priority over config file for boolean values", func(t *testing.T) {
+		// Setup: create config file with values and dummy kube config
+		tempDir := t.TempDir()
+		configContent := `
+prometheus_cr:
+  enabled: false
+https:
+  enabled: false
+`
+		configPath := filepath.Join(tempDir, "config.yaml")
+		err := os.WriteFile(configPath, []byte(configContent), 0600)
+		require.NoError(t, err)
+
+		kubeConfigPath := createDummyKubeConfig(t, tempDir)
+
+		// Prepare args for Load function that override config file
+		args := []string{
+			"--" + configFilePathFlagName + "=" + configPath,
+			"--" + prometheusCREnabledFlagName + "=true",
+			"--" + httpsEnabledFlagName + "=true",
+			"--" + kubeConfigPathFlagName + "=" + kubeConfigPath,
+		}
+
+		// Load config using the full Load function
+		config, err := Load(args)
+		require.NoError(t, err)
+
+		// Assert CLI values override config file
+		assert.True(t, config.PrometheusCR.Enabled, "CLI should override config file for prometheus CR enabled")
+		assert.True(t, config.HTTPS.Enabled, "CLI should override config file for HTTPS enabled")
+	})
+
+	t.Run("command-line has priority over config file for string values", func(t *testing.T) {
+		// Setup: create config file with values and dummy kube config
+		tempDir := t.TempDir()
+		configContent := `
+listen_addr: ":9090"
+https:
+  listen_addr: ":9443"
+  ca_file_path: "/config/ca.pem"
+  tls_cert_file_path: "/config/cert.pem"
+  tls_key_file_path: "/config/key.pem"
+kube_config_file_path: "/config/kube.config"
+`
+		configPath := filepath.Join(tempDir, "config.yaml")
+		err := os.WriteFile(configPath, []byte(configContent), 0600)
+		require.NoError(t, err)
+
+		kubeConfigPath := createDummyKubeConfig(t, tempDir)
+
+		// CLI values different from config file
+		cliListenAddr := ":8888"
+		cliHttpsListenAddr := ":8443"
+		cliCAPath := "/cli/ca.pem"
+		cliCertPath := "/cli/cert.pem"
+		cliKeyPath := "/cli/key.pem"
+
+		// Prepare args for Load function that override config file
+		args := []string{
+			"--" + configFilePathFlagName + "=" + configPath,
+			"--" + listenAddrFlagName + "=" + cliListenAddr,
+			"--" + listenAddrHttpsFlagName + "=" + cliHttpsListenAddr,
+			"--" + httpsCAFilePathFlagName + "=" + cliCAPath,
+			"--" + httpsTLSCertFilePathFlagName + "=" + cliCertPath,
+			"--" + httpsTLSKeyFilePathFlagName + "=" + cliKeyPath,
+			"--" + kubeConfigPathFlagName + "=" + kubeConfigPath,
+		}
+
+		// Load config using the full Load function
+		config, err := Load(args)
+		require.NoError(t, err)
+
+		// Assert CLI values override config file
+		assert.Equal(t, cliListenAddr, config.ListenAddr, "CLI should override config file for listen address")
+		assert.Equal(t, cliHttpsListenAddr, config.HTTPS.ListenAddr, "CLI should override config file for HTTPS listen address")
+		assert.Equal(t, cliCAPath, config.HTTPS.CAFilePath, "CLI should override config file for CA file path")
+		assert.Equal(t, cliCertPath, config.HTTPS.TLSCertFilePath, "CLI should override config file for TLS cert file path")
+		assert.Equal(t, cliKeyPath, config.HTTPS.TLSKeyFilePath, "CLI should override config file for TLS key file path")
+		assert.Equal(t, kubeConfigPath, config.KubeConfigFilePath, "CLI should override config file for kube config path")
+	})
+
+	t.Run("config file overrides defaults when CLI not specified", func(t *testing.T) {
+		// Setup: create config file with values and dummy kube config
+		tempDir := t.TempDir()
+		configListenAddr := ":7070"
+		kubeConfigPath := createDummyKubeConfig(t, tempDir)
+
+		configContent := `
+listen_addr: "` + configListenAddr + `"
+prometheus_cr:
+  enabled: true
+https:
+  enabled: true
+  listen_addr: ":7443"
+kube_config_file_path: "` + kubeConfigPath + `"
+`
+		configPath := filepath.Join(tempDir, "config.yaml")
+		err := os.WriteFile(configPath, []byte(configContent), 0600)
+		require.NoError(t, err)
+
+		// Prepare args for Load function with only config file path
+		args := []string{
+			"--" + configFilePathFlagName + "=" + configPath,
+		}
+
+		// Load config using the full Load function
+		config, err := Load(args)
+		require.NoError(t, err)
+
+		// Assert config file values override defaults
+		assert.Equal(t, configListenAddr, config.ListenAddr, "Config file should override defaults for listen address")
+		assert.True(t, config.PrometheusCR.Enabled, "Config file should override defaults for prometheus CR enabled")
+		assert.True(t, config.HTTPS.Enabled, "Config file should override defaults for HTTPS enabled")
+		assert.Equal(t, ":7443", config.HTTPS.ListenAddr, "Config file should override defaults for HTTPS listen address")
+		assert.Equal(t, kubeConfigPath, config.KubeConfigFilePath, "Config file should set kube config path")
+	})
+
+	t.Run("environment variables are applied", func(t *testing.T) {
+		// Setup: create empty config file and dummy kube config
+		tempDir := t.TempDir()
+		emptyConfigPath := filepath.Join(tempDir, "empty.yaml")
+		err := os.WriteFile(emptyConfigPath, []byte("{}"), 0600)
+		require.NoError(t, err)
+
+		kubeConfigPath := createDummyKubeConfig(t, tempDir)
+
+		// Save original env var value and restore after test
+		originalNamespace := os.Getenv("OTELCOL_NAMESPACE")
+		defer func() {
+			envErr := os.Setenv("OTELCOL_NAMESPACE", originalNamespace)
+			assert.NoError(t, envErr)
+		}()
+
+		// Set environment variable
+		testNamespace := "test-namespace"
+		err = os.Setenv("OTELCOL_NAMESPACE", testNamespace)
+		require.NoError(t, err)
+
+		// Prepare args for Load function
+		args := []string{
+			"--" + configFilePathFlagName + "=" + emptyConfigPath,
+			"--" + kubeConfigPathFlagName + "=" + kubeConfigPath,
+		}
+
+		// Load config using the full Load function
+		config, err := Load(args)
+		require.NoError(t, err)
+
+		// Assert environment variable is applied
+		assert.Equal(t, testNamespace, config.CollectorNamespace, "Environment variable should be applied")
+	})
+
+	t.Run("loading priority order: defaults <- config file <- env vars <- CLI", func(t *testing.T) {
+		// Setup: create config file with values and dummy kube config
+		tempDir := t.TempDir()
+		kubeConfigPath := createDummyKubeConfig(t, tempDir)
+
+		// Config file sets values
+		configContent := `
+collector_namespace: "config-file-namespace"
+listen_addr: ":9090"
+prometheus_cr:
+  enabled: false
+kube_config_file_path: "` + kubeConfigPath + `"
+`
+		configPath := filepath.Join(tempDir, "config.yaml")
+		err := os.WriteFile(configPath, []byte(configContent), 0600)
+		require.NoError(t, err)
+
+		// Save original env var value and restore after test
+		originalNamespace := os.Getenv("OTELCOL_NAMESPACE")
+		defer func() {
+			envErr := os.Setenv("OTELCOL_NAMESPACE", originalNamespace)
+			assert.NoError(t, envErr)
+		}()
+
+		// Environment variable sets value
+		testNamespace := "env-var-namespace"
+		err = os.Setenv("OTELCOL_NAMESPACE", testNamespace)
+		require.NoError(t, err)
+
+		// Prepare args for Load function with CLI values
+		cliListenAddr := ":8888"
+		args := []string{
+			"--" + configFilePathFlagName + "=" + configPath,
+			"--" + listenAddrFlagName + "=" + cliListenAddr,
+			"--" + prometheusCREnabledFlagName + "=true",
+		}
+
+		// Load config using the full Load function
+		config, err := Load(args)
+		require.NoError(t, err)
+
+		// Assert correct priority: CLI over env vars over config file over defaults
+		assert.Equal(t, testNamespace, config.CollectorNamespace, "Env var should override config file for namespace")
+		assert.Equal(t, cliListenAddr, config.ListenAddr, "CLI should override config file for listen address")
+		assert.True(t, config.PrometheusCR.Enabled, "CLI should override config file for prometheus CR enabled")
+	})
+}
diff --git a/cmd/otel-allocator/internal/config/flags_test.go b/cmd/otel-allocator/internal/config/flags_test.go
index f1ea0de2..5c76da9a 100644
--- a/cmd/otel-allocator/internal/config/flags_test.go
+++ b/cmd/otel-allocator/internal/config/flags_test.go
@@ -39,20 +39,26 @@ func TestFlagGetters(t *testing.T) {
 			name:          "GetKubeConfigFilePath",
 			flagArgs:      []string{"--" + kubeConfigPathFlagName, filepath.Join("~", ".kube", "config")},
 			expectedValue: filepath.Join("~", ".kube", "config"),
-			getterFunc:    func(fs *pflag.FlagSet) (interface{}, error) { return getKubeConfigFilePath(fs) },
+			getterFunc: func(fs *pflag.FlagSet) (interface{}, error) {
+				value, _, err := getKubeConfigFilePath(fs)
+				return value, err
+			},
 		},
 		{
 			name:          "GetListenAddr",
 			flagArgs:      []string{"--" + listenAddrFlagName, ":8081"},
 			expectedValue: ":8081",
-			getterFunc:    func(fs *pflag.FlagSet) (interface{}, error) { return getListenAddr(fs) },
+			getterFunc: func(fs *pflag.FlagSet) (interface{}, error) {
+				value, _, err := getListenAddr(fs)
+				return value, err
+			},
 		},
 		{
 			name:          "GetPrometheusCREnabled",
 			flagArgs:      []string{"--" + prometheusCREnabledFlagName, "true"},
 			expectedValue: true,
 			getterFunc: func(fs *pflag.FlagSet) (interface{}, error) {
-				_, value, err := getPrometheusCREnabled(fs)
+				value, _, err := getPrometheusCREnabled(fs)
 				return value, err
 			},
 		},