Repository: kubernetes-sigs/node-feature-discovery
Pull request #429 (2021-02-03 11:10:10)

Added nodename rule to custom source
# What
With this PR we add a new `nodename` rule for the `custom` source for the NFD worker, which will apply features based on  nodes' hostnames.

For making more dynamic configuration by potentially multiple parties easier, `custom` source rules can also be read from the `/etc/kubernetes/node-feature-discovery/custom.d` directory now (probabaly mostly populated by mounted ConfigMaps).

Also an optional `value` field was added, which allows overruling the default `true` value for matching features if set.

# Details

## Example
Consider this config:
```
- name: "feature.by.nodename"
  # default value is true
  matchOn:
    - nodename:
      - worker-0
      - worker-1
- name: "another.features.by.nodename.regex"
  value: customValue
  matchOn:
    - nodename:
      - wor.*-0
```

The config will apply the `feature.by.nodename=true` feature to the `worker-0` and `worker-1` nodes, and the `another.features.by.nodename.regex=customValue` feature to all nodes matching the `wor.*-0` regex.

## Implementation
- the mounting of additional ConfigMaps into the NFD worker pod is a bit cumbersome, but can be automated with the NFD operator in a follow up (e.g. mount all ConfigMaps in the NFD namespace which match a certain name pattern).

# Usecase
A customer with many nodes is able to set the nodename according the node's purpose during node deployment, but not to add the needed labels for workload scheduling. The nodename is the only metadata available for determining the node's purpose.
With this new source the customer can put the desired nodename - label mapping(s) into a ConfigMap, and let NFD do the work of putting the labels on the nodes.

/cc @zvonkok @ArangoGutierrez @marquiz 


k8s-ci-robot: Welcome @slintes! <br><br>It looks like this is your first PR to <a href='https://github.com/kubernetes-sigs/node-feature-discovery'>kubernetes-sigs/node-feature-discovery</a> 🎉. Please refer to our [pull request process documentation](https://git.k8s.io/community/contributors/guide/pull-requests.md) to help your PR have a smooth ride to approval. <br><br>You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. [Here is the bot commands documentation](https://go.k8s.io/bot-commands). <br><br>You can also check if kubernetes-sigs/node-feature-discovery has [its own contribution guidelines](https://github.com/kubernetes-sigs/node-feature-discovery/tree/master/CONTRIBUTING.md). <br><br>You may want to refer to our [testing guide](https://git.k8s.io/community/contributors/devel/sig-testing/testing.md) if you run into trouble with your tests not passing. <br><br>If you are having difficulty getting your pull request seen, please follow the [recommended escalation practices](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#why-is-my-pull-request-not-getting-reviewed). Also, for tips and tricks in the contribution process you may want to read the [Kubernetes contributor cheat sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md). We want to make sure your contribution gets all the attention it needs! <br><br>Thank you, and welcome to Kubernetes. :smiley:
k8s-ci-robot: Hi @slintes. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fnode-feature-discovery).

<details>

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

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

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/node-feature-discovery/pull/429#" title="Author self-approved">slintes</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **marquiz** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @marquiz` in a comment when ready.

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).

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

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

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"]} -->
ArangoGutierrez: /ok-to-test
ArangoGutierrez: /assign
k8s-ci-robot: @slintes: The following test **failed**, say `/retest` to rerun all failed tests:

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

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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: Thanks @slintes for the patchset!

I see the use case. What I immediately started to think is that would it be possible to extend the existing `custom` source, instead of adding a new one(?) Is there need for multiple configmaps or could the single nfd-worker configmap instance be used instead? If multiple configs would be required we could enable a config dir something like `/etc/kubernetes/node-feature-discovery/custom.d` (similar to `source.d` and `features.d`, currently). Currently, the limitation with the custom feature rules is that there can only be one label per rule but I think that could be extended with a `labels` field (similar to what you have in this PR). WDYT?
marquiz: > Patch is looking good, I need to run a couple of tests before digging deeper.
> At a first glance, this patch requires unit testing for the big chunks of GO code being added, could you please include at least some minor unit tests for this new features, thanks

I really want to explore extending the existing source(s) (`custom`) before going forward with this as is
k8s-ci-robot: @slintes: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
slintes: @marquiz Thanks for the feedback, and I understand it is preferable to extend an existing source.

The hostname - label matching rules are potentially very dynamic, and there might even be multiple parties which want to add or modify the rules (the exact requirements are vague unfortunately, that's why I went with a very flexible implementation).
Having to modify the main worker config for this would need some careful coordination. Having dedicated ConfigMaps for each party simplifies configuration from the user's POV a lot in this scenario IMHO.

For having multiple ConfigMaps you proposed a `/etc/kubernetes/node-feature-discovery/custom.d` dir... do I understand correctly that
- those additional ConfigMaps should be mounted into that directory
- the custom source will use them additionally to the existing config (similar to how it's implemented in the my current approach)
- but in contrast to the current impl those ConfigMaps will contain the rules in the same format like in the existing worker config?

I think multiple labels per rule are not really needed... it was just easy to implement. But I don't see an actual usecase. If you actually need multiple labels, it will be very few only, which can still be configured with multiple rules. Not worth a modification of the current config structure IMHO.

@ArangoGutierrez yes, I will add unit tests as soon as we have an agreement to the way forward. I expected the e2e test to run on the PR as well, that's why I focused on that one in the 1st iteration.
marquiz: > The hostname - label matching rules are potentially very dynamic, and there might even be multiple parties which want to add or modify the rules (the exact requirements are vague unfortunately, that's why I went with a very flexible implementation).

Yes, I understand this, and makes sense. That's why I proposed to extend the custom source to support multiple config files.

> For having multiple ConfigMaps you proposed a `/etc/kubernetes/node-feature-discovery/custom.d` dir... do I understand correctly that
> 
> * those additional ConfigMaps should be mounted into that directory

Yes, that's right.

> * the custom source will use them additionally to the existing config (similar to how it's implemented in the my current approach)

Yes, rules could be defined in the nfd-worker's main config file like we already have. In addition to that, additional rules could be specified by adding rule files under the `custom.d`.

> * but in contrast to the current impl those ConfigMaps will contain the rules in the same format like in the existing worker config?

Yes, the format would be similar. That would make it possible to define any rules there and for example combine hostname/nodename matching with other rules. E.g.
```yaml
- name: "my.host.feature"
  matchOn:
    - hostName: ["host-1", "host-2"]
    - nodeName: ["host-1", "host-2"]
- name: "my.kernel.feature"
  matchOn:
    - loadedKMod: ["kmod-1"]
- name: "my.combo.feature"
  matchOn:
    - hostName: ["host-1", "host-2"]
    - pciId:
        vendor: ["8086"]
        device: ["1234"]
```
I think the `hostName`/`nodeName` could be a treated as a regexp to make matching super flexible.

> I think multiple labels per rule are not really needed... it was just easy to implement.

OK, even better. And, I think that could be added later, then, if there's a need for that. We might want to add a `value` field for the custom config, though (IIRC, we don't we have that at the moment). All labels just get value `true`.
slintes: > I think the hostName/nodeName could be a treated as a regexp to make matching super flexible.

Yes, that's a requirement actually :)

> We might want to add a value field ... All labels just get value true

oh, good point! Didn't notice that yet.
k8s-ci-robot: @slintes: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-build-image | fca392adee1ded02ec35f1194bfd33763426e150 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build-image/1359511513726980096) | `/test pull-node-feature-discovery-build-image`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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 -->
k8s-ci-robot: @slintes: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-build-image | fca392adee1ded02ec35f1194bfd33763426e150 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build-image/1359511513726980096) | `/test pull-node-feature-discovery-build-image`
pull-node-feature-discovery-verify | fca392adee1ded02ec35f1194bfd33763426e150 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-verify/1359511513617928192) | `/test pull-node-feature-discovery-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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 -->
k8s-ci-robot: @slintes: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-build-image | fca392adee1ded02ec35f1194bfd33763426e150 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build-image/1359511513726980096) | `/test pull-node-feature-discovery-build-image`
pull-node-feature-discovery-verify | fca392adee1ded02ec35f1194bfd33763426e150 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-verify/1359511513617928192) | `/test pull-node-feature-discovery-verify`
pull-node-feature-discovery-build | fca392adee1ded02ec35f1194bfd33763426e150 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build/1359511513676648448) | `/test pull-node-feature-discovery-build`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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 -->
k8s-ci-robot: @slintes: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-build-image | fca392adee1ded02ec35f1194bfd33763426e150 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build-image/1359511513726980096) | `/test pull-node-feature-discovery-build-image`
pull-node-feature-discovery-verify | fca392adee1ded02ec35f1194bfd33763426e150 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-verify/1359511513617928192) | `/test pull-node-feature-discovery-verify`
pull-node-feature-discovery-build | fca392adee1ded02ec35f1194bfd33763426e150 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build/1359511513676648448) | `/test pull-node-feature-discovery-build`
pull-node-feature-discovery-build-gh-pages | fca392adee1ded02ec35f1194bfd33763426e150 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build-gh-pages/1359511513781506048) | `/test pull-node-feature-discovery-build-gh-pages`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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 -->
slintes: - refactored to custom source
- added possibility to set an optional label value, defaults to the old `true` value
- rebased
- unit test and doc update pending
- e2e test succeeds in a local test
slintes: forgot this:

- added possibility to read more custom source configs from additional ConfigMaps
- updated PR description to reflect the changes

:)

marquiz: Really cool, thanks @slintes ! I'll take a detailed look as soon as I have time. Trying to do that on Friday but I'm afraid tomorrow is lost to other things
k8s-ci-robot: @slintes: PR needs rebase.

<details>

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

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

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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 -->
k8s-ci-robot: @slintes: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-verify | ce761d3ecd4278ffbb6028afecc01c4618a7d3be | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-verify/1360235095633432576) | `/test pull-node-feature-discovery-verify`
pull-node-feature-discovery-build-image | ce761d3ecd4278ffbb6028afecc01c4618a7d3be | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build-image/1360235095708930048) | `/test pull-node-feature-discovery-build-image`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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 -->
k8s-ci-robot: @slintes: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-verify | ce761d3ecd4278ffbb6028afecc01c4618a7d3be | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-verify/1360235095633432576) | `/test pull-node-feature-discovery-verify`
pull-node-feature-discovery-build-image | ce761d3ecd4278ffbb6028afecc01c4618a7d3be | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build-image/1360235095708930048) | `/test pull-node-feature-discovery-build-image`
pull-node-feature-discovery-build-gh-pages | ce761d3ecd4278ffbb6028afecc01c4618a7d3be | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build-gh-pages/1360235095742484480) | `/test pull-node-feature-discovery-build-gh-pages`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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 -->
k8s-ci-robot: @slintes: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-verify | ce761d3ecd4278ffbb6028afecc01c4618a7d3be | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-verify/1360235095633432576) | `/test pull-node-feature-discovery-verify`
pull-node-feature-discovery-build-image | ce761d3ecd4278ffbb6028afecc01c4618a7d3be | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build-image/1360235095708930048) | `/test pull-node-feature-discovery-build-image`
pull-node-feature-discovery-build-gh-pages | ce761d3ecd4278ffbb6028afecc01c4618a7d3be | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build-gh-pages/1360235095742484480) | `/test pull-node-feature-discovery-build-gh-pages`
pull-node-feature-discovery-build | ce761d3ecd4278ffbb6028afecc01c4618a7d3be | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/429/pull-node-feature-discovery-build/1360235095671181312) | `/test pull-node-feature-discovery-build`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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: Thanks for the reviews @adrianchiris and @ArangoGutierrez ! I need to call it a day, but, I'll take a look tomorrow
slintes: Thanks for the reviews and feedback all :+1: 
Most things are addressed:
- Renamed hostname -> nodename at all places
- Use more generic name for the additional configs (ConfigMap -> Directory)
- Improved and less logging (I commented some debug logs for reducing amount of logs, can be re-enabled when switching to a logging framework with configurable verbosity... but that's out of scope of this PR :slightly_smiling_face: )
- and some smaller things

About the manifests: should I add the ConfigMap and its mounts to all manifests? I'd prefer to mention the daemonset in documentation as example instead of copying more yaml snippets around :thinking:  

Talking about doc: that's the next item on my list...
k8s-ci-robot: @slintes: The following test **failed**, say `/retest` to rerun all failed tests:

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

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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 -->
slintes: Updated PR title and description to reflect recent changes.
And updated doc :slightly_smiling_face: 
ArangoGutierrez: /label needs-rebase
k8s-ci-robot: @ArangoGutierrez: The label(s) `/label needs-rebase` cannot be applied. These labels are supported: `api-review, community/discussion, community/maintenance, community/question, cuj/build-train-deploy, cuj/multi-user, platform/aws, platform/azure, platform/gcp, platform/minikube, platform/other, tide/merge-method-merge, tide/merge-method-rebase, tide/merge-method-squash`

<details>

In response to [this](https://github.com/kubernetes-sigs/node-feature-discovery/pull/429#issuecomment-781569312):

>/label needs-rebase


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
ArangoGutierrez: meh, ok @slintes a simple rebase will fix the CI after your PR update 
k8s-ci-robot: @slintes: The following test **failed**, say `/retest` to rerun all failed tests:

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

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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 -->
slintes: rebased, and squashed commits
ArangoGutierrez: I think during the squash event you removed the tests
slintes: Thanks for review, all remarks are addressed.

> One thing I'm a bit dubious are the now-commented-out log messages. But, I think that in the light of the much-needed (and upcoming) logging rework we can leave them there (and uncomment with the logger patch)

yes, that's the idea
adrianchiris: commit message contains some typos, can be fixed when merged or if an update is pushed to this PR
slintes: > Overall looks OK, some minor nits.

thanks! Pushed another new version :slightly_smiling_face: 
slintes: - refactored the nested loops in custom.go
- read config files from 1st level subdirs of custom.d and test multiple CMs in e2e test

For easier review commits are not squashed yet, happy to do so after review
k8s-ci-robot: @slintes: The following test **failed**, say `/retest` to rerun all failed tests:

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

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=429). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aslintes). 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: Looks good and I don't think I've anything to add. I think you could squash the commits, now
slintes: > Looks good and I don't think I've anything to add. I think you could squash the commits, now

thanks & done :)
marquiz: > Also the commit message body could be a bit more better read but I think I can live with that. Even if it's quite draft-like and I'm not a big fan of a bullet list only.

Talking about the commit message body, you already addressed *[Use the commit message body to explain the what and why of the commit](https://www.kubernetes.dev/docs/guide/pull-requests/#use-the-commit-message-body-to-explain-the-_what_-and-_why_-of-the-commit)* in the [description of this PR](https://github.com/kubernetes-sigs/node-feature-discovery/pull/429#issue-566745454). Some of that could be re-used in the commit message, I think.
slintes: fixed whitespaces and improved commit message
ArangoGutierrez: ```bash
Ran 3 of 220 Specs in 62.471 seconds
SUCCESS! -- 3 Passed | 0 Failed | 0 Pending | 217 Skipped
--- PASS: TestE2E (62.47s)
PASS
ok      sigs.k8s.io/node-feature-discovery/test/e2e     62.575s
```
ArangoGutierrez: /retest all
k8s-ci-robot: @ArangoGutierrez: The `/retest` command does not accept any targets.
The following commands are available to trigger jobs:
* `/test pull-node-feature-discovery-verify`
* `/test pull-node-feature-discovery-build`
* `/test pull-node-feature-discovery-build-image`
* `/test pull-node-feature-discovery-build-gh-pages`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/kubernetes-sigs/node-feature-discovery/pull/429#issuecomment-785128396):

>/retest all


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

Patch:
diff --git a/cmd/nfd-master/main.go b/cmd/nfd-master/main.go
index 6611d5ccd..9f66a37c3 100644
--- a/cmd/nfd-master/main.go
+++ b/cmd/nfd-master/main.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019-2021 The Kubernetes Authors.
+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.
@@ -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,29 +34,17 @@ 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 {
@@ -68,37 +56,88 @@ func main() {
 	}
 }
 
-func initFlags(flagset *flag.FlagSet) *master.Args {
-	args := &master.Args{
-		LabelWhiteList: utils.RegexpVal{Regexp: *regexp.MustCompile("")},
+// 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{}{}
 	}
+	args.ResourceLabels = strings.Split(arguments["--resource-labels"].(string), ",")
+	args.Prune = arguments["--prune"].(bool)
+	args.Kubeconfig = arguments["--kubeconfig"].(string)
 
-	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
+	return args, nil
 }
diff --git a/cmd/nfd-worker/main.go b/cmd/nfd-worker/main.go
index 3182a6a53..01f93c4f9 100644
--- a/cmd/nfd-worker/main.go
+++ b/cmd/nfd-worker/main.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019-2021 The Kubernetes Authors.
+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.
@@ -17,13 +17,14 @@ limitations under the License.
 package main
 
 import (
-	"flag"
 	"fmt"
 	"log"
-	"os"
+	"regexp"
+	"strings"
+	"time"
 
+	"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"
 )
 
@@ -33,22 +34,17 @@ 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 {
@@ -60,74 +56,104 @@ func main() {
 	}
 }
 
-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)
+// 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
+		}
 	}
-
-	// 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
+	if arguments["--no-publish"].(bool) {
+		b := true
+		args.NoPublish = &b
+	}
+	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
 		}
-	})
-
-	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{},
 	}
-	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
+	if v := arguments["--sources"]; v != nil {
+		fmt.Println(v)
+		s := strings.Split(v.(string), ",")
+		args.Sources = &s
+	}
+	return args, nil
 }
diff --git a/deployment/node-feature-discovery/templates/master.yaml b/deployment/node-feature-discovery/templates/master.yaml
index 3521bcd3b..41e6d6a60 100644
--- a/deployment/node-feature-discovery/templates/master.yaml
+++ b/deployment/node-feature-discovery/templates/master.yaml
@@ -44,10 +44,6 @@ spec:
             - "nfd-master"
           resources:
             {{- toYaml .Values.master.resources | nindent 12 }}
-          args:
-            {{- if .Values.master.instance | empty | not }}
-            - "--instance={{ .Values.master.instance }}"
-            {{- end }}
 ## Enable TLS authentication
 ## The example below assumes having the root certificate named ca.crt stored in
 ## a ConfigMap named nfd-ca-cert, and, the TLS authentication credentials stored
@@ -55,6 +51,7 @@ spec:
 ## Additional hardening can be enabled by specifying --verify-node-name in
 ## args, in which case every nfd-worker requires a individual node-specific
 ## TLS certificate.
+#          args:
 #            - "--ca-file=/etc/kubernetes/node-feature-discovery/trust/ca.crt"
 #            - "--key-file=/etc/kubernetes/node-feature-discovery/certs/tls.key"
 #            - "--cert-file=/etc/kubernetes/node-feature-discovery/certs/tls.crt"
diff --git a/deployment/node-feature-discovery/values.yaml b/deployment/node-feature-discovery/values.yaml
index ba4334df6..033a893e7 100644
--- a/deployment/node-feature-discovery/values.yaml
+++ b/deployment/node-feature-discovery/values.yaml
@@ -19,7 +19,6 @@ nameOverride: ""
 fullnameOverride: ""
 
 master:
-  instance:
   replicaCount: 1
 
   podSecurityContext: {}
@@ -162,6 +161,10 @@ worker:
     #            vendor: ["15b3"]
     #            device: ["1014", "1017"]
     #          loadedKMod : ["vendor_kmod1", "vendor_kmod2"]
+    #    - name: "feature.by.nodename"
+    #      value: customValue
+    #      matchOn:
+    #        - nodename: ["worker-0", "my-.*-node"]
 ### <NFD-WORKER-CONF-END-DO-NOT-REMOVE>
 
   podSecurityContext: {}
diff --git a/docs/advanced/developer-guide.md b/docs/advanced/developer-guide.md
index ce472f50a..80268bcf1 100644
--- a/docs/advanced/developer-guide.md
+++ b/docs/advanced/developer-guide.md
@@ -164,34 +164,43 @@ $ 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 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.
+$ 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: ]
 ```
 
 ### NFD-Worker
