Repository: kubernetes-sigs/node-feature-discovery
Pull request #444 (2021-02-19 17:27:18)

Switch to flags package in command line arg parsing
Time to let the docopt-go go 😄  One motivation for this change is to move to klog-based logging.

k8s-ci-robot: Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with `/test all`
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/444#" 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":[]} -->
marquiz: > I'm not sure what would be the nicest way to handle the nfd-worker cmdline to config overrides. This implements one way but I'm not sure this is the best 🤔

I'm a bit more satisfied with this, now

k8s-ci-robot: @marquiz: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-verify | a2fce8d2a6489791837d4fdaa530229353bb4211 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/444/pull-node-feature-discovery-verify/1363800422023696384) | `/test pull-node-feature-discovery-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=444). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Amarquiz). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/node-feature-discovery/issues?q=is:issue+is:open) when you hit one in your PR.

<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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
marquiz: Exploring some yet another implementation
/hold
marquiz: > Exploring some yet another implementation

Ach, I didn't end up as nice as I hoped.
/unhold

Patch:
diff --git a/cmd/nfd-master/main.go b/cmd/nfd-master/main.go
index 9f66a37c3..6611d5ccd 100644
--- a/cmd/nfd-master/main.go
+++ b/cmd/nfd-master/main.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Kubernetes Authors.
+Copyright 2019-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.
@@ -17,14 +17,14 @@ limitations under the License.
 package main
 
 import (
+	"flag"
 	"fmt"
 	"log"
+	"os"
 	"regexp"
-	"strconv"
-	"strings"
 
-	"github.com/docopt/docopt-go"
 	master "sigs.k8s.io/node-feature-discovery/pkg/nfd-master"
+	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"sigs.k8s.io/node-feature-discovery/pkg/version"
 )
 
@@ -34,17 +34,29 @@ const (
 )
 
 func main() {
+	flags := flag.NewFlagSet(ProgramName, flag.ExitOnError)
+
+	printVersion := flags.Bool("version", false, "Print version and exit.")
+
+	args := initFlags(flags)
+
+	_ = flags.Parse(os.Args[1:])
+	if len(flags.Args()) > 0 {
+		fmt.Printf("unknown command line argument: %s\n", flags.Args()[0])
+		flags.Usage()
+		os.Exit(2)
+	}
+
+	if *printVersion {
+		fmt.Println(ProgramName, version.Get())
+		os.Exit(0)
+	}
+
 	// Assert that the version is known
 	if version.Undefined() {
 		log.Print("WARNING: version not set! Set -ldflags \"-X sigs.k8s.io/node-feature-discovery/pkg/version.version=`git describe --tags --dirty --always`\" during build or run.")
 	}
 
-	// Parse command-line arguments.
-	args, err := argsParse(nil)
-	if err != nil {
-		log.Fatalf("failed to parse command line: %v", err)
-	}
-
 	// Get new NfdMaster instance
 	instance, err := master.NewNfdMaster(args)
 	if err != nil {
@@ -56,88 +68,37 @@ func main() {
 	}
 }
 
-// argsParse parses the command line arguments passed to the program.
-// The argument argv is passed only for testing purposes.
-func argsParse(argv []string) (master.Args, error) {
-	args := master.Args{}
-	usage := fmt.Sprintf(`%s.
-
-  Usage:
-  %s [--prune] [--no-publish] [--label-whitelist=<pattern>] [--port=<port>]
-     [--ca-file=<path>] [--cert-file=<path>] [--key-file=<path>]
-     [--verify-node-name] [--extra-label-ns=<list>] [--resource-labels=<list>]
-     [--kubeconfig=<path>] [--instance=<name>]
-  %s -h | --help
-  %s --version
-
-  Options:
-  -h --help                       Show this screen.
-  --version                       Output version and exit.
-  --prune                         Prune all NFD related attributes from all nodes
-                                  of the cluster and exit.
-  --instance=<name>               Instance name. Used to separate annotation
-                                  namespaces for multiple parallel deployments.
-                                  [Default: ]
-  --kubeconfig=<path>             Kubeconfig to use [Default: ]
-                                  of the cluster and exit.
-  --port=<port>                   Port on which to listen for connections.
-                                  [Default: 8080]
-  --ca-file=<path>                Root certificate for verifying connections
-                                  [Default: ]
-  --cert-file=<path>              Certificate used for authenticating connections
-                                  [Default: ]
-  --key-file=<path>               Private key matching --cert-file
-                                  [Default: ]
-  --verify-node-name              Verify worker node name against CN from the TLS
-                                  certificate. Only has effect when TLS authentication
-                                  has been enabled.
-  --no-publish                    Do not publish feature labels
-  --label-whitelist=<pattern>     Regular expression to filter label names to
-                                  publish to the Kubernetes API server.
-                                  NB: the label namespace is omitted i.e. the filter
-                                  is only applied to the name part after '/'.
-                                  [Default: ]
-  --extra-label-ns=<list>         Comma separated list of allowed extra label namespaces
-                                  [Default: ]
-  --resource-labels=<list>        Comma separated list of labels to be exposed as extended resources.
-                                  [Default: ]`,
-		ProgramName,
-		ProgramName,
-		ProgramName,
-		ProgramName,
-	)
-
-	arguments, _ := docopt.ParseArgs(usage, argv,
-		fmt.Sprintf("%s %s", ProgramName, version.Get()))
-
-	// Parse argument values as usable types.
-	var err error
-	args.Instance = arguments["--instance"].(string)
-	if ok, _ := regexp.MatchString(`^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$`, args.Instance); args.Instance != "" && !ok {
-		return args, fmt.Errorf("invalid --instance %q: instance name "+
-			"must start and end with an alphanumeric character and may only contain "+
-			"alphanumerics, `-`, `_` or `.`", args.Instance)
-	}
-	args.CaFile = arguments["--ca-file"].(string)
-	args.CertFile = arguments["--cert-file"].(string)
-	args.KeyFile = arguments["--key-file"].(string)
-	args.NoPublish = arguments["--no-publish"].(bool)
-	args.Port, err = strconv.Atoi(arguments["--port"].(string))
-	if err != nil {
-		return args, fmt.Errorf("invalid --port defined: %s", err)
-	}
-	args.LabelWhiteList, err = regexp.Compile(arguments["--label-whitelist"].(string))
-	if err != nil {
-		return args, fmt.Errorf("error parsing whitelist regex (%s): %s", arguments["--label-whitelist"], err)
-	}
-	args.VerifyNodeName = arguments["--verify-node-name"].(bool)
-	args.ExtraLabelNs = map[string]struct{}{}
-	for _, n := range strings.Split(arguments["--extra-label-ns"].(string), ",") {
-		args.ExtraLabelNs[n] = struct{}{}
+func initFlags(flagset *flag.FlagSet) *master.Args {
+	args := &master.Args{
+		LabelWhiteList: utils.RegexpVal{Regexp: *regexp.MustCompile("")},
 	}
-	args.ResourceLabels = strings.Split(arguments["--resource-labels"].(string), ",")
-	args.Prune = arguments["--prune"].(bool)
-	args.Kubeconfig = arguments["--kubeconfig"].(string)
 
-	return args, nil
+	flagset.StringVar(&args.CaFile, "ca-file", "",
+		"Root certificate for verifying connections")
+	flagset.StringVar(&args.CertFile, "cert-file", "",
+		"Certificate used for authenticating connections")
+	flagset.Var(&args.ExtraLabelNs, "extra-label-ns",
+		"Comma separated list of allowed extra label namespaces")
+	flagset.StringVar(&args.Instance, "instance", "",
+		"Instance name. Used to separate annotation namespaces for multiple parallel deployments.")
+	flagset.StringVar(&args.KeyFile, "key-file", "",
+		"Private key matching -cert-file")
+	flagset.StringVar(&args.Kubeconfig, "kubeconfig", "",
+		"Kubeconfig to use")
+	flagset.Var(&args.LabelWhiteList, "label-whitelist",
+		"Regular expression to filter label names to publish to the Kubernetes API server. "+
+			"NB: the label namespace is omitted i.e. the filter is only applied to the name part after '/'.")
+	flagset.BoolVar(&args.NoPublish, "no-publish", false,
+		"Do not publish feature labels")
+	flagset.IntVar(&args.Port, "port", 8080,
+		"Port on which to listen for connections.")
+	flagset.BoolVar(&args.Prune, "prune", false,
+		"Prune all NFD related attributes from all nodes of the cluaster and exit.")
+	flagset.Var(&args.ResourceLabels, "resource-labels",
+		"Comma separated list of labels to be exposed as extended resources.")
+	flagset.BoolVar(&args.VerifyNodeName, "verify-node-name", false,
+		"Verify worker node name against CN from the TLS certificate. "+
+			"Only takes effect when TLS authentication has been enabled.")
+
+	return args
 }
diff --git a/cmd/nfd-worker/main.go b/cmd/nfd-worker/main.go
index 01f93c4f9..3182a6a53 100644
--- a/cmd/nfd-worker/main.go
+++ b/cmd/nfd-worker/main.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Kubernetes Authors.
+Copyright 2019-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.
@@ -17,14 +17,13 @@ limitations under the License.
 package main
 
 import (
+	"flag"
 	"fmt"
 	"log"
-	"regexp"
-	"strings"
-	"time"
+	"os"
 
-	"github.com/docopt/docopt-go"
 	worker "sigs.k8s.io/node-feature-discovery/pkg/nfd-worker"
+	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"sigs.k8s.io/node-feature-discovery/pkg/version"
 )
 
@@ -34,17 +33,22 @@ const (
 )
 
 func main() {
+	flags := flag.NewFlagSet(ProgramName, flag.ExitOnError)
+
+	printVersion := flags.Bool("version", false, "Print version and exit.")
+
+	args := parseArgs(flags, os.Args[1:]...)
+
+	if *printVersion {
+		fmt.Println(ProgramName, version.Get())
+		os.Exit(0)
+	}
+
 	// Assert that the version is known
 	if version.Undefined() {
 		log.Printf("WARNING: version not set! Set -ldflags \"-X sigs.k8s.io/node-feature-discovery/pkg/version.version=`git describe --tags --dirty --always`\" during build or run.")
 	}
 
-	// Parse command-line arguments.
-	args, err := argsParse(nil)
-	if err != nil {
-		log.Fatalf("failed to parse command line: %v", err)
-	}
-
 	// Get new NfdWorker instance
 	instance, err := worker.NewNfdWorker(args)
 	if err != nil {
@@ -56,104 +60,74 @@ func main() {
 	}
 }
 
-// argsParse parses the command line arguments passed to the program.
-// The argument argv is passed only for testing purposes.
-func argsParse(argv []string) (worker.Args, error) {
-	args := worker.Args{}
-	usage := fmt.Sprintf(`%s.
-
-  Usage:
-  %s [--no-publish] [--sources=<sources>] [--label-whitelist=<pattern>]
-     [--oneshot | --sleep-interval=<seconds>] [--config=<path>]
-     [--options=<config>] [--server=<server>] [--server-name-override=<name>]
-     [--ca-file=<path>] [--cert-file=<path>] [--key-file=<path>]
-  %s -h | --help
-  %s --version
-
-  Options:
-  -h --help                   Show this screen.
-  --version                   Output version and exit.
-  --config=<path>             Config file to use.
-                              [Default: /etc/kubernetes/node-feature-discovery/nfd-worker.conf]
-  --options=<config>          Specify config options from command line. Config
-                              options are specified in the same format as in the
-                              config file (i.e. json or yaml). These options
-                              will override settings read from the config file.
-                              [Default: ]
-  --ca-file=<path>            Root certificate for verifying connections
-                              [Default: ]
-  --cert-file=<path>          Certificate used for authenticating connections
-                              [Default: ]
-  --key-file=<path>           Private key matching --cert-file
-                              [Default: ]
-  --server=<server>           NFD server address to connecto to.
-                              [Default: localhost:8080]
-  --server-name-override=<name> Name (CN) expect from server certificate, useful
-                              in testing
-                              [Default: ]
-  --sources=<sources>         Comma separated list of feature sources. Special
-                              value 'all' enables all feature sources.
-                              (DEPRECATED: This parameter should be set via the
-                              config file)
-  --no-publish                Do not publish discovered features to the
-                              cluster-local Kubernetes API server.
-  --label-whitelist=<pattern> Regular expression to filter label names to
-                              publish to the Kubernetes API server.
-                              NB: the label namespace is omitted i.e. the filter
-                              is only applied to the name part after '/'.
-                              (DEPRECATED: This parameter should be set via the
-                              config file)
-  --oneshot                   Label once and exit.
-  --sleep-interval=<seconds>  Time to sleep between re-labeling. Non-positive
-                              value implies no re-labeling (i.e. infinite
-                              sleep).
-                              (DEPRECATED: This parameter should be set via the
-                              config file)`,
-		ProgramName,
-		ProgramName,
-		ProgramName,
-		ProgramName,
-	)
-
-	arguments, _ := docopt.ParseArgs(usage, argv,
-		fmt.Sprintf("%s %s", ProgramName, version.Get()))
-
-	// Parse argument values as usable types.
-	args.CaFile = arguments["--ca-file"].(string)
-	args.CertFile = arguments["--cert-file"].(string)
-	args.ConfigFile = arguments["--config"].(string)
-	args.KeyFile = arguments["--key-file"].(string)
-	args.Options = arguments["--options"].(string)
-	args.Server = arguments["--server"].(string)
-	args.ServerNameOverride = arguments["--server-name-override"].(string)
-	args.Oneshot = arguments["--oneshot"].(bool)
-
-	// Parse deprecated/override args
-	if v := arguments["--label-whitelist"]; v != nil {
-		s := v.(string)
-		// Compile labelWhiteList regex
-		if r, err := regexp.Compile(s); err != nil {
-			return args, fmt.Errorf("error parsing --label-whitelist regex (%s): %v", s, err)
-		} else {
-			args.LabelWhiteList = r
-		}
-	}
-	if arguments["--no-publish"].(bool) {
-		b := true
-		args.NoPublish = &b
+func parseArgs(flags *flag.FlagSet, osArgs ...string) *worker.Args {
+	args, overrides := initFlags(flags)
+
+	_ = flags.Parse(osArgs)
+	if len(flags.Args()) > 0 {
+		fmt.Printf("unknown command line argument: %s\n", flags.Args()[0])
+		flags.Usage()
+		os.Exit(2)
 	}
-	if v := arguments["--sleep-interval"]; v != nil {
-		log.Printf("WARNING: --sleep-interval is deprecated, use 'core.sleepInterval' option in the config file, instead")
-		if s, err := time.ParseDuration(v.(string)); err != nil {
-			return args, fmt.Errorf("invalid --sleep-interval specified: %s", err.Error())
-		} else {
-			args.SleepInterval = &s
+
+	// Handle overrides
+	flags.Visit(func(f *flag.Flag) {
+		switch f.Name {
+		case "no-publish":
+			args.Overrides.NoPublish = overrides.NoPublish
+		case "label-whitelist":
+			log.Printf("WARNING: --label-whitelist is deprecated, use 'core.labelWhiteList' option in the config file, instead")
+			args.Overrides.LabelWhiteList = overrides.LabelWhiteList
+		case "sleep-interval":
+			log.Printf("WARNING: --sleep-interval is deprecated, use 'core.sleepInterval' option in the config file, instead")
+			args.Overrides.SleepInterval = overrides.SleepInterval
+		case "sources":
+			log.Printf("WARNING: --sources is deprecated, use 'core.sources' option in the config file, instead")
+			args.Overrides.Sources = overrides.Sources
 		}
+	})
+
+	return args
+}
+
+func initFlags(flagset *flag.FlagSet) (*worker.Args, *worker.ConfigOverrideArgs) {
+	args := &worker.Args{}
+
+	flagset.StringVar(&args.CaFile, "ca-file", "",
+		"Root certificate for verifying connections")
+	flagset.StringVar(&args.CertFile, "cert-file", "",
+		"Certificate used for authenticating connections")
+	flagset.StringVar(&args.ConfigFile, "config", "/etc/kubernetes/node-feature-discovery/nfd-worker.conf",
+		"Config file to use.")
+	flagset.StringVar(&args.KeyFile, "key-file", "",
+		"Private key matching -cert-file")
+	flagset.BoolVar(&args.Oneshot, "oneshot", false,
+		"Do not publish feature labels")
+	flagset.StringVar(&args.Options, "options", "",
+		"Specify config options from command line. Config options are specified "+
+			"in the same format as in the config file (i.e. json or yaml). These options")
+	flagset.StringVar(&args.Server, "server", "localhost:8080",
+		"NFD server address to connecto to.")
+	flagset.StringVar(&args.ServerNameOverride, "server-name-override", "",
+		"Hostname expected from server certificate, useful in testing")
+
+	// Flags overlapping with config file options
+	overrides := &worker.ConfigOverrideArgs{
+		LabelWhiteList: &utils.RegexpVal{},
+		Sources:        &utils.StringSliceVal{},
 	}
-	if v := arguments["--sources"]; v != nil {
-		fmt.Println(v)
-		s := strings.Split(v.(string), ",")
-		args.Sources = &s
-	}
-	return args, nil
+	overrides.NoPublish = flagset.Bool("no-publish", false,
+		"Do not publish discovered features, disable connection to nfd-master.")
+	flagset.Var(overrides.LabelWhiteList, "label-whitelist",
+		"Regular expression to filter label names to publish to the Kubernetes API server. "+
+			"NB: the label namespace is omitted i.e. the filter is only applied to the name part after '/'. "+
+			"DEPRECATED: This parameter should be set via the config file.")
+	overrides.SleepInterval = flagset.Duration("sleep-interval", 0,
+		"Time to sleep between re-labeling. Non-positive value implies no re-labeling (i.e. infinite sleep). "+
+			"DEPRECATED: This parameter should be set via the config file")
+	flagset.Var(overrides.Sources, "sources",
+		"Comma separated list of feature sources. Special value 'all' enables all feature sources. "+
+			"DEPRECATED: This parameter should be set via the config file")
+
+	return args, overrides
 }
diff --git a/docs/advanced/developer-guide.md b/docs/advanced/developer-guide.md
index 80268bcf1..ce472f50a 100644
--- a/docs/advanced/developer-guide.md
+++ b/docs/advanced/developer-guide.md
@@ -164,43 +164,34 @@ $ docker run --rm --name=nfd-test ${NFD_CONTAINER_IMAGE} nfd-master --no-publish
 Command line flags of nfd-master:
 
 ```bash
