Skip to content

Commit dad8671

Browse files
committed
Fix CodeRabbit AI review findings
Resolves all 7 CodeRabbit AI issues from PR review: 1. Fix controller requeue bug that could delay expiration past status.ExpiresAt - Change warning period requeue to min(5min, timeUntilExpiration) 2. Refactor webhook validation to close validation gaps - Extract validateBMCUserExpirationSpec helper for DRY code - Add ttlChanged and expiresAtChanged helpers for nil-safe comparisons - Apply full validation on updates (future dates, TTL ranges) - Warn on all expiration field changes (removals, switches, modifications) - Add comprehensive test coverage (13 new test cases, 346 lines) 3. Add missing CRD list markers for proper API server merge behavior - Add +listType=map and +listMapKey=type to Conditions field 4. Update sample YAML timestamps from 2026 to 2099 to prevent validation failures 5. Update documentation example timestamps from 2026 to 2099 6. Clarify ExpiresAt field documentation for permanence condition 7. Fix test suite consistency by reusing shared accessor variable Additional improvement: - Reduce TTL maximum from 10 years to 1 week for more reasonable temporary access Signed-off-by: Stefan Hipfel <stefan.hipfel@sap.com>
1 parent 1bfb595 commit dad8671

8 files changed

Lines changed: 413 additions & 38 deletions

File tree

