Repository: kubernetes-sigs/node-feature-discovery
Pull request #528 (2021-05-18 09:05:31)

topology updater: add e2e tests
Builds on top of https://github.com/kubernetes-sigs/node-feature-discovery/pull/525 and add e2e tests for the new component.
Some refactoring was performed along the way to make room for the new tests; no intended changes in the behaviour of the existing tests.

Design Document: [here](https://docs.google.com/document/d/1Q-4wSu1tzmbOXyGk_2r5_mK6JdXXJA-bOd3cAtBFnwo/edit?ts=5f24171f#)
Issue: https://github.com/kubernetes-sigs/node-feature-discovery/issues/333
Documentation: https://github.com/kubernetes-sigs/node-feature-discovery/pull/526

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/528#" title="Author self-approved">fromanirh</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"]} -->
k8s-ci-robot: Welcome @fromanirh! <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 @fromanirh. 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>

fromanirh: reviewers please note the first 4 commits (up to `Expose Topology manager scope`, included) are part of https://github.com/kubernetes-sigs/node-feature-discovery/pull/525 , and are included here for PR consistency sake and to make CI pass.
swatisehgal: /cc @marquiz @ArangoGutierrez In case you would like to review the E2E tests corresponding to #525. 
k8s-ci-robot: @fromanirh: PR needs rebase.

<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-build-image | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-image/1438515080575586304) | `/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=528). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afromanirh). 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: @fromanirh: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-build-image | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-image/1438515080575586304) | `/test pull-node-feature-discovery-build-image`
pull-node-feature-discovery-build-gh-pages | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-gh-pages/1438515080609140736) | `/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=528). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afromanirh). 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: @fromanirh: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-build-image | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-image/1438515080575586304) | `/test pull-node-feature-discovery-build-image`
pull-node-feature-discovery-build-gh-pages | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-gh-pages/1438515080609140736) | `/test pull-node-feature-discovery-build-gh-pages`
pull-node-feature-discovery-verify-docs | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-verify-docs/1438515080516866048) | `/test pull-node-feature-discovery-verify-docs`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-build-image | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-image/1438515080575586304) | `/test pull-node-feature-discovery-build-image`
pull-node-feature-discovery-build-gh-pages | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-gh-pages/1438515080609140736) | `/test pull-node-feature-discovery-build-gh-pages`
pull-node-feature-discovery-verify-docs | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-verify-docs/1438515080516866048) | `/test pull-node-feature-discovery-verify-docs`
pull-node-feature-discovery-build | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build/1438515080542031872) | `/test pull-node-feature-discovery-build`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-node-feature-discovery-build-image | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-image/1438515080575586304) | `/test pull-node-feature-discovery-build-image`
pull-node-feature-discovery-build-gh-pages | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-gh-pages/1438515080609140736) | `/test pull-node-feature-discovery-build-gh-pages`
pull-node-feature-discovery-verify-docs | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-verify-docs/1438515080516866048) | `/test pull-node-feature-discovery-verify-docs`
pull-node-feature-discovery-build | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build/1438515080542031872) | `/test pull-node-feature-discovery-build`
pull-node-feature-discovery-verify | b3d281932cd0fa984ea298dc6430a2e146820663 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-verify/1438515080474923008) | `/test pull-node-feature-discovery-verify`

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-node-feature-discovery-verify | 7fee80119eae23cee0e1b75f428f5bf5dd946754 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-verify/1440278981709402112) | true | `/test pull-node-feature-discovery-verify`

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-node-feature-discovery-verify | 0f8437430a5ff5946b9fefcf4e457fa957fe02ac | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-verify/1440286173237874688) | true | `/test pull-node-feature-discovery-verify`

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-node-feature-discovery-verify | 42cfc1f9b046cd03aae7d2a05e677b156736bc85 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-verify/1440288578490863616) | true | `/test pull-node-feature-discovery-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=528). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afromanirh). 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: @fromanirh: 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>
fromanirh: Hey! just a quick update: being swamped with other things lately, but I will totally keep pushing this forward. On the upside, the code quality of the tests improved quite a lot and it should be reflected in the next respin of this PR, which will happen (as) soon (as possible). Sorry!
marquiz: Np, we're all swamped 😸  Thanks @fromanirh for the update and for working on this
k8s-ci-robot: @fromanirh: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-node-feature-discovery-build-image-cross | 9d1a6dbb4ebefa485ac0e2683202da4eedf78507 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-image-cross/1484460984918609920) | true | `/test pull-node-feature-discovery-build-image-cross`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=528). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afromanirh). 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 -->
ArangoGutierrez: @fromanirh any updates here? we would love to have better test coverage for next release cycle of NFD 
fromanirh: @ArangoGutierrez yeah, this is totally on my TODO (and I agree about having better coverage) but had little to none capacity to work in this area. I'll try harder to reprioritize this or next week and I'll let you folks know.
k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue or PR as fresh with `/remove-lifecycle stale`
- Mark this issue or PR as rotten with `/lifecycle rotten`
- Close this issue or PR with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
k8s-ci-robot: @fromanirh: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-node-feature-discovery-build-image-cross | 9d1a6dbb4ebefa485ac0e2683202da4eedf78507 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-build-image-cross/1484460984918609920) | true | `/test pull-node-feature-discovery-build-image-cross`
pull-node-feature-discovery-verify | 35104228ed6e4d64567c13e85bcc46b417160541 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-verify/1536640514852392960) | true | `/test pull-node-feature-discovery-verify`

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-node-feature-discovery-verify | 8c9c2a9adef043f794a02c76b029a6e4651ace08 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_node-feature-discovery/528/pull-node-feature-discovery-verify/1536671472028749824) | true | `/test pull-node-feature-discovery-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=node-feature-discovery&pr=528). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afromanirh). 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 -->
fromanirh: /hold
need to read the CRD manifest as requested in the review
fromanirh: /hold cancel
addressed all review comments
marquiz: Thanks @fromanirh for the update! Great to have this (just reminds me about missing e2e tests in my own backlog 🙄 ). I took a quick look and no comments based on that. Now I'm just trying to get the tests pass in our e2e cluster 🤪 After that's resolved I'll take a final look and let's get this merged
fromanirh: thanks @marquiz . There most likely a bug I need to address. I'll work more on that and keep you posted. I think we're not far from the ready state.
k8s-ci-robot: @fromanirh: 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>
fromanirh: > I did some debugging today. When is the last time you tried running these tests as there were typos etc that made it impossible for them to pass? wink I had a few comments but they are not enough, need more work...
> 
> Also, needs a rebase. Prolly better to wait for #839 before rebasing blush

Indeed, sorry about that: I should have marked WIP earlier. I need to do a deeper review to fix and update.
Again, sorry it take time of yours to run through the debug, I'll lift the WIP when I'm confident the tests are really up to date.
marquiz: @fromanirh any update on this one?
fromanirh: sure, going on work on this during this week
fromanirh: @marquiz *if* you agree with the refactoring of the e2e test utilities included in this PR, I split only that part in https://github.com/kubernetes-sigs/node-feature-discovery/pull/854. I think that if we like this direction, merging that part first makes sense and will also make the finalization of this PR easier. WDYT?
marquiz: > @marquiz _if_ you agree with the refactoring of the e2e test utilities included in this PR, I split only that part in #854. I think that if we like this direction, merging that part first makes sense and will also make the finalization of this PR easier. WDYT?

Yes, makes sense to me as well 👍

I think I had some comments in the utils part. Do you think you can address those in the separate PR?
fromanirh: > > @marquiz _if_ you agree with the refactoring of the e2e test utilities included in this PR, I split only that part in #854. I think that if we like this direction, merging that part first makes sense and will also make the finalization of this PR easier. WDYT?
> 
> Yes, makes sense to me as well +1
> 
> I think I had some comments in the utils part. Do you think you can address those in the separate PR?

