diff --git a/PROJECT b/PROJECT index e0e9448..63b8569 100644 --- a/PROJECT +++ b/PROJECT @@ -60,4 +60,12 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + controller: true + domain: ironcore.dev + group: metal + kind: ServerBIOS + path: github.com/ironcore-dev/metal-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go index 6f329ee..0a050a4 100644 --- a/api/v1alpha1/common_types.go +++ b/api/v1alpha1/common_types.go @@ -11,6 +11,26 @@ import ( "k8s.io/apimachinery/pkg/util/runtime" ) +type JobType string + +const ( + ScanBIOSVersionJobType JobType = "ScanBIOSVersion" + UpdateBIOSVersionJobType JobType = "UpdateBIOSVersion" + ApplyBIOSSettingsJobType JobType = "ApplyBiosSettings" +) + +// RunningJobRef contains job type and reference to Job object +type RunningJobRef struct { + // Type reflects the type of the job. + // +kubebuilder:validation:Enum=ScanBIOSVersion;UpdateBIOSVersion;ApplyBiosSettings + // +required + Type JobType `json:"type"` + + // JobRef contains the reference to the Job object. + // +required + JobRef v1.ObjectReference `json:"jobRef"` +} + // IP is an IP address. type IP struct { netip.Addr `json:"-"` diff --git a/api/v1alpha1/server_types.go b/api/v1alpha1/server_types.go index aac6906..7672178 100644 --- a/api/v1alpha1/server_types.go +++ b/api/v1alpha1/server_types.go @@ -65,14 +65,6 @@ type BootOrder struct { Device string `json:"device"` } -// BIOSSettings represents the BIOS settings for a server. -type BIOSSettings struct { - // Version specifies the version of the server BIOS for which the settings are defined. - Version string `json:"version"` - // Settings is a map of key-value pairs representing the BIOS settings. - Settings map[string]string `json:"settings,omitempty"` -} - // ServerSpec defines the desired state of a Server. type ServerSpec struct { // UUID is the unique identifier for the server. @@ -103,8 +95,9 @@ type ServerSpec struct { // BootOrder specifies the boot order of the server. BootOrder []BootOrder `json:"bootOrder,omitempty"` - // BIOS specifies the BIOS settings for the server. - BIOS []BIOSSettings `json:"BIOS,omitempty"` + + // BIOSSettingsRef is a reference to a ServerBIOS object. + BIOSSettingsRef v1.LocalObjectReference `json:"biOSSettingsRef,omitempty"` } // ServerState defines the possible states of a server. @@ -168,8 +161,6 @@ type ServerStatus struct { // NetworkInterfaces is a list of network interfaces associated with the server. NetworkInterfaces []NetworkInterface `json:"networkInterfaces,omitempty"` - BIOS BIOSSettings `json:"BIOS,omitempty"` - // Conditions represents the latest available observations of the server's current state. // +patchStrategy=merge // +patchMergeKey=type @@ -192,18 +183,18 @@ type NetworkInterface struct { MACAddress string `json:"macAddress"` } -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:resource:scope=Cluster -//+kubebuilder:printcolumn:name="UUID",type=string,JSONPath=`.spec.uuid` -//+kubebuilder:printcolumn:name="Manufacturer",type=string,JSONPath=`.status.manufacturer` -//+kubebuilder:printcolumn:name="Model",type=string,JSONPath=`.status.model` -//+kubebuilder:printcolumn:name="SKU",type=string,JSONPath=`.status.sku`,priority=100 -//+kubebuilder:printcolumn:name="SerialNumber",type=string,JSONPath=`.status.serialNumber`,priority=100 -//+kubebuilder:printcolumn:name="PowerState",type=string,JSONPath=`.status.powerState` -//+kubebuilder:printcolumn:name="IndicatorLED",type=string,JSONPath=`.status.indicatorLED`,priority=100 -//+kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state` -//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:printcolumn:name="UUID",type=string,JSONPath=`.spec.uuid` +// +kubebuilder:printcolumn:name="Manufacturer",type=string,JSONPath=`.status.manufacturer` +// +kubebuilder:printcolumn:name="Model",type=string,JSONPath=`.status.model` +// +kubebuilder:printcolumn:name="SKU",type=string,JSONPath=`.status.sku`,priority=100 +// +kubebuilder:printcolumn:name="SerialNumber",type=string,JSONPath=`.status.serialNumber`,priority=100 +// +kubebuilder:printcolumn:name="PowerState",type=string,JSONPath=`.status.powerState` +// +kubebuilder:printcolumn:name="IndicatorLED",type=string,JSONPath=`.status.indicatorLED`,priority=100 +// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // Server is the Schema for the servers API type Server struct { @@ -214,7 +205,7 @@ type Server struct { Status ServerStatus `json:"status,omitempty"` } -//+kubebuilder:object:root=true +// +kubebuilder:object:root=true // ServerList contains a list of Server type ServerList struct { diff --git a/api/v1alpha1/serverbios_types.go b/api/v1alpha1/serverbios_types.go new file mode 100644 index 0000000..e5220f8 --- /dev/null +++ b/api/v1alpha1/serverbios_types.go @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ServerBIOSSpec defines the desired state of ServerBIOS +type ServerBIOSSpec struct { + // ScanPeriodMinutes defines the period in minutes after which scanned data is considered obsolete. + // +kubebuilder:default=30 + // +optional + ScanPeriodMinutes int32 `json:"scanPeriodMinutes,omitempty"` + + // ServerRef is a reference to Server object + // +optional + ServerRef v1.LocalObjectReference `json:"serverRef,omitempty"` + + // BIOS contains a bios version and settings. + // +optional + BIOS BIOSSettings `json:"bios,omitempty"` +} + +// BIOSSettings contains a version, settings and a flag defining whether it is a current version +type BIOSSettings struct { + // Version contains BIOS version + // +required + Version string `json:"version"` + + // Settings contains BIOS settings as map + // +optional + Settings map[string]string `json:"settings,omitempty"` +} + +// ServerBIOSStatus defines the observed state of ServerBIOS +type ServerBIOSStatus struct { + // LastScanTime reflects the timestamp when the scanning for installed firmware was performed + // +optional + LastScanTime metav1.Time `json:"lastScanTime,omitempty"` + + // BIOS contains a bios version and settings. + // +optional + BIOS BIOSSettings `json:"bios,omitempty"` + + // RunningJob reflects the invoked scan or update job running + // +optional + RunningJob RunningJobRef `json:"runningJob,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:printcolumn:name="Server",type=string,JSONPath=`.spec.serverRef.name`,description="Server name" +// +kubebuilder:printcolumn:name="BIOS Version",type=string,JSONPath=`.status.version`,description="Installed BIOS Version" + +// ServerBIOS is the Schema for the serverbios API +type ServerBIOS struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ServerBIOSSpec `json:"spec,omitempty"` + Status ServerBIOSStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ServerBIOSList contains a list of ServerBIOS +type ServerBIOSList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ServerBIOS `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ServerBIOS{}, &ServerBIOSList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 24bd8c0..c506215 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -393,6 +393,22 @@ func (in *Protocol) DeepCopy() *Protocol { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RunningJobRef) DeepCopyInto(out *RunningJobRef) { + *out = *in + out.JobRef = in.JobRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunningJobRef. +func (in *RunningJobRef) DeepCopy() *RunningJobRef { + if in == nil { + return nil + } + out := new(RunningJobRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Server) DeepCopyInto(out *Server) { *out = *in @@ -420,6 +436,100 @@ func (in *Server) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerBIOS) DeepCopyInto(out *ServerBIOS) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerBIOS. +func (in *ServerBIOS) DeepCopy() *ServerBIOS { + if in == nil { + return nil + } + out := new(ServerBIOS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerBIOS) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerBIOSList) DeepCopyInto(out *ServerBIOSList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ServerBIOS, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerBIOSList. +func (in *ServerBIOSList) DeepCopy() *ServerBIOSList { + if in == nil { + return nil + } + out := new(ServerBIOSList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ServerBIOSList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerBIOSSpec) DeepCopyInto(out *ServerBIOSSpec) { + *out = *in + out.ServerRef = in.ServerRef + in.BIOS.DeepCopyInto(&out.BIOS) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerBIOSSpec. +func (in *ServerBIOSSpec) DeepCopy() *ServerBIOSSpec { + if in == nil { + return nil + } + out := new(ServerBIOSSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerBIOSStatus) DeepCopyInto(out *ServerBIOSStatus) { + *out = *in + in.LastScanTime.DeepCopyInto(&out.LastScanTime) + in.BIOS.DeepCopyInto(&out.BIOS) + out.RunningJob = in.RunningJob +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerBIOSStatus. +func (in *ServerBIOSStatus) DeepCopy() *ServerBIOSStatus { + if in == nil { + return nil + } + out := new(ServerBIOSStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServerBootConfiguration) DeepCopyInto(out *ServerBootConfiguration) { *out = *in @@ -679,13 +789,7 @@ func (in *ServerSpec) DeepCopyInto(out *ServerSpec) { *out = make([]BootOrder, len(*in)) copy(*out, *in) } - if in.BIOS != nil { - in, out := &in.BIOS, &out.BIOS - *out = make([]BIOSSettings, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } + out.BIOSSettingsRef = in.BIOSSettingsRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerSpec. @@ -708,7 +812,6 @@ func (in *ServerStatus) DeepCopyInto(out *ServerStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - in.BIOS.DeepCopyInto(&out.BIOS) if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) diff --git a/config/crd/bases/metal.ironcore.dev_serverbioses.yaml b/config/crd/bases/metal.ironcore.dev_serverbioses.yaml new file mode 100644 index 0000000..d589bdd --- /dev/null +++ b/config/crd/bases/metal.ironcore.dev_serverbioses.yaml @@ -0,0 +1,170 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: serverbioses.metal.ironcore.dev +spec: + group: metal.ironcore.dev + names: + kind: ServerBIOS + listKind: ServerBIOSList + plural: serverbioses + singular: serverbios + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Server name + jsonPath: .spec.serverRef.name + name: Server + type: string + - description: Installed BIOS Version + jsonPath: .status.version + name: BIOS Version + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ServerBIOS is the Schema for the serverbios 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: ServerBIOSSpec defines the desired state of ServerBIOS + properties: + bios: + description: BIOS contains a bios version and settings. + properties: + settings: + additionalProperties: + type: string + description: Settings contains BIOS settings as map + type: object + version: + description: Version contains BIOS version + type: string + required: + - version + type: object + scanPeriodMinutes: + default: 30 + description: ScanPeriodMinutes defines the period in minutes after + which scanned data is considered obsolete. + format: int32 + type: integer + serverRef: + description: ServerRef is a reference to Server object + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: object + status: + description: ServerBIOSStatus defines the observed state of ServerBIOS + properties: + bios: + description: BIOS contains a bios version and settings. + properties: + settings: + additionalProperties: + type: string + description: Settings contains BIOS settings as map + type: object + version: + description: Version contains BIOS version + type: string + required: + - version + type: object + lastScanTime: + description: LastScanTime reflects the timestamp when the scanning + for installed firmware was performed + format: date-time + type: string + runningJob: + description: RunningJob reflects the invoked scan or update job running + properties: + jobRef: + description: JobRef contains the reference to the Job object. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type reflects the type of the job. + enum: + - ScanBIOSVersion + - UpdateBIOSVersion + - ApplyBiosSettings + type: string + required: + - jobRef + - type + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/metal.ironcore.dev_servers.yaml b/config/crd/bases/metal.ironcore.dev_servers.yaml index 491c962..949610d 100644 --- a/config/crd/bases/metal.ironcore.dev_servers.yaml +++ b/config/crd/bases/metal.ironcore.dev_servers.yaml @@ -70,25 +70,20 @@ spec: spec: description: ServerSpec defines the desired state of a Server. properties: - BIOS: - description: BIOS specifies the BIOS settings for the server. - items: - description: BIOSSettings represents the BIOS settings for a server. - properties: - settings: - additionalProperties: - type: string - description: Settings is a map of key-value pairs representing - the BIOS settings. - type: object - version: - description: Version specifies the version of the server BIOS - for which the settings are defined. - type: string - required: - - version - type: object - type: array + biOSSettingsRef: + description: BIOSSettingsRef is a reference to a ServerBIOS object. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic bmc: description: |- BMC contains the access details for the BMC. @@ -280,22 +275,6 @@ spec: status: description: ServerStatus defines the observed state of Server. properties: - BIOS: - description: BIOSSettings represents the BIOS settings for a server. - properties: - settings: - additionalProperties: - type: string - description: Settings is a map of key-value pairs representing - the BIOS settings. - type: object - version: - description: Version specifies the version of the server BIOS - for which the settings are defined. - type: string - required: - - version - type: object conditions: description: Conditions represents the latest available observations of the server's current state. diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 26782f5..4b08a73 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -34,6 +34,7 @@ rules: - bmcs - bmcsecrets - endpoints + - serverbioses - serverbootconfigurations - serverclaims - serverconfigurations @@ -52,6 +53,7 @@ rules: - bmcs/finalizers - bmcsecrets/finalizers - endpoints/finalizers + - serverbioses/finalizers - serverbootconfigurations/finalizers - serverclaims/finalizers - servers/finalizers @@ -63,6 +65,7 @@ rules: - bmcs/status - bmcsecrets/status - endpoints/status + - serverbioses/status - serverbootconfigurations/status - serverclaims/status - servers/status diff --git a/config/rbac/serverbios_editor_role.yaml b/config/rbac/serverbios_editor_role.yaml new file mode 100644 index 0000000..106d6cd --- /dev/null +++ b/config/rbac/serverbios_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit servers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: serverbios-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: metal-operator + app.kubernetes.io/part-of: metal-operator + app.kubernetes.io/managed-by: kustomize + name: serverbios-editor-role +rules: + - apiGroups: + - metal.ironcore.dev + resources: + - serverbioses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - metal.ironcore.dev + resources: + - serverbioses/status + verbs: + - get diff --git a/config/rbac/serverbios_viewer_role.yaml b/config/rbac/serverbios_viewer_role.yaml new file mode 100644 index 0000000..133eae0 --- /dev/null +++ b/config/rbac/serverbios_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view servers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: serverbios-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: metal-operator + app.kubernetes.io/part-of: metal-operator + app.kubernetes.io/managed-by: kustomize + name: serverbios-viewer-role +rules: + - apiGroups: + - metal.ironcore.dev + resources: + - serverbioses + verbs: + - get + - list + - watch + - apiGroups: + - metal.ironcore.dev + resources: + - serverbioses/status + verbs: + - get diff --git a/internal/controller/server_controller.go b/internal/controller/server_controller.go index df9f5b1..dc48a0b 100644 --- a/internal/controller/server_controller.go +++ b/internal/controller/server_controller.go @@ -23,7 +23,6 @@ import ( "github.com/stmcginnis/gofish/redfish" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - meta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -67,15 +66,15 @@ type ServerReconciler struct { PowerPollingTimeout time.Duration } -//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcs,verbs=get;list;watch -//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcsecrets,verbs=get;list;watch -//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=endpoints,verbs=get;list;watch -//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=servers,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=servers/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=servers/finalizers,verbs=update -//+kubebuilder:rbac:groups=metal.ironcore.dev,resources=serverconfigurations,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups="batch",resources=jobs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcs,verbs=get;list;watch +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcsecrets,verbs=get;list;watch +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=endpoints,verbs=get;list;watch +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=servers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=servers/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=servers/finalizers,verbs=update +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=serverconfigurations,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="batch",resources=jobs,verbs=get;list;watch;create;update;patch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -162,11 +161,6 @@ func (r *ServerReconciler) reconcile(ctx context.Context, log logr.Logger, serve } log.V(1).Info("Updated Server status") - if err := r.applyBiosSettings(ctx, log, server); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to update server bios settings: %w", err) - } - log.V(1).Info("Updated Server BIOS settings") - if err := r.applyBootOrder(ctx, log, server); err != nil { return ctrl.Result{}, fmt.Errorf("failed to update server bios boot order: %w", err) } @@ -328,7 +322,7 @@ func (r *ServerReconciler) handleReservedState(ctx context.Context, log logr.Log } log.V(1).Info("Server boot configuration is ready") - //TODO: handle working Reserved Server that was suddenly powered off but needs to boot from disk + // TODO: handle working Reserved Server that was suddenly powered off but needs to boot from disk if server.Status.PowerState == metalv1alpha1.ServerOffPowerState { if err := r.pxeBootServer(ctx, log, server); err != nil { return false, fmt.Errorf("failed to boot server: %w", err) @@ -386,27 +380,6 @@ func (r *ServerReconciler) updateServerStatus(ctx context.Context, log logr.Logg server.Status.Model = systemInfo.Model server.Status.IndicatorLED = metalv1alpha1.IndicatorLED(systemInfo.IndicatorLED) - currentBiosVersion, err := bmcClient.GetBiosVersion(server.Spec.UUID) - if err != nil { - return fmt.Errorf("failed to load bios version: %w", err) - } - - for _, bios := range server.Spec.BIOS { - if bios.Version == currentBiosVersion { - // with go 1.23: switch to maps.Keys(bios.Settings) - keys := make([]string, 0, len(bios.Settings)) - for k := range bios.Settings { - keys = append(keys, k) - } - attributes, err := bmcClient.GetBiosAttributeValues(server.Spec.UUID, keys) - if err != nil { - return fmt.Errorf("failed load bios settings: %w", err) - } - server.Status.BIOS.Version = currentBiosVersion - server.Status.BIOS.Settings = attributes - } - } - if err := r.Status().Patch(ctx, server, client.MergeFrom(serverBase)); err != nil { return fmt.Errorf("failed to patch Server status: %w", err) } @@ -806,58 +779,6 @@ func (r *ServerReconciler) applyBootOrder(ctx context.Context, log logr.Logger, return nil } -func (r *ServerReconciler) applyBiosSettings(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server) error { - serverBase := server.DeepCopy() - if server.Spec.BMCRef == nil && server.Spec.BMC == nil { - log.V(1).Info("Server has no BMC connection configured") - return nil - } - bmcClient, err := GetBMCClientForServer(ctx, r.Client, server, r.Insecure) - if err != nil { - return fmt.Errorf("failed to create BMC client: %w", err) - } - defer bmcClient.Logout() - - version, err := bmcClient.GetBiosVersion(server.Spec.UUID) - if err != nil { - return fmt.Errorf("failed to create BMC client: %w", err) - } - - versionMatch := false - diff := map[string]string{} - for _, bios := range server.Spec.BIOS { - if bios.Version == version { - versionMatch = true - for key, value := range bios.Settings { - if res, ok := server.Status.BIOS.Settings[key]; !ok { - if !ok || res != value { - diff[key] = value - } - } - } - reset, err := bmcClient.SetBiosAttributes(server.Spec.UUID, diff) - if err != nil { - return err - } - if reset { - if changed := meta.SetStatusCondition(&server.Status.Conditions, metav1.Condition{ - Type: "Reboot needed", - }); changed { - if err := r.Status().Patch(ctx, server, client.MergeFrom(serverBase)); err != nil { - return fmt.Errorf("failed to patch Server status: %w", err) - } - } - } - break - } - } - if !versionMatch { - log.V(1).Info("None of the Bios versions match") - return nil - } - return nil -} - func (r *ServerReconciler) handleAnnotionOperations(ctx context.Context, log logr.Logger, server *metalv1alpha1.Server) (bool, error) { annotations := server.GetAnnotations() operation, ok := annotations[metalv1alpha1.OperationAnnotation] diff --git a/internal/controller/serverbios_controller.go b/internal/controller/serverbios_controller.go new file mode 100644 index 0000000..7c4e802 --- /dev/null +++ b/internal/controller/serverbios_controller.go @@ -0,0 +1,377 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + "github.com/google/go-cmp/cmp" + "github.com/ironcore-dev/controller-utils/clientutils" + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const serverBIOSFinalizer = "metal.ironcore.dev/serverbios" + +// ServerBIOSReconciler reconciles a ServerBIOS object +type ServerBIOSReconciler struct { + client.Client + Scheme *runtime.Scheme + + // todo: need to decide how to provide jobs' configuration to controller + JobNamespace string + JobImage string + JobServiceAccountName string +} + +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=serverbioses,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=serverbioses/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=serverbioses/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. +func (r *ServerBIOSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + serverBIOS := &metalv1alpha1.ServerBIOS{} + if err := r.Get(ctx, req.NamespacedName, serverBIOS); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + return r.reconciliationRequired(ctx, log, serverBIOS) +} + +// Determine whether reconciliation is required. It's not required if: +// - object is being deleted; +// - object does not contain reference to server; +// - object contains reference to server, but server references to another object; +// - there is active job related to the object already running; +func (r *ServerBIOSReconciler) reconciliationRequired( + ctx context.Context, + log logr.Logger, + serverBIOS *metalv1alpha1.ServerBIOS, +) (ctrl.Result, error) { + // if object is being deleted - reconcile deletion + if !serverBIOS.DeletionTimestamp.IsZero() { + log.V(1).Info("object is being deleted") + return r.reconcileDeletion(ctx, log, serverBIOS) + } + + // if object does not refer to server object - stop reconciliation + if serverBIOS.Spec.ServerRef == (corev1.LocalObjectReference{}) { + log.V(1).Info("object does not refer to server object") + return ctrl.Result{}, nil + } + + // if referred server contains reference to different ServerBIOS object - stop reconciliation + server, err := r.getReferredServer(ctx, log, serverBIOS.Spec.ServerRef.Name) + if err != nil { + return ctrl.Result{}, err + } + if server.Spec.BIOSSettingsRef != (corev1.LocalObjectReference{}) && + server.Spec.BIOSSettingsRef.Name != serverBIOS.Name { + log.V(1).Info("referred server contains reference to different ServerBIOS object") + return ctrl.Result{}, nil + } + + // patch server with serverbios reference + if server.Spec.BIOSSettingsRef == (corev1.LocalObjectReference{}) { + reference := corev1.LocalObjectReference{Name: serverBIOS.Name} + if err := r.patchBIOSSettingsRef(ctx, log, &server, reference); err != nil { + return ctrl.Result{}, err + } + } + log.V(1).Info("ensured mutual reference", "server", server.Name) + + // if related job is already running - stop reconciliation + if jobInProgress, err := r.jobInProgress(ctx, log, serverBIOS.Status.RunningJob); err != nil || jobInProgress { + log.V(1).Info("related job is already running") + return ctrl.Result{}, err + } + + return r.reconcile(ctx, log, serverBIOS) +} + +func (r *ServerBIOSReconciler) reconcileDeletion( + ctx context.Context, + log logr.Logger, + serverBIOS *metalv1alpha1.ServerBIOS, +) (ctrl.Result, error) { + if !controllerutil.ContainsFinalizer(serverBIOS, serverBIOSFinalizer) { + return ctrl.Result{}, nil + } + if err := r.cleanupReferences(ctx, log, serverBIOS); err != nil { + log.Error(err, "failed to cleanup references") + return ctrl.Result{}, err + } + log.V(1).Info("ensured references were cleaned up") + + _, err := clientutils.PatchEnsureNoFinalizer(ctx, r.Client, serverBIOS, serverBIOSFinalizer) + return ctrl.Result{}, err +} + +func (r *ServerBIOSReconciler) cleanupReferences( + ctx context.Context, + log logr.Logger, + serverBIOS *metalv1alpha1.ServerBIOS, +) error { + if serverBIOS.Spec.ServerRef == (corev1.LocalObjectReference{}) { + return nil + } + server, err := r.getReferredServer(ctx, log, serverBIOS.Spec.ServerRef.Name) + if err != nil { + return err + } + if server.Spec.BIOSSettingsRef == (corev1.LocalObjectReference{}) { + return nil + } + if server.Spec.BIOSSettingsRef.Name != serverBIOS.Name { + return nil + } + return r.patchBIOSSettingsRef(ctx, log, &server, corev1.LocalObjectReference{}) +} + +// Reconciliation flow for ServerBIOS: +// 1. Ensure finalizer is set on the object +// 2. Ensure info about current BIOS version and settings is not outdated, otherwise: +// 2.1. Invoke scan job and set reference to this job in status +// 2.2. Wait until object will be updated by job runner with up-to-date info, empty reference to the job and the +// last scan time +// 3. Ensure referred server is in Available state +// 4. Ensure desired and current BIOS versions match, otherwise: +// 4.1. Invoke BIOS version update job and set reference to this job in status +// 4.2. Wait until object will be updated by job runner with up-to-date info and empty reference to the job +// 5. Ensure desired and current BIOS settings match, otherwise: +// 5.1. Invoke BIOS settings update job and set reference to this job in status +// 5.2. Wait until object will be updated by job runner with up-to-date info and empty reference to the job +func (r *ServerBIOSReconciler) reconcile( + ctx context.Context, + log logr.Logger, + serverBIOS *metalv1alpha1.ServerBIOS, +) (ctrl.Result, error) { + if modified, err := clientutils.PatchEnsureFinalizer(ctx, r.Client, serverBIOS, serverBIOSFinalizer); err != nil || modified { + return ctrl.Result{}, err + } + log.V(1).Info("ensured finalizer has been added") + + // if scanned data outdated - run scan + if time.Since(serverBIOS.Status.LastScanTime.Time) > time.Duration(serverBIOS.Spec.ScanPeriodMinutes)*time.Minute { + return r.reconcileScan(ctx, log, serverBIOS) + } + return r.reconcileUpdate(ctx, log, serverBIOS) +} + +func (r *ServerBIOSReconciler) reconcileScan( + ctx context.Context, + log logr.Logger, + serverBIOS *metalv1alpha1.ServerBIOS, +) (ctrl.Result, error) { + log.V(1).Info("invoking scan job") + jobReference, err := r.createJob(ctx, log, serverBIOS, metalv1alpha1.ScanBIOSVersionJobType) + if err != nil { + return ctrl.Result{}, err + } + return r.patchJobReference(ctx, log, serverBIOS, metalv1alpha1.RunningJobRef{ + Type: metalv1alpha1.ScanBIOSVersionJobType, + JobRef: jobReference, + }) +} + +func (r *ServerBIOSReconciler) reconcileUpdate( + ctx context.Context, + log logr.Logger, + serverBIOS *metalv1alpha1.ServerBIOS, +) (ctrl.Result, error) { + server, err := r.getReferredServer(ctx, log, serverBIOS.Spec.ServerRef.Name) + if err != nil { + return ctrl.Result{}, err + } + // if referred server is not in Available state - stop reconciliation + if server.Status.State != metalv1alpha1.ServerStateAvailable { + // todo: maybe return ctrl.Result{RequeueAfter: ?} + return ctrl.Result{}, nil + } + // if desired bios version does not match actual version - run version update + if serverBIOS.Spec.BIOS.Version != serverBIOS.Status.BIOS.Version { + return r.reconcileVersionUpdate(ctx, log, serverBIOS) + } + // if desired bios settings do not match actual settings - run settings update + if !cmp.Equal(serverBIOS.Spec.BIOS.Settings, serverBIOS.Status.BIOS.Settings) { + return r.reconcileSettingsUpdate(ctx, log, serverBIOS) + } + return ctrl.Result{}, nil +} + +func (r *ServerBIOSReconciler) reconcileVersionUpdate( + ctx context.Context, + log logr.Logger, + serverBIOS *metalv1alpha1.ServerBIOS, +) (ctrl.Result, error) { + log.V(1).Info("invoking version update job") + jobReference, err := r.createJob(ctx, log, serverBIOS, metalv1alpha1.UpdateBIOSVersionJobType) + if err != nil { + return ctrl.Result{}, err + } + return r.patchJobReference(ctx, log, serverBIOS, metalv1alpha1.RunningJobRef{ + Type: metalv1alpha1.UpdateBIOSVersionJobType, + JobRef: jobReference, + }) +} + +func (r *ServerBIOSReconciler) reconcileSettingsUpdate( + ctx context.Context, + log logr.Logger, + serverBIOS *metalv1alpha1.ServerBIOS, +) (ctrl.Result, error) { + log.V(1).Info("invoking settings update job") + jobReference, err := r.createJob(ctx, log, serverBIOS, metalv1alpha1.ApplyBIOSSettingsJobType) + if err != nil { + return ctrl.Result{}, err + } + return r.patchJobReference(ctx, log, serverBIOS, metalv1alpha1.RunningJobRef{ + Type: metalv1alpha1.ApplyBIOSSettingsJobType, + JobRef: jobReference, + }) +} + +func (r *ServerBIOSReconciler) createJob( + ctx context.Context, + log logr.Logger, + serverBIOS *metalv1alpha1.ServerBIOS, + jobType metalv1alpha1.JobType, +) (corev1.ObjectReference, error) { + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-", serverBIOS.Name), + Namespace: r.JobNamespace, + }, + Spec: batchv1.JobSpec{ + Completions: nil, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "metal.ironcore.dev/serverbios": serverBIOS.Name, + "metal.ironcore.dev/jobtype": string(jobType), + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "metal.ironcore.dev/serverbios": serverBIOS.Name, + "metal.ironcore.dev/jobtype": string(jobType), + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: r.JobImage, + Env: []corev1.EnvVar{ + { + Name: "JOB_TYPE", + Value: string(jobType), + }, + { + Name: "SERVER_BIOS_REF", + Value: serverBIOS.Name, + }, + }, + }, + }, + ServiceAccountName: r.JobServiceAccountName, + AutomountServiceAccountToken: ptr.To(true), + RestartPolicy: corev1.RestartPolicyNever, + }, + }, + }, + } + if err := r.Create(ctx, job); err != nil { + log.Error(err, "failed to create job") + return corev1.ObjectReference{}, err + } + reference := corev1.ObjectReference{ + Kind: "Job", + Namespace: job.Namespace, + Name: job.Name, + UID: job.UID, + APIVersion: "batch/v1", + } + return reference, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ServerBIOSReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&metalv1alpha1.ServerBIOS{}). + Complete(r) +} + +func (r *ServerBIOSReconciler) getReferredServer( + ctx context.Context, + log logr.Logger, + name string, +) (metalv1alpha1.Server, error) { + key := client.ObjectKey{Name: name, Namespace: metav1.NamespaceNone} + server := metalv1alpha1.Server{} + if err := r.Get(ctx, key, &server); err != nil { + log.Error(err, "failed to get referred server") + return server, err + } + return server, nil +} + +func (r *ServerBIOSReconciler) jobInProgress( + ctx context.Context, + log logr.Logger, + jobReference metalv1alpha1.RunningJobRef, +) (bool, error) { + if jobReference == (metalv1alpha1.RunningJobRef{}) { + return false, nil + } + key := client.ObjectKey{Namespace: r.JobNamespace, Name: r.JobImage} + job := batchv1.Job{} + if err := r.Get(ctx, key, &job); err != nil { + log.Error(err, "failed to get job") + return false, err + } + log.V(1).Info("active job found") + return job.Status.Active > 0, nil +} + +func (r *ServerBIOSReconciler) patchBIOSSettingsRef( + ctx context.Context, + log logr.Logger, + server *metalv1alpha1.Server, + serverBIOSReference corev1.LocalObjectReference, +) error { + var err error + serverBase := server.DeepCopy() + server.Spec.BIOSSettingsRef = serverBIOSReference + if err = r.Patch(ctx, server, client.MergeFrom(serverBase)); err != nil { + log.Error(err, "failed to patch bios settings ref") + } + return err +} + +func (r *ServerBIOSReconciler) patchJobReference( + ctx context.Context, + log logr.Logger, + serverBIOS *metalv1alpha1.ServerBIOS, + jobReference metalv1alpha1.RunningJobRef, +) (ctrl.Result, error) { + var err error + serverBIOSBase := serverBIOS.DeepCopy() + serverBIOS.Status.RunningJob = jobReference + if err = r.Patch(ctx, serverBIOSBase, client.MergeFrom(serverBIOS)); err != nil { + log.Error(err, "failed to patch server BIOS") + } + return ctrl.Result{}, err +}