@@ -211,34 +220,52 @@ 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 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.
+$ 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]
+
 ```
 
 **NOTE** Some feature sources need certain directories and/or files from the
diff --git a/docs/get-started/deployment-and-usage.md b/docs/get-started/deployment-and-usage.md
index ddccd735e..e09a9dc45 100644
--- a/docs/get-started/deployment-and-usage.md
+++ b/docs/get-started/deployment-and-usage.md
@@ -171,7 +171,6 @@ We have introduced the following Chart parameters.
 ##### Master pod parameters
 
 | `master.*` | dict |  | NFD master deployment configuration |
-| `master.instance` | string |  |  Instance name. Used to separate annotation namespaces for multiple parallel deployments |
 | `master.replicaCount` | integer | 1 | Number of desired pods. This is a pointer to distinguish between explicit zero and not specified |
 | `master.podSecurityContext` | dict | {} | SecurityContext holds pod-level security attributes and common container settings |
 | `master.service.type` | string | ClusterIP | NFD master service type |
diff --git a/docs/get-started/features.md b/docs/get-started/features.md
index e6d85248b..314892416 100644
--- a/docs/get-started/features.md
+++ b/docs/get-started/features.md
@@ -134,6 +134,17 @@ examples how to set-up and manage the worker configuration.
 To aid in making Custom Features clearer, we define a general and a per rule
 nomenclature, keeping things as consistent as possible.
 
+#### Additional configuration directory
+
+Additionally to the rules defined in the nfd-worker configuration file, the
+Custom feature can read more configuration files located in the
+`/etc/kubernetes/node-feature-discovery/custom.d/` directory. This makes more
+dynamic and flexible configuration easier. This directory must be available
+inside the NFD worker container, so Volumes and VolumeMounts must be used for
+mounting e.g. ConfigMap(s). The example deployment manifests provide an example
+(commented out) for providing Custom configuration with an additional
+ConfigMap, mounted into the `custom.d` directory.
+
 #### General Nomenclature & Definitions
 
 ```
@@ -151,6 +162,7 @@ file.
 sources:
   custom:
   - name: <feature name>
+    value: <optional feature value, defaults to "true">
     matchOn:
     - <Rule-1>: <Rule-1 Input>
       [<Rule-2>: <Rule-2 Input>]
@@ -291,6 +303,26 @@ Matching is done by performing logical _AND_ for each provided Element, i.e the
 Rule will match if all provided Elements (kernel config options) are enabled
 (`y` or `m`) or matching `=<value>` in the kernel.
 
+##### Nodename Rule
+
+###### Nomenclature
+
+```
+Element     :A nodename regexp pattern
+```
+
+The Rule allows matching the node's name against a provided list of Elements.
+
+###### Format
+
+```yaml
+nodename: [ <nodename regexp pattern>, ... ]
+```
+
+Matching is done by performing logical _OR_ for each provided Element, i.e the
+Rule will match if one of the provided Elements (nodename regexp pattern)
+matches the node's name.
+
 #### Example
 
 ```yaml
@@ -328,6 +360,10 @@ custom:
     matchOn:
       - kConfig: ["GCC_VERSION=100101"]
         loadedKMod: ["kmod1"]
+  - name: "my.datacenter"
+    value: "datacenter-1"
+    matchOn:
+      - nodename: [ "node-datacenter1-rack.*-server.*" ]
 ```
 
 __In the example above:__
@@ -360,6 +396,10 @@ __In the example above:__
   `feature.node.kubernetes.io/custom-my.kernel.modulecompiler=true` if the
   in-tree `kmod1` kernel module is loaded __AND__ it's built with
   `GCC_VERSION=100101`.
+- A node would contain the label:
+  `feature.node.kubernetes.io/my.datacenter=datacenter-1` if the node's name
+  matches the `node-datacenter1-rack.*-server.*` pattern, e.g.
+  `node-datacenter1-rack2-server42`
 
 #### Statically defined features
 
diff --git a/go.mod b/go.mod
index 3895db012..5ef0f147c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ 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/nfd-daemonset-combined.yaml.template b/nfd-daemonset-combined.yaml.template
index 2bb4f148f..7c14f7c70 100644
--- a/nfd-daemonset-combined.yaml.template
+++ b/nfd-daemonset-combined.yaml.template
@@ -115,6 +115,11 @@ spec:
             - name: nfd-worker-conf
               mountPath: "/etc/kubernetes/node-feature-discovery"
               readOnly: true
+## Example for more custom configs in an additional configmap (1/3)
+## Mounting into subdirectories of custom.d makes it easy to use multiple configmaps
+#            - name: custom-source-extra-rules
+#              mountPath: "/etc/kubernetes/node-feature-discovery/custom.d/extra-rules-1"
+#              readOnly: true
       volumes:
         - name: host-boot
           hostPath:
@@ -134,6 +139,10 @@ spec:
         - name: nfd-worker-conf
           configMap:
             name: nfd-worker-conf
+## Example for more custom configs in an additional configmap (2/3)
+#        - name: custom-source-extra-rules
+#          configMap:
+#            name: custom-source-extra-rules
 ---
 apiVersion: v1
 kind: ConfigMap
@@ -232,4 +241,25 @@ data:
     #            vendor: ["15b3"]
     #            device: ["1014", "1017"]
     #          loadedKMod : ["vendor_kmod1", "vendor_kmod2"]
+    #    - name: "feature.by.nodename"
+    #      value: customValue
+    #      matchOn:
+    #        - nodename: ["worker-0", "my-.*-node"]
 ### <NFD-WORKER-CONF-END-DO-NOT-REMOVE>
+---
+## Example for more custom configs in an additional configmap (3/3)
+#apiVersion: v1
+#kind: ConfigMap
+#metadata:
+#  name: custom-source-extra-rules
+#  namespace: node-feature-discovery
+#data:
+## Filename doesn't matter, and there can be multiple. They just need to be unique.
+#  custom.conf: |
+#    - name: "more.kernel.features"
+#      matchOn:
+#      - loadedKMod: ["example_kmod3"]
+#    - name: "more.features.by.nodename"
+#      value: customValue
+#      matchOn:
+#      - nodename: ["special-.*-node-.*"]
diff --git a/nfd-worker-daemonset.yaml.template b/nfd-worker-daemonset.yaml.template
index b4066c114..d6ced4ba1 100644
--- a/nfd-worker-daemonset.yaml.template
+++ b/nfd-worker-daemonset.yaml.template
@@ -32,6 +32,11 @@ spec:
               drop: ["ALL"]
             readOnlyRootFilesystem: true
             runAsNonRoot: true
+          tolerations:
+              - key: "node-role.kubernetes.io/master"
+                operator: "Equal"
+                value: ""
+                effect: "NoSchedule"
           command:
             - "nfd-worker"
           args:
@@ -63,6 +68,11 @@ spec:
             - name: nfd-worker-conf
               mountPath: "/etc/kubernetes/node-feature-discovery"
               readOnly: true
+## Example for more custom configs in an additional configmap (1/3)
+## Mounting into subdirectories of custom.d makes it easy to use multiple configmaps
+#            - name: custom-source-extra-rules
+#              mountPath: "/etc/kubernetes/node-feature-discovery/custom.d/extra-rules-1"
+#              readOnly: true
 ## Enable TLS authentication (2/3)
 #            - name: nfd-ca-cert
 #              mountPath: "/etc/kubernetes/node-feature-discovery/trust"
@@ -89,6 +99,10 @@ spec:
         - name: nfd-worker-conf
           configMap:
             name: nfd-worker-conf
+## Example for more custom configs in an additional configmap (2/3)
+#        - name: custom-source-extra-rules
+#          configMap:
+#            name: custom-source-extra-rules
 ## Enable TLS authentication (3/3)
 #        - name: nfd-ca-cert
 #          configMap:
@@ -194,4 +208,25 @@ data:
     #            vendor: ["15b3"]
     #            device: ["1014", "1017"]
     #          loadedKMod : ["vendor_kmod1", "vendor_kmod2"]
+    #    - name: "feature.by.nodename"
+    #      value: customValue
+    #      matchOn:
+    #        - nodename: ["worker-0", "my-.*-node"]
 ### <NFD-WORKER-CONF-END-DO-NOT-REMOVE>
+---
+## Example for more custom configs in an additional configmap (3/3)
+#apiVersion: v1
+#kind: ConfigMap
+#metadata:
+#  name: custom-source-extra-rules
+#  namespace: node-feature-discovery
+#data:
+## Filename doesn't matter, and there can be multiple. They just need to be unique.
+#  custom.conf: |
+#    - name: "more.kernel.features"
+#      matchOn:
+#      - loadedKMod: ["example_kmod3"]
+#    - name: "more.features.by.nodename"
+#      value: customValue
+#      matchOn:
+#      - nodename: ["special-.*-node-.*"]
diff --git a/nfd-worker-job.yaml.template b/nfd-worker-job.yaml.template
index 6eaf9ec49..7fd8a0b65 100644
--- a/nfd-worker-job.yaml.template
+++ b/nfd-worker-job.yaml.template
@@ -72,6 +72,11 @@ spec:
             - name: nfd-worker-conf
               mountPath: "/etc/kubernetes/node-feature-discovery"
               readOnly: true
