Skip to content

fix(tenant): always write an explicit S3 Object Lock retention ceiling - #155

Merged
yehlo merged 1 commit into
feat/initial-devfrom
claude/tenant-object-lock-retention-422e02
Aug 29, 2026
Merged

fix(tenant): always write an explicit S3 Object Lock retention ceiling#155
yehlo merged 1 commit into
feat/initial-devfrom
claude/tenant-object-lock-retention-422e02

Conversation

@yehlo

@yehlo yehlo commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Problem

Every tenant the operator created ended up with maxRetentionYears: 100 in the backend instead of a ceiling the operator controls. Verified against the dev grid — the pattern was unmistakable:

Origin maxRetentionDays maxRetentionYears
Operator-created null 100
Managed 90 null
Tenant Manager UI 1 1

StorageGrid applies a 100 year ceiling to any tenant whose request body carries no retention field at all.

Three defects combined:

  1. The field was omitted rather than set. spec.s3ObjectLock is an optional pointer, so when absent the CRD defaults never materialise. desiredMaxRetentionDays returned nil for both spec == nil and mode == Disabled

  2. Drift detection was blind to maxRetentionYears. GetConfiguredMaxRetentionDays only read MaxRetentionDays; MaxRetentionYears appeared nowhere in the repo. A tenant at years: 100 compared nil == nil and logged "already in sync" forever.

  3. When drift was detected, the write was a silent no-op that looped forever. For two adopted tenants with a hand-set maxRetentionDays: 90, the operator issued a full PUT on every reconcile, logged Successfully updated S3 Object Lock policy, and the grid still read 90. StorageGrid refuses to leave a tenant with no ceiling at all, so the empty PUT changed nothing.

Worth naming: maxRetentionDays caps Governance retention too — allowComplianceMode is a separate gate. A tenant admin can create a Governance object-lock bucket straight through the S3 API, bypassing the S3Bucket CR mode gate, and the backend ceiling is the only backstop. That backstop was 100 years on every managed tenant.

Change

The operator now always resolves and writes a concrete ceiling.

  • StorageGrid.spec.defaultMaxRetentionInDays — new grid-wide default, 365 (matching what the Tenant Manager writes), bounds 1–36500. Mirrored into status for visibility.
  • EffectiveMaxRetentionInDays in internal/controller — tenant value → grid default → 365. Never returns zero. Deliberately independent of mode, since the ceiling binds Governance retention the bucket-level gate never sees.
  • Nothing is written into the tenant spec. A tenant that does not need object lock sets nothing and inherits the grid default in the backend only; spec.s3ObjectLock.maxRetentionInDays (or 0 = inherit) overrides it.
  • pkg/gridCreateTenant and UpdateTenantObjectLockPolicy take a plain int instead of *int, so a ceiling can no longer silently vanish through omitempty. MaxRetentionYears is reset to nil before the PUT: a freshly fetched tenant carries 100, which is non-nil and would otherwise be written back alongside the days value.
  • Drift detection now includes maxRetentionYears, which is what actually ends the infinite PUT loop.
  • Bucket validation is now unconditional against the tenant's effective ceiling instead of firing only when the tenant set one explicitly. The maxLowered guard no longer skips when the mode is Disabled.

No SDK change required: StorageGrid stores the ceiling as either days or years and writing one clears the other, so sending a concrete maxRetentionDays is sufficient on both paths.

Verification

Deployed to the dev grid. All seven managed tenants went days=null, years=100days=365, years=null in a single reconcile burst, including the two that were stuck in the PUT loop. The loop is gone.

Backend semantics were confirmed directly against the grid API rather than assumed:

  • POST with no retention keys → days=null, years=100 (reproduces the bug)
  • POST with maxRetentionDays: 365, maxRetentionYears omitted → days=365, years=null (setting days clears years)

Also added:

  • Table test for the resolution helper (internal/controller/objectlock_retention_test.go)
  • pkg/grid tests covering the years accessor, the clear-before-PUT, and the omitempty contract that forces callers to resolve a concrete value
  • test/e2e/chainsaw/s3tenant/objectlock-retention/ — there was no tenant-level object-lock e2e test before

make test and make lint pass.

Note for rollout

defaultMaxRetentionInDays defaults to 365, so the two tenants that carried a hand-set 90-day ceiling (x7g-loki-s3tenant-deployment-legacy, syx2-kubernetes-itop-sync) are now at 365. If 90 was deliberate, set spec.s3ObjectLock.maxRetentionInDays: 90 on them and the next reconcile restores it.

🤖 Generated with Claude Code

@yehlo
yehlo merged commit cf74f41 into feat/initial-dev Aug 29, 2026
@yehlo
yehlo deleted the claude/tenant-object-lock-retention-422e02 branch August 30, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant