Repository: prometheus/snmp_exporter
Pull request #970 (2023-08-27 16:15:28)

Support loading multiple configuration files
Support loading multiple configuration files by by allowing `--config.file` to be repeateable. As well as supporting glob file matching for `config.d` style setups.
* Conflicting auth or module keys are treated as errors.

Fixes: https://github.com/prometheus/snmp_exporter/issues/628


Patch:
diff --git a/README.md b/README.md
index d3f0a58..55e59a6 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,11 @@ by hand. If you need to change it, see
 The default `snmp.yml` file covers a variety of common hardware walking them
 using SNMP v2 GETBULK.
 
+The `--config.file` parameter can be used multiple times to load more than one file.
+It also supports [glob filename matching](https://pkg.go.dev/path/filepath#Glob), e.g. `snmp*.yml`.
+
+Duplicate `module` or `auth` entries are treated as invalid and can not be loaded.
+
 ## Prometheus Configuration
 
 The URL params `target`, `auth`, and `module` can be controlled through relabelling.
diff --git a/config/config.go b/config/config.go
index 3f3ce5a..428f688 100644
--- a/config/config.go
+++ b/config/config.go
@@ -16,6 +16,7 @@ package config
 import (
 	"fmt"
 	"os"
+	"path/filepath"
 	"regexp"
 	"time"
 
@@ -23,15 +24,23 @@ import (
 	"gopkg.in/yaml.v2"
 )
 
-func LoadFile(filename string) (*Config, error) {
-	content, err := os.ReadFile(filename)
-	if err != nil {
-		return nil, err
-	}
+func LoadFile(paths []string) (*Config, error) {
 	cfg := &Config{}
-	err = yaml.UnmarshalStrict(content, cfg)
-	if err != nil {
-		return nil, err
+	for _, p := range paths {
+		files, err := filepath.Glob(p)
+		if err != nil {
+			return nil, err
+		}
+		for _, f := range files {
+			content, err := os.ReadFile(f)
+			if err != nil {
+				return nil, err
+			}
+			err = yaml.UnmarshalStrict(content, cfg)
+			if err != nil {
+				return nil, err
+			}
+		}
 	}
 	return cfg, nil
 }
diff --git a/main.go b/main.go
index f89a402..ed553b0 100644
--- a/main.go
+++ b/main.go
@@ -41,7 +41,7 @@ import (
 )
 
 var (
-	configFile  = kingpin.Flag("config.file", "Path to configuration file.").Default("snmp.yml").String()
+	configFile  = kingpin.Flag("config.file", "Path to configuration file.").Default("snmp.yml").Strings()
 	dryRun      = kingpin.Flag("dry-run", "Only verify configuration is valid and exit.").Default("false").Bool()
 	concurrency = kingpin.Flag("snmp.module-concurrency", "The number of modules to fetch concurrently per scrape").Default("1").Int()
 	metricsPath = kingpin.Flag(
@@ -152,7 +152,7 @@ type SafeConfig struct {
 	C *config.Config
 }
 
-func (sc *SafeConfig) ReloadConfig(configFile string) (err error) {
+func (sc *SafeConfig) ReloadConfig(configFile []string) (err error) {
 	conf, err := config.LoadFile(configFile)
 	if err != nil {
 		return err


Test patch:
diff --git a/config_test.go b/config_test.go
index dba55fe..31ae12f 100644
--- a/config_test.go
+++ b/config_test.go
@@ -22,7 +22,7 @@ import (
 
 func TestHideConfigSecrets(t *testing.T) {
 	sc := &SafeConfig{}
-	err := sc.ReloadConfig("testdata/snmp-auth.yml")
+	err := sc.ReloadConfig([]string{"testdata/snmp-auth.yml"})
 	if err != nil {
 		t.Errorf("Error loading config %v: %v", "testdata/snmp-auth.yml", err)
 	}
@@ -41,7 +41,7 @@ func TestHideConfigSecrets(t *testing.T) {
 
 func TestLoadConfigWithOverrides(t *testing.T) {
 	sc := &SafeConfig{}
-	err := sc.ReloadConfig("testdata/snmp-with-overrides.yml")
+	err := sc.ReloadConfig([]string{"testdata/snmp-with-overrides.yml"})
 	if err != nil {
 		t.Errorf("Error loading config %v: %v", "testdata/snmp-with-overrides.yml", err)
 	}
@@ -52,3 +52,18 @@ func TestLoadConfigWithOverrides(t *testing.T) {
 		t.Errorf("Error marshaling config: %v", err)
 	}
 }
+
+func TestLoadMultipleConfigs(t *testing.T) {
+	sc := &SafeConfig{}
+	configs := []string{"testdata/snmp-auth.yml", "testdata/snmp-with-overrides.yml"}
+	err := sc.ReloadConfig(configs)
+	if err != nil {
+		t.Errorf("Error loading configs %v: %v", configs, err)
+	}
+	sc.RLock()
+	_, err = yaml.Marshal(sc.C)
+	sc.RUnlock()
+	if err != nil {
+		t.Errorf("Error marshaling config: %v", err)
+	}
+}