absolutely!
fromanirh: Implemented all the changes. Still WIP because I need to verify locally. Not ready for review yet.
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/528#" title="Author self-approved">fromanirh</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign marquiz for approval by writing `/assign @marquiz` in a comment. For more information see:[The Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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"]} -->
fromanirh: the PR is now reivewable again, but depends on https://github.com/kubernetes-sigs/node-feature-discovery/pull/854
marquiz: We actually need to update our e2e-test-config in k8s test-infra before merging this (I'm on it)
/hold

Patch:
diff --git a/Dockerfile_generator b/Dockerfile_generator
index 02178bc4b..c0865e990 100644
--- a/Dockerfile_generator
+++ b/Dockerfile_generator
@@ -2,9 +2,9 @@ FROM golang:1.18-buster as builder
 
 # Install tools
 RUN go install github.com/vektra/mockery/v2@v2.13.0 && \
-    go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2 && \
-    git clone https://github.com/kubernetes/code-generator -b v0.24.3 --depth 1 && \
-    go install k8s.io/code-generator/cmd/...@v0.24.3 && \
+    go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.7.0 && \
+    git clone https://github.com/kubernetes/code-generator -b v0.20.7 --depth 1 && \
+    go install k8s.io/code-generator/cmd/...@v0.20.7 && \
     go install golang.org/x/tools/cmd/goimports@v0.1.1 && \
     go install github.com/golang/protobuf/protoc-gen-go@v1.4.3
 
diff --git a/Makefile b/Makefile
index 4f8562e39..5349f92e0 100644
--- a/Makefile
+++ b/Makefile
@@ -55,6 +55,7 @@ HOSTMOUNT_PREFIX ?= /
 
 KUBECONFIG ?=
 E2E_TEST_CONFIG ?=
+E2E_PULL_IF_NOT_PRESENT ?= false
 
 LDFLAGS = -ldflags "-s -w -X sigs.k8s.io/node-feature-discovery/pkg/version.version=$(VERSION) -X sigs.k8s.io/node-feature-discovery/source.pathPrefix=$(HOSTMOUNT_PREFIX)"
 
@@ -167,10 +168,16 @@ test:
 e2e-test:
 	@if [ -z ${KUBECONFIG} ]; then echo "[ERR] KUBECONFIG missing, must be defined"; exit 1; fi
 	$(GO_CMD) test -v ./test/e2e/ -args -nfd.repo=$(IMAGE_REPO) -nfd.tag=$(IMAGE_TAG_NAME) \
-	    -kubeconfig=$(KUBECONFIG) -nfd.e2e-config=$(E2E_TEST_CONFIG) -ginkgo.focus="\[kubernetes-sigs\]" \
+	    -kubeconfig=$(KUBECONFIG) \
+	    -nfd.e2e-config=$(E2E_TEST_CONFIG) \
+	    -nfd.pull-if-not-present=$(E2E_PULL_IF_NOT_PRESENT) \
+	    -ginkgo.focus="\[kubernetes-sigs\]" \
 	    $(if $(OPENSHIFT),-nfd.openshift,)
 	$(GO_CMD) test -v ./test/e2e/ -args -nfd.repo=$(IMAGE_REPO) -nfd.tag=$(IMAGE_TAG_NAME)-minimal \
-	    -kubeconfig=$(KUBECONFIG) -nfd.e2e-config=$(E2E_TEST_CONFIG) -ginkgo.focus="\[kubernetes-sigs\]" \
+	    -kubeconfig=$(KUBECONFIG) \
+	    -nfd.e2e-config=$(E2E_TEST_CONFIG) \
+	    -nfd.pull-if-not-present=$(E2E_PULL_IF_NOT_PRESENT) \
+	    -ginkgo.focus="\[kubernetes-sigs\]" \
 	    $(if $(OPENSHIFT),-nfd.openshift,)
 
 push:
diff --git a/deployment/base/nfd-crds/nodefeaturerule-crd.yaml b/deployment/base/nfd-crds/nodefeaturerule-crd.yaml
index c40af9119..cd9b48bf4 100644
--- a/deployment/base/nfd-crds/nodefeaturerule-crd.yaml
+++ b/deployment/base/nfd-crds/nodefeaturerule-crd.yaml
@@ -1,9 +1,10 @@
+
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.9.2
+    controller-gen.kubebuilder.io/version: v0.7.0
   creationTimestamp: null
   name: nodefeaturerules.nfd.k8s-sigs.io
 spec:
@@ -78,9 +79,9 @@ spec:
                                       the input and an array of values that the operator
                                       evaluates the input against. \n NB: CreateMatchExpression
                                       or MustCreateMatchExpression() should be used
-                                      for creating new instances. NB: Validate() must
-                                      be called if Op or Value fields are modified
-                                      or if a new instance is created from scratch
+                                      for     creating new instances. NB: Validate()
+                                      must be called if Op or Value fields are modified
+                                      or if a new     instance is created from scratch
                                       without using the helper functions."
                                     properties:
                                       op:
@@ -143,9 +144,9 @@ spec:
                                 an operator that is applied when matching the input
                                 and an array of values that the operator evaluates
                                 the input against. \n NB: CreateMatchExpression or
-                                MustCreateMatchExpression() should be used for creating
+                                MustCreateMatchExpression() should be used for     creating
                                 new instances. NB: Validate() must be called if Op
-                                or Value fields are modified or if a new instance
+                                or Value fields are modified or if a new     instance
                                 is created from scratch without using the helper functions."
                               properties:
                                 op:
@@ -214,3 +215,9 @@ spec:
         type: object
     served: true
     storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/deployment/helm/node-feature-discovery/manifests/nodefeaturerule-crd.yaml b/deployment/helm/node-feature-discovery/manifests/nodefeaturerule-crd.yaml
index c40af9119..cd9b48bf4 100644
--- a/deployment/helm/node-feature-discovery/manifests/nodefeaturerule-crd.yaml
+++ b/deployment/helm/node-feature-discovery/manifests/nodefeaturerule-crd.yaml
@@ -1,9 +1,10 @@
+
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.9.2
+    controller-gen.kubebuilder.io/version: v0.7.0
   creationTimestamp: null
   name: nodefeaturerules.nfd.k8s-sigs.io
 spec:
@@ -78,9 +79,9 @@ spec:
                                       the input and an array of values that the operator
                                       evaluates the input against. \n NB: CreateMatchExpression
                                       or MustCreateMatchExpression() should be used
-                                      for creating new instances. NB: Validate() must
-                                      be called if Op or Value fields are modified
-                                      or if a new instance is created from scratch
+                                      for     creating new instances. NB: Validate()
+                                      must be called if Op or Value fields are modified
+                                      or if a new     instance is created from scratch
                                       without using the helper functions."
                                     properties:
                                       op:
@@ -143,9 +144,9 @@ spec:
                                 an operator that is applied when matching the input
                                 and an array of values that the operator evaluates
                                 the input against. \n NB: CreateMatchExpression or
-                                MustCreateMatchExpression() should be used for creating
+                                MustCreateMatchExpression() should be used for     creating
                                 new instances. NB: Validate() must be called if Op
-                                or Value fields are modified or if a new instance
+                                or Value fields are modified or if a new     instance
                                 is created from scratch without using the helper functions."
                               properties:
                                 op:
@@ -214,3 +215,9 @@ spec:
         type: object
     served: true
     storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/docs/advanced/developer-guide.md b/docs/advanced/developer-guide.md
index d320d06c6..b154e552b 100644
--- a/docs/advanced/developer-guide.md
+++ b/docs/advanced/developer-guide.md
@@ -124,6 +124,7 @@ makefile overrides.
 | K8S_NAMESPACE              | nfd-master and nfd-worker namespace                               | node-feature-discovery
 | KUBECONFIG                 | Kubeconfig for running e2e-tests                                  | *empty*
 | E2E_TEST_CONFIG            | Parameterization file of e2e-tests (see [example][e2e-config-sample]) | *empty*
+| E2E_PULL_IF_NOT_PRESENT    | True-ish value makes the image pull policy IfNotPresent (to be used only in e2e tests) | false
 | OPENSHIFT                  | Non-empty value enables OpenShift specific support (currently only effective in e2e tests) | *empty*
 | BASE_IMAGE_FULL            | Container base image for target image full (--target full)        | debian:buster-slim
 | BASE_IMAGE_MINIMAL         | Container base image for target image minimal (--target minimal)  | gcr.io/distroless/base
diff --git a/go.mod b/go.mod
index 0b18f3e9c..894448a55 100644
--- a/go.mod
+++ b/go.mod
@@ -21,12 +21,14 @@ require (
 	google.golang.org/grpc v1.40.0
 	google.golang.org/protobuf v1.27.1
 	k8s.io/api v0.24.2
+	k8s.io/apiextensions-apiserver v0.0.0
 	k8s.io/apimachinery v0.24.2
 	k8s.io/client-go v0.24.2
 	k8s.io/klog/v2 v2.60.1
 	k8s.io/kubectl v0.24.2
 	k8s.io/kubelet v0.24.2
 	k8s.io/kubernetes v1.24.2
+	k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
 	sigs.k8s.io/yaml v1.2.0
 )
 
@@ -180,7 +182,6 @@ require (
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 	howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
-	k8s.io/apiextensions-apiserver v0.0.0 // indirect
 	k8s.io/apiserver v0.24.2 // indirect
 	k8s.io/cloud-provider v0.24.2 // indirect
 	k8s.io/component-base v0.24.2 // indirect
@@ -193,7 +194,6 @@ require (
 	k8s.io/legacy-cloud-providers v0.0.0 // indirect
 	k8s.io/mount-utils v0.24.2 // indirect
 	k8s.io/pod-security-admission v0.0.0 // indirect
-	k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
 	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 // indirect
 	sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
diff --git a/pkg/api/feature/generated.pb.go b/pkg/api/feature/generated.pb.go
index 4b9c8556b..cfe7c772b 100644
--- a/pkg/api/feature/generated.pb.go
+++ b/pkg/api/feature/generated.pb.go
@@ -230,38 +230,38 @@ func init() { proto.RegisterFile("feature/generated.proto", fileDescriptor_870e6
 
 var fileDescriptor_870e6eafce4d2482 = []byte{
 	// 503 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x92, 0xc1, 0x6e, 0xd3, 0x40,
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x94, 0xc1, 0x6e, 0xd3, 0x40,
 	0x10, 0x86, 0xbd, 0x71, 0x5b, 0xea, 0x49, 0x9b, 0x94, 0x15, 0x02, 0xcb, 0x48, 0xdb, 0x10, 0x50,
-	0x95, 0x03, 0xac, 0x45, 0x4f, 0x08, 0xc4, 0x81, 0x40, 0x2b, 0x55, 0x91, 0x8a, 0xe4, 0x22, 0x10,
-	0xa8, 0x17, 0xa7, 0x6c, 0x83, 0xd5, 0xc4, 0x46, 0xf6, 0x1a, 0xc9, 0x37, 0x1e, 0x81, 0x87, 0xe0,
-	0xcc, 0x73, 0xe4, 0xd8, 0x63, 0x4f, 0x15, 0x31, 0xaf, 0xc1, 0x01, 0x65, 0xbd, 0xde, 0xd8, 0x89,
-	0x0d, 0xe2, 0x66, 0xef, 0xfe, 0xff, 0x37, 0xff, 0xcc, 0x0e, 0xdc, 0x39, 0x67, 0x2e, 0x8f, 0x43,
-	0x66, 0x8f, 0x98, 0xcf, 0x42, 0x97, 0xb3, 0x8f, 0xf4, 0x73, 0x18, 0xf0, 0x00, 0xdf, 0x90, 0x17,
-	0xd6, 0xa3, 0x91, 0xc7, 0x3f, 0xc5, 0x43, 0x7a, 0x16, 0x4c, 0xec, 0x51, 0x30, 0x0a, 0x6c, 0x71,
-	0x3f, 0x8c, 0xcf, 0xc5, 0x9f, 0xf8, 0x11, 0x5f, 0x99, 0xaf, 0xfb, 0x5b, 0x87, 0xd6, 0xab, 0x60,
-	0xe2, 0x7a, 0xfe, 0x61, 0x06, 0x88, 0xf0, 0x4b, 0x58, 0xbb, 0x60, 0x49, 0x64, 0xa2, 0x8e, 0xde,
-	0x6b, 0xee, 0xdf, 0xa3, 0x92, 0x4c, 0xcb, 0x32, 0x3a, 0x60, 0x49, 0x74, 0xe0, 0xf3, 0x30, 0xe9,
-	0x6f, 0x4d, 0xaf, 0x77, 0xb5, 0xf4, 0x7a, 0x77, 0x6d, 0x7e, 0xe4, 0x08, 0x33, 0x1e, 0xc0, 0xc6,
-	0x17, 0x77, 0x1c, 0xb3, 0xc8, 0x6c, 0x08, 0xcc, 0xfd, 0x3a, 0xcc, 0x5b, 0xa1, 0xca, 0x40, 0x2d,
-	0x09, 0xda, 0xc8, 0x0e, 0x1d, 0x89, 0xc0, 0xef, 0xc0, 0xf0, 0xfc, 0x88, 0xbb, 0xfe, 0x19, 0x8b,
-	0x4c, 0x5d, 0xf0, 0xf6, 0xea, 0x78, 0x47, 0xb9, 0x30, 0x43, 0xde, 0x94, 0x48, 0x43, 0x9d, 0x3b,
-	0x0b, 0x96, 0xf5, 0x1a, 0x0c, 0xd5, 0x06, 0xde, 0x01, 0xfd, 0x82, 0x25, 0x26, 0xea, 0xa0, 0x9e,
-	0xe1, 0xcc, 0x3f, 0xf1, 0x43, 0x58, 0x17, 0x09, 0xcc, 0x46, 0x07, 0xf5, 0x9a, 0xfb, 0xb7, 0x55,
-	0xcd, 0x01, 0x4b, 0x64, 0xc1, 0x13, 0xc6, 0x9d, 0x4c, 0xf4, 0xb4, 0xf1, 0x04, 0x59, 0x27, 0xd0,
-	0x2c, 0x34, 0x54, 0x81, 0xa4, 0x65, 0xa4, 0xa9, 0x90, 0xc2, 0x56, 0x0d, 0x7d, 0x0f, 0xad, 0x72,
-	0x57, 0x15, 0xdc, 0xc7, 0x65, 0xee, 0x5d, 0xc5, 0xcd, 0x9d, 0x95, 0xe8, 0xee, 0x0f, 0x04, 0xed,
-	0x25, 0x05, 0x3e, 0x05, 0x70, 0x39, 0x0f, 0xbd, 0x61, 0xcc, 0x59, 0xbe, 0x05, 0xbd, 0x3a, 0x1e,
-	0x7d, 0xa1, 0xa4, 0xd9, 0xc0, 0xb1, 0x1c, 0x38, 0x2c, 0x2e, 0x9c, 0x02, 0xcf, 0x7a, 0x0e, 0xed,
-	0x25, 0x4b, 0x45, 0x37, 0xb7, 0x8a, 0xdd, 0x18, 0xc5, 0xc0, 0xa7, 0x80, 0x57, 0x3b, 0xc2, 0x87,
-	0xb0, 0xc9, 0xc6, 0x6c, 0xc2, 0x7c, 0x9e, 0x07, 0x36, 0xeb, 0x02, 0xf7, 0x77, 0x64, 0xc0, 0xcd,
-	0x03, 0xe9, 0x70, 0x94, 0x77, 0x3e, 0x8e, 0xed, 0xd2, 0xdb, 0x62, 0x67, 0x85, 0xfc, 0xa0, 0x7a,
-	0x0b, 0x68, 0x8e, 0xcb, 0xc6, 0xf0, 0x97, 0x2a, 0xd6, 0x11, 0x6c, 0x97, 0xc4, 0x15, 0x03, 0xe8,
-	0x96, 0x9f, 0x73, 0x4b, 0xd5, 0x3c, 0xf6, 0xc6, 0xc5, 0x71, 0xac, 0x83, 0x7e, 0xec, 0x8d, 0xbb,
-	0xdf, 0x11, 0xb4, 0x97, 0x16, 0x08, 0xbf, 0x59, 0x49, 0xbe, 0x57, 0xb7, 0x6c, 0xff, 0x91, 0xfd,
-	0xd9, 0xbf, 0xb3, 0xd7, 0x3e, 0x5e, 0xdf, 0x9e, 0xce, 0x88, 0x76, 0x39, 0x23, 0xda, 0xd5, 0x8c,
-	0x68, 0x5f, 0x53, 0x82, 0xa6, 0x29, 0x41, 0x97, 0x29, 0x41, 0x57, 0x29, 0x41, 0x3f, 0x53, 0x82,
-	0xbe, 0xfd, 0x22, 0xda, 0x07, 0x83, 0xda, 0x32, 0xe7, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x83,
-	0x7e, 0xe5, 0x1f, 0xef, 0x04, 0x00, 0x00,
+	0x95, 0x03, 0x75, 0x44, 0x4f, 0x08, 0xc4, 0x81, 0x40, 0x2b, 0x55, 0x91, 0x8a, 0xe4, 0x22, 0x10,
+	0xa8, 0x17, 0xa7, 0x4c, 0x83, 0x55, 0xc7, 0x46, 0xf6, 0x1a, 0xc9, 0x37, 0x1e, 0x81, 0x87, 0xe0,
+	0xcc, 0x73, 0xe4, 0xd8, 0x63, 0x4f, 0x15, 0x31, 0xaf, 0xc1, 0x01, 0x65, 0xbd, 0x76, 0xed, 0xc4,
+	0x06, 0xf5, 0xe6, 0xdd, 0xfd, 0xff, 0x6f, 0xff, 0x99, 0x1d, 0x19, 0xee, 0x9d, 0xa1, 0xcd, 0xa3,
+	0x00, 0xfb, 0x63, 0xf4, 0x30, 0xb0, 0x39, 0x7e, 0x32, 0xbf, 0x04, 0x3e, 0xf7, 0xe9, 0x2d, 0x79,
+	0x60, 0xec, 0x8e, 0x1d, 0xfe, 0x39, 0x1a, 0x99, 0xa7, 0xfe, 0xa4, 0x3f, 0xf6, 0xc7, 0x7e, 0x5f,
+	0x9c, 0x8f, 0xa2, 0x33, 0xb1, 0x12, 0x0b, 0xf1, 0x95, 0xfa, 0xba, 0x7f, 0x54, 0x68, 0xbd, 0xf6,
+	0x27, 0xb6, 0xe3, 0x1d, 0xa4, 0x80, 0x90, 0xbe, 0x82, 0x95, 0x73, 0x8c, 0x43, 0x9d, 0x74, 0xd4,
+	0x5e, 0x73, 0xef, 0x81, 0x29, 0xc9, 0x66, 0x59, 0x66, 0x0e, 0x31, 0x0e, 0xf7, 0x3d, 0x1e, 0xc4,
+	0x83, 0x8d, 0xe9, 0xd5, 0xb6, 0x92, 0x5c, 0x6d, 0xaf, 0xcc, 0xb7, 0x2c, 0x61, 0xa6, 0x43, 0x58,
+	0xfb, 0x6a, 0xbb, 0x11, 0x86, 0x7a, 0x43, 0x60, 0x1e, 0xd6, 0x61, 0xde, 0x09, 0x55, 0x0a, 0x6a,
+	0x49, 0xd0, 0x5a, 0xba, 0x69, 0x49, 0x04, 0x7d, 0x0f, 0x9a, 0xe3, 0x85, 0xdc, 0xf6, 0x4e, 0x31,
+	0xd4, 0x55, 0xc1, 0xdb, 0xa9, 0xe3, 0x1d, 0x66, 0xc2, 0x14, 0x79, 0x5b, 0x22, 0xb5, 0x7c, 0xdf,
+	0xba, 0x66, 0x19, 0x6f, 0x40, 0xcb, 0xcb, 0xa0, 0x5b, 0xa0, 0x9e, 0x63, 0xac, 0x93, 0x0e, 0xe9,
+	0x69, 0xd6, 0xfc, 0x93, 0x3e, 0x86, 0x55, 0x91, 0x40, 0x6f, 0x74, 0x48, 0xaf, 0xb9, 0x77, 0x37,
+	0xbf, 0x73, 0x88, 0xb1, 0xbc, 0xf0, 0x18, 0xb9, 0x95, 0x8a, 0x9e, 0x35, 0x9e, 0x12, 0xe3, 0x18,
+	0x9a, 0x85, 0x82, 0x2a, 0x90, 0x66, 0x19, 0xa9, 0xe7, 0x48, 0x61, 0xab, 0x86, 0x7e, 0x80, 0x56,
+	0xb9, 0xaa, 0x0a, 0xee, 0x93, 0x32, 0xf7, 0x7e, 0xce, 0xcd, 0x9c, 0x95, 0xe8, 0xee, 0x4f, 0x02,
+	0xed, 0x05, 0x05, 0x3d, 0x01, 0xb0, 0x39, 0x0f, 0x9c, 0x51, 0xc4, 0x31, 0x9b, 0x82, 0x5e, 0x1d,
+	0xcf, 0x7c, 0x99, 0x4b, 0xd3, 0x86, 0x53, 0xd9, 0x70, 0xb8, 0x3e, 0xb0, 0x0a, 0x3c, 0xe3, 0x05,
+	0xb4, 0x17, 0x2c, 0x15, 0xd5, 0xdc, 0x29, 0x56, 0xa3, 0x15, 0x03, 0x9f, 0x00, 0x5d, 0xae, 0x88,
+	0x1e, 0xc0, 0x3a, 0xba, 0x38, 0x41, 0x8f, 0x67, 0x81, 0xf5, 0xba, 0xc0, 0x83, 0x2d, 0x19, 0x70,
+	0x7d, 0x5f, 0x3a, 0xac, 0xdc, 0x3b, 0x6f, 0xc7, 0x66, 0xe9, 0x6d, 0xa9, 0xb5, 0x44, 0x7e, 0x54,
+	0x3d, 0x05, 0x66, 0x86, 0x4b, 0xdb, 0xf0, 0x8f, 0x5b, 0x8c, 0x43, 0xd8, 0x2c, 0x89, 0x2b, 0x1a,
+	0xd0, 0x2d, 0x3f, 0xe7, 0x46, 0x7e, 0xe7, 0x91, 0xe3, 0x16, 0xdb, 0xb1, 0x0a, 0xea, 0x91, 0xe3,
+	0x76, 0x7f, 0x10, 0x68, 0x2f, 0x0c, 0x10, 0x7d, 0xbb, 0x94, 0x7c, 0xa7, 0x6e, 0xd8, 0x6e, 0x90,
+	0xfd, 0xf9, 0xff, 0xb3, 0xd7, 0x3e, 0xde, 0x60, 0x77, 0x3a, 0x63, 0xca, 0xc5, 0x8c, 0x29, 0x97,
+	0x33, 0xa6, 0x7c, 0x4b, 0x18, 0x99, 0x26, 0x8c, 0x5c, 0x24, 0x8c, 0x5c, 0x26, 0x8c, 0xfc, 0x4a,
+	0x18, 0xf9, 0xfe, 0x9b, 0x29, 0x1f, 0xb3, 0x5f, 0xd9, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x1f,
+	0x6e, 0x71, 0x14, 0xed, 0x04, 0x00, 0x00,
 }
 
 func (m *DomainFeatures) Marshal() (dAtA []byte, err error) {
diff --git a/pkg/apis/nfd/v1alpha1/expression.go b/pkg/apis/nfd/v1alpha1/expression.go
index b0e1f5884..41a032b40 100644
--- a/pkg/apis/nfd/v1alpha1/expression.go
+++ b/pkg/apis/nfd/v1alpha1/expression.go
@@ -45,6 +45,16 @@ var matchOps = map[MatchOp]struct{}{
 
 type valueRegexpCache []*regexp.Regexp
 
+// NewMatchExpressionSet returns a new MatchExpressionSet instance.
+func NewMatchExpressionSet() *MatchExpressionSet {
+	return &MatchExpressionSet{Expressions: make(Expressions)}
+}
+
+// Len returns the number of expressions.
+func (e *Expressions) Len() int {
+	return len(*e)
+}
+
 // CreateMatchExpression creates a new MatchExpression instance. Returns an
 // error if validation fails.
 func CreateMatchExpression(op MatchOp, values ...string) (*MatchExpression, error) {
@@ -319,9 +329,9 @@ type MatchedKey struct {
 // an empty MatchExpressionSet and an empty set of keys returns an empty slice
 // which is not nil and is treated as a match.
 func (m *MatchExpressionSet) MatchGetKeys(keys map[string]feature.Nil) (bool, []MatchedKey, error) {
-	ret := make([]MatchedKey, 0, len(*m))
+	ret := make([]MatchedKey, 0, m.Len())
 
-	for n, e := range *m {
+	for n, e := range (*m).Expressions {
 		match, err := e.MatchKeys(n, keys)
 		if err != nil {
 			return false, nil, err
@@ -354,9 +364,9 @@ type MatchedValue struct {
 // MatchExpressionSet and an empty set of values returns an empty non-nil map
 // which is treated as a match.
 func (m *MatchExpressionSet) MatchGetValues(values map[string]string) (bool, []MatchedValue, error) {
-	ret := make([]MatchedValue, 0, len(*m))
+	ret := make([]MatchedValue, 0, m.Len())
 
-	for n, e := range *m {
+	for n, e := range (*m).Expressions {
 		match, err := e.MatchValues(n, values)
 		if err != nil {
 			return false, nil, err
@@ -401,7 +411,7 @@ func (m *MatchExpressionSet) MatchGetInstances(instances []feature.InstanceFeatu
 
 // UnmarshalJSON implements the Unmarshaler interface of "encoding/json".
 func (m *MatchExpressionSet) UnmarshalJSON(data []byte) error {
-	*m = MatchExpressionSet{}
+	*m = *NewMatchExpressionSet()
 
 	names := make([]string, 0)
 	if err := json.Unmarshal(data, &names); err == nil {
@@ -409,9 +419,9 @@ func (m *MatchExpressionSet) UnmarshalJSON(data []byte) error {
 		for _, name := range names {
 			split := strings.SplitN(name, "=", 2)
 			if len(split) == 1 {
-				(*m)[split[0]] = newMatchExpression(MatchExists)
+				(*m).Expressions[split[0]] = newMatchExpression(MatchExists)
 			} else {
-				(*m)[split[0]] = newMatchExpression(MatchIn, split[1])
+				(*m).Expressions[split[0]] = newMatchExpression(MatchIn, split[1])
 			}
 		}
 	} else {
@@ -422,9 +432,9 @@ func (m *MatchExpressionSet) UnmarshalJSON(data []byte) error {
 		}
 		for k, v := range expressions {
 			if v != nil {
-				(*m)[k] = v
+				(*m).Expressions[k] = v
 			} else {
-				(*m)[k] = newMatchExpression(MatchExists)
+				(*m).Expressions[k] = newMatchExpression(MatchExists)
 			}
 		}
 	}
diff --git a/pkg/apis/nfd/v1alpha1/types.go b/pkg/apis/nfd/v1alpha1/types.go
index dafdaf851..b8982b49e 100644
--- a/pkg/apis/nfd/v1alpha1/types.go
+++ b/pkg/apis/nfd/v1alpha1/types.go
@@ -111,9 +111,12 @@ type FeatureMatcherTerm struct {
 
 // MatchExpressionSet contains a set of MatchExpressions, each of which is
 // evaluated against a set of input values.
-type MatchExpressionSet map[string]*MatchExpression
+type MatchExpressionSet struct {
+	Expressions `json:",inline"`
+}
 
 // Expressions is a helper type to work around issues with k8s deepcopy-gen
+type Expressions map[string]*MatchExpression
 
 // MatchExpression specifies an expression to evaluate against a set of input
 // values. It contains an operator that is applied when matching the input and
diff --git a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
index ff8f19f12..ece20769a 100644
--- a/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/nfd/v1alpha1/zz_generated.deepcopy.go
@@ -9,6 +9,35 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in Expressions) DeepCopyInto(out *Expressions) {
+	{
+		in := &in
+		*out = make(Expressions, len(*in))
+		for key, val := range *in {
+			var outVal *MatchExpression
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = new(MatchExpression)
+				(*in).DeepCopyInto(*out)
+			}
+			(*out)[key] = outVal
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Expressions.
+func (in Expressions) DeepCopy() Expressions {
+	if in == nil {
+		return nil
+	}
+	out := new(Expressions)
+	in.DeepCopyInto(out)
+	return *out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in FeatureMatcher) DeepCopyInto(out *FeatureMatcher) {
 	{
@@ -33,21 +62,7 @@ func (in FeatureMatcher) DeepCopy() FeatureMatcher {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *FeatureMatcherTerm) DeepCopyInto(out *FeatureMatcherTerm) {
 	*out = *in
-	if in.MatchExpressions != nil {
-		in, out := &in.MatchExpressions, &out.MatchExpressions
-		*out = make(MatchExpressionSet, len(*in))
-		for key, val := range *in {
-			var outVal *MatchExpression
-			if val == nil {
-				(*out)[key] = nil
-			} else {
-				in, out := &val, &outVal
-				*out = new(MatchExpression)
-				(*in).DeepCopyInto(*out)
-			}
-			(*out)[key] = outVal
-		}
-	}
+	in.MatchExpressions.DeepCopyInto(&out.MatchExpressions)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureMatcherTerm.
@@ -104,10 +119,11 @@ func (in *MatchExpression) DeepCopy() *MatchExpression {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in MatchExpressionSet) DeepCopyInto(out *MatchExpressionSet) {
-	{
-		in := &in
-		*out = make(MatchExpressionSet, len(*in))
+func (in *MatchExpressionSet) DeepCopyInto(out *MatchExpressionSet) {
+	*out = *in
+	if in.Expressions != nil {
+		in, out := &in.Expressions, &out.Expressions
+		*out = make(Expressions, len(*in))
 		for key, val := range *in {
 			var outVal *MatchExpression
 			if val == nil {
@@ -123,13 +139,13 @@ func (in MatchExpressionSet) DeepCopyInto(out *MatchExpressionSet) {
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchExpressionSet.
-func (in MatchExpressionSet) DeepCopy() MatchExpressionSet {
+func (in *MatchExpressionSet) DeepCopy() *MatchExpressionSet {
 	if in == nil {
 		return nil
 	}
 	out := new(MatchExpressionSet)
 	in.DeepCopyInto(out)
-	return *out
+	return out
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
diff --git a/pkg/generated/clientset/versioned/clientset.go b/pkg/generated/clientset/versioned/clientset.go
index e61b0938b..75330a9d9 100644
--- a/pkg/generated/clientset/versioned/clientset.go
+++ b/pkg/generated/clientset/versioned/clientset.go
@@ -20,7 +20,6 @@ package versioned
 
 import (
 	"fmt"
-	"net/http"
 
 	discovery "k8s.io/client-go/discovery"
 	rest "k8s.io/client-go/rest"
@@ -56,45 +55,22 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface {
 // NewForConfig creates a new Clientset for the given config.
 // If config's RateLimiter is not set and QPS and Burst are acceptable,
 // NewForConfig will generate a rate-limiter in configShallowCopy.
-// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
-// where httpClient was generated with rest.HTTPClientFor(c).
 func NewForConfig(c *rest.Config) (*Clientset, error) {
 	configShallowCopy := *c
-
-	if configShallowCopy.UserAgent == "" {
-		configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent()
-	}
-
-	// share the transport between all clients
-	httpClient, err := rest.HTTPClientFor(&configShallowCopy)
-	if err != nil {
-		return nil, err
-	}
-
-	return NewForConfigAndClient(&configShallowCopy, httpClient)
-}
-
-// NewForConfigAndClient creates a new Clientset for the given config and http client.
-// Note the http client provided takes precedence over the configured transport values.
-// If config's RateLimiter is not set and QPS and Burst are acceptable,
-// NewForConfigAndClient will generate a rate-limiter in configShallowCopy.
-func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) {
-	configShallowCopy := *c
 	if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
 		if configShallowCopy.Burst <= 0 {
 			return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0")
 		}
 		configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
 	}
-
 	var cs Clientset
 	var err error
-	cs.nfdV1alpha1, err = nfdv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient)
+	cs.nfdV1alpha1, err = nfdv1alpha1.NewForConfig(&configShallowCopy)
 	if err != nil {
 		return nil, err
 	}
 
-	cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient)
+	cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
 	if err != nil {
 		return nil, err
 	}
@@ -104,11 +80,11 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset,
 // NewForConfigOrDie creates a new Clientset for the given config and
 // panics if there is an error in the config.
 func NewForConfigOrDie(c *rest.Config) *Clientset {
-	cs, err := NewForConfig(c)
-	if err != nil {
-		panic(err)
-	}
-	return cs
+	var cs Clientset
+	cs.nfdV1alpha1 = nfdv1alpha1.NewForConfigOrDie(c)
+
+	cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
+	return &cs
 }
 
 // New creates a new Clientset for the given RESTClient.
diff --git a/pkg/generated/clientset/versioned/fake/clientset_generated.go b/pkg/generated/clientset/versioned/fake/clientset_generated.go
index 9ba9e8390..a38a480cf 100644
--- a/pkg/generated/clientset/versioned/fake/clientset_generated.go
+++ b/pkg/generated/clientset/versioned/fake/clientset_generated.go
@@ -74,10 +74,7 @@ func (c *Clientset) Tracker() testing.ObjectTracker {
 	return c.tracker
 }
 
-var (
-	_ clientset.Interface = &Clientset{}
-	_ testing.FakeClient  = &Clientset{}
-)
+var _ clientset.Interface = &Clientset{}
 
 // NfdV1alpha1 retrieves the NfdV1alpha1Client
 func (c *Clientset) NfdV1alpha1() nfdv1alpha1.NfdV1alpha1Interface {
diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nodefeaturerule.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nodefeaturerule.go
index 158ff71a1..b4afd87f9 100644
--- a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nodefeaturerule.go
+++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/fake/fake_nodefeaturerule.go
@@ -99,7 +99,7 @@ func (c *FakeNodeFeatureRules) Update(ctx context.Context, nodeFeatureRule *v1al
 // Delete takes name of the nodeFeatureRule and deletes it. Returns an error if one occurs.
 func (c *FakeNodeFeatureRules) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewRootDeleteActionWithOptions(nodefeaturerulesResource, name, opts), &v1alpha1.NodeFeatureRule{})
+		Invokes(testing.NewRootDeleteAction(nodefeaturerulesResource, name), &v1alpha1.NodeFeatureRule{})
 	return err
 }
 
diff --git a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go
index 034d5cbe7..653f75df2 100644
--- a/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go
+++ b/pkg/generated/clientset/versioned/typed/nfd/v1alpha1/nfd_client.go
@@ -19,8 +19,6 @@ limitations under the License.
 package v1alpha1
 
 import (
-	"net/http"
-
 	rest "k8s.io/client-go/rest"
 	v1alpha1 "sigs.k8s.io/node-feature-discovery/pkg/apis/nfd/v1alpha1"
 	"sigs.k8s.io/node-feature-discovery/pkg/generated/clientset/versioned/scheme"
@@ -41,28 +39,12 @@ func (c *NfdV1alpha1Client) NodeFeatureRules() NodeFeatureRuleInterface {
 }
 
 // NewForConfig creates a new NfdV1alpha1Client for the given config.
-// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
-// where httpClient was generated with rest.HTTPClientFor(c).
 func NewForConfig(c *rest.Config) (*NfdV1alpha1Client, error) {
 	config := *c
 	if err := setConfigDefaults(&config); err != nil {
 		return nil, err
 	}
-	httpClient, err := rest.HTTPClientFor(&config)
-	if err != nil {
-		return nil, err
-	}
-	return NewForConfigAndClient(&config, httpClient)
-}
-
-// NewForConfigAndClient creates a new NfdV1alpha1Client for the given config and http client.
-// Note the http client provided takes precedence over the configured transport values.
-func NewForConfigAndClient(c *rest.Config, h *http.Client) (*NfdV1alpha1Client, error) {
-	config := *c
-	if err := setConfigDefaults(&config); err != nil {
-		return nil, err
-	}
-	client, err := rest.RESTClientForConfigAndClient(&config, h)
+	client, err := rest.RESTClientFor(&config)
 	if err != nil {
 		return nil, err
 	}
diff --git a/source/custom/static_features.go b/source/custom/static_features.go
index fed8a22c5..db1856c9f 100644
--- a/source/custom/static_features.go
+++ b/source/custom/static_features.go
@@ -32,7 +32,9 @@ func getStaticFeatureConfig() []CustomRule {
 					{
 						PciID: &rules.PciIDRule{
 							MatchExpressionSet: nfdv1alpha1.MatchExpressionSet{
-								"vendor": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "15b3"),
+								Expressions: nfdv1alpha1.Expressions{
+									"vendor": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchIn, "15b3"),
+								},
 							},
 						},
 					},
@@ -46,8 +48,10 @@ func getStaticFeatureConfig() []CustomRule {
 					{
 						LoadedKMod: &rules.LoadedKModRule{
 							MatchExpressionSet: nfdv1alpha1.MatchExpressionSet{
-								"ib_uverbs": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists),
-								"rdma_ucm":  nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists),
+								Expressions: nfdv1alpha1.Expressions{
+									"ib_uverbs": nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists),
+									"rdma_ucm":  nfdv1alpha1.MustCreateMatchExpression(nfdv1alpha1.MatchExists),
+								},
 							},
 						},
 					},


Test patch:
diff --git a/pkg/apis/nfd/v1alpha1/rule_test.go b/pkg/apis/nfd/v1alpha1/rule_test.go
index 144999cdb..87d9c7b3b 100644
--- a/pkg/apis/nfd/v1alpha1/rule_test.go
+++ b/pkg/apis/nfd/v1alpha1/rule_test.go
@@ -33,7 +33,7 @@ func TestRule(t *testing.T) {
 			FeatureMatcherTerm{
 				Feature: "domain-1.kf-1",
 				MatchExpressions: MatchExpressionSet{
-					"key-1": MustCreateMatchExpression(MatchExists),
+					Expressions: Expressions{"key-1": MustCreateMatchExpression(MatchExists)},
 				},
 			},
 		},
@@ -111,7 +111,7 @@ func TestRule(t *testing.T) {
 			FeatureMatcherTerm{
 				Feature: "domain-1.vf-1",
 				MatchExpressions: MatchExpressionSet{
-					"key-1": MustCreateMatchExpression(MatchIn, "val-1"),
+					Expressions: Expressions{"key-1": MustCreateMatchExpression(MatchIn, "val-1")},
 				},
 			},
 		},
@@ -132,7 +132,7 @@ func TestRule(t *testing.T) {
 			FeatureMatcherTerm{
 				Feature: "domain-1.if-1",
 				MatchExpressions: MatchExpressionSet{
-					"attr-1": MustCreateMatchExpression(MatchIn, "val-1"),
+					Expressions: Expressions{"attr-1": MustCreateMatchExpression(MatchIn, "val-1")},
 				},
 			},
 		},
@@ -153,13 +153,13 @@ func TestRule(t *testing.T) {
 			FeatureMatcherTerm{
 				Feature: "domain-1.vf-1",
 				MatchExpressions: MatchExpressionSet{
-					"key-1": MustCreateMatchExpression(MatchIn, "val-x"),
+					Expressions: Expressions{"key-1": MustCreateMatchExpression(MatchIn, "val-x")},
 				},
 			},
 			FeatureMatcherTerm{
 				Feature: "domain-1.if-1",
 				MatchExpressions: MatchExpressionSet{
-					"attr-1": MustCreateMatchExpression(MatchIn, "val-1"),
+					Expressions: Expressions{"attr-1": MustCreateMatchExpression(MatchIn, "val-1")},
 				},
 			},
 		},
@@ -168,7 +168,7 @@ func TestRule(t *testing.T) {
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Nil(t, m.Labels, "instances should not have matched")
 
-	r5.MatchFeatures[0].MatchExpressions["key-1"] = MustCreateMatchExpression(MatchIn, "val-1")
+	r5.MatchFeatures[0].MatchExpressions.Expressions["key-1"] = MustCreateMatchExpression(MatchIn, "val-1")
 	m, err = r5.Execute(f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r5.Labels, m.Labels, "instances should have matched")
@@ -180,7 +180,7 @@ func TestRule(t *testing.T) {
 				FeatureMatcherTerm{
 					Feature: "domain-1.kf-1",
 					MatchExpressions: MatchExpressionSet{
-						"key-na": MustCreateMatchExpression(MatchExists),
+						Expressions: Expressions{"key-na": MustCreateMatchExpression(MatchExists)},
 					},
 				},
 			},
@@ -196,12 +196,12 @@ func TestRule(t *testing.T) {
 				FeatureMatcherTerm{
 					Feature: "domain-1.kf-1",
 					MatchExpressions: MatchExpressionSet{
-						"key-1": MustCreateMatchExpression(MatchExists),
+						Expressions: Expressions{"key-1": MustCreateMatchExpression(MatchExists)},
 					},
 				},
 			},
 		})
-	r5.MatchFeatures[0].MatchExpressions["key-1"] = MustCreateMatchExpression(MatchIn, "val-1")
+	r5.MatchFeatures[0].MatchExpressions.Expressions["key-1"] = MustCreateMatchExpression(MatchIn, "val-1")
 	m, err = r5.Execute(f)
 	assert.Nilf(t, err, "unexpected error: %v", err)
 	assert.Equal(t, r5.Labels, m.Labels, "instances should have matched")
@@ -275,24 +275,27 @@ var-2=
 		MatchFeatures: FeatureMatcher{
 			FeatureMatcherTerm{
 				Feature: "domain_1.kf_1",
-				MatchExpressions: MatchExpressionSet{
+				MatchExpressions: MatchExpressionSet{Expressions: Expressions{
 					"key-a": MustCreateMatchExpression(MatchExists),
 					"key-c": MustCreateMatchExpression(MatchExists),
 					"foo":   MustCreateMatchExpression(MatchDoesNotExist),
 				},
+				},
 			},
 			FeatureMatcherTerm{
 				Feature: "domain_1.vf_1",
-				MatchExpressions: MatchExpressionSet{
+				MatchExpressions: MatchExpressionSet{Expressions: Expressions{
 					"key-1": MustCreateMatchExpression(MatchIn, "val-1", "val-2"),
 					"bar":   MustCreateMatchExpression(MatchDoesNotExist),
 				},
+				},
 			},
 			FeatureMatcherTerm{
 				Feature: "domain_1.if_1",
-				MatchExpressions: MatchExpressionSet{
+				MatchExpressions: MatchExpressionSet{Expressions: Expressions{
 					"attr-1": MustCreateMatchExpression(MatchLt, "100"),
 				},
+				},
 			},
 		},
 	}
@@ -334,9 +337,10 @@ var-2=
 			// Use a simple empty matchexpression set to match anything.
 			FeatureMatcherTerm{
 				Feature: "domain_1.kf_1",
-				MatchExpressions: MatchExpressionSet{
+				MatchExpressions: MatchExpressionSet{Expressions: Expressions{
 					"key-a": MustCreateMatchExpression(MatchExists),
 				},
+				},
 			},
 		},
 	}
diff --git a/test/e2e/e2e-test-config.example.yaml b/test/e2e/e2e-test-config.example.yaml
index fcd1dce62..8a567d995 100644
--- a/test/e2e/e2e-test-config.example.yaml
+++ b/test/e2e/e2e-test-config.example.yaml
@@ -112,4 +112,7 @@ defaultFeatures:
       expectedAnnotationKeys:
         - "nfd.node.kubernetes.io/worker.version"
         - "nfd.node.kubernetes.io/feature-labels"
+  kubelet:
+    configPath: "/var/lib/kubelet/config.yaml"
+    podResourcesSocketPath: "/var/lib/kubelet/pod-resources/kubelet.sock"
 
diff --git a/test/e2e/topology_updater.go b/test/e2e/topology_updater.go
new file mode 100644
index 000000000..de7329389
--- /dev/null
+++ b/test/e2e/topology_updater.go
@@ -0,0 +1,266 @@
+/*
+Copyright 2020-2022 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 e2e
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	"github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha1"
+	topologyclientset "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned"
+
+	v1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	extclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
+	"k8s.io/kubernetes/test/e2e/framework"
+	e2enetwork "k8s.io/kubernetes/test/e2e/framework/network"
+
+	testutils "sigs.k8s.io/node-feature-discovery/test/e2e/utils"
+)
+
+var _ = SIGDescribe("Node Feature Discovery topology updater", func() {
+	var (
+		extClient           *extclient.Clientset
+		topologyClient      *topologyclientset.Clientset
+		crd                 *apiextensionsv1.CustomResourceDefinition
+		topologyUpdaterNode *v1.Node
+		workerNodes         []v1.Node
+		kubeletConfig       *kubeletconfig.KubeletConfiguration
+	)
+
+	f := framework.NewDefaultFramework("node-topology-updater")
+
+	BeforeEach(func() {
+		var err error
+
+		if extClient == nil {
+			extClient, err = extclient.NewForConfig(f.ClientConfig())
+			Expect(err).NotTo(HaveOccurred())
+		}
+
+		if topologyClient == nil {
+			topologyClient, err = topologyclientset.NewForConfig(f.ClientConfig())
+			Expect(err).NotTo(HaveOccurred())
+		}
+
+		cfg, err := testutils.GetConfig()
+		Expect(err).ToNot(HaveOccurred())
+
+		kcfg := cfg.GetKubeletConfig()
+		By(fmt.Sprintf("Using config (%#v)", kcfg))
+
+		By("Creating the node resource topologies CRD")
+		crd, err = testutils.CreateNodeResourceTopologies(extClient)
+		Expect(err).NotTo(HaveOccurred())
+
+		err = testutils.ConfigureRBAC(f.ClientSet, f.Namespace.Name)
+		Expect(err).NotTo(HaveOccurred())
+
+		image := fmt.Sprintf("%s:%s", *dockerRepo, *dockerTag)
+		f.PodClient().CreateSync(testutils.NFDMasterPod(image, false))
+
+		// Create nfd-master service
+		masterService, err := testutils.CreateService(f.ClientSet, f.Namespace.Name)
+		Expect(err).NotTo(HaveOccurred())
+
+		By("Waiting for the nfd-master service to be up")
+		Expect(e2enetwork.WaitForService(f.ClientSet, f.Namespace.Name, masterService.Name, true, time.Second, 10*time.Second)).NotTo(HaveOccurred())
+
+		By("Creating nfd-topology-updater daemonset")
+		topologyUpdaterDaemonSet := testutils.NFDTopologyUpdaterDaemonSet(kcfg, fmt.Sprintf("%s:%s", *dockerRepo, *dockerTag), []string{})
+		topologyUpdaterDaemonSet, err = f.ClientSet.AppsV1().DaemonSets(f.Namespace.Name).Create(context.TODO(), topologyUpdaterDaemonSet, metav1.CreateOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		By("Waiting for daemonset pods to be ready")
+		Expect(testutils.WaitForPodsReady(f.ClientSet, f.Namespace.Name, topologyUpdaterDaemonSet.Spec.Template.Labels["name"], 5)).NotTo(HaveOccurred())
+
+		label := labels.SelectorFromSet(map[string]string{"name": topologyUpdaterDaemonSet.Spec.Template.Labels["name"]})
+		pods, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).List(context.TODO(), metav1.ListOptions{LabelSelector: label.String()})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(pods.Items).ToNot(BeEmpty())
+
+		topologyUpdaterNode, err = f.ClientSet.CoreV1().Nodes().Get(context.TODO(), pods.Items[0].Spec.NodeName, metav1.GetOptions{})
+		Expect(err).NotTo(HaveOccurred())
+
+		kubeletConfig, err = testutils.GetCurrentKubeletConfig(topologyUpdaterNode.Name, "", true)
+		Expect(err).NotTo(HaveOccurred())
+
+		workerNodes, err = testutils.GetWorkerNodes(f)
+		Expect(err).NotTo(HaveOccurred())
+	})
+
+	Context("with single nfd-master pod", func() {
+		It("should fill the node resource topologies CR with the data", func() {
+			nodeTopology := testutils.GetNodeTopology(topologyClient, topologyUpdaterNode.Name)
+			isValid := testutils.IsValidNodeTopology(nodeTopology, kubeletConfig)
+			Expect(isValid).To(BeTrue(), "received invalid topology: %v", nodeTopology)
+		})
+
+		It("it should not account for any cpus if a container doesn't request exclusive cpus (best effort QOS)", func() {
+			By("getting the initial topology information")
+			initialNodeTopo := testutils.GetNodeTopology(topologyClient, topologyUpdaterNode.Name)
+			By("creating a pod consuming resources from the shared, non-exclusive CPU pool (best-effort QoS)")
+			sleeperPod := testutils.BestEffortSleeperPod()
+
+			podMap := make(map[string]*v1.Pod)
+			pod := f.PodClient().CreateSync(sleeperPod)
+			podMap[pod.Name] = pod
+			defer testutils.DeletePodsAsync(f, podMap)
+
+			cooldown := 30 * time.Second
+			By(fmt.Sprintf("getting the updated topology - sleeping for %v", cooldown))
+			// the object, hance the resource version must NOT change, so we can only sleep
+			time.Sleep(cooldown)
+			By("checking the changes in the updated topology - expecting none")
+			finalNodeTopo := testutils.GetNodeTopology(topologyClient, topologyUpdaterNode.Name)
+
+			initialAllocRes := testutils.AllocatableResourceListFromNodeResourceTopology(initialNodeTopo)
+			finalAllocRes := testutils.AllocatableResourceListFromNodeResourceTopology(finalNodeTopo)
+			if len(initialAllocRes) == 0 || len(finalAllocRes) == 0 {
+				Fail(fmt.Sprintf("failed to find allocatable resources from node topology initial=%v final=%v", initialAllocRes, finalAllocRes))
+			}
+			zoneName, resName, cmp, ok := testutils.CompareAllocatableResources(initialAllocRes, finalAllocRes)
+			framework.Logf("zone=%q resource=%q cmp=%v ok=%v", zoneName, resName, cmp, ok)
+			if !ok {
+				Fail(fmt.Sprintf("failed to compare allocatable resources from node topology initial=%v final=%v", initialAllocRes, finalAllocRes))
+			}
+
+			// This is actually a workaround.
+			// Depending on the (random, by design) order on which ginkgo runs the tests, a test which exclusively allocates CPUs may run before.
+			// We cannot (nor should) care about what runs before this test, but we know that this may happen.
+			// The proper solution is to wait for ALL the container requesting exclusive resources to be gone before to end the related test.
+			// To date, we don't yet have a clean way to wait for these pod (actually containers) to be completely gone
+			// (hence, releasing the exclusively allocated CPUs) before to end the test, so this test can run with some leftovers hanging around,
+			// which makes the accounting harder. And this is what we handle here.
+			isGreaterEqual := (cmp >= 0)
+			Expect(isGreaterEqual).To(BeTrue(), fmt.Sprintf("final allocatable resources not restored - cmp=%d initial=%v final=%v", cmp, initialAllocRes, finalAllocRes))
+		})
+
+		It("it should not account for any cpus if a container doesn't request exclusive cpus (guaranteed QOS, nonintegral cpu request)", func() {
+			By("getting the initial topology information")
+			initialNodeTopo := testutils.GetNodeTopology(topologyClient, topologyUpdaterNode.Name)
+			By("creating a pod consuming resources from the shared, non-exclusive CPU pool (guaranteed QoS, nonintegral request)")
+			sleeperPod := testutils.GuaranteedSleeperPod("500m")
+
+			podMap := make(map[string]*v1.Pod)
+			pod := f.PodClient().CreateSync(sleeperPod)
+			podMap[pod.Name] = pod
+			defer testutils.DeletePodsAsync(f, podMap)
+
+			cooldown := 30 * time.Second
+			By(fmt.Sprintf("getting the updated topology - sleeping for %v", cooldown))
+			// the object, hance the resource version must NOT change, so we can only sleep
+			time.Sleep(cooldown)
+			By("checking the changes in the updated topology - expecting none")
+			finalNodeTopo := testutils.GetNodeTopology(topologyClient, topologyUpdaterNode.Name)
+
+			initialAllocRes := testutils.AllocatableResourceListFromNodeResourceTopology(initialNodeTopo)
+			finalAllocRes := testutils.AllocatableResourceListFromNodeResourceTopology(finalNodeTopo)
+			if len(initialAllocRes) == 0 || len(finalAllocRes) == 0 {
+				Fail(fmt.Sprintf("failed to find allocatable resources from node topology initial=%v final=%v", initialAllocRes, finalAllocRes))
+			}
+			zoneName, resName, cmp, ok := testutils.CompareAllocatableResources(initialAllocRes, finalAllocRes)
+			framework.Logf("zone=%q resource=%q cmp=%v ok=%v", zoneName, resName, cmp, ok)
+			if !ok {
+				Fail(fmt.Sprintf("failed to compare allocatable resources from node topology initial=%v final=%v", initialAllocRes, finalAllocRes))
+			}
+
+			// This is actually a workaround.
+			// Depending on the (random, by design) order on which ginkgo runs the tests, a test which exclusively allocates CPUs may run before.
+			// We cannot (nor should) care about what runs before this test, but we know that this may happen.
+			// The proper solution is to wait for ALL the container requesting exclusive resources to be gone before to end the related test.
+			// To date, we don't yet have a clean way to wait for these pod (actually containers) to be completely gone
+			// (hence, releasing the exclusively allocated CPUs) before to end the test, so this test can run with some leftovers hanging around,
+			// which makes the accounting harder. And this is what we handle here.
+			isGreaterEqual := (cmp >= 0)
+			Expect(isGreaterEqual).To(BeTrue(), fmt.Sprintf("final allocatable resources not restored - cmp=%d initial=%v final=%v", cmp, initialAllocRes, finalAllocRes))
+		})
+
+		It("it should account for containers requesting exclusive cpus", func() {
+			nodes, err := testutils.FilterNodesWithEnoughCores(workerNodes, "1000m")
+			Expect(err).NotTo(HaveOccurred())
+			if len(nodes) < 1 {
+				Skip("not enough allocatable cores for this test")
+			}
+
+			By("getting the initial topology information")
+			initialNodeTopo := testutils.GetNodeTopology(topologyClient, topologyUpdaterNode.Name)
+			By("creating a pod consuming exclusive CPUs")
+			sleeperPod := testutils.GuaranteedSleeperPod("1000m")
+
+			podMap := make(map[string]*v1.Pod)
+			pod := f.PodClient().CreateSync(sleeperPod)
+			podMap[pod.Name] = pod
+			defer testutils.DeletePodsAsync(f, podMap)
+
+			By("getting the updated topology")
+			var finalNodeTopo *v1alpha1.NodeResourceTopology
+			Eventually(func() bool {
+				finalNodeTopo, err = topologyClient.TopologyV1alpha1().NodeResourceTopologies().Get(context.TODO(), topologyUpdaterNode.Name, metav1.GetOptions{})
+				if err != nil {
+					framework.Logf("failed to get the node topology resource: %v", err)
+					return false
+				}
+				return finalNodeTopo.ObjectMeta.ResourceVersion != initialNodeTopo.ObjectMeta.ResourceVersion
+			}, time.Minute, 5*time.Second).Should(BeTrue(), "didn't get updated node topology info")
+			By("checking the changes in the updated topology")
+
+			initialAllocRes := testutils.AllocatableResourceListFromNodeResourceTopology(initialNodeTopo)
+			finalAllocRes := testutils.AllocatableResourceListFromNodeResourceTopology(finalNodeTopo)
+			if len(initialAllocRes) == 0 || len(finalAllocRes) == 0 {
+				Fail(fmt.Sprintf("failed to find allocatable resources from node topology initial=%v final=%v", initialAllocRes, finalAllocRes))
+			}
+			zoneName, resName, isLess := lessAllocatableResources(initialAllocRes, finalAllocRes)
+			framework.Logf("zone=%q resource=%q isLess=%v", zoneName, resName, isLess)
+			Expect(isLess).To(BeTrue(), fmt.Sprintf("final allocatable resources not decreased - initial=%v final=%v", initialAllocRes, finalAllocRes))
+		})
+
+	})
+
+	JustAfterEach(func() {
+		err := testutils.DeconfigureRBAC(f.ClientSet, f.Namespace.Name)
+		if err != nil {
+			framework.Logf("failed to delete RBAC resources: %v", err)
+		}
+
+		err = extClient.ApiextensionsV1().CustomResourceDefinitions().Delete(context.TODO(), crd.Name, metav1.DeleteOptions{})
+		if err != nil {
+			framework.Logf("failed to delete node resources topologies CRD: %v", err)
+		}
+	})
+})
+
+// lessAllocatableResources specialize CompareAllocatableResources for this specific e2e use case.
+func lessAllocatableResources(expected, got map[string]v1.ResourceList) (string, string, bool) {
+	zoneName, resName, cmp, ok := testutils.CompareAllocatableResources(expected, got)
+	if !ok {
+		framework.Logf("-> cmp failed (not ok)")
+		return "", "", false
+	}
+	if cmp < 0 {
+		return zoneName, resName, true
+	}
+	framework.Logf("-> cmp failed (value=%d)", cmp)
+	return "", "", false
+}
diff --git a/test/e2e/utils/config.go b/test/e2e/utils/config.go
index 475c3c7e7..b5d68969d 100644
--- a/test/e2e/utils/config.go
+++ b/test/e2e/utils/config.go
@@ -26,18 +26,48 @@ import (
 	"sigs.k8s.io/yaml"
 )
 
+const (
+	DefaultConfigPath             = "/var/lib/kubelet/config.yaml"
+	DefaultPodResourcesSocketPath = "/var/lib/kubelet/pod-resources/kubelet.sock"
+)
+
 var (
 	e2eConfigFile = flag.String("nfd.e2e-config", "", "Configuration parameters for end-to-end tests")
 
 	config *E2EConfig
 )
 
+type KubeletConfig struct {
+	ConfigPath             string
+	PodResourcesSocketPath string
+}
+
 type E2EConfig struct {
 	DefaultFeatures *struct {
 		LabelWhitelist      lookupMap
 		AnnotationWhitelist lookupMap
 		Nodes               []NodeConfig
 	}
+
+	Kubelet *KubeletConfig
+}
+
+// GetKubeletConfig returns a KubeletConfig object with default values, possibly overridden by user settings.
+func (conf *E2EConfig) GetKubeletConfig() KubeletConfig {
+	kcfg := KubeletConfig{
+		ConfigPath:             DefaultConfigPath,
+		PodResourcesSocketPath: DefaultPodResourcesSocketPath,
+	}
+	if conf.Kubelet == nil {
+		return kcfg
+	}
+	if conf.Kubelet.ConfigPath != "" {
+		kcfg.ConfigPath = conf.Kubelet.ConfigPath
+	}
+	if conf.Kubelet.PodResourcesSocketPath != "" {
+		kcfg.PodResourcesSocketPath = conf.Kubelet.PodResourcesSocketPath
+	}
+	return kcfg
 }
 
 type NodeConfig struct {
diff --git a/test/e2e/utils/kubeletconfig.go b/test/e2e/utils/kubeletconfig.go
new file mode 100644
index 000000000..764532203
--- /dev/null
+++ b/test/e2e/utils/kubeletconfig.go
@@ -0,0 +1,150 @@
+/*
+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 utils
+
+/*
+ * borrow
+ * https://raw.githubusercontent.com/kubernetes/kubernetes/v1.25.0-beta.0/test/e2e/framework/kubelet/config.go
+ * until we can bump the project dependencies to 1.25+
+ */
+
+import (
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"regexp"
+	"strconv"
+	"time"
+
+	"k8s.io/apimachinery/pkg/util/wait"
+	kubeletconfigv1beta1 "k8s.io/kubelet/config/v1beta1"
+	kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
+	kubeletconfigscheme "k8s.io/kubernetes/pkg/kubelet/apis/config/scheme"
+
+	"k8s.io/kubernetes/test/e2e/framework"
+	e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
+)
+
+// GetCurrentKubeletConfig fetches the current Kubelet Config for the given node
+func GetCurrentKubeletConfig(nodeName, namespace string, useProxy bool) (*kubeletconfig.KubeletConfiguration, error) {
+	resp := pollConfigz(5*time.Minute, 5*time.Second, nodeName, namespace, useProxy)
+	if len(resp) == 0 {
+		return nil, fmt.Errorf("failed to fetch /configz from %q", nodeName)
+	}
+	kubeCfg, err := decodeConfigz(resp)
+	if err != nil {
+		return nil, err
+	}
+	return kubeCfg, nil
+}
+
+// returns a status 200 response from the /configz endpoint or nil if fails
+func pollConfigz(timeout time.Duration, pollInterval time.Duration, nodeName, namespace string, useProxy bool) []byte {
+	endpoint := ""
+	if useProxy {
+		// start local proxy, so we can send graceful deletion over query string, rather than body parameter
+		framework.Logf("Opening proxy to cluster")
+		tk := e2ekubectl.NewTestKubeconfig(framework.TestContext.CertDir, framework.TestContext.Host, framework.TestContext.KubeConfig, framework.TestContext.KubeContext, framework.TestContext.KubectlPath, namespace)
+		cmd := tk.KubectlCmd("proxy", "-p", "0")
+		stdout, stderr, err := framework.StartCmdAndStreamOutput(cmd)
+		framework.ExpectNoError(err)
+		defer stdout.Close()
+		defer stderr.Close()
+		defer framework.TryKill(cmd)
+
+		buf := make([]byte, 128)
+		var n int
+		n, err = stdout.Read(buf)
+		framework.ExpectNoError(err)
+		output := string(buf[:n])
+		proxyRegexp := regexp.MustCompile("Starting to serve on 127.0.0.1:([0-9]+)")
+		match := proxyRegexp.FindStringSubmatch(output)
+		framework.ExpectEqual(len(match), 2)
+		port, err := strconv.Atoi(match[1])
+		framework.ExpectNoError(err)
+		framework.Logf("http requesting node kubelet /configz")
+		endpoint = fmt.Sprintf("http://127.0.0.1:%d/api/v1/nodes/%s/proxy/configz", port, nodeName)
+	} else {
+		endpoint = fmt.Sprintf("%s/api/v1/nodes/%s/proxy/configz", framework.TestContext.Host, framework.TestContext.NodeName)
+	}
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+	}
+	client := &http.Client{Transport: tr}
+	req, err := http.NewRequest("GET", endpoint, nil)
+	framework.ExpectNoError(err)
+	if !useProxy {
+		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", framework.TestContext.BearerToken))
+	}
+	req.Header.Add("Accept", "application/json")
+
+	var respBody []byte
+	err = wait.PollImmediate(pollInterval, timeout, func() (bool, error) {
+		resp, err := client.Do(req)
+		if err != nil {
+			framework.Logf("Failed to get /configz, retrying. Error: %v", err)
+			return false, nil
+		}
+		defer resp.Body.Close()
+
+		if resp.StatusCode != 200 {
+			framework.Logf("/configz response status not 200, retrying. Response was: %+v", resp)
+			return false, nil
+		}
+
+		respBody, err = io.ReadAll(resp.Body)
+		if err != nil {
+			framework.Logf("failed to read body from /configz response, retrying. Error: %v", err)
+			return false, nil
+		}
+
+		return true, nil
+	})
+	framework.ExpectNoError(err, "Failed to get successful response from /configz")
+
+	return respBody
+}
+
+// Decodes the http response from /configz and returns a kubeletconfig.KubeletConfiguration (internal type).
+func decodeConfigz(respBody []byte) (*kubeletconfig.KubeletConfiguration, error) {
+	// This hack because /configz reports the following structure:
+	// {"kubeletconfig": {the JSON representation of kubeletconfigv1beta1.KubeletConfiguration}}
+	type configzWrapper struct {
+		ComponentConfig kubeletconfigv1beta1.KubeletConfiguration `json:"kubeletconfig"`
+	}
+
+	configz := configzWrapper{}
+	kubeCfg := kubeletconfig.KubeletConfiguration{}
+
+	err := json.Unmarshal(respBody, &configz)
+	if err != nil {
+		return nil, err
+	}
+
+	scheme, _, err := kubeletconfigscheme.NewSchemeAndCodecs()
+	if err != nil {
+		return nil, err
+	}
+	err = scheme.Convert(&configz.ComponentConfig, &kubeCfg, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return &kubeCfg, nil
+}
diff --git a/test/e2e/utils/node.go b/test/e2e/utils/node.go
new file mode 100644
index 000000000..c0e4c3c80
--- /dev/null
+++ b/test/e2e/utils/node.go
@@ -0,0 +1,87 @@
+/*
+Copyright 2021-2022 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 (
+	"context"
+	"fmt"
+
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/kubernetes/test/e2e/framework"
+)
+
+const (
+	// RoleWorker contains the worker role
+	RoleWorker = "worker"
+)
+
+const (
+	// LabelRole contains the key for the role label
+	LabelRole = "node-role.kubernetes.io"
+	// LabelHostname contains the key for the hostname label
+	LabelHostname = "kubernetes.io/hostname"
+)
+
+// GetWorkerNodes returns all nodes labeled as worker
+func GetWorkerNodes(f *framework.Framework) ([]v1.Node, error) {
+	return GetNodesByRole(f, RoleWorker)
+}
+
+// GetByRole returns all nodes with the specified role
+func GetNodesByRole(f *framework.Framework, role string) ([]v1.Node, error) {
+	selector, err := labels.Parse(fmt.Sprintf("%s/%s=", LabelRole, role))
+	if err != nil {
+		return nil, err
+	}
+	return GetNodesBySelector(f, selector)
+}
+
+// GetBySelector returns all nodes with the specified selector
+func GetNodesBySelector(f *framework.Framework, selector labels.Selector) ([]v1.Node, error) {
+	nodes, err := f.ClientSet.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
+	if err != nil {
+		return nil, err
+	}
+	return nodes.Items, nil
+}
+
+// FilterNodesWithEnoughCores returns all nodes with at least the amount of given CPU allocatable
+func FilterNodesWithEnoughCores(nodes []v1.Node, cpuAmount string) ([]v1.Node, error) {
+	requestCpu := resource.MustParse(cpuAmount)
+	framework.Logf("checking request %v on %d nodes", requestCpu, len(nodes))
+
+	resNodes := []v1.Node{}
+	for _, node := range nodes {
+		availCpu, ok := node.Status.Allocatable[v1.ResourceCPU]
+		if !ok || availCpu.IsZero() {
+			return nil, fmt.Errorf("node %q has no allocatable CPU", node.Name)
+		}
+
+		if availCpu.Cmp(requestCpu) < 1 {
+			framework.Logf("node %q available cpu %v requested cpu %v", node.Name, availCpu, requestCpu)
+			continue
+		}
+
+		framework.Logf("node %q has enough resources, cluster OK", node.Name)
+		resNodes = append(resNodes, node)
+	}
+
+	return resNodes, nil
+}
diff --git a/test/e2e/utils/noderesourcetopology.go b/test/e2e/utils/noderesourcetopology.go
new file mode 100644
index 000000000..565126c5e
--- /dev/null
+++ b/test/e2e/utils/noderesourcetopology.go
@@ -0,0 +1,249 @@
+/*
+Copyright 2020-2022 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 (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"time"
+
+	"github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha1"
+	topologyclientset "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/generated/clientset/versioned"
+	"github.com/onsi/gomega"
+	"sigs.k8s.io/node-feature-discovery/pkg/topologypolicy"
+
+	v1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	extclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes/scheme"
+	kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
+	"k8s.io/kubernetes/test/e2e/framework"
+)
+
+func init() {
+	// make golangci-lint happy
+	_ = apiextensionsv1.AddToScheme(scheme.Scheme)
+}
+
+// NewNodeResourceTopologies makes a CRD golang object representing NodeResourceTopology definition
+func NewNodeResourceTopologies() (*apiextensionsv1.CustomResourceDefinition, error) {
+	_, file, _, ok := runtime.Caller(0)
+	if !ok {
+		return nil, fmt.Errorf("cannot retrieve manifests directory")
+	}
+
+	baseDir := filepath.Dir(file)
+	crdPath := filepath.Clean(filepath.Join(baseDir, "..", "..", "..", "deployment", "base", "noderesourcetopologies-crd", "noderesourcetopologies.yaml"))
+
+	data, err := os.ReadFile(crdPath)
+	if err != nil {
+		return nil, err
+	}
+
+	decode := scheme.Codecs.UniversalDeserializer().Decode
+	obj, _, err := decode(data, nil, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	crd, ok := obj.(*apiextensionsv1.CustomResourceDefinition)
+	if !ok {
+		return nil, fmt.Errorf("unexpected type, got %t", obj)
+	}
+	return crd, nil
+}
+
+// CreateNodeResourceTopologies creates the NodeResourceTopology in the cluster if the CRD doesn't exists already.
+// Returns the CRD golang object present in the cluster.
+func CreateNodeResourceTopologies(extClient extclient.Interface) (*apiextensionsv1.CustomResourceDefinition, error) {
+	crd, err := NewNodeResourceTopologies()
+	if err != nil {
+		return nil, err
+	}
+
+	updatedCrd, err := extClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), crd.Name, metav1.GetOptions{})
+	if err != nil && !errors.IsNotFound(err) {
+		return nil, err
+	}
+
+	if err == nil {
+		return updatedCrd, nil
+	}
+
+	return extClient.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{})
+}
+
+// GetNodeTopology returns the NodeResourceTopology data for the node identified by `nodeName`.
+func GetNodeTopology(topologyClient *topologyclientset.Clientset, nodeName string) *v1alpha1.NodeResourceTopology {
+	var nodeTopology *v1alpha1.NodeResourceTopology
+	var err error
+	gomega.EventuallyWithOffset(1, func() bool {
+		nodeTopology, err = topologyClient.TopologyV1alpha1().NodeResourceTopologies().Get(context.TODO(), nodeName, metav1.GetOptions{})
+		if err != nil {
+			framework.Logf("failed to get the node topology resource: %v", err)
+			return false
+		}
+		return true
+	}, time.Minute, 5*time.Second).Should(gomega.BeTrue())
+	return nodeTopology
+}
+
+// AllocatableResourceListFromNodeResourceTopology extract the map zone:allocatableResources from the given NodeResourceTopology instance.
+func AllocatableResourceListFromNodeResourceTopology(nodeTopo *v1alpha1.NodeResourceTopology) map[string]v1.ResourceList {
+	allocRes := make(map[string]v1.ResourceList)
+	for _, zone := range nodeTopo.Zones {
+		if zone.Type != "Node" {
+			continue
+		}
+		resList := make(v1.ResourceList)
+		for _, res := range zone.Resources {
+			resList[v1.ResourceName(res.Name)] = res.Allocatable.DeepCopy()
+		}
+		if len(resList) == 0 {
+			continue
+		}
+		allocRes[zone.Name] = resList
+	}
+	return allocRes
+}
+
+// CompareAllocatableResources compares `expected` and `got` map zone:allocatableResources respectively (see: AllocatableResourceListFromNodeResourceTopology),
+// and informs the caller if the maps are equal. Here `equal` means the same zoneNames with the same resources, where the resources are equal if they have
+// the same resources with the same quantities. Returns the name of the different zone, the name of the different resources within the zone,
+// the comparison result (same semantic as strings.Compare) and a boolean that reports if the resourceLists are consistent. See `CompareResourceList`.
+func CompareAllocatableResources(expected, got map[string]v1.ResourceList) (string, string, int, bool) {
+	if len(got) != len(expected) {
+		framework.Logf("-> expected=%v (len=%d) got=%v (len=%d)", expected, len(expected), got, len(got))
+		return "", "", 0, false
+	}
+	for expZoneName, expResList := range expected {
+		gotResList, ok := got[expZoneName]
+		if !ok {
+			return expZoneName, "", 0, false
+		}
+		if resName, cmp, ok := CompareResourceList(expResList, gotResList); !ok || cmp != 0 {
+			return expZoneName, resName, cmp, ok
+		}
+	}
+	return "", "", 0, true
+}
+
+// CompareResourceList compares `expected` and `got` ResourceList respectively, and informs the caller if the two ResourceList
+// are equal. Here `equal` means the same resources with the same quantities. Returns the different resource,
+// the comparison result (same semantic as strings.Compare) and a boolean that reports if the resourceLists are consistent.
+// The ResourceLists are consistent only if the represent the same resource set (all the resources listed in one are
+// also present in the another; no ResourceList is a superset nor a subset of the other)
+func CompareResourceList(expected, got v1.ResourceList) (string, int, bool) {
+	if len(got) != len(expected) {
+		framework.Logf("-> expected=%v (len=%d) got=%v (len=%d)", expected, len(expected), got, len(got))
+		return "", 0, false
+	}
+	for expResName, expResQty := range expected {
+		gotResQty, ok := got[expResName]
+		if !ok {
+			return string(expResName), 0, false
+		}
+		if cmp := gotResQty.Cmp(expResQty); cmp != 0 {
+			framework.Logf("-> resource=%q cmp=%d expected=%v got=%v", expResName, cmp, expResQty, gotResQty)
+			return string(expResName), cmp, true
+		}
+	}
+	return "", 0, true
+}
+
+// IsValidNodeTopology checks the provided NodeResourceTopology object if it is well-formad, internally consistent and
+// consistent with the given kubelet config object. Returns true if the NodeResourceTopology object is consistent and well
+// formet, false otherwise; if return false, logs the failure reason.
+func IsValidNodeTopology(nodeTopology *v1alpha1.NodeResourceTopology, kubeletConfig *kubeletconfig.KubeletConfiguration) bool {
+	if nodeTopology == nil || len(nodeTopology.TopologyPolicies) == 0 {
+		framework.Logf("failed to get topology policy from the node topology resource")
+		return false
+	}
+
+	tmPolicy := string(topologypolicy.DetectTopologyPolicy(kubeletConfig.TopologyManagerPolicy, kubeletConfig.TopologyManagerScope))
+	if nodeTopology.TopologyPolicies[0] != tmPolicy {
+		framework.Logf("topology policy mismatch got %q expected %q", nodeTopology.TopologyPolicies[0], tmPolicy)
+		return false
+	}
+
+	if nodeTopology.Zones == nil || len(nodeTopology.Zones) == 0 {
+		framework.Logf("failed to get topology zones from the node topology resource")
+		return false
+	}
+
+	foundNodes := 0
+	for _, zone := range nodeTopology.Zones {
+		// TODO constant not in the APIs
+		if !strings.HasPrefix(strings.ToUpper(zone.Type), "NODE") {
+			continue
+		}
+		foundNodes++
+
+		if !isValidCostList(zone.Name, zone.Costs) {
+			framework.Logf("invalid cost list for zone %q", zone.Name)
+			return false
+		}
+
+		if !isValidResourceList(zone.Name, zone.Resources) {
+			framework.Logf("invalid resource list for zone %q", zone.Name)
+			return false
+		}
+	}
+	return foundNodes > 0
+}
+
+func isValidCostList(zoneName string, costs v1alpha1.CostList) bool {
+	if len(costs) == 0 {
+		framework.Logf("failed to get topology costs for zone %q from the node topology resource", zoneName)
+		return false
+	}
+
+	// TODO cross-validate zone names
+	for _, cost := range costs {
+		if cost.Name == "" || cost.Value < 0 {
+			framework.Logf("malformed cost %v for zone %q", cost, zoneName)
+		}
+	}
+	return true
+}
+
+func isValidResourceList(zoneName string, resources v1alpha1.ResourceInfoList) bool {
+	if len(resources) == 0 {
+		framework.Logf("failed to get topology resources for zone %q from the node topology resource", zoneName)
+		return false
+	}
+	foundCpu := false
+	for _, resource := range resources {
+		// TODO constant not in the APIs
+		if strings.ToUpper(resource.Name) == "CPU" {
+			foundCpu = true
+		}
+		allocatable, ok1 := resource.Allocatable.AsInt64()
+		capacity, ok2 := resource.Capacity.AsInt64()
+		if (!ok1 || !ok2) || ((allocatable < 0 || capacity < 0) || (capacity < allocatable)) {
+			framework.Logf("malformed resource %v for zone %q", resource, zoneName)
+			return false
+		}
+	}
+	return foundCpu
+}
diff --git a/test/e2e/utils/pod.go b/test/e2e/utils/pod.go
index bab9914c9..7f76f6039 100644
--- a/test/e2e/utils/pod.go
+++ b/test/e2e/utils/pod.go
@@ -18,18 +18,99 @@ package utils
 
 import (
 	"context"
+	"flag"
+	"sync"
 	"time"
 
+	"github.com/onsi/ginkgo"
+
 	appsv1 "k8s.io/api/apps/v1"
 	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/uuid"
 	"k8s.io/apimachinery/pkg/util/wait"
 	clientset "k8s.io/client-go/kubernetes"
 	"k8s.io/kubectl/pkg/util/podutils"
+	"k8s.io/kubernetes/test/e2e/framework"
+	"k8s.io/utils/pointer"
+)
+
+var pullIfNotPresent = flag.Bool("nfd.pull-if-not-present", false, "Pull Images if not present - not always")
+
+const (
+	PauseImage = "k8s.gcr.io/pause"
 )
 
+// GuarenteedSleeperPod makes a Guaranteed QoS class Pod object which long enough forever but requires `cpuLimit` exclusive CPUs.
+func GuaranteedSleeperPod(cpuLimit string) *v1.Pod {
+	return &v1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "sleeper-gu-pod",
+		},
+		Spec: v1.PodSpec{
+			RestartPolicy: v1.RestartPolicyNever,
+			Containers: []v1.Container{
+				v1.Container{
+					Name:  "sleeper-gu-cnt",
+					Image: PauseImage,
+					Resources: v1.ResourceRequirements{
+						Limits: v1.ResourceList{
+							// we use 1 core because that's the minimal meaningful quantity
+							v1.ResourceName(v1.ResourceCPU): resource.MustParse(cpuLimit),
+							// any random reasonable amount is fine
+							v1.ResourceName(v1.ResourceMemory): resource.MustParse("100Mi"),
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+// BestEffortSleeperPod makes a Best Effort QoS class Pod object which sleeps long enough
+func BestEffortSleeperPod() *v1.Pod {
+	return &v1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "sleeper-be-pod",
+		},
+		Spec: v1.PodSpec{
+			RestartPolicy: v1.RestartPolicyNever,
+			Containers: []v1.Container{
+				v1.Container{
+					Name:  "sleeper-be-cnt",
+					Image: PauseImage,
+				},
+			},
+		},
+	}
+}
+
+// DeletePodsAsync concurrently deletes all the pods in the given name:pod_object mapping. Returns when the longer operation ends.
+func DeletePodsAsync(f *framework.Framework, podMap map[string]*v1.Pod) {
+	var wg sync.WaitGroup
+	for _, pod := range podMap {
+		wg.Add(1)
+		go func(podNS, podName string) {
+			defer ginkgo.GinkgoRecover()
+			defer wg.Done()
+
+			DeletePodSyncByName(f, podName)
+		}(pod.Namespace, pod.Name)
+	}
+	wg.Wait()
+}
+
+// DeletePodSyncByName deletes the pod identified by `podName` in the current namespace
+func DeletePodSyncByName(f *framework.Framework, podName string) {
+	gp := int64(0)
+	delOpts := metav1.DeleteOptions{
+		GracePeriodSeconds: &gp,
+	}
+	f.PodClient().DeleteSync(podName, delOpts, framework.DefaultPodDeletionTimeout)
+}
+
 // NFDMasterPod provide NFD master pod definition
 func NFDMasterPod(image string, onMasterNode bool) *v1.Pod {
 	p := &v1.Pod{
@@ -42,7 +123,7 @@ func NFDMasterPod(image string, onMasterNode bool) *v1.Pod {
 				{
 					Name:            "node-feature-discovery",
 					Image:           image,
-					ImagePullPolicy: v1.PullAlways,
+					ImagePullPolicy: pullPolicy(),
 					Command:         []string{"nfd-master"},
 					Env: []v1.EnvVar{
 						{
@@ -94,6 +175,12 @@ func NFDWorkerDaemonSet(image string, extraArgs []string) *appsv1.DaemonSet {
 	return newDaemonSet("nfd-worker", podSpec)
 }
 
+// NFDTopologyUpdaterDaemonSet provides the NFD daemon set topology updater
+func NFDTopologyUpdaterDaemonSet(kc KubeletConfig, image string, extraArgs []string) *appsv1.DaemonSet {
+	podSpec := nfdTopologyUpdaterPodSpec(kc, image, extraArgs)
+	return newDaemonSet("nfd-topology-updater", podSpec)
+}
+
 // newDaemonSet provide the new daemon set
 func newDaemonSet(name string, podSpec *v1.PodSpec) *appsv1.DaemonSet {
 	return &appsv1.DaemonSet{
@@ -121,7 +208,7 @@ func nfdWorkerPodSpec(image string, extraArgs []string) *v1.PodSpec {
 			{
 				Name:            "node-feature-discovery",
 				Image:           image,
-				ImagePullPolicy: v1.PullAlways,
+				ImagePullPolicy: pullPolicy(),
 				Command:         []string{"nfd-worker"},
 				Args:            append([]string{"-server=nfd-master-e2e:8080"}, extraArgs...),
 				Env: []v1.EnvVar{
@@ -213,7 +300,89 @@ func nfdWorkerPodSpec(image string, extraArgs []string) *v1.PodSpec {
 			},
 		},
 	}
+}
 
+func nfdTopologyUpdaterPodSpec(kc KubeletConfig, image string, extraArgs []string) *v1.PodSpec {
+	return &v1.PodSpec{
+		Containers: []v1.Container{
+			{
+				Name:            "node-topology-updater",
+				Image:           image,
+				ImagePullPolicy: pullPolicy(),
+				Command:         []string{"nfd-topology-updater"},
+				Args: append([]string{
+					"--kubelet-config-file=/podresources/config.yaml",
+					"--podresources-socket=unix:///podresources/kubelet.sock",
+					"--sleep-interval=3s",
+					"--watch-namespace=rte",
+					"--server=nfd-master-e2e:8080",
+				}, extraArgs...),
+				Env: []v1.EnvVar{
+					{
+						Name: "NODE_NAME",
+						ValueFrom: &v1.EnvVarSource{
+							FieldRef: &v1.ObjectFieldSelector{
+								FieldPath: "spec.nodeName",
+							},
+						},
+					},
+				},
+				SecurityContext: &v1.SecurityContext{
+					Capabilities: &v1.Capabilities{
+						Drop: []v1.Capability{"ALL"},
+					},
+					RunAsUser:                pointer.Int64Ptr(0),
+					ReadOnlyRootFilesystem:   pointer.BoolPtr(true),
+					AllowPrivilegeEscalation: pointer.BoolPtr(false),
+				},
+				VolumeMounts: []v1.VolumeMount{
+					{
+						Name:      "kubelet-podresources-conf",
+						MountPath: "/podresources/config.yaml",
+					},
+					{
+						Name:      "kubelet-podresources-sock",
+						MountPath: "/podresources/kubelet.sock",
+					},
+					{
+						Name:      "host-sys",
+						MountPath: "/host-sys",
+					},
+				},
+			},
+		},
+		ServiceAccountName: "nfd-topology-updater-e2e",
+		DNSPolicy:          v1.DNSClusterFirstWithHostNet,
+		Volumes: []v1.Volume{
+			{
+				Name: "kubelet-podresources-conf",
+				VolumeSource: v1.VolumeSource{
+					HostPath: &v1.HostPathVolumeSource{
+						Path: kc.ConfigPath,
+						Type: newHostPathType(v1.HostPathFile),
+					},
+				},
+			},
+			{
+				Name: "kubelet-podresources-sock",
+				VolumeSource: v1.VolumeSource{
+					HostPath: &v1.HostPathVolumeSource{
+						Path: kc.PodResourcesSocketPath,
+						Type: newHostPathType(v1.HostPathSocket),
+					},
+				},
+			},
+			{
+				Name: "host-sys",
+				VolumeSource: v1.VolumeSource{
+					HostPath: &v1.HostPathVolumeSource{
+						Path: "/sys",
+						Type: newHostPathType(v1.HostPathDirectory),
+					},
+				},
+			},
+		},
+	}
 }
 
 func newHostPathType(typ v1.HostPathType) *v1.HostPathType {
@@ -242,3 +411,10 @@ func WaitForPodsReady(c clientset.Interface, ns, name string, minReadySeconds in
 		return true, nil
 	})
 }
+
+func pullPolicy() v1.PullPolicy {
+	if *pullIfNotPresent {
+		return v1.PullIfNotPresent
+	}
+	return v1.PullAlways
+}
diff --git a/test/e2e/utils/rbac.go b/test/e2e/utils/rbac.go
index d5d9da52a..c5b93b0cd 100644
--- a/test/e2e/utils/rbac.go
+++ b/test/e2e/utils/rbac.go
@@ -32,17 +32,32 @@ var (
 
 // ConfigureRBAC creates required RBAC configuration
 func ConfigureRBAC(cs clientset.Interface, ns string) error {
-	_, err := createServiceAccount(cs, ns)
+	_, err := createServiceAccountMaster(cs, ns)
 	if err != nil {
 		return err
 	}
 
-	_, err = createClusterRole(cs)
+	_, err = createServiceAccountTopologyUpdater(cs, ns)
 	if err != nil {
 		return err
 	}
 
-	_, err = createClusterRoleBinding(cs, ns)
+	_, err = createClusterRoleMaster(cs)
+	if err != nil {
+		return err
+	}
+
+	_, err = createClusterRoleTopologyUpdater(cs)
+	if err != nil {
+		return err
+	}
+
+	_, err = createClusterRoleBindingMaster(cs, ns)
+	if err != nil {
+		return err
+	}
+
+	_, err = createClusterRoleBindingTopologyUpdater(cs, ns)
 	if err != nil {
 		return err
 	}
@@ -52,7 +67,15 @@ func ConfigureRBAC(cs clientset.Interface, ns string) error {
 
 // DeconfigureRBAC removes RBAC configuration
 func DeconfigureRBAC(cs clientset.Interface, ns string) error {
-	err := cs.RbacV1().ClusterRoleBindings().Delete(context.TODO(), "nfd-master-e2e", metav1.DeleteOptions{})
+	err := cs.RbacV1().ClusterRoleBindings().Delete(context.TODO(), "nfd-topology-updater-e2e", metav1.DeleteOptions{})
+	if err != nil {
+		return err
+	}
+	err = cs.RbacV1().ClusterRoleBindings().Delete(context.TODO(), "nfd-master-e2e", metav1.DeleteOptions{})
+	if err != nil {
+		return err
+	}
+	err = cs.RbacV1().ClusterRoles().Delete(context.TODO(), "nfd-topology-updater-e2e", metav1.DeleteOptions{})
 	if err != nil {
 		return err
 	}
@@ -60,6 +83,10 @@ func DeconfigureRBAC(cs clientset.Interface, ns string) error {
 	if err != nil {
 		return err
 	}
+	err = cs.CoreV1().ServiceAccounts(ns).Delete(context.TODO(), "nfd-topology-updater-e2e", metav1.DeleteOptions{})
+	if err != nil {
+		return err
+	}
 	err = cs.CoreV1().ServiceAccounts(ns).Delete(context.TODO(), "nfd-master-e2e", metav1.DeleteOptions{})
 	if err != nil {
 		return err
@@ -67,8 +94,8 @@ func DeconfigureRBAC(cs clientset.Interface, ns string) error {
 	return nil
 }
 
-// Configure service account required by NFD
-func createServiceAccount(cs clientset.Interface, ns string) (*v1.ServiceAccount, error) {
+// Configure service account required by NFD Master
+func createServiceAccountMaster(cs clientset.Interface, ns string) (*v1.ServiceAccount, error) {
 	sa := &v1.ServiceAccount{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "nfd-master-e2e",
@@ -78,8 +105,19 @@ func createServiceAccount(cs clientset.Interface, ns string) (*v1.ServiceAccount
 	return cs.CoreV1().ServiceAccounts(ns).Create(context.TODO(), sa, metav1.CreateOptions{})
 }
 
-// Configure cluster role required by NFD
-func createClusterRole(cs clientset.Interface) (*rbacv1.ClusterRole, error) {
+// Configure service account required by NFD MTopology Updater
+func createServiceAccountTopologyUpdater(cs clientset.Interface, ns string) (*v1.ServiceAccount, error) {
+	sa := &v1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "nfd-topology-updater-e2e",
+			Namespace: ns,
+		},
+	}
+	return cs.CoreV1().ServiceAccounts(ns).Create(context.TODO(), sa, metav1.CreateOptions{})
+}
+
+// Configure cluster role required by NFD Master
+func createClusterRoleMaster(cs clientset.Interface) (*rbacv1.ClusterRole, error) {
 	cr := &rbacv1.ClusterRole{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: "nfd-master-e2e",
@@ -114,8 +152,38 @@ func createClusterRole(cs clientset.Interface) (*rbacv1.ClusterRole, error) {
 	return cs.RbacV1().ClusterRoles().Update(context.TODO(), cr, metav1.UpdateOptions{})
 }
 
-// Configure cluster role binding required by NFD
-func createClusterRoleBinding(cs clientset.Interface, ns string) (*rbacv1.ClusterRoleBinding, error) {
+// Configure cluster role required by NFD Topology Updater
+func createClusterRoleTopologyUpdater(cs clientset.Interface) (*rbacv1.ClusterRole, error) {
+	cr := &rbacv1.ClusterRole{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "nfd-topology-updater-e2e",
+		},
+		// the Topology Updater doesn't need to access any kube object:
+		// it reads from the podresources socket and it sends updates to the
+		// nfd-master using the gRPC interface.
+		Rules: []rbacv1.PolicyRule{
+			{
+				APIGroups: []string{""},
+				Resources: []string{"pods"},
+				Verbs:     []string{"get", "list", "watch"},
+			},
+		},
+	}
+	if *openShift {
+		cr.Rules = append(cr.Rules,
+			rbacv1.PolicyRule{
+				// 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{})
+}
+
+// Configure cluster role binding required by NFD Master
+func createClusterRoleBindingMaster(cs clientset.Interface, ns string) (*rbacv1.ClusterRoleBinding, error) {
 	crb := &rbacv1.ClusterRoleBinding{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: "nfd-master-e2e",
@@ -136,3 +204,26 @@ func createClusterRoleBinding(cs clientset.Interface, ns string) (*rbacv1.Cluste
 
 	return cs.RbacV1().ClusterRoleBindings().Update(context.TODO(), crb, metav1.UpdateOptions{})
 }
+
+// Configure cluster role binding required by NFD Topology Updater
+func createClusterRoleBindingTopologyUpdater(cs clientset.Interface, ns string) (*rbacv1.ClusterRoleBinding, error) {
+	crb := &rbacv1.ClusterRoleBinding{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "nfd-topology-updater-e2e",
+		},
+		Subjects: []rbacv1.Subject{
+			{
+				Kind:      rbacv1.ServiceAccountKind,
+				Name:      "nfd-topology-updater-e2e",
+				Namespace: ns,
+			},
+		},
+		RoleRef: rbacv1.RoleRef{
+			APIGroup: rbacv1.GroupName,
+			Kind:     "ClusterRole",
+			Name:     "nfd-topology-updater-e2e",
+		},
+	}
+
+	return cs.RbacV1().ClusterRoleBindings().Update(context.TODO(), crb, metav1.UpdateOptions{})
+}