diff --git a/PROJECT b/PROJECT index 54985e959..713b6e412 100644 --- a/PROJECT +++ b/PROJECT @@ -155,6 +155,14 @@ resources: kind: BMCUser path: github.com/ironcore-dev/metal-operator/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + controller: true + domain: ironcore.dev + group: metal + kind: BMCUserSet + path: github.com/ironcore-dev/metal-operator/api/v1alpha1 + version: v1alpha1 - api: crdVersion: v1 controller: true diff --git a/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcuserset.go b/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcuserset.go new file mode 100644 index 000000000..e851bf81e --- /dev/null +++ b/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcuserset.go @@ -0,0 +1,232 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// BMCUserSetApplyConfiguration represents a declarative configuration of the BMCUserSet type for use +// with apply. +// +// BMCUserSet is the Schema for the bmcusersets API. +type BMCUserSetApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *BMCUserSetSpecApplyConfiguration `json:"spec,omitempty"` + Status *BMCUserSetStatusApplyConfiguration `json:"status,omitempty"` +} + +// BMCUserSet constructs a declarative configuration of the BMCUserSet type for use with +// apply. +func BMCUserSet(name, namespace string) *BMCUserSetApplyConfiguration { + b := &BMCUserSetApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("BMCUserSet") + b.WithAPIVersion("metal.ironcore.dev/v1alpha1") + return b +} + +func (b BMCUserSetApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithKind(value string) *BMCUserSetApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithAPIVersion(value string) *BMCUserSetApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithName(value string) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithGenerateName(value string) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithNamespace(value string) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithUID(value types.UID) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithResourceVersion(value string) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithGeneration(value int64) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithCreationTimestamp(value metav1.Time) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *BMCUserSetApplyConfiguration) WithLabels(entries map[string]string) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *BMCUserSetApplyConfiguration) WithAnnotations(entries map[string]string) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *BMCUserSetApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *BMCUserSetApplyConfiguration) WithFinalizers(values ...string) *BMCUserSetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *BMCUserSetApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithSpec(value *BMCUserSetSpecApplyConfiguration) *BMCUserSetApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *BMCUserSetApplyConfiguration) WithStatus(value *BMCUserSetStatusApplyConfiguration) *BMCUserSetApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *BMCUserSetApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *BMCUserSetApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *BMCUserSetApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *BMCUserSetApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcusersetspec.go b/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcusersetspec.go new file mode 100644 index 000000000..5805a7ab0 --- /dev/null +++ b/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcusersetspec.go @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// BMCUserSetSpecApplyConfiguration represents a declarative configuration of the BMCUserSetSpec type for use +// with apply. +// +// BMCUserSetSpec defines the desired state of BMCUserSet. +type BMCUserSetSpecApplyConfiguration struct { + // BMCSelector specifies a label selector to identify the BMCs that are to be selected. + BMCSelector *v1.LabelSelectorApplyConfiguration `json:"bmcSelector,omitempty"` + // BMCUserTemplate defines the template for the BMCUser Resource to be applied to the BMCs. + BMCUserTemplate *BMCUserTemplateApplyConfiguration `json:"bmcUserTemplate,omitempty"` +} + +// BMCUserSetSpecApplyConfiguration constructs a declarative configuration of the BMCUserSetSpec type for use with +// apply. +func BMCUserSetSpec() *BMCUserSetSpecApplyConfiguration { + return &BMCUserSetSpecApplyConfiguration{} +} + +// WithBMCSelector sets the BMCSelector field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BMCSelector field is set to the value of the last call. +func (b *BMCUserSetSpecApplyConfiguration) WithBMCSelector(value *v1.LabelSelectorApplyConfiguration) *BMCUserSetSpecApplyConfiguration { + b.BMCSelector = value + return b +} + +// WithBMCUserTemplate sets the BMCUserTemplate field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BMCUserTemplate field is set to the value of the last call. +func (b *BMCUserSetSpecApplyConfiguration) WithBMCUserTemplate(value *BMCUserTemplateApplyConfiguration) *BMCUserSetSpecApplyConfiguration { + b.BMCUserTemplate = value + return b +} diff --git a/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcusersetstatus.go b/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcusersetstatus.go new file mode 100644 index 000000000..3d70d0cdb --- /dev/null +++ b/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcusersetstatus.go @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +// BMCUserSetStatusApplyConfiguration represents a declarative configuration of the BMCUserSetStatus type for use +// with apply. +// +// BMCUserSetStatus defines the observed state of BMCUserSet. +type BMCUserSetStatusApplyConfiguration struct { + // FullyLabeledBMCs is the number of BMC in the set. + FullyLabeledBMCs *int32 `json:"fullyLabeledBMCs,omitempty"` + // AvailableBMCUsers is the number of BMCUsers currently created by the set. + AvailableBMCUsers *int32 `json:"availableBMCUsers,omitempty"` +} + +// BMCUserSetStatusApplyConfiguration constructs a declarative configuration of the BMCUserSetStatus type for use with +// apply. +func BMCUserSetStatus() *BMCUserSetStatusApplyConfiguration { + return &BMCUserSetStatusApplyConfiguration{} +} + +// WithFullyLabeledBMCs sets the FullyLabeledBMCs field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the FullyLabeledBMCs field is set to the value of the last call. +func (b *BMCUserSetStatusApplyConfiguration) WithFullyLabeledBMCs(value int32) *BMCUserSetStatusApplyConfiguration { + b.FullyLabeledBMCs = &value + return b +} + +// WithAvailableBMCUsers sets the AvailableBMCUsers field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AvailableBMCUsers field is set to the value of the last call. +func (b *BMCUserSetStatusApplyConfiguration) WithAvailableBMCUsers(value int32) *BMCUserSetStatusApplyConfiguration { + b.AvailableBMCUsers = &value + return b +} diff --git a/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcuserspec.go b/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcuserspec.go index 9f976ed94..690cd19bc 100644 --- a/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcuserspec.go +++ b/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcuserspec.go @@ -6,8 +6,8 @@ package v1alpha1 import ( - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // BMCUserSpecApplyConfiguration represents a declarative configuration of the BMCUserSpec type for use @@ -15,20 +15,9 @@ import ( // // BMCUserSpec defines the desired state of BMCUser. type BMCUserSpecApplyConfiguration struct { - // UserName is the username of the BMC user. - UserName *string `json:"userName,omitempty"` - // RoleID is the ID of the role to assign to the user. - RoleID *string `json:"roleID,omitempty"` - // Description is a description for the BMC user. - Description *string `json:"description,omitempty"` - // RotationPeriod defines how often the password should be rotated. - // If not set, the password will not be rotated. - RotationPeriod *v1.Duration `json:"rotationPeriod,omitempty"` - // BMCSecretRef references the BMCSecret containing the credentials for this user. - // If not set, the operator will generate a secure password based on BMC manufacturer requirements. - BMCSecretRef *corev1.LocalObjectReference `json:"bmcSecretRef,omitempty"` + BMCUserTemplateApplyConfiguration `json:",inline"` // BMCRef references the BMC this user should be created on. - BMCRef *corev1.LocalObjectReference `json:"bmcRef,omitempty"` + BMCRef *v1.LocalObjectReference `json:"bmcRef,omitempty"` } // BMCUserSpecApplyConfiguration constructs a declarative configuration of the BMCUserSpec type for use with @@ -41,7 +30,7 @@ func BMCUserSpec() *BMCUserSpecApplyConfiguration { // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the UserName field is set to the value of the last call. func (b *BMCUserSpecApplyConfiguration) WithUserName(value string) *BMCUserSpecApplyConfiguration { - b.UserName = &value + b.BMCUserTemplateApplyConfiguration.UserName = &value return b } @@ -49,7 +38,7 @@ func (b *BMCUserSpecApplyConfiguration) WithUserName(value string) *BMCUserSpecA // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the RoleID field is set to the value of the last call. func (b *BMCUserSpecApplyConfiguration) WithRoleID(value string) *BMCUserSpecApplyConfiguration { - b.RoleID = &value + b.BMCUserTemplateApplyConfiguration.RoleID = &value return b } @@ -57,30 +46,30 @@ func (b *BMCUserSpecApplyConfiguration) WithRoleID(value string) *BMCUserSpecApp // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Description field is set to the value of the last call. func (b *BMCUserSpecApplyConfiguration) WithDescription(value string) *BMCUserSpecApplyConfiguration { - b.Description = &value + b.BMCUserTemplateApplyConfiguration.Description = &value return b } // WithRotationPeriod sets the RotationPeriod field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the RotationPeriod field is set to the value of the last call. -func (b *BMCUserSpecApplyConfiguration) WithRotationPeriod(value v1.Duration) *BMCUserSpecApplyConfiguration { - b.RotationPeriod = &value +func (b *BMCUserSpecApplyConfiguration) WithRotationPeriod(value metav1.Duration) *BMCUserSpecApplyConfiguration { + b.BMCUserTemplateApplyConfiguration.RotationPeriod = &value return b } // WithBMCSecretRef sets the BMCSecretRef field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the BMCSecretRef field is set to the value of the last call. -func (b *BMCUserSpecApplyConfiguration) WithBMCSecretRef(value corev1.LocalObjectReference) *BMCUserSpecApplyConfiguration { - b.BMCSecretRef = &value +func (b *BMCUserSpecApplyConfiguration) WithBMCSecretRef(value v1.LocalObjectReference) *BMCUserSpecApplyConfiguration { + b.BMCUserTemplateApplyConfiguration.BMCSecretRef = &value return b } // WithBMCRef sets the BMCRef field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the BMCRef field is set to the value of the last call. -func (b *BMCUserSpecApplyConfiguration) WithBMCRef(value corev1.LocalObjectReference) *BMCUserSpecApplyConfiguration { +func (b *BMCUserSpecApplyConfiguration) WithBMCRef(value v1.LocalObjectReference) *BMCUserSpecApplyConfiguration { b.BMCRef = &value return b } diff --git a/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcusertemplate.go b/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcusertemplate.go new file mode 100644 index 000000000..ededce666 --- /dev/null +++ b/api/v1alpha1/applyconfiguration/api/v1alpha1/bmcusertemplate.go @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// BMCUserTemplateApplyConfiguration represents a declarative configuration of the BMCUserTemplate type for use +// with apply. +// +// BMCUserTemplate defines the template for the BMCUser Resource to be applied to the BMCs. +type BMCUserTemplateApplyConfiguration struct { + // Username of the BMC user. + UserName *string `json:"userName,omitempty"` + // RoleID is the ID of the role to assign to the user. + // The available roles depend on the BMC implementation. + // For Redfish, common role IDs are "Administrator", "Operator", "ReadOnly". + RoleID *string `json:"roleID,omitempty"` + // Description is an optional description for the BMC user. + Description *string `json:"description,omitempty"` + // RotationPeriod defines how often the password should be rotated. + // if not set, the password will not be rotated. + RotationPeriod *v1.Duration `json:"rotationPeriod,omitempty"` + // BMCSecretRef references the BMCSecret containing the credentials for this user. + // If not set, the operator will generate a secure password based on BMC manufacturer requirements. + BMCSecretRef *corev1.LocalObjectReference `json:"bmcSecretRef,omitempty"` +} + +// BMCUserTemplateApplyConfiguration constructs a declarative configuration of the BMCUserTemplate type for use with +// apply. +func BMCUserTemplate() *BMCUserTemplateApplyConfiguration { + return &BMCUserTemplateApplyConfiguration{} +} + +// WithUserName sets the UserName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UserName field is set to the value of the last call. +func (b *BMCUserTemplateApplyConfiguration) WithUserName(value string) *BMCUserTemplateApplyConfiguration { + b.UserName = &value + return b +} + +// WithRoleID sets the RoleID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RoleID field is set to the value of the last call. +func (b *BMCUserTemplateApplyConfiguration) WithRoleID(value string) *BMCUserTemplateApplyConfiguration { + b.RoleID = &value + return b +} + +// WithDescription sets the Description field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Description field is set to the value of the last call. +func (b *BMCUserTemplateApplyConfiguration) WithDescription(value string) *BMCUserTemplateApplyConfiguration { + b.Description = &value + return b +} + +// WithRotationPeriod sets the RotationPeriod field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RotationPeriod field is set to the value of the last call. +func (b *BMCUserTemplateApplyConfiguration) WithRotationPeriod(value v1.Duration) *BMCUserTemplateApplyConfiguration { + b.RotationPeriod = &value + return b +} + +// WithBMCSecretRef sets the BMCSecretRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BMCSecretRef field is set to the value of the last call. +func (b *BMCUserTemplateApplyConfiguration) WithBMCSecretRef(value corev1.LocalObjectReference) *BMCUserTemplateApplyConfiguration { + b.BMCSecretRef = &value + return b +} diff --git a/api/v1alpha1/applyconfiguration/api/v1alpha1/dynamicsetting.go b/api/v1alpha1/applyconfiguration/api/v1alpha1/dynamicsetting.go new file mode 100644 index 000000000..9b05a178d --- /dev/null +++ b/api/v1alpha1/applyconfiguration/api/v1alpha1/dynamicsetting.go @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +// DynamicSettingApplyConfiguration represents a declarative configuration of the DynamicSetting type for use +// with apply. +type DynamicSettingApplyConfiguration struct { + // Key is the BMC setting key to set. + Key *string `json:"key,omitempty"` + // ValueFrom defines a simple single source for the setting value. + ValueFrom *DynamicSettingSourceApplyConfiguration `json:"valueFrom,omitempty"` + // Format defines a composite setting format with placeholders like $(name). + Format *string `json:"format,omitempty"` + // Variables maps format placeholder names to their sources. + Variables map[string]DynamicSettingSourceApplyConfiguration `json:"variables,omitempty"` +} + +// DynamicSettingApplyConfiguration constructs a declarative configuration of the DynamicSetting type for use with +// apply. +func DynamicSetting() *DynamicSettingApplyConfiguration { + return &DynamicSettingApplyConfiguration{} +} + +// WithKey sets the Key field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Key field is set to the value of the last call. +func (b *DynamicSettingApplyConfiguration) WithKey(value string) *DynamicSettingApplyConfiguration { + b.Key = &value + return b +} + +// WithValueFrom sets the ValueFrom field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ValueFrom field is set to the value of the last call. +func (b *DynamicSettingApplyConfiguration) WithValueFrom(value *DynamicSettingSourceApplyConfiguration) *DynamicSettingApplyConfiguration { + b.ValueFrom = value + return b +} + +// WithFormat sets the Format field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Format field is set to the value of the last call. +func (b *DynamicSettingApplyConfiguration) WithFormat(value string) *DynamicSettingApplyConfiguration { + b.Format = &value + return b +} + +// WithVariables puts the entries into the Variables field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Variables field, +// overwriting an existing map entries in Variables field with the same key. +func (b *DynamicSettingApplyConfiguration) WithVariables(entries map[string]DynamicSettingSourceApplyConfiguration) *DynamicSettingApplyConfiguration { + if b.Variables == nil && len(entries) > 0 { + b.Variables = make(map[string]DynamicSettingSourceApplyConfiguration, len(entries)) + } + for k, v := range entries { + b.Variables[k] = v + } + return b +} diff --git a/api/v1alpha1/applyconfiguration/api/v1alpha1/dynamicsettingsource.go b/api/v1alpha1/applyconfiguration/api/v1alpha1/dynamicsettingsource.go new file mode 100644 index 000000000..d1a7110d8 --- /dev/null +++ b/api/v1alpha1/applyconfiguration/api/v1alpha1/dynamicsettingsource.go @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +// DynamicSettingSourceApplyConfiguration represents a declarative configuration of the DynamicSettingSource type for use +// with apply. +type DynamicSettingSourceApplyConfiguration struct { + // BMCLabel is sourced from a label on the selected BMC. + BMCLabel *string `json:"bmcLabel,omitempty"` + // ConfigMapKeyRef points to a namespaced ConfigMap key. + ConfigMapKeyRef *NamespacedKeySelectorApplyConfiguration `json:"configMapKeyRef,omitempty"` + // SecretKeyRef points to a namespaced Secret key. + SecretKeyRef *NamespacedKeySelectorApplyConfiguration `json:"secretKeyRef,omitempty"` +} + +// DynamicSettingSourceApplyConfiguration constructs a declarative configuration of the DynamicSettingSource type for use with +// apply. +func DynamicSettingSource() *DynamicSettingSourceApplyConfiguration { + return &DynamicSettingSourceApplyConfiguration{} +} + +// WithBMCLabel sets the BMCLabel field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the BMCLabel field is set to the value of the last call. +func (b *DynamicSettingSourceApplyConfiguration) WithBMCLabel(value string) *DynamicSettingSourceApplyConfiguration { + b.BMCLabel = &value + return b +} + +// WithConfigMapKeyRef sets the ConfigMapKeyRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ConfigMapKeyRef field is set to the value of the last call. +func (b *DynamicSettingSourceApplyConfiguration) WithConfigMapKeyRef(value *NamespacedKeySelectorApplyConfiguration) *DynamicSettingSourceApplyConfiguration { + b.ConfigMapKeyRef = value + return b +} + +// WithSecretKeyRef sets the SecretKeyRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SecretKeyRef field is set to the value of the last call. +func (b *DynamicSettingSourceApplyConfiguration) WithSecretKeyRef(value *NamespacedKeySelectorApplyConfiguration) *DynamicSettingSourceApplyConfiguration { + b.SecretKeyRef = value + return b +} diff --git a/api/v1alpha1/applyconfiguration/utils.go b/api/v1alpha1/applyconfiguration/utils.go index 689cf239f..fa89945e6 100644 --- a/api/v1alpha1/applyconfiguration/utils.go +++ b/api/v1alpha1/applyconfiguration/utils.go @@ -75,10 +75,18 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1alpha1.BMCStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("BMCUser"): return &apiv1alpha1.BMCUserApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("BMCUserSet"): + return &apiv1alpha1.BMCUserSetApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("BMCUserSetSpec"): + return &apiv1alpha1.BMCUserSetSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("BMCUserSetStatus"): + return &apiv1alpha1.BMCUserSetStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("BMCUserSpec"): return &apiv1alpha1.BMCUserSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("BMCUserStatus"): return &apiv1alpha1.BMCUserStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("BMCUserTemplate"): + return &apiv1alpha1.BMCUserTemplateApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("BMCVersion"): return &apiv1alpha1.BMCVersionApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("BMCVersionSet"): diff --git a/api/v1alpha1/bmcuser_types.go b/api/v1alpha1/bmcuser_types.go index c6a244bea..df25c60c3 100644 --- a/api/v1alpha1/bmcuser_types.go +++ b/api/v1alpha1/bmcuser_types.go @@ -10,22 +10,7 @@ import ( // BMCUserSpec defines the desired state of BMCUser. type BMCUserSpec struct { - // UserName is the username of the BMC user. - UserName string `json:"userName"` - - // RoleID is the ID of the role to assign to the user. - RoleID string `json:"roleID"` - - // Description is a description for the BMC user. - Description string `json:"description,omitempty"` - - // RotationPeriod defines how often the password should be rotated. - // If not set, the password will not be rotated. - RotationPeriod *metav1.Duration `json:"rotationPeriod,omitempty"` - - // BMCSecretRef references the BMCSecret containing the credentials for this user. - // If not set, the operator will generate a secure password based on BMC manufacturer requirements. - BMCSecretRef *v1.LocalObjectReference `json:"bmcSecretRef,omitempty"` + BMCUserTemplate `json:",inline"` // BMCRef references the BMC this user should be created on. BMCRef *v1.LocalObjectReference `json:"bmcRef,omitempty"` diff --git a/api/v1alpha1/bmcuserset_types.go b/api/v1alpha1/bmcuserset_types.go new file mode 100644 index 000000000..33644a0e7 --- /dev/null +++ b/api/v1alpha1/bmcuserset_types.go @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// BMCUserTemplate defines the template for the BMCUser Resource to be applied to the BMCs. +type BMCUserTemplate struct { + // Username of the BMC user. + // +required + UserName string `json:"userName"` + // RoleID is the ID of the role to assign to the user. + // The available roles depend on the BMC implementation. + // For Redfish, common role IDs are "Administrator", "Operator", "ReadOnly". + // +required + RoleID string `json:"roleID"` + // Description is an optional description for the BMC user. + Description string `json:"description,omitempty"` + // RotationPeriod defines how often the password should be rotated. + // if not set, the password will not be rotated. + RotationPeriod *metav1.Duration `json:"rotationPeriod,omitempty"` + // BMCSecretRef references the BMCSecret containing the credentials for this user. + // If not set, the operator will generate a secure password based on BMC manufacturer requirements. + BMCSecretRef *corev1.LocalObjectReference `json:"bmcSecretRef,omitempty"` +} + +// BMCUserSetSpec defines the desired state of BMCUserSet. +type BMCUserSetSpec struct { + // BMCSelector specifies a label selector to identify the BMCs that are to be selected. + // +required + BMCSelector metav1.LabelSelector `json:"bmcSelector"` + + // BMCUserTemplate defines the template for the BMCUser Resource to be applied to the BMCs. + // +required + BMCUserTemplate BMCUserTemplate `json:"bmcUserTemplate"` +} + +// BMCUserSetStatus defines the observed state of BMCUserSet. +type BMCUserSetStatus struct { + // FullyLabeledBMCs is the number of BMC in the set. + FullyLabeledBMCs int32 `json:"fullyLabeledBMCs,omitempty"` + // AvailableBMCUsers is the number of BMCUsers currently created by the set. + AvailableBMCUsers int32 `json:"availableBMCUsers,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:printcolumn:name="UserName",type=string,JSONPath=`.spec.bmcUserTemplate.userName` +// +kubebuilder:printcolumn:name="RoleID",type=string,JSONPath=`.spec.bmcUserTemplate.roleID` +// +kubebuilder:printcolumn:name="TotalBMCs",type="integer",JSONPath=`.status.fullyLabeledBMCs` +// +kubebuilder:printcolumn:name="AvailableBMCUsers",type="integer",JSONPath=`.status.availableBMCUsers` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// BMCUserSet is the Schema for the bmcusersets API. +type BMCUserSet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec BMCUserSetSpec `json:"spec,omitempty"` + Status BMCUserSetStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// BMCUserSetList contains a list of BMCUserSet. +type BMCUserSetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []BMCUserSet `json:"items"` +} + +func init() { + SchemeBuilder.Register(&BMCUserSet{}, &BMCUserSetList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e55cebd46..3fd802d1f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1025,18 +1025,100 @@ func (in *BMCUserList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BMCUserSpec) DeepCopyInto(out *BMCUserSpec) { +func (in *BMCUserSet) DeepCopyInto(out *BMCUserSet) { *out = *in - if in.RotationPeriod != nil { - in, out := &in.RotationPeriod, &out.RotationPeriod - *out = new(metav1.Duration) - **out = **in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BMCUserSet. +func (in *BMCUserSet) DeepCopy() *BMCUserSet { + if in == nil { + return nil } - if in.BMCSecretRef != nil { - in, out := &in.BMCSecretRef, &out.BMCSecretRef - *out = new(v1.LocalObjectReference) - **out = **in + out := new(BMCUserSet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BMCUserSet) 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 *BMCUserSetList) DeepCopyInto(out *BMCUserSetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BMCUserSet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BMCUserSetList. +func (in *BMCUserSetList) DeepCopy() *BMCUserSetList { + if in == nil { + return nil + } + out := new(BMCUserSetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BMCUserSetList) 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 *BMCUserSetSpec) DeepCopyInto(out *BMCUserSetSpec) { + *out = *in + in.BMCSelector.DeepCopyInto(&out.BMCSelector) + in.BMCUserTemplate.DeepCopyInto(&out.BMCUserTemplate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BMCUserSetSpec. +func (in *BMCUserSetSpec) DeepCopy() *BMCUserSetSpec { + if in == nil { + return nil + } + out := new(BMCUserSetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BMCUserSetStatus) DeepCopyInto(out *BMCUserSetStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BMCUserSetStatus. +func (in *BMCUserSetStatus) DeepCopy() *BMCUserSetStatus { + if in == nil { + return nil + } + out := new(BMCUserSetStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BMCUserSpec) DeepCopyInto(out *BMCUserSpec) { + *out = *in + in.BMCUserTemplate.DeepCopyInto(&out.BMCUserTemplate) if in.BMCRef != nil { in, out := &in.BMCRef, &out.BMCRef *out = new(v1.LocalObjectReference) @@ -1082,6 +1164,31 @@ func (in *BMCUserStatus) DeepCopy() *BMCUserStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BMCUserTemplate) DeepCopyInto(out *BMCUserTemplate) { + *out = *in + if in.RotationPeriod != nil { + in, out := &in.RotationPeriod, &out.RotationPeriod + *out = new(metav1.Duration) + **out = **in + } + if in.BMCSecretRef != nil { + in, out := &in.BMCSecretRef, &out.BMCSecretRef + *out = new(v1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BMCUserTemplate. +func (in *BMCUserTemplate) DeepCopy() *BMCUserTemplate { + if in == nil { + return nil + } + out := new(BMCUserTemplate) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BMCVersion) DeepCopyInto(out *BMCVersion) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 173c59058..f0d62686f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -512,8 +512,7 @@ func main() { // nolint: gocyclo ResourcePollingInterval: resourcePollingInterval, ResourcePollingTimeout: resourcePollingTimeout, }, - TimeoutExpiry: biosSettingsApplyTimeout, - DefaultFailedAutoRetryCount: int32(defaultFailedAutoRetryCount), + TimeoutExpiry: biosSettingsApplyTimeout, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create controller", "controller", "biossettings") os.Exit(1) @@ -533,7 +532,6 @@ func main() { // nolint: gocyclo ResourcePollingInterval: resourcePollingInterval, ResourcePollingTimeout: resourcePollingTimeout, }, - DefaultFailedAutoRetryCount: int32(defaultFailedAutoRetryCount), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create controller", "controller", "biosversion") os.Exit(1) @@ -553,7 +551,6 @@ func main() { // nolint: gocyclo ResourcePollingInterval: resourcePollingInterval, ResourcePollingTimeout: resourcePollingTimeout, }, - DefaultFailedAutoRetryCount: int32(defaultFailedAutoRetryCount), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create controller", "controller", "bmcsettings") os.Exit(1) @@ -573,7 +570,6 @@ func main() { // nolint: gocyclo ResourcePollingInterval: resourcePollingInterval, ResourcePollingTimeout: resourcePollingTimeout, }, - DefaultFailedAutoRetryCount: int32(defaultFailedAutoRetryCount), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create controller", "controller", "bmcversion") os.Exit(1) @@ -676,6 +672,50 @@ func main() { // nolint: gocyclo os.Exit(1) } } + if err = (&controller.BMCVersionSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "BMCVersionSet") + os.Exit(1) + } + if err = (&controller.BIOSSettingsSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "BIOSSettingsSet") + os.Exit(1) + } + if err := (&controller.BMCSettingsSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "BMCSettingsSet") + os.Exit(1) + } + if err := (&controller.BMCUserSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "BMCUserSet") + os.Exit(1) + } + if err = (&controller.BMCUserReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + DefaultProtocol: effectiveProtocol, + SkipCertValidation: effectiveSkipCert, + BMCOptions: bmc.Options{ + BasicAuth: true, + PowerPollingInterval: powerPollingInterval, + PowerPollingTimeout: powerPollingTimeout, + ResourcePollingInterval: resourcePollingInterval, + ResourcePollingTimeout: resourcePollingTimeout, + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "User") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/metal.ironcore.dev_bmcusers.yaml b/config/crd/bases/metal.ironcore.dev_bmcusers.yaml index a14f5d6df..b5fa53327 100644 --- a/config/crd/bases/metal.ironcore.dev_bmcusers.yaml +++ b/config/crd/bases/metal.ironcore.dev_bmcusers.yaml @@ -92,18 +92,21 @@ spec: type: object x-kubernetes-map-type: atomic description: - description: Description is a description for the BMC user. + description: Description is an optional description for the BMC user. type: string roleID: - description: RoleID is the ID of the role to assign to the user. + description: |- + RoleID is the ID of the role to assign to the user. + The available roles depend on the BMC implementation. + For Redfish, common role IDs are "Administrator", "Operator", "ReadOnly". type: string rotationPeriod: description: |- RotationPeriod defines how often the password should be rotated. - If not set, the password will not be rotated. + if not set, the password will not be rotated. type: string userName: - description: UserName is the username of the BMC user. + description: Username of the BMC user. type: string required: - roleID diff --git a/config/crd/bases/metal.ironcore.dev_bmcusersets.yaml b/config/crd/bases/metal.ironcore.dev_bmcusersets.yaml new file mode 100644 index 000000000..0461bff25 --- /dev/null +++ b/config/crd/bases/metal.ironcore.dev_bmcusersets.yaml @@ -0,0 +1,168 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: bmcusersets.metal.ironcore.dev +spec: + group: metal.ironcore.dev + names: + kind: BMCUserSet + listKind: BMCUserSetList + plural: bmcusersets + singular: bmcuserset + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.bmcUserTemplate.userName + name: UserName + type: string + - jsonPath: .spec.bmcUserTemplate.roleID + name: RoleID + type: string + - jsonPath: .status.fullyLabeledBMCs + name: TotalBMCs + type: integer + - jsonPath: .status.availableBMCUsers + name: AvailableBMCUsers + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: BMCUserSet is the Schema for the bmcusersets 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: BMCUserSetSpec defines the desired state of BMCUserSet. + properties: + bmcSelector: + description: BMCSelector specifies a label selector to identify the + BMCs that are to be selected. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + bmcUserTemplate: + description: BMCUserTemplate defines the template for the BMCUser + Resource to be applied to the BMCs. + properties: + bmcSecretRef: + description: |- + BMCSecretRef references the BMCSecret containing the credentials for this user. + If not set, the operator will generate a secure password based on BMC manufacturer requirements. + 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 + description: + description: Description is an optional description for the BMC + user. + type: string + roleID: + description: |- + RoleID is the ID of the role to assign to the user. + The available roles depend on the BMC implementation. + For Redfish, common role IDs are "Administrator", "Operator", "ReadOnly". + type: string + rotationPeriod: + description: |- + RotationPeriod defines how often the password should be rotated. + if not set, the password will not be rotated. + type: string + userName: + description: Username of the BMC user. + type: string + required: + - roleID + - userName + type: object + required: + - bmcSelector + - bmcUserTemplate + type: object + status: + description: BMCUserSetStatus defines the observed state of BMCUserSet. + properties: + availableBMCUsers: + description: AvailableBMCUsers is the number of BMCUsers currently + created by the set. + format: int32 + type: integer + fullyLabeledBMCs: + description: FullyLabeledBMCs is the number of BMC in the set. + format: int32 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index d6d14202e..6e0f33fe4 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -17,6 +17,7 @@ resources: - bases/metal.ironcore.dev_bmcversionsets.yaml - bases/metal.ironcore.dev_biossettingssets.yaml - bases/metal.ironcore.dev_bmcusers.yaml +- bases/metal.ironcore.dev_bmcusersets.yaml - bases/metal.ironcore.dev_bmcsettingssets.yaml #+kubebuilder:scaffold:crdkustomizeresource diff --git a/config/rbac/bmcuserset_admin_role.yaml b/config/rbac/bmcuserset_admin_role.yaml new file mode 100644 index 000000000..65eeaa9f8 --- /dev/null +++ b/config/rbac/bmcuserset_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over metal.ironcore.dev. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: metal-operator + app.kubernetes.io/managed-by: kustomize + name: bmcuserset-admin-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets + verbs: + - '*' +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets/status + verbs: + - get diff --git a/config/rbac/bmcuserset_editor_role.yaml b/config/rbac/bmcuserset_editor_role.yaml new file mode 100644 index 000000000..c2ddd75af --- /dev/null +++ b/config/rbac/bmcuserset_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the metal.ironcore.dev. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: metal-operator + app.kubernetes.io/managed-by: kustomize + name: bmcuserset-editor-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets/status + verbs: + - get diff --git a/config/rbac/bmcuserset_viewer_role.yaml b/config/rbac/bmcuserset_viewer_role.yaml new file mode 100644 index 000000000..f6a8d6eb8 --- /dev/null +++ b/config/rbac/bmcuserset_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to metal.ironcore.dev resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: metal-operator + app.kubernetes.io/managed-by: kustomize + name: bmcuserset-viewer-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets + verbs: + - get + - list + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 03a6590cf..474c6019b 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -1,72 +1,67 @@ resources: - # All RBAC will be applied under this service account in - # the deployment namespace. You may comment out this resource - # if your manager will use a service account that exists at - # runtime. Be sure to update RoleBinding and ClusterRoleBinding - # subjects if changing service account names. - - service_account.yaml - - role.yaml - - role_binding.yaml - - leader_election_role.yaml - - leader_election_role_binding.yaml - # The following RBAC configurations are used to protect - # the metrics endpoint with authn/authz. These configurations - # ensure that only authorized users and service accounts - # can access the metrics endpoint. Comment the following - # permissions if you want to disable this protection. - # More info: https://book.kubebuilder.io/reference/metrics.html - - metrics_auth_role.yaml - - metrics_auth_role_binding.yaml - - metrics_reader_role.yaml - # For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by - # default, aiding admins in cluster management. Those roles are - # not used by the {{ .ProjectName }} itself. You can comment the following lines - # if you do not want those helpers be installed with your Project. - - biossettings_admin_role.yaml - - biossettings_editor_role.yaml - - biossettings_viewer_role.yaml - - biossettingsset_admin_role.yaml - - biossettingsset_editor_role.yaml - - biossettingsset_viewer_role.yaml - - biosversion_admin_role.yaml - - biosversion_editor_role.yaml - - biosversion_viewer_role.yaml - - biosversionset_admin_role.yaml - - biosversionset_editor_role.yaml - - biosversionset_viewer_role.yaml - - bmc_admin_role.yaml - - bmc_editor_role.yaml - - bmc_viewer_role.yaml - - bmcsecret_admin_role.yaml - - bmcsecret_editor_role.yaml - - bmcsecret_viewer_role.yaml - - bmcsettings_admin_role.yaml - - bmcsettings_editor_role.yaml - - bmcsettings_viewer_role.yaml - - bmcsettingsset_admin_role.yaml - - bmcsettingsset_editor_role.yaml - - bmcsettingsset_viewer_role.yaml - - bmcuser_admin_role.yaml - - bmcuser_editor_role.yaml - - bmcuser_viewer_role.yaml - - bmcversion_admin_role.yaml - - bmcversion_editor_role.yaml - - bmcversion_viewer_role.yaml - - bmcversionset_admin_role.yaml - - bmcversionset_editor_role.yaml - - bmcversionset_viewer_role.yaml - - endpoint_admin_role.yaml - - endpoint_editor_role.yaml - - endpoint_viewer_role.yaml - - server_admin_role.yaml - - server_editor_role.yaml - - server_viewer_role.yaml - - serverbootconfiguration_admin_role.yaml - - serverbootconfiguration_editor_role.yaml - - serverbootconfiguration_viewer_role.yaml - - serverclaim_admin_role.yaml - - serverclaim_editor_role.yaml - - serverclaim_viewer_role.yaml - - servermaintenance_admin_role.yaml - - servermaintenance_editor_role.yaml - - servermaintenance_viewer_role.yaml +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# The following RBAC configurations are used to protect +# the metrics endpoint with authn/authz. These configurations +# ensure that only authorized users and service accounts +# can access the metrics endpoint. Comment the following +# permissions if you want to disable this protection. +# More info: https://book.kubebuilder.io/reference/metrics.html +- metrics_auth_role.yaml +- metrics_auth_role_binding.yaml +- metrics_reader_role.yaml +# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the {{ .ProjectName }} itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- bmcuser_admin_role.yaml +- bmcuser_editor_role.yaml +- bmcuser_viewer_role.yaml +- bmcuserset_admin_role.yaml +- bmcuserset_editor_role.yaml +- bmcuserset_viewer_role.yaml +- bmcversionset_admin_role.yaml +- bmcversionset_editor_role.yaml +- bmcversionset_viewer_role.yaml +- biosversionset_admin_role.yaml +- biosversionset_editor_role.yaml +- biosversionset_viewer_role.yaml +- biossettingsset_admin_role.yaml +- biossettingsset_editor_role.yaml +- biossettingsset_viewer_role.yaml +- biosversion_admin_role.yaml +- biosversion_editor_role.yaml +- biosversion_viewer_role.yaml +- bmcsettings_admin_role.yaml +- bmcsettings_editor_role.yaml +- bmcsettings_viewer_role.yaml +- bmcversion_admin_role.yaml +- bmcversion_editor_role.yaml +- bmcversion_viewer_role.yaml +- servermaintenance_admin_role.yaml +- servermaintenance_editor_role.yaml +- servermaintenance_viewer_role.yaml + +# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the {{ .ProjectName }} itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- biossettings_admin_role.yaml +- biossettings_editor_role.yaml +- biossettings_viewer_role.yaml + +# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the metal-operator itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- bmcsettingsset_admin_role.yaml +- bmcsettingsset_editor_role.yaml +- bmcsettingsset_viewer_role.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index be414d599..793544194 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -57,6 +57,7 @@ rules: - bmcsettings - bmcsettingssets - bmcusers + - bmcusersets - bmcversions - bmcversionsets - endpoints @@ -85,6 +86,7 @@ rules: - bmcsettings/finalizers - bmcsettingssets/finalizers - bmcusers/finalizers + - bmcusersets/finalizers - bmcversions/finalizers - bmcversionsets/finalizers - endpoints/finalizers @@ -106,6 +108,7 @@ rules: - bmcsettings/status - bmcsettingssets/status - bmcusers/status + - bmcusersets/status - bmcversions/status - bmcversionsets/status - endpoints/status diff --git a/dist/chart/templates/crd/metal.ironcore.dev_bmcusers.yaml b/dist/chart/templates/crd/metal.ironcore.dev_bmcusers.yaml index e167a0b8c..41a7762cd 100644 --- a/dist/chart/templates/crd/metal.ironcore.dev_bmcusers.yaml +++ b/dist/chart/templates/crd/metal.ironcore.dev_bmcusers.yaml @@ -98,18 +98,21 @@ spec: type: object x-kubernetes-map-type: atomic description: - description: Description is a description for the BMC user. + description: Description is an optional description for the BMC user. type: string roleID: - description: RoleID is the ID of the role to assign to the user. + description: |- + RoleID is the ID of the role to assign to the user. + The available roles depend on the BMC implementation. + For Redfish, common role IDs are "Administrator", "Operator", "ReadOnly". type: string rotationPeriod: description: |- RotationPeriod defines how often the password should be rotated. - If not set, the password will not be rotated. + if not set, the password will not be rotated. type: string userName: - description: UserName is the username of the BMC user. + description: Username of the BMC user. type: string required: - roleID diff --git a/dist/chart/templates/crd/metal.ironcore.dev_bmcusersets.yaml b/dist/chart/templates/crd/metal.ironcore.dev_bmcusersets.yaml new file mode 100644 index 000000000..e509e9373 --- /dev/null +++ b/dist/chart/templates/crd/metal.ironcore.dev_bmcusersets.yaml @@ -0,0 +1,175 @@ +{{- if .Values.crd.enable }} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + labels: + {{- include "chart.labels" . | nindent 4 }} + annotations: + {{- if .Values.crd.keep }} + "helm.sh/resource-policy": keep + {{- end }} + controller-gen.kubebuilder.io/version: v0.20.1 + name: bmcusersets.metal.ironcore.dev +spec: + group: metal.ironcore.dev + names: + kind: BMCUserSet + listKind: BMCUserSetList + plural: bmcusersets + singular: bmcuserset + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.bmcUserTemplate.userName + name: UserName + type: string + - jsonPath: .spec.bmcUserTemplate.roleID + name: RoleID + type: string + - jsonPath: .status.fullyLabeledBMCs + name: TotalBMCs + type: integer + - jsonPath: .status.availableBMCUsers + name: AvailableBMCUsers + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: BMCUserSet is the Schema for the bmcusersets 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: BMCUserSetSpec defines the desired state of BMCUserSet. + properties: + bmcSelector: + description: BMCSelector specifies a label selector to identify the + BMCs that are to be selected. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + bmcUserTemplate: + description: BMCUserTemplate defines the template for the BMCUser + Resource to be applied to the BMCs. + properties: + bmcSecretRef: + description: |- + BMCSecretRef references the BMCSecret containing the credentials for this user. + If not set, the operator will generate a secure password based on BMC manufacturer requirements. + 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 + description: + description: Description is an optional description for the BMC + user. + type: string + roleID: + description: |- + RoleID is the ID of the role to assign to the user. + The available roles depend on the BMC implementation. + For Redfish, common role IDs are "Administrator", "Operator", "ReadOnly". + type: string + rotationPeriod: + description: |- + RotationPeriod defines how often the password should be rotated. + if not set, the password will not be rotated. + type: string + userName: + description: Username of the BMC user. + type: string + required: + - roleID + - userName + type: object + required: + - bmcSelector + - bmcUserTemplate + type: object + status: + description: BMCUserSetStatus defines the observed state of BMCUserSet. + properties: + availableBMCUsers: + description: AvailableBMCUsers is the number of BMCUsers currently + created by the set. + format: int32 + type: integer + fullyLabeledBMCs: + description: FullyLabeledBMCs is the number of BMC in the set. + format: int32 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +{{- end -}} diff --git a/dist/chart/templates/rbac/bmcuserset_admin_role.yaml b/dist/chart/templates/rbac/bmcuserset_admin_role.yaml new file mode 100644 index 000000000..bb160d35b --- /dev/null +++ b/dist/chart/templates/rbac/bmcuserset_admin_role.yaml @@ -0,0 +1,28 @@ +{{- if .Values.rbac.enable }} +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over metal.ironcore.dev. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "chart.labels" . | nindent 4 }} + name: bmcuserset-admin-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets + verbs: + - '*' +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets/status + verbs: + - get +{{- end -}} diff --git a/dist/chart/templates/rbac/bmcuserset_editor_role.yaml b/dist/chart/templates/rbac/bmcuserset_editor_role.yaml new file mode 100644 index 000000000..7053721ed --- /dev/null +++ b/dist/chart/templates/rbac/bmcuserset_editor_role.yaml @@ -0,0 +1,34 @@ +{{- if .Values.rbac.enable }} +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the metal.ironcore.dev. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "chart.labels" . | nindent 4 }} + name: bmcuserset-editor-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets/status + verbs: + - get +{{- end -}} diff --git a/dist/chart/templates/rbac/bmcuserset_viewer_role.yaml b/dist/chart/templates/rbac/bmcuserset_viewer_role.yaml new file mode 100644 index 000000000..795c548f4 --- /dev/null +++ b/dist/chart/templates/rbac/bmcuserset_viewer_role.yaml @@ -0,0 +1,30 @@ +{{- if .Values.rbac.enable }} +# This rule is not used by the project metal-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to metal.ironcore.dev resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "chart.labels" . | nindent 4 }} + name: bmcuserset-viewer-role +rules: +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets + verbs: + - get + - list + - watch +- apiGroups: + - metal.ironcore.dev + resources: + - bmcusersets/status + verbs: + - get +{{- end -}} diff --git a/dist/chart/templates/rbac/role.yaml b/dist/chart/templates/rbac/role.yaml index f37becf76..987b78d3a 100755 --- a/dist/chart/templates/rbac/role.yaml +++ b/dist/chart/templates/rbac/role.yaml @@ -60,6 +60,7 @@ rules: - bmcsettings - bmcsettingssets - bmcusers + - bmcusersets - bmcversions - bmcversionsets - endpoints @@ -88,6 +89,7 @@ rules: - bmcsettings/finalizers - bmcsettingssets/finalizers - bmcusers/finalizers + - bmcusersets/finalizers - bmcversions/finalizers - bmcversionsets/finalizers - endpoints/finalizers @@ -109,6 +111,7 @@ rules: - bmcsettings/status - bmcsettingssets/status - bmcusers/status + - bmcusersets/status - bmcversions/status - bmcversionsets/status - endpoints/status diff --git a/docs/api-reference/api.md b/docs/api-reference/api.md index e7a1b624a..46eb37d8b 100644 --- a/docs/api-reference/api.md +++ b/docs/api-reference/api.md @@ -20,6 +20,7 @@ Package v1alpha1 contains API Schema definitions for the metal v1alpha1 API grou - [BMCSettings](#bmcsettings) - [BMCSettingsSet](#bmcsettingsset) - [BMCUser](#bmcuser) +- [BMCUserSet](#bmcuserset) - [BMCVersion](#bmcversion) - [BMCVersionSet](#bmcversionset) - [Endpoint](#endpoint) @@ -708,6 +709,59 @@ BMCUser is the Schema for the bmcusers API. | `status` _[BMCUserStatus](#bmcuserstatus)_ | | | | +#### BMCUserSet + + + +BMCUserSet is the Schema for the bmcusersets API. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `metal.ironcore.dev/v1alpha1` | | | +| `kind` _string_ | `BMCUserSet` | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[BMCUserSetSpec](#bmcusersetspec)_ | | | | +| `status` _[BMCUserSetStatus](#bmcusersetstatus)_ | | | | + + +#### BMCUserSetSpec + + + +BMCUserSetSpec defines the desired state of BMCUserSet. + + + +_Appears in:_ +- [BMCUserSet](#bmcuserset) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `bmcSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#labelselector-v1-meta)_ | BMCSelector specifies a label selector to identify the BMCs that are to be selected. | | | +| `bmcUserTemplate` _[BMCUserTemplate](#bmcusertemplate)_ | BMCUserTemplate defines the template for the BMCUser Resource to be applied to the BMCs. | | | + + +#### BMCUserSetStatus + + + +BMCUserSetStatus defines the observed state of BMCUserSet. + + + +_Appears in:_ +- [BMCUserSet](#bmcuserset) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `fullyLabeledBMCs` _integer_ | FullyLabeledBMCs is the number of BMC in the set. | | | +| `availableBMCUsers` _integer_ | AvailableBMCUsers is the number of BMCUsers currently created by the set. | | | + + #### BMCUserSpec @@ -721,10 +775,10 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `userName` _string_ | UserName is the username of the BMC user. | | | -| `roleID` _string_ | RoleID is the ID of the role to assign to the user. | | | -| `description` _string_ | Description is a description for the BMC user. | | | -| `rotationPeriod` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#duration-v1-meta)_ | RotationPeriod defines how often the password should be rotated.
If not set, the password will not be rotated. | | | +| `userName` _string_ | Username of the BMC user. | | | +| `roleID` _string_ | RoleID is the ID of the role to assign to the user.
The available roles depend on the BMC implementation.
For Redfish, common role IDs are "Administrator", "Operator", "ReadOnly". | | | +| `description` _string_ | Description is an optional description for the BMC user. | | | +| `rotationPeriod` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#duration-v1-meta)_ | RotationPeriod defines how often the password should be rotated.
if not set, the password will not be rotated. | | | | `bmcSecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core)_ | BMCSecretRef references the BMCSecret containing the credentials for this user.
If not set, the operator will generate a secure password based on BMC manufacturer requirements. | | | | `bmcRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core)_ | BMCRef references the BMC this user should be created on. | | | @@ -748,6 +802,27 @@ _Appears in:_ | `id` _string_ | ID is the identifier of the user in the BMC system. | | | +#### BMCUserTemplate + + + +BMCUserTemplate defines the template for the BMCUser Resource to be applied to the BMCs. + + + +_Appears in:_ +- [BMCUserSetSpec](#bmcusersetspec) +- [BMCUserSpec](#bmcuserspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `userName` _string_ | Username of the BMC user. | | | +| `roleID` _string_ | RoleID is the ID of the role to assign to the user.
The available roles depend on the BMC implementation.
For Redfish, common role IDs are "Administrator", "Operator", "ReadOnly". | | | +| `description` _string_ | Description is an optional description for the BMC user. | | | +| `rotationPeriod` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#duration-v1-meta)_ | RotationPeriod defines how often the password should be rotated.
if not set, the password will not be rotated. | | | +| `bmcSecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core)_ | BMCSecretRef references the BMCSecret containing the credentials for this user.
If not set, the operator will generate a secure password based on BMC manufacturer requirements. | | | + + #### BMCVersion diff --git a/internal/controller/bmcuser_controller.go b/internal/controller/bmcuser_controller.go index 760f21f88..112a4d56e 100644 --- a/internal/controller/bmcuser_controller.go +++ b/internal/controller/bmcuser_controller.go @@ -128,11 +128,13 @@ func (r *BMCUserReconciler) patchUserStatus(ctx context.Context, user *metalv1al log.V(1).Info("BMC account already exists", "ID", account.ID) userBase := user.DeepCopy() user.Status.ID = account.ID - exp, err := time.Parse(time.RFC3339, account.PasswordExpiration) - if err == nil { - user.Status.PasswordExpiration = &metav1.Time{Time: exp} - } else { - log.Error(err, "Failed to parse password expiration time from BMC account", "Expiration", account.PasswordExpiration) + if account.PasswordExpiration != "" { + exp, err := time.Parse(time.RFC3339, account.PasswordExpiration) + if err == nil { + user.Status.PasswordExpiration = &metav1.Time{Time: exp} + } else { + log.Error(err, "Failed to parse password expiration time from BMC account", "Expiration", account.PasswordExpiration) + } } if err := r.Status().Patch(ctx, user, client.MergeFrom(userBase)); err != nil { return fmt.Errorf("failed to patch User status with BMC account ID: %w", err) diff --git a/internal/controller/bmcuser_controller_test.go b/internal/controller/bmcuser_controller_test.go index 53f59fd8d..e612c2c2f 100644 --- a/internal/controller/bmcuser_controller_test.go +++ b/internal/controller/bmcuser_controller_test.go @@ -83,8 +83,10 @@ var _ = Describe("BMCUser Controller", func() { Name: "test-user", }, Spec: metalv1alpha1.BMCUserSpec{ - UserName: "user", - RoleID: "ReadOnly", + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "user", + RoleID: "ReadOnly", + }, BMCRef: &v1.LocalObjectReference{ Name: bmc.Name, }, @@ -140,8 +142,10 @@ var _ = Describe("BMCUser Controller", func() { Name: "user01", }, Spec: metalv1alpha1.BMCUserSpec{ - UserName: "user01", - RoleID: "Readonly", + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "user01", + RoleID: "Readonly", + }, BMCRef: &v1.LocalObjectReference{ Name: bmc.Name, }, @@ -170,8 +174,10 @@ var _ = Describe("BMCUser Controller", func() { Name: "user02", }, Spec: metalv1alpha1.BMCUserSpec{ - UserName: "user02", - RoleID: "Readonly", + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "user02", + RoleID: "Readonly", + }, BMCRef: &v1.LocalObjectReference{ Name: bmc.Name, }, @@ -212,14 +218,16 @@ var _ = Describe("BMCUser Controller", func() { Name: "user03", }, Spec: metalv1alpha1.BMCUserSpec{ - UserName: "user03", - RoleID: "Readonly", + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "user03", + RoleID: "Readonly", + BMCSecretRef: &v1.LocalObjectReference{ + Name: user03Secret.Name, + }, + }, BMCRef: &v1.LocalObjectReference{ Name: bmc.Name, }, - BMCSecretRef: &v1.LocalObjectReference{ - Name: user03Secret.Name, - }, }, } Expect(k8sClient.Create(ctx, user03)).To(Succeed()) @@ -246,17 +254,19 @@ var _ = Describe("BMCUser Controller", func() { Name: "admin-user", }, Spec: metalv1alpha1.BMCUserSpec{ - UserName: "admin-user", - RoleID: "Administrator", + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "admin-user", + RoleID: "Administrator", + BMCSecretRef: &v1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + RotationPeriod: &metav1.Duration{ + Duration: 1 * time.Second, + }, + }, BMCRef: &v1.LocalObjectReference{ Name: bmc.Name, }, - BMCSecretRef: &v1.LocalObjectReference{ - Name: bmcSecret.Name, - }, - RotationPeriod: &metav1.Duration{ - Duration: 1 * time.Second, - }, }, } By("Creating a User resource") @@ -300,8 +310,10 @@ var _ = Describe("BMCUser Controller", func() { Name: "delete-user", }, Spec: metalv1alpha1.BMCUserSpec{ - UserName: "deleteUser", - RoleID: "ReadOnly", + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "deleteUser", + RoleID: "ReadOnly", + }, BMCRef: &v1.LocalObjectReference{ Name: bmc.Name, }, @@ -345,8 +357,10 @@ var _ = Describe("BMCUser Controller", func() { Name: "annotated-user", }, Spec: metalv1alpha1.BMCUserSpec{ - UserName: "annotated-user", - RoleID: "ReadOnly", + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "annotated-user", + RoleID: "ReadOnly", + }, BMCRef: &v1.LocalObjectReference{ Name: bmc.Name, }, diff --git a/internal/controller/bmcuserset_controller.go b/internal/controller/bmcuserset_controller.go new file mode 100644 index 000000000..1f2033813 --- /dev/null +++ b/internal/controller/bmcuserset_controller.go @@ -0,0 +1,451 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "context" + "errors" + "fmt" + "maps" + "strings" + + "github.com/ironcore-dev/controller-utils/clientutils" + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + utilvalidation "k8s.io/apimachinery/pkg/util/validation" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +type BMCUserSetReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +const BMCUserSetFinalizer = "metal.ironcore.dev/bmcuserset" + +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcusersets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcusersets/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcusersets/finalizers,verbs=update +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcusers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=metal.ironcore.dev,resources=bmcs,verbs=get;list;watch + +func (r *BMCUserSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + bmcUserSet := &metalv1alpha1.BMCUserSet{} + if err := r.Get(ctx, req.NamespacedName, bmcUserSet); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + log.V(1).Info("Reconciling BMCUserSet") + return r.reconcileExists(ctx, bmcUserSet) +} + +func (r *BMCUserSetReconciler) reconcileExists( + ctx context.Context, + bmcUserSet *metalv1alpha1.BMCUserSet, +) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + if !bmcUserSet.DeletionTimestamp.IsZero() { + log.V(1).Info("Object is being deleted") + return r.delete(ctx, bmcUserSet) + } + log.V(1).Info("Object exists and is not being deleted") + return r.reconcile(ctx, bmcUserSet) +} + +func (r *BMCUserSetReconciler) reconcile( + ctx context.Context, + bmcUserSet *metalv1alpha1.BMCUserSet, +) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + if shouldIgnoreReconciliation(bmcUserSet) { + log.V(1).Info("Skipped BMCUserSet reconciliation") + return ctrl.Result{}, nil + } + if modified, err := clientutils.PatchEnsureFinalizer(ctx, r.Client, bmcUserSet, BMCUserSetFinalizer); err != nil || modified { + return ctrl.Result{}, err + } + + bmcList, err := r.getBMCsBySelector(ctx, bmcUserSet) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get BMCs through label selector: %w", err) + } + + ownedBMCUsers, err := r.getOwnedBMCUsers(ctx, bmcUserSet) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to list owned BMCUsers: %w", err) + } + + if err := r.createMissingBMCUsers(ctx, bmcList, ownedBMCUsers, bmcUserSet); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create missing BMCUsers: %w", err) + } + + if err := r.deleteOrphanedBMCUsers(ctx, bmcList, ownedBMCUsers); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to delete orphaned BMCUsers: %w", err) + } + + // Re-fetch owned BMCUsers after mutations to avoid stale list + ownedBMCUsers, err = r.getOwnedBMCUsers(ctx, bmcUserSet) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to re-fetch owned BMCUsers: %w", err) + } + + if err := r.patchBMCUsersFromTemplate(ctx, bmcUserSet, ownedBMCUsers); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to patch BMCUsers from template: %w", err) + } + + log.V(1).Info("Updating BMCUserSet status") + currentStatus := r.getOwnedBMCUserSetStatus(ownedBMCUsers) + currentStatus.FullyLabeledBMCs = int32(len(bmcList.Items)) + if err := r.updateStatus(ctx, currentStatus, bmcUserSet); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update current BMCUserSet status: %w", err) + } + + return ctrl.Result{}, nil +} + +func (r *BMCUserSetReconciler) delete( + ctx context.Context, + bmcUserSet *metalv1alpha1.BMCUserSet, +) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + if !controllerutil.ContainsFinalizer(bmcUserSet, BMCUserSetFinalizer) { + return ctrl.Result{}, nil + } + + ownedBMCUsers, err := r.getOwnedBMCUsers(ctx, bmcUserSet) + if err != nil { + log.Error(err, "Failed to list owned BMCUsers") + return ctrl.Result{}, fmt.Errorf("failed to get owned BMCUsers: %w", err) + } + + var errs []error + for i := range ownedBMCUsers.Items { + if err := client.IgnoreNotFound(r.Delete(ctx, &ownedBMCUsers.Items[i])); err != nil { + errs = append(errs, err) + } + } + + // Re-fetch to get current state after deletion + ownedBMCUsers, err = r.getOwnedBMCUsers(ctx, bmcUserSet) + if err != nil { + log.Error(err, "Failed to re-fetch owned BMCUsers after deletion") + return ctrl.Result{}, fmt.Errorf("failed to get owned BMCUsers after deletion: %w", err) + } + + if len(ownedBMCUsers.Items) > 0 { + currentStatus := r.getOwnedBMCUserSetStatus(ownedBMCUsers) + if err := r.updateStatus(ctx, currentStatus, bmcUserSet); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update current BMCUserSet status: %w", err) + } + log.V(1).Info("Waiting on the created BMCUsers to be deleted") + return ctrl.Result{}, errors.Join(errs...) + } + + log.V(1).Info("Ensuring that the finalizer is removed") + if modified, err := clientutils.PatchEnsureNoFinalizer(ctx, r.Client, bmcUserSet, BMCUserSetFinalizer); err != nil || modified { + return ctrl.Result{}, err + } + + log.V(1).Info("BMCUserSet is deleted") + return ctrl.Result{}, errors.Join(errs...) +} + +func (r *BMCUserSetReconciler) getOwnedBMCUsers( + ctx context.Context, + bmcUserSet *metalv1alpha1.BMCUserSet, +) (*metalv1alpha1.BMCUserList, error) { + bmcUserList := &metalv1alpha1.BMCUserList{} + if err := clientutils.ListAndFilterControlledBy(ctx, r.Client, bmcUserSet, bmcUserList); err != nil { + return nil, err + } + return bmcUserList, nil +} + +func (r *BMCUserSetReconciler) getOwnedBMCUserSetStatus( + bmcUserList *metalv1alpha1.BMCUserList, +) *metalv1alpha1.BMCUserSetStatus { + currentStatus := &metalv1alpha1.BMCUserSetStatus{} + currentStatus.AvailableBMCUsers = int32(len(bmcUserList.Items)) + return currentStatus +} + +func (r *BMCUserSetReconciler) updateStatus( + ctx context.Context, + currentStatus *metalv1alpha1.BMCUserSetStatus, + bmcUserSet *metalv1alpha1.BMCUserSet, +) error { + log := ctrl.LoggerFrom(ctx) + bmcUserSetBase := bmcUserSet.DeepCopy() + bmcUserSet.Status = *currentStatus + if err := r.Status().Patch(ctx, bmcUserSet, client.MergeFrom(bmcUserSetBase)); err != nil { + return err + } + log.V(1).Info("Updated BMCUserSet status", "new status", currentStatus) + return nil +} + +func (r *BMCUserSetReconciler) getBMCsBySelector( + ctx context.Context, + bmcUserSet *metalv1alpha1.BMCUserSet, +) (*metalv1alpha1.BMCList, error) { + selector, err := metav1.LabelSelectorAsSelector(&bmcUserSet.Spec.BMCSelector) + if err != nil { + return nil, err + } + + bmcList := &metalv1alpha1.BMCList{} + if err := r.List(ctx, bmcList, client.MatchingLabelsSelector{Selector: selector}); err != nil { + return nil, err + } + return bmcList, nil +} + +func (r *BMCUserSetReconciler) createMissingBMCUsers( + ctx context.Context, + bmcList *metalv1alpha1.BMCList, + bmcUserList *metalv1alpha1.BMCUserList, + bmcUserSet *metalv1alpha1.BMCUserSet, +) error { + log := ctrl.LoggerFrom(ctx) + bmcWithUser := make(map[string]struct{}) + for _, bmcUser := range bmcUserList.Items { + if bmcUser.Spec.BMCRef == nil { + continue + } + bmcWithUser[bmcUser.Spec.BMCRef.Name] = struct{}{} + } + + var errs []error + for _, bmc := range bmcList.Items { + if _, ok := bmcWithUser[bmc.Name]; ok { + continue + } + + newBMCUserName := fmt.Sprintf("%s-%s", bmcUserSet.Name, bmc.Name) + var newBMCUser *metalv1alpha1.BMCUser + if len(newBMCUserName) > utilvalidation.DNS1123SubdomainMaxLength { + log.V(1).Info("BMCUser name is too long, it will be shortened using random string", + "name", newBMCUserName) + newBMCUser = &metalv1alpha1.BMCUser{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: newBMCUserName[:utilvalidation.DNS1123SubdomainMaxLength-10] + "-", + }, + } + } else { + newBMCUser = &metalv1alpha1.BMCUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: newBMCUserName, + }, + } + } + + opResult, err := controllerutil.CreateOrPatch(ctx, r.Client, newBMCUser, func() error { + newBMCUser.Spec.UserName = bmcUserSet.Spec.BMCUserTemplate.UserName + newBMCUser.Spec.RoleID = bmcUserSet.Spec.BMCUserTemplate.RoleID + newBMCUser.Spec.Description = bmcUserSet.Spec.BMCUserTemplate.Description + newBMCUser.Spec.RotationPeriod = nil + if bmcUserSet.Spec.BMCUserTemplate.RotationPeriod != nil { + newBMCUser.Spec.RotationPeriod = bmcUserSet.Spec.BMCUserTemplate.RotationPeriod + } + newBMCUser.Spec.BMCSecretRef = nil + if bmcUserSet.Spec.BMCUserTemplate.BMCSecretRef != nil { + newBMCUser.Spec.BMCSecretRef = &corev1.LocalObjectReference{Name: bmcUserSet.Spec.BMCUserTemplate.BMCSecretRef.Name} + } + newBMCUser.Spec.BMCRef = &corev1.LocalObjectReference{Name: bmc.Name} + syncBMCUserAnnotationsFromSet(newBMCUser, bmcUserSet) + return controllerutil.SetControllerReference(bmcUserSet, newBMCUser, r.Scheme) + }) + if err != nil { + errs = append(errs, err) + } else { + log.V(1).Info("Created BMCUser", "BMCUser", newBMCUser.Name, "bmc ref", bmc.Name, "operation", opResult) + } + } + return errors.Join(errs...) +} + +func (r *BMCUserSetReconciler) deleteOrphanedBMCUsers( + ctx context.Context, + bmcList *metalv1alpha1.BMCList, + bmcUserList *metalv1alpha1.BMCUserList, +) error { + log := ctrl.LoggerFrom(ctx) + bmcMap := make(map[string]struct{}) + for _, bmc := range bmcList.Items { + bmcMap[bmc.Name] = struct{}{} + } + + var errs []error + for i := range bmcUserList.Items { + bmcUser := &bmcUserList.Items[i] + if bmcUser.Spec.BMCRef == nil { + continue + } + if _, ok := bmcMap[bmcUser.Spec.BMCRef.Name]; !ok { + if err := client.IgnoreNotFound(r.Delete(ctx, bmcUser)); err != nil { + errs = append(errs, err) + } else { + log.V(1).Info("Deleted orphaned BMCUser", "BMCUser", bmcUser.Name) + } + } + } + + return errors.Join(errs...) +} + +func (r *BMCUserSetReconciler) patchBMCUsersFromTemplate( + ctx context.Context, + bmcUserSet *metalv1alpha1.BMCUserSet, + bmcUserList *metalv1alpha1.BMCUserList, +) error { + log := ctrl.LoggerFrom(ctx) + if len(bmcUserList.Items) == 0 { + log.V(1).Info("No BMCUsers found, skipping spec template update") + return nil + } + + var errs []error + for i := range bmcUserList.Items { + bmcUser := &bmcUserList.Items[i] + if !bmcUser.DeletionTimestamp.IsZero() { + continue + } + opResult, err := controllerutil.CreateOrPatch(ctx, r.Client, bmcUser, func() error { + bmcUser.Spec.UserName = bmcUserSet.Spec.BMCUserTemplate.UserName + bmcUser.Spec.RoleID = bmcUserSet.Spec.BMCUserTemplate.RoleID + bmcUser.Spec.Description = bmcUserSet.Spec.BMCUserTemplate.Description + bmcUser.Spec.RotationPeriod = nil + if bmcUserSet.Spec.BMCUserTemplate.RotationPeriod != nil { + bmcUser.Spec.RotationPeriod = &metav1.Duration{Duration: bmcUserSet.Spec.BMCUserTemplate.RotationPeriod.Duration} + } + bmcUser.Spec.BMCSecretRef = nil + if bmcUserSet.Spec.BMCUserTemplate.BMCSecretRef != nil { + bmcUser.Spec.BMCSecretRef = &corev1.LocalObjectReference{Name: bmcUserSet.Spec.BMCUserTemplate.BMCSecretRef.Name} + } + syncBMCUserAnnotationsFromSet(bmcUser, bmcUserSet) + return nil + }) + if err != nil { + errs = append(errs, err) + } + if opResult != controllerutil.OperationResultNone { + log.V(1).Info("Patched BMCUser with updated spec", "BMCUser", bmcUser.Name, "operation", opResult) + } + } + return errors.Join(errs...) +} + +func shouldFilterAnnotation(key string) bool { + if key == metalv1alpha1.OperationAnnotation { + return true + } + if strings.HasPrefix(key, "kubernetes.io/") || + strings.HasPrefix(key, "k8s.io/") || + strings.HasPrefix(key, "kubectl.kubernetes.io/") { + return true + } + return false +} + +func syncBMCUserAnnotationsFromSet(bmcUser *metalv1alpha1.BMCUser, bmcUserSet *metalv1alpha1.BMCUserSet) { + // Get current annotations + annotations := bmcUser.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + + // Build desired annotation set from BMCUserSet (filtered) + desired := map[string]string{} + for key, value := range bmcUserSet.GetAnnotations() { + if shouldFilterAnnotation(key) { + continue + } + desired[key] = value + } + + // Remove annotations that are no longer in the source + for key := range annotations { + if shouldFilterAnnotation(key) { + // Don't touch system annotations + continue + } + if _, exists := desired[key]; !exists { + delete(annotations, key) + } + } + + // Add/update annotations from source + maps.Copy(annotations, desired) + + bmcUser.SetAnnotations(annotations) +} + +func (r *BMCUserSetReconciler) enqueueByBMC( + ctx context.Context, + obj client.Object, +) []ctrl.Request { + log := ctrl.LoggerFrom(ctx) + + bmc := obj.(*metalv1alpha1.BMC) + bmcUserSetList := &metalv1alpha1.BMCUserSetList{} + + if err := r.List(ctx, bmcUserSetList); err != nil { + log.Error(err, "Failed to list BMCUserSet") + return nil + } + var reqs []ctrl.Request + for _, bmcUserSet := range bmcUserSetList.Items { + selector, err := metav1.LabelSelectorAsSelector(&bmcUserSet.Spec.BMCSelector) + if err != nil { + log.Error(err, "Failed to parse BMCSelector", "BMCUserSet", bmcUserSet.Name) + continue + } + if selector.Matches(labels.Set(bmc.GetLabels())) { + reqs = append(reqs, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: bmcUserSet.Name, + Namespace: bmcUserSet.Namespace, + }, + }) + } else { + ownedBMCUsers, err := r.getOwnedBMCUsers(ctx, &bmcUserSet) + if err != nil { + log.Error(err, "Failed to list owned BMCUsers") + continue + } + for _, bmcUser := range ownedBMCUsers.Items { + if bmcUser.Spec.BMCRef != nil && bmcUser.Spec.BMCRef.Name == bmc.Name { + reqs = append(reqs, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: bmcUserSet.Name, + Namespace: bmcUserSet.Namespace, + }, + }) + } + } + } + } + return reqs +} + +// SetupWithManager sets up the controller with the Manager. +func (r *BMCUserSetReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&metalv1alpha1.BMCUserSet{}). + Owns(&metalv1alpha1.BMCUser{}). + Watches( + &metalv1alpha1.BMC{}, + handler.EnqueueRequestsFromMapFunc(r.enqueueByBMC), + builder.WithPredicates(predicate.LabelChangedPredicate{})). + Named("bmcuserset"). + Complete(r) +} diff --git a/internal/controller/bmcuserset_controller_test.go b/internal/controller/bmcuserset_controller_test.go new file mode 100644 index 000000000..f8f33c675 --- /dev/null +++ b/internal/controller/bmcuserset_controller_test.go @@ -0,0 +1,679 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" + + metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1" + "github.com/ironcore-dev/metal-operator/internal/bmcutils" +) + +var _ = Describe("BMCUserSet Controller", func() { + _ = SetupTest(nil) + + var ( + bmc1 *metalv1alpha1.BMC + bmc2 *metalv1alpha1.BMC + bmcOther *metalv1alpha1.BMC + ) + + AfterEach(func(ctx SpecContext) { + EnsureCleanState() + }) + + It("Should create BMCUsers for matching BMCs and update status", func(ctx SpecContext) { + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-bmc-secret-", + }, + Data: map[string][]byte{ + metalv1alpha1.BMCSecretUsernameKeyName: []byte("foo"), + metalv1alpha1.BMCSecretPasswordKeyName: []byte("bar"), + }, + } + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + + selectorLabels := map[string]string{ + "role": "admin", + } + + By("Creating BMCs that match the selector") + bmc1 = &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bmc-1", + Labels: selectorLabels, + }, + Spec: metalv1alpha1.BMCSpec{ + Endpoint: &metalv1alpha1.InlineEndpoint{ + IP: metalv1alpha1.MustParseIP(MockServerIP), + MACAddress: "23:11:8A:33:CF:EA", + }, + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: MockServerPort, + }, + BMCSecretRef: corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, bmc1)).To(Succeed()) + + bmc2 = &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bmc-2", + Labels: selectorLabels, + }, + Spec: metalv1alpha1.BMCSpec{ + Endpoint: &metalv1alpha1.InlineEndpoint{ + IP: metalv1alpha1.MustParseIP(MockServerIP), + MACAddress: "23:11:8A:33:CF:EB", + }, + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: MockServerPort, + }, + BMCSecretRef: corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, bmc2)).To(Succeed()) + + By("Creating a non-matching BMC") + bmcOther = &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bmc-other", + Labels: map[string]string{ + "role": "reader", + }, + }, + Spec: metalv1alpha1.BMCSpec{ + Endpoint: &metalv1alpha1.InlineEndpoint{ + IP: metalv1alpha1.MustParseIP(MockServerIP), + MACAddress: "23:11:8A:33:CF:EC", + }, + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: MockServerPort, + }, + BMCSecretRef: corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, bmcOther)).To(Succeed()) + + By("Creating a BMCUserSet") + bmcUserSet := &metalv1alpha1.BMCUserSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-bmcuserset-", + Annotations: map[string]string{ + "example.com/propagated-annotation": "from-bmcuserset", + }, + }, + Spec: metalv1alpha1.BMCUserSetSpec{ + BMCSelector: metav1.LabelSelector{ + MatchLabels: selectorLabels, + }, + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "metal-operator", + RoleID: "Administrator", + Description: "managed by bmcuserset", + BMCSecretRef: &corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, bmcUserSet)).To(Succeed()) + + By("Ensuring BMCUsers are created for matching BMCs") + Eventually(func() []metalv1alpha1.BMCUser { + list := &metalv1alpha1.BMCUserList{} + Expect(k8sClient.List(ctx, list)).To(Succeed()) + return list.Items + }).Should(ConsistOf( + SatisfyAll( + HaveField("Spec.BMCRef.Name", bmc1.Name), + HaveField("Spec.UserName", "metal-operator"), + HaveField("Spec.RoleID", "Administrator"), + HaveField("Spec.Description", "managed by bmcuserset"), + HaveField("Spec.BMCSecretRef.Name", bmcSecret.Name), + HaveField("Annotations", HaveKeyWithValue("example.com/propagated-annotation", "from-bmcuserset")), + ), + SatisfyAll( + HaveField("Spec.BMCRef.Name", bmc2.Name), + HaveField("Spec.UserName", "metal-operator"), + HaveField("Spec.RoleID", "Administrator"), + HaveField("Spec.Description", "managed by bmcuserset"), + HaveField("Spec.BMCSecretRef.Name", bmcSecret.Name), + HaveField("Annotations", HaveKeyWithValue("example.com/propagated-annotation", "from-bmcuserset")), + ), + )) + + By("Ensuring BMCUserSet status is updated") + Eventually(Object(bmcUserSet)).Should(SatisfyAll( + HaveField("Status.FullyLabeledBMCs", int32(2)), + HaveField("Status.AvailableBMCUsers", int32(2)), + )) + + Expect(k8sClient.Delete(ctx, bmcUserSet)).To(Succeed()) + Expect(k8sClient.Delete(ctx, bmc1)).To(Succeed()) + Expect(k8sClient.Delete(ctx, bmc2)).To(Succeed()) + Expect(k8sClient.Delete(ctx, bmcOther)).To(Succeed()) + + server1 := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmc1), + }, + } + server2 := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmc2), + }, + } + server3 := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmcOther), + }, + } + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, server1))).To(Succeed()) + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, server2))).To(Succeed()) + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, server3))).To(Succeed()) + Eventually(ObjectList(&metalv1alpha1.ServerList{})).Should(HaveField("Items", HaveLen(0))) + + bmcUserList := &metalv1alpha1.BMCUserList{} + Expect(k8sClient.List(ctx, bmcUserList)).To(Succeed()) + for i := range bmcUserList.Items { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, &bmcUserList.Items[i]))).To(Succeed()) + } + Eventually(ObjectList(&metalv1alpha1.BMCUserList{})).Should(HaveField("Items", HaveLen(0))) + + secretList := &metalv1alpha1.BMCSecretList{} + Expect(k8sClient.List(ctx, secretList)).To(Succeed()) + for i := range secretList.Items { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, &secretList.Items[i]))).To(Succeed()) + } + + }) + + It("Should update existing BMCUsers when template changes", func(ctx SpecContext) { + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "bmc-secret-", + }, + Data: map[string][]byte{ + metalv1alpha1.BMCSecretUsernameKeyName: []byte("foo"), + metalv1alpha1.BMCSecretPasswordKeyName: []byte("bar"), + }, + } + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + + By("Creating a BMC") + bmc1 = &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bmc-update", + Labels: map[string]string{ + "role": "admin", + }, + }, + Spec: metalv1alpha1.BMCSpec{ + Endpoint: &metalv1alpha1.InlineEndpoint{ + IP: metalv1alpha1.MustParseIP(MockServerIP), + MACAddress: "23:11:8A:33:CF:ED", + }, + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: MockServerPort, + }, + BMCSecretRef: corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, bmc1)).To(Succeed()) + + By("Creating a BMCUserSet") + bmcUserSet := &metalv1alpha1.BMCUserSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-bmcuserset-", + }, + Spec: metalv1alpha1.BMCUserSetSpec{ + BMCSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"role": "admin"}, + }, + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "metal-operator", + RoleID: "Administrator", + Description: "managed by bmcuserset", + }, + }, + } + Expect(k8sClient.Create(ctx, bmcUserSet)).To(Succeed()) + + By("Ensuring BMCUser is created") + bmcUserList := &metalv1alpha1.BMCUserList{} + Eventually(ObjectList(bmcUserList)).Should(HaveField("Items", HaveLen(1))) + + By("Updating the BMCUserSet template") + Eventually(Update(bmcUserSet, func() { + bmcUserSet.Spec.BMCUserTemplate.Description = "updated description" + })).Should(Succeed()) + + By("Ensuring the BMCUser is patched from the updated template") + Eventually(ObjectList(bmcUserList)).Should(SatisfyAll( + HaveField("Items", HaveLen(1)), + HaveField("Items", ContainElement( + HaveField("Spec.Description", "updated description"), + )), + )) + Expect(k8sClient.Delete(ctx, bmcUserSet)).To(Succeed()) + Expect(k8sClient.Delete(ctx, bmc1)).To(Succeed()) + + server1 := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmc1), + }, + } + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, server1))).To(Succeed()) + Eventually(ObjectList(&metalv1alpha1.ServerList{})).Should(HaveField("Items", HaveLen(0))) + + secretList := &metalv1alpha1.BMCSecretList{} + Expect(k8sClient.List(ctx, secretList)).To(Succeed()) + for i := range secretList.Items { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, &secretList.Items[i]))).To(Succeed()) + } + }) + + It("Should remove annotations from BMCUsers when removed from BMCUserSet", func(ctx SpecContext) { + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "bmc-secret-", + }, + Data: map[string][]byte{ + metalv1alpha1.BMCSecretUsernameKeyName: []byte("foo"), + metalv1alpha1.BMCSecretPasswordKeyName: []byte("bar"), + }, + } + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + + By("Creating a BMC") + bmc := &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bmc-annotation-removal", + Labels: map[string]string{ + "test": "annotation-removal", + }, + }, + Spec: metalv1alpha1.BMCSpec{ + Endpoint: &metalv1alpha1.InlineEndpoint{ + IP: metalv1alpha1.MustParseIP(MockServerIP), + MACAddress: "23:11:8A:33:CF:EE", + }, + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: MockServerPort, + }, + BMCSecretRef: corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + + By("Creating a BMCUserSet with annotations") + bmcUserSet := &metalv1alpha1.BMCUserSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-bmcuserset-", + Annotations: map[string]string{ + "example.com/test-annotation": "test-value", + "example.com/will-be-removed": "remove-me", + }, + }, + Spec: metalv1alpha1.BMCUserSetSpec{ + BMCSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"test": "annotation-removal"}, + }, + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "test-user", + RoleID: "Administrator", + }, + }, + } + Expect(k8sClient.Create(ctx, bmcUserSet)).To(Succeed()) + + By("Ensuring BMCUser is created with annotations") + bmcUserList := &metalv1alpha1.BMCUserList{} + Eventually(ObjectList(bmcUserList)).Should(SatisfyAll( + HaveField("Items", HaveLen(1)), + HaveField("Items", ContainElement(SatisfyAll( + HaveField("Annotations", HaveKeyWithValue("example.com/test-annotation", "test-value")), + HaveField("Annotations", HaveKeyWithValue("example.com/will-be-removed", "remove-me")), + ))), + )) + + By("Removing one annotation from BMCUserSet") + Eventually(Update(bmcUserSet, func() { + annotations := bmcUserSet.GetAnnotations() + delete(annotations, "example.com/will-be-removed") + bmcUserSet.SetAnnotations(annotations) + })).Should(Succeed()) + + By("Ensuring the annotation is removed from BMCUser") + Eventually(ObjectList(bmcUserList)).Should(SatisfyAll( + HaveField("Items", HaveLen(1)), + HaveField("Items", ContainElement(SatisfyAll( + HaveField("Annotations", HaveKeyWithValue("example.com/test-annotation", "test-value")), + HaveField("Annotations", Not(HaveKey("example.com/will-be-removed"))), + ))), + )) + + Expect(k8sClient.Delete(ctx, bmcUserSet)).To(Succeed()) + Expect(k8sClient.Delete(ctx, bmc)).To(Succeed()) + + server := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmc), + }, + } + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, server))).To(Succeed()) + Eventually(ObjectList(&metalv1alpha1.ServerList{})).Should(HaveField("Items", HaveLen(0))) + + Expect(k8sClient.List(ctx, bmcUserList)).To(Succeed()) + for i := range bmcUserList.Items { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, &bmcUserList.Items[i]))).To(Succeed()) + } + Eventually(ObjectList(&metalv1alpha1.BMCUserList{})).Should(HaveField("Items", HaveLen(0))) + + secretList := &metalv1alpha1.BMCSecretList{} + Expect(k8sClient.List(ctx, secretList)).To(Succeed()) + for i := range secretList.Items { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, &secretList.Items[i]))).To(Succeed()) + } + }) + + It("Should filter out system annotations during propagation", func(ctx SpecContext) { + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "bmc-secret-", + }, + Data: map[string][]byte{ + metalv1alpha1.BMCSecretUsernameKeyName: []byte("foo"), + metalv1alpha1.BMCSecretPasswordKeyName: []byte("bar"), + }, + } + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + + By("Creating a BMC") + bmc := &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bmc-annotation-filter", + Labels: map[string]string{ + "test": "annotation-filter", + }, + }, + Spec: metalv1alpha1.BMCSpec{ + Endpoint: &metalv1alpha1.InlineEndpoint{ + IP: metalv1alpha1.MustParseIP(MockServerIP), + MACAddress: "23:11:8A:33:CF:EF", + }, + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: MockServerPort, + }, + BMCSecretRef: corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + + By("Creating a BMCUserSet with system and user annotations") + bmcUserSet := &metalv1alpha1.BMCUserSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-bmcuserset-", + Annotations: map[string]string{ + "kubernetes.io/should-not-propagate": "system-value", + "k8s.io/also-should-not-propagate": "k8s-value", + "kubectl.kubernetes.io/third-system-prefix": "kubectl-value", + "example.com/user-annotation": "user-value", + }, + }, + Spec: metalv1alpha1.BMCUserSetSpec{ + BMCSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"test": "annotation-filter"}, + }, + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "test-user", + RoleID: "Administrator", + }, + }, + } + Expect(k8sClient.Create(ctx, bmcUserSet)).To(Succeed()) + + By("Ensuring BMCUser is created with only user annotations") + bmcUserList := &metalv1alpha1.BMCUserList{} + Eventually(ObjectList(bmcUserList)).Should(SatisfyAll( + HaveField("Items", HaveLen(1)), + HaveField("Items", ContainElement(SatisfyAll( + HaveField("Annotations", HaveKeyWithValue("example.com/user-annotation", "user-value")), + HaveField("Annotations", Not(HaveKey("kubernetes.io/should-not-propagate"))), + HaveField("Annotations", Not(HaveKey("k8s.io/also-should-not-propagate"))), + HaveField("Annotations", Not(HaveKey("kubectl.kubernetes.io/third-system-prefix"))), + ))), + )) + + Expect(k8sClient.Delete(ctx, bmcUserSet)).To(Succeed()) + Expect(k8sClient.Delete(ctx, bmc)).To(Succeed()) + + server := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmc), + }, + } + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, server))).To(Succeed()) + Eventually(ObjectList(&metalv1alpha1.ServerList{})).Should(HaveField("Items", HaveLen(0))) + + Expect(k8sClient.List(ctx, bmcUserList)).To(Succeed()) + for i := range bmcUserList.Items { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, &bmcUserList.Items[i]))).To(Succeed()) + } + Eventually(ObjectList(&metalv1alpha1.BMCUserList{})).Should(HaveField("Items", HaveLen(0))) + + secretList := &metalv1alpha1.BMCSecretList{} + Expect(k8sClient.List(ctx, secretList)).To(Succeed()) + for i := range secretList.Items { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, &secretList.Items[i]))).To(Succeed()) + } + }) + + It("Should filter out OperationAnnotation during propagation", func(ctx SpecContext) { + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "bmc-secret-", + }, + Data: map[string][]byte{ + metalv1alpha1.BMCSecretUsernameKeyName: []byte("foo"), + metalv1alpha1.BMCSecretPasswordKeyName: []byte("bar"), + }, + } + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + + By("Creating a BMC") + bmc := &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bmc-operation-annotation", + Labels: map[string]string{ + "test": "operation-annotation", + }, + }, + Spec: metalv1alpha1.BMCSpec{ + Endpoint: &metalv1alpha1.InlineEndpoint{ + IP: metalv1alpha1.MustParseIP(MockServerIP), + MACAddress: "23:11:8A:33:CF:F0", + }, + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: MockServerPort, + }, + BMCSecretRef: corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + + By("Creating a BMCUserSet with OperationAnnotation") + bmcUserSet := &metalv1alpha1.BMCUserSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-bmcuserset-", + Annotations: map[string]string{ + metalv1alpha1.OperationAnnotation: "Restart", + "example.com/user-annotation": "should-propagate", + }, + }, + Spec: metalv1alpha1.BMCUserSetSpec{ + BMCSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"test": "operation-annotation"}, + }, + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "test-user", + RoleID: "Administrator", + }, + }, + } + Expect(k8sClient.Create(ctx, bmcUserSet)).To(Succeed()) + + By("Ensuring BMCUser is created without OperationAnnotation") + bmcUserList := &metalv1alpha1.BMCUserList{} + Eventually(ObjectList(bmcUserList)).Should(SatisfyAll( + HaveField("Items", HaveLen(1)), + HaveField("Items", ContainElement(SatisfyAll( + HaveField("Annotations", HaveKeyWithValue("example.com/user-annotation", "should-propagate")), + HaveField("Annotations", Not(HaveKey(metalv1alpha1.OperationAnnotation))), + ))), + )) + + Expect(k8sClient.Delete(ctx, bmcUserSet)).To(Succeed()) + Expect(k8sClient.Delete(ctx, bmc)).To(Succeed()) + + server := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmc), + }, + } + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, server))).To(Succeed()) + Eventually(ObjectList(&metalv1alpha1.ServerList{})).Should(HaveField("Items", HaveLen(0))) + + Expect(k8sClient.List(ctx, bmcUserList)).To(Succeed()) + for i := range bmcUserList.Items { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, &bmcUserList.Items[i]))).To(Succeed()) + } + Eventually(ObjectList(&metalv1alpha1.BMCUserList{})).Should(HaveField("Items", HaveLen(0))) + + secretList := &metalv1alpha1.BMCSecretList{} + Expect(k8sClient.List(ctx, secretList)).To(Succeed()) + for i := range secretList.Items { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, &secretList.Items[i]))).To(Succeed()) + } + }) + + It("Should remove finalizer immediately when all owned BMCUsers are deleted", func(ctx SpecContext) { + By("Creating a BMCSecret") + bmcSecret := &metalv1alpha1.BMCSecret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "bmc-secret-", + }, + Data: map[string][]byte{ + metalv1alpha1.BMCSecretUsernameKeyName: []byte("foo"), + metalv1alpha1.BMCSecretPasswordKeyName: []byte("bar"), + }, + } + Expect(k8sClient.Create(ctx, bmcSecret)).To(Succeed()) + + By("Creating BMCs") + bmc := &metalv1alpha1.BMC{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-bmc-finalizer", + Labels: map[string]string{ + "test": "finalizer", + }, + }, + Spec: metalv1alpha1.BMCSpec{ + Endpoint: &metalv1alpha1.InlineEndpoint{ + IP: metalv1alpha1.MustParseIP(MockServerIP), + MACAddress: "23:11:8A:33:CF:F1", + }, + Protocol: metalv1alpha1.Protocol{ + Name: metalv1alpha1.ProtocolRedfishLocal, + Port: MockServerPort, + }, + BMCSecretRef: corev1.LocalObjectReference{ + Name: bmcSecret.Name, + }, + }, + } + Expect(k8sClient.Create(ctx, bmc)).To(Succeed()) + + By("Creating a BMCUserSet") + bmcUserSet := &metalv1alpha1.BMCUserSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "test-bmcuserset-", + }, + Spec: metalv1alpha1.BMCUserSetSpec{ + BMCSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"test": "finalizer"}, + }, + BMCUserTemplate: metalv1alpha1.BMCUserTemplate{ + UserName: "test-user", + RoleID: "Administrator", + }, + }, + } + Expect(k8sClient.Create(ctx, bmcUserSet)).To(Succeed()) + + By("Ensuring BMCUser is created and finalizer is added to BMCUserSet") + bmcUserList := &metalv1alpha1.BMCUserList{} + Eventually(ObjectList(bmcUserList)).Should(HaveField("Items", HaveLen(1))) + Eventually(Object(bmcUserSet)).Should(HaveField("Finalizers", ContainElement(BMCUserSetFinalizer))) + + By("Deleting the BMCUserSet") + Expect(k8sClient.Delete(ctx, bmcUserSet)).To(Succeed()) + + By("Ensuring the finalizer is removed after BMCUsers are deleted") + Eventually(Object(bmcUserSet)).Should(HaveField("DeletionTimestamp", Not(BeNil()))) + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(bmcUserSet), bmcUserSet) + g.Expect(client.IgnoreNotFound(err)).To(Succeed()) + }).Should(Succeed()) + + Expect(k8sClient.Delete(ctx, bmc)).To(Succeed()) + + server := &metalv1alpha1.Server{ + ObjectMeta: metav1.ObjectMeta{ + Name: bmcutils.GetServerNameFromBMCandIndex(0, bmc), + }, + } + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, server))).To(Succeed()) + Eventually(ObjectList(&metalv1alpha1.ServerList{})).Should(HaveField("Items", HaveLen(0))) + + secretList := &metalv1alpha1.BMCSecretList{} + Expect(k8sClient.List(ctx, secretList)).To(Succeed()) + for i := range secretList.Items { + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, &secretList.Items[i]))).To(Succeed()) + } + }) +}) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 981c47642..4642ea4eb 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -311,6 +311,11 @@ func SetupTest(redfishMockServers []netip.AddrPort) *corev1.Namespace { ResyncInterval: 10 * time.Millisecond, }).SetupWithManager(k8sManager)).To(Succeed()) + Expect((&BMCUserSetReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + }).SetupWithManager(k8sManager)).To(Succeed()) + Expect((&BMCVersionSetReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), @@ -389,6 +394,8 @@ func EnsureCleanState() { &metalv1alpha1.EndpointList{}, &metalv1alpha1.BMCList{}, &metalv1alpha1.BMCSecretList{}, + &metalv1alpha1.BMCUserSetList{}, + &metalv1alpha1.BMCUserList{}, &metalv1alpha1.ServerClaimList{}, &metalv1alpha1.BMCSettingsSetList{}, &metalv1alpha1.BMCSettingsList{},