-$ docker run --rm ${NFD_CONTAINER_IMAGE} nfd-master --help
-...
-Usage:
-  nfd-master [--prune] [--no-publish] [--label-whitelist=<pattern>] [--port=<port>]
-     [--ca-file=<path>] [--cert-file=<path>] [--key-file=<path>]
-     [--verify-node-name] [--extra-label-ns=<list>] [--resource-labels=<list>]
-     [--kubeconfig=<path>]
-  nfd-master -h | --help
-  nfd-master --version
-
-  Options:
-  -h --help                       Show this screen.
-  --version                       Output version and exit.
-  --prune                         Prune all NFD related attributes from all nodes
-                                  of the cluster and exit.
-  --kubeconfig=<path>             Kubeconfig to use [Default: ]
-  --port=<port>                   Port on which to listen for connections.
-                                  [Default: 8080]
-  --ca-file=<path>                Root certificate for verifying connections
-                                  [Default: ]
-  --cert-file=<path>              Certificate used for authenticating connections
-                                  [Default: ]
-  --key-file=<path>               Private key matching --cert-file
-                                  [Default: ]
-  --verify-node-name              Verify worker node name against CN from the TLS
-                                  certificate. Only has effect when TLS authentication
-                                  has been enabled.
-  --no-publish                    Do not publish feature labels
-  --label-whitelist=<pattern>     Regular expression to filter label names to
-                                  publish to the Kubernetes API server.
-                                  NB: the label namespace is omitted i.e. the filter
-                                  is only applied to the name part after '/'.
-                                  [Default: ]
-  --extra-label-ns=<list>         Comma separated list of allowed extra label namespaces
-                                  [Default: ]
-  --resource-labels=<list>        Comma separated list of labels to be exposed as extended resources.
-                                  [Default: ]
+$ docker run --rm ${NFD_CONTAINER_IMAGE} nfd-master -help
+Usage of nfd-master:
+  -ca-file string
+    	Root certificate for verifying connections
+  -cert-file string
+    	Certificate used for authenticating connections
+  -extra-label-ns value
+    	Comma separated list of allowed extra label namespaces
+  -instance string
+    	Instance name. Used to separate annotation namespaces for multiple parallel deployments.
+  -key-file string
+    	Private key matching -cert-file
+  -kubeconfig string
+    	Kubeconfig to use
+  -label-whitelist value
+    	Regular expression to filter label names to publish to the Kubernetes API server. NB: the label namespace is omitted i.e. the filter is only applied to the name part after '/'.
+  -no-publish
+    	Do not publish feature labels
+  -port int
+    	Port on which to listen for connections. (default 8080)
+  -prune
+    	Prune all NFD related attributes from all nodes of the cluaster and exit.
+  -resource-labels value
+    	Comma separated list of labels to be exposed as extended resources.
+  -verify-node-name
+    	Verify worker node name against CN from the TLS certificate. Only takes effect when TLS authentication has been enabled.
+  -version
+    	Print version and exit.
 ```
 
 ### NFD-Worker
@@ -220,52 +211,34 @@ pass the `--no-publish` flag to nfd-worker.
 Command line flags of nfd-worker:
 
 ```bash
-$ docker run --rm ${NFD_CONTAINER_IMAGE} nfd-worker --help
-...
-  Usage:
-  nfd-worker [--no-publish] [--sources=<sources>] [--label-whitelist=<pattern>]
-     [--oneshot | --sleep-interval=<seconds>] [--config=<path>]
-     [--options=<config>] [--server=<server>] [--server-name-override=<name>]
-     [--ca-file=<path>] [--cert-file=<path>] [--key-file=<path>]
-  nfd-worker -h | --help
-  nfd-worker --version
-
-  Options:
-  -h --help                   Show this screen.
-  --version                   Output version and exit.
-  --config=<path>             Config file to use.
-                              [Default: /etc/kubernetes/node-feature-discovery/nfd-worker.conf]
-  --options=<config>          Specify config options from command line. Config
-                              options are specified in the same format as in the
-                              config file (i.e. json or yaml). These options
-                              will override settings read from the config file.
-                              [Default: ]
-  --ca-file=<path>            Root certificate for verifying connections
-                              [Default: ]
-  --cert-file=<path>          Certificate used for authenticating connections
-                              [Default: ]
-  --key-file=<path>           Private key matching --cert-file
-                              [Default: ]
-  --server=<server>           NFD server address to connecto to.
-                              [Default: localhost:8080]
-  --server-name-override=<name> Name (CN) expect from server certificate, useful
-                              in testing
-                              [Default: ]
-  --sources=<sources>         Comma separated list of feature sources. Special
-                              value 'all' enables all feature sources.
-                              [Default: all]
-  --no-publish                Do not publish discovered features to the
-                              cluster-local Kubernetes API server.
-  --label-whitelist=<pattern> Regular expression to filter label names to
-                              publish to the Kubernetes API server.
-                              NB: the label namespace is omitted i.e. the filter
-                              is only applied to the name part after '/'.
-                              [Default: ]
-  --oneshot                   Label once and exit.
-  --sleep-interval=<seconds>  Time to sleep between re-labeling. Non-positive
-                              value implies no re-labeling (i.e. infinite
-                              sleep). [Default: 60s]
-
+$ docker run --rm ${NFD_CONTAINER_IMAGE} nfd-worker -help
+Usage of nfd-worker:
+  -ca-file string
+    	Root certificate for verifying connections
+  -cert-file string
+    	Certificate used for authenticating connections
+  -config string
+    	Config file to use. (default "/etc/kubernetes/node-feature-discovery/nfd-worker.conf")
+  -key-file string
+    	Private key matching -cert-file
+  -label-whitelist value
+    	Regular expression to filter label names to publish to the Kubernetes API server. NB: the label namespace is omitted i.e. the filter is only applied to the name part after '/'. DEPRECATED: This parameter should be set via the config file.
+  -no-publish
+    	Do not publish discovered features, disable connection to nfd-master.
+  -oneshot
+    	Do not publish feature labels
+  -options string
+    	Specify config options from command line. Config options are specified in the same format as in the config file (i.e. json or yaml). These options
+  -server string
+    	NFD server address to connecto to. (default "localhost:8080")
+  -server-name-override string
+    	Hostname expected from server certificate, useful in testing
+  -sleep-interval duration
+    	Time to sleep between re-labeling. Non-positive value implies no re-labeling (i.e. infinite sleep). DEPRECATED: This parameter should be set via the config file
+  -sources value
+    	Comma separated list of feature sources. Special value 'all' enables all feature sources. DEPRECATED: This parameter should be set via the config file
+  -version
+    	Print version and exit.
 ```
 
 **NOTE** Some feature sources need certain directories and/or files from the
diff --git a/go.mod b/go.mod
index 5ef0f147c..3895db012 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module sigs.k8s.io/node-feature-discovery
 go 1.15
 
 require (
-	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
 	github.com/fsnotify/fsnotify v1.4.9
 	github.com/golang/protobuf v1.4.3
 	github.com/klauspost/cpuid/v2 v2.0.2
diff --git a/pkg/nfd-master/nfd-master.go b/pkg/nfd-master/nfd-master.go
index 6a3ac39c5..f1e9ed378 100644
--- a/pkg/nfd-master/nfd-master.go
+++ b/pkg/nfd-master/nfd-master.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Kubernetes Authors.
+Copyright 2019-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.
@@ -38,6 +38,7 @@ import (
 	api "k8s.io/api/core/v1"
 	"sigs.k8s.io/node-feature-discovery/pkg/apihelper"
 	pb "sigs.k8s.io/node-feature-discovery/pkg/labeler"
+	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"sigs.k8s.io/node-feature-discovery/pkg/version"
 )
 
@@ -74,16 +75,16 @@ type Annotations map[string]string
 type Args struct {
 	CaFile         string
 	CertFile       string
-	ExtraLabelNs   map[string]struct{}
+	ExtraLabelNs   utils.StringSetVal
 	Instance       string
 	KeyFile        string
 	Kubeconfig     string
-	LabelWhiteList *regexp.Regexp
+	LabelWhiteList utils.RegexpVal
 	NoPublish      bool
 	Port           int
 	Prune          bool
 	VerifyNodeName bool
-	ResourceLabels []string
+	ResourceLabels utils.StringSetVal
 }
 
 type NfdMaster interface {
@@ -102,8 +103,8 @@ type nfdMaster struct {
 }
 
 // Create new NfdMaster server instance.
-func NewNfdMaster(args Args) (NfdMaster, error) {
-	nfd := &nfdMaster{args: args,
+func NewNfdMaster(args *Args) (NfdMaster, error) {
+	nfd := &nfdMaster{args: *args,
 		nodeName: os.Getenv("NODE_NAME"),
 		ready:    make(chan bool, 1),
 	}
@@ -111,6 +112,11 @@ func NewNfdMaster(args Args) (NfdMaster, error) {
 	if args.Instance == "" {
 		nfd.annotationNs = AnnotationNsBase
 	} else {
+		if ok, _ := regexp.MatchString(`^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$`, args.Instance); !ok {
+			return nfd, fmt.Errorf("invalid --instance %q: instance name "+
+				"must start and end with an alphanumeric character and may only contain "+
+				"alphanumerics, `-`, `_` or `.`", args.Instance)
+		}
 		nfd.annotationNs = args.Instance + "." + AnnotationNsBase
 	}
 
@@ -283,7 +289,7 @@ func (m *nfdMaster) updateMasterNode() error {
 // into extended resources. This function also handles proper namespacing of
 // labels and ERs, i.e. adds the possibly missing default namespace for labels
 // arriving through the gRPC API.
-func filterFeatureLabels(labels Labels, extraLabelNs map[string]struct{}, labelWhiteList *regexp.Regexp, extendedResourceNames []string) (Labels, ExtendedResources) {
+func filterFeatureLabels(labels Labels, extraLabelNs map[string]struct{}, labelWhiteList regexp.Regexp, extendedResourceNames map[string]struct{}) (Labels, ExtendedResources) {
 	outLabels := Labels{}
 
 	for label, value := range labels {
@@ -310,7 +316,7 @@ func filterFeatureLabels(labels Labels, extraLabelNs map[string]struct{}, labelW
 
 	// Remove labels which are intended to be extended resources
 	extendedResources := ExtendedResources{}
-	for _, extendedResourceName := range extendedResourceNames {
+	for extendedResourceName := range extendedResourceNames {
 		// Add possibly missing default ns
 		extendedResourceName = addNs(extendedResourceName, LabelNs)
 		if value, ok := outLabels[extendedResourceName]; ok {
@@ -354,7 +360,7 @@ func (m *nfdMaster) SetLabels(c context.Context, r *pb.SetLabelsRequest) (*pb.Se
 	}
 	stdoutLogger.Printf("REQUEST Node: %s NFD-version: %s Labels: %s", r.NodeName, r.NfdVersion, r.Labels)
 
-	labels, extendedResources := filterFeatureLabels(r.Labels, m.args.ExtraLabelNs, m.args.LabelWhiteList, m.args.ResourceLabels)
+	labels, extendedResources := filterFeatureLabels(r.Labels, m.args.ExtraLabelNs, m.args.LabelWhiteList.Regexp, m.args.ResourceLabels)
 
 	if !m.args.NoPublish {
 		// Advertise NFD worker version as an annotation
diff --git a/pkg/nfd-worker/nfd-worker.go b/pkg/nfd-worker/nfd-worker.go
index 454eede3d..36beb6f8d 100644
--- a/pkg/nfd-worker/nfd-worker.go
+++ b/pkg/nfd-worker/nfd-worker.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Kubernetes Authors.
+Copyright 2019-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.
@@ -34,7 +34,10 @@ import (
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"k8s.io/apimachinery/pkg/util/validation"
+	"sigs.k8s.io/yaml"
+
 	pb "sigs.k8s.io/node-feature-discovery/pkg/labeler"
+	"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"
@@ -50,7 +53,6 @@ import (
 	"sigs.k8s.io/node-feature-discovery/source/storage"
 	"sigs.k8s.io/node-feature-discovery/source/system"
 	"sigs.k8s.io/node-feature-discovery/source/usb"
-	"sigs.k8s.io/yaml"
 )
 
 var (
@@ -66,7 +68,7 @@ type NFDConfig struct {
 }
 
 type coreConfig struct {
-	LabelWhiteList regex
+	LabelWhiteList utils.RegexpVal
 	NoPublish      bool
 	Sources        []string
 	SleepInterval  duration
@@ -87,11 +89,18 @@ type Args struct {
 	Oneshot            bool
 	Server             string
 	ServerNameOverride string
-	// Deprecated options that should be set via the config file
-	LabelWhiteList *regexp.Regexp
-	NoPublish      *bool
+
+	Overrides ConfigOverrideArgs
+}
+
+// ConfigOverrideArgs are args that override config file options
+type ConfigOverrideArgs struct {
+	NoPublish *bool
+
+	// Deprecated
+	LabelWhiteList *utils.RegexpVal
 	SleepInterval  *time.Duration
-	Sources        *[]string
+	Sources        *utils.StringSliceVal
 }
 
 type NfdWorker interface {
@@ -111,18 +120,14 @@ type nfdWorker struct {
 	enabledSources []source.FeatureSource
 }
 
-type regex struct {
-	regexp.Regexp
-}
-
 type duration struct {
 	time.Duration
 }
 
 // Create new NfdWorker instance.
-func NewNfdWorker(args Args) (NfdWorker, error) {
+func NewNfdWorker(args *Args) (NfdWorker, error) {
 	nfd := &nfdWorker{
-		args:   args,
+		args:   *args,
 		config: &NFDConfig{},
 		realSources: []source.FeatureSource{
 			&cpu.Source{},
@@ -203,7 +208,7 @@ func addConfigWatch(path string) (*fsnotify.Watcher, map[string]struct{}, error)
 func newDefaultConfig() *NFDConfig {
 	return &NFDConfig{
 		Core: coreConfig{
-			LabelWhiteList: regex{*regexp.MustCompile("")},
+			LabelWhiteList: utils.RegexpVal{Regexp: *regexp.MustCompile("")},
 			SleepInterval:  duration{60 * time.Second},
 			Sources:        []string{"all"},
 		},
@@ -238,7 +243,7 @@ func (w *nfdWorker) Run() error {
 		select {
 		case <-labelTrigger:
 			// Get the set of feature labels.
-			labels := createFeatureLabels(w.enabledSources, w.config.Core.LabelWhiteList)
+			labels := createFeatureLabels(w.enabledSources, w.config.Core.LabelWhiteList.Regexp)
 
 			// Update the node with the feature labels.
 			if w.client != nil {
@@ -448,17 +453,17 @@ func (w *nfdWorker) configure(filepath string, overrides string) error {
 		return fmt.Errorf("Failed to parse --options: %s", err)
 	}
 
-	if w.args.LabelWhiteList != nil {
-		c.Core.LabelWhiteList = regex{*w.args.LabelWhiteList}
+	if w.args.Overrides.LabelWhiteList != nil {
+		c.Core.LabelWhiteList = *w.args.Overrides.LabelWhiteList
 	}
-	if w.args.NoPublish != nil {
-		c.Core.NoPublish = *w.args.NoPublish
+	if w.args.Overrides.NoPublish != nil {
+		c.Core.NoPublish = *w.args.Overrides.NoPublish
 	}
-	if w.args.SleepInterval != nil {
-		c.Core.SleepInterval = duration{*w.args.SleepInterval}
+	if w.args.Overrides.SleepInterval != nil {
+		c.Core.SleepInterval = duration{*w.args.Overrides.SleepInterval}
 	}
-	if w.args.Sources != nil {
-		c.Core.Sources = *w.args.Sources
+	if w.args.Overrides.Sources != nil {
+		c.Core.Sources = *w.args.Overrides.Sources
 	}
 
 	c.Core.sanitize()
@@ -477,7 +482,7 @@ func (w *nfdWorker) configure(filepath string, overrides string) error {
 
 // createFeatureLabels returns the set of feature labels from the enabled
 // sources and the whitelist argument.
-func createFeatureLabels(sources []source.FeatureSource, labelWhiteList regex) (labels Labels) {
+func createFeatureLabels(sources []source.FeatureSource, labelWhiteList regexp.Regexp) (labels Labels) {
 	labels = Labels{}
 
 	// Do feature discovery from all configured sources.
@@ -500,7 +505,7 @@ func createFeatureLabels(sources []source.FeatureSource, labelWhiteList regex) (
 
 // getFeatureLabels returns node labels for features discovered by the
 // supplied source.
-func getFeatureLabels(source source.FeatureSource, labelWhiteList regex) (labels Labels, err error) {
+func getFeatureLabels(source source.FeatureSource, labelWhiteList regexp.Regexp) (labels Labels, err error) {
 	defer func() {
 		if r := recover(); r != nil {
 			stderrLogger.Printf("panic occurred during discovery of source [%s]: %v", source.Name(), r)
@@ -584,25 +589,6 @@ func advertiseFeatureLabels(client pb.LabelerClient, labels Labels) error {
 	return nil
 }
 
-// UnmarshalJSON implements the Unmarshaler interface from "encoding/json"
-func (r *regex) UnmarshalJSON(data []byte) error {
-	var v interface{}
-	if err := json.Unmarshal(data, &v); err != nil {
-		return err
-	}
-	switch val := v.(type) {
-	case string:
-		if rr, err := regexp.Compile(string(val)); err != nil {
-			return err
-		} else {
-			*r = regex{*rr}
-		}
-	default:
-		return fmt.Errorf("invalid regexp %s", data)
-	}
-	return nil
-}
-
 // UnmarshalJSON implements the Unmarshaler interface from "encoding/json"
 func (d *duration) UnmarshalJSON(data []byte) error {
 	var v interface{}
diff --git a/pkg/utils/flags.go b/pkg/utils/flags.go
new file mode 100644
index 000000000..b91c70c7f
--- /dev/null
+++ b/pkg/utils/flags.go
@@ -0,0 +1,100 @@
+/*
+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 utils
+
+import (
+	"encoding/json"
+	"fmt"
+	"regexp"
+	"sort"
+	"strings"
+)
+
+// RegexpVal is a wrapper for regexp command line flags
+type RegexpVal struct {
+	regexp.Regexp
+}
+
+// Set implements the flag.Value interface
+func (a *RegexpVal) Set(val string) error {
+	r, err := regexp.Compile(val)
+	a.Regexp = *r
+	return err
+}
+
+// UnmarshalJSON implements the Unmarshaler interface from "encoding/json"
+func (a *RegexpVal) UnmarshalJSON(data []byte) error {
+	var v interface{}
+	if err := json.Unmarshal(data, &v); err != nil {
+		return err
+	}
+	switch val := v.(type) {
+	case string:
+		if r, err := regexp.Compile(string(val)); err != nil {
+			return err
+		} else {
+			*a = RegexpVal{*r}
+		}
+	default:
+		return fmt.Errorf("invalid regexp %s", data)
+	}
+	return nil
+}
+
+// StringSetVal is a Value encapsulating a set of comma-separated strings
+type StringSetVal map[string]struct{}
+
+// Set implements the flag.Value interface
+func (a *StringSetVal) Set(val string) error {
+	m := map[string]struct{}{}
+	for _, n := range strings.Split(val, ",") {
+		m[n] = struct{}{}
+	}
+	*a = m
+	return nil
+}
+
+// String implements the flag.Value interface
+func (a *StringSetVal) String() string {
+	if *a == nil {
+		return ""
+	}
+
+	vals := make([]string, len(*a), 0)
+	for val := range *a {
+		vals = append(vals, val)
+	}
+	sort.Strings(vals)
+	return strings.Join(vals, ",")
+}
+
+// StringSliceVal is a Value encapsulating a slice of comma-separated strings
+type StringSliceVal []string
+
+// Set implements the regexp.Value interface
+func (a *StringSliceVal) Set(val string) error {
+	*a = strings.Split(val, ",")
+	return nil
+}
+
+// String implements the regexp.Value interface
+func (a *StringSliceVal) String() string {
+	if *a == nil {
+		return ""
+	}
+	return strings.Join(*a, ",")
+}


Test patch:
diff --git a/cmd/nfd-master/main_test.go b/cmd/nfd-master/main_test.go
deleted file mode 100644
index 57ab6bf4f..000000000
--- a/cmd/nfd-master/main_test.go
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
-Copyright 2019 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 main
-
-import (
-	"testing"
-
-	. "github.com/smartystreets/goconvey/convey"
-)
-
-func TestArgsParse(t *testing.T) {
-	Convey("When parsing command line arguments", t, func() {
-		Convey("When --no-publish and --oneshot flags are passed", func() {
-			args, err := argsParse([]string{"--no-publish"})
-			Convey("noPublish is set and args.sources is set to the default value", func() {
-				So(args.NoPublish, ShouldBeTrue)
-				So(len(args.LabelWhiteList.String()), ShouldEqual, 0)
-				So(err, ShouldBeNil)
-			})
-		})
-
-		Convey("When valid args are specified", func() {
-			args, err := argsParse([]string{"--label-whitelist=.*rdt.*", "--port=1234", "--cert-file=crt", "--key-file=key", "--ca-file=ca"})
-			Convey("Argument parsing should succeed and args set to correct values", func() {
-				So(args.NoPublish, ShouldBeFalse)
-				So(args.Port, ShouldEqual, 1234)
-				So(args.CertFile, ShouldEqual, "crt")
-				So(args.KeyFile, ShouldEqual, "key")
-				So(args.CaFile, ShouldEqual, "ca")
-				So(args.LabelWhiteList.String(), ShouldResemble, ".*rdt.*")
-				So(err, ShouldBeNil)
-			})
-		})
-		Convey("When invalid --port is defined", func() {
-			_, err := argsParse([]string{"--port=123a"})
-			Convey("argsParse should fail", func() {
-				So(err, ShouldNotBeNil)
-			})
-		})
-	})
-}
diff --git a/cmd/nfd-worker/main_test.go b/cmd/nfd-worker/main_test.go
index d6b222bc5..9481f8116 100644
--- a/cmd/nfd-worker/main_test.go
+++ b/cmd/nfd-worker/main_test.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Kubernetes Authors.
+Copyright 2019-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.
@@ -17,62 +17,44 @@ limitations under the License.
 package main
 
 import (
+	"flag"
 	"testing"
 	"time"
 
 	. "github.com/smartystreets/goconvey/convey"
+
+	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 )
 
-func TestArgsParse(t *testing.T) {
+func TestParseArgs(t *testing.T) {
 	Convey("When parsing command line arguments", t, func() {
-		Convey("When --no-publish and --oneshot flags are passed", func() {
-			args, err := argsParse([]string{"--no-publish", "--oneshot"})
+		flags := flag.NewFlagSet(ProgramName, flag.ExitOnError)
+
+		Convey("When no override args are specified", func() {
+			args := parseArgs(flags, "--oneshot")
 
-			Convey("noPublish is set and args.sources is set to the default value", func() {
-				So(args.SleepInterval, ShouldEqual, nil)
-				So(*args.NoPublish, ShouldBeTrue)
+			Convey("overrides should be nil", func() {
 				So(args.Oneshot, ShouldBeTrue)
-				So(args.Sources, ShouldBeNil)
-				So(args.LabelWhiteList, ShouldBeNil)
-				So(err, ShouldBeNil)
+				So(args.Overrides.NoPublish, ShouldBeNil)
+				So(args.Overrides.LabelWhiteList, ShouldBeNil)
+				So(args.Overrides.SleepInterval, ShouldBeNil)
+				So(args.Overrides.Sources, ShouldBeNil)
 			})
 		})
 
-		Convey("When --sources flag is passed and set to some values, --sleep-inteval is specified", func() {
-			args, err := argsParse([]string{"--sources=fake1,fake2,fake3", "--sleep-interval=30s"})
+		Convey("When all override args are specified", func() {
+			args := parseArgs(flags,
+				"--no-publish",
+				"-label-whitelist=.*rdt.*",
+				"-sources=fake1,fake2,fake3",
+				"-sleep-interval=30s")
 
 			Convey("args.sources is set to appropriate values", func() {
-				So(*args.SleepInterval, ShouldEqual, 30*time.Second)
-				So(args.NoPublish, ShouldBeNil)
 				So(args.Oneshot, ShouldBeFalse)
-				So(*args.Sources, ShouldResemble, []string{"fake1", "fake2", "fake3"})
-				So(args.LabelWhiteList, ShouldBeNil)
-				So(err, ShouldBeNil)
-			})
-		})
-
-		Convey("When --label-whitelist flag is passed and set to some value", func() {
-			args, err := argsParse([]string{"--label-whitelist=.*rdt.*"})
-
-			Convey("args.labelWhiteList is set to appropriate value and args.sources is set to default value", func() {
-				So(args.NoPublish, ShouldBeNil)
-				So(args.Sources, ShouldBeNil)
-				So(args.LabelWhiteList.String(), ShouldResemble, ".*rdt.*")
-				So(err, ShouldBeNil)
-			})
-		})
-
-		Convey("When valid args are specified", func() {
-			args, err := argsParse([]string{"--no-publish", "--sources=fake1,fake2,fake3", "--ca-file=ca", "--cert-file=crt", "--key-file=key"})
-
-			Convey("--no-publish is set and args.sources is set to appropriate values", func() {
-				So(*args.NoPublish, ShouldBeTrue)
-				So(args.CaFile, ShouldEqual, "ca")
-				So(args.CertFile, ShouldEqual, "crt")
-				So(args.KeyFile, ShouldEqual, "key")
-				So(*args.Sources, ShouldResemble, []string{"fake1", "fake2", "fake3"})
-				So(args.LabelWhiteList, ShouldBeNil)
-				So(err, ShouldBeNil)
+				So(*args.Overrides.NoPublish, ShouldBeTrue)
+				So(*args.Overrides.SleepInterval, ShouldEqual, 30*time.Second)
+				So(*args.Overrides.Sources, ShouldResemble, utils.StringSliceVal{"fake1", "fake2", "fake3"})
+				So(args.Overrides.LabelWhiteList.Regexp.String(), ShouldResemble, ".*rdt.*")
 			})
 		})
 	})
diff --git a/pkg/nfd-master/nfd-master-internal_test.go b/pkg/nfd-master/nfd-master-internal_test.go
index 45a1d239e..3accdaaa6 100644
--- a/pkg/nfd-master/nfd-master-internal_test.go
+++ b/pkg/nfd-master/nfd-master-internal_test.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Kubernetes Authors.
+Copyright 2019-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.
@@ -34,6 +34,7 @@ import (
 	k8sclient "k8s.io/client-go/kubernetes"
 	"sigs.k8s.io/node-feature-discovery/pkg/apihelper"
 	"sigs.k8s.io/node-feature-discovery/pkg/labeler"
+	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"sigs.k8s.io/node-feature-discovery/pkg/version"
 	"sigs.k8s.io/yaml"
 )
@@ -55,7 +56,7 @@ func newMockMaster(apihelper apihelper.APIHelpers) *nfdMaster {
 	return &nfdMaster{
 		nodeName:     mockNodeName,
 		annotationNs: AnnotationNsBase,
-		args:         Args{LabelWhiteList: regexp.MustCompile("")},
+		args:         Args{LabelWhiteList: utils.RegexpVal{Regexp: *regexp.MustCompile("")}},
 		apihelper:    apihelper,
 	}
 }
@@ -339,7 +340,7 @@ func TestSetLabels(t *testing.T) {
 				apihelper.NewJsonPatch("add", "/metadata/labels", LabelNs+"/feature-2", mockLabels["feature-2"]),
 			}
 
-			mockMaster.args.LabelWhiteList = regexp.MustCompile("^f.*2$")
+			mockMaster.args.LabelWhiteList.Regexp = *regexp.MustCompile("^f.*2$")
 			mockHelper.On("GetClient").Return(mockClient, nil)
 			mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil)
 			mockHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedPatches))).Return(nil)
@@ -390,7 +391,7 @@ func TestSetLabels(t *testing.T) {
 				apihelper.NewJsonPatch("add", "/status/capacity", LabelNs+"/feature-3", mockLabels["feature-3"]),
 			}
 
-			mockMaster.args.ResourceLabels = []string{"feature-3", "feature-1"}
+			mockMaster.args.ResourceLabels = map[string]struct{}{"feature-3": struct{}{}, "feature-1": struct{}{}}
 			mockHelper.On("GetClient").Return(mockClient, nil)
 			mockHelper.On("GetNode", mockClient, workerName).Return(mockNode, nil)
 			mockHelper.On("PatchNode", mockClient, mockNodeName, mock.MatchedBy(jsonPatchMatcher(expectedPatches))).Return(nil)
diff --git a/pkg/nfd-master/nfd-master_test.go b/pkg/nfd-master/nfd-master_test.go
index d3d5e085c..43b0b60a6 100644
--- a/pkg/nfd-master/nfd-master_test.go
+++ b/pkg/nfd-master/nfd-master_test.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Kubernetes Authors.
+Copyright 2019-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.
@@ -26,9 +26,9 @@ import (
 func TestNewNfdMaster(t *testing.T) {
 	Convey("When initializing new NfdMaster instance", t, func() {
 		Convey("When one of --cert-file, --key-file or --ca-file is missing", func() {
-			_, err := m.NewNfdMaster(m.Args{CertFile: "crt", KeyFile: "key"})
-			_, err2 := m.NewNfdMaster(m.Args{KeyFile: "key", CaFile: "ca"})
-			_, err3 := m.NewNfdMaster(m.Args{CertFile: "crt", CaFile: "ca"})
+			_, err := m.NewNfdMaster(&m.Args{CertFile: "crt", KeyFile: "key"})
+			_, err2 := m.NewNfdMaster(&m.Args{KeyFile: "key", CaFile: "ca"})
+			_, err3 := m.NewNfdMaster(&m.Args{CertFile: "crt", CaFile: "ca"})
 			Convey("An error should be returned", func() {
 				So(err, ShouldNotBeNil)
 				So(err2, ShouldNotBeNil)
diff --git a/pkg/nfd-worker/nfd-worker-internal_test.go b/pkg/nfd-worker/nfd-worker-internal_test.go
index eaeffa122..8b2943ad5 100644
--- a/pkg/nfd-worker/nfd-worker-internal_test.go
+++ b/pkg/nfd-worker/nfd-worker-internal_test.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Kubernetes Authors.
+Copyright 2019-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.
@@ -26,12 +26,12 @@ import (
 	"testing"
 	"time"
 
-	//"github.com/onsi/gomega"
 	. "github.com/smartystreets/goconvey/convey"
 	"github.com/stretchr/testify/mock"
 	"github.com/vektra/errors"
 
 	"sigs.k8s.io/node-feature-discovery/pkg/labeler"
+	"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"
@@ -53,13 +53,13 @@ func TestDiscoveryWithMockSources(t *testing.T) {
 
 		fakeFeatureSource := source.FeatureSource(mockFeatureSource)
 
-		labelWhiteList := regex{*regexp.MustCompile("^test")}
+		labelWhiteList := utils.RegexpVal{Regexp: *regexp.MustCompile("^test")}
 
 		Convey("When I successfully get the labels from the mock source", func() {
 			mockFeatureSource.On("Name").Return(fakeFeatureSourceName)
 			mockFeatureSource.On("Discover").Return(fakeFeatures, nil)
 
-			returnedLabels, err := getFeatureLabels(fakeFeatureSource, labelWhiteList)
+			returnedLabels, err := getFeatureLabels(fakeFeatureSource, labelWhiteList.Regexp)
 			Convey("Proper label is returned", func() {
 				So(returnedLabels, ShouldResemble, fakeFeatureLabels)
 			})
@@ -72,7 +72,7 @@ func TestDiscoveryWithMockSources(t *testing.T) {
 			expectedError := errors.New("fake error")
 			mockFeatureSource.On("Discover").Return(nil, expectedError)
 
-			returnedLabels, err := getFeatureLabels(fakeFeatureSource, labelWhiteList)
+			returnedLabels, err := getFeatureLabels(fakeFeatureSource, labelWhiteList.Regexp)
 			Convey("No label is returned", func() {
 				So(returnedLabels, ShouldBeNil)
 			})
@@ -109,7 +109,7 @@ func (w *nfdWorker) getSource(name string) source.FeatureSource {
 
 func TestConfigParse(t *testing.T) {
 	Convey("When parsing configuration", t, func() {
-		w, err := NewNfdWorker(Args{})
+		w, err := NewNfdWorker(&Args{})
 		So(err, ShouldBeNil)
 		worker := w.(*nfdWorker)
 		overrides := `{"core": {"sources": ["fake"],"noPublish": true},"sources": {"cpu": {"cpuid": {"attributeBlacklist": ["foo","bar"]}}}}`
@@ -123,7 +123,7 @@ func TestConfigParse(t *testing.T) {
 			})
 		})
 		Convey("and a non-accessible file, but core cmdline flags and some overrides are specified", func() {
-			worker.args = Args{Sources: &[]string{"cpu", "kernel", "pci"}}
+			worker.args = Args{Overrides: ConfigOverrideArgs{Sources: &utils.StringSliceVal{"cpu", "kernel", "pci"}}}
 			So(worker.configure("non-existing-file", overrides), ShouldBeNil)
 
 			Convey("core cmdline flags should be in effect instead overrides", func() {
@@ -157,7 +157,7 @@ sources:
 		So(err, ShouldBeNil)
 
 		Convey("and a proper config file is specified", func() {
-			worker.args = Args{Sources: &[]string{"cpu", "kernel", "pci"}}
+			worker.args = Args{Overrides: ConfigOverrideArgs{Sources: &utils.StringSliceVal{"cpu", "kernel", "pci"}}}
 			So(worker.configure(f.Name(), ""), ShouldBeNil)
 
 			Convey("specified configuration should take effect", func() {
@@ -178,7 +178,7 @@ sources:
 
 		Convey("and a proper config file and overrides are given", func() {
 			sleepIntervalArg := 15 * time.Second
-			worker.args = Args{SleepInterval: &sleepIntervalArg}
+			worker.args = Args{Overrides: ConfigOverrideArgs{SleepInterval: &sleepIntervalArg}}
 			overrides := `{"core": {"sources": ["fake"],"noPublish": true},"sources": {"pci": {"deviceClassWhitelist": ["03"]}}}`
 			So(worker.configure(f.Name(), overrides), ShouldBeNil)
 
@@ -229,11 +229,11 @@ core:
 `)
 
 		noPublish := true
