Skip to content

Add BMCUser type and controller#368

Merged
Nuckal777 merged 5 commits intomainfrom
accounts
Jan 28, 2026
Merged

Add BMCUser type and controller#368
Nuckal777 merged 5 commits intomainfrom
accounts

Conversation

@stefanhipfel
Copy link
Copy Markdown
Contributor

@stefanhipfel stefanhipfel commented Jun 12, 2025

Proposed Changes

Implement User controller which handles the creation of user accounts in a bmc

Summary by CodeRabbit

  • New Features
    • Added BMCUser CRD and controller to manage BMC accounts, roles, credential lifecycle and rotation.
    • Automatic credential rotation with expiration tracking and vendor-aware secure password generation.
  • Security
    • BMCSecret validating webhook (immutability enforcement) and public webhooks configuration.
    • New RBAC roles (admin/editor/viewer) for BMCUser access control.
  • Documentation
    • API reference, samples, and manifests updated for BMCUser.
  • Tests
    • Comprehensive controller and webhook tests added.

✏️ Tip: You can customize this high-level summary in your review settings.

@afritzler afritzler added this to the v0.1 milestone Jun 25, 2025
@stefanhipfel stefanhipfel marked this pull request as ready for review July 9, 2025 08:55
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 10, 2025
@afritzler afritzler removed this from the v0.1.0 milestone Jul 22, 2025
Comment thread internal/controller/bmcuser_controller.go Outdated
Comment thread api/v1alpha1/bmc_types.go Outdated
Comment thread internal/controller/user_controller.go Outdated
Comment thread internal/controller/user_controller.go Outdated
Comment thread internal/controller/bmcuser_controller.go
Comment thread api/v1alpha1/user_types.go Outdated
Comment thread internal/controller/user_controller.go Outdated
Comment thread internal/controller/user_controller.go Outdated
log.V(1).Info("Deleting User", "User", user.Name)
if user.Status.EffectiveBMCSecretRef != nil {
log.V(1).Info("Removing effective BMCSecret reference from User", "User", user.Name)
if err := r.removeEffectiveSecret(ctx, log, user); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this necessary? wont it get deleted as the resource creating it is anyway deleted?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to delete it, we should delete the BMC Secret ref from Spec which we created, along with the Ref in status

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to comment out the delete function call, since we did not decide if the controller should also remove users on CRD deletion.
It kind of makes sense to do it, but what if that user was the metal-operators admin user?! maybe an immutable flag is needed for those kind of users

Comment thread internal/controller/user_controller.go Outdated
Comment thread internal/webhook/v1alpha1/bmcsecret_webhook_test.go Outdated
Comment thread internal/webhook/v1alpha1/bmcsecret_webhook.go Outdated
Comment thread bmc/redfish_local.go Outdated
Copy link
Copy Markdown
Contributor

@Nuckal777 Nuckal777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, PR still needs some cleanup though.

Comment thread bmc/redfish.go
Comment thread bmc/redfish.go Outdated
Comment thread config/rbac/account_editor_role.yaml Outdated
Comment thread config/rbac/account_viewer_role.yaml Outdated
Comment thread config/samples/metal_v1alpha1_account.yaml Outdated
Comment thread dist/chart/templates/rbac/account_editor_role.yaml Outdated
Comment thread dist/chart/templates/rbac/account_viewer_role.yaml Outdated
Comment thread config/samples/metal_v1alpha1_bmcuser.yaml Outdated
Comment thread dist/chart/templates/crd/metal.ironcore.dev_users.yaml Outdated
Comment thread internal/controller/bmcuser_controller.go Outdated
Comment thread internal/controller/bmcuser_controller.go Outdated
@stefanhipfel stefanhipfel requested a review from a team as a code owner December 19, 2025 13:46
Comment thread bmc/redfish.go
}

