Repository: kubernetes-sigs/node-feature-discovery
Pull request #304 (2020-04-23 17:18:22)

Rework config handling
Extend the FeatureSource interface with new methods for configuration
handling. This enables easier on-the fly reconfiguration of the
feature sources. Further, it simplifies adding config support to feature
sources in the future.

On top of that, implement dumb re-read/re-parse of the configuration file on every round of
discoery. Probably not the most elegant solution to watch for config
file changes, but, it works and doesn't cost much overhead.

The patchset also fixes some (corner) cases with the overrides (--options)
handling, too:
- Overrides were not applied if config file was missing or its parsing
  failed
- Overrides for a certain source did not have effect if an empty config
  for the source was specified in the config file. This was caused by
  the first pass of parsing (config file) setting a nil pointer to the
  source-specific config, effectively detaching it from the main config.
  The second pass would then create a new instance of the source
  specific config, but, this was not visible in the feature source, of
  course.

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/node-feature-discovery/pull/304#" title="Author self-approved">marquiz</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fnode-feature-discovery).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/node-feature-discovery/blob/master/OWNERS)~~ [marquiz]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
Ethyling: We should maybe add something in the README about this.
marquiz: > We should maybe add something in the README about this.

You're right

/hold
Ethyling: Otherwise the code looks good to me. I still need to test it
marquiz: > Otherwise the code looks good to me. I still need to test it

README updated. Have you had time to test this?

/unhold
Ethyling: Not yet, I will check it soon.
Ethyling: Tested and working fine. Just a small detail, should we add a log line to specify that we are reloading the config file?
k8s-ci-robot: @marquiz: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
marquiz: > Tested and working fine.

Rebased, but, didn't have the time to properly re-test yet.

> Just a small detail, should we add a log line to specify that we are reloading the config file?

Added a log print after every successful loading of the config.


Ethyling: When there is an error in the config file we have this output:
```
2020/05/20 20:18:33 Failed to parse config file: error converting YAML to JSON: yaml: line 30: could not find expected ':'
2020/05/20 20:18:33 Configuration successfully loaded from "/etc/kubernetes/node-feature-discovery/nfd-worker.conf"
```
marquiz: > When there is an error in the config file we have this output:

😭Fixed. Looks like it's time for me to get to bed 😴 
Ethyling: /lgtm
k8s-ci-robot: @marquiz: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

Patch:
diff --git a/README.md b/README.md
index 1d1f80b21..1d80c9cd6 100644
--- a/README.md
+++ b/README.md
@@ -714,8 +714,11 @@ kubectl create -f nfd-worker-daemonset.yaml
 Nfd-worker connects to the nfd-master service to advertise hardware features.
 
 When run as a daemonset, nodes are re-labeled at an interval specified using