api/v1alpha1/bmcuser_types.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ type BMCUserSpec struct {
4343

4444
// ExpiresAt specifies an absolute timestamp when this user should be deleted.
4545
// This is useful for users that need to expire at a specific time.
46-
// If not set along with TTL, the user is permanent.
46+
// If neither ExpiresAt nor TTL is set, the user is permanent (no automatic deletion).
4747
// Mutually exclusive with TTL - only one should be set.
4848
// +optional
4949
ExpiresAt *metav1.Time `json:"expiresAt,omitempty"`
@@ -73,6 +73,8 @@ type BMCUserStatus struct {
7373
// Conditions represents the latest available observations of the BMCUser's current state.
7474
// +patchStrategy=merge
7575
// +patchMergeKey=type
76+
// +listType=map
77+
// +listMapKey=type
7678
// +optional
7779
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
7880
}

config/crd/bases/metal.ironcore.dev_bmcusers.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ spec:
102102
description: |-
103103
ExpiresAt specifies an absolute timestamp when this user should be deleted.
104104
This is useful for users that need to expire at a specific time.
105-
If not set along with TTL, the user is permanent.
105+
If neither ExpiresAt nor TTL is set, the user is permanent (no automatic deletion).
106106
Mutually exclusive with TTL - only one should be set.
107107
format: date-time
108108
type: string
@@ -194,6 +194,9 @@ spec:
194194
- type
195195
type: object
196196
type: array
197+
x-kubernetes-list-map-keys:
198+
- type
199+
x-kubernetes-list-type: map
197200
effectiveBMCSecretRef:
198201
description: |-
199202
EffectiveBMCSecretRef references the BMCSecret currently used for this user.

config/samples/metal_v1alpha1_bmcuser_temporary.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ spec:
3636
description: "User for maintenance window"
3737

3838
# Absolute expiration at specific time (end of maintenance window)
39-
expiresAt: "2026-04-11T02:00:00Z"
39+
# Replace with future date before applying
40+
expiresAt: "2099-04-11T02:00:00Z"
4041

4142
bmcRef:
4243
name: bmc-sample

docs/usage/bmcuser-ttl.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ metadata:
3737
spec:
3838
userName: maintenance
3939
roleID: "Operator"
40-
expiresAt: "2026-04-11T02:00:00Z" # User expires at this time
40+
expiresAt: "2099-04-11T02:00:00Z" # User expires at this time
4141
bmcRef:
4242
name: my-bmc
4343
```
@@ -103,11 +103,11 @@ spec:
103103
apiVersion: metal.ironcore.dev/v1alpha1
104104
kind: BMCUser
105105
metadata:
106-
name: maintenance-2026-04-10
106+
name: maintenance-2099-04-10
107107
spec:
108108
userName: maintenance-tech
109109
roleID: "Operator"
110-
expiresAt: "2026-04-11T06:00:00Z"
110+
expiresAt: "2099-04-11T06:00:00Z"
111111
bmcRef:
112112
name: datacenter-bmc
113113
```

internal/controller/bmcuser_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,8 +642,8 @@ func (r *BMCUserReconciler) coordinateRequeue(user *metalv1alpha1.BMCUser, rotat
642642
// Not in warning period yet - requeue when warning starts
643643
expirationRequeue = timeUntilExpiration - warningThreshold
644644
} else {
645-
// In warning period - check every 5 minutes for expiration
646-
expirationRequeue = 5 * time.Minute
645+
// In warning period - check frequently, but not past expiration
646+
expirationRequeue = min(5*time.Minute, timeUntilExpiration)
647647
}
648648

649649
// Choose the earlier of rotation or expiration requeue

internal/controller/suite_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ func SetupTest(redfishMockServers []netip.AddrPort) *corev1.Namespace {
324324
Scheme: k8sManager.GetScheme(),
325325
DefaultProtocol: metalv1alpha1.HTTPProtocolScheme,
326326
SkipCertValidation: true,
327-
Conditions: conditionutils.NewAccessor(conditionutils.AccessorOptions{}),
327+
Conditions: accessor,
328328
BMCOptions: bmc.Options{
329329
PowerPollingInterval: 50 * time.Millisecond,
330330
PowerPollingTimeout: 200 * time.Millisecond,

internal/webhook/v1alpha1/bmcuser_webhook.go

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,33 +35,60 @@ type BMCUserCustomValidator struct {
3535
Client client.Client
3636
}
3737

38-
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type BMCUser.
39-
func (v *BMCUserCustomValidator) ValidateCreate(_ context.Context, obj *metalv1alpha1.BMCUser) (admission.Warnings, error) {
40-
bmcuserlog.Info("Validation for BMCUser upon creation", "name", obj.GetName())
41-
38+
// validateBMCUserExpirationSpec validates TTL and ExpiresAt fields.
39+
// Returns error if validation fails.
40+
func validateBMCUserExpirationSpec(spec *metalv1alpha1.BMCUserSpec) error {
4241
// Validate TTL and ExpiresAt are mutually exclusive
43-
if obj.Spec.TTL != nil && obj.Spec.ExpiresAt != nil {
44-
return nil, fmt.Errorf("spec.ttl and spec.expiresAt are mutually exclusive")
42+
if spec.TTL != nil && spec.ExpiresAt != nil {
43+
return fmt.Errorf("spec.ttl and spec.expiresAt are mutually exclusive")
4544
}
4645

4746
// Validate ExpiresAt is in the future
48-
if obj.Spec.ExpiresAt != nil {
49-
if obj.Spec.ExpiresAt.Before(&metav1.Time{Time: time.Now()}) {
50-
return nil, fmt.Errorf("spec.expiresAt must be in the future")
47+
if spec.ExpiresAt != nil {
48+
if spec.ExpiresAt.Before(&metav1.Time{Time: time.Now()}) {
49+
return fmt.Errorf("spec.expiresAt must be in the future")
5150
}
5251
}
5352

54-
// Validate TTL is reasonable (> 0, < 10 years)
55-
if obj.Spec.TTL != nil {
56-
if obj.Spec.TTL.Duration <= 0 {
57-
return nil, fmt.Errorf("spec.ttl must be positive")
53+
// Validate TTL is reasonable (> 0, <= 1 week)
54+
if spec.TTL != nil {
55+
if spec.TTL.Duration <= 0 {
56+
return fmt.Errorf("spec.ttl must be positive")
5857
}
59-
if obj.Spec.TTL.Duration > 87600*time.Hour { // 10 years
60-
return nil, fmt.Errorf("spec.ttl exceeds maximum of 10 years")
58+
if spec.TTL.Duration > 168*time.Hour { // 1 week
59+
return fmt.Errorf("spec.ttl exceeds maximum of 1 week")
6160
}
6261
}
6362

64-
return nil, nil
63+
return nil
64+
}
65+
66+
// ttlChanged returns true if TTL field changed between old and new objects.
67+
func ttlChanged(oldTTL, newTTL *metav1.Duration) bool {
68+
if oldTTL == nil && newTTL == nil {
69+
return false
70+
}
71+
if oldTTL == nil || newTTL == nil {
72+
return true // one is nil, other isn't
73+
}
74+
return oldTTL.Duration != newTTL.Duration
75+
}
76+
77+
// expiresAtChanged returns true if ExpiresAt field changed between old and new objects.
78+
func expiresAtChanged(oldTime, newTime *metav1.Time) bool {
79+
if oldTime == nil && newTime == nil {
80+
return false
81+
}
82+
if oldTime == nil || newTime == nil {
83+
return true // one is nil, other isn't
84+
}
85+
return !oldTime.Equal(newTime)
86+
}
87+
88+
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type BMCUser.
89+
func (v *BMCUserCustomValidator) ValidateCreate(_ context.Context, obj *metalv1alpha1.BMCUser) (admission.Warnings, error) {
90+
bmcuserlog.Info("Validation for BMCUser upon creation", "name", obj.GetName())
91+
return nil, validateBMCUserExpirationSpec(&obj.Spec)
6592
}
6693

6794
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type BMCUser.
@@ -70,24 +97,20 @@ func (v *BMCUserCustomValidator) ValidateUpdate(_ context.Context, oldObj, newOb
7097

7198
var warnings admission.Warnings
7299

73-
// Validate TTL and ExpiresAt are mutually exclusive
74-
if newObj.Spec.TTL != nil && newObj.Spec.ExpiresAt != nil {
75-
return warnings, fmt.Errorf("spec.ttl and spec.expiresAt are mutually exclusive")
100+
// Apply same validation as create
101+
if err := validateBMCUserExpirationSpec(&newObj.Spec); err != nil {
102+
return warnings, err
76103
}
77104

78105
// Warn if TTL/ExpiresAt changed after expiration was calculated
79106
if oldObj.Status.ExpiresAt != nil {
80-
if oldObj.Spec.TTL != nil && newObj.Spec.TTL != nil {
81-
if oldObj.Spec.TTL.Duration != newObj.Spec.TTL.Duration {
82-
warnings = append(warnings,
83-
"TTL was changed but expiration time is already set and will not be recalculated")
84-
}
107+
if ttlChanged(oldObj.Spec.TTL, newObj.Spec.TTL) {
108+
warnings = append(warnings,
109+
"TTL was changed but expiration time is already set and will not be recalculated")
85110
}
86-
if oldObj.Spec.ExpiresAt != nil && newObj.Spec.ExpiresAt != nil {
87-
if !oldObj.Spec.ExpiresAt.Equal(newObj.Spec.ExpiresAt) {
88-
warnings = append(warnings,
89-
"ExpiresAt was changed but expiration time is already calculated and will not be updated")
90-
}
111+
if expiresAtChanged(oldObj.Spec.ExpiresAt, newObj.Spec.ExpiresAt) {
112+
warnings = append(warnings,
113+
"ExpiresAt was changed but expiration time is already calculated and will not be updated")
91114
}
92115
}
93116

0 commit comments

Comments
 (0)