// Vendor-specific constraints map.
var manufacturerPasswordConfigs = map[Manufacturer]ManufacturerPasswordConfig{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for future improvement. this needs to be changeable (in config map, or a user input spec) as this changes based on some settings and preference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creating an issue for that

Comment thread bmc/redfish_local.go
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

improvement for next PR: we need to move these to our mock server setup.

Comment thread api/v1alpha1/bmcuser_types.go Outdated
@afritzler
Copy link
Copy Markdown
Member

In order to move this PR forward, can you please rebase this PR @stefanhipfel?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 9, 2026

Walkthrough

Adds a cluster-scoped BMCUser CRD, its controller and tests, account-management APIs in BMC implementations (Redfish and local/mock), a BMCSecret validating webhook, CRD/RBAC/samples, docs, and supporting mock data for credential lifecycle and rotation.

Changes

Cohort / File(s) Summary
API Types & Generated
api/v1alpha1/bmcuser_types.go, api/v1alpha1/constants.go, api/v1alpha1/zz_generated.deepcopy.go
Introduces new cluster-scoped BMCUser types (Spec/Status/List), registers them, adds OperationAnnotationRotateCredentials constant, and generated deep-copy methods.
Controller Implementation & Tests
internal/controller/bmcuser_controller.go, internal/controller/bmcuser_controller_test.go, internal/controller/suite_test.go
New BMCUserReconciler with finalizer, credential creation/rotation, effective-secret management, BMC connectivity checks, SetupWithManager wiring, and comprehensive Ginkgo tests; tests register the reconciler in suite.
BMC Interface & Implementations
bmc/bmc.go, bmc/redfish.go, bmc/redfish_local.go, bmc/mockup.go
Extends BMC interface with account management methods; Redfish implementation adds create/update/delete/get accounts and secure password generation; local/mock clients expose account management and auth support.
Mock Data
bmc/mock/server/data/AccountService/index.json, bmc/mock/server/data/AccountService/Accounts/1/index.json
Added MaxPasswordLength and PasswordExpiration fields; minor formatting/newline fixes.
Webhooks & Tests
internal/webhook/v1alpha1/bmcsecret_webhook.go, internal/webhook/v1alpha1/bmcsecret_webhook_test.go, internal/webhook/v1alpha1/webhook_suite_test.go
Adds BMCSecretCustomValidator implementing create/update/delete validation (immutability enforcement) and test coverage; wired into manager setup in webhook suite.
Manager Wiring
cmd/manager/main.go
Wires BMCUserReconciler into manager startup via SetupWithManager.
CRD Manifests & Samples
config/crd/bases/metal.ironcore.dev_bmcusers.yaml, config/crd/kustomization.yaml, config/samples/metal_v1alpha1_bmcuser.yaml, config/samples/kustomization.yaml
Adds BMCUser CRD manifest (openAPIV3 schema), sample BMCUser resource, and kustomization entries.
RBAC
config/rbac/bmcuser_admin_role.yaml, config/rbac/bmcuser_editor_role.yaml, config/rbac/bmcuser_viewer_role.yaml, config/rbac/role.yaml, config/rbac/kustomization.yaml
Adds ClusterRoles for bmcuser admin/editor/viewer, updates manager role to include bmcusers resources, and adjusts RBAC kustomization.
Documentation
docs/api-reference/api.md
Documents BMCUser, BMCUserSpec, and BMCUserStatus in the API reference.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Request
    participant K8s as Kubernetes API
    participant Ctrl as BMCUserReconciler
    participant BMC as BMC (Redfish/Local)
    participant Secret as BMCSecret

    User->>K8s: Create BMCUser
    K8s->>Ctrl: Reconcile(BMCUser)
    Ctrl->>K8s: Get BMCUser, referenced BMC and Secret
    Ctrl->>Ctrl: Ensure finalizer, decide create/rotate credentials
    Ctrl->>BMC: CreateOrUpdateAccount(userName, role, password)
    BMC-->>Ctrl: Account result (ID, expiration)
    Ctrl->>K8s: Create immutable credentials Secret
    K8s-->>Ctrl: Secret created
    Ctrl->>K8s: Patch BMCUser status (ID, lastRotation, passwordExpiration)
    K8s-->>Ctrl: Status updated
Loading
sequenceDiagram
    participant User as User
    participant K8s as Kubernetes API
    participant Webhook as Admission Webhook
    participant Val as BMCSecretCustomValidator

    User->>K8s: Update BMCSecret (Immutable: true)
    K8s->>Webhook: ValidateUpdate
    Webhook->>Val: ValidateUpdate(oldObj, newObj)
    Val->>Val: Compare Data/StringData (DeepEqual)
    alt Data changed and Immutable == true
        Val-->>Webhook: Return error (deny)
        Webhook-->>K8s: Reject update
    else
        Val-->>Webhook: Allow update
        Webhook-->>K8s: Accept update
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

enhancement

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is minimal and incomplete. It provides only a brief statement about implementing a user controller but lacks structured details about the changes, motivations, or scope. Expand the description to follow the template: add bullet points under 'Proposed Changes' detailing the new type, controller, API methods, RBAC, and any other significant additions; optionally reference the related issue number.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add BMCUser type and controller' clearly and specifically summarizes the main change: introducing a new BMCUser API type and its associated Kubernetes controller.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bmc/redfish_local.go (1)

30-53: Avoid leaking the underlying Redfish client on auth failure; also guard against nil Accounts.
If NewRedfishBMCClient succeeds and the credentials don’t match, you return 401 but never log out/close the created client. Also, UnitTestMockUps.Accounts being nil will panic on map access.

Possible fix
 bmc, err := NewRedfishBMCClient(ctx, options)
 if err != nil {
     return nil, err
 }
+if UnitTestMockUps.Accounts == nil {
+    UnitTestMockUps.Accounts = map[string]*redfish.ManagerAccount{}
+}
 if acc, ok := UnitTestMockUps.Accounts[options.Username]; ok {
     if acc.Password == options.Password {
         // authenticated
         return &RedfishLocalBMC{RedfishBMC: bmc}, nil
     }
 }
+// avoid leaking a successfully created client
+bmc.Logout()
 return nil, &gofishCommon.Error{
     HTTPReturnedStatusCode: 401,
 }
🤖 Fix all issues with AI agents
In @api/v1alpha1/bmcuser_types.go:
- Around line 11-24: Add kubebuilder validation markers and omitempty json tags
on BMCUserSpec: add +kubebuilder:validation:Pattern and/or
+kubebuilder:validation:MinLength/MaxLength above UserName; add either
+kubebuilder:validation:Enum (or +kubebuilder:validation:Pattern) above RoleID
to restrict valid role IDs; add a minimum constraint above RotationPeriod (e.g.,
+kubebuilder:validation:Minimum=<seconds> or an appropriate duration validation)
to prevent zero/negative durations; and add `omitempty` to the json tags for
Enabled and BMCControllerUser so false values are not always serialized. Apply
these markers to the BMCUserSpec struct fields UserName, RoleID, RotationPeriod,
Enabled, and BMCControllerUser.
- Around line 26-32: The PasswordExpiration field on BMCUserStatus is currently
a string which prevents type-safe time handling; change its type from string to
*metav1.Time (update the struct definition of BMCUserStatus and any
constructors/serializers/deserializers that reference PasswordExpiration),
update imports to ensure metav1 is available, and update controller/comparison
logic that reads/writes PasswordExpiration to use metav1.Time operations; if you
must keep string for vendor reasons, add a clear comment on
BMCUserStatus.PasswordExpiration explaining the format constraints and add
parsing/validation functions instead.

In @bmc/redfish_local.go:
- Around line 55-62: UnitTestMockUps.Accounts access is not concurrency-safe and
some ops are inefficient/inconsistent: protect reads/writes with a sync.RWMutex
on UnitTestMockUps (use RLock/RUnlock for reads in GetAccounts and RLock when
checking in CreateOrUpdateAccount, Lock/Unlock for mutations), change
CreateOrUpdateAccount to look up the account directly using if a, ok :=
UnitTestMockUps.Accounts[userName] instead of scanning all values, and make
DeleteAccount either honor the passed id (validate that id matches the account
being deleted) or explicitly document/validate that id is ignored in local mock
mode and delete by userName under the same mutex protection.

In @bmc/redfish.go:
- Around line 797-803: The GetAccountService method on RedfishBMC should return
nil when AccountService() fails to follow Go conventions; update the error
branch in RedfishBMC.GetAccountService so that when
r.client.GetService().AccountService() returns an error you return (nil,
fmt.Errorf("failed to get account service: %w", err)) instead of returning the
potentially non-nil service value.
- Around line 740-773: In CreateOrUpdateAccount, fix the incorrect error message
and credential usage: update the fmt.Errorf message returned after
service.CreateAccount(...) to say "failed to create account" instead of "failed
to update account", and confirm ChangePassword is called with the current/old
password for authorization (ensure using the existing session/admin credential
rather than r.options.Password if that is not the current password) in the
CreateOrUpdateAccount function where ChangePassword(user password,
currentPassword) is invoked.

In @config/samples/metal_v1alpha1_bmcuser.yaml:
- Around line 13-16: The bmcSecretRef in this sample uses the wrong resource
name; change the value of bmcSecretRef.name from "bmc-secret-sample" to the
actual resource name "bmcsecret-sample" so it matches the BMCSecret resource
(referenced symbol: bmcSecretRef.name -> bmcsecret-sample) and the sample can be
applied successfully.

In @docs/api-reference/api.md:
- Around line 597-656: The API docs for BMCUser/BMCUserSpec/BMCUserStatus lack
field descriptions; update the tables in the BMCUser section to add clear,
user-facing descriptions for each field: for BMCUserSpec.userName describe
expected format, allowed characters, length limits and uniqueness; roleID list
valid roles and examples (e.g., Administrator, Operator, ReadOnly); description
explain purpose; rotationPeriod explain rotation behavior, valid duration
format/ranges and default policy; bmcSecretRef state when to supply vs
auto-generate, secret key/format requirements; bmcRef indicate required/optional
and operator behavior when unset; enabled explain effect of toggling the user.
For BMCUserStatus, document effectiveBMCSecretRef and how it differs from
spec.bmcSecretRef, lastRotation semantics (timestamp format, UTC),
passwordExpiration exact format and timezone handling (or change to Time if
appropriate), and id format/uniqueness; include example values for userName and
roleID to aid users. Ensure these descriptions appear next to the corresponding
table rows for BMCUserSpec and BMCUserStatus in the existing docs block.

In @internal/controller/bmcuser_controller_test.go:
- Around line 120-135: The test is deleting the effectiveSecret explicitly and
using broad ShouldNot(Succeed()) checks which are flaky because the secret may
be garbage-collected; remove the explicit call to k8sClient.Delete(ctx,
effectiveSecret) and instead assert deletion by polling the API: use
Eventually(Get(effectiveSecret)).Should(Satisfy(apierrors.IsNotFound)); likewise
replace any Eventually(Get(...)).ShouldNot(Succeed()) occurrences (e.g. for user
or other resources) with
Eventually(Get(resource)).Should(Satisfy(apierrors.IsNotFound)) so ownership/GC
races are handled consistently (apply same change to the other affected block at
lines 284-328).

In @internal/controller/bmcuser_controller.go:
- Around line 216-227: handleUpdatedSecretRef currently updates the BMC account
but does not update user.Status.EffectiveBMCSecretRef, which causes repeated
reconciles; after a successful CreateOrUpdateAccount call in
handleUpdatedSecretRef, set user.Status.EffectiveBMCSecretRef =
user.Spec.BMCSecretRef and persist the status (use r.Status().Update(ctx, user)
or equivalent) and handle/return any error from that status update so the status
change is saved and the reconcile loop no longer retriggers the same update.
- Around line 356-372: The bmcConnectionTest currently discards the BMC client
returned by bmcutils.CreateBMCClient and returns without closing it; change the
call to capture the client (e.g., bmcClient, err :=
bmcutils.CreateBMCClient(...)) and ensure you call bmcClient.Logout(ctx) when
the connection test succeeds (either via defer immediately after a successful
CreateBMCClient or by calling Logout before returning true/false), handling any
logout error (log it) but still returning the original success result; keep the
existing 401/403 handling and error returns intact.
- Around line 374-378: In BMCUserReconciler.delete, when user.Spec.BMCRef is nil
the function returns without removing the finalizer; change this so that before
returning you remove the BMCUser finalizer from user.Finalizers (use
controllerutil.RemoveFinalizer or equivalent with the same finalizer identifier
used elsewhere), call r.Client.Update(ctx, user) to persist the change, log the
outcome, and handle/update any update error so the resource can finish deleting;
keep this logic inside the delete method (referencing BMCUserReconciler.delete,
user.Spec.BMCRef, controllerutil.RemoveFinalizer / user.Finalizers and
r.Client.Update).
- Around line 394-397: The current code logs "Removed finalizer for User" even
when PatchEnsureNoFinalizer returned an error; update the control flow around
clientutils.PatchEnsureNoFinalizer(ctx, r.Client, user, BMCUserFinalizer) so you
only call log.Info("Removed finalizer for User", "User", user.Name) when
modified is true and err is nil, and log the error via log.Error(err, "failed to
remove finalizer for User", "User", user.Name) (or return the error) when err !=
nil; keep the same return signature but ensure the success log is not emitted on
error.

In @internal/webhook/v1alpha1/bmcsecret_webhook_test.go:
- Around line 26-36: The test BMCSecret fixture has an invalid Namespace value
and uses awkward slice-index pointer hacks for booleans; set a real namespace
string (e.g., "test-namespace") on the BMCSecret.ObjectMeta.Namespace and
replace the &[]bool{true}[0] pattern by declaring a bool variable (e.g.,
immutableTrue := true) and using &immutableTrue for BMCSecret.Immutable; also
create immutableFalse := false and use &immutableFalse where the test currently
uses &[]bool{false}[0].
🧹 Nitpick comments (14)
bmc/mock/server/server.go (1)

160-175: Minor inconsistency in log output and potential mock behavior consideration.

  1. The log statement on line 161 is missing the "address", s.addr field that other handlers include (see lines 92, 247).
  2. Returning 404 for resources that exist in the embedded filesystem but not in overrides may be acceptable for mock purposes, but verify this aligns with expected test scenarios where DELETE is called on resources created via POST.
🔧 Suggested fix for log consistency
 func (s *MockServer) handleRedfishDELETE(w http.ResponseWriter, r *http.Request) {
-	s.log.Info("Received request", "method", r.Method, "path", r.URL.Path)
+	s.log.Info("Received request", "method", r.Method, "path", r.URL.Path, "address", s.addr)
bmc/redfish.go (1)

1092-1105: Type mismatch and potential double-percent in vendor config.

  1. Line 1102: The key "default" is a string literal, but the map type is map[Manufacturer]ManufacturerPasswordConfig. Since Manufacturer is a string type alias, this works but is inconsistent. Consider defining a constant like ManufacturerDefault Manufacturer = "default".

  2. Line 1094: The Dell special chars contain %% which results in two % characters in the string. If only a single % is intended, change to %.

🔧 Suggested improvements
+const ManufacturerDefault Manufacturer = "default"
+
 var manufacturerPasswordConfigs = map[Manufacturer]ManufacturerPasswordConfig{
 	ManufacturerDell: {
-		SpecialChars: "!#$%%&()*.?-@[]^_`{}|~+=",
+		SpecialChars: "!#$%&()*.?-@[]^_`{}|~+=",
 	},
 	// ...
-	"default": {
+	ManufacturerDefault: {
 		SpecialChars: "!@#$%&*()_-+=[]{}/?~|",
 	},
 }

And update line 1111:

-		config = manufacturerPasswordConfigs["default"]
+		config = manufacturerPasswordConfigs[ManufacturerDefault]
config/samples/metal_v1alpha1_bmcuser.yaml (1)

18-18: Verify the field name casing is intentional.

The BMCControllerUser field uses PascalCase, which is inconsistent with other fields in the spec that use camelCase (e.g., userName, roleID, bmcSecretRef). Please confirm whether this naming is:

  1. Intentional (perhaps to emphasize it's a special system field), or
  2. Should be changed to bmcControllerUser for consistency with Kubernetes naming conventions.
bmc/mockup.go (1)

6-9: Consider updating gofish to v0.20.1.

The code currently uses github.com/stmcginnis/gofish v0.20.0. Version v0.20.1 is available and no known security vulnerabilities were found in either version. While the update is optional, keeping dependencies current is a good practice.

internal/webhook/v1alpha1/bmcsecret_webhook_test.go (1)

44-46: Remove empty AfterEach or implement teardown logic.

The AfterEach block contains only a TODO comment with no implementation. If no teardown logic is needed beyond the DeferCleanup in BeforeEach (line 39), remove this empty block entirely.

cmd/manager/main.go (1)

416-430: Use consistent controller name in logs (“BMCUser” vs “User”).
Right now the setup error logs "controller", "User", while the reconciler is BMCUserReconciler and the CRD kind is BMCUser—this makes grepping logs harder.

Proposed tweak
- setupLog.Error(err, "unable to create controller", "controller", "User")
+ setupLog.Error(err, "unable to create controller", "controller", "BMCUser")
internal/controller/bmcuser_controller_test.go (2)

66-77: AfterEach cleanup is brittle (may hang / fail when server or objects don’t exist).
Eventually(Get(server)).Should(Succeed()) can hang if the server never got created (e.g., earlier failure). Also, deletes for bmcSecret/bmc can fail if already deleted.


123-125: Make password/RoleID assertions less fragile (exact length + inconsistent casing).

  • Expect(password).To(HaveLen(30)) will break if the generator changes but still respects max length; consider asserting <= 30 (and maybe a minimum / complexity expectation if needed).
  • RoleID strings vary (ReadOnly vs Readonly)—if the controller validates known roles, this can cause inconsistent test outcomes. Prefer a shared constant/enum if available.

Also applies to: 137-241

internal/webhook/v1alpha1/bmcsecret_webhook.go (2)

26-34: Either wire Client: mgr.GetClient() or remove the unused field.
Right now BMCSecretCustomValidator.Client is never set and never used, which is confusing for future extensions.

Proposed wiring
 func SetupBMCSecretWebhookWithManager(mgr ctrl.Manager) error {
   return ctrl.NewWebhookManagedBy(mgr).For(&metalv1alpha1.BMCSecret{}).
-    WithValidator(&BMCSecretCustomValidator{}).
+    WithValidator(&BMCSecretCustomValidator{Client: mgr.GetClient()}).
     Complete()
 }

52-73: Immutability checks: watch out for nil-vs-empty map diffs; consider structured field errors.
reflect.DeepEqual will treat nil and {} as different; if admission decoding/defaulting ever changes representation, this could block “no-op” updates. Also, returning fmt.Errorf(...) works but loses field context; a structured invalid error is nicer for UX.

internal/bmcutils/bmcutils.go (1)

147-188: Replace commented code with a TODO referencing the correct type, and consider a stricter options API.

The commented admin-user block references metalv1alpha1.User, but the codebase only defines metalv1alpha1.BMCUser. Replace the large commented block with a concise TODO pointing to the intended feature and tracking issue, using the correct type name to prevent future confusion.

The opts ...BMCClientOptions variadic pattern (int-based enum) works but is prone to misuse as the codebase scales. Consider migrating to functional options or a small struct for better type safety.

config/crd/bases/metal.ironcore.dev_bmcusers.yaml (1)

84-86: Add format: date-time to passwordExpiration field.
status.passwordExpiration (line 118–119) is a timestamp but lacks the OpenAPI format declaration. status.lastRotation (line 115–117) correctly uses format: date-time; passwordExpiration should match.

Optionally, add pattern validation to spec.rotationPeriod (line 84–86) to enforce duration format, e.g. pattern: "^([0-9]+(ns|us|µs|ms|s|m|h)?)+$". Note: durations do not use an OpenAPI format value, only type: string.

internal/controller/bmcuser_controller.go (2)

239-250: Use Create instead of CreateOrPatch for secrets with GenerateName.

Since the secret uses GenerateName (line 233) and is marked immutable, it will always be a new creation—never a patch. Using CreateOrPatch is misleading here and adds unnecessary complexity. Additionally, consider using ptr.To(true) from k8s.io/utils/ptr for the boolean pointer.

♻️ Proposed refactor
+	"k8s.io/utils/ptr"
 ...
 	secret := &metalv1alpha1.BMCSecret{
 		ObjectMeta: metav1.ObjectMeta{
 			GenerateName: user.Name + "-bmcsecret-",
 		},
 		Data: map[string][]byte{
 			metalv1alpha1.BMCSecretUsernameKeyName: []byte(user.Spec.UserName),
 			metalv1alpha1.BMCSecretPasswordKeyName: []byte(password),
 		},
-		Immutable: &[]bool{true}[0], // Make the secret immutable
+		Immutable: ptr.To(true),
 	}
-	op, err := controllerutil.CreateOrPatch(ctx, r.Client, secret, func() error {
-		if err := controllerutil.SetControllerReference(user, secret, r.Scheme); err != nil {
-			return fmt.Errorf("failed to set controller reference for BMCSecret: %w", err)
-		}
-		return nil
-	})
+	if err := controllerutil.SetControllerReference(user, secret, r.Scheme); err != nil {
+		return fmt.Errorf("failed to set controller reference for BMCSecret: %w", err)
+	}
+	if err := r.Create(ctx, secret); err != nil {
+		return fmt.Errorf("failed to create BMCSecret: %w", err)
+	}
-	if err != nil {
-		return fmt.Errorf("failed to create or patch BMCSecret: %w", err)
-	}
-	log.V(1).Info("BMCSecret created or patched", "BMCSecret", secret.Name, "Operation", op)
+	log.V(1).Info("BMCSecret created", "BMCSecret", secret.Name)

333-340: Redundant nil check for EffectiveBMCSecretRef.

The nil case is already handled and returns at lines 325-331, so the check at line 334 is always true here.

♻️ Minor cleanup
 	effSecret := &metalv1alpha1.BMCSecret{}
-	if user.Status.EffectiveBMCSecretRef != nil {
-		if err := r.Get(ctx, client.ObjectKey{
-			Name: user.Status.EffectiveBMCSecretRef.Name,
-		}, effSecret); err != nil {
-			return fmt.Errorf("failed to get effective BMCSecret %s: %w", user.Status.EffectiveBMCSecretRef.Name, err)
-		}
+	if err := r.Get(ctx, client.ObjectKey{
+		Name: user.Status.EffectiveBMCSecretRef.Name,
+	}, effSecret); err != nil {
+		return fmt.Errorf("failed to get effective BMCSecret %s: %w", user.Status.EffectiveBMCSecretRef.Name, err)
 	}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05698e0 and fd922d2.

⛔ Files ignored due to path filters (6)
  • dist/chart/templates/crd/metal.ironcore.dev_bmcusers.yaml is excluded by !**/dist/**
  • dist/chart/templates/crd/metal.ironcore.dev_users.yaml is excluded by !**/dist/**
  • dist/chart/templates/rbac/bmcuser_admin_role.yaml is excluded by !**/dist/**
  • dist/chart/templates/rbac/bmcuser_editor_role.yaml is excluded by !**/dist/**
  • dist/chart/templates/rbac/bmcuser_viewer_role.yaml is excluded by !**/dist/**
  • dist/chart/templates/rbac/role.yaml is excluded by !**/dist/**
📒 Files selected for processing (31)
  • PROJECT
  • api/v1alpha1/bmc_types.go
  • api/v1alpha1/bmcuser_types.go
  • api/v1alpha1/constants.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • bmc/bmc.go
  • bmc/mock/server/data/AccountService/index.json
  • bmc/mock/server/server.go
  • bmc/mockup.go
  • bmc/redfish.go
  • bmc/redfish_local.go
  • cmd/manager/main.go
  • config/certmanager/issuer.yaml
  • config/crd/bases/metal.ironcore.dev_bmcusers.yaml
  • config/crd/kustomization.yaml
  • config/rbac/bmcuser_admin_role.yaml
  • config/rbac/bmcuser_editor_role.yaml
  • config/rbac/bmcuser_viewer_role.yaml
  • config/rbac/kustomization.yaml
  • config/rbac/role.yaml
  • config/samples/kustomization.yaml
  • config/samples/metal_v1alpha1_bmcuser.yaml
  • docs/api-reference/api.md
  • internal/bmcutils/bmcutils.go
  • internal/controller/bmcuser_controller.go
  • internal/controller/bmcuser_controller_test.go
  • internal/controller/suite_test.go
  • internal/controller/test_helper.go
  • internal/webhook/v1alpha1/bmcsecret_webhook.go
  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
  • internal/webhook/v1alpha1/webhook_suite_test.go
🧰 Additional context used
🧬 Code graph analysis (12)
internal/controller/suite_test.go (2)
internal/controller/bmcuser_controller.go (1)
  • BMCUserReconciler (31-36)
bmc/redfish.go (1)
  • Options (43-53)
internal/webhook/v1alpha1/webhook_suite_test.go (1)
internal/webhook/v1alpha1/bmcsecret_webhook.go (1)
  • SetupBMCSecretWebhookWithManager (26-30)
api/v1alpha1/bmcuser_types.go (1)
api/v1alpha1/groupversion_info.go (1)
  • SchemeBuilder (19-19)
internal/controller/test_helper.go (1)
api/v1alpha1/bmcuser_types.go (1)
  • BMCUserList (50-54)
bmc/redfish_local.go (3)
bmc/mockup.go (1)
  • UnitTestMockUps (193-193)
bmc/redfish.go (1)
  • RedfishBMC (56-59)
bmc/bmc.go (1)
  • Entity (137-142)
internal/bmcutils/bmcutils.go (1)
api/v1alpha1/bmc_types.go (1)
  • Protocol (127-138)
internal/webhook/v1alpha1/bmcsecret_webhook.go (1)
api/v1alpha1/bmcsecret_types.go (1)
  • BMCSecret (24-57)
internal/webhook/v1alpha1/bmcsecret_webhook_test.go (2)
api/v1alpha1/bmcsecret_types.go (1)
  • BMCSecret (24-57)
internal/webhook/v1alpha1/bmcsecret_webhook.go (1)
  • BMCSecretCustomValidator (32-34)
api/v1alpha1/zz_generated.deepcopy.go (1)
api/v1alpha1/bmcuser_types.go (4)
  • BMCUser (39-45)
  • BMCUserList (50-54)
  • BMCUserSpec (12-24)
  • BMCUserStatus (27-32)
cmd/manager/main.go (2)
internal/controller/bmcuser_controller.go (1)
  • BMCUserReconciler (31-36)
bmc/redfish.go (1)
  • Options (43-53)
internal/controller/bmcuser_controller.go (7)
bmc/redfish.go (1)
  • Options (43-53)
api/v1alpha1/bmcuser_types.go (1)
  • BMCUser (39-45)
api/v1alpha1/bmc_types.go (2)
  • BMC (257-263)
  • Protocol (127-138)
internal/bmcutils/bmcutils.go (4)
  • GetBMCCredentialsFromSecret (49-60)
  • GetBMCClientFromBMC (147-188)
  • GetBMCAddressForBMC (93-109)
  • CreateBMCClient (190-229)
api/v1alpha1/constants.go (2)
  • OperationAnnotation (21-21)
  • OperationAnnotationRotateCredentials (45-45)
bmc/bmc.go (1)
  • Manufacturer (19-19)
api/v1alpha1/bmcsecret_types.go (2)
  • BMCSecretUsernameKeyName (13-13)
  • BMCSecretPasswordKeyName (15-15)
bmc/redfish.go (1)
bmc/bmc.go (4)
  • Manufacturer (19-19)
  • ManufacturerDell (22-22)
  • ManufacturerHPE (24-24)
  • ManufacturerLenovo (23-23)
🪛 Checkov (3.2.334)
config/rbac/bmcuser_admin_role.yaml

[medium] 8-27: Minimize wildcard use in Roles and ClusterRoles

(CKV_K8S_49)

🔇 Additional comments (33)
bmc/mock/server/server.go (1)

84-85: LGTM!

The DELETE method routing follows the established pattern for other HTTP methods in the handler.

bmc/mock/server/data/AccountService/index.json (1)

12-12: LGTM!

Adding MaxPasswordLength: 30 provides the constraint needed for password generation validation in the mock AccountService. This aligns with the broader BMCUser controller changes.

PROJECT (2)

32-34: LGTM!

Adding webhook validation support for BMCSecret enables credential validation during create/update operations, which is important for the BMCUser credential management workflow.


148-155: LGTM!

The BMCUser resource is correctly registered as a cluster-scoped resource with controller support, following the pattern of other resources in this project.

api/v1alpha1/constants.go (2)

44-45: LGTM!

The new OperationAnnotationRotateCredentials constant follows the established naming convention and provides a clear mechanism for triggering credential rotation via annotations.


49-61: Old constant names already removed or never existed in codebase.

The old names GracefulShutdownServerPower and GracefulShutdownBMC do not appear anywhere in the repository. The new constants GracefulRestartServerPower and GracefulRestartBMC are actively used and consistently referenced across multiple controller files (biossettings_controller.go, biosversion_controller.go, bmcsettings_controller.go) and in the AnnotationToRedfishMapping. Since this is a v1alpha1 API, breaking changes are expected and acceptable per standard API versioning practices.

api/v1alpha1/zz_generated.deepcopy.go (1)

837-948: LGTM!

This is auto-generated code by controller-gen for the new BMCUser types. The deepcopy implementations correctly handle all pointer fields (RotationPeriod, BMCSecretRef, BMCRef, EffectiveBMCSecretRef, LastRotation) as expected.

bmc/redfish.go (5)

84-89: LGTM!

Enabling ReuseConnections: true improves performance by reusing HTTP connections, which is beneficial for the multiple API calls made during account management operations.


775-795: LGTM!

The dual verification of both userName and id before deletion is a good safety measure to prevent accidental deletion of the wrong account.


805-815: LGTM!

Clean wrapper method following standard patterns.


1107-1161: LGTM - Secure password generation implementation.

The password generation logic correctly:

  • Uses crypto/rand for cryptographic randomness
  • Ensures minimum complexity by including one character from each required category
  • Shuffles the result to avoid predictable character positions
  • Validates minimum length requirements

1163-1181: LGTM!

Helper functions randomChar and shuffleRunes correctly use crypto/rand for secure random number generation with Fisher-Yates shuffle algorithm.

api/v1alpha1/bmc_types.go (1)

23-30: Verify usage of the new PasswordPolicy type.

The PasswordPolicy type and its constants are well-defined with clear documentation. However, ensure this type is referenced in the BMCUserSpec or related API types to be useful in the API surface.

#!/bin/bash
# Verify PasswordPolicy type is used in the API types
rg -n "PasswordPolicy" --type=go -C 2
bmc/mockup.go (1)

6-9: LGTM!

The imports for gofish common and redfish packages are correctly added to support the ManagerAccount functionality.

config/crd/kustomization.yaml (2)

19-19: LGTM!

The BMCUser CRD base resource is correctly added to the kustomization resources list.


41-41: The patch file cainjection_in_accounts.yaml does not exist.

The commented line references a patch that has no implementation in the config/crd/patches/ directory. Since this line is currently commented out, it poses no runtime risk. However, if this patch is intended to be used, it either needs to be created or the reference should be removed.

Note: The original concern about naming inconsistency between the resource (bmcusers) and patch (accounts) is not the issue—both patch files are missing from the directory.

Likely an incorrect or invalid review comment.

internal/controller/suite_test.go (2)

131-131: LGTM!

Adding the Logger to the test manager configuration improves observability during test execution.


264-273: LGTM!

The BMCUserReconciler setup is properly configured for the test environment with appropriate polling intervals and options consistent with other reconcilers in the suite.

config/samples/kustomization.yaml (1)

17-17: LGTM!

The BMCUser sample manifest is correctly added to the kustomization resources.

config/samples/metal_v1alpha1_bmcuser.yaml (1)

1-12: LGTM!

The sample manifest structure and field values are well-formed and provide a good example for BMCUser resource creation. The rotation period of 720 hours (30 days) is a reasonable default.

Also applies to: 17-19

internal/webhook/v1alpha1/webhook_suite_test.go (1)

120-122: LGTM! BMCSecret webhook setup follows the established pattern.

The webhook registration is correctly positioned and includes proper error handling, consistent with other webhook setups in the test suite.

config/rbac/kustomization.yaml (1)

25-27: LGTM! BMCUser RBAC roles added correctly.

The three RBAC role files (admin, editor, viewer) follow the standard scaffolding pattern used for other CRDs in this project.

Note: The AI summary indicates that bmcversionset_* roles are removed, but they remain present at lines 28-30. Only the bmcuser_* roles are being added in this change.

internal/controller/test_helper.go (1)

25-27: LGTM! BMCUser cleanup check follows the established pattern.

The cleanup verification for BMCUserList is correctly implemented and consistent with the other resource cleanup checks in EnsureCleanState.

config/rbac/bmcuser_viewer_role.yaml (1)

1-29: LGTM! BMCUser viewer role is properly configured.

The ClusterRole grants appropriate read-only permissions (get, list, watch) for monitoring and visibility purposes. The role follows Kubernetes RBAC best practices and is consistent with other viewer roles in the project.

config/rbac/bmcuser_editor_role.yaml (1)

1-33: LGTM! BMCUser editor role is properly configured.

The ClusterRole grants appropriate permissions for managing BMCUser resources. The role correctly provides full CRUD operations on bmcusers while limiting status access to read-only, which aligns with Kubernetes controller best practices where status is typically managed by controllers.

config/rbac/role.yaml (1)

50-50: LGTM! BMCUser RBAC permissions are properly configured.

The additions of bmcusers, bmcusers/finalizers, and bmcusers/status are consistent with the existing pattern for other CRDs in this ClusterRole. All necessary permissions for the BMCUser controller are in place.

Also applies to: 77-77, 97-97

config/rbac/bmcuser_admin_role.yaml (1)

1-27: Verify wildcard verb usage is intentional for admin role.

The ClusterRole uses wildcard (*) verbs for the bmcusers resource (line 21). While the comments indicate this is intended for cluster admins who can delegate permissions, consider whether a more restrictive set of verbs would be appropriate:

  • Create, get, list, watch, update, patch, delete are typical for admin roles
  • Wildcard includes future verbs that may be added to the API

The pattern is consistent with Kubernetes admin role conventions, but explicitly listing verbs provides better auditability and security boundaries.

Based on learnings from static analysis (Checkov CKV_K8S_49), wildcard usage in RBAC should be minimized.

🔒 Consider explicit verb listing
 - apiGroups:
   - metal.ironcore.dev
   resources:
   - bmcusers
   verbs:
-  - '*'
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
api/v1alpha1/bmcuser_types.go (1)

34-58: LGTM! BMCUser type definitions follow Kubernetes conventions.

The BMCUser and BMCUserList types are properly structured with:

  • Correct kubebuilder markers for CRD generation (root object, status subresource, cluster scope)
  • Standard TypeMeta, ObjectMeta, Spec, and Status composition
  • Proper scheme registration in init()

The structure aligns with Kubernetes API conventions and other CRDs in this project.

bmc/bmc.go (1)

118-129: All BMC implementations are complete; no breaking changes to resolve. DeleteAccount semantics are clear from usage patterns in the codebase.

All three BMC implementations—RedfishBMC, RedfishLocalBMC, and RedfishKubeBMC—have been updated with the new account management methods. RedfishKubeBMC inherits all methods from RedfishBMC via struct embedding, so it needs no explicit implementations. The DeleteAccount method signature is unambiguous: userName is the user's login name and id is the unique account identifier returned by the BMC, as shown by usage in bmcuser_controller.go where it's called with user.Spec.UserName and user.Status.ID.

internal/controller/bmcuser_controller.go (4)

26-36: LGTM!

The reconciler struct and finalizer constant follow standard controller-runtime patterns.


42-58: LGTM!

The entry point follows the standard controller pattern with proper deletion timestamp handling and client.IgnoreNotFound usage.


402-409: LGTM!

The controller setup correctly watches BMCUser resources and owns BMCSecret resources for proper event triggering.


163-168: Nil pointer dereference when forceRotation is true but RotationPeriod is nil.

If forceRotation is true (due to annotation or expired password) but user.Spec.RotationPeriod is nil, accessing user.Spec.RotationPeriod.Duration on line 163 will cause a panic.

🐛 Proposed fix
-	if user.Status.LastRotation != nil && user.Status.LastRotation.Add(user.Spec.RotationPeriod.Duration).After(metav1.Now().Time) && !forceRotation {
+	if user.Spec.RotationPeriod != nil && user.Status.LastRotation != nil && user.Status.LastRotation.Add(user.Spec.RotationPeriod.Duration).After(metav1.Now().Time) && !forceRotation {
		log.V(1).Info("BMC user password rotation is not needed yet", "User", user.Name)
		return ctrl.Result{
			Requeue:      true,
			RequeueAfter: user.Spec.RotationPeriod.Duration,
		}, nil
	}

Likely an incorrect or invalid review comment.

Comment on lines +11 to +24
// BMCUserSpec defines the desired state of BMCUser.
type BMCUserSpec struct {
UserName string `json:"userName"`
RoleID string `json:"roleID"`
Description string `json:"description,omitempty"`
RotationPeriod *metav1.Duration `json:"rotationPeriod,omitempty"`
// if not set, the operator will generate a secure password based on BMC manufacturer requirements.
BMCSecretRef *v1.LocalObjectReference `json:"bmcSecretRef,omitempty"`
BMCRef *v1.LocalObjectReference `json:"bmcRef,omitempty"`
Enabled bool `json:"enabled"`
// set if the user should be used by the BMC controller to access the system.
// +kubebuilder:default=false
BMCControllerUser bool `json:"bmcControllerUser"`
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add validation markers for BMCUserSpec fields.

The BMCUserSpec lacks validation constraints that would help prevent invalid configurations:

  1. UserName (line 13): No pattern, length constraints, or format validation
  2. RoleID (line 14): No validation for valid role IDs (consider enum or pattern)
  3. RotationPeriod (line 16): No minimum duration validation
  4. Enabled (line 20): Missing omitempty tag - false will always be serialized
  5. BMCControllerUser (line 23): Missing omitempty tag - false will always be serialized
🔧 Proposed validation additions
 type BMCUserSpec struct {
-	UserName       string           `json:"userName"`
-	RoleID         string           `json:"roleID"`
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=64
+	// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9_-]+$`
+	UserName       string           `json:"userName"`
+	// +kubebuilder:validation:MinLength=1
+	RoleID         string           `json:"roleID"`
 	Description    string           `json:"description,omitempty"`
+	// +kubebuilder:validation:Minimum=1
 	RotationPeriod *metav1.Duration `json:"rotationPeriod,omitempty"`
 	// if not set, the operator will generate a secure password based on BMC manufacturer requirements.
 	BMCSecretRef *v1.LocalObjectReference `json:"bmcSecretRef,omitempty"`
 	BMCRef       *v1.LocalObjectReference `json:"bmcRef,omitempty"`
-	Enabled      bool                     `json:"enabled"`
+	Enabled      bool                     `json:"enabled,omitempty"`
 	// set if the user should be used by the BMC controller to access the system.
 	// +kubebuilder:default=false
-	BMCControllerUser bool `json:"bmcControllerUser"`
+	BMCControllerUser bool `json:"bmcControllerUser,omitempty"`
 }

Validation patterns should be confirmed against BMC vendor requirements. If RoleID has a fixed set of values, use an enum validation instead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// BMCUserSpec defines the desired state of BMCUser.
type BMCUserSpec struct {
UserName string `json:"userName"`
RoleID string `json:"roleID"`
Description string `json:"description,omitempty"`
RotationPeriod *metav1.Duration `json:"rotationPeriod,omitempty"`
// if not set, the operator will generate a secure password based on BMC manufacturer requirements.
BMCSecretRef *v1.LocalObjectReference `json:"bmcSecretRef,omitempty"`
BMCRef *v1.LocalObjectReference `json:"bmcRef,omitempty"`
Enabled bool `json:"enabled"`
// set if the user should be used by the BMC controller to access the system.
// +kubebuilder:default=false
BMCControllerUser bool `json:"bmcControllerUser"`
}
// BMCUserSpec defines the desired state of BMCUser.
type BMCUserSpec struct {
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=64
// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9_-]+$`
UserName string `json:"userName"`
// +kubebuilder:validation:MinLength=1
RoleID string `json:"roleID"`
Description string `json:"description,omitempty"`
// +kubebuilder:validation:Minimum=1
RotationPeriod *metav1.Duration `json:"rotationPeriod,omitempty"`
// if not set, the operator will generate a secure password based on BMC manufacturer requirements.
BMCSecretRef *v1.LocalObjectReference `json:"bmcSecretRef,omitempty"`
BMCRef *v1.LocalObjectReference `json:"bmcRef,omitempty"`
Enabled bool `json:"enabled,omitempty"`
// set if the user should be used by the BMC controller to access the system.
// +kubebuilder:default=false
BMCControllerUser bool `json:"bmcControllerUser,omitempty"`
}
🤖 Prompt for AI Agents
In @api/v1alpha1/bmcuser_types.go around lines 11 - 24, Add kubebuilder
validation markers and omitempty json tags on BMCUserSpec: add
+kubebuilder:validation:Pattern and/or
+kubebuilder:validation:MinLength/MaxLength above UserName; add either
+kubebuilder:validation:Enum (or +kubebuilder:validation:Pattern) above RoleID
to restrict valid role IDs; add a minimum constraint above RotationPeriod (e.g.,
+kubebuilder:validation:Minimum=<seconds> or an appropriate duration validation)
to prevent zero/negative durations; and add `omitempty` to the json tags for
Enabled and BMCControllerUser so false values are not always serialized. Apply
these markers to the BMCUserSpec struct fields UserName, RoleID, RotationPeriod,
Enabled, and BMCControllerUser.

Comment thread api/v1alpha1/bmcuser_types.go
Comment thread bmc/redfish_local.go
Comment on lines +55 to +62
// GetAccounts retrieves all user accounts from the BMC.
func (r *RedfishLocalBMC) GetAccounts(ctx context.Context) ([]*redfish.ManagerAccount, error) {
accounts := make([]*redfish.ManagerAccount, 0, len(UnitTestMockUps.Accounts))
for _, a := range UnitTestMockUps.Accounts {
accounts = append(accounts, a)
}
return accounts, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file to see the actual implementation of all three methods
head -100 bmc/redfish_local.go | tail -50

Repository: ironcore-dev/metal-operator

Length of output: 1565


🏁 Script executed:

# Also check the full file structure around the specified lines
wc -l bmc/redfish_local.go

Repository: ironcore-dev/metal-operator

Length of output: 95


🏁 Script executed:

# Let's look at the specific lines mentioned
sed -n '55,96p' bmc/redfish_local.go

Repository: ironcore-dev/metal-operator

Length of output: 1342


🏁 Script executed:

# Check for UnitTestMockUps.Accounts usage patterns
rg -n --type=go 'UnitTestMockUps\.Accounts' -C2

Repository: ironcore-dev/metal-operator

Length of output: 2913


🏁 Script executed:

# Check for concurrent test patterns
rg -n --type=go 'Parallel\(|GOMAXPROCS' -C2

Repository: ironcore-dev/metal-operator

Length of output: 53


🏁 Script executed:

# Check if tests use -race flag in CI configuration
fd -e yml -e yaml | xargs rg -l 'race|go.*test' || echo "No matches"

Repository: ironcore-dev/metal-operator

Length of output: 163


🏁 Script executed:

# Also check the UnitTestMockUps definition to understand the data structure
rg -n 'UnitTestMockUps.*{|var.*UnitTestMockUps|type.*UnitTestMockUps' -A 20 --type=go

Repository: ironcore-dev/metal-operator

Length of output: 232


🏁 Script executed:

# Search for UnitTestMockUps definition
rg -n 'UnitTestMockUps\s*=' --type=go -A 5

Repository: ironcore-dev/metal-operator

Length of output: 844


🏁 Script executed:

# Search for type definition
rg -n 'type.*Mock|var.*Mock' --type=go | head -20

Repository: ironcore-dev/metal-operator

Length of output: 224


🏁 Script executed:

# Check for goroutines in tests
rg -n 'go\s+' bmc/ --type=go -C2

Repository: ironcore-dev/metal-operator

Length of output: 4866


🏁 Script executed:

# Check CI configuration files for race flag
find . -name '.github' -o -name 'Makefile' -o -name '*.yml' -o -name '*.yaml' | head -20

Repository: ironcore-dev/metal-operator

Length of output: 718


🏁 Script executed:

# Check the RedfishMockUps struct definition
sed -n '12,100p' bmc/mockup.go

Repository: ironcore-dev/metal-operator

Length of output: 2291


🏁 Script executed:

# Check if there's a Makefile with test commands
grep -n 'test\|race' Makefile | head -20

Repository: ironcore-dev/metal-operator

Length of output: 1429


🏁 Script executed:

# Check CI workflow for test flags
cat .github/workflows/test.yml | grep -A 5 -B 5 'race\|go test'

Repository: ironcore-dev/metal-operator

Length of output: 53


🏁 Script executed:

# Let's verify the concurrent goroutines access the Accounts field specifically
rg -n 'BIOSUpgradeTaskIndex|PendingBMCSetting|BMCUpgradeTaskIndex' bmc/redfish_local.go -B 5 -A 5

Repository: ironcore-dev/metal-operator

Length of output: 3983


🏁 Script executed:

# Check if these goroutines run during tests
rg -n 'UpdateFirmwareImage|ResetManager|UpgradeBMCFirmware' bmc/redfish_local.go

Repository: ironcore-dev/metal-operator

Length of output: 254


🏁 Script executed:

# Final verification: check if any tests actually call these three account methods concurrently
rg -n 'GetAccounts|CreateOrUpdateAccount|DeleteAccount' internal/controller/ --type=go -B 2 -A 2

Repository: ironcore-dev/metal-operator

Length of output: 3175


Mock account storage isn't concurrency-safe and has efficiency issues.

The three account methods (GetAccounts, CreateOrUpdateAccount, DeleteAccount) mutate/read the shared global UnitTestMockUps.Accounts map without synchronization, making them race-prone if tests or concurrent operations access them.

Additionally:

  • CreateOrUpdateAccount scans all map values unnecessarily; use direct lookup by userName key instead: if a, ok := UnitTestMockUps.Accounts[userName]
  • DeleteAccount accepts an id parameter but ignores it, using only userName for deletion. This creates API inconsistency since the controller passes user.Status.ID (line 391 in bmcuser_controller.go); consider validating or documenting that id is unused in local mock mode.
🤖 Prompt for AI Agents
In @bmc/redfish_local.go around lines 55 - 62, UnitTestMockUps.Accounts access
is not concurrency-safe and some ops are inefficient/inconsistent: protect
reads/writes with a sync.RWMutex on UnitTestMockUps (use RLock/RUnlock for reads
in GetAccounts and RLock when checking in CreateOrUpdateAccount, Lock/Unlock for
mutations), change CreateOrUpdateAccount to look up the account directly using
if a, ok := UnitTestMockUps.Accounts[userName] instead of scanning all values,
and make DeleteAccount either honor the passed id (validate that id matches the
account being deleted) or explicitly document/validate that id is ignored in
local mock mode and delete by userName under the same mutex protection.

Comment thread bmc/redfish.go
Comment thread bmc/redfish.go Outdated
Comment thread internal/controller/bmcuser_controller.go
Comment on lines +216 to +227
func (r *BMCUserReconciler) handleUpdatedSecretRef(ctx context.Context, log logr.Logger, user *metalv1alpha1.BMCUser, bmcSecret *metalv1alpha1.BMCSecret, bmcClient bmc.BMC) error {
log.Info("BMCSecret credentials have changed, updating BMC user", "User", user.Name)
_, password, err := bmcutils.GetBMCCredentialsFromSecret(bmcSecret)
if err != nil {
return fmt.Errorf("failed to get credentials from BMCSecret: %w", err)
}
// Update the BMC account with the new password
if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, password, r.Insecure); err != nil {
return fmt.Errorf("failed to create or update BMC account with new password: %w", err)
}
return nil
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

EffectiveBMCSecretRef not updated after handling secret reference change.

After successfully updating the BMC account with the new password, user.Status.EffectiveBMCSecretRef is not updated to match user.Spec.BMCSecretRef. This causes the condition at line 112 to trigger on every subsequent reconcile, repeatedly calling CreateOrUpdateAccount.

🐛 Proposed fix
 func (r *BMCUserReconciler) handleUpdatedSecretRef(ctx context.Context, log logr.Logger, user *metalv1alpha1.BMCUser, bmcSecret *metalv1alpha1.BMCSecret, bmcClient bmc.BMC) error {
 	log.Info("BMCSecret credentials have changed, updating BMC user", "User", user.Name)
 	_, password, err := bmcutils.GetBMCCredentialsFromSecret(bmcSecret)
 	if err != nil {
 		return fmt.Errorf("failed to get credentials from BMCSecret: %w", err)
 	}
 	// Update the BMC account with the new password
 	if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, password, r.Insecure); err != nil {
 		return fmt.Errorf("failed to create or update BMC account with new password: %w", err)
 	}
+	// Update the effective secret reference to match the new secret
+	if err := r.setEffectiveSecretRef(ctx, log, user, bmcSecret); err != nil {
+		return fmt.Errorf("failed to update effective BMCSecretRef: %w", err)
+	}
 	return nil
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @internal/controller/bmcuser_controller.go around lines 216 - 227,
handleUpdatedSecretRef currently updates the BMC account but does not update
user.Status.EffectiveBMCSecretRef, which causes repeated reconciles; after a
successful CreateOrUpdateAccount call in handleUpdatedSecretRef, set
user.Status.EffectiveBMCSecretRef = user.Spec.BMCSecretRef and persist the
status (use r.Status().Update(ctx, user) or equivalent) and handle/return any
error from that status update so the status change is saved and the reconcile
loop no longer retriggers the same update.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updating the EffectiveBMCSecretRef, happens only at the beginning of a reconcile loop, when password was changed and when the new user/password have been tested.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stefanhipfel, thank you for the clarification! I understand now—the updateEffectiveSecret method at the beginning of the reconcile loop is responsible for testing the new credentials and updating EffectiveBMCSecretRef after validation, while handleUpdatedSecretRef focuses solely on updating the BMC account itself. This separation of concerns makes sense, and the reconcile loop naturally coordinates the status update and BMC account update across iterations. I appreciate you explaining the design.


🧠 Learnings used
Learnt from: Nuckal777
Repo: ironcore-dev/metal-operator PR: 471
File: internal/controller/bmcsettingsset_controller.go:233-236
Timestamp: 2026-01-14T19:27:23.322Z
Learning: In the ironcore-dev/metal-operator repository, BMCSettings.Spec.BMCRef is marked with +required kubebuilder marker, ensuring API-level validation that prevents nil values; defensive nil checks are not needed when iterating BMCSettings from List operations.

Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Flag non-idempotent reconcile logic, Spec mutations during reconciliation, and custom patterns duplicating standard Kubernetes behavior

Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Reconcile logic must be idempotent and safe to re-run at any time without side effects

Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Prevent hot loops caused by status-only changes triggering reconciliation

Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Reconciliation must be based on current cluster state, not assumptions; handle NotFound errors correctly and gracefully manage partial failures

Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Use `client.Client`, `controllerutil.CreateOrUpdate`, and OwnerReferences from controller-runtime rather than direct API calls

Comment thread internal/controller/bmcuser_controller.go Outdated
Comment thread internal/controller/bmcuser_controller.go
Comment thread internal/webhook/v1alpha1/bmcsecret_webhook_test.go
Copy link
Copy Markdown
Contributor

@Nuckal777 Nuckal777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, for following up on this topic. Unfortunately, this PR needs another cleanup.

Comment thread internal/bmcutils/bmcutils.go Outdated
Comment thread internal/controller/bmcuser_controller.go Outdated
Comment thread internal/controller/bmcuser_controller.go Outdated
Comment thread internal/controller/bmcuser_controller.go Outdated
Comment thread internal/webhook/v1alpha1/bmcsecret_webhook_test.go
Comment thread internal/webhook/v1alpha1/bmcsecret_webhook_test.go Outdated
Comment thread internal/controller/bmcuser_controller.go
Comment thread api/v1alpha1/bmcuser_types.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @internal/webhook/v1alpha1/bmcsecret_webhook_test.go:
- Line 38: The test descriptions use incorrect article "an" before the
initialism BMC; update the string literals in the test cases (e.g., the
By("Creating an BMCSecret") occurrences in bmcsecret_webhook_test.go, including
the other occurrence at the same file) to use "a" instead (By("Creating a
BMCSecret")) so the grammar matches the consonant sound of "BMC".
- Around line 27-31: The Namespace field on the BMCSecret ObjectMeta is set to
the literal string "ns.Name" which is invalid; update the metav1.ObjectMeta for
BMCSecret to use a valid namespace by either removing the quotes to reference
the test namespace variable (e.g., use ns.Name) or replace it with a valid
namespace string like "test-namespace" so metav1.ObjectMeta.Namespace is a
proper Kubernetes name.
🧹 Nitpick comments (7)
internal/webhook/v1alpha1/bmcsecret_webhook_test.go (3)

36-36: Use ptr.To(true) for consistency.

Line 56 already uses ptr.To(false) from the imported k8s.io/utils/ptr package. Use the same approach here for consistency and readability.

♻️ Proposed fix
-			Immutable: &[]bool{true}[0],
+			Immutable: ptr.To(true),

17-19: Variable naming should follow Go conventions.

Go convention uses camelCase for variable names. BMCSecret looks like a type name rather than a variable. Consider renaming to bmcSecret to distinguish it from the type metalv1alpha1.BMCSecret.

♻️ Proposed fix
 	var (
-		BMCSecret *metalv1alpha1.BMCSecret
+		bmcSecret *metalv1alpha1.BMCSecret
 		validator BMCSecretCustomValidator
 	)

Then update all references throughout the test accordingly (BMCSecretUpdatedbmcSecretUpdated, BMCSecretMutablebmcSecretMutable).


26-26: Redundant nil check on struct value.

validator is a struct value (not a pointer), so it can never be nil. This assertion will always pass and doesn't provide meaningful validation.

♻️ Proposed fix
 		validator = BMCSecretCustomValidator{
 			Client: k8sClient,
 		}
-		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
+		Expect(validator.Client).NotTo(BeNil(), "Expected validator client to be initialized")

Alternatively, remove the check entirely if client initialization is guaranteed by the test suite.

docs/api-reference/api.md (1)

627-656: Missing field descriptions in BMCUserSpec and BMCUserStatus documentation.

The documentation tables for BMCUserSpec (lines 629-636) and BMCUserStatus (lines 651-655) have empty description columns for most fields. This is inconsistent with other types documented in this file and reduces API discoverability.

Consider adding descriptions for fields like userName, roleID, enabled, lastRotation, passwordExpiration, and id to match the documentation quality of other types in this API reference.

internal/controller/bmcuser_controller_test.go (3)

126-134: Clarify or fix effective secret deletion test logic.

The test verifies owner references are set (lines 110-118), implying the effective secret should be garbage collected when the BMCUser is deleted. However, line 133 explicitly deletes the effectiveSecret after the user is deleted, which contradicts testing the GC behavior.

If the intent is to verify cascading deletion works, wait for the secret to be garbage collected instead of manually deleting it. If manual cleanup is intended (e.g., background deletion isn't synchronous in envtest), add a comment explaining this.

Suggested approach to verify GC behavior
 		Expect(k8sClient.Delete(ctx, user)).To(Succeed())
 		By("Ensuring that the User resource has been deleted")
 		Eventually(Get(user)).Should(Satisfy(apierrors.IsNotFound))
-		By("Ensuring that the effective BMCSecret has been deleted")
-		Expect(k8sClient.Delete(ctx, effectiveSecret)).To(Succeed())
+		By("Ensuring that the effective BMCSecret has been garbage collected")
+		// Owner reference with BlockOwnerDeletion should trigger cascading deletion
 		Eventually(Get(effectiveSecret)).Should(Satisfy(apierrors.IsNotFound))

319-328: Use consistent assertion pattern for deletion verification.

Line 321 uses Eventually(Get(user)).ShouldNot(Succeed()) while line 131 uses the more precise Eventually(Get(user)).Should(Satisfy(apierrors.IsNotFound)). The latter pattern explicitly verifies the expected error type rather than just any failure.

Suggested fix
 		By("Ensuring that the User resource has been deleted")
-		Eventually(Get(user)).ShouldNot(Succeed())
+		Eventually(Get(user)).Should(Satisfy(apierrors.IsNotFound))

273-279: Clarify password rotation assertion.

Line 279 asserts the password is not "bar", but "bar" doesn't appear in the test setup - the initial secret has password "adminpass" (line 35). This assertion may be a remnant from a previous test version or checking against a mock default.

Consider either:

  • Asserting the password differs from "adminpass" (the actual initial password)
  • Adding a comment explaining what "bar" represents
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd922d2 and 635afe0.

⛔ Files ignored due to path filters (1)
  • dist/chart/templates/crd/metal.ironcore.dev_bmcusers.yaml is excluded by !**/dist/**
📒 Files selected for processing (8)
  • api/v1alpha1/bmcuser_types.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • config/crd/bases/metal.ironcore.dev_bmcusers.yaml
  • docs/api-reference/api.md
  • internal/controller/bmcuser_controller.go
  • internal/controller/bmcuser_controller_test.go
  • internal/webhook/v1alpha1/bmcsecret_webhook.go
  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/webhook/v1alpha1/bmcsecret_webhook.go
  • api/v1alpha1/bmcuser_types.go
  • internal/controller/bmcuser_controller.go
  • config/crd/bases/metal.ironcore.dev_bmcusers.yaml
🧰 Additional context used
📓 Path-based instructions (3)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Code must follow standard Go formatting and idioms; use clear, explicit error handling with minimal global state
Use small, focused functions; introduce interfaces only when justified

Files:

  • internal/controller/bmcuser_controller_test.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Tests must be deterministic and not rely on timing assumptions

Files:

  • internal/controller/bmcuser_controller_test.go
  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
**/api/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Implement defaulting and validation via webhooks (preferred) or OpenAPI schema annotations

Files:

  • api/v1alpha1/zz_generated.deepcopy.go
🧠 Learnings (10)
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller_test.go : Use Envtest-based tests for controller behavior and API interactions

Applied to files:

  • internal/controller/bmcuser_controller_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller_test.go : Write unit tests for reconcile logic and pure functions

Applied to files:

  • internal/controller/bmcuser_controller_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_test.go : Use fake clients appropriately in tests

Applied to files:

  • internal/controller/bmcuser_controller_test.go
  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*.go : Include relevant identifiers in logs; emit events for meaningful user-facing state changes

Applied to files:

  • internal/controller/bmcuser_controller_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Use `client.Client`, `controllerutil.CreateOrUpdate`, and OwnerReferences from controller-runtime rather than direct API calls

Applied to files:

  • internal/controller/bmcuser_controller_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/*_test.go : Tests must be deterministic and not rely on timing assumptions

Applied to files:

  • internal/controller/bmcuser_controller_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Reconciliation must be based on current cluster state, not assumptions; handle NotFound errors correctly and gracefully manage partial failures

Applied to files:

  • internal/controller/bmcuser_controller_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Flag non-idempotent reconcile logic, Spec mutations during reconciliation, and custom patterns duplicating standard Kubernetes behavior

Applied to files:

  • internal/controller/bmcuser_controller_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/api/**/*_types.go : Clearly mark and document deprecated fields in API types

Applied to files:

  • api/v1alpha1/zz_generated.deepcopy.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/api/**/*.go : Implement defaulting and validation via webhooks (preferred) or OpenAPI schema annotations

Applied to files:

  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
🧬 Code graph analysis (1)
internal/webhook/v1alpha1/bmcsecret_webhook_test.go (2)
api/v1alpha1/bmcsecret_types.go (1)
  • BMCSecret (24-57)
internal/webhook/v1alpha1/bmcsecret_webhook.go (1)
  • BMCSecretCustomValidator (32-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests
  • GitHub Check: Run linter
  • GitHub Check: Run test chart
  • GitHub Check: Run test e2e
🔇 Additional comments (4)
internal/webhook/v1alpha1/bmcsecret_webhook_test.go (1)

45-62: Test coverage looks appropriate for validating webhook behavior.

The two test cases correctly cover both branches of the immutability validation logic:

  1. Deny mutation when Immutable=true
  2. Allow mutation when Immutable=false

The use of ValidateUpdate directly on the validator is a good approach for unit testing webhook logic.

internal/controller/bmcuser_controller_test.go (2)

243-282: Potential timing sensitivity in rotation test.

This test uses a 1 * time.Second rotation period (line 257-259) and relies on the controller to detect expiration and perform rotation. While the 4-second Eventually timeout (line 268) provides margin, this pattern can be flaky under CI load where reconciliation may be delayed.

Consider using the annotation-driven rotation approach (tested separately at line 330) as the primary rotation mechanism test, or ensure the controller triggers immediate reconciliation when rotation period expires rather than relying on wall-clock time.

Based on learnings, tests must be deterministic and not rely on timing assumptions.


21-77: LGTM on test structure and setup.

The test setup follows good practices:

  • Uses Ginkgo/Gomega with envtest appropriately per learnings
  • BeforeEach/AfterEach properly manage test fixtures
  • Uses Eventually with Object/Get patterns for async assertions

The overall test coverage for BMCUser lifecycle (creation, multi-user scenarios, rotation, deletion, annotation-driven rotation) is comprehensive.

api/v1alpha1/zz_generated.deepcopy.go (1)

837-952: Auto-generated deepcopy implementations look correct.

The controller-gen generated deepcopy functions for BMCUser, BMCUserList, BMCUserSpec, and BMCUserStatus follow the established patterns in this file and correctly handle:

  • Pointer fields with nil checks and explicit copying
  • metav1.Time pointers using .DeepCopy()
  • Slice items with element-wise DeepCopyInto

As this is auto-generated code, ensure it stays in sync with the source types in bmcuser_types.go by running make generate when type definitions change.

Comment thread internal/webhook/v1alpha1/bmcsecret_webhook_test.go
Comment thread internal/webhook/v1alpha1/bmcsecret_webhook_test.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In @internal/controller/bmcuser_controller.go:
- Around line 165-175: The code can panic when forceRotation is true but
user.Spec.RotationPeriod is nil because the second if accesses
user.Spec.RotationPeriod.Duration; fix by guarding that access: ensure you check
user.Spec.RotationPeriod != nil before any use of .Duration (either move the
existing nil check to run unconditionally or change the second condition to
start with user.Spec.RotationPeriod != nil && user.Status.LastRotation != nil &&
...), and if RotationPeriod is nil decide and document the intended behavior for
forceRotation (e.g., skip rotation or handle as immediate rotation) so
RequeueAfter is only set when Duration is valid.
- Around line 185-196: The rotation is non-atomic because createSecret(...) runs
before bmcClient.CreateOrUpdateAccount(...), leaving Spec.BMCSecretRef pointing
at creds that may not match the BMC on failure; generate the newPassword first,
call bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID,
newPassword, r.Insecure) before calling r.createSecret(ctx, log, user,
newPassword), and only patch user.Status.LastRotation after both account update
and secret creation succeed; if you prefer to keep the current order instead
implement a rollback: if createSecret succeeds but CreateOrUpdateAccount fails,
delete the newly created secret (or restore the previous secret reference) to
avoid leaving the cluster pointing at invalid credentials.
- Around line 335-351: bmcConnectionTest creates a BMC client via
CreateBMCClient but never closes it; change the call to capture the returned
client (e.g., client, err := bmcutils.CreateBMCClient(...)), and after a
successful creation immediately defer closing or logging out the client (call
the client's Close/Logout method) so the connection/session is released; ensure
the defer runs regardless of subsequent error handling (including the 401/403
path) and log any logout error using the reconciler logger.
🧹 Nitpick comments (4)
internal/controller/bmcuser_controller.go (4)

257-261: Spec mutation during reconciliation.

The createSecret function modifies user.Spec.BMCSecretRef via a patch operation. Mutating the Spec within a reconcile loop is generally discouraged as it can cause reconciliation loops and makes the controller less predictable. Consider having the user provide the BMCSecretRef externally, or if auto-generation is intentional, document this behavior clearly and ensure the reconciliation handles the re-queue properly.

Based on learnings, spec mutations during reconciliation should be flagged.


131-136: Consider surfacing password expiration parsing failures.

When parsing fails, the error is logged but the reconciliation continues successfully. If the BMC consistently returns an unparseable format, this issue will be silently hidden. Consider emitting a Kubernetes event or setting a condition on the status to make this visible to operators.

Based on learnings, events should be emitted for meaningful user-facing state changes.


245-245: Consider using ptr.To(true) for cleaner pointer creation.

The expression &[]bool{true}[0] works but is non-idiomatic Go. The k8s.io/utils/ptr package provides a cleaner helper.

♻️ Suggested improvement
+import "k8s.io/utils/ptr"
+
 	secret := &metalv1alpha1.BMCSecret{
 		ObjectMeta: metav1.ObjectMeta{
 			GenerateName: user.Name + "-bmcsecret-",
 		},
 		Data: map[string][]byte{
 			metalv1alpha1.BMCSecretUsernameKeyName: []byte(user.Spec.UserName),
 			metalv1alpha1.BMCSecretPasswordKeyName: []byte(password),
 		},
-		Immutable: &[]bool{true}[0], // Make the secret immutable
+		Immutable: ptr.To(true),
 	}

381-388: Consider watching BMC resources for state changes.

The controller watches BMCUser and owned BMCSecret resources, but doesn't watch BMC resources. If a referenced BMC transitions to Enabled state after the BMCUser is created, the controller won't be notified to retry. Consider adding a watch on BMC resources with an appropriate filter.

♻️ Suggested enhancement
+import (
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+)
+
 func (r *BMCUserReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&metalv1alpha1.BMCUser{}).
 		Owns(&metalv1alpha1.BMCSecret{}).
+		Watches(
+			&metalv1alpha1.BMC{},
+			handler.EnqueueRequestsFromMapFunc(r.findBMCUsersForBMC),
+		).
 		Named("bmcuser").
 		Complete(r)
 }
+
+func (r *BMCUserReconciler) findBMCUsersForBMC(ctx context.Context, obj client.Object) []ctrl.Request {
+	// Implementation to find BMCUsers referencing this BMC
+}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0aecc9 and 3f3014d.

📒 Files selected for processing (1)
  • internal/controller/bmcuser_controller.go
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Code must follow standard Go formatting and idioms; use clear, explicit error handling with minimal global state
Use small, focused functions; introduce interfaces only when justified

Files:

  • internal/controller/bmcuser_controller.go
🧠 Learnings (4)
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Flag non-idempotent reconcile logic, Spec mutations during reconciliation, and custom patterns duplicating standard Kubernetes behavior

Applied to files:

  • internal/controller/bmcuser_controller.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Use `client.Client`, `controllerutil.CreateOrUpdate`, and OwnerReferences from controller-runtime rather than direct API calls

Applied to files:

  • internal/controller/bmcuser_controller.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Prevent hot loops caused by status-only changes triggering reconciliation

Applied to files:

  • internal/controller/bmcuser_controller.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*.go : Include relevant identifiers in logs; emit events for meaningful user-facing state changes

Applied to files:

  • internal/controller/bmcuser_controller.go
🧬 Code graph analysis (1)
internal/controller/bmcuser_controller.go (6)
api/v1alpha1/bmcuser_types.go (1)
  • BMCUser (39-45)
api/v1alpha1/bmc_types.go (2)
  • BMC (257-263)
  • Protocol (127-138)
internal/webhook/v1alpha1/bmcsecret_webhook_test.go (1)
  • BMCSecret (18-18)
internal/bmcutils/bmcutils.go (4)
  • GetBMCCredentialsFromSecret (49-60)
  • GetBMCClientFromBMC (147-188)
  • GetProtocolScheme (39-47)
  • CreateBMCClient (190-229)
api/v1alpha1/constants.go (2)
  • OperationAnnotation (21-21)
  • OperationAnnotationRotateCredentials (45-45)
api/v1alpha1/bmcsecret_types.go (2)
  • BMCSecretUsernameKeyName (13-13)
  • BMCSecretPasswordKeyName (15-15)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run test chart
  • GitHub Check: Run linter
  • GitHub Check: Run tests
  • GitHub Check: Run test e2e
🔇 Additional comments (2)
internal/controller/bmcuser_controller.go (2)

26-36: LGTM!

The reconciler struct follows the standard controller-runtime pattern, and the RBAC markers correctly declare permissions for BMCUser resources and their status/finalizers.


42-58: LGTM!

The reconciliation entry point follows idiomatic controller-runtime patterns with proper error handling and deletion timestamp checking.

Comment thread internal/controller/bmcuser_controller.go
Comment on lines +185 to +196
if err := r.createSecret(ctx, log, user, newPassword); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to create BMCSecret: %w", err)
}
if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, newPassword, r.Insecure); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to create or update BMC account with new password: %w", err)
}
// Update the last rotation time
userBase := user.DeepCopy()
user.Status.LastRotation = &metav1.Time{Time: metav1.Now().Time}
if err := r.Status().Patch(ctx, user, client.MergeFrom(userBase)); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch User status with last rotation time: %w", err)
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Non-atomic credential rotation may leave inconsistent state.

The sequence creates a new secret first (line 185), then updates the BMC account (line 188). If CreateOrUpdateAccount fails, the Spec.BMCSecretRef already points to a secret with credentials that don't match the BMC. Subsequent reconciliation attempts will fail to authenticate.

Consider reversing the order: update the BMC account first (you can generate the password before calling createSecret), then create the secret only on success. Alternatively, implement a rollback mechanism.

🛠️ Suggested approach
 	log.Info("Rotating BMC user password", "User", user.Name)
 	accountService, err := bmcClient.GetAccountService(ctx)
 	if err != nil {
 		return ctrl.Result{}, fmt.Errorf("failed to get account service: %w", err)
 	}
 	newPassword, err := bmc.GenerateSecurePassword(bmc.Manufacturer(bmcObj.Status.Manufacturer), accountService.MaxPasswordLength)
 	if err != nil {
 		return ctrl.Result{}, fmt.Errorf("failed to generate new password for BMC user %s: %w", user.Name, err)
 	}
-	if err := r.createSecret(ctx, log, user, newPassword); err != nil {
-		return ctrl.Result{}, fmt.Errorf("failed to create BMCSecret: %w", err)
-	}
+	// Update BMC account first to ensure credentials are valid before persisting
 	if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, newPassword, r.Insecure); err != nil {
 		return ctrl.Result{}, fmt.Errorf("failed to create or update BMC account with new password: %w", err)
 	}
+	if err := r.createSecret(ctx, log, user, newPassword); err != nil {
+		return ctrl.Result{}, fmt.Errorf("failed to create BMCSecret: %w", err)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := r.createSecret(ctx, log, user, newPassword); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to create BMCSecret: %w", err)
}
if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, newPassword, r.Insecure); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to create or update BMC account with new password: %w", err)
}
// Update the last rotation time
userBase := user.DeepCopy()
user.Status.LastRotation = &metav1.Time{Time: metav1.Now().Time}
if err := r.Status().Patch(ctx, user, client.MergeFrom(userBase)); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch User status with last rotation time: %w", err)
}
// Update BMC account first to ensure credentials are valid before persisting
if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, newPassword, r.Insecure); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to create or update BMC account with new password: %w", err)
}
if err := r.createSecret(ctx, log, user, newPassword); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to create BMCSecret: %w", err)
}
// Update the last rotation time
userBase := user.DeepCopy()
user.Status.LastRotation = &metav1.Time{Time: metav1.Now().Time}
if err := r.Status().Patch(ctx, user, client.MergeFrom(userBase)); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch User status with last rotation time: %w", err)
}
🤖 Prompt for AI Agents
In @internal/controller/bmcuser_controller.go around lines 185 - 196, The
rotation is non-atomic because createSecret(...) runs before
bmcClient.CreateOrUpdateAccount(...), leaving Spec.BMCSecretRef pointing at
creds that may not match the BMC on failure; generate the newPassword first,
call bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID,
newPassword, r.Insecure) before calling r.createSecret(ctx, log, user,
newPassword), and only patch user.Status.LastRotation after both account update
and secret creation succeed; if you prefer to keep the current order instead
implement a rollback: if createSecret succeeds but CreateOrUpdateAccount fails,
delete the newly created secret (or restore the previous secret reference) to
avoid leaving the cluster pointing at invalid credentials.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other way round is more critical. If we update the user password on the BMC successfully, but then fail to create the k8s Secret, we will loose the password and possibly the access to our BMC

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stefanhipfel You make an excellent point! You're absolutely right that losing the password entirely is a more critical failure mode than having a temporary mismatch. If the BMC is updated successfully but the secret creation fails, the randomly generated password would be lost forever, potentially locking out access to that BMC account.

