feat: make key version count configurable per landscape and provider - #420
davidbolet wants to merge 7 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
There was a problem hiding this comment.
🟡 Changes recommended
The eviction implementation currently hard-caps reads to 10,000 versions, which can lead to incomplete enforcement and unnecessary load, and should be refactored to page/delete without a fixed cap.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds per-landscape, per-provider configuration for maximum retained key versions and enforces those limits during key version sync, with accompanying tests and documentation notes.
Changes:
- Extend
config.LandscapewithmaxKeyVersions(per-provider), plus defaults, unlimited sentinel, and validation. - Enforce version retention limits after
KeyVersionManager.UpdateVersions()upserts versions. - Add unit tests for landscape config helpers/validation and key-version eviction scenarios; update example
config.yamland add implementation/analysis docs.
File summaries
| File | Description |
|---|---|
PLUGIN-VERIFICATION-STATUS.md |
Documents plugin SDK GetKeyVersions interface compliance investigation. |
KMS20-6953-ANALYSIS-UPDATED.md |
Updated technical analysis reflecting recent architecture changes and recommended implementation approach. |
internal/manager/keyversion.go |
Inject landscape config into KeyVersionManager and enforce per-provider version eviction after upsert. |
internal/manager/keyversion_test.go |
Adds unit tests for eviction behavior across limits/providers/unlimited/nil-config scenarios. |
internal/manager/key.go |
Wires landscape config into KeyVersionManager from the loaded application config. |
internal/manager/base.go |
Updates manager construction to pass landscape config into KeyVersionManager. |
internal/config/landscape_test.go |
New unit tests for Landscape.GetMaxVersionsForProvider() and Landscape.Validate(). |
internal/config/config.go |
Adds Landscape.MaxKeyVersions, default/unlimited constants, getter, validation, and hooks validation into Config.Validate(). |
IMPLEMENTATION-PROGRESS.md |
Progress/decision log for KMS20-6953 implementation steps and open questions. |
config.yaml |
Adds an example landscape.maxKeyVersions configuration block. |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| maxVersions := kvm.landscapeConfig.GetMaxVersionsForProvider(key.Provider) | ||
| if maxVersions == config.UnlimitedKeyVersions { | ||
| // Unlimited versions - no eviction | ||
| return nil | ||
| } |
| // 3. Get all current versions ordered by RotatedAt DESC (most recent first) | ||
| allVersions, _, err := kvm.GetKeyVersions( | ||
| ctx, | ||
| keyID, | ||
| repo.Pagination{Top: 10000, Skip: 0, Count: true}, // High limit to get all versions | ||
| ) |
| // This method logs warnings on failures but does not return errors to avoid | ||
| // rolling back version upserts that have already succeeded. |
| assert.Equal(t, primaryVersion.ID, allVersions[0].ID, "Primary should match latest") | ||
| }) | ||
|
|
||
| t.Run("Should restore previously evicted version on re-addition", func(t *testing.T) { |
b574c4d to
7cc7d44
Compare
There was a problem hiding this comment.
🟡 Changes recommended
There is a test compilation issue (missing context import) and the eviction enforcement currently relies on a fixed page size and len() rather than total count, which can fail to enforce limits for large version histories.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 4
- Review effort level: Lite
| import ( | ||
| "fmt" | ||
| "testing" | ||
| "time" | ||
|
|
| // 3. Get all current versions ordered by RotatedAt DESC (most recent first) | ||
| allVersions, _, err := kvm.GetKeyVersions( | ||
| ctx, | ||
| keyID, | ||
| repo.Pagination{Top: 10000, Skip: 0, Count: true}, // High limit to get all versions | ||
| ) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get versions for limit enforcement: %w", err) | ||
| } | ||
|
|
||
| // 4. Check if eviction is needed | ||
| if len(allVersions) <= maxVersions { | ||
| // Within limit - nothing to do | ||
| return nil | ||
| } | ||
|
|
||
| // 5. Delete oldest versions (beyond limit) | ||
| return kvm.deleteExcessVersions(ctx, keyID, key.Provider, allVersions, maxVersions) | ||
| } |
| return &Manager{ | ||
| Keys: keyManager, | ||
| KeyVersions: NewKeyVersionManager(repo, svcRegistry, tenantConfigManager, certManager, cmkAuditor), | ||
| KeyVersions: NewKeyVersionManager( | ||
| repo, svcRegistry, tenantConfigManager, certManager, cmkAuditor, &config.Landscape, | ||
| ), | ||
| TenantConfigs: tenantConfigManager, |
| // This method logs warnings on failures but does not return errors to avoid | ||
| // rolling back version upserts that have already succeeded. |
- Add MaxKeyVersions map to Landscape struct for provider-scoped version limits - Implement GetMaxVersionsForProvider() helper with default of 5 versions - Add Landscape.Validate() to ensure limits are -1 (unlimited) or >= 1 - Add comprehensive unit tests for config loading and validation - Update example config.yaml with maxKeyVersions configuration Part of KMS20-6953: Make managed key versions count configurable per landscape Default behavior: - 5 versions per key if not configured - -1 for unlimited (no eviction) - Independent limits per provider (AWS, GCP, FORTANIX) Test coverage: - GetMaxVersionsForProvider with various scenarios - Validate with valid and invalid configurations - All tests passing (0.713s) Related: - KMS20-6484 (GCP HYOK provider - in progress) Signed-off-by: David <davidbolet@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
- Add PLUGIN-VERIFICATION-STATUS.md with plugin compatibility analysis - Add IMPLEMENTATION-PROGRESS.md with detailed progress summary - Add KMS20-6953-ANALYSIS-UPDATED.md with updated technical analysis Documentation includes: - Plugin GetKeyVersions interface verification - Implementation steps 1 & 2 completion status - Test results and coverage metrics - Open questions for review - Next steps and blockers Signed-off-by: David <davidbolet@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: David <davidbolet@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: David <davidbolet@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
7cc7d44 to
1a4413f
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The new eviction/enforcement logic has correctness bugs (missing-key handling can return nil error and eviction relies on a capped list) that can silently skip enforcement or fail to enforce limits.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 4
- Review effort level: Lite
| key := &model.Key{ID: keyID} | ||
| found, err := kvm.repo.First(ctx, key, *repo.NewQuery()) | ||
| if err != nil || !found { | ||
| return fmt.Errorf("failed to get key for version limit enforcement: %w", err) | ||
| } |
| // 3. Get all current versions ordered by RotatedAt DESC (most recent first) | ||
| allVersions, _, err := kvm.GetKeyVersions( | ||
| ctx, | ||
| keyID, | ||
| repo.Pagination{Top: 10000, Skip: 0, Count: true}, // High limit to get all versions | ||
| ) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get versions for limit enforcement: %w", err) | ||
| } | ||
|
|
||
| // 4. Check if eviction is needed | ||
| if len(allVersions) <= maxVersions { | ||
| // Within limit - nothing to do | ||
| return nil | ||
| } | ||
|
|
||
| // 5. Delete oldest versions (beyond limit) | ||
| return kvm.deleteExcessVersions(ctx, keyID, key.Provider, allVersions, maxVersions) |
| // This method logs warnings on failures but does not return errors to avoid | ||
| // rolling back version upserts that have already succeeded. |
| assert.Equal(t, primaryVersion.ID, allVersions[0].ID, "Primary should match latest") | ||
| }) | ||
|
|
||
| t.Run("Should restore previously evicted version on re-addition", func(t *testing.T) { |
Signed-off-by: David <davidbolet@gmail.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The eviction enforcement has a correctness bug when the key is not found and should use a safer deletion strategy (and tests should cover the “evicted version becomes primary” re-addition case).
Review details
Suppressed comments (5)
Previously missed (5) — in code that hasn't changed since the last review.
internal/manager/keyversion.go:234
repo.Firstreturns (found=false, err=nil) when the key doesn't exist; the current checkif err != nil || !foundthen wrapserrwith%w, which will returnnilwhenerris nil. That makes missing keys silently skip enforcement, and defeats the warning log inUpdateVersions. Handle the!foundcase explicitly so it becomes a real error.
internal/manager/keyversion.go:268- Eviction currently loads up to 10k versions into memory and assumes that contains all versions; if a key ever has >10k versions, this will leave older versions undeleted (still exceeding the limit). A safer approach is to delete versions beyond the limit in batches using pagination with
Skip=maxVersions, so you don't need to fetch the entire history.
internal/manager/keyversion_test.go:796 - This test intends to cover the acceptance criterion that a previously evicted version is restored if it becomes the primary/most-recent version. As written, the re-added
v1keeps the old timestamp, so it correctly gets evicted again and the primary-restoration path isn't exercised. Adjust the re-add input sov1becomes most recent.
internal/manager/keyversion.go:224 - The doc comment says this method "does not return errors", but it does return an error to the caller (which then logs and continues). Updating the comment avoids confusion for future maintainers.
internal/manager/keyversion_test.go:818 - With the re-addition scenario making
v1the most recent version, the assertions should expectv1to be present and ordered first (primary), with the remaining most-recent versions filling the rest of the limit.
- Files reviewed: 10/10 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
There are correctness issues in the eviction/enforcement implementation (including nil-config handling and incomplete/unsafe eviction behavior under larger datasets) that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
internal/manager/keyversion.go:195
- When
k.CreationTimeis nil, this path usesrepo.Createwithout any conflict handling. If the provider repeatedly omitsCreationTimefor the same version ID, the next sync will hit the (key_id, native_id) unique constraint and fail; using the same upsert strategy as the non-nil path keepsUpdateVersionsidempotent.
internal/manager/keyversion.go:255 - Eviction currently fetches at most 10,000 versions and then deletes
allVersions[maxVersions:]. If a key has >10,000 versions, this will not evict enough rows to reachmaxVersions(older versions beyond the first 10k are never considered/deleted). Consider implementing a DB-side delete that removes all rows older than the Nth most recent version (e.g., DELETE WHERE key_id=? AND id NOT IN (SELECT id ... ORDER BY rotated_at DESC, created_at DESC LIMIT ?)), or iterating deletion in a way that can’t leave more thanmaxVersionsbehind.
internal/manager/keyversion_test.go:712 - This subtest name says it "restores" an evicted version, but it only re-sends the same (oldest) timestamps and then asserts the version is evicted again. This doesn’t cover the acceptance criterion that an evicted version should be re-inserted and retained if it becomes primary/most-recent (e.g., same nativeID returning with a newer CreationTime).
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Lite
| KeyVersions: NewKeyVersionManager( | ||
| repo, svcRegistry, tenantConfigManager, certManager, cmkAuditor, &config.Landscape, | ||
| ), |
Signed-off-by: David <davidbolet@gmail.com>
de21332 to
eda48b6
Compare
There was a problem hiding this comment.
Please remove these .md files. Not only is this slop that shouldn't be committed as it also exposes internal stuff... Please be careful and ALWAYS review AI output
| now := time.Now().UTC() | ||
| k.CreationTime = &now |
There was a problem hiding this comment.
Why are we making this more verbose when the new() exists in current go version?
| if kvm.landscapeConfig == nil { | ||
| // No config available - skip enforcement | ||
| log.Debug(ctx, "Landscape config not available, skipping version limit enforcement", | ||
| slog.String("keyId", keyID.String())) | ||
| return nil | ||
| } |
There was a problem hiding this comment.
This is wrong, it should use the default
| allVersions, _, err := kvm.GetKeyVersions( | ||
| ctx, | ||
| keyID, | ||
| repo.Pagination{Top: 10000, Skip: 0, Count: true}, // High limit to get all versions |
There was a problem hiding this comment.
This should also use the Count method as you only want the number of versions
|
As mentioned before in multiple occasions, please disable your copilot reviews! If you want automated PR reviews use the AI agent we have on openkcm. coderabbit |
Implements configurable key version limits per landscape and provider
Changes
MaxKeyVersionsmap toLandscapestruct with per-provider limitsKeyVersionManager.UpdateVersions()Configuration Example