Repository: metal3-io/baremetal-operator
Pull request #1559 (2024-02-15 23:44:39)

✨ HostFirmwareComponents CRD and Controller  & Tests
- New Controller for HostFirmwareComponents
- Updated the CRD based on kustomization
- added GetFirmwareComponentsForNode to the Provisioner interface
- implemented GetFirmwareComponentsForNode in ironic.go


**What this PR does / why we need it**:

metal3-io/metal3-docs#364


metal3-io-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please assign [honza](https://github.com/honza) for approval. 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=metal3-io%2Fbaremetal-operator).

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

- **[OWNERS](https://github.com/metal3-io/baremetal-operator/blob/main/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":["honza"]} -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
generate | 9fab17e776f588b9f16ff284ba9e306324bc1bd4 | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/generate/1758276309831651328) | true | `/test generate`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
generate | 9fab17e776f588b9f16ff284ba9e306324bc1bd4 | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/generate/1758276309831651328) | true | `/test generate`
golint | 9fab17e776f588b9f16ff284ba9e306324bc1bd4 | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/golint/1758276309877788672) | true | `/test golint`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
generate | 9fab17e776f588b9f16ff284ba9e306324bc1bd4 | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/generate/1758276309831651328) | true | `/test generate`
golint | 9fab17e776f588b9f16ff284ba9e306324bc1bd4 | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/golint/1758276309877788672) | true | `/test golint`
unit | 9fab17e776f588b9f16ff284ba9e306324bc1bd4 | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/unit/1758276309747765248) | true | `/test unit`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
golint | 9fab17e776f588b9f16ff284ba9e306324bc1bd4 | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/golint/1758276309877788672) | true | `/test golint`
unit | 9fab17e776f588b9f16ff284ba9e306324bc1bd4 | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/unit/1758276309747765248) | true | `/test unit`
generate | f862054cc368fb1a0fb3f550f01726a4fa43405f | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/generate/1758325879492055040) | true | `/test generate`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
iurygregory: /test metal3-bmo-e2e-test-pull
/test-centos-e2e-integration-main
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
generate | 3392307cde1a15227cc20d09f0df1d2abea32a8a | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/generate/1763374367120560128) | true | `/test generate`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
iurygregory: /test metal3-bmo-e2e-test-pull
/test-centos-e2e-integration-main
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
metal3-bmo-e2e-test-pull | dd3e96275ba37cfbda1de6da25e95b17dd21442a | [link](https://jenkins.nordix.org/view/Metal3/job/metal3-bmo-e2e-test-pull/311/) | true | `/test metal3-bmo-e2e-test-pull`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
metal3-bmo-e2e-test-pull | dd3e96275ba37cfbda1de6da25e95b17dd21442a | [link](https://jenkins.nordix.org/view/Metal3/job/metal3-bmo-e2e-test-pull/311/) | true | `/test metal3-bmo-e2e-test-pull`

<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 -->
iurygregory: /test metal3-bmo-e2e-test-pull
/test-centos-e2e-integration-main

Didn't find any logs in the links .-. let's try again to see
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
metal3-bmo-e2e-test-pull | dd3e96275ba37cfbda1de6da25e95b17dd21442a | [link](https://jenkins.nordix.org/view/Metal3/job/metal3-bmo-e2e-test-pull/314/) | true | `/test metal3-bmo-e2e-test-pull`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
metal3-bmo-e2e-test-pull | dd3e96275ba37cfbda1de6da25e95b17dd21442a | [link](https://jenkins.nordix.org/view/Metal3/job/metal3-bmo-e2e-test-pull/314/) | true | `/test metal3-bmo-e2e-test-pull`

<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 -->
kashifest: /test metal3-bmo-e2e-test-pull
metal3-io-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please assign [kashifest](https://github.com/kashifest) for approval. 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=metal3-io%2Fbaremetal-operator).

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

- **[OWNERS](https://github.com/metal3-io/baremetal-operator/blob/main/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":["kashifest"]} -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
golint | 18a4c9abbee3160ecea1a71c079a3838a934736e | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/golint/1765207078994972672) | true | `/test golint`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
golint | 18a4c9abbee3160ecea1a71c079a3838a934736e | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/golint/1765207078994972672) | true | `/test golint`
unit | 18a4c9abbee3160ecea1a71c079a3838a934736e | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/unit/1765207078818811904) | true | `/test unit`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
golint | 18a4c9abbee3160ecea1a71c079a3838a934736e | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/golint/1765207078994972672) | true | `/test golint`
unit | bef6f66f65dfe0d52de054ba5ae8c4ada69d58bf | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/unit/1765211200972394496) | true | `/test unit`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
unit | b4e8aeb56bc743a1970df6bbd11b342abfcb915a | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/unit/1765537599008542720) | true | `/test unit`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
unit | 3136a247a5f7fd501d78509d7dcdcae76fd5515d | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/unit/1765538263419850752) | true | `/test unit`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
unit | 12a93584b27add6a3d47f4994d0995c13e84b950 | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/unit/1765714071623045120) | true | `/test unit`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
iurygregory: /test metal3-bmo-e2e-test-pull
/test-centos-e2e-integration-main
iurygregory: /test metal3-bmo-e2e-test-pull
/test-centos-e2e-integration-main
iurygregory: /test metal3-bmo-e2e-test-pull
/test-centos-e2e-integration-main
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
metal3-bmo-e2e-test-pull | 8fefaaee3b1a855c5cb9aaf7b0979971de59283f | [link](https://jenkins.nordix.org/view/Metal3/job/metal3-bmo-e2e-test-pull/351/) | true | `/test metal3-bmo-e2e-test-pull`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
metal3-bmo-e2e-test-pull | 8fefaaee3b1a855c5cb9aaf7b0979971de59283f | [link](https://jenkins.nordix.org/view/Metal3/job/metal3-bmo-e2e-test-pull/351/) | true | `/test metal3-bmo-e2e-test-pull`

<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 -->
iurygregory: /test metal3-bmo-e2e-test-pul
metal3-io-bot: @iurygregory: The specified target(s) for `/test` were not found.
The following commands are available to trigger required jobs:
* `/test generate`
* `/test gomod`
* `/test manifestlint`
* `/test markdownlint`
* `/test metal3-bmo-e2e-test-pull`
* `/test shellcheck`
* `/test test`
* `/test unit`

The following commands are available to trigger optional jobs:
* `/test metal3-bmo-e2e-test-optional-pull`
* `/test metal3-centos-e2e-basic-test-main`
* `/test metal3-centos-e2e-basic-test-release-1-6`
* `/test metal3-ubuntu-e2e-basic-test-main`
* `/test metal3-ubuntu-e2e-basic-test-release-1-6`

Use `/test all` to run the following jobs that were automatically triggered:
* `generate`
* `gomod`
* `manifestlint`
* `unit`



<details>

In response to [this](https://github.com/metal3-io/baremetal-operator/pull/1559#issuecomment-1988344837):

>/test metal3-bmo-e2e-test-pul


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>
iurygregory: /test metal3-bmo-e2e-test-pull
metal3-io-bot: @iurygregory: 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
--- | --- | --- | --- | ---
generate | 3481c24704c77a1cdada27326291c3c02e4ea49d | [link](https://prow.apps.test.metal3.io/view/s3/prow-logs/pr-logs/pull/metal3-io_baremetal-operator/1559/generate/1768084833797410816) | true | `/test generate`

[Full PR test history](https://prow.apps.test.metal3.io/pr-history?org=metal3-io&repo=baremetal-operator&pr=1559). [Your PR dashboard](https://prow.apps.test.metal3.io/pr?query=is:pr+state:open+author:iurygregory).

<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 -->
iurygregory: /test metal3-bmo-e2e-test-pull
/test-centos-e2e-integration-main
iurygregory: /test metal3-bmo-e2e-test-pull
/test-centos-e2e-integration-main
iurygregory: /test metal3-bmo-e2e-test-pull
/test-centos-e2e-integration-main

Patch:
diff --git a/PROJECT b/PROJECT
index 01f6bffc..192a6021 100644
--- a/PROJECT
+++ b/PROJECT
@@ -64,10 +64,11 @@ resources:
 - api:
     crdVersion: v1
     namespaced: true
+  controller: true
   domain: metal3.io
   group: metal3.io
   kind: HostFirmwareComponents
-  path: github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha
+  path: github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1
   version: v1alpha1
 - api:
     crdVersion: v1
diff --git a/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types.go b/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types.go
index c4d3ad5f..4cd10143 100644
--- a/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types.go
+++ b/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types.go
@@ -22,8 +22,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.
-
 // FirmwareUpdate defines a firmware update specification.
 type FirmwareUpdate struct {
 	Component string `json:"component"`
@@ -34,9 +32,9 @@ type FirmwareUpdate struct {
 type FirmwareComponentStatus struct {
 	Component          string      `json:"component"`
 	InitialVersion     string      `json:"initialVersion"`
-	CurrentVersion     string      `json:"currentVersion"`
-	LastVersionFlashed string      `json:"lastVersionFlashed"`
-	UpdatedAt          metav1.Time `json:"updatedAt"`
+	CurrentVersion     string      `json:"currentVersion,omitempty"`
+	LastVersionFlashed string      `json:"lastVersionFlashed,omitempty"`
+	UpdatedAt          metav1.Time `json:"updatedAt,omitempty"`
 }
 
 type UpdatesConditionType string
@@ -77,7 +75,6 @@ type HostFirmwareComponentsStatus struct {
 }
 
 //+kubebuilder:object:root=true
-//+kubebuilder:resource:shortName=hfc
 //+kubebuilder:subresource:status
 
 // HostFirmwareComponents is the Schema for the hostfirmwarecomponents API.
diff --git a/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml b/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml
index 292348e5..095763de 100644
--- a/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml
+++ b/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml
@@ -11,8 +11,6 @@ spec:
     kind: HostFirmwareComponents
     listKind: HostFirmwareComponentsList
     plural: hostfirmwarecomponents
-    shortNames:
-    - hfc
     singular: hostfirmwarecomponents
   scope: Namespaced
   versions:
@@ -77,10 +75,7 @@ spec:
                       type: string
                   required:
                   - component
-                  - currentVersion
                   - initialVersion
-                  - lastVersionFlashed
-                  - updatedAt
                   type: object
                 type: array
               conditions:
diff --git a/config/base/crds/kustomization.yaml b/config/base/crds/kustomization.yaml
index 3aae0699..7912eb81 100644
--- a/config/base/crds/kustomization.yaml
+++ b/config/base/crds/kustomization.yaml
@@ -3,6 +3,7 @@
 # It should be run by config/default
 resources:
 - bases/metal3.io_baremetalhosts.yaml
+- bases/metal3.io_hostfirmwarecomponents.yaml
 - bases/metal3.io_hostfirmwaresettings.yaml
 - bases/metal3.io_firmwareschemas.yaml
 - bases/metal3.io_preprovisioningimages.yaml
@@ -15,6 +16,7 @@ patches:
 # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
 # patches here are for enabling the conversion webhook for each CRD
 #- patches/webhook_in_baremetalhosts.yaml
+#- patches/webhook_in_hostfirmwarecomponents.yaml
 #- patches/webhook_in_hostfirmwaresettings.yaml
 #- patches/webhook_in_firmwareschemas.yaml
 #- patches/webhook_in_preprovisioningimages.yaml
@@ -26,6 +28,7 @@ patches:
 # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix.
 # patches here are for enabling the CA injection for each CRD
 - path: patches/cainjection_in_baremetalhosts.yaml
+#- patches/cainjection_in_hostfirmwarecomponents.yaml
 #- patches/cainjection_in_hostfirmwaresettings.yaml
 #- patches/cainjection_in_firmwareschemas.yaml
 #- patches/cainjection_in_preprovisioningimages.yaml
diff --git a/config/base/crds/patches/cainjection_in_hostfirmwarecomponents.yaml b/config/base/crds/patches/cainjection_in_hostfirmwarecomponents.yaml
new file mode 100644
index 00000000..cb970cf5
--- /dev/null
+++ b/config/base/crds/patches/cainjection_in_hostfirmwarecomponents.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+  name: hostfirmwarecomponents.metal3.io
diff --git a/config/base/crds/patches/webhook_in_hostfirmwarecomponents.yaml b/config/base/crds/patches/webhook_in_hostfirmwarecomponents.yaml
new file mode 100644
index 00000000..e77b92c7
--- /dev/null
+++ b/config/base/crds/patches/webhook_in_hostfirmwarecomponents.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: hostfirmwarecomponents.metal3.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          namespace: system
+          name: webhook-service
+          path: /convert
+      conversionReviewVersions:
+      - v1
diff --git a/config/base/rbac/hostfirmwarecomponents_editor_role.yaml b/config/base/rbac/hostfirmwarecomponents_editor_role.yaml
index ae97a230..1646a289 100644
--- a/config/base/rbac/hostfirmwarecomponents_editor_role.yaml
+++ b/config/base/rbac/hostfirmwarecomponents_editor_role.yaml
@@ -2,6 +2,13 @@
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
+  labels:
+    app.kubernetes.io/name: clusterrole
+    app.kubernetes.io/instance: hostfirmwarecomponents-editor-role
+    app.kubernetes.io/component: rbac
+    app.kubernetes.io/created-by: baremetal-operator
+    app.kubernetes.io/part-of: baremetal-operator
+    app.kubernetes.io/managed-by: kustomize
   name: hostfirmwarecomponents-editor-role
 rules:
 - apiGroups:
@@ -9,16 +16,16 @@ rules:
   resources:
   - hostfirmwarecomponents
   verbs:
+  - create
+  - delete
   - get
   - list
-  - watch
-  - create
-  - update
   - patch
-  - delete
+  - update
+  - watch
 - apiGroups:
   - metal3.io
   resources:
   - hostfirmwarecomponents/status
   verbs:
-  - get
\ No newline at end of file
+  - get
diff --git a/config/base/rbac/hostfirmwarecomponents_viewer_role.yaml b/config/base/rbac/hostfirmwarecomponents_viewer_role.yaml
index 05cd38c6..a511d3a3 100644
--- a/config/base/rbac/hostfirmwarecomponents_viewer_role.yaml
+++ b/config/base/rbac/hostfirmwarecomponents_viewer_role.yaml
@@ -1,7 +1,14 @@
-# permissions for end users to edit hostfirmwarecomponents.
+# permissions for end users to view hostfirmwarecomponents.
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
+  labels:
+    app.kubernetes.io/name: clusterrole
+    app.kubernetes.io/instance: hostfirmwarecomponents-viewer-role
+    app.kubernetes.io/component: rbac
+    app.kubernetes.io/created-by: baremetal-operator
+    app.kubernetes.io/part-of: baremetal-operator
+    app.kubernetes.io/managed-by: kustomize
   name: hostfirmwarecomponents-viewer-role
 rules:
 - apiGroups:
@@ -17,4 +24,4 @@ rules:
   resources:
   - hostfirmwarecomponents/status
   verbs:
-  - get
\ No newline at end of file
+  - get
diff --git a/config/base/rbac/role.yaml b/config/base/rbac/role.yaml
index 837707e7..0094daea 100644
--- a/config/base/rbac/role.yaml
+++ b/config/base/rbac/role.yaml
@@ -109,6 +109,32 @@ rules:
   - patch
   - update
   - watch
+- apiGroups:
+  - metal3.io
+  resources:
+  - hostfirmwarecomponents
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - metal3.io
+  resources:
+  - hostfirmwarecomponents/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - metal3.io
+  resources:
+  - hostfirmwarecomponents/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - metal3.io
   resources:
diff --git a/config/render/capm3.yaml b/config/render/capm3.yaml
index 1f5ea863..518a174d 100644
--- a/config/render/capm3.yaml
+++ b/config/render/capm3.yaml
@@ -1601,6 +1601,187 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.12.1
+  name: hostfirmwarecomponents.metal3.io
+spec:
+  group: metal3.io
+  names:
+    kind: HostFirmwareComponents
+    listKind: HostFirmwareComponentsList
+    plural: hostfirmwarecomponents
+    singular: hostfirmwarecomponents
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: HostFirmwareComponents is the Schema for the hostfirmwarecomponents
+          API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: HostFirmwareComponentsSpec defines the desired state of HostFirmwareComponents.
+            properties:
+              updates:
+                items:
+                  description: FirmwareUpdate defines a firmware update specification.
+                  properties:
+                    component:
+                      type: string
+                    url:
+                      type: string
+                  required:
+                  - component
+                  - url
+                  type: object
+                type: array
+            required:
+            - updates
+            type: object
+          status:
+            description: HostFirmwareComponentsStatus defines the observed state of
+              HostFirmwareComponents.
+            properties:
+              components:
+                description: Components is the list of all available firmware components
+                  and their information.
+                items:
+                  description: FirmwareComponentStatus defines the status of a firmware
+                    component.
+                  properties:
+                    component:
+                      type: string
+                    currentVersion:
+                      type: string
+                    initialVersion:
+                      type: string
+                    lastVersionFlashed:
+                      type: string
+                    updatedAt:
+                      format: date-time
+                      type: string
+                  required:
+                  - component
+                  - initialVersion
+                  type: object
+                type: array
+              conditions:
+                description: Track whether updates stored in the spec are valid based
+                  on the schema
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource. --- This struct is intended for direct
+                    use as an array at the field path .status.conditions.  For example,
+                    \n type FooStatus struct{ // Represents the observations of a
+                    foo's current state. // Known .status.conditions.type are: \"Available\",
+                    \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+                    // +listType=map // +listMapKey=type Conditions []metav1.Condition
+                    `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+                    protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+                  properties:
+                    lastTransitionTime:
+                      description: lastTransitionTime is the last time the condition
+                        transitioned from one status to another. This should be when
+                        the underlying condition changed.  If that is not known, then
+                        using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: message is a human readable message indicating
+                        details about the transition. This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: observedGeneration represents the .metadata.generation
+                        that the condition was set based upon. For instance, if .metadata.generation
+                        is currently 12, but the .status.conditions[x].observedGeneration
+                        is 9, the condition is out of date with respect to the current
+                        state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: reason contains a programmatic identifier indicating
+                        the reason for the condition's last transition. Producers
+                        of specific condition types may define expected values and
+                        meanings for this field, and whether the values are considered
+                        a guaranteed API. The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                        --- Many .condition.type values are consistent across resources
+                        like Available, but because arbitrary conditions can be useful
+                        (see .node.status.conditions), the ability to deconflict is
+                        important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
+                x-kubernetes-list-map-keys:
+                - type
+                x-kubernetes-list-type: map
+              lastUpdated:
+                description: Time that the status was last updated
+                format: date-time
+                type: string
+              updates:
+                description: Updates is the list of all firmware components that should
+                  be updated they are specified via name and url fields.
+                items:
+                  description: FirmwareUpdate defines a firmware update specification.
+                  properties:
+                    component:
+                      type: string
+                    url:
+                      type: string
+                  required:
+                  - component
+                  - url
+                  type: object
+                type: array
+            required:
+            - components
+            - updates
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.12.1
@@ -2103,6 +2284,32 @@ rules:
   - patch
   - update
   - watch
+- apiGroups:
+  - metal3.io
+  resources:
+  - hostfirmwarecomponents
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - metal3.io
+  resources:
+  - hostfirmwarecomponents/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - metal3.io
+  resources:
+  - hostfirmwarecomponents/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - metal3.io
   resources:
diff --git a/config/samples/metal3.io_v1alpha1_hostfirmwarecomponents.yaml b/config/samples/metal3.io_v1alpha1_hostfirmwarecomponents.yaml
index 3283a8c7..f5b2537d 100644
--- a/config/samples/metal3.io_v1alpha1_hostfirmwarecomponents.yaml
+++ b/config/samples/metal3.io_v1alpha1_hostfirmwarecomponents.yaml
@@ -1,7 +1,13 @@
 apiVersion: metal3.io/v1alpha1
 kind: HostFirmwareComponents
 metadata:
-  namespace: host3firmwarecomponents.metal3.io
+  labels:
+    app.kubernetes.io/name: hostfirmwarecomponents
+    app.kubernetes.io/instance: hostfirmwarecomponents-sample
+    app.kubernetes.io/part-of: baremetal-operator
+    app.kubernetes.io/managed-by: kustomize
+    app.kubernetes.io/created-by: baremetal-operator
+  name: hostfirmwarecomponents-sample
 spec:
   updates:
   - name: bios
diff --git a/controllers/metal3.io/baremetalhost_controller.go b/controllers/metal3.io/baremetalhost_controller.go
index 7b9d8346..8dd6d9e4 100644
--- a/controllers/metal3.io/baremetalhost_controller.go
+++ b/controllers/metal3.io/baremetalhost_controller.go
@@ -93,10 +93,11 @@ func (info *reconcileInfo) publishEvent(reason, message string) {
 // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;update;delete
 // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch
 
-// Allow for managing hostfirmwaresettings and firmwareschema
+// Allow for managing hostfirmwaresettings, firmwareschema, bmceventsubscriptions and hostfirmwarecomponents
 //+kubebuilder:rbac:groups=metal3.io,resources=hostfirmwaresettings,verbs=get;list;watch;create;update;patch
 //+kubebuilder:rbac:groups=metal3.io,resources=firmwareschemas,verbs=get;list;watch;create;update;patch
 //+kubebuilder:rbac:groups=metal3.io,resources=bmceventsubscriptions,verbs=get;list;watch;create;update;patch
+//+kubebuilder:rbac:groups=metal3.io,resources=hostfirmwarecomponents,verbs=get;list;watch;create;update;patch
 
 // Reconcile handles changes to BareMetalHost resources.
 func (r *BareMetalHostReconciler) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
diff --git a/controllers/metal3.io/hostfirmwarecomponents_controller.go b/controllers/metal3.io/hostfirmwarecomponents_controller.go
new file mode 100644
index 00000000..3e6eedf0
--- /dev/null
+++ b/controllers/metal3.io/hostfirmwarecomponents_controller.go
@@ -0,0 +1,328 @@
+/*
+
+
+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 controllers
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	corev1 "k8s.io/api/core/v1"
+	k8serrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+
+	"github.com/go-logr/logr"
+	metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
+	"github.com/metal3-io/baremetal-operator/pkg/provisioner"
+)
+
+// HostFirmwareComponentsReconciler reconciles a HostFirmwareComponents object.
+type HostFirmwareComponentsReconciler struct {
+	client.Client
+	Log                logr.Logger
+	ProvisionerFactory provisioner.Factory
+}
+
+// rhfcInfo is used to simplify the pass or arguments.
+type rhfcInfo struct {
+	ctx    context.Context
+	log    logr.Logger
+	hfc    *metal3api.HostFirmwareComponents
+	bmh    *metal3api.BareMetalHost
+	events []corev1.Event
+}
+
+type conditionReasonHFC string
+
+const (
+	reasonInvalidComponent conditionReasonHFC = "InvalidComponent"
+	reasonValidComponent   conditionReasonHFC = "OK"
+)
+
+func (info *rhfcInfo) publishEvent(reason, message string) {
+	t := metav1.Now()
+	hfcEvent := corev1.Event{
+		ObjectMeta: metav1.ObjectMeta{
+			GenerateName: reason + "-",
+			Namespace:    info.hfc.ObjectMeta.Namespace,
+		},
+		InvolvedObject: corev1.ObjectReference{
+			Kind:       info.hfc.Kind,
+			Namespace:  info.hfc.Namespace,
+			Name:       info.hfc.Name,
+			UID:        info.hfc.UID,
+			APIVersion: metal3api.GroupVersion.String(),
+		},
+		Reason:  reason,
+		Message: message,
+		Source: corev1.EventSource{
+			Component: "metal3-hostfirmwarecomponents-controller",
+		},
+		FirstTimestamp:      t,
+		LastTimestamp:       t,
+		Count:               1,
+		Type:                corev1.EventTypeNormal,
+		ReportingController: "metal3.io/hostfirmwarecomponents-controller",
+	}
+
+	info.events = append(info.events, hfcEvent)
+}
+
+//+kubebuilder:rbac:groups=metal3.io,resources=hostfirmwarecomponents,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=metal3.io,resources=hostfirmwarecomponents/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=metal3.io,resources=hostfirmwarecomponents/finalizers,verbs=update
+
+// Reconcile handles changes to HostFirmwareComponents resources.
+func (r *HostFirmwareComponentsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
+	reqLogger := r.Log.WithValues("hostfirmwarecomponents", req.NamespacedName)
+	reqLogger.Info("start")
+
+	// Get the corresponding baremetalhost in this namespace, if one doesn't exist don't continue processing
+	bmh := &metal3api.BareMetalHost{}
+	err = r.Get(ctx, req.NamespacedName, bmh)
+	if err != nil {
+		if k8serrors.IsNotFound(err) {
+			return ctrl.Result{}, nil
+		}
+		reqLogger.Error(err, "could not get baremetal host, not running hostfirmwarecomponents reconciler")
+		return ctrl.Result{Requeue: true, RequeueAfter: resourceNotAvailableRetryDelay}, err
+	}
+
+	if hasDetachedAnnotation(bmh) {
+		reqLogger.Info("the host is detached, not running hostfirmwarecomponents reconciler")
+		return ctrl.Result{Requeue: true, RequeueAfter: unmanagedRetryDelay}, nil
+	}
+	// If the reconciliation is paused, requeue
+	annotations := bmh.GetAnnotations()
+
+	if _, ok := annotations[metal3api.PausedAnnotation]; ok {
+		reqLogger.Info("host is paused, no work to do")
+		return ctrl.Result{Requeue: true, RequeueAfter: subResourceNotReadyRetryDelay}, nil
+	}
+
+	// Fetch the HostFirmwareComponents
+	hfc := &metal3api.HostFirmwareComponents{}
+	info := &rhfcInfo{ctx: ctx, log: reqLogger, hfc: hfc, bmh: bmh}
+	if err = r.Get(ctx, req.NamespacedName, hfc); err != nil {
+		// The HFC resource may have been deleted
+		if k8serrors.IsNotFound(err) {
+			reqLogger.Info("HostFirmwareComponents not found")
+			return ctrl.Result{Requeue: false}, err
+		}
+		// Error reading the object - requeue the request.
+		return ctrl.Result{}, fmt.Errorf("could not load hostFirmwareComponents: %w", err)
+	}
+
+	// Create a provisioner to access Ironic API
+	prov, err := r.ProvisionerFactory.NewProvisioner(ctx, provisioner.BuildHostDataNoBMC(*bmh), info.publishEvent)
+	if err != nil {
+		return ctrl.Result{}, fmt.Errorf("failed to create provisioner: %w", err)
+	}
+
+	ready, err := prov.TryInit()
+	if err != nil || !ready {
+		var msg string
+		if err == nil {
+			msg = "not ready"
+		} else {
+			msg = err.Error()
+		}
+		reqLogger.Info("provisioner is not ready", "Error", msg, "RequeueAfter", provisionerRetryDelay)
+		return ctrl.Result{Requeue: true, RequeueAfter: provisionerRetryDelay}, nil
+	}
+
+	newStatus, err := r.updateHostFirmware(info)
+	if err != nil {
+		return ctrl.Result{}, fmt.Errorf("could not update hostfirmwarecomponents: %w", err)
+	}
+
+	// Check ironic for the components information if possible
+	components, err := prov.GetFirmwareComponents()
+	info.log.Info("retrieving firmware components and saving to resource", "Node", bmh.Status.Provisioning.ID)
+
+	if err != nil {
+		reqLogger.Error(err, "provisioner returns error", "RequeueAfter", provisionerRetryDelay)
+		setUpdatesCondition(info.hfc.GetGeneration(), &newStatus, info, metal3api.HostFirmwareComponentsValid, metav1.ConditionFalse, reasonInvalidComponent, err.Error())
+		return ctrl.Result{Requeue: true, RequeueAfter: provisionerRetryDelay}, err
+	}
+
+	if err = r.updateHostFirmwareComponents(newStatus, components, info); err != nil {
+		return ctrl.Result{Requeue: false}, err
+	}
+
+	for _, e := range info.events {
+		r.publishEvent(info.ctx, req, e)
+	}
+
+	if meta.IsStatusConditionTrue(info.hfc.Status.Conditions, string(metal3api.HostFirmwareComponentsChangeDetected)) {
+		return ctrl.Result{Requeue: true, RequeueAfter: reconcilerRequeueDelayChangeDetected}, nil
+	}
+	return ctrl.Result{Requeue: true, RequeueAfter: reconcilerRequeueDelay}, nil
+}
+
+// Update the HostFirmwareComponents resource using the components from provisioner.
+func (r *HostFirmwareComponentsReconciler) updateHostFirmware(info *rhfcInfo) (newStatus metal3api.HostFirmwareComponentsStatus, err error) {
+	dirty := false
+
+	// change the Updates in Status
+	newStatus.Updates = info.hfc.Spec.Updates
+
+	// Check if the updates in the Spec are different than Status
+	updatesMismatch := !reflect.DeepEqual(info.hfc.Status.Updates, info.hfc.Spec.Updates)
+
+	reason := reasonValidComponent
+	generation := info.hfc.GetGeneration()
+
+	if updatesMismatch {
+		if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsChangeDetected, metav1.ConditionTrue, reason, "") {
+			dirty = true
+		}
+
+		err := r.validateHostFirmwareComponents(info)
+		if err != nil {
+			info.publishEvent("ValidationFailed", fmt.Sprintf("Invalid Firmware Components: %s", err))
+			reason = reasonInvalidComponent
+			if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsValid, metav1.ConditionFalse, reason, fmt.Sprintf("Invalid Firmware Components: %s", err)) {
+				dirty = true
+			}
+		} else if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsValid, metav1.ConditionTrue, reason, "") {
+			dirty = true
+		}
+	} else {
+		if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsValid, metav1.ConditionTrue, reason, "") {
+			dirty = true
+		}
+		if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsChangeDetected, metav1.ConditionFalse, reason, "") {
+			dirty = true
+		}
+	}
+
+	// Update Status if has changed
+	if dirty {
+		info.log.Info("Status for HostFirmwareComponents changed")
+		info.hfc.Status = *newStatus.DeepCopy()
+
+		t := metav1.Now()
+		info.hfc.Status.LastUpdated = &t
+		return newStatus, r.Status().Update(info.ctx, info.hfc)
+	}
+	return newStatus, nil
+}
+
+// Update the HostFirmwareComponents resource using the components from provisioner.
+func (r *HostFirmwareComponentsReconciler) updateHostFirmwareComponents(newStatus metal3api.HostFirmwareComponentsStatus, components []metal3api.FirmwareComponentStatus, info *rhfcInfo) (err error) {
+	dirty := false
+	// change the Components in Status
+	newStatus.Components = components
+	// Check if the components information we retrieved is different from the one in Status
+	componentsInfoMismatch := !reflect.DeepEqual(components, info.hfc.Status.Components)
+	reason := reasonValidComponent
+	generation := info.hfc.GetGeneration()
+	// Log the components we have
+	info.log.Info("firmware components for node", "components", components, "bmh", info.bmh.Name)
+	if componentsInfoMismatch {
+		setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsChangeDetected, metav1.ConditionTrue, reason, "")
+		dirty = true
+	}
+	if dirty {
+		info.log.Info("Components Status for HostFirmwareComponents changed")
+		info.hfc.Status = *newStatus.DeepCopy()
+
+		t := metav1.Now()
+		info.hfc.Status.LastUpdated = &t
+		return r.Status().Update(info.ctx, info.hfc)
+	}
+	return nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *HostFirmwareComponentsReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrentReconcile int) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&metal3api.HostFirmwareComponents{}).
+		WithOptions(controller.Options{MaxConcurrentReconciles: maxConcurrentReconcile}).
+		WithEventFilter(
+			predicate.Funcs{
+				UpdateFunc: r.updateEventHandler,
+			}).
+		Complete(r)
+}
+
+func (r *HostFirmwareComponentsReconciler) updateEventHandler(e event.UpdateEvent) bool {
+	r.Log.Info("hostfirmwarecomponents in event handler")
+
+	return e.ObjectNew.GetGeneration() != e.ObjectOld.GetGeneration()
+}
+
+func (r *HostFirmwareComponentsReconciler) validateHostFirmwareComponents(info *rhfcInfo) []error {
+	var errors []error
+	allowedNames := map[string]struct{}{"bmc": {}, "bios": {}}
+	for _, update := range info.hfc.Spec.Updates {
+		componentName := update.Component
+		if _, ok := allowedNames[componentName]; !ok {
+			errors = append(errors, fmt.Errorf("component %s is invalid, only 'bmc' or 'bios' are allowed as update names", componentName))
+		}
+		if len(errors) == 0 {
+			componentInStatus := false
+			for _, componentStatus := range info.hfc.Status.Components {
+				if componentName == componentStatus.Component {
+					componentInStatus = true
+					break
+				}
+			}
+			if !componentInStatus {
+				errors = append(errors, fmt.Errorf("component %s is invalid because is not present in status", componentName))
+			}
+		}
+	}
+
+	return errors
+}
+
+func (r *HostFirmwareComponentsReconciler) publishEvent(ctx context.Context, request ctrl.Request, event corev1.Event) {
+	reqLogger := r.Log.WithValues("hostfirmwarecomponents", request.NamespacedName)
+	reqLogger.Info("publishing event", "reason", event.Reason, "message", event.Message)
+	err := r.Create(ctx, &event)
+	if err != nil {
+		reqLogger.Info("failed to record event, ignoring",
+			"reason", event.Reason, "message", event.Message, "error", err)
+	}
+}
+
+func setUpdatesCondition(generation int64, status *metal3api.HostFirmwareComponentsStatus, info *rhfcInfo,
+	cond metal3api.UpdatesConditionType, newStatus metav1.ConditionStatus,
+	reason conditionReasonHFC, message string) bool {
+	newCondition := metav1.Condition{
+		Type:               string(cond),
+		Status:             newStatus,
+		ObservedGeneration: generation,
+		Reason:             string(reason),
+		Message:            message,
+	}
+	currCond := meta.FindStatusCondition(info.hfc.Status.Conditions, string(cond))
+	meta.SetStatusCondition(&status.Conditions, newCondition)
+
+	if currCond == nil || currCond.Status != newStatus {
+		return true
+	}
+	return false
+}
diff --git a/main.go b/main.go
index 47ce07e4..b5aa8d05 100644
--- a/main.go
+++ b/main.go
@@ -291,6 +291,15 @@ func main() {
 		os.Exit(1)
 	}
 
+	if err = (&metal3iocontroller.HostFirmwareComponentsReconciler{
+		Client:             mgr.GetClient(),
+		Log:                ctrl.Log.WithName("controllers").WithName("HostFirmwareComponents"),
+		ProvisionerFactory: provisionerFactory,
+	}).SetupWithManager(mgr, maxConcurrency); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "HostFirmwareComponents")
+		os.Exit(1)
+	}
+
 	setupChecks(mgr)
 
 	if enableWebhook {
diff --git a/pkg/provisioner/demo/demo.go b/pkg/provisioner/demo/demo.go
index f6741975..77e61b02 100644
--- a/pkg/provisioner/demo/demo.go
+++ b/pkg/provisioner/demo/demo.go
@@ -302,3 +302,7 @@ func (p *demoProvisioner) AddBMCEventSubscriptionForNode(_ *metal3api.BMCEventSu
 func (p *demoProvisioner) RemoveBMCEventSubscriptionForNode(_ metal3api.BMCEventSubscription) (result provisioner.Result, err error) {
 	return result, nil
 }
+
+func (p *demoProvisioner) GetFirmwareComponents() (components []metal3api.FirmwareComponentStatus, err error) {
+	return components, nil
+}
diff --git a/pkg/provisioner/fixture/fixture.go b/pkg/provisioner/fixture/fixture.go
index 8005d07a..6b004a31 100644
--- a/pkg/provisioner/fixture/fixture.go
+++ b/pkg/provisioner/fixture/fixture.go
@@ -349,3 +349,7 @@ func (p *fixtureProvisioner) AddBMCEventSubscriptionForNode(_ *metal3api.BMCEven
 func (p *fixtureProvisioner) RemoveBMCEventSubscriptionForNode(_ metal3api.BMCEventSubscription) (result provisioner.Result, err error) {
 	return result, nil
 }
+
+func (p *fixtureProvisioner) GetFirmwareComponents() (components []metal3api.FirmwareComponentStatus, err error) {
+	return components, nil
+}
diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go
index 5fe505e8..a9e6c793 100644
--- a/pkg/provisioner/ironic/ironic.go
+++ b/pkg/provisioner/ironic/ironic.go
@@ -1152,6 +1152,53 @@ func (p *ironicProvisioner) GetFirmwareSettings(includeSchema bool) (settings me
 	return settings, schema, nil
 }
 
+// GetFirmwareComponents gets all available firmware components for a node and return a list.
+func (p *ironicProvisioner) GetFirmwareComponents() ([]metal3api.FirmwareComponentStatus, error) {
+	ironicNode, err := p.getNode()
+	if err != nil {
+		return nil, fmt.Errorf("could not get node to retrieve firmware components: %w", err)
+	}
+
+	if !p.availableFeatures.HasFirmwareUpdates() {
+		return nil, fmt.Errorf("current ironic version does not support firmware updates")
+	}
+	// Get the components from Ironic via Gophercloud
+	componentList, componentListErr := nodes.ListFirmware(p.ctx, p.client, ironicNode.UUID).Extract()
+
+	if componentListErr != nil {
+		bmcAccess, _ := p.bmcAccess()
+		if bmcAccess.FirmwareInterface() == "no-firmware" {
+			return nil, fmt.Errorf("node %s is using firmware interface %s: %w", ironicNode.UUID, bmcAccess.FirmwareInterface(), componentListErr)
+		}
+
+		return nil, fmt.Errorf("could not get firmware components for node %s: %w", ironicNode.UUID, componentListErr)
+	}
+
+	// Setting to 2 since we only support bmc and bios
+	componentsInfo := make([]metal3api.FirmwareComponentStatus, 0, 2)
+
+	// Iterate over the list of components to extract their information and update the list.
+	for _, fwc := range componentList {
+		if fwc.Component != "bios" && fwc.Component != "bmc" {
+			p.log.Info("ignoring firmware component for node", "component", fwc.Component, "node", ironicNode.UUID)
+			continue
+		}
+		component := metal3api.FirmwareComponentStatus{
+			Component:          fwc.Component,
+			InitialVersion:     fwc.InitialVersion,
+			CurrentVersion:     fwc.CurrentVersion,
+			LastVersionFlashed: fwc.LastVersionFlashed,
+			UpdatedAt: metav1.Time{
+				Time: *fwc.UpdatedAt,
+			},
+		}
+		componentsInfo = append(componentsInfo, component)
+		p.log.Info("firmware component added for node", "component", fwc.Component, "node", ironicNode.UUID)
+	}
+
+	return componentsInfo, componentListErr
+}
+
 // We can't just replace the capabilities because we need to keep the
 // values provided by inspection. We can't replace only the boot_mode
 // because the API isn't fine-grained enough for that. So we have to
diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go
index 29a15d54..7a43209f 100644
--- a/pkg/provisioner/provisioner.go
+++ b/pkg/provisioner/provisioner.go
@@ -200,6 +200,9 @@ type Provisioner interface {
 
 	// RemoveBMCEventSubscriptionForNode delete the subscription
 	RemoveBMCEventSubscriptionForNode(subscription metal3api.BMCEventSubscription) (result Result, err error)
+
+	// GetFirmwareComponents gets all firmware components available from a note
+	GetFirmwareComponents() (components []metal3api.FirmwareComponentStatus, err error)
 }
 
 // Result holds the response from a call in the Provsioner API.


Test patch:
diff --git a/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types_test.go b/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types_test.go
index e3b3ab85..649a4933 100644
--- a/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types_test.go
+++ b/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types_test.go
@@ -24,7 +24,6 @@ func TestValidateHostFirmwareComponents(t *testing.T) {
 					Component:      "bios",
 					InitialVersion: "1.0",
 					CurrentVersion: "1.0",
-					UpdatedAt:      metav1.NewTime(time.Now()),
 				},
 				{
 					Component:          "bmc",
diff --git a/controllers/metal3.io/host_state_machine_test.go b/controllers/metal3.io/host_state_machine_test.go
index fd02ecd2..987a134d 100644
--- a/controllers/metal3.io/host_state_machine_test.go
+++ b/controllers/metal3.io/host_state_machine_test.go
@@ -1397,6 +1397,10 @@ func (m *mockProvisioner) RemoveBMCEventSubscriptionForNode(_ metal3api.BMCEvent
 	return result, nil
 }
 
+func (p *mockProvisioner) GetFirmwareComponents() (components []metal3api.FirmwareComponentStatus, err error) {
+	return components, nil
+}
+
 func TestUpdateBootModeStatus(t *testing.T) {
 	testCases := []struct {
 		Scenario       string
diff --git a/controllers/metal3.io/hostfirmwarecomponents_test.go b/controllers/metal3.io/hostfirmwarecomponents_test.go
new file mode 100644
index 00000000..1998d1a7
--- /dev/null
+++ b/controllers/metal3.io/hostfirmwarecomponents_test.go
@@ -0,0 +1,550 @@
+package controllers
+
+import (
+	"context"
+	"testing"
+
+	metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
+	"github.com/stretchr/testify/assert"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+// Test support for HostFirmwareComponents in the HostFirmwareComponentsReconciler.
+func getTestHFCReconciler(host *metal3api.HostFirmwareComponents) *HostFirmwareComponentsReconciler {
+	c := fakeclient.NewClientBuilder().WithRuntimeObjects(host).WithStatusSubresource(host).Build()
+
+	reconciler := &HostFirmwareComponentsReconciler{
+		Client: c,
+		Log:    ctrl.Log.WithName("test_reconciler").WithName("HostFirmwareComponents"),
+	}
+
+	return reconciler
+}
+
+func getMockHFCProvisioner(components []metal3api.FirmwareComponentStatus) *hfcMockProvisioner {
+	return &hfcMockProvisioner{
+		Components: components,
+		Error:      nil,
+	}
+}
+
+type hfcMockProvisioner struct {
+	Components []metal3api.FirmwareComponentStatus
+	Error      error
+}
+
+func (m *hfcMockProvisioner) GetFirmwareComponents() (components []metal3api.FirmwareComponentStatus, err error) {
+	return m.Components, m.Error
+}
+
+// Mock components to return from provisioner.
+func getCurrentComponents(updatedComponents string) []metal3api.FirmwareComponentStatus {
+	var components []metal3api.FirmwareComponentStatus
+	switch updatedComponents {
+	case "bmc":
+		components = []metal3api.FirmwareComponentStatus{
+			{
+				Component:          "bmc",
+				InitialVersion:     "1.0.0",
+				CurrentVersion:     "1.1.0",
+				LastVersionFlashed: "1.1.0",
+			},
+			{
+				Component:      "bios",
+				InitialVersion: "1.0.1",
+				CurrentVersion: "1.0.1",
+			},
+		}
+	case "bios":
+		components = []metal3api.FirmwareComponentStatus{
+			{
+				Component:      "bmc",
+				InitialVersion: "1.0.0",
+				CurrentVersion: "1.0.0",
+			},
+			{
+				Component:          "bios",
+				InitialVersion:     "1.0.1",
+				CurrentVersion:     "1.1.10",
+				LastVersionFlashed: "1.1.10",
+			},
+		}
+	default:
+		components = []metal3api.FirmwareComponentStatus{
+			{
+				Component:          "bmc",
+				InitialVersion:     "1.0.0",
+				CurrentVersion:     "1.1.0",
+				LastVersionFlashed: "1.1.0",
+			},
+			{
+				Component:          "bios",
+				InitialVersion:     "1.0.1",
+				CurrentVersion:     "1.1.10",
+				LastVersionFlashed: "1.1.10",
+			},
+		}
+	}
+
+	return components
+}
+
+// Create the baremetalhost reconciler and use that to create bmh in same namespace.
+func createBaremetalHostHFC() *metal3api.BareMetalHost {
+	bmh := &metal3api.BareMetalHost{}
+	bmh.ObjectMeta = metav1.ObjectMeta{Name: "hostName", Namespace: "hostNamespace"}
+	c := fakeclient.NewFakeClient(bmh)
+
+	reconciler := &BareMetalHostReconciler{
+		Client:             c,
+		ProvisionerFactory: nil,
+		Log:                ctrl.Log.WithName("bmh_reconciler").WithName("BareMetalHost"),
+	}
+	reconciler.Create(context.TODO(), bmh)
+
+	return bmh
+}
+
+// Create and HFC with input spec components.
+func getHFC(spec metal3api.HostFirmwareComponentsSpec) *metal3api.HostFirmwareComponents {
+	hfc := &metal3api.HostFirmwareComponents{}
+
+	hfc.Status = metal3api.HostFirmwareComponentsStatus{
+		Components: []metal3api.FirmwareComponentStatus{
+			{
+				Component:      "bmc",
+				InitialVersion: "1.0.0",
+				CurrentVersion: "1.0.0",
+			},
+			{
+				Component:      "bios",
+				InitialVersion: "1.0.1",
+				CurrentVersion: "1.0.1",
+			},
+		},
+	}
+
+	hfc.TypeMeta = metav1.TypeMeta{
+		Kind:       "HostFirmwareComponents",
+		APIVersion: "metal3.io/v1alpha1"}
+	hfc.ObjectMeta = metav1.ObjectMeta{
+		Name:      hostName,
+		Namespace: hostNamespace}
+
+	hfc.Spec = spec
+	return hfc
+}
+
+// Test the hostfirmwarecomponents reconciler functions.
+func TestStoreHostFirmwareComponents(t *testing.T) {
+	testCases := []struct {
+		Scenario           string
+		UpdatedComponents  string
+		CurrentHFCResource *metal3api.HostFirmwareComponents
+		ExpectedComponents *metal3api.HostFirmwareComponents
+	}{
+		{
+			Scenario:          "update bmc",
+			UpdatedComponents: "bmc",
+			CurrentHFCResource: &metal3api.HostFirmwareComponents{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "HostFirmwareComponents",
+					APIVersion: "metal3.io/v1alpha1"},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:            "hostName",
+					Namespace:       "hostNamespace",
+					ResourceVersion: "1"},
+				Spec: metal3api.HostFirmwareComponentsSpec{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bmc",
+							URL:       "https://myurl/newbmcfirmware",
+						},
+					},
+				},
+				Status: metal3api.HostFirmwareComponentsStatus{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bmc",
+							URL:       "https://myurls/newbmcfirmware",
+						},
+					},
+					Components: []metal3api.FirmwareComponentStatus{
+						{
+							Component:          "bmc",
+							InitialVersion:     "1.0.0",
+							CurrentVersion:     "1.1.0",
+							LastVersionFlashed: "1.1.0",
+						},
+						{
+							Component:      "bios",
+							InitialVersion: "1.0.1",
+							CurrentVersion: "1.0.1",
+						},
+					},
+				},
+			},
+			ExpectedComponents: &metal3api.HostFirmwareComponents{
+				Spec: metal3api.HostFirmwareComponentsSpec{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bmc",
+							URL:       "https://myurl/newbmcfirmware",
+						},
+					},
+				},
+				Status: metal3api.HostFirmwareComponentsStatus{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bmc",
+							URL:       "https://myurl/newbmcfirmware",
+						},
+					},
+					Components: []metal3api.FirmwareComponentStatus{
+						{
+							Component:          "bmc",
+							InitialVersion:     "1.0.0",
+							CurrentVersion:     "1.1.0",
+							LastVersionFlashed: "1.1.0",
+						},
+						{
+							Component:      "bios",
+							InitialVersion: "1.0.1",
+							CurrentVersion: "1.0.1",
+						},
+					},
+					Conditions: []metav1.Condition{
+						{Type: "ChangeDetected", Status: "True", Reason: "OK"},
+						{Type: "Valid", Status: "True", Reason: "OK"},
+					},
+				},
+			},
+		},
+		{
+			Scenario:          "update bios",
+			UpdatedComponents: "bios",
+			CurrentHFCResource: &metal3api.HostFirmwareComponents{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "HostFirmwareComponents",
+					APIVersion: "metal3.io/v1alpha1"},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:            "hostName",
+					Namespace:       "hostNamespace",
+					ResourceVersion: "1"},
+				Spec: metal3api.HostFirmwareComponentsSpec{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bios",
+							URL:       "https://myurl/newbiosfirmware",
+						},
+					},
+				},
+				Status: metal3api.HostFirmwareComponentsStatus{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bios",
+							URL:       "https://myurls/newbiosfirmware",
+						},
+					},
+					Components: []metal3api.FirmwareComponentStatus{
+						{
+							Component:      "bmc",
+							InitialVersion: "1.0.0",
+							CurrentVersion: "1.0.0",
+						},
+						{
+							Component:          "bios",
+							InitialVersion:     "1.0.1",
+							CurrentVersion:     "1.1.10",
+							LastVersionFlashed: "1.1.10",
+						},
+					},
+				},
+			},
+			ExpectedComponents: &metal3api.HostFirmwareComponents{
+				Spec: metal3api.HostFirmwareComponentsSpec{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bios",
+							URL:       "https://myurl/newbiosfirmware",
+						},
+					},
+				},
+				Status: metal3api.HostFirmwareComponentsStatus{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bios",
+							URL:       "https://myurl/newbiosfirmware",
+						},
+					},
+					Components: []metal3api.FirmwareComponentStatus{
+						{
+							Component:      "bmc",
+							InitialVersion: "1.0.0",
+							CurrentVersion: "1.0.0",
+						},
+						{
+							Component:          "bios",
+							InitialVersion:     "1.0.1",
+							CurrentVersion:     "1.1.10",
+							LastVersionFlashed: "1.1.10",
+						},
+					},
+					Conditions: []metav1.Condition{
+						{Type: "ChangeDetected", Status: "True", Reason: "OK"},
+						{Type: "Valid", Status: "True", Reason: "OK"},
+					},
+				},
+			},
+		},
+		{
+			Scenario:          "update all",
+			UpdatedComponents: "all",
+			CurrentHFCResource: &metal3api.HostFirmwareComponents{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "HostFirmwareComponents",
+					APIVersion: "metal3.io/v1alpha1"},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:            "hostName",
+					Namespace:       "hostNamespace",
+					ResourceVersion: "1"},
+				Spec: metal3api.HostFirmwareComponentsSpec{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bmc",
+							URL:       "https://myurl/newbmcfirmware",
+						},
+						{
+							Component: "bios",
+							URL:       "https://myurl/newbiosfirmware",
+						},
+					},
+				},
+				Status: metal3api.HostFirmwareComponentsStatus{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bmc",
+							URL:       "https://myurl/newbmcfirmware",
+						},
+						{
+							Component: "bios",
+							URL:       "https://myurls/newbiosfirmware",
+						},
+					},
+					Components: []metal3api.FirmwareComponentStatus{
+						{
+							Component:          "bmc",
+							InitialVersion:     "1.0.0",
+							CurrentVersion:     "1.1.0",
+							LastVersionFlashed: "1.1.0",
+						},
+						{
+							Component:          "bios",
+							InitialVersion:     "1.0.1",
+							CurrentVersion:     "1.1.10",
+							LastVersionFlashed: "1.1.10",
+						},
+					},
+				},
+			},
+			ExpectedComponents: &metal3api.HostFirmwareComponents{
+				Spec: metal3api.HostFirmwareComponentsSpec{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bmc",
+							URL:       "https://myurl/newbmcfirmware",
+						},
+						{
+							Component: "bios",
+							URL:       "https://myurl/newbiosfirmware",
+						},
+					},
+				},
+				Status: metal3api.HostFirmwareComponentsStatus{
+					Updates: []metal3api.FirmwareUpdate{
+						{
+							Component: "bmc",
+							URL:       "https://myurl/newbmcfirmware",
+						},
+						{
+							Component: "bios",
+							URL:       "https://myurl/newbiosfirmware",
+						},
+					},
+					Components: []metal3api.FirmwareComponentStatus{
+						{
+							Component:          "bmc",
+							InitialVersion:     "1.0.0",
+							CurrentVersion:     "1.1.0",
+							LastVersionFlashed: "1.1.0",
+						},
+						{
+							Component:          "bios",
+							InitialVersion:     "1.0.1",
+							CurrentVersion:     "1.1.10",
+							LastVersionFlashed: "1.1.10",
+						},
+					},
+					Conditions: []metav1.Condition{
+						{Type: "ChangeDetected", Status: "True", Reason: "OK"},
+						{Type: "Valid", Status: "True", Reason: "OK"},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.Scenario, func(t *testing.T) {
+			ctx := context.TODO()
+			prov := getMockHFCProvisioner(getCurrentComponents(tc.UpdatedComponents))
+
+			tc.ExpectedComponents.TypeMeta = metav1.TypeMeta{
+				Kind:       "HostFirmwareComponents",
+				APIVersion: "metal3.io/v1alpha1"}
+			tc.ExpectedComponents.ObjectMeta = metav1.ObjectMeta{
+				Name:            "hostName",
+				Namespace:       "hostNamespace",
+				ResourceVersion: "2"}
+
+			hfc := tc.CurrentHFCResource
+			r := getTestHFCReconciler(hfc)
+			// Create a bmh resource needed by hfc reconciler
+			bmh := createBaremetalHostHFC()
+
+			info := &rhfcInfo{
+				ctx: ctx,
+				log: logf.Log.WithName("controllers").WithName("HostFirmwareComponents"),
+				hfc: tc.CurrentHFCResource,
+				bmh: bmh,
+			}
+
+			currentStatus, err := r.updateHostFirmware(info)
+			assert.NoError(t, err)
+
+			components, err := prov.GetFirmwareComponents()
+			assert.NoError(t, err)
+			err = r.updateHostFirmwareComponents(currentStatus, components, info)
+			assert.NoError(t, err)
+
+			// Check that resources get created or updated
+			key := client.ObjectKey{
+				Namespace: hfc.ObjectMeta.Namespace, Name: hfc.ObjectMeta.Name}
+			actual := &metal3api.HostFirmwareComponents{}
+			err = r.Client.Get(ctx, key, actual)
+			assert.Equal(t, nil, err)
+
+			// Ensure ExpectedComponents matches actual
+			assert.Equal(t, tc.ExpectedComponents.Spec.Updates, actual.Spec.Updates)
+			assert.Equal(t, tc.ExpectedComponents.Status.Components, actual.Status.Components)
+			assert.Equal(t, tc.ExpectedComponents.Status.Updates, actual.Status.Updates)
+			currentTime := metav1.Now()
+			tc.ExpectedComponents.Status.LastUpdated = &currentTime
+			actual.Status.LastUpdated = &currentTime
+			for i := range tc.ExpectedComponents.Status.Conditions {
+				tc.ExpectedComponents.Status.Conditions[i].LastTransitionTime = currentTime
+				actual.Status.Conditions[i].LastTransitionTime = currentTime
+			}
+			assert.Equal(t, tc.ExpectedComponents.Status.LastUpdated, actual.Status.LastUpdated)
+			assert.Equal(t, tc.ExpectedComponents.Status.Conditions, actual.Status.Conditions)
+		})
+	}
+}
+
+// Test the function to validate the components in the Spec.
+func TestValidadeHostFirmwareComponents(t *testing.T) {
+	testCases := []struct {
+		Scenario       string
+		SpecUpdates    metal3api.HostFirmwareComponentsSpec
+		ExpectedErrors []string
+	}{
+		{
+			Scenario: "valid spec - all components",
+			SpecUpdates: metal3api.HostFirmwareComponentsSpec{
+				Updates: []metal3api.FirmwareUpdate{
+					{Component: "bmc", URL: "https://myurl/mybmcfw"},
+					{Component: "bios", URL: "https://myurl/mybiosfw"},
+				},
+			},
+			ExpectedErrors: []string{""},
+		},
+		{
+			Scenario: "valid spec - only bios",
+			SpecUpdates: metal3api.HostFirmwareComponentsSpec{
+				Updates: []metal3api.FirmwareUpdate{
+					{Component: "bios", URL: "https://myurl/mybiosfw"},
+				},
+			},
+			ExpectedErrors: []string{""},
+		},
+		{
+			Scenario: "valid spec - only bmc",
+			SpecUpdates: metal3api.HostFirmwareComponentsSpec{
+				Updates: []metal3api.FirmwareUpdate{
+					{Component: "bmc", URL: "https://myurl/mybmcfw"},
+				},
+			},
+			ExpectedErrors: []string{""},
+		},
+		{
+			Scenario: "invalid nic component",
+			SpecUpdates: metal3api.HostFirmwareComponentsSpec{
+				Updates: []metal3api.FirmwareUpdate{
+					{Component: "nic", URL: "https://myurl/mynicfw"},
+				},
+			},
+			ExpectedErrors: []string{"component nic is invalid, only 'bmc' or 'bios' are allowed as update names"},
+		},
+		{
+			Scenario: "invalid nic component with other valid components",
+			SpecUpdates: metal3api.HostFirmwareComponentsSpec{
+				Updates: []metal3api.FirmwareUpdate{
+					{Component: "bmc", URL: "https://myurl/mybmcfw"},
+					{Component: "bios", URL: "https://myurl/mybiosfw"},
+					{Component: "nic", URL: "https://myurl/mynicfw"},
+				},
+			},
+			ExpectedErrors: []string{"component nic is invalid, only 'bmc' or 'bios' are allowed as update names"},
+		},
+		{
+			Scenario: "component not in lowercase",
+			SpecUpdates: metal3api.HostFirmwareComponentsSpec{
+				Updates: []metal3api.FirmwareUpdate{
+					{Component: "BMC", URL: "https://myurl/mybmcfw"},
+					{Component: "BIOS", URL: "https://myurl/mybiosfw"},
+				},
+			},
+			ExpectedErrors: []string{
+				"component BMC is invalid, only 'bmc' or 'bios' are allowed as update names",
+				"component BIOS is invalid, only 'bmc' or 'bios' are allowed as update names",
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.Scenario, func(t *testing.T) {
+			ctx := context.TODO()
+			hfc := getHFC(tc.SpecUpdates)
+			r := getTestHFCReconciler(hfc)
+			info := rhfcInfo{
+				ctx: ctx,
+				log: logf.Log.WithName("controllers").WithName("HostFirmwareComponents"),
+				hfc: hfc,
+			}
+			errors := r.validateHostFirmwareComponents(&info)
+			if len(errors) == 0 {
+				assert.Equal(t, tc.ExpectedErrors[0], "")
+			} else {
+				for i := range errors {
+					assert.Equal(t, tc.ExpectedErrors[i], errors[i].Error())
+				}
+			}
+		})
+	}
+}