The current order (create secret → update BMC) is safer in that regard because the password is persisted before we touch the BMC.

However, to handle the failure case where the secret is created but the BMC update fails, you might want to consider adding some safeguards:

  1. Preserve the old secret: Ensure the old secret (referenced by Status.EffectiveBMCSecretRef) isn't deleted until the BMC update succeeds. That way, if the BMC update fails, the next reconciliation can still authenticate with the old credentials.

  2. Status conditions: Add a status condition to track the rotation state (e.g., RotationInProgress, RotationFailed) so operators can detect and remediate stuck rotations.

  3. Exponential backoff: The controller-runtime will already retry on errors, but you might want to add explicit logic to handle transient BMC update failures gracefully.

The current implementation is reasonable given the trade-offs. The most important thing is that you've clearly thought through the failure modes!


🧠 Learnings used
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Prevent hot loops caused by status-only changes triggering reconciliation

Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Flag non-idempotent reconcile logic, Spec mutations during reconciliation, and custom patterns duplicating standard Kubernetes behavior

Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Reconcile logic must be idempotent and safe to re-run at any time without side effects

Copy link
Copy Markdown
Member

@afritzler afritzler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @stefanhipfel for the updates. A few more comments from my side here.

Comment thread api/v1alpha1/bmcuser_types.go Outdated
Comment thread api/v1alpha1/bmcuser_types.go
Comment thread internal/controller/bmcuser_controller.go Outdated
Comment thread internal/webhook/v1alpha1/bmcsecret_webhook_test.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @internal/controller/bmcuser_controller.go:
- Around line 234-262: The createSecret function currently mutates
user.Spec.BMCSecretRef (in BMCUserReconciler.createSecret) which is a spec
anti-pattern; instead, after creating/patching the BMCSecret, do not modify
user.Spec — create a deep copy of the original user, set
user.Status.EffectiveBMCSecretRef = &v1.LocalObjectReference{Name: secret.Name},
and call r.Status().Patch(ctx, user, client.MergeFrom(userBase)) (or r.Patch on
status) to persist the status change; remove the lines that assign to
Spec.BMCSecretRef and update any reconcile logic to prefer
user.Status.EffectiveBMCSecretRef when user.Spec.BMCSecretRef is nil.
- Around line 334-350: The bmcConnectionTest function calls
bmcutils.CreateBMCClient but doesn't close the returned client on success;
capture the returned client (e.g., client or bc) from CreateBMCClient and ensure
you call its appropriate cleanup method (Logout/Close/Destroy) before returning
(use defer immediately after verifying client != nil) so the Redfish session is
terminated; update bmcConnectionTest to defer the client's logout/close after
CreateBMCClient succeeds and ensure nil-checks to avoid panics.
- Around line 86-97: The code may dereference user.Spec.BMCSecretRef.Name after
calling handleMissingBMCSecretRef, which can still leave BMCSecretRef nil; after
handleMissingBMCSecretRef returns successfully, re-fetch the User resource
(using r.Get) or add a defensive check that user.Spec.BMCSecretRef != nil before
accessing .Name; update the code paths around
handleMissingBMCSecretRef/createSecret to either refresh the local user object
or return an explicit error if the ref is still nil, then safely proceed to
populate bmcSecret via r.Get using user.Spec.BMCSecretRef.Name.
- Around line 352-378: The delete function may return without removing the
BMCUserFinalizer causing stuck resources; ensure you always attempt to remove
the finalizer (using clientutils.PatchEnsureNoFinalizer(ctx, r.Client, user,
BMCUserFinalizer)) before every early return path: when user.Spec.BMCRef == nil,
and when the referenced BMC lookup returns NotFound (the client.IgnoreNotFound
branch), and also when getBMCClient fails if appropriate; consolidate the
finalizer removal call so it runs once before returning on error/early-exit,
handle its (modified, err) results correctly (log removal only when modified is
true, and return err if err != nil), and remove or relocate the unreachable log
statement after the finalizer-removal logic so logs reflect actual execution.
- Around line 59-118: The CreateOrUpdateAccount calls are passing r.Insecure for
the account "enabled" flag; replace that with the BMCUser spec value
user.Spec.Enabled. Update every invocation of CreateOrUpdateAccount (referenced
by the method name CreateOrUpdateAccount) from the controller: inside reconcile
and in the helper flows that reconcile triggers (e.g.,
handleMissingBMCSecretRef, handleUpdatedSecretRef, handleRotatingPassword) so
each call uses user.Spec.Enabled instead of r.Insecure. Ensure the user
parameter (user) is in scope where you change the call or pass it through to the
helper so user.Spec.Enabled can be used.
🧹 Nitpick comments (8)
internal/webhook/v1alpha1/bmcsecret_webhook_test.go (6)