+## Example for more custom configs in an additional configmap (1/3)
+## Mounting into subdirectories of custom.d makes it easy to use multiple configmaps
+#            - name: custom-source-extra-rules
+#              mountPath: "/etc/kubernetes/node-feature-discovery/custom.d/extra-rules-1"
+#              readOnly: true
 ## Enable TLS authentication (2/3)
 #            - name: nfd-ca-cert
 #              mountPath: "/etc/kubernetes/node-feature-discovery/trust"
@@ -99,6 +104,10 @@ spec:
         - name: nfd-worker-conf
           configMap:
             name: nfd-worker-conf
+## Example for more custom configs in an additional configmap (2/3)
+#        - name: custom-source-extra-rules
+#          configMap:
+#            name: custom-source-extra-rules
 ## Enable TLS authentication (3/3)
 #        - name: nfd-ca-cert
 #          configMap:
@@ -204,4 +213,25 @@ data:
     #            vendor: ["15b3"]
     #            device: ["1014", "1017"]
     #          loadedKMod : ["vendor_kmod1", "vendor_kmod2"]
+    #    - name: "feature.by.nodename"
+    #      value: customValue
+    #      matchOn:
+    #        - nodename: ["worker-0", "my-.*-node"]
 ### <NFD-WORKER-CONF-END-DO-NOT-REMOVE>
+---
+## Example for more custom configs in an additional configmap (3/3)
+#apiVersion: v1
+#kind: ConfigMap
+#metadata:
+#  name: custom-source-extra-rules
+#  namespace: node-feature-discovery
+#data:
+## Filename doesn't matter, and there can be multiple. They just need to be unique.
+#  custom.conf: |
+#    - name: "more.kernel.features"
+#      matchOn:
+#      - loadedKMod: ["example_kmod3"]
+#    - name: "more.features.by.nodename"
+#      value: customValue
+#      matchOn:
+#      - nodename: ["special-.*-node-.*"]
diff --git a/nfd-worker.conf.example b/nfd-worker.conf.example
index 8f9447c1a..1f60ae521 100644
--- a/nfd-worker.conf.example
+++ b/nfd-worker.conf.example
@@ -88,3 +88,7 @@
 #            vendor: ["15b3"]
 #            device: ["1014", "1017"]
 #          loadedKMod : ["vendor_kmod1", "vendor_kmod2"]
+#    - name: "feature.by.nodename"
+#      value: customValue
+#      matchOn:
+#        - nodename: ["worker-0", "my-.*-node"]
diff --git a/pkg/nfd-master/nfd-master.go b/pkg/nfd-master/nfd-master.go
index f1e9ed378..6a3ac39c5 100644
--- a/pkg/nfd-master/nfd-master.go
+++ b/pkg/nfd-master/nfd-master.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019-2021 The Kubernetes Authors.
+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.
@@ -38,7 +38,6 @@ 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"
 )
 
@@ -75,16 +74,16 @@ type Annotations map[string]string
 type Args struct {
 	CaFile         string
 	CertFile       string
-	ExtraLabelNs   utils.StringSetVal
+	ExtraLabelNs   map[string]struct{}
 	Instance       string
 	KeyFile        string
 	Kubeconfig     string
-	LabelWhiteList utils.RegexpVal
+	LabelWhiteList *regexp.Regexp
 	NoPublish      bool
 	Port           int
 	Prune          bool
 	VerifyNodeName bool
-	ResourceLabels utils.StringSetVal
+	ResourceLabels []string
 }
 
 type NfdMaster interface {
@@ -103,8 +102,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),
 	}
@@ -112,11 +111,6 @@ 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
 	}
 
@@ -289,7 +283,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 map[string]struct{}) (Labels, ExtendedResources) {
+func filterFeatureLabels(labels Labels, extraLabelNs map[string]struct{}, labelWhiteList *regexp.Regexp, extendedResourceNames []string) (Labels, ExtendedResources) {
 	outLabels := Labels{}
 
 	for label, value := range labels {
@@ -316,7 +310,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 {
@@ -360,7 +354,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.Regexp, m.args.ResourceLabels)
+	labels, extendedResources := filterFeatureLabels(r.Labels, m.args.ExtraLabelNs, m.args.LabelWhiteList, 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 36beb6f8d..454eede3d 100644
--- a/pkg/nfd-worker/nfd-worker.go
+++ b/pkg/nfd-worker/nfd-worker.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019-2021 The Kubernetes Authors.
+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.
@@ -34,10 +34,7 @@ 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"
@@ -53,6 +50,7 @@ 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 (
@@ -68,7 +66,7 @@ type NFDConfig struct {
 }
 
 type coreConfig struct {
-	LabelWhiteList utils.RegexpVal
+	LabelWhiteList regex
 	NoPublish      bool
 	Sources        []string
 	SleepInterval  duration
@@ -89,18 +87,11 @@ type Args struct {
 	Oneshot            bool
 	Server             string
 	ServerNameOverride string
-
-	Overrides ConfigOverrideArgs
-}
-
-// ConfigOverrideArgs are args that override config file options
-type ConfigOverrideArgs struct {
-	NoPublish *bool
-
-	// Deprecated
-	LabelWhiteList *utils.RegexpVal
+	// Deprecated options that should be set via the config file
+	LabelWhiteList *regexp.Regexp
+	NoPublish      *bool
 	SleepInterval  *time.Duration
-	Sources        *utils.StringSliceVal
+	Sources        *[]string
 }
 
 type NfdWorker interface {
@@ -120,14 +111,18 @@ 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{},
@@ -208,7 +203,7 @@ func addConfigWatch(path string) (*fsnotify.Watcher, map[string]struct{}, error)
 func newDefaultConfig() *NFDConfig {
 	return &NFDConfig{
 		Core: coreConfig{
-			LabelWhiteList: utils.RegexpVal{Regexp: *regexp.MustCompile("")},
+			LabelWhiteList: regex{*regexp.MustCompile("")},
 			SleepInterval:  duration{60 * time.Second},
 			Sources:        []string{"all"},
 		},
@@ -243,7 +238,7 @@ func (w *nfdWorker) Run() error {
 		select {
 		case <-labelTrigger:
 			// Get the set of feature labels.
-			labels := createFeatureLabels(w.enabledSources, w.config.Core.LabelWhiteList.Regexp)
+			labels := createFeatureLabels(w.enabledSources, w.config.Core.LabelWhiteList)
 
 			// Update the node with the feature labels.
 			if w.client != nil {
@@ -453,17 +448,17 @@ func (w *nfdWorker) configure(filepath string, overrides string) error {
 		return fmt.Errorf("Failed to parse --options: %s", err)
 	}
 
-	if w.args.Overrides.LabelWhiteList != nil {
-		c.Core.LabelWhiteList = *w.args.Overrides.LabelWhiteList
+	if w.args.LabelWhiteList != nil {
+		c.Core.LabelWhiteList = regex{*w.args.LabelWhiteList}
 	}
-	if w.args.Overrides.NoPublish != nil {
-		c.Core.NoPublish = *w.args.Overrides.NoPublish
+	if w.args.NoPublish != nil {
+		c.Core.NoPublish = *w.args.NoPublish
 	}
-	if w.args.Overrides.SleepInterval != nil {
-		c.Core.SleepInterval = duration{*w.args.Overrides.SleepInterval}
+	if w.args.SleepInterval != nil {
+		c.Core.SleepInterval = duration{*w.args.SleepInterval}
 	}
-	if w.args.Overrides.Sources != nil {
-		c.Core.Sources = *w.args.Overrides.Sources
+	if w.args.Sources != nil {
+		c.Core.Sources = *w.args.Sources
 	}
 
 	c.Core.sanitize()
@@ -482,7 +477,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 regexp.Regexp) (labels Labels) {
+func createFeatureLabels(sources []source.FeatureSource, labelWhiteList regex) (labels Labels) {
 	labels = Labels{}
 
 	// Do feature discovery from all configured sources.
@@ -505,7 +500,7 @@ func createFeatureLabels(sources []source.FeatureSource, labelWhiteList regexp.R
 
 // getFeatureLabels returns node labels for features discovered by the
 // supplied source.
-func getFeatureLabels(source source.FeatureSource, labelWhiteList regexp.Regexp) (labels Labels, err error) {
+func getFeatureLabels(source source.FeatureSource, labelWhiteList regex) (labels Labels, err error) {
 	defer func() {
 		if r := recover(); r != nil {
 			stderrLogger.Printf("panic occurred during discovery of source [%s]: %v", source.Name(), r)
@@ -589,6 +584,25 @@ 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
deleted file mode 100644
index b91c70c7f..000000000
--- a/pkg/utils/flags.go
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
-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, ",")
-}
diff --git a/source/custom/custom.go b/source/custom/custom.go
index f77e0dcd6..a49ad510e 100644
--- a/source/custom/custom.go
+++ b/source/custom/custom.go
@@ -18,6 +18,7 @@ package custom
 
 import (
 	"log"
+	"reflect"
 
 	"sigs.k8s.io/node-feature-discovery/source"
 	"sigs.k8s.io/node-feature-discovery/source/custom/rules"
@@ -30,10 +31,12 @@ type MatchRule struct {
 	LoadedKMod *rules.LoadedKModRule `json:"loadedKMod,omitempty"`
 	CpuID      *rules.CpuIDRule      `json:"cpuId,omitempty"`
 	Kconfig    *rules.KconfigRule    `json:"kConfig,omitempty"`
+	Nodename   *rules.NodenameRule   `json:"nodename,omitempty"`
 }
 
 type FeatureSpec struct {
 	Name    string      `json:"name"`
+	Value   *string     `json:"value"`
 	MatchOn []MatchRule `json:"matchOn"`
 }
 
@@ -72,6 +75,7 @@ func (s *Source) SetConfig(conf source.Config) {
 func (s Source) Discover() (source.Features, error) {
 	features := source.Features{}
 	allFeatureConfig := append(getStaticFeatureConfig(), *s.config...)
+	allFeatureConfig = append(allFeatureConfig, getDirectoryFeatureConfig()...)
 	log.Printf("INFO: Custom features: %+v", allFeatureConfig)
 	// Iterate over features
 	for _, customFeature := range allFeatureConfig {
@@ -81,7 +85,11 @@ func (s Source) Discover() (source.Features, error) {
 			continue
 		}
 		if featureExist {
-			features[customFeature.Name] = true
+			var value interface{} = true
+			if customFeature.Value != nil {
+				value = *customFeature.Value
+			}
+			features[customFeature.Name] = value
 		}
 	}
 	return features, nil
@@ -90,58 +98,37 @@ func (s Source) Discover() (source.Features, 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) {
-	for _, rule := range feature.MatchOn {
-		// PCI ID rule
-		if rule.PciID != nil {
-			match, err := rule.PciID.Match()
-			if err != nil {
-				return false, err
-			}
-			if !match {
-				continue
-			}
-		}
-		// USB ID rule
-		if rule.UsbID != nil {
-			match, err := rule.UsbID.Match()
-			if err != nil {
-				return false, err
-			}
-			if !match {
-				continue
-			}
-		}
-		// Loaded kernel module rule
-		if rule.LoadedKMod != nil {
-			match, err := rule.LoadedKMod.Match()
-			if err != nil {
-				return false, err
-			}
-			if !match {
-				continue
-			}
+	for _, matchRules := range feature.MatchOn {
+
+		allRules := []rules.Rule{
+			matchRules.PciID,
+			matchRules.UsbID,
+			matchRules.LoadedKMod,
+			matchRules.CpuID,
+			matchRules.Kconfig,
+			matchRules.Nodename,
 		}
-		// cpuid rule
-		if rule.CpuID != nil {
-			match, err := rule.CpuID.Match()
-			if err != nil {
-				return false, err
-			}
-			if !match {
-				continue
+
+		// return true, nil if all rules match
+		matchRules := func(rules []rules.Rule) (bool, error) {
+			for _, rule := range rules {
+				if reflect.ValueOf(rule).IsNil() {
+					continue
+				}
+				if match, err := rule.Match(); err != nil {
+					return false, err
+				} else if !match {
+					return false, nil
+				}
 			}
+			return true, nil
 		}
-		// kconfig rule
-		if rule.Kconfig != nil {
-			match, err := rule.Kconfig.Match()
-			if err != nil {
-				return false, err
-			}
-			if !match {
-				continue
-			}
+
+		if match, err := matchRules(allRules); err != nil {
+			return false, err
+		} else if match {
+			return true, nil
 		}
-		return true, nil
 	}
 	return false, nil
 }
diff --git a/source/custom/directory_features.go b/source/custom/directory_features.go
new file mode 100644
index 000000000..333547a55
--- /dev/null
+++ b/source/custom/directory_features.go
@@ -0,0 +1,88 @@
+/*
+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 custom
+
+import (
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"sigs.k8s.io/yaml"
+)
+
+const Directory = "/etc/kubernetes/node-feature-discovery/custom.d"
+
+// getDirectoryFeatureConfig returns features configured in the "/etc/kubernetes/node-feature-discovery/custom.d"
+// host directory and its 1st level subdirectories, which can be populated e.g. by ConfigMaps
+func getDirectoryFeatureConfig() []FeatureSpec {
+	features := readDir(Directory, true)
+	//log.Printf("DEBUG: all configmap based custom feature specs: %+v", features)
+	return features
+}
+
+func readDir(dirName string, recursive bool) []FeatureSpec {
+	features := make([]FeatureSpec, 0)
+
+	log.Printf("DEBUG: getting files in %s", dirName)
+	files, err := ioutil.ReadDir(dirName)
+	if err != nil {
+		if os.IsNotExist(err) {
+			log.Printf("DEBUG: custom config directory %q does not exist", dirName)
+		} else {
+			log.Printf("ERROR: unable to access custom config directory %q, %v", dirName, err)
+		}
+		return features
+	}
+
+	for _, file := range files {
+		fileName := filepath.Join(dirName, file.Name())
+
+		if file.IsDir() {
+			if recursive {
+				//log.Printf("DEBUG: going into dir %q", fileName)
+				features = append(features, readDir(fileName, false)...)
+				//} else {
+				//	log.Printf("DEBUG: skipping dir %q", fileName)
+			}
+			continue
+		}
+		if strings.HasPrefix(file.Name(), ".") {
+			//log.Printf("DEBUG: skipping hidden file %q", fileName)
+			continue
+		}
+		//log.Printf("DEBUG: processing file %q", fileName)
+
+		bytes, err := ioutil.ReadFile(fileName)
+		if err != nil {
+			log.Printf("ERROR: could not read custom config file %q, %v", fileName, err)
+			continue
+		}
+		//log.Printf("DEBUG: custom config rules raw: %s", string(bytes))
+
+		config := &[]FeatureSpec{}
+		err = yaml.UnmarshalStrict(bytes, config)
+		if err != nil {
+			log.Printf("ERROR: could not parse custom config file %q, %v", fileName, err)
+			continue
+		}
+
+		features = append(features, *config...)
+	}
+	return features
+}
diff --git a/source/custom/rules/nodename_rule.go b/source/custom/rules/nodename_rule.go
new file mode 100644
index 000000000..744f6dd6c
--- /dev/null
+++ b/source/custom/rules/nodename_rule.go
@@ -0,0 +1,50 @@
+/*
+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 rules
+
+import (
+	"log"
+	"os"
+	"regexp"
+)
+
+var (
+	nodeName = os.Getenv("NODE_NAME")
+)
+
+// Rule that matches on nodenames configured in a ConfigMap
+type NodenameRule []string
+
+// Force implementation of Rule
+var _ Rule = NodenameRule{}
+
+func (n NodenameRule) Match() (bool, error) {
+	for _, nodenamePattern := range n {
+		log.Printf("DEBUG: matchNodename %s", nodenamePattern)
+		match, err := regexp.MatchString(nodenamePattern, nodeName)
+		if err != nil {
+			log.Printf("ERROR: nodename rule: invalid nodename regexp %q: %v", nodenamePattern, err)
+			continue
+		}
+		if !match {
+			//log.Printf("DEBUG: nodename rule: No match for pattern %q with node %q", nodenamePattern, nodeName)
+			continue
+		}
+		//log.Printf("DEBUG: nodename rule: Match for pattern %q with node %q", nodenamePattern, nodeName)
+		return true, nil
+	}
+	return false, nil
+}


Test patch:
diff --git a/cmd/nfd-master/main_test.go b/cmd/nfd-master/main_test.go
new file mode 100644
index 000000000..57ab6bf4f
--- /dev/null
+++ b/cmd/nfd-master/main_test.go
@@ -0,0 +1,55 @@
+/*
+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 9481f8116..d6b222bc5 100644
--- a/cmd/nfd-worker/main_test.go
+++ b/cmd/nfd-worker/main_test.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019-2021 The Kubernetes Authors.
+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.
@@ -17,44 +17,62 @@ limitations under the License.
 package main
 
 import (
-	"flag"
 	"testing"
 	"time"
 
 	. "github.com/smartystreets/goconvey/convey"
-
-	"sigs.k8s.io/node-feature-discovery/pkg/utils"
 )
 
-func TestParseArgs(t *testing.T) {
+func TestArgsParse(t *testing.T) {
 	Convey("When parsing command line arguments", t, func() {
-		flags := flag.NewFlagSet(ProgramName, flag.ExitOnError)
-
-		Convey("When no override args are specified", func() {
-			args := parseArgs(flags, "--oneshot")
+		Convey("When --no-publish and --oneshot flags are passed", func() {
+			args, err := argsParse([]string{"--no-publish", "--oneshot"})
 
-			Convey("overrides should be nil", func() {
+			Convey("noPublish is set and args.sources is set to the default value", func() {
+				So(args.SleepInterval, ShouldEqual, nil)
+				So(*args.NoPublish, ShouldBeTrue)
 				So(args.Oneshot, ShouldBeTrue)
-				So(args.Overrides.NoPublish, ShouldBeNil)
-				So(args.Overrides.LabelWhiteList, ShouldBeNil)
-				So(args.Overrides.SleepInterval, ShouldBeNil)
-				So(args.Overrides.Sources, ShouldBeNil)
+				So(args.Sources, ShouldBeNil)
+				So(args.LabelWhiteList, ShouldBeNil)
+				So(err, ShouldBeNil)
 			})
 		})
 
-		Convey("When all override args are specified", func() {
-			args := parseArgs(flags,
-				"--no-publish",
-				"-label-whitelist=.*rdt.*",
-				"-sources=fake1,fake2,fake3",
-				"-sleep-interval=30s")
+		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("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.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.*")
+				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)
 			})
 		})
 	})
diff --git a/pkg/nfd-master/nfd-master-internal_test.go b/pkg/nfd-master/nfd-master-internal_test.go
index 3accdaaa6..45a1d239e 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-2021 The Kubernetes Authors.
+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.
@@ -34,7 +34,6 @@ 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"
 )
@@ -56,7 +55,7 @@ func newMockMaster(apihelper apihelper.APIHelpers) *nfdMaster {
 	return &nfdMaster{
 		nodeName:     mockNodeName,
 		annotationNs: AnnotationNsBase,
-		args:         Args{LabelWhiteList: utils.RegexpVal{Regexp: *regexp.MustCompile("")}},
+		args:         Args{LabelWhiteList: regexp.MustCompile("")},
 		apihelper:    apihelper,
 	}
 }
@@ -340,7 +339,7 @@ func TestSetLabels(t *testing.T) {
 				apihelper.NewJsonPatch("add", "/metadata/labels", LabelNs+"/feature-2", mockLabels["feature-2"]),
 			}
 
-			mockMaster.args.LabelWhiteList.Regexp = *regexp.MustCompile("^f.*2$")
+			mockMaster.args.LabelWhiteList = 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)
@@ -391,7 +390,7 @@ func TestSetLabels(t *testing.T) {
 				apihelper.NewJsonPatch("add", "/status/capacity", LabelNs+"/feature-3", mockLabels["feature-3"]),
 			}
 
-			mockMaster.args.ResourceLabels = map[string]struct{}{"feature-3": struct{}{}, "feature-1": struct{}{}}
+			mockMaster.args.ResourceLabels = []string{"feature-3", "feature-1"}
 			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 43b0b60a6..d3d5e085c 100644
--- a/pkg/nfd-master/nfd-master_test.go
+++ b/pkg/nfd-master/nfd-master_test.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019-2021 The Kubernetes Authors.
+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.
@@ -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 8b2943ad5..eaeffa122 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-2021 The Kubernetes Authors.
+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.
@@ -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 := utils.RegexpVal{Regexp: *regexp.MustCompile("^test")}
+		labelWhiteList := regex{*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.Regexp)
+			returnedLabels, err := getFeatureLabels(fakeFeatureSource, labelWhiteList)
 			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.Regexp)
+			returnedLabels, err := getFeatureLabels(fakeFeatureSource, labelWhiteList)
 			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{Overrides: ConfigOverrideArgs{Sources: &utils.StringSliceVal{"cpu", "kernel", "pci"}}}
+			worker.args = Args{Sources: &[]string{"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{Overrides: ConfigOverrideArgs{Sources: &utils.StringSliceVal{"cpu", "kernel", "pci"}}}
+			worker.args = Args{Sources: &[]string{"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{Overrides: ConfigOverrideArgs{SleepInterval: &sleepIntervalArg}}
+			worker.args = Args{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,
-			Overrides: ConfigOverrideArgs{
-				Sources:   &utils.StringSliceVal{"fake"},
-				NoPublish: &noPublish},
+		w, err := NewNfdWorker(Args{
+			ConfigFile:    configFile,
+			Sources:       &[]string{"fake"},
+			NoPublish:     &noPublish,
+			SleepInterval: new(time.Duration),
 		})
 		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 := utils.RegexpVal{Regexp: *regexp.MustCompile("")}
+		emptyRegexp := regex{*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{Overrides: ConfigOverrideArgs{Sources: &utils.StringSliceVal{"fake"}}}
+			args := Args{Sources: &[]string{"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{Overrides: ConfigOverrideArgs{LabelWhiteList: &utils.RegexpVal{Regexp: *regexp.MustCompile(".*rdt.*")}}}
+			args := Args{LabelWhiteList: 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 := utils.RegexpVal{Regexp: *regexp.MustCompile(".*rdt.*")}
+			expectRegexp := regex{*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 := regexp.MustCompile("")
+			emptyLabelWL := regex{*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,10 +374,11 @@ 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, *regexp.MustCompile(".*rdt.*"))
+			labels := createFeatureLabels(sources, labelWL)
 
 			Convey("fake labels are not returned", func() {
 				So(len(labels), ShouldEqual, 0)
@@ -393,7 +394,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, *regexp.MustCompile(""))
+		returnedLabels, err := getFeatureLabels(fakePanicFeatureSource, regex{*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 2f7b4ea9b..2707a98bb 100644
--- a/pkg/nfd-worker/nfd-worker_test.go
+++ b/pkg/nfd-worker/nfd-worker_test.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2019-2021 The Kubernetes Authors.
+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.
@@ -24,24 +24,22 @@ import (
 	"time"
 
 	. "github.com/smartystreets/goconvey/convey"
-
-	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"
+	nfdmaster "sigs.k8s.io/node-feature-discovery/pkg/nfd-master"
+	w "sigs.k8s.io/node-feature-discovery/pkg/nfd-worker"
 	"sigs.k8s.io/node-feature-discovery/test/data"
 )
 
 type testContext struct {
-	master master.NfdMaster
+	master nfdmaster.NfdMaster
 	errs   chan error
 }
 
-func setupTest(args *master.Args) testContext {
+func setupTest(args nfdmaster.Args) testContext {
 	// Fixed port and no-publish, for convenience
 	args.NoPublish = true
 	args.Port = 8192
-	args.LabelWhiteList.Regexp = *regexp.MustCompile("")
-	m, err := master.NewNfdMaster(args)
+	args.LabelWhiteList = regexp.MustCompile("")
+	m, err := nfdmaster.NewNfdMaster(args)
 	if err != nil {
 		fmt.Printf("Test setup failed: %v\n", err)
 		os.Exit(1)
@@ -75,9 +73,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 := 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"})
+			_, 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"})
 			Convey("An error should be returned", func() {
 				So(err, ShouldNotBeNil)
 				So(err2, ShouldNotBeNil)
@@ -88,17 +86,12 @@ func TestNewNfdWorker(t *testing.T) {
 }
 
 func TestRun(t *testing.T) {
-	ctx := setupTest(&master.Args{})
+	ctx := setupTest(nfdmaster.Args{})
 	defer teardownTest(ctx)
 	Convey("When running nfd-worker against nfd-master", t, func() {
 		Convey("When publishing features from fake source", func() {
-			args := &worker.Args{
-				Oneshot:   true,
-				Server:    "localhost:8192",
-				Overrides: worker.ConfigOverrideArgs{Sources: &utils.StringSliceVal{"fake"}},
-			}
-			fooasdf, _ := worker.NewNfdWorker(args)
-			err := fooasdf.Run()
+			worker, _ := w.NewNfdWorker(w.Args{Oneshot: true, Sources: &[]string{"fake"}, Server: "localhost:8192"})
+			err := worker.Run()
 			Convey("No error should be returned", func() {
 				So(err, ShouldBeNil)
 			})
@@ -107,7 +100,7 @@ func TestRun(t *testing.T) {
 }
 
 func TestRunTls(t *testing.T) {
-	masterArgs := &master.Args{
+	masterArgs := nfdmaster.Args{
 		CaFile:         data.FilePath("ca.crt"),
 		CertFile:       data.FilePath("nfd-test-master.crt"),
 		KeyFile:        data.FilePath("nfd-test-master.key"),
@@ -117,17 +110,16 @@ 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 := worker.Args{
+			workerArgs := w.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",
-				Overrides:          worker.ConfigOverrideArgs{Sources: &utils.StringSliceVal{"fake"}},
-			}
-			w, _ := worker.NewNfdWorker(&workerArgs)
-			err := w.Run()
+				ServerNameOverride: "nfd-test-master"}
+			worker, _ := w.NewNfdWorker(workerArgs)
+			err := worker.Run()
 			Convey("No error should be returned", func() {
 				So(err, ShouldBeNil)
 			})
diff --git a/test/e2e/node_feature_discovery.go b/test/e2e/node_feature_discovery.go
index 5526786d0..7880c0b48 100644
--- a/test/e2e/node_feature_discovery.go
+++ b/test/e2e/node_feature_discovery.go
@@ -21,12 +21,13 @@ import (
 	"flag"
 	"fmt"
 	"io/ioutil"
+	"path/filepath"
 	"regexp"
 	"strings"
 	"time"
 
-	"github.com/onsi/ginkgo"
-	"github.com/onsi/gomega"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
 
 	appsv1 "k8s.io/api/apps/v1"
 	v1 "k8s.io/api/core/v1"
@@ -40,6 +41,7 @@ import (
 	e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
 
 	master "sigs.k8s.io/node-feature-discovery/pkg/nfd-master"
+	"sigs.k8s.io/node-feature-discovery/source/custom"
 	"sigs.k8s.io/yaml"
 )
 
@@ -90,18 +92,18 @@ func readConfig() {
 		return
 	}
 
-	ginkgo.By("Reading end-to-end test configuration file")
+	By("Reading end-to-end test configuration file")
 	data, err := ioutil.ReadFile(*e2eConfigFile)
-	gomega.Expect(err).NotTo(gomega.HaveOccurred())
+	Expect(err).NotTo(HaveOccurred())
 
-	ginkgo.By("Parsing end-to-end test configuration data")
+	By("Parsing end-to-end test configuration data")
 	err = yaml.Unmarshal(data, &conf)
-	gomega.Expect(err).NotTo(gomega.HaveOccurred())
+	Expect(err).NotTo(HaveOccurred())
 
 	// Pre-compile node name matching regexps
 	for name, nodeConf := range conf.DefaultFeatures.Nodes {
 		nodeConf.nameRe, err = regexp.Compile(name)
-		gomega.Expect(err).NotTo(gomega.HaveOccurred())
+		Expect(err).NotTo(HaveOccurred())
 		conf.DefaultFeatures.Nodes[name] = nodeConf
 	}
 }
@@ -166,6 +168,13 @@ func createClusterRole(cs clientset.Interface) (*rbacv1.ClusterRole, error) {
 				Resources: []string{"nodes"},
 				Verbs:     []string{"get", "patch", "update"},
 			},
+			{
+				// needed on OpenShift clusters
+				APIGroups:     []string{"security.openshift.io"},
+				Resources:     []string{"securitycontextconstraints"},
+				ResourceNames: []string{"hostaccess"},
+				Verbs:         []string{"use"},
+			},
 		},
 	}
 	return cs.RbacV1().ClusterRoles().Update(context.TODO(), cr, metav1.UpdateOptions{})
@@ -373,14 +382,14 @@ func newHostPathType(typ v1.HostPathType) *v1.HostPathType {
 // labels and annotations
 func cleanupNode(cs clientset.Interface) {
 	nodeList, err := cs.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
-	gomega.Expect(err).NotTo(gomega.HaveOccurred())
+	Expect(err).NotTo(HaveOccurred())
 
 	for _, n := range nodeList.Items {
 		var err error
 		var node *v1.Node
 		for retry := 0; retry < 5; retry++ {
 			node, err = cs.CoreV1().Nodes().Get(context.TODO(), n.Name, metav1.GetOptions{})
-			gomega.Expect(err).NotTo(gomega.HaveOccurred())
+			Expect(err).NotTo(HaveOccurred())
 
 			update := false
 			// Remove labels
@@ -403,7 +412,7 @@ func cleanupNode(cs clientset.Interface) {
 				break
 			}
 
-			ginkgo.By("Deleting NFD labels and annotations from node " + node.Name)
+			By("Deleting NFD labels and annotations from node " + node.Name)
 			_, err = cs.CoreV1().Nodes().Update(context.TODO(), node, metav1.UpdateOptions{})
 			if err != nil {
 				time.Sleep(100 * time.Millisecond)
@@ -412,7 +421,7 @@ func cleanupNode(cs clientset.Interface) {
 			}
 
 		}
-		gomega.Expect(err).NotTo(gomega.HaveOccurred())
+		Expect(err).NotTo(HaveOccurred())
 	}
 }
 
@@ -420,42 +429,42 @@ func cleanupNode(cs clientset.Interface) {
 var _ = framework.KubeDescribe("[NFD] Node Feature Discovery", func() {
 	f := framework.NewDefaultFramework("node-feature-discovery")
 
-	ginkgo.Context("when deploying a single nfd-master pod", func() {
+	Context("when deploying a single nfd-master pod", func() {
 		var masterPod *v1.Pod
 
-		ginkgo.BeforeEach(func() {
+		BeforeEach(func() {
 			err := configureRBAC(f.ClientSet, f.Namespace.Name)
-			gomega.Expect(err).NotTo(gomega.HaveOccurred())
+			Expect(err).NotTo(HaveOccurred())
 
 			// Launch nfd-master
-			ginkgo.By("Creating nfd master pod and nfd-master service")
+			By("Creating nfd master pod and nfd-master service")
 			image := fmt.Sprintf("%s:%s", *dockerRepo, *dockerTag)
 			masterPod = nfdMasterPod(image, false)
 			masterPod, err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(context.TODO(), masterPod, metav1.CreateOptions{})
-			gomega.Expect(err).NotTo(gomega.HaveOccurred())
+			Expect(err).NotTo(HaveOccurred())
 
 			// Create nfd-master service
 			nfdSvc, err := createService(f.ClientSet, f.Namespace.Name)
-			gomega.Expect(err).NotTo(gomega.HaveOccurred())
+			Expect(err).NotTo(HaveOccurred())
 
-			ginkgo.By("Waiting for the nfd-master pod to be running")
-			gomega.Expect(e2epod.WaitTimeoutForPodRunningInNamespace(f.ClientSet, masterPod.Name, masterPod.Namespace, time.Minute)).NotTo(gomega.HaveOccurred())
+			By("Waiting for the nfd-master pod to be running")
+			Expect(e2epod.WaitTimeoutForPodRunningInNamespace(f.ClientSet, masterPod.Name, masterPod.Namespace, time.Minute)).NotTo(HaveOccurred())
 
-			ginkgo.By("Waiting for the nfd-master service to be up")
-			gomega.Expect(e2enetwork.WaitForService(f.ClientSet, f.Namespace.Name, nfdSvc.ObjectMeta.Name, true, time.Second, 10*time.Second)).NotTo(gomega.HaveOccurred())
+			By("Waiting for the nfd-master service to be up")
+			Expect(e2enetwork.WaitForService(f.ClientSet, f.Namespace.Name, nfdSvc.ObjectMeta.Name, true, time.Second, 10*time.Second)).NotTo(HaveOccurred())
 		})
 
-		ginkgo.AfterEach(func() {
+		AfterEach(func() {
 			err := deconfigureRBAC(f.ClientSet, f.Namespace.Name)
-			gomega.Expect(err).NotTo(gomega.HaveOccurred())
+			Expect(err).NotTo(HaveOccurred())
 
 		})
 
 		//
 		// Simple test with only the fake source enabled
 		//
-		ginkgo.Context("and a single worker pod with fake source enabled", func() {
-			ginkgo.It("it should decorate the node with the fake feature labels", func() {
+		Context("and a single worker pod with fake source enabled", func() {
+			It("it should decorate the node with the fake feature labels", func() {
 
 				fakeFeatureLabels := map[string]string{
 					master.LabelNs + "/fake-fakefeature1": "true",
@@ -467,34 +476,34 @@ var _ = framework.KubeDescribe("[NFD] Node Feature Discovery", func() {
 				cleanupNode(f.ClientSet)
 
 				// Launch nfd-worker
-				ginkgo.By("Creating a nfd worker pod")
+				By("Creating a nfd worker pod")
 				image := fmt.Sprintf("%s:%s", *dockerRepo, *dockerTag)
 				workerPod := nfdWorkerPod(image, []string{"--oneshot", "--sources=fake"})
 				workerPod, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(context.TODO(), workerPod, metav1.CreateOptions{})
-				gomega.Expect(err).NotTo(gomega.HaveOccurred())
+				Expect(err).NotTo(HaveOccurred())
 
-				ginkgo.By("Waiting for the nfd-worker pod to succeed")
-				gomega.Expect(e2epod.WaitForPodSuccessInNamespace(f.ClientSet, workerPod.ObjectMeta.Name, f.Namespace.Name)).NotTo(gomega.HaveOccurred())
+				By("Waiting for the nfd-worker pod to succeed")
+				Expect(e2epod.WaitForPodSuccessInNamespace(f.ClientSet, workerPod.ObjectMeta.Name, f.Namespace.Name)).NotTo(HaveOccurred())
 				workerPod, err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Get(context.TODO(), workerPod.ObjectMeta.Name, metav1.GetOptions{})
-				gomega.Expect(err).NotTo(gomega.HaveOccurred())
+				Expect(err).NotTo(HaveOccurred())
 
-				ginkgo.By(fmt.Sprintf("Making sure '%s' was decorated with the fake feature labels", workerPod.Spec.NodeName))
+				By(fmt.Sprintf("Making sure '%s' was decorated with the fake feature labels", workerPod.Spec.NodeName))
 				node, err := f.ClientSet.CoreV1().Nodes().Get(context.TODO(), workerPod.Spec.NodeName, metav1.GetOptions{})
-				gomega.Expect(err).NotTo(gomega.HaveOccurred())
+				Expect(err).NotTo(HaveOccurred())
 				for k, v := range fakeFeatureLabels {
-					gomega.Expect(node.Labels[k]).To(gomega.Equal(v))
+					Expect(node.Labels[k]).To(Equal(v))
 				}
 
 				// Check that there are no unexpected NFD labels
 				for k := range node.Labels {
 					if strings.HasPrefix(k, master.LabelNs) {
-						gomega.Expect(fakeFeatureLabels).Should(gomega.HaveKey(k))
+						Expect(fakeFeatureLabels).Should(HaveKey(k))
 					}
 				}
 
-				ginkgo.By("Deleting the node-feature-discovery worker pod")
+				By("Deleting the node-feature-discovery worker pod")
 				err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Delete(context.TODO(), workerPod.ObjectMeta.Name, metav1.DeleteOptions{})
-				gomega.Expect(err).NotTo(gomega.HaveOccurred())
+				Expect(err).NotTo(HaveOccurred())
 
 				cleanupNode(f.ClientSet)
 			})
@@ -503,31 +512,31 @@ var _ = framework.KubeDescribe("[NFD] Node Feature Discovery", func() {
 		//
 		// More comprehensive test when --e2e-node-config is enabled
 		//
-		ginkgo.Context("and nfd-workers as a daemonset with default sources enabled", func() {
-			ginkgo.It("the node labels and annotations listed in the e2e config should be present", func() {
+		Context("and nfd-workers as a daemonset with default sources enabled", func() {
+			It("the node labels and annotations listed in the e2e config should be present", func() {
 				readConfig()
 				if conf == nil {
-					ginkgo.Skip("no e2e-config was specified")
+					Skip("no e2e-config was specified")
 				}
 				if conf.DefaultFeatures == nil {
-					ginkgo.Skip("no 'defaultFeatures' specified in e2e-config")
+					Skip("no 'defaultFeatures' specified in e2e-config")
 				}
 				fConf := conf.DefaultFeatures
 
 				// Remove pre-existing stale annotations and labels
 				cleanupNode(f.ClientSet)
 
-				ginkgo.By("Creating nfd-worker daemonset")
+				By("Creating nfd-worker daemonset")
 				workerDS := nfdWorkerDaemonSet(fmt.Sprintf("%s:%s", *dockerRepo, *dockerTag), []string{})
 				workerDS, err := f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).Create(context.TODO(), workerDS, metav1.CreateOptions{})
-				gomega.Expect(err).NotTo(gomega.HaveOccurred())
+				Expect(err).NotTo(HaveOccurred())
 
-				ginkgo.By("Waiting for daemonset pods to be ready")
-				gomega.Expect(e2epod.WaitForPodsReady(f.ClientSet, f.Namespace.Name, workerDS.Spec.Template.Labels["name"], 5)).NotTo(gomega.HaveOccurred())
+				By("Waiting for daemonset pods to be ready")
+				Expect(e2epod.WaitForPodsReady(f.ClientSet, f.Namespace.Name, workerDS.Spec.Template.Labels["name"], 5)).NotTo(HaveOccurred())
 
-				ginkgo.By("Getting node objects")
+				By("Getting node objects")
 				nodeList, err := f.ClientSet.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
-				gomega.Expect(err).NotTo(gomega.HaveOccurred())
+				Expect(err).NotTo(HaveOccurred())
 
 				for _, node := range nodeList.Items {
 					var nodeConf *nodeConfig
@@ -546,10 +555,10 @@ var _ = framework.KubeDescribe("[NFD] Node Feature Discovery", func() {
 					// Check labels
 					e2elog.Logf("verifying labels of node %q...", node.Name)
 					for k, v := range nodeConf.ExpectedLabelValues {
-						gomega.Expect(node.Labels).To(gomega.HaveKeyWithValue(k, v))
+						Expect(node.Labels).To(HaveKeyWithValue(k, v))
 					}
 					for k := range nodeConf.ExpectedLabelKeys {
-						gomega.Expect(node.Labels).To(gomega.HaveKey(k))
+						Expect(node.Labels).To(HaveKey(k))
 					}
 					for k := range node.Labels {
 						if strings.HasPrefix(k, master.LabelNs) {
@@ -560,17 +569,17 @@ var _ = framework.KubeDescribe("[NFD] Node Feature Discovery", func() {
 								continue
 							}
 							// Ignore if the label key was not whitelisted
-							gomega.Expect(fConf.LabelWhitelist).NotTo(gomega.HaveKey(k))
+							Expect(fConf.LabelWhitelist).NotTo(HaveKey(k))
 						}
 					}
 
 					// Check annotations
 					e2elog.Logf("verifying annotations of node %q...", node.Name)
 					for k, v := range nodeConf.ExpectedAnnotationValues {
-						gomega.Expect(node.Annotations).To(gomega.HaveKeyWithValue(k, v))
+						Expect(node.Annotations).To(HaveKeyWithValue(k, v))
 					}
 					for k := range nodeConf.ExpectedAnnotationKeys {
-						gomega.Expect(node.Annotations).To(gomega.HaveKey(k))
+						Expect(node.Annotations).To(HaveKey(k))
 					}
 					for k := range node.Annotations {
 						if strings.HasPrefix(k, master.AnnotationNsBase) {
@@ -581,23 +590,181 @@ var _ = framework.KubeDescribe("[NFD] Node Feature Discovery", func() {
 								continue
 							}
 							// Ignore if the annotation was not whitelisted
-							gomega.Expect(fConf.AnnotationWhitelist).NotTo(gomega.HaveKey(k))
+							Expect(fConf.AnnotationWhitelist).NotTo(HaveKey(k))
 						}
 					}
 
 					// Node running nfd-master should have master version annotation
 					if node.Name == masterPod.Spec.NodeName {
-						gomega.Expect(node.Annotations).To(gomega.HaveKey(master.AnnotationNsBase + "master.version"))
+						Expect(node.Annotations).To(HaveKey(master.AnnotationNsBase + "master.version"))
 					}
 				}
 
-				ginkgo.By("Deleting nfd-worker daemonset")
+				By("Deleting nfd-worker daemonset")
 				err = f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).Delete(context.TODO(), workerDS.ObjectMeta.Name, metav1.DeleteOptions{})
-				gomega.Expect(err).NotTo(gomega.HaveOccurred())
+				Expect(err).NotTo(HaveOccurred())
 
 				cleanupNode(f.ClientSet)
 			})
 		})
+
+		//
+		// Test custom nodename source configured in 2 additional ConfigMaps
+		//
+		Context("and nfd-workers as a daemonset with 2 additional configmaps for the custom source configured", func() {
+			It("the nodename matching features listed in the configmaps should be present", func() {
+				// Remove pre-existing stale annotations and labels
+				cleanupNode(f.ClientSet)
+
+				By("Getting a worker node")
+
+				// We need a valid nodename for the configmap
+				nodeList, err := f.ClientSet.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(len(nodeList.Items)).ToNot(BeZero())
+
+				targetNodeName := ""
+				for _, node := range nodeList.Items {
+					if _, ok := node.Labels["node-role.kubernetes.io/master"]; !ok {
+						targetNodeName = node.Name
+						break
+					}
+				}
+				Expect(targetNodeName).ToNot(BeEmpty(), "No worker node found")
+
+				// create a wildcard name as well for this node
+				targetNodeNameWildcard := fmt.Sprintf("%s.*%s", targetNodeName[:2], targetNodeName[4:])
+
+				By("Creating the configmaps")
+				targetLabelName := "nodename-test"
+				targetLabelValue := "true"
+
+				targetLabelNameWildcard := "nodename-test-wildcard"
+				targetLabelValueWildcard := "customValue"
+
+				targetLabelNameNegative := "nodename-test-negative"
+
+				// create 2 configmaps
+				data1 := make(map[string]string)
+				data1["custom1.conf"] = `
+- name: ` + targetLabelName + `
+  matchOn:
+  # default value is true
+  - nodename:
+    - ` + targetNodeName
+
+				cm1 := &v1.ConfigMap{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "custom-config-extra-" + string(uuid.NewUUID()),
+					},
+					Data: data1,
+				}
+				cm1, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(context.TODO(), cm1, metav1.CreateOptions{})
+				Expect(err).NotTo(HaveOccurred())
+
+				data2 := make(map[string]string)
+				data2["custom1.conf"] = `
+- name: ` + targetLabelNameWildcard + `
+  value: ` + targetLabelValueWildcard + `
+  matchOn:
+  - nodename:
+    - ` + targetNodeNameWildcard + `
+- name: ` + targetLabelNameNegative + `
+  matchOn:
+  - nodename:
+    - "thisNameShouldNeverMatch"`
+
+				cm2 := &v1.ConfigMap{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "custom-config-extra-" + string(uuid.NewUUID()),
+					},
+					Data: data2,
+				}
+				cm2, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(context.TODO(), cm2, metav1.CreateOptions{})
+				Expect(err).NotTo(HaveOccurred())
+
+				By("Creating nfd-worker daemonset with configmap mounted")
+				workerDS := nfdWorkerDaemonSet(fmt.Sprintf("%s:%s", *dockerRepo, *dockerTag), []string{})
+
+				// add configmap mount config
+				volumeName1 := "custom-configs-extra1"
+				volumeName2 := "custom-configs-extra2"
+				workerDS.Spec.Template.Spec.Volumes = append(workerDS.Spec.Template.Spec.Volumes,
+					v1.Volume{
+						Name: volumeName1,
+						VolumeSource: v1.VolumeSource{
+							ConfigMap: &v1.ConfigMapVolumeSource{
+								LocalObjectReference: v1.LocalObjectReference{
+									Name: cm1.Name,
+								},
+							},
+						},
+					},
+					v1.Volume{
+						Name: volumeName2,
+						VolumeSource: v1.VolumeSource{
+							ConfigMap: &v1.ConfigMapVolumeSource{
+								LocalObjectReference: v1.LocalObjectReference{
+									Name: cm2.Name,
+								},
+							},
+						},
+					},
+				)
+				workerDS.Spec.Template.Spec.Containers[0].VolumeMounts = append(workerDS.Spec.Template.Spec.Containers[0].VolumeMounts,
+					v1.VolumeMount{
+						Name:      volumeName1,
+						ReadOnly:  true,
+						MountPath: filepath.Join(custom.Directory, "cm1"),
+					},
+					v1.VolumeMount{
+						Name:      volumeName2,
+						ReadOnly:  true,
+						MountPath: filepath.Join(custom.Directory, "cm2"),
+					},
+				)
+
+				workerDS, err = f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).Create(context.TODO(), workerDS, metav1.CreateOptions{})
+				Expect(err).NotTo(HaveOccurred())
+
+				By("Waiting for daemonset pods to be ready")
+				Expect(e2epod.WaitForPodsReady(f.ClientSet, f.Namespace.Name, workerDS.Spec.Template.Labels["name"], 5)).NotTo(HaveOccurred())
+
+				By("Getting target node and checking labels")
+				targetNode, err := f.ClientSet.CoreV1().Nodes().Get(context.TODO(), targetNodeName, metav1.GetOptions{})
+				Expect(err).ToNot(HaveOccurred())
+
+				labelFound := false
+				labelWildcardFound := false
+				labelNegativeFound := false
+				for k := range targetNode.Labels {
+					if strings.Contains(k, targetLabelName) {
+						if targetNode.Labels[k] == targetLabelValue {
+							labelFound = true
+						}
+					}
+					if strings.Contains(k, targetLabelNameWildcard) {
+						if targetNode.Labels[k] == targetLabelValueWildcard {
+							labelWildcardFound = true
+						}
+					}
+					if strings.Contains(k, targetLabelNameNegative) {
+						labelNegativeFound = true
+					}
+				}
+
+				Expect(labelFound).To(BeTrue(), "label not found!")
+				Expect(labelWildcardFound).To(BeTrue(), "label for wildcard nodename not found!")
+				Expect(labelNegativeFound).To(BeFalse(), "label for not existing nodename found!")
+
+				By("Deleting nfd-worker daemonset")
+				err = f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).Delete(context.TODO(), workerDS.ObjectMeta.Name, metav1.DeleteOptions{})
+				Expect(err).NotTo(HaveOccurred())
+
+				cleanupNode(f.ClientSet)
+			})
+		})
+
 	})
 
 })