@@ -1139,7 +1139,7 @@ func (r *S3TenantAccountReconciler) reconcileCreate(ctx context.Context, rctx *a
11391139 // initial description will include some details about the account in case the reconcileDescription fails later.
11401140 initialDescription := fmt .Sprintf ("Created by storagegrid-operator for S3TenantAccount %s in namespace %s at %s" , rctx .Account .Name , rctx .Account .Namespace , time .Now ().Format (time .RFC3339 ))
11411141
1142- tenantID , password , err := grid .CreateTenant (ctx , * rctx .Account .Status .DesiredTenantBackendName , initialDescription , rctx .Account .Spec .StorageQuota .Value (), desiredAllowComplianceMode (rctx .Account .Spec .S3ObjectLock ), desiredMaxRetentionDays (rctx .Account .Spec .S3ObjectLock ), rctx .GridClient )
1142+ tenantID , password , err := grid .CreateTenant (ctx , * rctx .Account .Status .DesiredTenantBackendName , initialDescription , rctx .Account .Spec .StorageQuota .Value (), desiredAllowComplianceMode (rctx .Account .Spec .S3ObjectLock ), desiredMaxRetentionDays (rctx .Account .Spec .S3ObjectLock , rctx . SG ), rctx .GridClient )
11431143 if err != nil {
11441144 r .emitEvent (rctx , corev1 .EventTypeWarning , EventTenantCreateFailed ,
11451145 fmt .Sprintf ("Failed to create tenant: %v" , err ))
@@ -1417,48 +1417,58 @@ func desiredAllowComplianceMode(spec *s3v1alpha1.S3ObjectLockTenantSpec) bool {
14171417 return spec .Mode == s3v1alpha1 .S3ObjectLockModeCompliance
14181418}
14191419
1420- // desiredMaxRetentionDays maps the spec MaxRetentionInDays to the backend tenant's
1421- // MaxRetentionDays. Returns nil when object lock is Disabled (no per-tenant cap).
1422- func desiredMaxRetentionDays (spec * s3v1alpha1.S3ObjectLockTenantSpec ) * int {
1423- if spec == nil || spec .Mode == "" || spec .Mode == s3v1alpha1 .S3ObjectLockModeDisabled {
1424- return nil
1420+ // EffectiveMaxRetentionInDays resolves the S3 Object Lock retention ceiling for a tenant:
1421+ // its own MaxRetentionInDays when set, otherwise the grid-wide default.
1422+ //
1423+ // Deliberately independent of Mode. Mode governs whether an S3Bucket may enable object lock at
1424+ // all, but the ceiling also binds Governance retention a tenant can request directly over the S3
1425+ // API, where the bucket-level gate never applies.
1426+ //
1427+ // Never returns zero: StorageGrid rejects an empty ceiling once S3 Object Lock is enabled
1428+ // grid-wide, and silently substitutes 100 years for a request that carries no ceiling at all.
1429+ func EffectiveMaxRetentionInDays (spec * s3v1alpha1.S3ObjectLockTenantSpec , sg * s3v1alpha1.StorageGrid ) int32 {
1430+ if spec != nil && spec .MaxRetentionInDays > 0 {
1431+ return spec .MaxRetentionInDays
1432+ }
1433+ if sg != nil && sg .Spec .DefaultMaxRetentionInDays > 0 {
1434+ return sg .Spec .DefaultMaxRetentionInDays
14251435 }
1426- v := int (spec .MaxRetentionInDays )
1427- return & v
1436+ return s3v1alpha1 .DefaultMaxRetentionInDays
14281437}
14291438
1430- // intPtrEqual compares two *int values for equality, treating nil == nil as equal.
1431- func intPtrEqual (a , b * int ) bool {
1432- if a == nil && b == nil {
1433- return true
1434- }
1435- if a == nil || b == nil {
1436- return false
1437- }
1438- return * a == * b
1439+ // desiredMaxRetentionDays adapts the resolved ceiling to the backend tenant's MaxRetentionDays.
1440+ func desiredMaxRetentionDays (spec * s3v1alpha1.S3ObjectLockTenantSpec , sg * s3v1alpha1.StorageGrid ) int {
1441+ return int (EffectiveMaxRetentionInDays (spec , sg ))
14391442}
14401443
14411444// reconcileObjectLockPolicy syncs the backend tenant's S3 Object Lock policy fields
1442- // (AllowComplianceMode, MaxRetentionDays) with the spec. Issues a single full PUT on drift.
1445+ // (AllowComplianceMode, MaxRetentionDays, MaxRetentionYears) with the spec.
1446+ // Issues a single full PUT on drift.
1447+ //
1448+ // MaxRetentionYears participates in the comparison even though the operator never sets it:
1449+ // StorageGrid stamps 100 years onto any tenant created without an explicit ceiling, and a
1450+ // non-nil value there means the tenant is not yet expressed in the operator's terms.
14431451func (r * S3TenantAccountReconciler ) reconcileObjectLockPolicy (ctx context.Context , rctx * accountReconcileContext ) error {
14441452 log := log .FromContext (ctx )
14451453
14461454 desiredAllow := desiredAllowComplianceMode (rctx .Account .Spec .S3ObjectLock )
1447- desiredMax := desiredMaxRetentionDays (rctx .Account .Spec .S3ObjectLock )
1455+ desiredMax := desiredMaxRetentionDays (rctx .Account .Spec .S3ObjectLock , rctx . SG )
14481456
14491457 currentAllow := grid .GetConfiguredAllowComplianceMode (rctx .BackendTenant )
14501458 currentMax := grid .GetConfiguredMaxRetentionDays (rctx .BackendTenant )
1459+ currentYears := grid .GetConfiguredMaxRetentionYears (rctx .BackendTenant )
14511460
1452- if currentAllow == desiredAllow && intPtrEqual ( currentMax , desiredMax ) {
1461+ if currentAllow == desiredAllow && currentMax != nil && * currentMax == desiredMax && currentYears == nil {
14531462 log .V (1 ).Info ("Object lock policy already in sync" )
14541463 return nil
14551464 }
14561465
14571466 log .V (1 ).Info ("Object lock policy drift detected, updating tenant" ,
14581467 "currentAllowComplianceMode" , currentAllow , "desiredAllowComplianceMode" , desiredAllow ,
1459- "currentMaxRetentionDays" , currentMax , "desiredMaxRetentionDays" , desiredMax )
1468+ "currentMaxRetentionDays" , currentMax , "desiredMaxRetentionDays" , desiredMax ,
1469+ "currentMaxRetentionYears" , currentYears )
14601470 r .emitEvent (rctx , corev1 .EventTypeNormal , EventTenantUpdating ,
1461- fmt .Sprintf ("Updating S3 Object Lock policy (allowComplianceMode=%v, maxRetentionDays=%v )" , desiredAllow , desiredMax ))
1471+ fmt .Sprintf ("Updating S3 Object Lock policy (allowComplianceMode=%v, maxRetentionDays=%d )" , desiredAllow , desiredMax ))
14621472
14631473 if err := grid .UpdateTenantObjectLockPolicy (ctx , desiredAllow , desiredMax , rctx .BackendTenant , rctx .GridClient ); err != nil {
14641474 r .emitEvent (rctx , corev1 .EventTypeWarning , EventTenantUpdateFailed ,
0 commit comments