17-17: Use lowercase for local variable name.

Go convention uses lowercase names for local variables. BMCSecret should be bmcSecret to avoid confusion with the type name.

♻️ Suggested change
-		BMCSecret *metalv1alpha1.BMCSecret
+		bmcSecret *metalv1alpha1.BMCSecret

Then update all references throughout the test accordingly.


25-25: Remove meaningless nil check.

validator is a struct value, not a pointer. Struct values cannot be nil in Go, making this assertion always pass and provide no value.

♻️ Suggested fix
-		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")

35-35: Use ptr.To(true) for consistency.

The ptr.To() utility from k8s.io/utils/ptr is already imported and used on line 53. Using it here improves readability and maintains consistency.

♻️ Suggested fix
-			Immutable: &[]bool{true}[0],
+			Immutable: ptr.To(true),

37-37: Minor grammar fix.

"an BMCSecret" should be "a BMCSecret" (BMC starts with a consonant sound).

♻️ Suggested fix
-		By("Creating an BMCSecret")
+		By("Creating a BMCSecret")

43-48: Test logic is correct; minor grammar fix needed.

The test correctly validates that updates are denied when Immutable is true. Grammar: "an BMCSecret" → "a BMCSecret" on line 44.

♻️ Suggested fix
-			By("Updating an BMCSecret with Immutable set to True")
+			By("Updating a BMCSecret with Immutable set to True")