-		w, err := NewNfdWorker(Args{
-			ConfigFile:    configFile,
-			Sources:       &[]string{"fake"},
-			NoPublish:     &noPublish,
-			SleepInterval: new(time.Duration),
+		w, err := NewNfdWorker(&Args{
+			ConfigFile: configFile,
+			Overrides: ConfigOverrideArgs{
+				Sources:   &utils.StringSliceVal{"fake"},
+				NoPublish: &noPublish},
 		})
 		So(err, ShouldBeNil)
 		worker := w.(*nfdWorker)
@@ -309,10 +309,10 @@ func withTimeout(actual interface{}, expected ...interface{}) string {
 func TestNewNfdWorker(t *testing.T) {
 	Convey("When creating new NfdWorker instance", t, func() {
 
-		emptyRegexp := regex{*regexp.MustCompile("")}
+		emptyRegexp := utils.RegexpVal{Regexp: *regexp.MustCompile("")}
 
 		Convey("without any args specified", func() {
-			args := Args{}
+			args := &Args{}
 			w, err := NewNfdWorker(args)
 			Convey("no error should be returned", func() {
 				So(err, ShouldBeNil)
@@ -326,7 +326,7 @@ func TestNewNfdWorker(t *testing.T) {
 		})
 
 		Convey("with non-empty Sources arg specified", func() {
-			args := Args{Sources: &[]string{"fake"}}
+			args := &Args{Overrides: ConfigOverrideArgs{Sources: &utils.StringSliceVal{"fake"}}}
 			w, err := NewNfdWorker(args)
 			Convey("no error should be returned", func() {
 				So(err, ShouldBeNil)
@@ -341,14 +341,14 @@ func TestNewNfdWorker(t *testing.T) {
 		})
 
 		Convey("with valid LabelWhiteListStr arg specified", func() {
-			args := Args{LabelWhiteList: regexp.MustCompile(".*rdt.*")}
+			args := &Args{Overrides: ConfigOverrideArgs{LabelWhiteList: &utils.RegexpVal{Regexp: *regexp.MustCompile(".*rdt.*")}}}
 			w, err := NewNfdWorker(args)
 			Convey("no error should be returned", func() {
 				So(err, ShouldBeNil)
 			})
 			worker := w.(*nfdWorker)
 			So(worker.configure("", ""), ShouldBeNil)
-			expectRegexp := regex{*regexp.MustCompile(".*rdt.*")}
+			expectRegexp := utils.RegexpVal{Regexp: *regexp.MustCompile(".*rdt.*")}
 			Convey("proper labelWhiteList regexp should be produced", func() {
 				So(worker.config.Core.LabelWhiteList, ShouldResemble, expectRegexp)
 			})
@@ -359,12 +359,12 @@ func TestNewNfdWorker(t *testing.T) {
 func TestCreateFeatureLabels(t *testing.T) {
 	Convey("When creating feature labels from the configured sources", t, func() {
 		Convey("When fake feature source is configured", func() {
-			emptyLabelWL := regex{*regexp.MustCompile("")}
+			emptyLabelWL := regexp.MustCompile("")
 			fakeFeatureSource := source.FeatureSource(new(fake.Source))
 			fakeFeatureSource.SetConfig(fakeFeatureSource.NewConfig())
 			sources := []source.FeatureSource{}
 			sources = append(sources, fakeFeatureSource)
-			labels := createFeatureLabels(sources, emptyLabelWL)
+			labels := createFeatureLabels(sources, *emptyLabelWL)
 
 			Convey("Proper fake labels are returned", func() {
 				So(len(labels), ShouldEqual, 3)
@@ -374,11 +374,10 @@ func TestCreateFeatureLabels(t *testing.T) {
 			})
 		})
 		Convey("When fake feature source is configured with a whitelist that doesn't match", func() {
-			labelWL := regex{*regexp.MustCompile(".*rdt.*")}
 			fakeFeatureSource := source.FeatureSource(new(fake.Source))
 			sources := []source.FeatureSource{}
 			sources = append(sources, fakeFeatureSource)
-			labels := createFeatureLabels(sources, labelWL)
+			labels := createFeatureLabels(sources, *regexp.MustCompile(".*rdt.*"))
 
 			Convey("fake labels are not returned", func() {
 				So(len(labels), ShouldEqual, 0)
@@ -394,7 +393,7 @@ func TestGetFeatureLabels(t *testing.T) {
 	Convey("When I get feature labels and panic occurs during discovery of a feature source", t, func() {
 		fakePanicFeatureSource := source.FeatureSource(new(panicfake.Source))
 
-		returnedLabels, err := getFeatureLabels(fakePanicFeatureSource, regex{*regexp.MustCompile("")})
+		returnedLabels, err := getFeatureLabels(fakePanicFeatureSource, *regexp.MustCompile(""))
 		Convey("No label is returned", func() {
 			So(len(returnedLabels), ShouldEqual, 0)
 		})
diff --git a/pkg/nfd-worker/nfd-worker_test.go b/pkg/nfd-worker/nfd-worker_test.go
index 2707a98bb..2f7b4ea9b 100644
--- a/pkg/nfd-worker/nfd-worker_test.go
+++ b/pkg/nfd-worker/nfd-worker_test.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 The Kubernetes Authors.
+Copyright 2019-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.
@@ -24,22 +24,24 @@ import (
 	"time"
 
 	. "github.com/smartystreets/goconvey/convey"
-	nfdmaster "sigs.k8s.io/node-feature-discovery/pkg/nfd-master"
-	w "sigs.k8s.io/node-feature-discovery/pkg/nfd-worker"
+
+	master "sigs.k8s.io/node-feature-discovery/pkg/nfd-master"
+	worker "sigs.k8s.io/node-feature-discovery/pkg/nfd-worker"
+	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 	"sigs.k8s.io/node-feature-discovery/test/data"
 )
 
 type testContext struct {
-	master nfdmaster.NfdMaster
+	master master.NfdMaster
 	errs   chan error
 }
 
-func setupTest(args nfdmaster.Args) testContext {
+func setupTest(args *master.Args) testContext {
 	// Fixed port and no-publish, for convenience
 	args.NoPublish = true
 	args.Port = 8192
-	args.LabelWhiteList = regexp.MustCompile("")
-	m, err := nfdmaster.NewNfdMaster(args)
+	args.LabelWhiteList.Regexp = *regexp.MustCompile("")
+	m, err := master.NewNfdMaster(args)
 	if err != nil {
 		fmt.Printf("Test setup failed: %v\n", err)
 		os.Exit(1)
@@ -73,9 +75,9 @@ func teardownTest(ctx testContext) {
 func TestNewNfdWorker(t *testing.T) {
 	Convey("When initializing new NfdWorker instance", t, func() {
 		Convey("When one of --cert-file, --key-file or --ca-file is missing", func() {
-			_, err := w.NewNfdWorker(w.Args{CertFile: "crt", KeyFile: "key"})
-			_, err2 := w.NewNfdWorker(w.Args{KeyFile: "key", CaFile: "ca"})
-			_, err3 := w.NewNfdWorker(w.Args{CertFile: "crt", CaFile: "ca"})
+			_, err := worker.NewNfdWorker(&worker.Args{CertFile: "crt", KeyFile: "key"})
+			_, err2 := worker.NewNfdWorker(&worker.Args{KeyFile: "key", CaFile: "ca"})
+			_, err3 := worker.NewNfdWorker(&worker.Args{CertFile: "crt", CaFile: "ca"})
 			Convey("An error should be returned", func() {
 				So(err, ShouldNotBeNil)
 				So(err2, ShouldNotBeNil)
@@ -86,12 +88,17 @@ func TestNewNfdWorker(t *testing.T) {
 }
 
 func TestRun(t *testing.T) {
-	ctx := setupTest(nfdmaster.Args{})
+	ctx := setupTest(&master.Args{})
 	defer teardownTest(ctx)
 	Convey("When running nfd-worker against nfd-master", t, func() {
 		Convey("When publishing features from fake source", func() {
-			worker, _ := w.NewNfdWorker(w.Args{Oneshot: true, Sources: &[]string{"fake"}, Server: "localhost:8192"})
-			err := worker.Run()
+			args := &worker.Args{
+				Oneshot:   true,
+				Server:    "localhost:8192",
+				Overrides: worker.ConfigOverrideArgs{Sources: &utils.StringSliceVal{"fake"}},
+			}
+			fooasdf, _ := worker.NewNfdWorker(args)
+			err := fooasdf.Run()
 			Convey("No error should be returned", func() {
 				So(err, ShouldBeNil)
 			})
@@ -100,7 +107,7 @@ func TestRun(t *testing.T) {
 }
 
 func TestRunTls(t *testing.T) {
-	masterArgs := nfdmaster.Args{
+	masterArgs := &master.Args{
 		CaFile:         data.FilePath("ca.crt"),
 		CertFile:       data.FilePath("nfd-test-master.crt"),
 		KeyFile:        data.FilePath("nfd-test-master.key"),
@@ -110,16 +117,17 @@ func TestRunTls(t *testing.T) {
 	defer teardownTest(ctx)
 	Convey("When running nfd-worker against nfd-master with mutual TLS auth enabled", t, func() {
 		Convey("When publishing features from fake source", func() {
-			workerArgs := w.Args{
+			workerArgs := worker.Args{
 				CaFile:             data.FilePath("ca.crt"),
 				CertFile:           data.FilePath("nfd-test-worker.crt"),
 				KeyFile:            data.FilePath("nfd-test-worker.key"),
 				Oneshot:            true,
-				Sources:            &[]string{"fake"},
 				Server:             "localhost:8192",
-				ServerNameOverride: "nfd-test-master"}
-			worker, _ := w.NewNfdWorker(workerArgs)
-			err := worker.Run()
+				ServerNameOverride: "nfd-test-master",
+				Overrides:          worker.ConfigOverrideArgs{Sources: &utils.StringSliceVal{"fake"}},
+			}
+			w, _ := worker.NewNfdWorker(&workerArgs)
+			err := w.Run()
 			Convey("No error should be returned", func() {
 				So(err, ShouldBeNil)
 			})