diff --git a/PROJECT b/PROJECT index 5241f3d5a8..6443831ba3 100644 --- a/PROJECT +++ b/PROJECT @@ -1,3 +1,7 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: metal3.io layout: - go.kubebuilder.io/v3 @@ -64,9 +68,10 @@ 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 version: "3" diff --git a/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types.go b/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types.go index c4d3ad5f9a..6c01c36513 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 diff --git a/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types_test.go b/apis/metal3.io/v1alpha1/hostfirmwarecomponents_types_test.go index e3b3ab858c..649a49334f 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/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml b/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml index 292348e573..9c380cb15c 100644 --- a/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml +++ b/config/base/crds/bases/metal3.io_hostfirmwarecomponents.yaml @@ -77,10 +77,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 85b4033a0e..1c19937cd4 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 @@ -14,6 +15,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 @@ -24,6 +26,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 0000000000..cb970cf5d8 --- /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 0000000000..e77b92c7c7 --- /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 ae97a230e2..1646a289e2 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 05cd38c6de..a511d3a3c2 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 837707e704..8d7e14b06c 100644 --- a/config/base/rbac/role.yaml +++ b/config/base/rbac/role.yaml @@ -109,6 +109,17 @@ rules: - patch - update - watch +- apiGroups: + - metal3.io + resources: + - hostfirmwarecomponents + verbs: + - create + - get + - list + - patch + - update + - watch - apiGroups: - metal3.io resources: @@ -149,3 +160,29 @@ rules: - get - patch - update +- apiGroups: + - metal3.io.metal3.io + resources: + - hostfirmwarecomponents + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal3.io.metal3.io + resources: + - hostfirmwarecomponents/finalizers + verbs: + - update +- apiGroups: + - metal3.io.metal3.io + resources: + - hostfirmwarecomponents/status + verbs: + - get + - patch + - update diff --git a/config/render/capm3.yaml b/config/render/capm3.yaml index d7e1140690..3d4f85484f 100644 --- a/config/render/capm3.yaml +++ b/config/render/capm3.yaml @@ -1526,6 +1526,189 @@ 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 + shortNames: + - hfc + 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 @@ -2028,6 +2211,17 @@ rules: - patch - update - watch +- apiGroups: + - metal3.io + resources: + - hostfirmwarecomponents + verbs: + - create + - get + - list + - patch + - update + - watch - apiGroups: - metal3.io resources: @@ -2068,6 +2262,32 @@ rules: - get - patch - update +- apiGroups: + - metal3.io.metal3.io + resources: + - hostfirmwarecomponents + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal3.io.metal3.io + resources: + - hostfirmwarecomponents/finalizers + verbs: + - update +- apiGroups: + - metal3.io.metal3.io + resources: + - hostfirmwarecomponents/status + verbs: + - get + - patch + - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole diff --git a/config/samples/metal3.io_v1alpha1_hostfirmwarecomponents.yaml b/config/samples/metal3.io_v1alpha1_hostfirmwarecomponents.yaml index 3283a8c7a4..6a5ab281f1 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 +apiVersion: metal3.io.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 e22726a2d7..7834fb75a9 100644 --- a/controllers/metal3.io/baremetalhost_controller.go +++ b/controllers/metal3.io/baremetalhost_controller.go @@ -92,10 +92,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/host_state_machine_test.go b/controllers/metal3.io/host_state_machine_test.go index 947a4302c1..94ac9c8e9c 100644 --- a/controllers/metal3.io/host_state_machine_test.go +++ b/controllers/metal3.io/host_state_machine_test.go @@ -1396,6 +1396,10 @@ func (m *mockProvisioner) RemoveBMCEventSubscriptionForNode(_ metal3api.BMCEvent return result, nil } +func (p *mockProvisioner) GetFirmwareComponentsForNode() (components []metal3api.FirmwareComponentStatus, err error) { + return components, nil +} + func TestUpdateBootModeStatus(t *testing.T) { testCases := []struct { Scenario string diff --git a/controllers/metal3.io/hostfirmwarecomponents_controller.go b/controllers/metal3.io/hostfirmwarecomponents_controller.go new file mode 100644 index 0000000000..97bb952b4c --- /dev/null +++ b/controllers/metal3.io/hostfirmwarecomponents_controller.go @@ -0,0 +1,278 @@ +/* + + +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" + + "github.com/pkg/errors" + + 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 simply the pass or arguments. +type rhfcInfo struct { + log logr.Logger + hfc *metal3api.HostFirmwareComponents + bmh *metal3api.BareMetalHost + events []corev1.Event +} + +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: "HostFirmwareComponents", + 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.metal3.io,resources=hostfirmwarecomponents,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=metal3.io.metal3.io,resources=hostfirmwarecomponents/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=metal3.io.metal3.io,resources=hostfirmwarecomponents/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile +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(context.TODO(), req.NamespacedName, bmh) + if err != nil { + reqLogger.Info("could not get baremetal host, not running hostfirmwarecomponents reconciler") + if k8serrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{Requeue: true, RequeueAfter: resourceNotAvailableRetryDelay}, nil + } + + if hasDetachedAnnotation(bmh) { + reqLogger.Info("the host is detached, not running hostfirmwarecomponents reconciler") + return ctrl.Result{Requeue: true, RequeueAfter: unmanagedRetryDelay}, nil + } + + // Fetch the HostFirmwareComponents + hfc := &metal3api.HostFirmwareComponents{} + info := &rhfcInfo{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("hostFirmwareComponnets not found") + return ctrl.Result{Requeue: true, RequeueAfter: resourceNotAvailableRetryDelay}, nil + } + // Error reading the object - requeue the request. + return ctrl.Result{}, errors.Wrap(err, "could not load hostFirmwareComponents") + } + + // Create a provisioner to access Ironic API + prov, err := r.ProvisionerFactory.NewProvisioner(provisioner.BuildHostDataNoBMC(*bmh), info.publishEvent) + if err != nil { + return ctrl.Result{}, errors.Wrap(err, "failed to create provisioner") + } + + 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 + } + + // Check ironic for the components information if possible + components, err := prov.GetFirmwareComponentsForNode() + if err != nil { + reqLogger.Info("provisioner returns error", "Error", err.Error(), "RequeueAfter", provisionerRetryDelay) + return ctrl.Result{Requeue: true, RequeueAfter: provisionerRetryDelay}, nil + } + + if err = r.updateHostFirmwareComponents(components, info); err != nil { + return ctrl.Result{}, errors.Wrap(err, "Could not update hostFirmwareComponents") + } + + for _, e := range info.events { + r.publishEvent(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) updateHostFirmwareComponents(components []metal3api.FirmwareComponentStatus, info *rhfcInfo) (err error) { + dirty := false + var newStatus metal3api.HostFirmwareComponentsStatus + + // change the Components in Status + newStatus.Components = components + + // Check if the updates in the Spec are different than Status + updatesMismatch := !reflect.DeepEqual(info.hfc.Status.Updates, info.hfc.Spec.Updates) + // Check if the components information we retrieved is different from the one in Status + componentsInfoMismatch := !reflect.DeepEqual(components, info.hfc.Status.Components) + + reason := reasonSuccess + 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)) + if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsValid, metav1.ConditionFalse, reasonConfigurationError, "Invalid Firmware Components") { + dirty = true + } + } + if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsValid, metav1.ConditionFalse, reason, "") { + dirty = true + } + } + + if componentsInfoMismatch { + if setUpdatesCondition(generation, &newStatus, info, metal3api.HostFirmwareComponentsChangeDetected, metav1.ConditionTrue, 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 r.Status().Update(context.TODO(), 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") + + if e.ObjectNew.GetGeneration() != e.ObjectOld.GetGeneration() { + r.Log.Info("returning true as generation changed from event handler") + } + + return false +} + +func (r *HostFirmwareComponentsReconciler) validateHostFirmwareComponents(info *rhfcInfo) error { + allowedNames := map[string]struct{}{"bmc": {}, "bios": {}} + for _, update := range info.hfc.Spec.Updates { + componentName := update.Component + if _, ok := allowedNames[componentName]; !ok { + return fmt.Errorf("component %s is invalid, only 'bmc' or 'bios' are allowed as update names", update.Component) + } + } + + return nil +} + +func (r *HostFirmwareComponentsReconciler) publishEvent(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(context.TODO(), &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 conditionReason, message string) bool { + newCondition := metav1.Condition{ + Type: string(cond), + Status: newStatus, + ObservedGeneration: generation, + Reason: string(reason), + Message: message, + } + meta.SetStatusCondition(&status.Conditions, newCondition) + + currCond := meta.FindStatusCondition(info.hfc.Status.Conditions, string(cond)) + if currCond == nil || currCond.Status != newStatus { + return true + } + return false +} diff --git a/main.go b/main.go index 47ce07e478..b5aa8d0533 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 cf82e8794e..c7228ebe85 100644 --- a/pkg/provisioner/demo/demo.go +++ b/pkg/provisioner/demo/demo.go @@ -301,3 +301,7 @@ func (p *demoProvisioner) AddBMCEventSubscriptionForNode(_ *metal3api.BMCEventSu func (p *demoProvisioner) RemoveBMCEventSubscriptionForNode(_ metal3api.BMCEventSubscription) (result provisioner.Result, err error) { return result, nil } + +func (p *demoProvisioner) GetFirmwareComponentsForNode() (components []metal3api.FirmwareComponentStatus, err error) { + return components, nil +} diff --git a/pkg/provisioner/fixture/fixture.go b/pkg/provisioner/fixture/fixture.go index 693c993da7..66b60ee565 100644 --- a/pkg/provisioner/fixture/fixture.go +++ b/pkg/provisioner/fixture/fixture.go @@ -348,3 +348,7 @@ func (p *fixtureProvisioner) AddBMCEventSubscriptionForNode(_ *metal3api.BMCEven func (p *fixtureProvisioner) RemoveBMCEventSubscriptionForNode(_ metal3api.BMCEventSubscription) (result provisioner.Result, err error) { return result, nil } + +func (p *fixtureProvisioner) GetFirmwareComponentsForNode() (components []metal3api.FirmwareComponentStatus, err error) { + return components, nil +} diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go index 190e54f5c2..14846ba64c 100644 --- a/pkg/provisioner/ironic/ironic.go +++ b/pkg/provisioner/ironic/ironic.go @@ -1124,6 +1124,45 @@ func (p *ironicProvisioner) GetFirmwareSettings(includeSchema bool) (settings me return settings, schema, nil } +// GetFirmwareComponentsForNode gets all available firmware components for a node and return a list. +func (p *ironicProvisioner) GetFirmwareComponentsForNode() ([]metal3api.FirmwareComponentStatus, error) { + ironicNode, err := p.getNode() + if err != nil { + return nil, errors.Wrap(err, "could not get node to retrieve firmware components") + } + + // Get the components from Ironic via Gophercloud + var componentList []nodes.FirmwareComponent + var componentListErr error + componentList, componentListErr = nodes.ListFirmware(p.client, ironicNode.UUID).Extract() + + if componentListErr != nil { + return nil, errors.Wrap(componentListErr, + fmt.Sprintf("could not get firmware components for node %s", ironicNode.UUID)) + } + p.log.Info("retrieved firmware components for node", "node", ironicNode.UUID, "size", len(componentList)) + + // 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 { + 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 24191e5809..626933b372 100644 --- a/pkg/provisioner/provisioner.go +++ b/pkg/provisioner/provisioner.go @@ -199,6 +199,9 @@ type Provisioner interface { // RemoveBMCEventSubscriptionForNode delete the subscription RemoveBMCEventSubscriptionForNode(subscription metal3api.BMCEventSubscription) (result Result, err error) + + // GetFirmwareComponentsForNode gets all firmware components available from a note + GetFirmwareComponentsForNode() (components []metal3api.FirmwareComponentStatus, err error) } // Result holds the response from a call in the Provsioner API.