50-59: Test logic is correct; minor grammar fix needed.

The test properly updates Immutable to false via the API before validating that data changes are allowed. Grammar: "an BMCSecret" → "a BMCSecret" on line 51.

♻️ Suggested fix
-			By("Updating an BMCSecret with Immutable set to False")
+			By("Updating a BMCSecret with Immutable set to False")
api/v1alpha1/bmcuser_types.go (1)

12-34: Consider adding validation markers and review the Enabled field default behavior.

A few observations on the spec definition:

  1. Enabled field (line 30): The boolean defaults to false (Go zero value), meaning newly created BMCUser resources will have disabled accounts unless explicitly set. If the typical use case expects enabled accounts, consider using a pointer (*bool) or adding +kubebuilder:default=true.

  2. Missing validation markers: Consider adding +kubebuilder:validation:Required for mandatory fields like userName and roleID, and +kubebuilder:validation:MinLength=1 to prevent empty strings.

  3. BMCControllerUser comment (line 31): The comment should start with the field name per Go conventions: "BMCControllerUser indicates if the user should be used by...".

Suggested improvements
 // BMCUserSpec defines the desired state of BMCUser.
 type BMCUserSpec struct {
 	// Username of the BMC user.
+	// +kubebuilder:validation:Required
+	// +kubebuilder:validation:MinLength=1
 	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".
+	// +kubebuilder:validation:Required
+	// +kubebuilder:validation:MinLength=1
 	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 *v1.LocalObjectReference `json:"bmcSecretRef,omitempty"`
 	// BMCRef references the BMC this user should be created on.
 	BMCRef *v1.LocalObjectReference `json:"bmcRef,omitempty"`
 	// Enabled indicates whether the user account is enabled.
+	// +kubebuilder:default=true
 	Enabled bool `json:"enabled"`
-	// set if the user should be used by the BMC controller to access the system.
+	// BMCControllerUser indicates if this user should be used by the BMC controller to access the system.
 	// +kubebuilder:default=false
 	BMCControllerUser bool `json:"bmcControllerUser"`
 }
internal/controller/bmcuser_controller.go (1)

334-350: Consider renaming function for clarity on return value semantics.

The function bmcConnectionTest returns (bool, error) where true indicates invalid credentials. This is counter-intuitive—typically a "test" returning true implies success. The callers at lines 297-304 and 320-330 use invalidCredentials variable name, which helps, but the function signature is still confusing.

Consider renaming to areCredentialsInvalid or checkCredentialsInvalid to make the boolean semantics explicit.

-func (r *BMCUserReconciler) bmcConnectionTest(ctx context.Context, secret *metalv1alpha1.BMCSecret, bmcObj *metalv1alpha1.BMC) (bool, error) {
+func (r *BMCUserReconciler) areCredentialsInvalid(ctx context.Context, secret *metalv1alpha1.BMCSecret, bmcObj *metalv1alpha1.BMC) (bool, error) {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f3014d and 29a9e90.

⛔ Files ignored due to path filters (1)
  • dist/chart/templates/crd/metal.ironcore.dev_bmcusers.yaml is excluded by !**/dist/**
📒 Files selected for processing (5)
  • api/v1alpha1/bmcuser_types.go
  • config/crd/bases/metal.ironcore.dev_bmcusers.yaml
  • docs/api-reference/api.md
  • internal/controller/bmcuser_controller.go
  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/api-reference/api.md
  • config/crd/bases/metal.ironcore.dev_bmcusers.yaml
🧰 Additional context used
📓 Path-based instructions (4)
**/api/**/*_types.go

📄 CodeRabbit inference engine (AGENTS.md)

**/api/**/*_types.go: API type fields must follow Kubernetes naming conventions: camelCase in JSON, PascalCase in Go structs
Use metav1.Time, metav1.Condition, and resource.Quantity for standard Kubernetes types in API definitions
Spec fields must be declarative and user-facing; Status fields must be controller-owned
Conditions in Status must follow Kubernetes conventions with stable Type, correct Status, Reason, and Message fields
No mutable fields in Spec that belong in Status; maintain clear separation between desired state and observed state
Do not make breaking changes to existing API fields without versioning; ensure additive-only changes for existing API versions
Clearly mark and document deprecated fields in API types

Files:

  • api/v1alpha1/bmcuser_types.go
**/api/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Implement defaulting and validation via webhooks (preferred) or OpenAPI schema annotations

Files:

  • api/v1alpha1/bmcuser_types.go
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Code must follow standard Go formatting and idioms; use clear, explicit error handling with minimal global state
Use small, focused functions; introduce interfaces only when justified

Files:

  • api/v1alpha1/bmcuser_types.go
  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
  • internal/controller/bmcuser_controller.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Tests must be deterministic and not rely on timing assumptions

Files:

  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
🧠 Learnings (11)
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/api/**/*_types.go : Spec fields must be declarative and user-facing; Status fields must be controller-owned

Applied to files:

  • api/v1alpha1/bmcuser_types.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/api/**/*_types.go : Use `metav1.Time`, `metav1.Condition`, and `resource.Quantity` for standard Kubernetes types in API definitions

Applied to files:

  • api/v1alpha1/bmcuser_types.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/api/**/*_types.go : No mutable fields in Spec that belong in Status; maintain clear separation between desired state and observed state

Applied to files:

  • api/v1alpha1/bmcuser_types.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/api/**/*.go : Implement defaulting and validation via webhooks (preferred) or OpenAPI schema annotations

Applied to files:

  • api/v1alpha1/bmcuser_types.go
  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/api/**/*_types.go : Conditions in Status must follow Kubernetes conventions with stable Type, correct Status, Reason, and Message fields

Applied to files:

  • api/v1alpha1/bmcuser_types.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/*_test.go : Tests must be deterministic and not rely on timing assumptions

Applied to files:

  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_test.go : Use fake clients appropriately in tests

Applied to files:

  • internal/webhook/v1alpha1/bmcsecret_webhook_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Flag non-idempotent reconcile logic, Spec mutations during reconciliation, and custom patterns duplicating standard Kubernetes behavior

Applied to files:

  • internal/controller/bmcuser_controller.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Use `client.Client`, `controllerutil.CreateOrUpdate`, and OwnerReferences from controller-runtime rather than direct API calls

Applied to files:

  • internal/controller/bmcuser_controller.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Prevent hot loops caused by status-only changes triggering reconciliation

Applied to files:

  • internal/controller/bmcuser_controller.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*.go : Include relevant identifiers in logs; emit events for meaningful user-facing state changes

Applied to files:

  • internal/controller/bmcuser_controller.go
🧬 Code graph analysis (3)
api/v1alpha1/bmcuser_types.go (1)
api/v1alpha1/groupversion_info.go (1)
  • SchemeBuilder (19-19)
internal/webhook/v1alpha1/bmcsecret_webhook_test.go (2)
api/v1alpha1/bmcsecret_types.go (1)
  • BMCSecret (24-57)
internal/webhook/v1alpha1/bmcsecret_webhook.go (1)
  • BMCSecretCustomValidator (32-34)
internal/controller/bmcuser_controller.go (8)
bmc/redfish.go (2)
  • Options (43-53)
  • GenerateSecurePassword (1108-1161)
api/v1alpha1/bmcuser_types.go (1)
  • BMCUser (54-60)
api/v1alpha1/bmc_types.go (2)
  • BMC (257-263)
  • Protocol (127-138)
internal/webhook/v1alpha1/bmcsecret_webhook_test.go (1)
  • BMCSecret (17-17)
internal/bmcutils/bmcutils.go (5)
  • GetBMCCredentialsFromSecret (49-60)
  • GetBMCClientFromBMC (147-188)
  • GetProtocolScheme (39-47)
  • GetBMCAddressForBMC (93-109)
  • CreateBMCClient (190-229)
api/v1alpha1/constants.go (2)
  • OperationAnnotation (21-21)
  • OperationAnnotationRotateCredentials (45-45)
bmc/bmc.go (2)
  • Manufacturer (19-19)
  • Manager (301-312)
api/v1alpha1/bmcsecret_types.go (2)
  • BMCSecretUsernameKeyName (13-13)
  • BMCSecretPasswordKeyName (15-15)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Run test e2e
  • GitHub Check: Run test chart
  • GitHub Check: Run tests
🔇 Additional comments (5)
internal/webhook/v1alpha1/bmcsecret_webhook_test.go (1)

1-13: LGTM!

License header and imports are appropriate for a Ginkgo/Gomega webhook test.

api/v1alpha1/bmcuser_types.go (2)

36-47: LGTM - Status struct follows proper conventions.

The status fields correctly use metav1.Time for timestamps and v1.LocalObjectReference for secret references. All fields are appropriately marked as optional with omitempty. The separation between Spec (user-desired state) and Status (controller-observed state) is correct.


49-73: LGTM - CRD type definitions and registration are correct.

The kubebuilder markers are properly configured for a cluster-scoped resource with status subresource. The scheme registration follows the standard pattern.

internal/controller/bmcuser_controller.go (2)

120-144: LGTM - Status patching follows correct patterns.

The status patching uses client.MergeFrom correctly. Error handling for password expiration parsing is reasonable—logging the error and continuing rather than failing the entire reconciliation for an optional field.


380-387: LGTM - Controller setup is correct.

The controller setup properly watches BMCUser resources and owns BMCSecret resources, which is appropriate since the controller creates BMCSecrets with owner references. The named controller "bmcuser" follows conventions.

Comment thread internal/controller/bmcuser_controller.go
Comment thread internal/controller/bmcuser_controller.go
Comment on lines +111 to +116
if user.Status.EffectiveBMCSecretRef != nil && user.Spec.BMCSecretRef.Name != user.Status.EffectiveBMCSecretRef.Name {
log.Info("BMCSecret reference has changed, updating BMC account", "User", user.Name)
if err := r.handleUpdatedSecretRef(ctx, log, user, bmcSecret, bmcClient); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to handle updated BMCSecret reference: %w", err)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing status update after secret reference change may cause repeated updates.

After handleUpdatedSecretRef updates the BMC account password, Status.EffectiveBMCSecretRef is not updated to match Spec.BMCSecretRef. On subsequent reconciliations, the condition at line 111 will continue to detect a mismatch, potentially causing repeated BMC account updates until updateEffectiveSecret (line 70) updates the status under specific conditions.

Consider updating Status.EffectiveBMCSecretRef in handleUpdatedSecretRef after successfully updating the BMC account:

Suggested fix
 func (r *BMCUserReconciler) handleUpdatedSecretRef(ctx context.Context, log logr.Logger, user *metalv1alpha1.BMCUser, bmcSecret *metalv1alpha1.BMCSecret, bmcClient bmc.BMC) error {
 	log.Info("BMCSecret credentials have changed, updating BMC user", "User", user.Name)
 	_, password, err := bmcutils.GetBMCCredentialsFromSecret(bmcSecret)
 	if err != nil {
 		return fmt.Errorf("failed to get credentials from BMCSecret: %w", err)
 	}
 	// Update the BMC account with the new password
 	if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, password, r.Insecure); err != nil {
 		return fmt.Errorf("failed to create or update BMC account with new password: %w", err)
 	}
+	// Update the effective secret reference to match the new spec
+	if err := r.setEffectiveSecretRef(ctx, log, user, bmcSecret); err != nil {
+		return fmt.Errorf("failed to update effective BMCSecretRef: %w", err)
+	}
 	return nil
 }

Also applies to: 221-232

Comment on lines +234 to +262
func (r *BMCUserReconciler) createSecret(ctx context.Context, log logr.Logger, user *metalv1alpha1.BMCUser, password string) error {
log.Info("Creating BMCSecret for User", "User", user.Name)
secret := &metalv1alpha1.BMCSecret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: user.Name + "-bmcsecret-",
},
Data: map[string][]byte{
metalv1alpha1.BMCSecretUsernameKeyName: []byte(user.Spec.UserName),
metalv1alpha1.BMCSecretPasswordKeyName: []byte(password),
},
Immutable: &[]bool{true}[0], // Make the secret immutable
}
op, err := controllerutil.CreateOrPatch(ctx, r.Client, secret, func() error {
if err := controllerutil.SetControllerReference(user, secret, r.Scheme); err != nil {
return fmt.Errorf("failed to set controller reference for BMCSecret: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to create or patch BMCSecret: %w", err)
}
log.V(1).Info("BMCSecret created or patched", "BMCSecret", secret.Name, "Operation", op)
userBase := user.DeepCopy()
user.Spec.BMCSecretRef = &v1.LocalObjectReference{Name: secret.Name}
if err := r.Patch(ctx, user, client.MergeFrom(userBase)); err != nil {
return fmt.Errorf("failed to patch User status with effective BMCSecretRef: %w", err)
}
return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Spec mutation anti-pattern: Controller should not modify Spec.BMCSecretRef.

The controller modifies user.Spec.BMCSecretRef at lines 256-259. This violates the Kubernetes convention that Spec fields are user-owned (declarative desired state) while Status fields are controller-owned (observed state). Based on coding guidelines, "Spec fields must be declarative and user-facing; Status fields must be controller-owned."

The BMCUserStatus.EffectiveBMCSecretRef field already exists for this purpose. Instead of patching Spec, the controller should:

  1. Generate the secret
  2. Set Status.EffectiveBMCSecretRef to reference the generated secret
  3. Use Status.EffectiveBMCSecretRef throughout reconciliation when Spec.BMCSecretRef is nil

This also improves idempotency—currently, if reconciliation is interrupted after the Spec patch but before the BMC account creation, the next reconciliation may behave unexpectedly.

Suggested refactor
 func (r *BMCUserReconciler) createSecret(ctx context.Context, log logr.Logger, user *metalv1alpha1.BMCUser, password string) error {
 	log.Info("Creating BMCSecret for User", "User", user.Name)
 	secret := &metalv1alpha1.BMCSecret{
 		ObjectMeta: metav1.ObjectMeta{
 			GenerateName: user.Name + "-bmcsecret-",
 		},
 		Data: map[string][]byte{
 			metalv1alpha1.BMCSecretUsernameKeyName: []byte(user.Spec.UserName),
 			metalv1alpha1.BMCSecretPasswordKeyName: []byte(password),
 		},
 		Immutable: &[]bool{true}[0],
 	}
-	op, err := controllerutil.CreateOrPatch(ctx, r.Client, secret, func() error {
+	if err := controllerutil.SetControllerReference(user, secret, r.Scheme); err != nil {
+		return fmt.Errorf("failed to set controller reference for BMCSecret: %w", err)
+	}
+	if err := r.Create(ctx, secret); err != nil {
+		return fmt.Errorf("failed to create BMCSecret: %w", err)
+	}
+	log.V(1).Info("BMCSecret created", "BMCSecret", secret.Name)
+	// Update Status, not Spec
+	if err := r.setEffectiveSecretRef(ctx, log, user, secret); err != nil {
+		return fmt.Errorf("failed to set effective BMCSecretRef in status: %w", err)
+	}
-		if err := controllerutil.SetControllerReference(user, secret, r.Scheme); err != nil {
-			return fmt.Errorf("failed to set controller reference for BMCSecret: %w", err)
-		}
-		return nil
-	})
-	if err != nil {
-		return fmt.Errorf("failed to create or patch BMCSecret: %w", err)
-	}
-	log.V(1).Info("BMCSecret created or patched", "BMCSecret", secret.Name, "Operation", op)
-	userBase := user.DeepCopy()
-	user.Spec.BMCSecretRef = &v1.LocalObjectReference{Name: secret.Name}
-	if err := r.Patch(ctx, user, client.MergeFrom(userBase)); err != nil {
-		return fmt.Errorf("failed to patch User status with effective BMCSecretRef: %w", err)
-	}
 	return nil
 }

Then update the reconcile logic to use Status.EffectiveBMCSecretRef when Spec.BMCSecretRef is nil.

🤖 Prompt for AI Agents
In @internal/controller/bmcuser_controller.go around lines 234 - 262, The
createSecret function currently mutates user.Spec.BMCSecretRef (in
BMCUserReconciler.createSecret) which is a spec anti-pattern; instead, after
creating/patching the BMCSecret, do not modify user.Spec — create a deep copy of
the original user, set user.Status.EffectiveBMCSecretRef =
&v1.LocalObjectReference{Name: secret.Name}, and call r.Status().Patch(ctx,
user, client.MergeFrom(userBase)) (or r.Patch on status) to persist the status
change; remove the lines that assign to Spec.BMCSecretRef and update any
reconcile logic to prefer user.Status.EffectiveBMCSecretRef when
user.Spec.BMCSecretRef is nil.

Comment thread internal/controller/bmcuser_controller.go
Comment thread internal/controller/bmcuser_controller.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@bmc/redfish.go`:
- Around line 788-794: The Delete call currently discards the *http.Response and
leaks its Body; update the block that calls r.client.Delete(a.ODataID) (inside
the account deletion check for a.UserName==userName && a.ID==id) to capture the
response (resp, err := r.client.Delete(...)), ensure the body is closed on all
paths (defer resp.Body.Close() after a non-nil resp), and on error ensure any
non-nil resp.Body is closed before returning; optionally drain the body
(io.Copy(io.Discard, resp.Body)) before closing to allow connection reuse.

In `@internal/controller/suite_test.go`:
- Around line 266-275: Add a ManagerNamespace string field to the
BMCUserReconciler struct and ensure createSecret uses it when constructing
BMCSecret objects (set the Secret.ObjectMeta.Namespace = r.ManagerNamespace); in
the test instantiate BMCUserReconciler with ManagerNamespace set (e.g.,
k8sManager.GetNamespace() or the same namespace used by other reconcilers) so
SetupWithManager uses the correct namespace; mirror the pattern used by
BMCSettingsReconciler/BIOSVersionReconciler/BMCReconciler and ensure any
test-created secrets specify that namespace.
♻️ Duplicate comments (2)
bmc/redfish.go (2)

771-774: Incorrect error message for account creation.

Line 773 says "failed to update account" but this code path handles CreateAccount. This was flagged in a previous review.

🐛 Fix error message
 	_, err = service.CreateAccount(userName, password, role)
 	if err != nil {
-		return fmt.Errorf("failed to update account: %w", err)
+		return fmt.Errorf("failed to create account: %w", err)
 	}

800-806: Return nil on error for consistent Go error handling.

When AccountService() fails, returning (service, error) may provide a nil or invalid service to callers. This was flagged in a previous review.

🔧 Suggested fix
 func (r *RedfishBMC) GetAccountService(ctx context.Context) (*redfish.AccountService, error) {
 	service, err := r.client.GetService().AccountService()
 	if err != nil {
-		return service, fmt.Errorf("failed to get account service: %w", err)
+		return nil, fmt.Errorf("failed to get account service: %w", err)
 	}
 	return service, nil
 }
🧹 Nitpick comments (2)
PROJECT (1)

148-155: LGTM - BMCUser resource scaffolding.

The BMCUser resource is correctly configured as a cluster-scoped resource (no namespaced: true), consistent with other BMC-related resources in the project.

Consider whether a validation webhook should be added for BMCUser in a follow-up, similar to other resources like BMCSettings and BMCVersion, to enforce constraints on fields like roleID or userName.

bmc/redfish.go (1)

1095-1108: Double % character and type inconsistency.

Two issues:

  1. Line 1097: %% in a regular Go string literal is literally two % characters. If you intended a single %, use just %.

  2. Line 1105: Using the string literal "default" as a map key is inconsistent with the Manufacturer type. Consider defining a constant like ManufacturerDefault Manufacturer = "default" for type safety.

♻️ Proposed fix
 var manufacturerPasswordConfigs = map[Manufacturer]ManufacturerPasswordConfig{
 	ManufacturerDell: {
-		SpecialChars: "!#$%%&()*.?-@[]^_`{}|~+=",
+		SpecialChars: "!#$%&()*.?-@[]^_`{}|~+=",
 	},
 	ManufacturerHPE: {
 		SpecialChars: "~`!@#$%^&*()_-+={[}]|.?/",
 	},
 	ManufacturerLenovo: {
 		SpecialChars: ";@!$%-+=[]{}|/?~_",
 	},
-	"default": {
+	ManufacturerDefault: {
 		SpecialChars: "!@#$%&*()_-+=[]{}/?~|",
 	},
 }

Add to bmc/bmc.go:

ManufacturerDefault Manufacturer = "default"
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29a9e90 and 21edf29.

📒 Files selected for processing (9)
  • PROJECT
  • api/v1alpha1/zz_generated.deepcopy.go
  • bmc/redfish.go
  • cmd/manager/main.go
  • config/crd/kustomization.yaml
  • config/rbac/kustomization.yaml
  • config/samples/kustomization.yaml
  • docs/api-reference/api.md
  • internal/controller/suite_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • config/samples/kustomization.yaml
  • config/rbac/kustomization.yaml
  • cmd/manager/main.go
🧰 Additional context used
📓 Path-based instructions (3)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Code must follow standard Go formatting and idioms; use clear, explicit error handling with minimal global state
Use small, focused functions; introduce interfaces only when justified

Files:

  • bmc/redfish.go
  • api/v1alpha1/zz_generated.deepcopy.go
  • internal/controller/suite_test.go
**/api/**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

Implement defaulting and validation via webhooks (preferred) or OpenAPI schema annotations

Files:

  • api/v1alpha1/zz_generated.deepcopy.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

Tests must be deterministic and not rely on timing assumptions

Files:

  • internal/controller/suite_test.go
🧠 Learnings (10)
📚 Learning: 2026-01-14T19:27:09.972Z
Learnt from: Nuckal777
Repo: ironcore-dev/metal-operator PR: 471
File: internal/controller/bmcsettingsset_controller.go:233-236
Timestamp: 2026-01-14T19:27:09.972Z
Learning: In the ironcore-dev/metal-operator repository, BMCSettings.Spec.BMCRef is marked with +required kubebuilder marker, ensuring API-level validation that prevents nil values; defensive nil checks are not needed when iterating BMCSettings from List operations.

Applied to files:

  • docs/api-reference/api.md
  • config/crd/kustomization.yaml
  • PROJECT
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/api/**/*_types.go : Clearly mark and document deprecated fields in API types

Applied to files:

  • api/v1alpha1/zz_generated.deepcopy.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller_test.go : Write unit tests for reconcile logic and pure functions

Applied to files:

  • internal/controller/suite_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*.go : Include relevant identifiers in logs; emit events for meaningful user-facing state changes

Applied to files:

  • internal/controller/suite_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller_test.go : Use Envtest-based tests for controller behavior and API interactions

Applied to files:

  • internal/controller/suite_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Flag non-idempotent reconcile logic, Spec mutations during reconciliation, and custom patterns duplicating standard Kubernetes behavior

Applied to files:

  • internal/controller/suite_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*.go : Use structured logging with controller-runtime logger; avoid excessive log noise in hot paths

Applied to files:

  • internal/controller/suite_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Prevent hot loops caused by status-only changes triggering reconciliation

Applied to files:

  • internal/controller/suite_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/controllers/**/*_controller.go : Avoid silent failures; ensure logging includes sufficient context (namespaced name, resource identifiers)

Applied to files:

  • internal/controller/suite_test.go
📚 Learning: 2026-01-09T15:31:43.862Z
Learnt from: CR
Repo: ironcore-dev/metal-operator PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-09T15:31:43.862Z
Learning: Applies to **/api/**/*.go : Implement defaulting and validation via webhooks (preferred) or OpenAPI schema annotations

Applied to files:

  • PROJECT
🧬 Code graph analysis (3)
bmc/redfish.go (1)
bmc/bmc.go (4)
  • Manufacturer (19-19)
  • ManufacturerDell (22-22)
  • ManufacturerHPE (24-24)
  • ManufacturerLenovo (23-23)
api/v1alpha1/zz_generated.deepcopy.go (1)
api/v1alpha1/bmcuser_types.go (4)
  • BMCUser (54-60)
  • BMCUserList (65-69)
  • BMCUserSpec (12-34)
  • BMCUserStatus (37-47)
internal/controller/suite_test.go (2)
internal/controller/bmcuser_controller.go (1)
  • BMCUserReconciler (31-36)
bmc/redfish.go (1)
  • Options (43-53)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests
  • GitHub Check: Run test e2e
  • GitHub Check: Run test chart
  • GitHub Check: Run linter
🔇 Additional comments (11)
config/crd/kustomization.yaml (2)

19-19: LGTM!

The BMCUser CRD base resource is correctly added to the kustomization resources list, aligning with the new BMCUser API type introduced in this PR.


42-42: Clarify the commented CA injection patch naming.

The commented patch references cainjection_in_accounts.yaml, but there's no corresponding "accounts" CRD in the resources list. Should this be cainjection_in_bmcusers.yaml to match the new BMCUser CRD, or is this an unrelated placeholder?

internal/controller/suite_test.go (1)

133-133: LGTM!

Adding the GinkgoLogr logger to the manager improves test observability and debugging capabilities.

api/v1alpha1/zz_generated.deepcopy.go (1)

944-1059: LGTM - Auto-generated code.

The deepcopy implementations for BMCUser, BMCUserList, BMCUserSpec, and BMCUserStatus are correctly generated by controller-gen and properly handle all pointer fields with appropriate nil checks and deep copy semantics.

docs/api-reference/api.md (2)

22-22: LGTM!

The BMCUser resource is correctly added to the Resource Types list, maintaining alphabetical ordering within the BMC-prefixed resources.


674-733: LGTM - API documentation now includes field descriptions.

The BMCUser API documentation comprehensively covers:

  • BMCUserSpec: userName, roleID (with Redfish role examples), description, rotationPeriod, bmcSecretRef, bmcRef, enabled, and bmcControllerUser fields
  • BMCUserStatus: effectiveBMCSecretRef, lastRotation, passwordExpiration, and id fields

All fields now have meaningful descriptions that explain their purpose and behavior.

PROJECT (1)

32-34: LGTM!

The BMCSecret webhook configuration is correctly added, enabling validation webhooks for BMCSecret resources. This aligns with the webhook implementation in internal/webhook/v1alpha1/bmcsecret_webhook.go.

bmc/redfish.go (4)

84-89: LGTM!

Enabling ReuseConnections: true is a sensible optimization for reducing connection overhead when making multiple Redfish API calls.


808-818: LGTM!

The implementation correctly retrieves accounts with proper error handling.


1110-1164: Solid password generation implementation.

The algorithm correctly ensures complexity by mandating one character from each category (lowercase, uppercase, digit, special), then fills the remainder and shuffles using crypto-secure randomness. The length validation at line 1127 correctly rejects lengths below the minimum required for complexity.

Note: Line 1114 uses the same string literal "default" issue mentioned above.


1166-1184: LGTM!

Both helper functions correctly use crypto/rand for cryptographically secure randomness. The Fisher-Yates shuffle implementation is correct and unbiased.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread bmc/redfish.go
Comment thread internal/controller/suite_test.go Outdated
Comment on lines +266 to +275
Expect((&BMCUserReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
Insecure: true,
BMCOptions: bmc.Options{
PowerPollingInterval: 50 * time.Millisecond,
PowerPollingTimeout: 200 * time.Millisecond,
BasicAuth: true,
},
}).SetupWithManager(k8sManager)).To(Succeed())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check BMCUserReconciler struct definition
echo "=== BMCUserReconciler struct definition ==="
ast-grep --pattern $'type BMCUserReconciler struct {
  $$$
}'