-the `--sleep-interval` option. In the [template](https://github.com/kubernetes-sigs/node-feature-discovery/blob/master/nfd-worker-daemonset.yaml.template#L26) the default interval is set to 60s
-which is also the default when no `--sleep-interval` is specified.
+the `--sleep-interval` option. In the
+[template](https://github.com/kubernetes-sigs/node-feature-discovery/blob/master/nfd-worker-daemonset.yaml.template#L26)
+the default interval is set to 60s which is also the default when no
+`--sleep-interval` is specified. Also, the configuration file is re-read on
+each iteration providing a simple mechanism of run-time reconfiguration.
 
 Feature discovery can alternatively be configured as a one-shot job. There is
 an example script in this repo that demonstrates how to deploy the job in the cluster.
@@ -771,11 +774,16 @@ each nfd-worker requires a individual node-specific TLS certificate.
 
 Nfd-worker supports a configuration file. The default location is
 `/etc/kubernetes/node-feature-discovery/nfd-worker.conf`, but,
-this can be changed by specifying the`--config` command line flag. The file is
-read inside the container, and thus, Volumes and VolumeMounts are needed to
-make your configuration available for NFD. The preferred method is to use a
-ConfigMap.
-For example, create a config map using the example config as a template:
+this can be changed by specifying the`--config` command line flag.
+Configuration file is re-read on each labeling pass (determined by
+`--sleep-interval`) which makes run-time re-configuration of nfd-worker
+possible.
+
+Worker configuration file is read inside the container, and thus, Volumes and
+VolumeMounts are needed to make your configuration available for NFD. The
+preferred method is to use a ConfigMap which provides easy deployment and
+re-configurability.  For example, create a config map using the example config
+as a template:
 ```
 cp nfd-worker.conf.example nfd-worker.conf
 vim nfd-worker.conf  # edit the configuration
diff --git a/pkg/apihelper/mock_APIHelpers.go b/pkg/apihelper/mock_APIHelpers.go
index 4788406ca..f6c4f5415 100644
--- a/pkg/apihelper/mock_APIHelpers.go
+++ b/pkg/apihelper/mock_APIHelpers.go
@@ -4,9 +4,12 @@
 
 package apihelper
 
-import kubernetes "k8s.io/client-go/kubernetes"
-import mock "github.com/stretchr/testify/mock"
-import v1 "k8s.io/api/core/v1"
+import (
+	mock "github.com/stretchr/testify/mock"
+	kubernetes "k8s.io/client-go/kubernetes"
+
+	v1 "k8s.io/api/core/v1"
+)
 
 // MockAPIHelpers is an autogenerated mock type for the APIHelpers type
 type MockAPIHelpers struct {
diff --git a/pkg/labeler/mock_LabelerClient.go b/pkg/labeler/mock_LabelerClient.go
index 145bc1403..b1645d561 100644
--- a/pkg/labeler/mock_LabelerClient.go
+++ b/pkg/labeler/mock_LabelerClient.go
@@ -4,9 +4,13 @@
 
 package labeler
 
-import context "context"
-import grpc "google.golang.org/grpc"
-import mock "github.com/stretchr/testify/mock"
+import (
+	context "context"
+
+	grpc "google.golang.org/grpc"
+
+	mock "github.com/stretchr/testify/mock"
+)
 
 // MockLabelerClient is an autogenerated mock type for the LabelerClient type
 type MockLabelerClient struct {
diff --git a/pkg/nfd-worker/nfd-worker.go b/pkg/nfd-worker/nfd-worker.go
index 2468c12ac..780d4d63e 100644
--- a/pkg/nfd-worker/nfd-worker.go
+++ b/pkg/nfd-worker/nfd-worker.go
@@ -19,6 +19,7 @@ package nfdworker
 import (
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"log"
@@ -50,7 +51,6 @@ import (
 	"sigs.k8s.io/yaml"
 )
 
-// package loggers
 var (
 	stdoutLogger = log.New(os.Stdout, "", log.LstdFlags)
 	stderrLogger = log.New(os.Stderr, "", log.LstdFlags)
@@ -59,16 +59,10 @@ var (
 
 // Global config
 type NFDConfig struct {
-	Sources struct {
-		CPU    *cpu.NFDConfig    `json:"cpu,omitempty"`
-		Kernel *kernel.NFDConfig `json:"kernel,omitempty"`
-		Pci    *pci.NFDConfig    `json:"pci,omitempty"`
-		Usb    *usb.NFDConfig    `json:"usb,omitempty"`
-		Custom *custom.NFDConfig `json:"custom,omitempty"`
-	} `json:"sources,omitempty"`
+	Sources sourcesConfig
 }
 
-var config = NFDConfig{}
+type sourcesConfig map[string]source.Config
 
 // Labels are a Kubernetes representation of discovered features.
 type Labels map[string]string
@@ -94,14 +88,21 @@ type NfdWorker interface {
 }
 
 type nfdWorker struct {
-	args       Args
-	clientConn *grpc.ClientConn
-	client     pb.LabelerClient
+	args           Args
+	clientConn     *grpc.ClientConn
+	client         pb.LabelerClient
+	config         NFDConfig
+	sources        []source.FeatureSource
+	labelWhiteList *regexp.Regexp
 }
 
 // Create new NfdWorker instance.
 func NewNfdWorker(args Args) (NfdWorker, error) {
-	nfd := &nfdWorker{args: args}
+	nfd := &nfdWorker{
+		args:    args,
+		sources: []source.FeatureSource{},
+	}
+
 	if args.SleepInterval > 0 && args.SleepInterval < time.Second {
 		stderrLogger.Printf("WARNING: too short sleep-intervall specified (%s), forcing to 1s", args.SleepInterval.String())
 		args.SleepInterval = time.Second
@@ -120,6 +121,44 @@ func NewNfdWorker(args Args) (NfdWorker, error) {
 		}
 	}
 
+	// Figure out active sources
+	allSources := []source.FeatureSource{
+		&cpu.Source{},
+		&fake.Source{},
+		&iommu.Source{},
+		&kernel.Source{},
+		&memory.Source{},
+		&network.Source{},
+		&panicfake.Source{},
+		&pci.Source{},
+		&storage.Source{},
+		&system.Source{},
+		&usb.Source{},
+		&custom.Source{},
+		// local needs to be the last source so that it is able to override
+		// labels from other sources
+		&local.Source{},
+	}
+
+	sourceWhiteList := map[string]struct{}{}
+	for _, s := range args.Sources {
+		sourceWhiteList[strings.TrimSpace(s)] = struct{}{}
+	}
+
+	nfd.sources = []source.FeatureSource{}
+	for _, s := range allSources {
+		if _, enabled := sourceWhiteList[s.Name()]; enabled {
+			nfd.sources = append(nfd.sources, s)
+		}
+	}
+
+	// Compile labelWhiteList regex
+	var err error
+	nfd.labelWhiteList, err = regexp.Compile(args.LabelWhiteList)
+	if err != nil {
+		return nfd, fmt.Errorf("error parsing label whitelist regex (%s): %s", args.LabelWhiteList, err)
+	}
+
 	return nfd, nil
 }
 
@@ -129,28 +168,19 @@ func (w *nfdWorker) Run() error {
 	stdoutLogger.Printf("Node Feature Discovery Worker %s", version.Get())
 	stdoutLogger.Printf("NodeName: '%s'", nodeName)
 
-	// Parse config
-	err := configParse(w.args.ConfigFile, w.args.Options)
-	if err != nil {
-		stderrLogger.Print(err)
-	}
-
-	// Configure the parameters for feature discovery.
-	enabledSources, labelWhiteList, err := configureParameters(w.args.Sources, w.args.LabelWhiteList)
-	if err != nil {
-		return fmt.Errorf("error occurred while configuring parameters: %s", err.Error())
-	}
-
 	// Connect to NFD master
-	err = w.connect()
+	err := w.connect()
 	if err != nil {
 		return fmt.Errorf("failed to connect: %v", err)
 	}
 	defer w.disconnect()
 
 	for {
+		// Parse and apply configuration
+		w.configure(w.args.ConfigFile, w.args.Options)
+
 		// Get the set of feature labels.
-		labels := createFeatureLabels(enabledSources, labelWhiteList)
+		labels := createFeatureLabels(w.sources, w.labelWhiteList)
 
 		// Update the node with the feature labels.
 		if w.client != nil {
@@ -236,76 +266,38 @@ func (w *nfdWorker) disconnect() {
 }
 
 // Parse configuration options
-func configParse(filepath string, overrides string) error {
-	config.Sources.CPU = &cpu.Config
-	config.Sources.Kernel = &kernel.Config
-	config.Sources.Pci = &pci.Config
-	config.Sources.Usb = &usb.Config
-	config.Sources.Custom = &custom.Config
-
-	data, err := ioutil.ReadFile(filepath)
-	if err != nil {
-		return fmt.Errorf("Failed to read config file: %s", err)
+func (w *nfdWorker) configure(filepath string, overrides string) {
+	// Create a new default config
+	c := NFDConfig{Sources: make(map[string]source.Config, len(w.sources))}
+	for _, s := range w.sources {
+		c.Sources[s.Name()] = s.NewConfig()
 	}
 
-	// Read config file
-	err = yaml.Unmarshal(data, &config)
+	// Try to read and parse config file
+	data, err := ioutil.ReadFile(filepath)
 	if err != nil {
-		return fmt.Errorf("Failed to parse config file: %s", err)
+		stderrLogger.Printf("Failed to read config file: %s", err)
+	} else {
+		err = yaml.Unmarshal(data, &c)
+		if err != nil {
+			stderrLogger.Printf("Failed to parse config file: %s", err)
+		} else {
+			stdoutLogger.Printf("Configuration successfully loaded from %q", filepath)
+		}
 	}
 
 	// Parse config overrides
-	err = yaml.Unmarshal([]byte(overrides), &config)
+	err = yaml.Unmarshal([]byte(overrides), &c)
 	if err != nil {
-		return fmt.Errorf("Failed to parse --options: %s", err)
-	}
-
-	return nil
-}
-
-// configureParameters returns all the variables required to perform feature
-// discovery based on command line arguments.
-func configureParameters(sourcesWhiteList []string, labelWhiteListStr string) (enabledSources []source.FeatureSource, labelWhiteList *regexp.Regexp, err error) {
-	// A map for lookup
-	sourcesWhiteListMap := map[string]struct{}{}
-	for _, s := range sourcesWhiteList {
-		sourcesWhiteListMap[strings.TrimSpace(s)] = struct{}{}
+		stderrLogger.Printf("Failed to parse --options: %s", err)
 	}
 
-	// Configure feature sources.
-	allSources := []source.FeatureSource{
-		cpu.Source{},
-		fake.Source{},
-		iommu.Source{},
-		kernel.Source{},
-		memory.Source{},
-		network.Source{},
-		panicfake.Source{},
-		pci.Source{},
-		storage.Source{},
-		system.Source{},
-		usb.Source{},
-		custom.Source{},
-		// local needs to be the last source so that it is able to override
-		// labels from other sources
-		local.Source{},
-	}
-
-	enabledSources = []source.FeatureSource{}
-	for _, s := range allSources {
-		if _, enabled := sourcesWhiteListMap[s.Name()]; enabled {
-			enabledSources = append(enabledSources, s)
-		}
-	}
+	w.config = c
 
-	// Compile labelWhiteList regex
-	labelWhiteList, err = regexp.Compile(labelWhiteListStr)
-	if err != nil {
-		stderrLogger.Printf("error parsing whitelist regex (%s): %s", labelWhiteListStr, err)
-		return nil, nil, err
+	// (Re-)configure all sources
+	for _, s := range w.sources {
+		s.SetConfig(c.Sources[s.Name()])
 	}
-
-	return enabledSources, labelWhiteList, nil
 }
 
 // createFeatureLabels returns the set of feature labels from the enabled
@@ -350,7 +342,7 @@ func getFeatureLabels(source source.FeatureSource, labelWhiteList *regexp.Regexp
 	// Prefix for labels in the default namespace
 	prefix := source.Name() + "-"
 	switch source.(type) {
-	case local.Source:
+	case *local.Source:
 		// Do not prefix labels from the hooks
 		prefix = ""
 	}
@@ -416,3 +408,27 @@ func advertiseFeatureLabels(client pb.LabelerClient, labels Labels) error {
 
 	return nil
 }
+
+// UnmarshalJSON implements the Unmarshaler interface from "encoding/json"
+func (c *sourcesConfig) UnmarshalJSON(data []byte) error {
+	// First do a raw parse to get the per-source data
+	raw := map[string]json.RawMessage{}
+	err := yaml.Unmarshal(data, &raw)
+	if err != nil {
+		return err
+	}
+
+	// Then parse each source-specific data structure
+	// NOTE: we expect 'c' to be pre-populated with correct per-source data
+	//       types. Non-pre-populated keys are ignored.
+	for k, rawv := range raw {
+		if v, ok := (*c)[k]; ok {
+			err := yaml.Unmarshal(rawv, &v)
+			if err != nil {
+				return fmt.Errorf("failed to parse %q source config: %v", k, err)
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/source/cpu/cpu.go b/source/cpu/cpu.go
index 1c654273e..b116feeea 100644
--- a/source/cpu/cpu.go
+++ b/source/cpu/cpu.go
@@ -29,42 +29,43 @@ type cpuidConfig struct {
 	AttributeWhitelist []string `json:"attributeWhitelist,omitempty"`
 }
 
-// NFDConfig is the type holding configuration of the cpuid feature source
-type NFDConfig struct {
+type Config struct {
 	Cpuid cpuidConfig `json:"cpuid,omitempty"`
 }
 
-// Config contains the configuration of the cpuid source
-var Config = NFDConfig{
-	cpuidConfig{
-		AttributeBlacklist: []string{
-			"BMI1",
-			"BMI2",
-			"CLMUL",
-			"CMOV",
-			"CX16",
-			"ERMS",
-			"F16C",
-			"HTT",
-			"LZCNT",
-			"MMX",
-			"MMXEXT",
-			"NX",
-			"POPCNT",
-			"RDRAND",
-			"RDSEED",
-			"RDTSCP",
-			"SGX",
-			"SGXLC",
-			"SSE",
-			"SSE2",
-			"SSE3",
-			"SSE4.1",
-			"SSE4.2",
-			"SSSE3",
+// newDefaultConfig returns a new config with pre-populated defaults
+func newDefaultConfig() *Config {
+	return &Config{
+		cpuidConfig{
+			AttributeBlacklist: []string{
+				"BMI1",
+				"BMI2",
+				"CLMUL",
+				"CMOV",
+				"CX16",
+				"ERMS",
+				"F16C",
+				"HTT",
+				"LZCNT",
+				"MMX",
+				"MMXEXT",
+				"NX",
+				"POPCNT",
+				"RDRAND",
+				"RDSEED",
+				"RDTSCP",
+				"SGX",
+				"SGXLC",
+				"SSE",
+				"SSE2",
+				"SSE3",
+				"SSE4.1",
+				"SSE4.2",
+				"SSSE3",
+			},
+			AttributeWhitelist: []string{},
 		},
-		AttributeWhitelist: []string{},
-	},
+	}
 }
 
 // Filter for cpuid labels
@@ -73,19 +74,33 @@ type keyFilter struct {
 	whitelist bool
 }
 
-var cpuidFilter *keyFilter
-
 // Implement FeatureSource interface
-type Source struct{}
+type Source struct {
+	config      *Config
+	cpuidFilter *keyFilter
+}
 
 func (s Source) Name() string { return "cpu" }
 
-func (s Source) Discover() (source.Features, error) {
-	features := source.Features{}
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
 
-	if cpuidFilter == nil {
-		initCpuidFilter()
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return s.config }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(conf source.Config) {
+	switch v := conf.(type) {
+	case *Config:
+		s.config = v
+		s.initCpuidFilter()
+	default:
+		log.Printf("PANIC: invalid config type: %T", conf)
 	}
+}
+
+func (s *Source) Discover() (source.Features, error) {
+	features := source.Features{}
 
 	// Check if hyper-threading seems to be enabled
 	found, err := haveThreadSiblings()
@@ -106,7 +121,7 @@ func (s Source) Discover() (source.Features, error) {
 	// Detect CPUID
 	cpuidFlags := getCpuidFlags()
 	for _, f := range cpuidFlags {
-		if cpuidFilter.unmask(f) {
+		if s.cpuidFilter.unmask(f) {
 			features["cpuid."+f] = true
 		}
 	}
@@ -155,20 +170,20 @@ func haveThreadSiblings() (bool, error) {
 	return false, nil
 }
 
-func initCpuidFilter() {
+func (s *Source) initCpuidFilter() {
 	newFilter := keyFilter{keys: map[string]struct{}{}}
-	if len(Config.Cpuid.AttributeWhitelist) > 0 {
-		for _, k := range Config.Cpuid.AttributeWhitelist {
+	if len(s.config.Cpuid.AttributeWhitelist) > 0 {
+		for _, k := range s.config.Cpuid.AttributeWhitelist {
 			newFilter.keys[k] = struct{}{}
 		}
 		newFilter.whitelist = true
 	} else {
-		for _, k := range Config.Cpuid.AttributeBlacklist {
+		for _, k := range s.config.Cpuid.AttributeBlacklist {
 			newFilter.keys[k] = struct{}{}
 		}
 		newFilter.whitelist = false
 	}
-	cpuidFilter = &newFilter
+	s.cpuidFilter = &newFilter
 }
 
 func (f keyFilter) unmask(k string) bool {
diff --git a/source/custom/custom.go b/source/custom/custom.go
index 4da2b2ff6..67b39ef16 100644
--- a/source/custom/custom.go
+++ b/source/custom/custom.go
@@ -36,20 +36,41 @@ type FeatureSpec struct {
 	MatchOn []MatchRule `json:"matchOn"`
 }
 
-type NFDConfig []FeatureSpec
+type config []FeatureSpec
 
-var Config = NFDConfig{}
+// newDefaultConfig returns a new config with pre-populated defaults
+func newDefaultConfig() *config {
+	return &config{}
+}
 
 // Implements FeatureSource Interface
-type Source struct{}
+type Source struct {
+	config *config
+}
 
 // Return name of the feature source
 func (s Source) Name() string { return "custom" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return s.config }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(conf source.Config) {
+	switch v := conf.(type) {
+	case *config:
+		s.config = v
+	default:
+		log.Printf("PANIC: invalid config type: %T", conf)
+	}
+}
+
 // Discover features
 func (s Source) Discover() (source.Features, error) {
 	features := source.Features{}
-	allFeatureConfig := append(getStaticFeatureConfig(), Config...)
+	allFeatureConfig := append(getStaticFeatureConfig(), *s.config...)
 	log.Printf("INFO: Custom features: %+v", allFeatureConfig)
 	// Iterate over features
 	for _, customFeature := range allFeatureConfig {
diff --git a/source/fake/fake.go b/source/fake/fake.go
index 62246b04f..63326c2e0 100644
--- a/source/fake/fake.go
+++ b/source/fake/fake.go
@@ -24,6 +24,18 @@ type Source struct{}
 // Name returns an identifier string for this feature source.
 func (s Source) Name() string { return "fake" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return nil }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return nil }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(source.Config) {}
+
+// Configure method of the FeatureSource interface
+func (s Source) Configure([]byte) error { return nil }
+
 // Discover returns feature names for some fake features.
 func (s Source) Discover() (source.Features, error) {
 	// Adding three fake features.
diff --git a/source/iommu/iommu.go b/source/iommu/iommu.go
index 9dfe0080a..8221c6b03 100644
--- a/source/iommu/iommu.go
+++ b/source/iommu/iommu.go
@@ -28,6 +28,15 @@ type Source struct{}
 
 func (s Source) Name() string { return "iommu" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return nil }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return nil }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(source.Config) {}
+
 func (s Source) Discover() (source.Features, error) {
 	features := source.Features{}
 
diff --git a/source/kernel/kernel.go b/source/kernel/kernel.go
index 37c301c77..6592afe2f 100644
--- a/source/kernel/kernel.go
+++ b/source/kernel/kernel.go
@@ -30,27 +30,48 @@ import (
 )
 
 // Configuration file options
-type NFDConfig struct {
+type Config struct {
 	KconfigFile string
 	ConfigOpts  []string `json:"configOpts,omitempty"`
 }
 
-var Config = NFDConfig{
-	KconfigFile: "",
-	ConfigOpts: []string{
-		"NO_HZ",
-		"NO_HZ_IDLE",
-		"NO_HZ_FULL",
-		"PREEMPT",
-	},
+// newDefaultConfig returns a new config with pre-populated defaults
+func newDefaultConfig() *Config {
+	return &Config{
+		KconfigFile: "",
+		ConfigOpts: []string{
+			"NO_HZ",
+			"NO_HZ_IDLE",
+			"NO_HZ_FULL",
+			"PREEMPT",
+		},
+	}
 }
 
 // Implement FeatureSource interface
-type Source struct{}
+type Source struct {
+	config *Config
+}
+
+func (s *Source) Name() string { return "kernel" }
+
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
 
-func (s Source) Name() string { return "kernel" }
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return s.config }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(conf source.Config) {
+	switch v := conf.(type) {
+	case *Config:
+		s.config = v
+	default:
+		log.Printf("PANIC: invalid config type: %T", conf)
+	}
+}
 
-func (s Source) Discover() (source.Features, error) {
+func (s *Source) Discover() (source.Features, error) {
 	features := source.Features{}
 
 	// Read kernel version
@@ -64,13 +85,13 @@ func (s Source) Discover() (source.Features, error) {
 	}
 
 	// Read kconfig
-	kconfig, err := parseKconfig()
+	kconfig, err := s.parseKconfig()
 	if err != nil {
 		log.Printf("ERROR: Failed to read kconfig: %s", err)
 	}
 
 	// Check flags
-	for _, opt := range Config.ConfigOpts {
+	for _, opt := range s.config.ConfigOpts {
 		if val, ok := kconfig[opt]; ok {
 			features["config."+opt] = val
 		}
@@ -137,16 +158,16 @@ func readKconfigGzip(filename string) ([]byte, error) {
 }
 
 // Read kconfig into a map
-func parseKconfig() (map[string]string, error) {
+func (s *Source) parseKconfig() (map[string]string, error) {
 	kconfig := map[string]string{}
 	raw := []byte(nil)
 	var err error
 
 	// First, try kconfig specified in the config file
-	if len(Config.KconfigFile) > 0 {
-		raw, err = ioutil.ReadFile(Config.KconfigFile)
+	if len(s.config.KconfigFile) > 0 {
+		raw, err = ioutil.ReadFile(s.config.KconfigFile)
 		if err != nil {
-			log.Printf("ERROR: Failed to read kernel config from %s: %s", Config.KconfigFile, err)
+			log.Printf("ERROR: Failed to read kernel config from %s: %s", s.config.KconfigFile, err)
 		}
 	}
 
diff --git a/source/local/local.go b/source/local/local.go
index fb19f9f95..ade714a77 100644
--- a/source/local/local.go
+++ b/source/local/local.go
@@ -40,6 +40,15 @@ type Source struct{}
 
 func (s Source) Name() string { return "local" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return nil }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return nil }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(source.Config) {}
+
 func (s Source) Discover() (source.Features, error) {
 	featuresFromHooks, err := getFeaturesFromHooks()
 	if err != nil {
diff --git a/source/memory/memory.go b/source/memory/memory.go
index 0e5e6d685..2e367d698 100644
--- a/source/memory/memory.go
+++ b/source/memory/memory.go
@@ -31,6 +31,15 @@ type Source struct{}
 // Name returns an identifier string for this feature source.
 func (s Source) Name() string { return "memory" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return nil }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return nil }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(source.Config) {}
+
 // Discover returns feature names for memory: numa if more than one memory node is present.
 func (s Source) Discover() (source.Features, error) {
 	features := source.Features{}
diff --git a/source/mock_FeatureSource.go b/source/mock_FeatureSource.go
index 608cd64e6..21e2fcad3 100644
--- a/source/mock_FeatureSource.go
+++ b/source/mock_FeatureSource.go
@@ -34,6 +34,22 @@ func (_m *MockFeatureSource) Discover() (Features, error) {
 	return r0, r1
 }
 
+// GetConfig provides a mock function with given fields:
+func (_m *MockFeatureSource) GetConfig() Config {
+	ret := _m.Called()
+
+	var r0 Config
+	if rf, ok := ret.Get(0).(func() Config); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(Config)
+		}
+	}
+
+	return r0
+}
+
 // Name provides a mock function with given fields:
 func (_m *MockFeatureSource) Name() string {
 	ret := _m.Called()
@@ -47,3 +63,24 @@ func (_m *MockFeatureSource) Name() string {
 
 	return r0
 }
+
+// NewConfig provides a mock function with given fields:
+func (_m *MockFeatureSource) NewConfig() Config {
+	ret := _m.Called()
+
+	var r0 Config
+	if rf, ok := ret.Get(0).(func() Config); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(Config)
+		}
+	}
+
+	return r0
+}
+
+// SetConfig provides a mock function with given fields: _a0
+func (_m *MockFeatureSource) SetConfig(_a0 Config) {
+	_m.Called(_a0)
+}
diff --git a/source/network/network.go b/source/network/network.go
index 58fb1a4b8..a82f19545 100644
--- a/source/network/network.go
+++ b/source/network/network.go
@@ -34,6 +34,15 @@ type Source struct{}
 // Name returns an identifier string for this feature source.
 func (s Source) Name() string { return "network" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return nil }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return nil }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(source.Config) {}
+
 // Discover returns feature names sriov-configured and sriov if SR-IOV capable NICs are present and/or SR-IOV virtual functions are configured on the node
 func (s Source) Discover() (source.Features, error) {
 	features := source.Features{}
diff --git a/source/panic_fake/fake_panic.go b/source/panic_fake/fake_panic.go
index 2ffff3592..741dd71b7 100644
--- a/source/panic_fake/fake_panic.go
+++ b/source/panic_fake/fake_panic.go
@@ -24,6 +24,15 @@ type Source struct{}
 // Name returns an identifier string for this feature source.
 func (s Source) Name() string { return "panic_fake" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return nil }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return nil }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(source.Config) {}
+
 // Discover calls panic().
 func (s Source) Discover() (source.Features, error) {
 	panic("fake panic error")
diff --git a/source/pci/pci.go b/source/pci/pci.go
index 6c776ce0a..d6cf751a6 100644
--- a/source/pci/pci.go
+++ b/source/pci/pci.go
@@ -25,22 +25,43 @@ import (
 	pciutils "sigs.k8s.io/node-feature-discovery/source/internal"
 )
 
-type NFDConfig struct {
+type Config struct {
 	DeviceClassWhitelist []string `json:"deviceClassWhitelist,omitempty"`
 	DeviceLabelFields    []string `json:"deviceLabelFields,omitempty"`
 }
 
-var Config = NFDConfig{
-	DeviceClassWhitelist: []string{"03", "0b40", "12"},
-	DeviceLabelFields:    []string{"class", "vendor"},
+// newDefaultConfig returns a new config with pre-populated defaults
+func newDefaultConfig() *Config {
+	return &Config{
+		DeviceClassWhitelist: []string{"03", "0b40", "12"},
+		DeviceLabelFields:    []string{"class", "vendor"},
+	}
 }
 
 // Implement FeatureSource interface
-type Source struct{}
+type Source struct {
+	config *Config
+}
 
 // Return name of the feature source
 func (s Source) Name() string { return "pci" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return s.config }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(conf source.Config) {
+	switch v := conf.(type) {
+	case *Config:
+		s.config = v
+	default:
+		log.Printf("PANIC: invalid config type: %T", conf)
+	}
+}
+
 // Discover features
 func (s Source) Discover() (source.Features, error) {
 	features := source.Features{}
@@ -48,7 +69,7 @@ func (s Source) Discover() (source.Features, error) {
 	// Construct a device label format, a sorted list of valid attributes
 	deviceLabelFields := []string{}
 	configLabelFields := map[string]bool{}
-	for _, field := range Config.DeviceLabelFields {
+	for _, field := range s.config.DeviceLabelFields {
 		configLabelFields[field] = true
 	}
 
@@ -87,7 +108,7 @@ func (s Source) Discover() (source.Features, error) {
 
 	// Iterate over all device classes
 	for class, classDevs := range devs {
-		for _, white := range Config.DeviceClassWhitelist {
+		for _, white := range s.config.DeviceClassWhitelist {
 			if strings.HasPrefix(class, strings.ToLower(white)) {
 				for _, dev := range classDevs {
 					devLabel := ""
diff --git a/source/source.go b/source/source.go
index d4165070d..0daaffba5 100644
--- a/source/source.go
+++ b/source/source.go
@@ -39,4 +39,16 @@ type FeatureSource interface {
 
 	// Discover returns discovered features for this node.
 	Discover() (Features, error)
+
+	// NewConfig returns a new default config of the source
+	NewConfig() Config
+
+	// GetConfig returns the effective configuration of the source
+	GetConfig() Config
+
+	// SetConfig changes the effective configuration of the source
+	SetConfig(Config)
+}
+
+type Config interface {
 }
diff --git a/source/storage/storage.go b/source/storage/storage.go
index 4e385df09..632a2ee3b 100644
--- a/source/storage/storage.go
+++ b/source/storage/storage.go
@@ -29,6 +29,15 @@ type Source struct{}
 // Name returns an identifier string for this feature source.
 func (s Source) Name() string { return "storage" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return nil }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return nil }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(source.Config) {}
+
 // Discover returns feature names for storage: nonrotationaldisk if any SSD drive present.
 func (s Source) Discover() (source.Features, error) {
 	features := source.Features{}
diff --git a/source/system/system.go b/source/system/system.go
index dab3ce89e..6d6819edc 100644
--- a/source/system/system.go
+++ b/source/system/system.go
@@ -36,6 +36,15 @@ type Source struct{}
 
 func (s Source) Name() string { return "system" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return nil }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return nil }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(source.Config) {}
+
 func (s Source) Discover() (source.Features, error) {
 	features := source.Features{}
 
diff --git a/source/usb/usb.go b/source/usb/usb.go
index 55ccf5c67..3b50ebabe 100644
--- a/source/usb/usb.go
+++ b/source/usb/usb.go
@@ -25,25 +25,46 @@ import (
 	usbutils "sigs.k8s.io/node-feature-discovery/source/internal"
 )
 
-type NFDConfig struct {
+type Config struct {
 	DeviceClassWhitelist []string `json:"deviceClassWhitelist,omitempty"`
 	DeviceLabelFields    []string `json:"deviceLabelFields,omitempty"`
 }
 
-var Config = NFDConfig{
-	// Whitelist specific USB classes: https://www.usb.org/defined-class-codes
-	// By default these include classes where different accelerators are typically mapped:
-	// Video (0e), Miscellaneous (ef), Application Specific (fe), and Vendor Specific (ff).
-	DeviceClassWhitelist: []string{"0e", "ef", "fe", "ff"},
-	DeviceLabelFields:    []string{"class", "vendor", "device"},
+// newDefaultConfig returns a new config with pre-populated defaults
+func newDefaultConfig() *Config {
+	return &Config{
+		// Whitelist specific USB classes: https://www.usb.org/defined-class-codes
+		// By default these include classes where different accelerators are typically mapped:
+		// Video (0e), Miscellaneous (ef), Application Specific (fe), and Vendor Specific (ff).
+		DeviceClassWhitelist: []string{"0e", "ef", "fe", "ff"},
+		DeviceLabelFields:    []string{"class", "vendor", "device"},
+	}
 }
 
 // Implement FeatureSource interface
-type Source struct{}
+type Source struct {
+	config *Config
+}
 
 // Return name of the feature source
 func (s Source) Name() string { return "usb" }
 
+// NewConfig method of the FeatureSource interface
+func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
+
+// GetConfig method of the FeatureSource interface
+func (s *Source) GetConfig() source.Config { return s.config }
+
+// SetConfig method of the FeatureSource interface
+func (s *Source) SetConfig(conf source.Config) {
+	switch v := conf.(type) {
+	case *Config:
+		s.config = v
+	default:
+		log.Printf("PANIC: invalid config type: %T", conf)
+	}
+}
+
 // Discover features
 func (s Source) Discover() (source.Features, error) {
 	features := source.Features{}
@@ -51,7 +72,7 @@ func (s Source) Discover() (source.Features, error) {
 	// Construct a device label format, a sorted list of valid attributes
 	deviceLabelFields := []string{}
 	configLabelFields := map[string]bool{}
-	for _, field := range Config.DeviceLabelFields {
+	for _, field := range s.config.DeviceLabelFields {
 		configLabelFields[field] = true
 	}
 
@@ -86,7 +107,7 @@ func (s Source) Discover() (source.Features, error) {
 
 	// Iterate over all device classes
 	for class, classDevs := range devs {
-		for _, white := range Config.DeviceClassWhitelist {
+		for _, white := range s.config.DeviceClassWhitelist {
 			if strings.HasPrefix(class, strings.ToLower(white)) {
 				for _, dev := range classDevs {
 					devLabel := ""


Test patch:
diff --git a/pkg/nfd-worker/nfd-worker-internal_test.go b/pkg/nfd-worker/nfd-worker-internal_test.go
index 5483dbb26..436ce4053 100644
--- a/pkg/nfd-worker/nfd-worker-internal_test.go
+++ b/pkg/nfd-worker/nfd-worker-internal_test.go
@@ -29,8 +29,11 @@ import (
 	"github.com/vektra/errors"
 	"sigs.k8s.io/node-feature-discovery/pkg/labeler"
 	"sigs.k8s.io/node-feature-discovery/source"
+	"sigs.k8s.io/node-feature-discovery/source/cpu"
 	"sigs.k8s.io/node-feature-discovery/source/fake"
+	"sigs.k8s.io/node-feature-discovery/source/kernel"
 	"sigs.k8s.io/node-feature-discovery/source/panic_fake"
+	"sigs.k8s.io/node-feature-discovery/source/pci"
 )
 
 const fakeFeatureSourceName string = "testSource"
@@ -91,13 +94,27 @@ func makeFakeFeatures(names []string) (source.Features, Labels) {
 	return features, labels
 }
 
-func TestConfigParse(t *testing.T) {
-	Convey("When parsing configuration file", t, func() {
-		Convey("When non-accessible file is given", func() {
-			err := configParse("non-existing-file", "")
+func (w *nfdWorker) getSource(name string) source.FeatureSource {
+	for _, s := range w.sources {
+		if s.Name() == name {
+			return s
+		}
+	}
+	return nil
+}
 
-			Convey("Should return error", func() {
-				So(err, ShouldNotBeNil)
+func TestConfigParse(t *testing.T) {
+	Convey("When parsing configuration", t, func() {
+		w, err := NewNfdWorker(Args{Sources: []string{"cpu", "kernel", "pci"}})
+		So(err, ShouldBeNil)
+		worker := w.(*nfdWorker)
+		Convey("and a non-accessible file and some overrides are specified", func() {
+			overrides := `{"sources": {"cpu": {"cpuid": {"attributeBlacklist": ["foo","bar"]}}}}`
+			worker.configure("non-existing-file", overrides)
+
+			Convey("overrides should take effect", func() {
+				c := worker.getSource("cpu").GetConfig().(*cpu.Config)
+				So(c.Cpuid.AttributeBlacklist, ShouldResemble, []string{"foo", "bar"})
 			})
 		})
 		// Create a temporary config file
@@ -114,76 +131,87 @@ func TestConfigParse(t *testing.T) {
 		f.Close()
 		So(err, ShouldBeNil)
 
-		Convey("When proper config file is given", func() {
-			err := configParse(f.Name(), "")
+		Convey("and a proper config file is specified", func() {
+			worker.configure(f.Name(), "")
 
-			Convey("Should return error", func() {
+			Convey("specified configuration should take effect", func() {
 				So(err, ShouldBeNil)
-				So(config.Sources.Kernel.ConfigOpts, ShouldResemble, []string{"DMI"})
-				So(config.Sources.Pci.DeviceClassWhitelist, ShouldResemble, []string{"ff"})
+				c := worker.getSource("kernel").GetConfig()
+				So(c.(*kernel.Config).ConfigOpts, ShouldResemble, []string{"DMI"})
+				c = worker.getSource("pci").GetConfig()
+				So(c.(*pci.Config).DeviceClassWhitelist, ShouldResemble, []string{"ff"})
+			})
+		})
+
+		Convey("and a proper config file and overrides are given", func() {
+			overrides := `{"sources": {"pci": {"deviceClassWhitelist": ["03"]}}}`
+			worker.configure(f.Name(), overrides)
+
+			Convey("overrides should take precedence over the config file", func() {
+				So(err, ShouldBeNil)
+				c := worker.getSource("kernel").GetConfig()
+				So(c.(*kernel.Config).ConfigOpts, ShouldResemble, []string{"DMI"})
+				c = worker.getSource("pci").GetConfig()
+				So(c.(*pci.Config).DeviceClassWhitelist, ShouldResemble, []string{"03"})
 			})
 		})
 	})
 }
 
-func TestConfigureParameters(t *testing.T) {
-	Convey("When configuring parameters for node feature discovery", t, func() {
+func TestNewNfdWorker(t *testing.T) {
+	Convey("When creating new NfdWorker instance", t, func() {
 
-		Convey("When no sourcesWhiteList and labelWhiteListStr are passed", func() {
-			sourcesWhiteList := []string{}
-			labelWhiteListStr := ""
+		Convey("without any args specified", func() {
+			args := Args{}
 			emptyRegexp, _ := regexp.Compile("")
-			enabledSources, labelWhiteList, err := configureParameters(sourcesWhiteList, labelWhiteListStr)
-
-			Convey("Error should not be produced", func() {
+			w, err := NewNfdWorker(args)
+			Convey("no error should be returned", func() {
 				So(err, ShouldBeNil)
 			})
-			Convey("No sourcesWhiteList or labelWhiteList are returned", func() {
-				So(len(enabledSources), ShouldEqual, 0)
-				So(labelWhiteList, ShouldResemble, emptyRegexp)
+			worker := w.(*nfdWorker)
+			Convey("no sources should be enabled and the whitelist regexp should be empty", func() {
+				So(len(worker.sources), ShouldEqual, 0)
+				So(worker.labelWhiteList, ShouldResemble, emptyRegexp)
 			})
 		})
 
-		Convey("When sourcesWhiteList is passed", func() {
-			sourcesWhiteList := []string{"fake"}
-			labelWhiteListStr := ""
+		Convey("with non-empty Sources arg specified", func() {
+			args := Args{Sources: []string{"fake"}}
 			emptyRegexp, _ := regexp.Compile("")
-			enabledSources, labelWhiteList, err := configureParameters(sourcesWhiteList, labelWhiteListStr)
-
-			Convey("Error should not be produced", func() {
+			w, err := NewNfdWorker(args)
+			Convey("no error should be returned", func() {
 				So(err, ShouldBeNil)
 			})
-			Convey("Proper sourcesWhiteList are returned", func() {
-				So(len(enabledSources), ShouldEqual, 1)
-				So(enabledSources[0], ShouldHaveSameTypeAs, fake.Source{})
-				So(labelWhiteList, ShouldResemble, emptyRegexp)
+			worker := w.(*nfdWorker)
+			Convey("proper sources should be enabled", func() {
+				So(len(worker.sources), ShouldEqual, 1)
+				So(worker.sources[0], ShouldHaveSameTypeAs, &fake.Source{})
+				So(worker.labelWhiteList, ShouldResemble, emptyRegexp)
 			})
 		})
 
-		Convey("When invalid labelWhiteListStr is passed", func() {
-			sourcesWhiteList := []string{""}
-			labelWhiteListStr := "*"
-			enabledSources, labelWhiteList, err := configureParameters(sourcesWhiteList, labelWhiteListStr)
-
-			Convey("Error is produced", func() {
-				So(enabledSources, ShouldBeNil)
-				So(labelWhiteList, ShouldBeNil)
+		Convey("with invalid LabelWhiteList arg specified", func() {
+			args := Args{LabelWhiteList: "*"}
+			w, err := NewNfdWorker(args)
+			worker := w.(*nfdWorker)
+			Convey("an error should be returned", func() {
+				So(len(worker.sources), ShouldEqual, 0)
+				So(worker.labelWhiteList, ShouldBeNil)
 				So(err, ShouldNotBeNil)
 			})
 		})
 
-		Convey("When valid labelWhiteListStr is passed", func() {
-			sourcesWhiteList := []string{""}
-			labelWhiteListStr := ".*rdt.*"
-			expectRegexp, err := regexp.Compile(".*rdt.*")
-			enabledSources, labelWhiteList, err := configureParameters(sourcesWhiteList, labelWhiteListStr)
-
-			Convey("Error should not be produced", func() {
+		Convey("with valid LabelWhiteListStr arg specified", func() {
+			args := Args{LabelWhiteList: ".*rdt.*"}
+			w, err := NewNfdWorker(args)
+			Convey("no error should be returned", func() {
 				So(err, ShouldBeNil)
 			})
-			Convey("Proper labelWhiteList is returned", func() {
-				So(len(enabledSources), ShouldEqual, 0)
-				So(labelWhiteList, ShouldResemble, expectRegexp)
+			worker := w.(*nfdWorker)
+			expectRegexp := regexp.MustCompile(".*rdt.*")
+			Convey("proper labelWhiteList regexp should be produced", func() {
+				So(len(worker.sources), ShouldEqual, 0)
+				So(worker.labelWhiteList, ShouldResemble, expectRegexp)
 			})
 		})
 	})