Repository: kubernetes-sigs/node-feature-discovery
Pull request #597 (2021-09-15 14:49:51)

source: make sources register themselves
Implement new registration infrastructure under the "source" package.
This change loosens the coupling between label sources and the
nfd-worker, making it easier to refactor and move the code around.

Also, create a separate interface (ConfigurableSource) for configurable
feature sources in order to eliminate boilerplate code.

Add safety checks to the sources that they actually implement the
interfaces they should.

In sake of consistency and predictability (of behavior) change all
methods of the sources to use pointer receivers.

Add simple unit tests for the new functionality and include source/...
into make test target.

Second piece towards getting #464 merged.

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/597#" 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":[]} -->

Patch:
diff --git a/Makefile b/Makefile
index b50d97b97..f5bfb38ff 100644
--- a/Makefile
+++ b/Makefile
@@ -129,7 +129,7 @@ helm-lint:
 	helm lint --strict deployment/helm/node-feature-discovery/
 
 test:
-	$(GO_CMD) test ./cmd/... ./pkg/...
+	$(GO_CMD) test ./cmd/... ./pkg/... ./source/...
 
 e2e-test:
 	@if [ -z ${KUBECONFIG} ]; then echo "[ERR] KUBECONFIG missing, must be defined"; exit 1; fi