# Check if ManagerNamespace is mentioned in bmcuser_controller.go
echo -e "\n=== ManagerNamespace usage in bmcuser_controller.go ==="
rg -n "ManagerNamespace" internal/controller/bmcuser_controller.go || echo "No ManagerNamespace found"

# Check other reconcilers setup in suite_test.go
echo -e "\n=== Other reconcilers setup in suite_test.go ==="
rg -n "Reconciler\{" internal/controller/suite_test.go -A 8 | head -80

Repository: ironcore-dev/metal-operator

Length of output: 3708


🏁 Script executed:

#!/bin/bash

# Check BMCUserReconciler's Reconcile method
echo "=== BMCUserReconciler Reconcile method ==="
rg -n "func.*Reconcile" internal/controller/bmcuser_controller.go -A 30

# Check if BMCSecret is accessed
echo -e "\n=== BMCSecret access in bmcuser_controller.go ==="
rg -n "BMCSecret\|Secret" internal/controller/bmcuser_controller.go -B 2 -A 2

# Check other reconcilers' Reconcile to see how they use ManagerNamespace
echo -e "\n=== How ManagerNamespace is used in another reconciler ==="
rg -n "ManagerNamespace" internal/controller/bmcsettings_controller.go -B 3 -A 3

Repository: ironcore-dev/metal-operator

Length of output: 14488


Add ManagerNamespace field to BMCUserReconciler struct and use it when creating/accessing namespaced resources.

The BMCUserReconciler is missing the ManagerNamespace field present in other reconcilers (BMCSettingsReconciler, BIOSVersionReconciler, BMCReconciler). More critically, the createSecret method creates BMCSecret objects without specifying a namespace, causing them to default to the "default" namespace instead of the manager's namespace. This mirrors how BMCSettingsReconciler uses ManagerNamespace when creating ServerMaintenance objects. The test setup should include this field for consistency and correctness.

