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{},