diff --git a/pkg/nfd-client/worker/nfd-worker.go b/pkg/nfd-client/worker/nfd-worker.go
index d34467c70..3d8c47770 100644
--- a/pkg/nfd-client/worker/nfd-worker.go
+++ b/pkg/nfd-client/worker/nfd-worker.go
@@ -23,6 +23,7 @@ import (
 	"os"
 	"path/filepath"
 	"regexp"
+	"sort"
 	"strings"
 	"time"
 
@@ -36,18 +37,20 @@ import (
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"sigs.k8s.io/node-feature-discovery/pkg/version"
 	"sigs.k8s.io/node-feature-discovery/source"
-	"sigs.k8s.io/node-feature-discovery/source/cpu"
-	"sigs.k8s.io/node-feature-discovery/source/custom"
-	"sigs.k8s.io/node-feature-discovery/source/fake"
-	"sigs.k8s.io/node-feature-discovery/source/iommu"
-	"sigs.k8s.io/node-feature-discovery/source/kernel"
-	"sigs.k8s.io/node-feature-discovery/source/local"
-	"sigs.k8s.io/node-feature-discovery/source/memory"
-	"sigs.k8s.io/node-feature-discovery/source/network"
-	"sigs.k8s.io/node-feature-discovery/source/pci"
-	"sigs.k8s.io/node-feature-discovery/source/storage"
-	"sigs.k8s.io/node-feature-discovery/source/system"
-	"sigs.k8s.io/node-feature-discovery/source/usb"
+
+	// Register all source packages
+	_ "sigs.k8s.io/node-feature-discovery/source/cpu"
+	_ "sigs.k8s.io/node-feature-discovery/source/custom"
+	_ "sigs.k8s.io/node-feature-discovery/source/fake"
+	_ "sigs.k8s.io/node-feature-discovery/source/iommu"
+	_ "sigs.k8s.io/node-feature-discovery/source/kernel"
+	_ "sigs.k8s.io/node-feature-discovery/source/local"
+	_ "sigs.k8s.io/node-feature-discovery/source/memory"
+	_ "sigs.k8s.io/node-feature-discovery/source/network"
+	_ "sigs.k8s.io/node-feature-discovery/source/pci"
+	_ "sigs.k8s.io/node-feature-discovery/source/storage"
+	_ "sigs.k8s.io/node-feature-discovery/source/system"
+	_ "sigs.k8s.io/node-feature-discovery/source/usb"
 )
 
 // Global config
@@ -99,9 +102,7 @@ type nfdWorker struct {
 	client         pb.LabelerClient
 	configFilePath string
 	config         *NFDConfig
-	realSources    []source.LabelSource
 	stop           chan struct{} // channel for signaling stop
-	testSources    []source.LabelSource
 	enabledSources []source.LabelSource
 }
 
@@ -121,25 +122,7 @@ func NewNfdWorker(args *Args) (nfdclient.NfdClient, error) {
 
 		args:   *args,
 		config: &NFDConfig{},
-		realSources: []source.LabelSource{
-			&cpu.Source{},
-			&iommu.Source{},
-			&kernel.Source{},
-			&memory.Source{},
-			&network.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{},
-		},
-		testSources: []source.LabelSource{
-			&fake.Source{},
-		},
-		stop: make(chan struct{}, 1),
+		stop:   make(chan struct{}, 1),
 	}
 
 	if args.ConfigFile != "" {
@@ -301,36 +284,44 @@ func (w *nfdWorker) configureCore(c coreConfig) error {
 	}
 
 	// Determine enabled feature sources
-	sourceList := map[string]struct{}{}
-	all := false
-	for _, s := range c.Sources {
-		if s == "all" {
-			all = true
-			continue
+	enabled := make(map[string]source.LabelSource)
+	for _, name := range c.Sources {
+		if name == "all" {
+			for n, s := range source.GetAllLabelSources() {
+				if ts, ok := s.(source.TestSource); !ok || !ts.IsTestSource() {
+					enabled[n] = s
+				}
+			}
+		} else {
+			if s := source.GetLabelSource(name); s != nil {
+				enabled[name] = s
+			} else {
+				klog.Warningf("skipping unknown source %q specified in core.sources (or --sources)", name)
+			}
 		}
-		sourceList[strings.TrimSpace(s)] = struct{}{}
 	}
 
-	w.enabledSources = []source.LabelSource{}
-	for _, s := range w.realSources {
-		if _, enabled := sourceList[s.Name()]; all || enabled {
-			w.enabledSources = append(w.enabledSources, s)
-			delete(sourceList, s.Name())
-		}
+	w.enabledSources = make([]source.LabelSource, 0, len(enabled))
+	for _, s := range enabled {
+		w.enabledSources = append(w.enabledSources, s)
 	}
-	for _, s := range w.testSources {
-		if _, enabled := sourceList[s.Name()]; enabled {
-			w.enabledSources = append(w.enabledSources, s)
-			delete(sourceList, s.Name())
+
+	sort.Slice(w.enabledSources, func(i, j int) bool {
+		iP, jP := w.enabledSources[i].Priority(), w.enabledSources[j].Priority()
+		if iP != jP {
+			return iP < jP
 		}
-	}
-	if len(sourceList) > 0 {
-		names := make([]string, 0, len(sourceList))
-		for n := range sourceList {
-			names = append(names, n)
+		return w.enabledSources[i].Name() < w.enabledSources[j].Name()
+	})
+
+	if klog.V(1).Enabled() {
+		n := make([]string, len(w.enabledSources))
+		for i, s := range w.enabledSources {
+			n[i] = s.Name()
 		}
-		klog.Warningf("skipping unknown source(s) %q specified in core.sources (or --sources)", strings.Join(names, ", "))
+		klog.Infof("enabled label sources: %s", strings.Join(n, ", "))
 	}
+
 	return nil
 }
 
@@ -338,9 +329,9 @@ func (w *nfdWorker) configureCore(c coreConfig) error {
 func (w *nfdWorker) configure(filepath string, overrides string) error {
 	// Create a new default config
 	c := newDefaultConfig()
-	allSources := append(w.realSources, w.testSources...)
-	c.Sources = make(map[string]source.Config, len(allSources))
-	for _, s := range allSources {
+	confSources := source.GetAllConfigurableSources()
+	c.Sources = make(map[string]source.Config, len(confSources))
+	for _, s := range confSources {
 		c.Sources[s.Name()] = s.NewConfig()
 	}
 
@@ -388,8 +379,8 @@ func (w *nfdWorker) configure(filepath string, overrides string) error {
 		return err
 	}
 
-	// (Re-)configure all "real" sources, test sources are not configurable
-	for _, s := range allSources {
+	// (Re-)configure sources
+	for _, s := range confSources {
 		s.SetConfig(c.Sources[s.Name()])
 	}
 
@@ -432,8 +423,8 @@ func getFeatureLabels(source source.LabelSource, labelWhiteList regexp.Regexp) (
 
 	// Prefix for labels in the default namespace
 	prefix := source.Name() + "-"
-	switch source.(type) {
-	case *local.Source:
+	switch source.Name() {
+	case "local":
 		// Do not prefix labels from the hooks
 		prefix = ""
 	}
diff --git a/source/cpu/cpu.go b/source/cpu/cpu.go
index 1563b9235..c23fff4f3 100644
--- a/source/cpu/cpu.go
+++ b/source/cpu/cpu.go
@@ -78,22 +78,29 @@ type keyFilter struct {
 	whitelist bool
 }
 
-// Source implements LabelSource.
-type Source struct {
+// cpuSource implements the LabelSource and ConfigurableSource interfaces.
+type cpuSource struct {
 	config      *Config
 	cpuidFilter *keyFilter
 }
 
-func (s Source) Name() string { return Name }
+// Singleton source instance
+var (
+	src cpuSource
+	_   source.LabelSource        = &src
+	_   source.ConfigurableSource = &src
+)
+
+func (s *cpuSource) Name() string { return Name }
 
 // NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
+func (s *cpuSource) NewConfig() source.Config { return newDefaultConfig() }
 
 // GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return s.config }
+func (s *cpuSource) GetConfig() source.Config { return s.config }
 
 // SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(conf source.Config) {
+func (s *cpuSource) SetConfig(conf source.Config) {
 	switch v := conf.(type) {
 	case *Config:
 		s.config = v
@@ -103,7 +110,10 @@ func (s *Source) SetConfig(conf source.Config) {
 	}
 }
 
-func (s *Source) Discover() (source.FeatureLabels, error) {
+// Priority method of the LabelSource interface
+func (s *cpuSource) Priority() int { return 0 }
+
+func (s *cpuSource) Discover() (source.FeatureLabels, error) {
 	features := source.FeatureLabels{}
 
 	// Check if hyper-threading seems to be enabled
@@ -182,7 +192,7 @@ func haveThreadSiblings() (bool, error) {
 	return false, nil
 }
 
-func (s *Source) initCpuidFilter() {
+func (s *cpuSource) initCpuidFilter() {
 	newFilter := keyFilter{keys: map[string]struct{}{}}
 	if len(s.config.Cpuid.AttributeWhitelist) > 0 {
 		for _, k := range s.config.Cpuid.AttributeWhitelist {
@@ -210,3 +220,7 @@ func (f keyFilter) unmask(k string) bool {
 	}
 	return false
 }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/custom/custom.go b/source/custom/custom.go
index b1bca0d94..6bfaf582a 100644
--- a/source/custom/custom.go
+++ b/source/custom/custom.go
@@ -51,22 +51,29 @@ func newDefaultConfig() *config {
 	return &config{}
 }
 
-// Source implements LabelSource.
-type Source struct {
+// customSource implements the LabelSource and ConfigurableSource interfaces.
+type customSource struct {
 	config *config
 }
 
+// Singleton source instance
+var (
+	src customSource
+	_   source.LabelSource        = &src
+	_   source.ConfigurableSource = &src
+)
+
 // Name returns the name of the feature source
-func (s Source) Name() string { return Name }
+func (s *customSource) Name() string { return Name }
 
 // NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
+func (s *customSource) NewConfig() source.Config { return newDefaultConfig() }
 
 // GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return s.config }
+func (s *customSource) GetConfig() source.Config { return s.config }
 
 // SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(conf source.Config) {
+func (s *customSource) SetConfig(conf source.Config) {
 	switch v := conf.(type) {
 	case *config:
 		s.config = v
@@ -75,8 +82,11 @@ func (s *Source) SetConfig(conf source.Config) {
 	}
 }
 
+// Priority method of the LabelSource interface
+func (s *customSource) Priority() int { return 10 }
+
 // Discover features
-func (s Source) Discover() (source.FeatureLabels, error) {
+func (s *customSource) Discover() (source.FeatureLabels, error) {
 	features := source.FeatureLabels{}
 	allFeatureConfig := append(getStaticFeatureConfig(), *s.config...)
 	allFeatureConfig = append(allFeatureConfig, getDirectoryFeatureConfig()...)
@@ -101,7 +111,7 @@ func (s Source) Discover() (source.FeatureLabels, error) {
 
 // Process a single feature by Matching on the defined rules.
 // A feature is present if all defined Rules in a MatchRule return a match.
-func (s Source) discoverFeature(feature FeatureSpec) (bool, error) {
+func (s *customSource) discoverFeature(feature FeatureSpec) (bool, error) {
 	for _, matchRules := range feature.MatchOn {
 
 		allRules := []rules.Rule{
@@ -136,3 +146,7 @@ func (s Source) discoverFeature(feature FeatureSpec) (bool, error) {
 	}
 	return false, nil
 }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/fake/fake.go b/source/fake/fake.go
index 43e676b02..9d9f8d2aa 100644
--- a/source/fake/fake.go
+++ b/source/fake/fake.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2017 The Kubernetes Authors.
+Copyright 2017-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -40,22 +40,29 @@ func newDefaultConfig() *Config {
 	}
 }
 
-// Source implements LabelSource.
-type Source struct {
+// fakeSource implements the LabelSource and ConfigurableSource interfaces.
+type fakeSource struct {
 	config *Config
 }
 
+// Singleton source instance
+var (
+	src fakeSource
+	_   source.LabelSource        = &src
+	_   source.ConfigurableSource = &src
+)
+
 // Name returns an identifier string for this feature source.
-func (s Source) Name() string { return Name }
+func (s *fakeSource) Name() string { return Name }
 
 // NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
+func (s *fakeSource) NewConfig() source.Config { return newDefaultConfig() }
 
 // GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return s.config }
+func (s *fakeSource) GetConfig() source.Config { return s.config }
 
 // SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(conf source.Config) {
+func (s *fakeSource) SetConfig(conf source.Config) {
 	switch v := conf.(type) {
 	case *Config:
 		s.config = v
@@ -65,10 +72,13 @@ func (s *Source) SetConfig(conf source.Config) {
 }
 
 // Configure method of the LabelSource interface
-func (s Source) Configure([]byte) error { return nil }
+func (s *fakeSource) Configure([]byte) error { return nil }
+
+// Priority method of the LabelSource interface
+func (s *fakeSource) Priority() int { return 0 }
 
 // Discover returns feature names for some fake features.
-func (s Source) Discover() (source.FeatureLabels, error) {
+func (s *fakeSource) Discover() (source.FeatureLabels, error) {
 	// Adding three fake features.
 	features := make(source.FeatureLabels, len(s.config.Labels))
 	for k, v := range s.config.Labels {
@@ -77,3 +87,10 @@ func (s Source) Discover() (source.FeatureLabels, error) {
 
 	return features, nil
 }
+
+// IsTestSource method of the LabelSource interface
+func (s *fakeSource) IsTestSource() bool { return true }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/iommu/iommu.go b/source/iommu/iommu.go
index 36849a847..f731bf934 100644
--- a/source/iommu/iommu.go
+++ b/source/iommu/iommu.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 The Kubernetes Authors.
+Copyright 2018-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -25,21 +25,21 @@ import (
 
 const Name = "iommu"
 
-// Source implements LabelSource.
-type Source struct{}
+// iommuSource implements the LabelSource interface.
+type iommuSource struct{}
 
-func (s Source) Name() string { return Name }
+func (s *iommuSource) Name() string { return Name }
 
-// NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return nil }
-
-// GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return nil }
+// Singleton source instance
+var (
+	src iommuSource
+	_   source.LabelSource = &src
+)
 
-// SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(source.Config) {}
+// Priority method of the LabelSource interface
+func (s *iommuSource) Priority() int { return 0 }
 
-func (s Source) Discover() (source.FeatureLabels, error) {
+func (s *iommuSource) Discover() (source.FeatureLabels, error) {
 	features := source.FeatureLabels{}
 
 	// Check if any iommu devices are available
@@ -54,3 +54,7 @@ func (s Source) Discover() (source.FeatureLabels, error) {
 
 	return features, nil
 }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/kernel/kernel.go b/source/kernel/kernel.go
index 88e8be782..045809135 100644
--- a/source/kernel/kernel.go
+++ b/source/kernel/kernel.go
@@ -47,21 +47,28 @@ func newDefaultConfig() *Config {
 	}
 }
 
-// Source implements LabelSource.
-type Source struct {
+// kernelSource implements the LabelSource and ConfigurableSource interfaces.
+type kernelSource struct {
 	config *Config
 }
 
-func (s *Source) Name() string { return Name }
+// Singleton source instance
+var (
+	src kernelSource
+	_   source.LabelSource        = &src
+	_   source.ConfigurableSource = &src
+)
+
+func (s *kernelSource) Name() string { return Name }
 
 // NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
+func (s *kernelSource) NewConfig() source.Config { return newDefaultConfig() }
 
 // GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return s.config }
+func (s *kernelSource) GetConfig() source.Config { return s.config }
 
 // SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(conf source.Config) {
+func (s *kernelSource) SetConfig(conf source.Config) {
 	switch v := conf.(type) {
 	case *Config:
 		s.config = v
@@ -70,7 +77,10 @@ func (s *Source) SetConfig(conf source.Config) {
 	}
 }
 
-func (s *Source) Discover() (source.FeatureLabels, error) {
+// Priority method of the LabelSource interface
+func (s *kernelSource) Priority() int { return 0 }
+
+func (s *kernelSource) Discover() (source.FeatureLabels, error) {
 	features := source.FeatureLabels{}
 
 	// Read kernel version
@@ -135,3 +145,7 @@ func parseVersion() (map[string]string, error) {
 
 	return version, nil
 }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/local/local.go b/source/local/local.go
index 8fc9331f7..dc3427fe1 100644
--- a/source/local/local.go
+++ b/source/local/local.go
@@ -38,23 +38,23 @@ var (
 	hookDir         = "/etc/kubernetes/node-feature-discovery/source.d/"
 )
 
-// Source implements LabelSource.
-type Source struct{}
+// localSource implements the LabelSource interface.
+type localSource struct{}
 
-// Name method of the LabelSource interface
-func (s Source) Name() string { return Name }
-
-// NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return nil }
+// Singleton source instance
+var (
+	src localSource
+	_   source.LabelSource = &src
+)
 
-// GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return nil }
+// Name method of the LabelSource interface
+func (s *localSource) Name() string { return Name }
 
-// SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(source.Config) {}
+// Priority method of the LabelSource interface
+func (s *localSource) Priority() int { return 20 }
 
 // Discover method of the LabelSource interface
-func (s Source) Discover() (source.FeatureLabels, error) {
+func (s *localSource) Discover() (source.FeatureLabels, error) {
 	featuresFromHooks, err := getFeaturesFromHooks()
 	if err != nil {
 		klog.Error(err)
@@ -240,3 +240,7 @@ func getFileContent(fileName string) ([][]byte, error) {
 
 	return lines, nil
 }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/memory/memory.go b/source/memory/memory.go
index 4311171ae..9fde92010 100644
--- a/source/memory/memory.go
+++ b/source/memory/memory.go
@@ -28,23 +28,23 @@ import (
 
 const Name = "memory"
 
-// Source implements LabelSource.
-type Source struct{}
+// memorySource implements the LabelSource interface.
+type memorySource struct{}
 
-// Name returns an identifier string for this feature source.
-func (s Source) Name() string { return Name }
-
-// NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return nil }
+// Singleton source instance
+var (
+	src memorySource
+	_   source.LabelSource = &src
+)
 
-// GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return nil }
+// Name returns an identifier string for this feature source.
+func (s *memorySource) Name() string { return Name }
 
-// SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(source.Config) {}
+// Priority method of the LabelSource interface
+func (s *memorySource) Priority() int { return 0 }
 
 // Discover returns feature names for memory: numa if more than one memory node is present.
-func (s Source) Discover() (source.FeatureLabels, error) {
+func (s *memorySource) Discover() (source.FeatureLabels, error) {
 	features := source.FeatureLabels{}
 
 	// Detect NUMA
@@ -118,3 +118,7 @@ func detectNvdimm() (map[string]bool, error) {
 
 	return features, nil
 }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/mock_LabelSource.go b/source/mock_LabelSource.go
index 82d55fdcc..96865c6fb 100644
--- a/source/mock_LabelSource.go
+++ b/source/mock_LabelSource.go
@@ -34,22 +34,6 @@ func (_m *MockLabelSource) Discover() (FeatureLabels, error) {
 	return r0, r1
 }
 
-// GetConfig provides a mock function with given fields:
-func (_m *MockLabelSource) 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 *MockLabelSource) Name() string {
 	ret := _m.Called()
@@ -64,23 +48,16 @@ func (_m *MockLabelSource) Name() string {
 	return r0
 }
 
-// NewConfig provides a mock function with given fields:
-func (_m *MockLabelSource) NewConfig() Config {
+// Priority provides a mock function with given fields:
+func (_m *MockLabelSource) Priority() int {
 	ret := _m.Called()
 
-	var r0 Config
-	if rf, ok := ret.Get(0).(func() Config); ok {
+	var r0 int
+	if rf, ok := ret.Get(0).(func() int); ok {
 		r0 = rf()
 	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(Config)
-		}
+		r0 = ret.Get(0).(int)
 	}
 
 	return r0
 }
-
-// SetConfig provides a mock function with given fields: _a0
-func (_m *MockLabelSource) SetConfig(_a0 Config) {
-	_m.Called(_a0)
-}
diff --git a/source/network/network.go b/source/network/network.go
index 9a1748722..2ebf26778 100644
--- a/source/network/network.go
+++ b/source/network/network.go
@@ -41,23 +41,23 @@ const (
 
 const sysfsBaseDir = "class/net"
 
-// Source implements LabelSource.
-type Source struct{}
+// networkSource implements the LabelSource interface.
+type networkSource struct{}
 
-// Name returns an identifier string for this feature source.
-func (s Source) Name() string { return Name }
-
-// NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return nil }
+// Singleton source instance
+var (
+	src networkSource
+	_   source.LabelSource = &src
+)
 
-// GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return nil }
+// Name returns an identifier string for this feature source.
+func (s *networkSource) Name() string { return Name }
 
-// SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(source.Config) {}
+// Priority method of the LabelSource interface
+func (s *networkSource) Priority() int { return 0 }
 
 // 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.FeatureLabels, error) {
+func (s *networkSource) Discover() (source.FeatureLabels, error) {
 	features := source.FeatureLabels{}
 
 	netInterfaces, err := ioutil.ReadDir(source.SysfsDir.Path(sysfsBaseDir))
@@ -128,3 +128,7 @@ func readIfFlags(name string) (uint64, error) {
 
 	return flags, nil
 }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/pci/pci.go b/source/pci/pci.go
index a68f270ae..b100f0fbe 100644
--- a/source/pci/pci.go
+++ b/source/pci/pci.go
@@ -41,22 +41,29 @@ func newDefaultConfig() *Config {
 	}
 }
 
-// Source implements LabelSource.
-type Source struct {
+// pciSource implements the LabelSource and ConfigurableSource interfaces.
+type pciSource struct {
 	config *Config
 }
 
+// Singleton source instance
+var (
+	src pciSource
+	_   source.LabelSource        = &src
+	_   source.ConfigurableSource = &src
+)
+
 // Name returns the name of the feature source
-func (s Source) Name() string { return Name }
+func (s *pciSource) Name() string { return Name }
 
 // NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
+func (s *pciSource) NewConfig() source.Config { return newDefaultConfig() }
 
 // GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return s.config }
+func (s *pciSource) GetConfig() source.Config { return s.config }
 
 // SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(conf source.Config) {
+func (s *pciSource) SetConfig(conf source.Config) {
 	switch v := conf.(type) {
 	case *Config:
 		s.config = v
@@ -65,8 +72,11 @@ func (s *Source) SetConfig(conf source.Config) {
 	}
 }
 
+// Priority method of the LabelSource interface
+func (s *pciSource) Priority() int { return 0 }
+
 // Discover features
-func (s Source) Discover() (source.FeatureLabels, error) {
+func (s *pciSource) Discover() (source.FeatureLabels, error) {
 	features := source.FeatureLabels{}
 
 	// Construct a device label format, a sorted list of valid attributes
@@ -132,3 +142,7 @@ func (s Source) Discover() (source.FeatureLabels, error) {
 	}
 	return features, nil
 }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/source.go b/source/source.go
index d70e10983..e490857f5 100644
--- a/source/source.go
+++ b/source/source.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2017 The Kubernetes Authors.
+Copyright 2017-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,20 +16,31 @@ limitations under the License.
 
 package source
 
-// FeatureLabelValue represents the value of one feature label
-type FeatureLabelValue interface{}
+import (
+	"fmt"
+)
 
-// FeatureLabels is a collection of feature labels
-type FeatureLabels map[string]FeatureLabelValue
+// Source is the base interface for all other source interfaces
+type Source interface {
+	// Name returns a friendly name for this source
+	Name() string
+}
 
 // LabelSource represents a source of node feature labels
 type LabelSource interface {
-	// Name returns a friendly name for this source
-	Name() string
+	Source
 
 	// Discover returns discovered feature labels
 	Discover() (FeatureLabels, error)
 
+	// Priority returns the priority of the source
+	Priority() int
+}
+
+// ConfigurableSource is an interface for a source that can be configured
+type ConfigurableSource interface {
+	Source
+
 	// NewConfig returns a new default config of the source
 	NewConfig() Config
 
@@ -40,6 +51,69 @@ type LabelSource interface {
 	SetConfig(Config)
 }
 
+// TestSource represents a source purposed for testing only
+type TestSource interface {
+	Source
+
+	// IsTestSource returns true if the source is not for production
+	IsTestSource() bool
+}
+
+// FeatureLabelValue represents the value of one feature label
+type FeatureLabelValue interface{}
+
+// FeatureLabels is a collection of feature labels
+type FeatureLabels map[string]FeatureLabelValue
+
 // Config is the generic interface for source configuration data
 type Config interface {
 }
+
+// sources contain all registered sources
+var sources = make(map[string]Source)
+
+// RegisterSource registers a source
+func Register(s Source) {
+	if name, ok := sources[s.Name()]; ok {
+		panic(fmt.Sprintf("source %q already registered", name))
+	}
+	sources[s.Name()] = s
+}
+
+// GetLabelSource a registered label source
+func GetLabelSource(name string) LabelSource {
+	if s, ok := sources[name].(LabelSource); ok {
+		return s
+	}
+	return nil
+}
+
+// GetAllLabelSources returns all registered label sources
+func GetAllLabelSources() map[string]LabelSource {
+	all := make(map[string]LabelSource)
+	for k, v := range sources {
+		if s, ok := v.(LabelSource); ok {
+			all[k] = s
+		}
+	}
+	return all
+}
+
+// GetConfigurableSource a registered configurable source
+func GetConfigurableSource(name string) ConfigurableSource {
+	if s, ok := sources[name].(ConfigurableSource); ok {
+		return s
+	}
+	return nil
+}
+
+// GetAllConfigurableSources returns all registered configurable sources
+func GetAllConfigurableSources() map[string]ConfigurableSource {
+	all := make(map[string]ConfigurableSource)
+	for k, v := range sources {
+		if s, ok := v.(ConfigurableSource); ok {
+			all[k] = s
+		}
+	}
+	return all
+}
diff --git a/source/storage/storage.go b/source/storage/storage.go
index a2ae032fc..34383340a 100644
--- a/source/storage/storage.go
+++ b/source/storage/storage.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2018 The Kubernetes Authors.
+Copyright 2018-2021 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -25,23 +25,23 @@ import (
 
 const Name = "storage"
 
-// Source implements LabelSource.
-type Source struct{}
+// storageSource implements the LabelSource interface.
+type storageSource struct{}
 
-// Name returns an identifier string for this feature source.
-func (s Source) Name() string { return Name }
-
-// NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return nil }
+// Singleton source instance
+var (
+	src storageSource
+	_   source.LabelSource = &src
+)
 
-// GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return nil }
+// Name returns an identifier string for this feature source.
+func (s *storageSource) Name() string { return Name }
 
-// SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(source.Config) {}
+// Priority method of the LabelSource interface
+func (s *storageSource) Priority() int { return 0 }
 
 // Discover returns feature names for storage: nonrotationaldisk if any SSD drive present.
-func (s Source) Discover() (source.FeatureLabels, error) {
+func (s *storageSource) Discover() (source.FeatureLabels, error) {
 	features := source.FeatureLabels{}
 
 	// Check if there is any non-rotational block devices attached to the node
@@ -62,3 +62,7 @@ func (s Source) Discover() (source.FeatureLabels, error) {
 	}
 	return features, nil
 }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/system/system.go b/source/system/system.go
index 889aa8ceb..2ee4ac029 100644
--- a/source/system/system.go
+++ b/source/system/system.go
@@ -34,21 +34,21 @@ var osReleaseFields = [...]string{
 
 const Name = "system"
 
-// Source implements LabelSource.
-type Source struct{}
+// systemSource implements the LabelSource interface.
+type systemSource struct{}
 
-func (s Source) Name() string { return Name }
-
-// NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return nil }
+// Singleton source instance
+var (
+	src systemSource
+	_   source.LabelSource = &src
+)
 
-// GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return nil }
+func (s *systemSource) Name() string { return Name }
 
-// SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(source.Config) {}
+// Priority method of the LabelSource interface
+func (s *systemSource) Priority() int { return 0 }
 
-func (s Source) Discover() (source.FeatureLabels, error) {
+func (s *systemSource) Discover() (source.FeatureLabels, error) {
 	features := source.FeatureLabels{}
 
 	release, err := parseOSRelease()
@@ -112,3 +112,7 @@ func splitVersion(version string) map[string]string {
 	}
 	return components
 }
+
+func init() {
+	source.Register(&src)
+}
diff --git a/source/usb/usb.go b/source/usb/usb.go
index 81b8003ef..80774fbec 100644
--- a/source/usb/usb.go
+++ b/source/usb/usb.go
@@ -44,22 +44,29 @@ func newDefaultConfig() *Config {
 	}
 }
 
-// Source implements LabelSource.
-type Source struct {
+// usbSource implements the LabelSource and ConfigurableSource interfaces.
+type usbSource struct {
 	config *Config
 }
 
+// Singleton source instance
+var (
+	src usbSource
+	_   source.LabelSource        = &src
+	_   source.ConfigurableSource = &src
+)
+
 // Name returns the name of the feature source
-func (s Source) Name() string { return Name }
+func (s *usbSource) Name() string { return Name }
 
 // NewConfig method of the LabelSource interface
-func (s *Source) NewConfig() source.Config { return newDefaultConfig() }
+func (s *usbSource) NewConfig() source.Config { return newDefaultConfig() }
 
 // GetConfig method of the LabelSource interface
-func (s *Source) GetConfig() source.Config { return s.config }
+func (s *usbSource) GetConfig() source.Config { return s.config }
 
 // SetConfig method of the LabelSource interface
-func (s *Source) SetConfig(conf source.Config) {
+func (s *usbSource) SetConfig(conf source.Config) {
 	switch v := conf.(type) {
 	case *Config:
 		s.config = v
@@ -68,8 +75,11 @@ func (s *Source) SetConfig(conf source.Config) {
 	}
 }
 
+// Priority method of the LabelSource interface
+func (s *usbSource) Priority() int { return 0 }
+
 // Discover features
-func (s Source) Discover() (source.FeatureLabels, error) {
+func (s *usbSource) Discover() (source.FeatureLabels, error) {
 	features := source.FeatureLabels{}
 
 	// Construct a device label format, a sorted list of valid attributes
@@ -127,3 +137,7 @@ func (s Source) Discover() (source.FeatureLabels, error) {
 	}
 	return features, nil
 }
+
+func init() {
+	source.Register(&src)
+}


Test patch:
diff --git a/pkg/nfd-client/worker/nfd-worker-internal_test.go b/pkg/nfd-client/worker/nfd-worker-internal_test.go
index d2255cb9b..9070c0969 100644
--- a/pkg/nfd-client/worker/nfd-worker-internal_test.go
+++ b/pkg/nfd-client/worker/nfd-worker-internal_test.go
@@ -33,7 +33,6 @@ import (
 	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"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/pci"
 )
@@ -96,15 +95,6 @@ func makeFakeFeatures(names []string) (source.FeatureLabels, Labels) {
 	return features, labels
 }
 
-func (w *nfdWorker) getSource(name string) source.LabelSource {
-	for _, s := range w.realSources {
-		if s.Name() == name {
-			return s
-		}
-	}
-	return nil
-}
-
 func TestConfigParse(t *testing.T) {
 	Convey("When parsing configuration", t, func() {
 		w, err := NewNfdWorker(&Args{})
@@ -130,7 +120,7 @@ func TestConfigParse(t *testing.T) {
 			Convey("overrides should take effect", func() {
 				So(worker.config.Core.NoPublish, ShouldBeTrue)
 
-				c := worker.getSource("cpu").GetConfig().(*cpu.Config)
+				c := source.GetConfigurableSource("cpu").GetConfig().(*cpu.Config)
 				So(c.Cpuid.AttributeBlacklist, ShouldResemble, []string{"foo", "bar"})
 			})
 		})
@@ -167,9 +157,9 @@ sources:
 
 				// Verify feature source config
 				So(err, ShouldBeNil)
-				c := worker.getSource("kernel").GetConfig()
+				c := source.GetConfigurableSource("kernel").GetConfig()
 				So(c.(*kernel.Config).ConfigOpts, ShouldResemble, []string{"DMI"})
-				c = worker.getSource("pci").GetConfig()
+				c = source.GetConfigurableSource("pci").GetConfig()
 				So(c.(*pci.Config).DeviceClassWhitelist, ShouldResemble, []string{"ff"})
 			})
 		})
@@ -189,9 +179,9 @@ sources:
 
 				// Verify feature source config
 				So(err, ShouldBeNil)
-				c := worker.getSource("kernel").GetConfig()
+				c := source.GetConfigurableSource("kernel").GetConfig()
 				So(c.(*kernel.Config).ConfigOpts, ShouldResemble, []string{"DMI"})
-				c = worker.getSource("pci").GetConfig()
+				c = source.GetConfigurableSource("pci").GetConfig()
 				So(c.(*pci.Config).DeviceClassWhitelist, ShouldResemble, []string{"03"})
 			})
 		})
@@ -318,7 +308,7 @@ func TestNewNfdWorker(t *testing.T) {
 			worker := w.(*nfdWorker)
 			So(worker.configure("", ""), ShouldBeNil)
 			Convey("all sources should be enabled and the whitelist regexp should be empty", func() {
-				So(len(worker.enabledSources), ShouldEqual, len(worker.realSources))
+				So(len(worker.enabledSources), ShouldEqual, len(source.GetAllLabelSources())-1)
 				So(worker.config.Core.LabelWhiteList, ShouldResemble, emptyRegexp)
 			})
 		})
@@ -333,7 +323,7 @@ func TestNewNfdWorker(t *testing.T) {
 			So(worker.configure("", ""), ShouldBeNil)
 			Convey("proper sources should be enabled", func() {
 				So(len(worker.enabledSources), ShouldEqual, 1)
-				So(worker.enabledSources[0], ShouldHaveSameTypeAs, &fake.Source{})
+				So(worker.enabledSources[0].Name(), ShouldEqual, "fake")
 				So(worker.config.Core.LabelWhiteList, ShouldResemble, emptyRegexp)
 			})
 		})
@@ -356,9 +346,9 @@ func TestNewNfdWorker(t *testing.T) {
 
 func TestCreateFeatureLabels(t *testing.T) {
 	Convey("When creating feature labels from the configured sources", t, func() {
-		fakeLabelSource := source.LabelSource(new(fake.Source))
-		fakeLabelSource.SetConfig(fakeLabelSource.NewConfig())
-		sources := []source.LabelSource{fakeLabelSource}
+		cs := source.GetConfigurableSource("fake")
+		cs.SetConfig(cs.NewConfig())
+		sources := []source.LabelSource{source.GetLabelSource("fake")}
 
 		Convey("When fake feature source is configured", func() {
 			emptyLabelWL := regexp.MustCompile("")
diff --git a/source/source_test.go b/source/source_test.go
new file mode 100644
index 000000000..c8a6980ae
--- /dev/null
+++ b/source/source_test.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2021 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package source_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	source "sigs.k8s.io/node-feature-discovery/source"
+
+	// Register all source packages
+	_ "sigs.k8s.io/node-feature-discovery/source/cpu"
+	_ "sigs.k8s.io/node-feature-discovery/source/custom"
+	_ "sigs.k8s.io/node-feature-discovery/source/fake"
+	_ "sigs.k8s.io/node-feature-discovery/source/iommu"
+	_ "sigs.k8s.io/node-feature-discovery/source/kernel"
+	_ "sigs.k8s.io/node-feature-discovery/source/local"
+	_ "sigs.k8s.io/node-feature-discovery/source/memory"
+	_ "sigs.k8s.io/node-feature-discovery/source/network"
+	_ "sigs.k8s.io/node-feature-discovery/source/pci"
+	_ "sigs.k8s.io/node-feature-discovery/source/storage"
+	_ "sigs.k8s.io/node-feature-discovery/source/system"
+	_ "sigs.k8s.io/node-feature-discovery/source/usb"
+)
+
+func TestLabelSources(t *testing.T) {
+	sources := source.GetAllLabelSources()
+	assert.NotZero(t, len(sources))
+
+	for n, s := range sources {
+		assert.Equalf(t, n, s.Name(), "testing labelsource %q failed", n)
+	}
+}
+
+func TestConfigurableSources(t *testing.T) {
+	sources := source.GetAllConfigurableSources()
+	assert.NotZero(t, len(sources))
+
+	for n, s := range sources {
+		assert.Equalf(t, n, s.Name(), "testing ConfigurableSource %q failed", n)
+
+		c := s.NewConfig()
+		s.SetConfig(c)
+		rc := s.GetConfig()
+
+		assert.Equalf(t, c, rc, "testing ConfigurableSource %q failed", n)
+	}
+}