🤖 Prompt for AI Agents
In `@internal/controller/suite_test.go` around lines 266 - 275, Add a
ManagerNamespace string field to the BMCUserReconciler struct and ensure
createSecret uses it when constructing BMCSecret objects (set the
Secret.ObjectMeta.Namespace = r.ManagerNamespace); in the test instantiate
BMCUserReconciler with ManagerNamespace set (e.g., k8sManager.GetNamespace() or
the same namespace used by other reconcilers) so SetupWithManager uses the
correct namespace; mirror the pattern used by
BMCSettingsReconciler/BIOSVersionReconciler/BMCReconciler and ensure any
test-created secrets specify that namespace.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@bmc/redfish.go`:
- Around line 1101-1114: The Dell special-character string in
manufacturerPasswordConfigs for ManufacturerDell contains an accidental double
percent ("% %") — update the SpecialChars value in the ManufacturerDell entry to
use a single '%' (i.e., change "$%%&" to "$%&"). Also replace the literal
"default" map key with a typed constant (e.g., define ManufacturerDefault of
type Manufacturer and use it as the key) so the default entry matches the
Manufacturer type used by other keys; ensure ManufacturerDefault is declared
before manufacturerPasswordConfigs.
- Around line 791-800: The Delete call may return a non-nil resp together with
err so the current code leaks resp.Body on the error path; update the block
around r.client.Delete(a.ODataID) to check if resp != nil immediately after the
call and close resp.Body (or defer closing) before returning the error, i.e.,
ensure resp.Body is closed both when err != nil and when returning nil;
reference r.client.Delete, resp, resp.Body and the existing deferred close to
implement this change.

Comment thread bmc/redfish.go
Comment on lines +791 to +800
resp, err := r.client.Delete(a.ODataID)
if err != nil {
return err
}
defer func(Body io.ReadCloser) {
if err = Body.Close(); err != nil {
log.Error(err, "failed to close response body")
}
}(resp.Body)
return nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential resource leak if Delete returns both response and error.

Some HTTP clients return a non-nil response alongside an error (e.g., to allow reading error details from the body). If r.client.Delete behaves this way, the response body won't be closed on the error path at line 793.

🐛 Proposed fix
 		if a.UserName == userName && a.ID == id {
 			resp, err := r.client.Delete(a.ODataID)
+			if resp != nil {
+				defer func(Body io.ReadCloser) {
+					if closeErr := Body.Close(); closeErr != nil {
+						log.Error(closeErr, "failed to close response body")
+					}
+				}(resp.Body)
+			}
 			if err != nil {
 				return err
 			}
-			defer func(Body io.ReadCloser) {
-				if err = Body.Close(); err != nil {
-					log.Error(err, "failed to close response body")
-				}
-			}(resp.Body)
 			return nil
 		}
🤖 Prompt for AI Agents
In `@bmc/redfish.go` around lines 791 - 800, The Delete call may return a non-nil
resp together with err so the current code leaks resp.Body on the error path;
update the block around r.client.Delete(a.ODataID) to check if resp != nil
immediately after the call and close resp.Body (or defer closing) before
returning the error, i.e., ensure resp.Body is closed both when err != nil and
when returning nil; reference r.client.Delete, resp, resp.Body and the existing
deferred close to implement this change.

Comment thread bmc/redfish.go
Comment on lines +1101 to +1114
var manufacturerPasswordConfigs = map[Manufacturer]ManufacturerPasswordConfig{
ManufacturerDell: {
SpecialChars: "!#$%%&()*.?-@[]^_`{}|~+=",
},
ManufacturerHPE: {
SpecialChars: "~`!@#$%^&*()_-+={[}]|.?/",
},
ManufacturerLenovo: {
SpecialChars: ";@!$%-+=[]{}|/?~_",
},
"default": {
SpecialChars: "!@#$%&*()_-+=[]{}/?~|",
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Double % in Dell special characters appears unintentional.

Line 1103 has $%%& which includes two consecutive % characters. In Go strings, % doesn't require escaping, so this likely should be a single %.

🐛 Proposed fix
 var manufacturerPasswordConfigs = map[Manufacturer]ManufacturerPasswordConfig{
 	ManufacturerDell: {
-		SpecialChars: "!#$%%&()*.?-@[]^_`{}|~+=",
+		SpecialChars: "!#$%&()*.?-@[]^_`{}|~+=",
 	},

Additionally, consider defining a ManufacturerDefault constant for the "default" key to maintain type consistency with other Manufacturer values.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var manufacturerPasswordConfigs = map[Manufacturer]ManufacturerPasswordConfig{
ManufacturerDell: {
SpecialChars: "!#$%%&()*.?-@[]^_`{}|~+=",
},
ManufacturerHPE: {
SpecialChars: "~`!@#$%^&*()_-+={[}]|.?/",
},
ManufacturerLenovo: {
SpecialChars: ";@!$%-+=[]{}|/?~_",
},
"default": {
SpecialChars: "!@#$%&*()_-+=[]{}/?~|",
},
}
var manufacturerPasswordConfigs = map[Manufacturer]ManufacturerPasswordConfig{
ManufacturerDell: {
SpecialChars: "!#$%&()*.?-@[]^_`{}|~+=",
},
ManufacturerHPE: {
SpecialChars: "~`!@#$%^&*()_-+={[}]|.?/",
},
ManufacturerLenovo: {
SpecialChars: ";@!$%-+=[]{}|/?~_",
},
"default": {
SpecialChars: "!@#$%&*()_-+=[]{}/?~|",
},
}
🤖 Prompt for AI Agents
In `@bmc/redfish.go` around lines 1101 - 1114, The Dell special-character string
in manufacturerPasswordConfigs for ManufacturerDell contains an accidental
double percent ("% %") — update the SpecialChars value in the ManufacturerDell
entry to use a single '%' (i.e., change "$%%&" to "$%&"). Also replace the
literal "default" map key with a typed constant (e.g., define
ManufacturerDefault of type Manufacturer and use it as the key) so the default
entry matches the Manufacturer type used by other keys; ensure
ManufacturerDefault is declared before manufacturerPasswordConfigs.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@config/crd/kustomization.yaml`:
- Line 42: The kustomization patch entry references a non-existent file
"cainjection_in_accounts.yaml"; update the patch path in
config/crd/kustomization.yaml so the commented line (currently "#- path:
patches/cainjection_in_accounts.yaml") instead references the correct BMCUsers
patch file "patches/cainjection_in_bmcusers.yaml" to match the BMCUsers resource
declared earlier (the resource at line 19) and follow the existing
cainjection_in_<resource>.yaml naming pattern.

In `@config/rbac/bmcuser_admin_role.yaml`:
- Around line 16-21: Replace the wildcard verbs entry under the RBAC rule for
apiGroups: metal.ironcore.dev and resources: bmcusers by enumerating explicit
actions to follow least-privilege; update the verbs list (the verbs field) to
include only create, delete, get, list, patch, update, and watch (and add any
other specific verbs you actually need) instead of '*' so policy scanners and
auditors accept the bmcusers rule.

In `@internal/webhook/v1alpha1/bmcsecret_webhook.go`:
- Around line 61-68: The immutability check only runs when the new object has
Immutable=true, allowing clients to unset Immutable and mutate Data/StringData;
update the logic in the webhook (referencing bmcsecret.Immutable,
oldSecret.Immutable, bmcsecret.Data, oldSecret.Data, bmcsecret.StringData,
oldSecret.StringData) to enforce immutability if the old resource was immutable
or the new one is set immutable — i.e., treat immutability as sticky by checking
if (oldSecret.Immutable != nil && *oldSecret.Immutable) || (bmcsecret.Immutable
!= nil && *bmcsecret.Immutable) before comparing Data/StringData and
additionally reject attempts to change Immutable from true to false (return an
error when oldSecret.Immutable is true and bmcsecret.Immutable is nil or false).

In `@REUSE.toml`:
- Line 26: The REUSE.toml annotation block currently lists "test/data/*.json"
but omits "test/data/*.yaml", leaving test/data/dns_record_template.yaml without
SPDX metadata; fix by either (A) adding the pattern "test/data/*.yaml" back into
the REUSE.toml annotation block so all YAML in test/data is covered, or (B)
adding SPDX headers to the YAML file itself (e.g., SPDX-FileCopyrightText and
SPDX-License-Identifier lines at the top of test/data/dns_record_template.yaml)
so the file has explicit licensing information.
♻️ Duplicate comments (12)
bmc/redfish_local.go (2)

68-76: Use direct map lookup instead of iteration.

The map is keyed by userName, so a direct lookup UnitTestMockUps.Accounts[userName] is more efficient than iterating over all values.

♻️ Suggested refactor
 func (r *RedfishLocalBMC) CreateOrUpdateAccount(
 	ctx context.Context, userName, role, password string, enabled bool,
 ) error {
-	for _, a := range UnitTestMockUps.Accounts {
-		if a.UserName == userName {
-			a.RoleID = role
-			a.UserName = userName
-			a.Enabled = enabled
-			a.Password = password
-			return nil
-		}
-	}
+	if a, ok := UnitTestMockUps.Accounts[userName]; ok {
+		a.RoleID = role
+		a.Enabled = enabled
+		a.Password = password
+		return nil
+	}
 	newAccount := redfish.ManagerAccount{

90-96: The id parameter is unused.

The DeleteAccount method accepts an id parameter (passed as user.Status.ID from the controller) but only uses userName for deletion. Consider either validating that id matches the account being deleted, or adding a comment documenting that id is intentionally ignored in the local mock implementation.

api/v1alpha1/bmcuser_types.go (1)

11-28: Add basic validation/defaulting for BMCUserSpec fields (UserName, RoleID, RotationPeriod, refs).

Consider OpenAPI/kubebuilder validation markers or a validating webhook to prevent invalid CRs (e.g., empty usernames/roles or zero/negative rotation periods).
As per coding guidelines, add validation/defaulting for API types.

bmc/redfish.go (2)

1132-1144: Dell special chars include a double “%”; consider a typed default key.

The Dell string has %% (likely unintended), and "default" is a magic string for a Manufacturer-typed map key.


804-812: Close response bodies even on the error path from DeleteAccount.

If r.client.Delete returns a non‑nil response alongside an error, the body is leaked. Close it before returning.

🐛 Suggested fix
 			resp, err := r.client.Delete(a.ODataID)
-			if err != nil {
-				return err
-			}
+			if resp != nil {
+				defer func(Body io.ReadCloser) {
+					if err = Body.Close(); err != nil {
+						log.Error(err, "failed to close response body")
+					}
+				}(resp.Body)
+			}
+			if err != nil {
+				return err
+			}
-			defer func(Body io.ReadCloser) {
-				if err = Body.Close(); err != nil {
-					log.Error(err, "failed to close response body")
-				}
-			}(resp.Body)
 			return nil
internal/webhook/v1alpha1/bmcsecret_webhook_test.go (1)

35-45: Use ptr.To(true) and fix “an BMCSecret” grammar.

Prefer ptr.To(true) over &[]bool{true}[0], and update “an BMCSecret” → “a BMCSecret.”

🔧 Suggested tweak
-			Immutable: &[]bool{true}[0],
+			Immutable: ptr.To(true),
 		}
-		By("Creating an BMCSecret")
+		By("Creating a BMCSecret")
cmd/manager/main.go (1)

550-563: Use “BMCUser” as the controller name in the setup error log.

This log label is inconsistent with other controllers and the reconciler name.

🔧 Suggested fix
-		setupLog.Error(err, "unable to create controller", "controller", "User")
+		setupLog.Error(err, "unable to create controller", "controller", "BMCUser")
internal/controller/bmcuser_controller_test.go (1)

126-135: Avoid explicit deletion of GC-owned effective secrets in tests.

These secrets are owner-referenced by the BMCUser, so explicit deletes can race with GC and intermittently fail. Prefer asserting deletion via apierrors.IsNotFound, and avoid ShouldNot(Succeed()) which can mask real errors.

🧪 Suggested pattern (apply similarly to the other blocks)
- Expect(k8sClient.Delete(ctx, effectiveSecret)).To(Succeed())
- Eventually(Get(effectiveSecret)).Should(Satisfy(apierrors.IsNotFound))
+ Eventually(Get(effectiveSecret)).Should(Satisfy(apierrors.IsNotFound))
- Eventually(Get(user)).ShouldNot(Succeed())
+ Eventually(Get(user)).Should(Satisfy(apierrors.IsNotFound))

Also applies to: 235-241, 280-282, 320-328, 388-395

internal/controller/bmcuser_controller.go (4)

106-118: Respect spec.enabled when creating/updating BMC accounts.

All CreateOrUpdateAccount calls hardcode true, so disabled users can’t be represented. Use user.Spec.Enabled (or its default) consistently.

✅ Suggested change
- if err = bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, password, true); err != nil {
+ if err = bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, password, user.Spec.Enabled); err != nil {
    return ctrl.Result{}, fmt.Errorf("failed to create or update BMC account with new password: %w", err)
}
- if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, newPassword, true); err != nil {
+ if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, newPassword, user.Spec.Enabled); err != nil {
    return ctrl.Result{}, fmt.Errorf("failed to create or update BMC account with new password: %w", err)
}
- if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, newPassword, true); err != nil {
+ if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, newPassword, user.Spec.Enabled); err != nil {
    return fmt.Errorf("failed to create or update BMC account with new password: %w", err)
}
- if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, password, true); err != nil {
+ if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, password, user.Spec.Enabled); err != nil {
    return fmt.Errorf("failed to create or update BMC account with new password: %w", err)
}

Also applies to: 186-200, 224-227, 238-241


267-271: Don’t mutate spec.bmcSecretRef in the controller.

This is a user-owned field; controllers should record observed state in status (e.g., EffectiveBMCSecretRef) and update reconcile logic to prefer status when spec.bmcSecretRef is nil. Based on learnings, Spec fields must remain user-owned while Status is controller-owned.


345-359: Close the BMC client after the connection test.

CreateBMCClient likely opens a session; without a logout/close, repeated checks can leak sessions.

🔧 Suggested fix
- _, err = bmcutils.CreateBMCClient(ctx, r.Client, protocolScheme, bmcObj.Spec.Protocol.Name, address, bmcObj.Spec.Protocol.Port, secret, r.BMCOptions)
+ bmcClient, err := bmcutils.CreateBMCClient(ctx, r.Client, protocolScheme, bmcObj.Spec.Protocol.Name, address, bmcObj.Spec.Protocol.Port, secret, r.BMCOptions)
  if err != nil {
      if httpErr, ok := err.(*common.Error); ok {
          if httpErr.HTTPReturnedStatusCode == 401 || httpErr.HTTPReturnedStatusCode == 403 {
              return true, nil
          }
      }
      return false, fmt.Errorf("failed to create BMC client: %w", err)
  }
+ defer bmcClient.Logout()
  return false, nil

245-266: CreateOrPatch with GenerateName will fail.

controllerutil.CreateOrPatch requires a stable metadata.name to fetch and patch the object. Since GenerateName leaves Name empty until the API server assigns it, the initial GET will fail. Use r.Create instead for generated names.

🛠️ Suggested fix
- op, err := controllerutil.CreateOrPatch(ctx, r.Client, secret, func() error {
-     if err := controllerutil.SetControllerReference(user, secret, r.Scheme); err != nil {
-         return fmt.Errorf("failed to set controller reference for BMCSecret: %w", err)
-     }
-     return nil
- })
- if err != nil {
-     return fmt.Errorf("failed to create or patch BMCSecret: %w", err)
- }
- log.V(1).Info("BMCSecret created or patched", "BMCSecret", secret.Name, "Operation", op)
+ if err := controllerutil.SetControllerReference(user, secret, r.Scheme); err != nil {
+     return fmt.Errorf("failed to set controller reference for BMCSecret: %w", err)
+ }
+ if err := r.Create(ctx, secret); err != nil {
+     return fmt.Errorf("failed to create BMCSecret: %w", err)
+ }
+ log.V(1).Info("BMCSecret created", "BMCSecret", secret.Name)
🧹 Nitpick comments (1)
internal/controller/bmcuser_controller_test.go (1)

243-282: Make the rotation-period test deterministic.

The 1s rotation window with a 4s Eventually timeout is wall‑clock dependent and can be flaky under load. Consider faking time or setting Status.LastRotation to a past timestamp and triggering reconcile immediately (or reuse the annotation-based rotation path) to avoid timing assumptions. As per coding guidelines, tests must be deterministic and avoid timing assumptions.

Comment thread config/crd/kustomization.yaml
Comment on lines +16 to +21
- apiGroups:
- metal.ironcore.dev
resources:
- bmcusers
verbs:
- '*'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid wildcard verbs to maintain least‑privilege RBAC.

Using '*' on bmcusers broadens permissions unnecessarily and will keep tripping policy scanners. Prefer explicit verbs (create, delete, get, list, patch, update, watch) unless there’s a concrete need for wildcard.

🔧 Suggested change
 rules:
 - apiGroups:
   - metal.ironcore.dev
   resources:
   - bmcusers
   verbs:
-  - '*'
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- apiGroups:
- metal.ironcore.dev
resources:
- bmcusers
verbs:
- '*'
- apiGroups:
- metal.ironcore.dev
resources:
- bmcusers
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
🤖 Prompt for AI Agents
In `@config/rbac/bmcuser_admin_role.yaml` around lines 16 - 21, Replace the
wildcard verbs entry under the RBAC rule for apiGroups: metal.ironcore.dev and
resources: bmcusers by enumerating explicit actions to follow least-privilege;
update the verbs list (the verbs field) to include only create, delete, get,
list, patch, update, and watch (and add any other specific verbs you actually
need) instead of '*' so policy scanners and auditors accept the bmcusers rule.

Comment on lines +61 to +68
if bmcsecret.Immutable != nil && *bmcsecret.Immutable {
if !reflect.DeepEqual(bmcsecret.Data, oldSecret.Data) {
return nil, fmt.Errorf("data field is immutable and cannot be updated")
}
if !reflect.DeepEqual(bmcsecret.StringData, oldSecret.StringData) {
return nil, fmt.Errorf("stringData field is immutable and cannot be updated")
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Immutability can be bypassed if Immutable is unset on update.

The check only runs when the new object has Immutable=true, so a client can flip it to false and mutate Data/StringData. If immutability should be sticky, gate on the old value too (and optionally reject unsetting).

🔒 Suggested guard
- if bmcsecret.Immutable != nil && *bmcsecret.Immutable {
+ if (oldSecret.Immutable != nil && *oldSecret.Immutable) || (bmcsecret.Immutable != nil && *bmcsecret.Immutable) {
+     if oldSecret.Immutable != nil && *oldSecret.Immutable && (bmcsecret.Immutable == nil || !*bmcsecret.Immutable) {
+         return nil, fmt.Errorf("immutable field cannot be unset")
+     }
      if !reflect.DeepEqual(bmcsecret.Data, oldSecret.Data) {
          return nil, fmt.Errorf("data field is immutable and cannot be updated")
      }
      if !reflect.DeepEqual(bmcsecret.StringData, oldSecret.StringData) {
          return nil, fmt.Errorf("stringData field is immutable and cannot be updated")
      }
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if bmcsecret.Immutable != nil && *bmcsecret.Immutable {
if !reflect.DeepEqual(bmcsecret.Data, oldSecret.Data) {
return nil, fmt.Errorf("data field is immutable and cannot be updated")
}
if !reflect.DeepEqual(bmcsecret.StringData, oldSecret.StringData) {
return nil, fmt.Errorf("stringData field is immutable and cannot be updated")
}
}
if (oldSecret.Immutable != nil && *oldSecret.Immutable) || (bmcsecret.Immutable != nil && *bmcsecret.Immutable) {
if oldSecret.Immutable != nil && *oldSecret.Immutable && (bmcsecret.Immutable == nil || !*bmcsecret.Immutable) {
return nil, fmt.Errorf("immutable field cannot be unset")
}
if !reflect.DeepEqual(bmcsecret.Data, oldSecret.Data) {
return nil, fmt.Errorf("data field is immutable and cannot be updated")
}
if !reflect.DeepEqual(bmcsecret.StringData, oldSecret.StringData) {
return nil, fmt.Errorf("stringData field is immutable and cannot be updated")
}
}
🤖 Prompt for AI Agents
In `@internal/webhook/v1alpha1/bmcsecret_webhook.go` around lines 61 - 68, The
immutability check only runs when the new object has Immutable=true, allowing
clients to unset Immutable and mutate Data/StringData; update the logic in the
webhook (referencing bmcsecret.Immutable, oldSecret.Immutable, bmcsecret.Data,
oldSecret.Data, bmcsecret.StringData, oldSecret.StringData) to enforce
immutability if the old resource was immutable or the new one is set immutable —
i.e., treat immutability as sticky by checking if (oldSecret.Immutable != nil &&
*oldSecret.Immutable) || (bmcsecret.Immutable != nil && *bmcsecret.Immutable)
before comparing Data/StringData and additionally reject attempts to change
Immutable from true to false (return an error when oldSecret.Immutable is true
and bmcsecret.Immutable is nil or false).

Comment thread REUSE.toml Outdated
"internal/cmd/visualizer/index.html",
"test/data/*.json",
"test/data/*.yaml"
"test/data/*.json"
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, examine REUSE.toml to understand the context
echo "=== REUSE.toml content ==="
cat REUSE.toml

echo ""
echo "=== YAML/YML files in test/data/ ==="
fd -e yaml -e yml . test/data/ 2>/dev/null || echo "No YAML files found"

echo ""
echo "=== Check SPDX headers in found YAML files ==="
for f in $(fd -e yaml -e yml . test/data/ 2>/dev/null); do
  echo "--- $f ---"
  head -10 "$f"
done

Repository: ironcore-dev/metal-operator

Length of output: 1674


Add SPDX licensing for test/data/*.yaml files.

The test/data/*.yaml pattern was removed from the annotation block, but test/data/dns_record_template.yaml exists without SPDX headers. Either add the pattern back to the first annotation block or add SPDX headers directly to the YAML file.

🤖 Prompt for AI Agents
In `@REUSE.toml` at line 26, The REUSE.toml annotation block currently lists
"test/data/*.json" but omits "test/data/*.yaml", leaving
test/data/dns_record_template.yaml without SPDX metadata; fix by either (A)
adding the pattern "test/data/*.yaml" back into the REUSE.toml annotation block
so all YAML in test/data is covered, or (B) adding SPDX headers to the YAML file
itself (e.g., SPDX-FileCopyrightText and SPDX-License-Identifier lines at the
top of test/data/dns_record_template.yaml) so the file has explicit licensing
information.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the yaml exclusion should be added back again.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afritzler, understood! Adding "test/data/*.yaml" back to the REUSE.toml annotation block will ensure that all YAML files in test/data/ are properly covered by SPDX licensing metadata.

Would you like me to plan the code change to restore this line, or will you handle it as part of your updates to this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is already. this is outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@internal/controller/bmcuser_controller.go`:
- Around line 379-384: The log "Successfully deleted BMC account and removed
finalizer for User" is misleading because the finalizer is removed in
reconcileExists, not here; update the log in the block that calls
bmcClient.DeleteAccount (the code that returns fmt.Errorf on failure) to only
state successful account deletion (e.g., "Successfully deleted BMC account for
User") and remove any mention of finalizer removal, keeping references to
user.Name and the bmcClient.DeleteAccount call to locate the change.

In `@internal/webhook/v1alpha1/bmcsecret_webhook.go`:
- Around line 61-68: The immutability check is using the new object
(bmcsecret.Immutable) allowing a client to flip Immutable=false and change data
in one update; change the logic to inspect the existing object
(oldSecret.Immutable) instead and, if oldSecret.Immutable is non-nil and true,
reject any update that modifies Immutable, Data, or StringData. Update the check
in the validation function (the block referencing bmcsecret.Immutable,
bmcsecret.Data, bmcsecret.StringData and oldSecret) to use oldSecret.Immutable
and return an error if the incoming bmcsecret attempts to change Immutable from
true or alter Data/StringData when oldSecret.Immutable was true.
♻️ Duplicate comments (12)
api/v1alpha1/bmcuser_types.go (1)

12-29: Add validation markers to BMCUserSpec and consider marking BMCRef as required.

The BMCUserSpec lacks validation markers for fields like UserName and RoleID, which should be non-empty. Additionally, BMCRef is currently optional (pointer type) but is required for the controller to function—similar to BMCSettings.BMCRef, which is marked as +required.

To align with the codebase patterns, add:

  • +kubebuilder:validation:Required and +kubebuilder:validation:MinLength=1 on UserName and RoleID
  • +kubebuilder:validation:Required on BMCRef

Alternatively, implement these validations via a webhook (preferred per coding guidelines). Currently, there is no bmcuser_webhook.go unlike similar types (BMCSettings, BMCSecret).

internal/webhook/v1alpha1/bmcsecret_webhook_test.go (2)

35-35: Use ptr.To(true) for consistency.

Line 53 correctly uses ptr.To(false), but this line uses the awkward &[]bool{true}[0] pattern.

♻️ Suggested fix
-			Immutable: &[]bool{true}[0],
+			Immutable: ptr.To(true),

37-37: Grammar: use "a" before "BMCSecret".

"BMC" is pronounced with a consonant sound, so it should be "a BMCSecret" not "an BMCSecret". Same applies to lines 44 and 51.

📝 Suggested fix
-		By("Creating an BMCSecret")
+		By("Creating a BMCSecret")

Also update lines 44 and 51 similarly.

cmd/manager/main.go (1)

550-563: Fix controller name in setup error log.
The reconciler is BMCUserReconciler, but the log says "User", which is inconsistent with other controller names.

🔧 Proposed fix
 	}).SetupWithManager(mgr); err != nil {
-		setupLog.Error(err, "unable to create controller", "controller", "User")
+		setupLog.Error(err, "unable to create controller", "controller", "BMCUser")
 		os.Exit(1)
 	}
bmc/redfish_local.go (1)

55-95: Guard mock account map access and honor id parameter.
UnitTestMockUps.Accounts is accessed/mutated without synchronization, and DeleteAccount ignores the id argument while CreateOrUpdateAccount scans all entries. Consider adding a mutex to UnitTestMockUps, using direct lookup by userName, and validating id when provided.

internal/controller/bmcuser_controller_test.go (2)

127-134: Avoid explicit deletion of effective secret; assert GC instead.
Deleting the effective secret directly can be flaky if GC already removed it. Prefer asserting NotFound.

🔧 Suggested adjustment
-	Expect(k8sClient.Delete(ctx, effectiveSecret)).To(Succeed())
-	Eventually(Get(effectiveSecret)).Should(Satisfy(apierrors.IsNotFound))
+	Eventually(Get(effectiveSecret)).Should(Satisfy(apierrors.IsNotFound))

317-328: Use explicit NotFound checks instead of ShouldNot(Succeed()).
ShouldNot(Succeed()) can hide real errors. Use apierrors.IsNotFound and avoid explicit secret deletes in GC scenarios.

🔧 Suggested adjustment
-	Eventually(Get(user)).ShouldNot(Succeed())
+	Eventually(Get(user)).Should(Satisfy(apierrors.IsNotFound))

-	Expect(k8sClient.Delete(ctx, effectiveSecret)).To(Succeed())
+	Eventually(Get(effectiveSecret)).Should(Satisfy(apierrors.IsNotFound))
bmc/redfish.go (2)

804-812: Close delete response body even when Delete returns an error.
If Delete returns a non‑nil response alongside an error, the current code leaks resp.Body.

🔧 Proposed fix
-			resp, err := r.client.Delete(a.ODataID)
-			if err != nil {
-				return err
-			}
-			defer func(Body io.ReadCloser) {
+			resp, err := r.client.Delete(a.ODataID)
+			if resp != nil {
+				defer func(Body io.ReadCloser) {
 					if err = Body.Close(); err != nil {
 						log.Error(err, "failed to close response body")
 					}
-			}(resp.Body)
+				}(resp.Body)
+			}
+			if err != nil {
+				return err
+			}
 			return nil

1132-1144: Fix Dell special‑char typo and use a typed default key.
The Dell character set contains %% and the "default" key isn’t typed as Manufacturer.

🔧 Proposed fix
 var manufacturerPasswordConfigs = map[Manufacturer]ManufacturerPasswordConfig{
 	ManufacturerDell: {
-		SpecialChars: "!#$%%&()*.?-@[]^_`{}|~+=",
+		SpecialChars: "!#$%&()*.?-@[]^_`{}|~+=",
 	},
@@
-	"default": {
+	Manufacturer("default"): {
 		SpecialChars: "!@#$%&*()_-+=[]{}/?~|",
 	},
 }
internal/controller/bmcuser_controller.go (3)

106-114: Use user.Spec.Enabled for account enablement.
The enabled flag is hard‑coded to true, which ignores the user’s desired state. Apply the same change to all CreateOrUpdateAccount calls in this file (Lines 112, 198, 225, 239).

🔧 Proposed fix (pattern)
-	if err = bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, password, true); err != nil {
+	if err = bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, password, user.Spec.Enabled); err != nil {
 		return ctrl.Result{}, fmt.Errorf("failed to create or update BMC account with new password: %w", err)
 	}

245-271: Do not mutate Spec when creating the secret.
createSecret patches Spec.BMCSecretRef, which should be user‑owned. Use status (EffectiveBMCSecretRef) instead.

🔧 Suggested direction
-	userBase := user.DeepCopy()
-	user.Spec.BMCSecretRef = &v1.LocalObjectReference{Name: secret.Name}
-	if err := r.Patch(ctx, user, client.MergeFrom(userBase)); err != nil {
-		return fmt.Errorf("failed to patch User status with effective BMCSecretRef: %w", err)
-	}
+	if err := r.setEffectiveSecretRef(ctx, log, user, secret); err != nil {
+		return fmt.Errorf("failed to patch User status with effective BMCSecretRef: %w", err)
+	}

As per coding guidelines, spec fields must remain user‑owned; controller state belongs in status.


345-358: Close the BMC client after connection test.
bmcConnectionTest creates a client but never logs out, which can leak sessions.

🔧 Proposed fix
-	_, err = bmcutils.CreateBMCClient(ctx, r.Client, protocolScheme, bmcObj.Spec.Protocol.Name, address, bmcObj.Spec.Protocol.Port, secret, r.BMCOptions)
+	bmcClient, err := bmcutils.CreateBMCClient(ctx, r.Client, protocolScheme, bmcObj.Spec.Protocol.Name, address, bmcObj.Spec.Protocol.Port, secret, r.BMCOptions)
 	if err != nil {
 		if httpErr, ok := err.(*common.Error); ok {
 			if httpErr.HTTPReturnedStatusCode == 401 || httpErr.HTTPReturnedStatusCode == 403 {
 				return true, nil
 			}
 		}
 		return false, fmt.Errorf("failed to create BMC client: %w", err)
 	}
+	defer bmcClient.Logout()
 	return false, nil
🧹 Nitpick comments (5)
config/rbac/bmcuser_admin_role.yaml (1)

15-27: Wildcard verb usage is appropriate for admin role, but status permissions may be incomplete.

The static analysis hint (CKV_K8S_49) flags the wildcard verb usage, but this is intentional for an admin role designed to delegate permissions to other users.

However, consider whether the bmcusers/status subresource should also have broader permissions (e.g., patch, update) for consistency with the admin role's purpose. Currently, admins can do everything on the main resource but only get the status.

🔧 Suggested enhancement for status permissions
 - apiGroups:
   - metal.ironcore.dev
   resources:
   - bmcusers/status
   verbs:
   - get
+  - patch
+  - update
api/v1alpha1/bmcuser_types.go (1)

44-55: Consider whether Spec should be required.

The Spec field uses omitempty (line 53), which means an empty spec is valid. If the BMCUser resource requires a spec to be meaningful, consider removing omitempty or adding validation to ensure required fields are present.

🔧 Optional: Make spec required
-	Spec   BMCUserSpec   `json:"spec,omitempty"`
+	Spec   BMCUserSpec   `json:"spec"`
internal/webhook/v1alpha1/bmcsecret_webhook.go (1)

32-34: Unused Client field.

The Client field is declared but never used in any of the validation methods. Either remove it or wire it up if it's intended for future use.

♻️ Suggested fix
 type BMCSecretCustomValidator struct {
-	Client client.Client
 }
bmc/mockup.go (1)

114-146: Good test fixture for account management.

The mock accounts provide varied roles (ReadOnly, Administrator) for comprehensive test coverage of account management operations.

Minor nit: Line 129 has an inconsistent blank line between Entity block and UserName that doesn't appear in other account entries.

♻️ Optional: remove inconsistent blank line
 		"admin": {
 			Entity: common.Entity{
 				ID: "1",
 			},
-
 			UserName: "admin",
internal/controller/bmcuser_controller_test.go (1)

243-272: Make rotation test deterministic (avoid real time waits).
The test relies on wall-clock delays (1s period + Eventually("4s")), which can be flaky under load. Consider injecting a fake clock or triggering rotation via annotation to remove timing dependence.

As per coding guidelines, tests must be deterministic and not rely on timing assumptions.

Comment on lines +379 to +384
log.Info("Deleting BMC account for User", "User", user.Name)
if err := bmcClient.DeleteAccount(ctx, user.Spec.UserName, user.Status.ID); err != nil {
return fmt.Errorf("failed to delete BMC account: %w", err)
}
log.Info("Successfully deleted BMC account and removed finalizer for User", "User", user.Name)
return nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Log message is misleading about finalizer removal.
This log claims the finalizer was removed here, but removal happens in reconcileExists. Consider updating the message to only mention account deletion.

🔧 Proposed fix
-	log.Info("Successfully deleted BMC account and removed finalizer for User", "User", user.Name)
+	log.Info("Successfully deleted BMC account for User", "User", user.Name)
🤖 Prompt for AI Agents
In `@internal/controller/bmcuser_controller.go` around lines 379 - 384, The log
"Successfully deleted BMC account and removed finalizer for User" is misleading
because the finalizer is removed in reconcileExists, not here; update the log in
the block that calls bmcClient.DeleteAccount (the code that returns fmt.Errorf
on failure) to only state successful account deletion (e.g., "Successfully
deleted BMC account for User") and remove any mention of finalizer removal,
keeping references to user.Name and the bmcClient.DeleteAccount call to locate
the change.

Comment on lines +61 to +68
if bmcsecret.Immutable != nil && *bmcsecret.Immutable {
if !reflect.DeepEqual(bmcsecret.Data, oldSecret.Data) {
return nil, fmt.Errorf("data field is immutable and cannot be updated")
}
if !reflect.DeepEqual(bmcsecret.StringData, oldSecret.StringData) {
return nil, fmt.Errorf("stringData field is immutable and cannot be updated")
}
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Immutability check uses wrong object — bypass possible.

The check uses bmcsecret.Immutable (new object) instead of oldSecret.Immutable. A user can bypass immutability by setting Immutable=false and changing Data in the same update request.

Following Kubernetes Secret semantics, once Immutable is true, the field itself and the data should be locked.

🐛 Proposed fix
-	if bmcsecret.Immutable != nil && *bmcsecret.Immutable {
+	if oldSecret.Immutable != nil && *oldSecret.Immutable {
+		// Once immutable, the Immutable field itself cannot be changed
+		if bmcsecret.Immutable == nil || !*bmcsecret.Immutable {
+			return nil, fmt.Errorf("immutable field cannot be changed once set to true")
+		}
 		if !reflect.DeepEqual(bmcsecret.Data, oldSecret.Data) {
 			return nil, fmt.Errorf("data field is immutable and cannot be updated")
 		}
 		if !reflect.DeepEqual(bmcsecret.StringData, oldSecret.StringData) {
 			return nil, fmt.Errorf("stringData field is immutable and cannot be updated")
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if bmcsecret.Immutable != nil && *bmcsecret.Immutable {
if !reflect.DeepEqual(bmcsecret.Data, oldSecret.Data) {
return nil, fmt.Errorf("data field is immutable and cannot be updated")
}
if !reflect.DeepEqual(bmcsecret.StringData, oldSecret.StringData) {
return nil, fmt.Errorf("stringData field is immutable and cannot be updated")
}
}
if oldSecret.Immutable != nil && *oldSecret.Immutable {
// Once immutable, the Immutable field itself cannot be changed
if bmcsecret.Immutable == nil || !*bmcsecret.Immutable {
return nil, fmt.Errorf("immutable field cannot be changed once set to true")
}
if !reflect.DeepEqual(bmcsecret.Data, oldSecret.Data) {
return nil, fmt.Errorf("data field is immutable and cannot be updated")
}
if !reflect.DeepEqual(bmcsecret.StringData, oldSecret.StringData) {
return nil, fmt.Errorf("stringData field is immutable and cannot be updated")
}
}
🤖 Prompt for AI Agents
In `@internal/webhook/v1alpha1/bmcsecret_webhook.go` around lines 61 - 68, The
immutability check is using the new object (bmcsecret.Immutable) allowing a
client to flip Immutable=false and change data in one update; change the logic
to inspect the existing object (oldSecret.Immutable) instead and, if
oldSecret.Immutable is non-nil and true, reject any update that modifies
Immutable, Data, or StringData. Update the check in the validation function (the
block referencing bmcsecret.Immutable, bmcsecret.Data, bmcsecret.StringData and
oldSecret) to use oldSecret.Immutable and return an error if the incoming
bmcsecret attempts to change Immutable from true or alter Data/StringData when
oldSecret.Immutable was true.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stefanhipfel that is actually a good point here. Shouldn't we check the immutability of the old object instead the new one?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Member

@afritzler afritzler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some small nitpicks from my side.

Comment on lines +58 to +60
if modified, err := clientutils.PatchEnsureNoFinalizer(ctx, r.Client, user, BMCUserFinalizer); err != nil || modified {
return ctrl.Result{}, err
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this into the delete() method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

return ctrl.Result{}, nil
}

func (r *BMCUserReconciler) handleMissingBMCSecretRef(ctx context.Context, log logr.Logger, bmcClient bmc.BMC, user *metalv1alpha1.BMCUser, bmcObj *metalv1alpha1.BMC) error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this method ensureBMCSecretForUser()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

log.Info("Creating BMCSecret for User", "User", user.Name)
secret := &metalv1alpha1.BMCSecret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: user.Name + "-bmcsecret-",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we drop the -bmcsecret- as the kind already is clear on what this object is?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

metalv1alpha1.BMCSecretUsernameKeyName: []byte(user.Spec.UserName),
metalv1alpha1.BMCSecretPasswordKeyName: []byte(password),
},
Immutable: &[]bool{true}[0], // Make the secret immutable
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use the ptr package here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return nil
}

func (r *BMCUserReconciler) createSecret(ctx context.Context, log logr.Logger, user *metalv1alpha1.BMCUser, password string) error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would actually split this method into:

  1. Creating the BMCSecret for a User and returning the secret object -> hence renaming it into createBMCSecretForUser
  2. And update the BMCUsers effective secret in the status outside e.g. in the ensureBMCSecretForUser method

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting the effective secret is done at a later point, after everything was done without errors

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be a setBMCSecretRef function, which just sets the new bmcSecretRef

Comment thread internal/controller/bmcuser_controller.go
Copy link
Copy Markdown
Member

@afritzler afritzler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to address the RBAC issues here.

Comment thread internal/controller/bmcuser_controller.go
Copy link
Copy Markdown
Contributor

@Nuckal777 Nuckal777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to go from my side. I annotated some stuff for cleanup.

Comment thread bmc/redfish.go Outdated
Comment on lines +808 to +812
defer func(Body io.ReadCloser) {
if err = Body.Close(); err != nil {
log.Error(err, "failed to close response body")
}
}(resp.Body)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferring here seems superfluous. The body could closed directly, instead.

Comment thread bmc/redfish.go Outdated
return fmt.Errorf("account %s not found", userName)
}

func (r *RedfishBMC) GetAccountService(ctx context.Context) (*redfish.AccountService, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctx appears unused.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true. We got unused ctx in many more functions as well in redfish

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering why linter did not report it

Comment thread bmc/redfish.go Outdated
return service, nil
}

func (r *RedfishBMC) GetAccounts(ctx context.Context) ([]*redfish.ManagerAccount, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctx appears unused.

validator = BMCSecretCustomValidator{
Client: k8sClient,
}
Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be deleted, validator is initialized 3 lines above and cannot be nil, because it's a struct.

@afritzler afritzler changed the title Implements User controller Add BMCUser types and controller Jan 28, 2026
@afritzler afritzler added enhancement New feature or request minor labels Jan 28, 2026
@afritzler afritzler changed the title Add BMCUser types and controller Add BMCUser type and controller Jan 28, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/controller/bmcuser_controller.go`:
- Around line 186-192: The code passes accountService.MaxPasswordLength directly
into bmc.GenerateSecurePassword which can be 0/unspecified and causes failures;
update the reconciliation logic (around GetAccountService() usage and where
newPassword is generated) to defensively compute an effective length: use
accountService.MinPasswordLength if present and >=4, otherwise fall back to a
sane default (e.g., 16), and cap/validate the chosen length to satisfy
bmc.GenerateSecurePassword's requirement (length >= 4) before calling
bmc.GenerateSecurePassword(bmc.Manufacturer(bmcObj.Status.Manufacturer),
effectiveLength); ensure the same fix is applied to the other occurrence around
lines 216-223.

Comment on lines +186 to +192
accountService, err := bmcClient.GetAccountService()
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to get account service: %w", err)
}
newPassword, err := bmc.GenerateSecurePassword(bmc.Manufacturer(bmcObj.Status.Manufacturer), accountService.MaxPasswordLength)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to generate new password for BMC user %s: %w", user.Name, err)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "*.go" -path "*/bmcuser*" | head -20

Repository: ironcore-dev/metal-operator

Length of output: 195


🏁 Script executed:

ls -la internal/controller/ | head -20

Repository: ironcore-dev/metal-operator

Length of output: 1534


🏁 Script executed:

cat -n internal/controller/bmcuser_controller.go | head -100

Repository: ironcore-dev/metal-operator

Length of output: 4414


🏁 Script executed:

cat -n internal/controller/bmcuser_controller.go | sed -n '180,230p'

Repository: ironcore-dev/metal-operator

Length of output: 2904


🏁 Script executed:

grep -n "GenerateSecurePassword" internal/controller/bmcuser_controller.go

Repository: ironcore-dev/metal-operator

Length of output: 334


🏁 Script executed:

grep -r "GenerateSecurePassword" bmc/ --include="*.go" -A 5

Repository: ironcore-dev/metal-operator

Length of output: 491


🏁 Script executed:

cat -n bmc/redfish.go | grep -A 30 "func GenerateSecurePassword"

Repository: ironcore-dev/metal-operator

Length of output: 1527


🏁 Script executed:

cat -n bmc/redfish.go | sed -n '1,100p'

Repository: ironcore-dev/metal-operator

Length of output: 3754


🏁 Script executed:

cat -n bmc/redfish.go | sed -n '1160,1200p'

Repository: ironcore-dev/metal-operator

Length of output: 1446


🏁 Script executed:

grep -n "GetAccountService" bmc/ --include="*.go" -A 10

Repository: ironcore-dev/metal-operator

Length of output: 97


🏁 Script executed:

grep -r "MaxPasswordLength" . --include="*.go"

Repository: ironcore-dev/metal-operator

Length of output: 414


🏁 Script executed:

grep -rn "GetAccountService" bmc/ --include="*.go" -A 10

Repository: ironcore-dev/metal-operator

Length of output: 1158


🏁 Script executed:

cat -n bmc/redfish.go | grep -A 20 "func.*GetAccountService"

Repository: ironcore-dev/metal-operator

Length of output: 929


🏁 Script executed:

grep -rn "MinPasswordLength" . --include="*.go"

Repository: ironcore-dev/metal-operator

Length of output: 53


🏁 Script executed:

grep -n "accountService\." internal/controller/bmcuser_controller.go

Repository: ironcore-dev/metal-operator

Length of output: 334


🏁 Script executed:

cat -n internal/controller/bmcuser_controller_test.go | head -100

Repository: ironcore-dev/metal-operator

Length of output: 3751


🏁 Script executed:

grep -n "MaxPasswordLength" internal/controller/bmcuser_controller_test.go

Repository: ironcore-dev/metal-operator

Length of output: 53


🏁 Script executed:

grep -n "accountService\|AccountService" internal/controller/bmcuser_controller_test.go | head -20

Repository: ironcore-dev/metal-operator

Length of output: 53


🏁 Script executed:

go mod graph | grep gofish

Repository: ironcore-dev/metal-operator

Length of output: 309


🏁 Script executed:

cat -n go.mod | grep gofish

Repository: ironcore-dev/metal-operator

Length of output: 115


🏁 Script executed:

find . -name "go.mod" -exec cat {} \; | grep -A 2 -B 2 gofish

Repository: ironcore-dev/metal-operator

Length of output: 234


🏁 Script executed:

cat -n internal/controller/bmcuser_controller_test.go | sed -n '79,200p'

Repository: ironcore-dev/metal-operator

Length of output: 5263


🏁 Script executed:

grep -rn "mock\|Mock" internal/controller/bmcuser_controller_test.go | head -20

Repository: ironcore-dev/metal-operator

Length of output: 390


🏁 Script executed:

grep -n "GenerateSecurePassword" bmc/redfish_test.go

Repository: ironcore-dev/metal-operator

Length of output: 123


🏁 Script executed:

grep -rn "UnitTestMockUps\|MockUps" bmc/ --include="*.go" | head -30

Repository: ironcore-dev/metal-operator

Length of output: 2266


🏁 Script executed:

grep -rn "default.*password\|password.*default" bmc/ --include="*.go" | head -20

Repository: ironcore-dev/metal-operator

Length of output: 53


🏁 Script executed:

cat -n bmc/redfish.go | sed -n '1140,1165p'

Repository: ironcore-dev/metal-operator

Length of output: 1323


🏁 Script executed:

cat -n bmc/mockup.go | sed -n '1,100p'

Repository: ironcore-dev/metal-operator

Length of output: 3301


🏁 Script executed:

grep -rn "AccountService\|MaxPasswordLength" bmc/mockup.go

Repository: ironcore-dev/metal-operator

Length of output: 53


🏁 Script executed:

cat -n internal/controller/bmcuser_controller.go | sed -n '150,180p'

Repository: ironcore-dev/metal-operator

Length of output: 1812


🏁 Script executed:

python3 << 'EOF'
import subprocess
import json

# Search for any tests or documentation about MaxPasswordLength being zero
result = subprocess.run(
    ["rg", "-n", "MaxPasswordLength|MinPasswordLength", "--type", "go"],
    capture_output=True,
    text=True,
    cwd="."
)

print("=== MaxPasswordLength/MinPasswordLength references ===")
print(result.stdout)
print(result.stderr)
EOF

Repository: ironcore-dev/metal-operator

Length of output: 157


🏁 Script executed:

grep -rn "if.*length\|if.*MaxPassword\|if.*0" bmc/redfish.go | grep -i "password\|length" | head -20

Repository: ironcore-dev/metal-operator

Length of output: 230


🏁 Script executed:

cat -n bmc/mockup.go | sed -n '100,200p'

Repository: ironcore-dev/metal-operator

Length of output: 2842


🏁 Script executed:

grep -rn "GetAccountService\|AccountService" bmc/ --include="*.go" -B 2 -A 8 | head -60

Repository: ironcore-dev/metal-operator

Length of output: 2709


🏁 Script executed:

cat -n internal/controller/bmcuser_controller.go | sed -n '185,195p'

Repository: ironcore-dev/metal-operator

Length of output: 698


🏁 Script executed:

grep -rn "MaxPasswordLength.*0\|if.*MaxPassword\|fallback" . --include="*.go"

Repository: ironcore-dev/metal-operator

Length of output: 207


🏁 Script executed:

grep -rn "type AccountService\|MaxPasswordLength\|MinPasswordLength" --include="*.go" | grep -v test | head -20

Repository: ironcore-dev/metal-operator

Length of output: 418


🏁 Script executed:

cat -n bmc/redfish.go | sed -n '1145,1165p'

Repository: ironcore-dev/metal-operator

Length of output: 1226


🏁 Script executed:

find . -name "*.go" -exec grep -l "gofish" {} \; | head -5

Repository: ironcore-dev/metal-operator

Length of output: 833


🏁 Script executed:

web_search
Redfish AccountService MaxPasswordLength specification - can it be zero or unspecified?

Repository: ironcore-dev/metal-operator

Length of output: 167


🏁 Script executed:

grep -rn "length.*4\|mustInclude" bmc/redfish.go

Repository: ironcore-dev/metal-operator

Length of output: 404


🏁 Script executed:

cat -n internal/controller/bmcuser_controller.go | sed -n '214,235p'

Repository: ironcore-dev/metal-operator

Length of output: 1518


🏁 Script executed:

grep -n "accountService\." internal/controller/bmcuser_controller.go

Repository: ironcore-dev/metal-operator

Length of output: 334


🏁 Script executed:

grep -rn "0.*password\|password.*0" bmc/ --include="*.go" | head -15

Repository: ironcore-dev/metal-operator

Length of output: 53


🏁 Script executed:

cat -n bmc/redfish.go | sed -n '1160,1165p'

Repository: ironcore-dev/metal-operator

Length of output: 399


🌐 Web query:

Redfish AccountService MaxPasswordLength specification optional field zero value

💡 Result:

In the DMTF Redfish specification, AccountService/MaxPasswordLength is simply “the maximum password length that the implementation allows” for local accounts (it “does not apply to accounts from external account providers”). It is not defined as having any special meaning when the value is 0. [1]

In the AccountService schema, MaxPasswordLength has a minimum of 0 (so 0 is syntactically valid), but the schema’s long description still describes it only as “the maximum password length” and does not say “0 means unlimited/unknown/unsupported”. [2]

So, if you see:

  • Property omitted: treat as not reported / not supported / unknown (implementation-dependent).
  • Property present with value 0: the Redfish spec does not define a standard interpretation; you cannot assume it means “no maximum” unless the vendor’s Redfish documentation explicitly says so. [1][2][3]

Sources:
[1] DMTF Redfish Data Model Specification (DSP0268) – MaxPasswordLength description (redfish.dmtf.org)
[2] DMTF AccountService schema (OData CSDL) – MaxPasswordLength min=0 + long description (gbmc.googlesource.com)
[3] DMTF Redfish Resource & Schema Guide (DSP2046) – MaxPasswordLength property listing (redfish.dmtf.org)

Citations:


Add defensive handling for zero or unspecified MaxPasswordLength.
Per the Redfish specification, MaxPasswordLength can be 0 or omitted (implementation-dependent). The current code at lines 190 and 220 passes this value directly to GenerateSecurePassword, which requires length ≥ 4 and fails otherwise, causing reconciliation to loop indefinitely. Introduce a fallback to a sane default (e.g., 16) or use MinPasswordLength when available.

🔧 Example fallback
-	newPassword, err := bmc.GenerateSecurePassword(bmc.Manufacturer(bmcObj.Status.Manufacturer), accountService.MaxPasswordLength)
+	length := accountService.MaxPasswordLength
+	if length <= 0 {
+		length = 16
+		// If MinPasswordLength exists and is larger, prefer it.
+		// if accountService.MinPasswordLength > length { length = accountService.MinPasswordLength }
+	}
+	newPassword, err := bmc.GenerateSecurePassword(bmc.Manufacturer(bmcObj.Status.Manufacturer), length)

Also applies to: 216-223

🤖 Prompt for AI Agents
In `@internal/controller/bmcuser_controller.go` around lines 186 - 192, The code
passes accountService.MaxPasswordLength directly into bmc.GenerateSecurePassword
which can be 0/unspecified and causes failures; update the reconciliation logic
(around GetAccountService() usage and where newPassword is generated) to
defensively compute an effective length: use accountService.MinPasswordLength if
present and >=4, otherwise fall back to a sane default (e.g., 16), and
cap/validate the chosen length to satisfy bmc.GenerateSecurePassword's
requirement (length >= 4) before calling
bmc.GenerateSecurePassword(bmc.Manufacturer(bmcObj.Status.Manufacturer),
effectiveLength); ensure the same fix is applied to the other occurrence around
lines 216-223.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-change area/metal-automation documentation Improvements or additions to documentation enhancement New feature or request minor size/XXL

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants