Skip to content

Fix CloudProfile API rejection by skipping invalid SemVer legacy tags#34

Merged
anton-paulovich merged 11 commits into
masterfrom
fix/gep-33-feature-flag
Jun 16, 2026
Merged

Fix CloudProfile API rejection by skipping invalid SemVer legacy tags#34
anton-paulovich merged 11 commits into
masterfrom
fix/gep-33-feature-flag

Conversation

@anton-paulovich

@anton-paulovich anton-paulovich commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

With the recent GEP-33 updates, cloud-profile-sync was made smarter: it now correctly extracts a valid, clean semantic version (e.g., 1877.9.2) from OCI image annotations. Because this clean version passes the initial filterImages() validation, images with historically malformed, non-SemVer compliant raw tags (e.g., 1877.9.2.0-metal-sci-pxe-amd64-...) are now successfully processed by the controller

However, when it attempts to write these raw tags back into the legacy spec.machineImages[].versions[].version array for backward compatibility, the Gardener API Server strictly enforces SemVer validation. This caused the API server to reject the entire CloudProfile patch as Invalid, silently halting the synchronization process

How it works now with feature flag enabled:

  1. Invalid SemVer Tag + Has CleanVersion: NEW format only.

  2. Invalid SemVer Tag + Invalid/No CleanVersion: everything dropped

  3. Valid SemVer Tag + Valid CleanVersion: BOTH formats (the ideal transition state).

  4. Valid SemVer Tag + NO CleanVersion (Old images): OLD format only

  5. Valid SemVer Tag + Invalid CleanVersion : BOTH formats (clean version will be adjusted to be valid)

Summary by CodeRabbit

  • Bug Fixes
    • Improved image version validation by correctly filtering OCI image entries based on architecture presence and SemVer parsing (handling both legacy version tags and clean versions).
    • Prevents invalid non-SemVer legacy version entries from being added, and improves log clarity when entries are skipped.
  • Tests
    • Added/expanded coverage for image filtering, legacy/clean version rewriting, normalization behavior, and cases with missing/invalid versions.
    • Added a test to ensure raw tags are forwarded appropriately without being written as legacy versions when invalid.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@anton-paulovich, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 23 minutes and 33 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da9c5c7a-90e9-4ee5-8f76-465fd0187a1f

📥 Commits

Reviewing files that changed from the base of the PR and between 805d5e1 and 64b04e2.

📒 Files selected for processing (5)
  • .claude/settings.local.json
  • .gitignore
  • LICENSES/CC0-1.0.txt
  • cloudprofilesync/imageupdater.go
  • cloudprofilesync/imageupdater_test.go
📝 Walkthrough

Walkthrough

filterImages is updated to validate SourceImage.Version (strict SemVer) and SourceImage.CleanVersion (tolerant parse) as independent paths, skipping entries only when both are invalid, and requiring at least one architecture. ImageUpdater.Update adds a semver.Parse guard before appending legacy MachineImageVersion entries. New and extended Ginkgo tests cover all branching cases. The Go linter configuration is updated to accept full patch versions in the version pattern.

Changes

SemVer validation in image filtering and updating

Layer / File(s) Summary
filterImages dual-path validation and Update legacy guard
cloudprofilesync/imageupdater.go
filterImages now checks for architectures first, then validates Version via strict semver.Parse and CleanVersion via tolerant parsing independently, skipping only when both fail. ImageUpdater.Update guards the legacy MachineImageVersion append with semver.Parse, logging and skipping invalid versions.
filterImages and dual-write ON test cases
cloudprofilesync/imageupdater_test.go
Adds a filterImages Ginkgo suite with a helper that runs ImageUpdater.Update and extracts MachineImageVersion entries, covering invalid tags, CleanVersion rewriting/normalization, dual-format output, and no-architecture dropping. Extends the dual-write ON suite to assert non-semver raw tags are excluded from cpSpec.MachineImages but still reach the provider via ProviderConfig.Raw.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 Hop hop through versions we go,
Strict semver or clean — both paths in a row,
No architectures? Skip with a bow!
Legacy tags guarded, the log will say so,
Tests confirm the filter's steady glow! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: fixing CloudProfile API rejection by implementing SemVer validation to skip invalid legacy tags.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/gep-33-feature-flag

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
cloudprofilesync/imageupdater.go (1)

108-124: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing validation: CleanVersion may still be invalid when written.

If a SourceImage has a valid legacy tag but a truly unparseable CleanVersion (not even tolerantly parseable like "garbage"), filterImages allows it through because validLegacyTag is true. However, CleanVersion is not cleared—it remains the original invalid string. The condition on line 108 evaluates to true, causing the invalid CleanVersion to be written to image.Versions, which would trigger the same API rejection this PR aims to fix.

🐛 Proposed fix: validate CleanVersion before writing
 		// When capabilities are enabled, also write the clean version entry.
 		if iu.EnableCapabilities && sourceImage.CleanVersion != "" && sourceImage.CleanVersion != sourceImage.Version {
+			// CleanVersion was normalized by filterImages if tolerantly parseable;
+			// skip writing if it's still invalid (truly unparseable input).
+			if _, err = semver.ParseTolerant(sourceImage.CleanVersion); err != nil {
+				iu.Log.V(1).Info("skipping clean version entry because it is not valid semver", "cleanVersion", sourceImage.CleanVersion)
+				continue
+			}
 			if idx, exists := existingVersions[sourceImage.CleanVersion]; exists {

Alternatively, filterImages could clear CleanVersion when parsing fails, but that may break downstream code expecting the original annotation value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloudprofilesync/imageupdater.go` around lines 108 - 124, The code on line
108 checks if CleanVersion is non-empty but does not validate that it is
actually a valid version string before writing it to image.Versions. Add
validation to ensure CleanVersion is truly parseable and valid before using it
in the condition that guards the append and update logic for image.Versions. If
CleanVersion is found to be invalid during validation, skip this block entirely
so invalid version strings are not written to the image.Versions array,
preventing API rejection downstream.
🧹 Nitpick comments (1)
cloudprofilesync/imageupdater_test.go (1)

84-96: ⚡ Quick win

Test name is misleading: "1921.0" is tolerantly parseable, not truly invalid.

The test name says "invalid clean version" but "1921.0" is successfully parsed by semver.ParseTolerant and normalized to "1921.0.0". This is more accurately described as "tolerantly parseable and normalized."

A test for a truly unparseable CleanVersion (e.g., "garbage") is missing—this is the edge case that could expose the bug mentioned in imageupdater.go where an invalid CleanVersion might be written to the spec.

♻️ Suggested: rename test and add coverage for truly invalid CleanVersion
-	It("valid tag + invalid clean version: BOTH formats with clean version normalized", func(ctx SpecContext) {
+	It("valid tag + tolerantly parseable clean version: BOTH formats with clean version normalized", func(ctx SpecContext) {
 		result := versions(ctx, []cloudprofilesync.SourceImage{
 			{
 				Version:       "1921.0.0-metal-sci-usi-amd64",
 				CleanVersion:  "1921.0",
 				Architectures: []string{"amd64"},
 				Capabilities:  gardencorev1beta1.Capabilities{"architecture": {"amd64"}, "feature": {"sci", "_usi"}},
 			},
 		})
 		Expect(result).To(HaveLen(2))
 		versionStrings := []string{result[0].Version, result[1].Version}
 		Expect(versionStrings).To(ContainElements("1921.0.0-metal-sci-usi-amd64", "1921.0.0"))
 	})
+
+	It("valid tag + truly invalid clean version: OLD format only (clean version skipped)", func(ctx SpecContext) {
+		result := versions(ctx, []cloudprofilesync.SourceImage{
+			{
+				Version:       "1921.0.0-metal-sci-usi-amd64",
+				CleanVersion:  "garbage",
+				Architectures: []string{"amd64"},
+			},
+		})
+		Expect(result).To(HaveLen(1))
+		Expect(result[0].Version).To(Equal("1921.0.0-metal-sci-usi-amd64"))
+	})

Note: This new test will fail until the validation fix in imageupdater.go is applied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloudprofilesync/imageupdater_test.go` around lines 84 - 96, The test named
"valid tag + invalid clean version: BOTH formats with clean version normalized"
is misleading because the CleanVersion value "1921.0" is actually tolerantly
parseable by semver.ParseTolerant and gets normalized successfully—it is not
truly invalid. Rename this test to accurately reflect that the CleanVersion is
"tolerantly parseable and normalized" rather than "invalid," and then add a new
separate test case that covers the actual edge case of a truly unparseable
CleanVersion value (such as "garbage") to properly test the validation and error
handling for invalid clean versions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cloudprofilesync/imageupdater.go`:
- Around line 108-124: The code on line 108 checks if CleanVersion is non-empty
but does not validate that it is actually a valid version string before writing
it to image.Versions. Add validation to ensure CleanVersion is truly parseable
and valid before using it in the condition that guards the append and update
logic for image.Versions. If CleanVersion is found to be invalid during
validation, skip this block entirely so invalid version strings are not written
to the image.Versions array, preventing API rejection downstream.

---

Nitpick comments:
In `@cloudprofilesync/imageupdater_test.go`:
- Around line 84-96: The test named "valid tag + invalid clean version: BOTH
formats with clean version normalized" is misleading because the CleanVersion
value "1921.0" is actually tolerantly parseable by semver.ParseTolerant and gets
normalized successfully—it is not truly invalid. Rename this test to accurately
reflect that the CleanVersion is "tolerantly parseable and normalized" rather
than "invalid," and then add a new separate test case that covers the actual
edge case of a truly unparseable CleanVersion value (such as "garbage") to
properly test the validation and error handling for invalid clean versions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8502fdb-b993-40c4-9091-d2981da6dc1e

📥 Commits

Reviewing files that changed from the base of the PR and between 88c1c72 and 6c8151d.

📒 Files selected for processing (2)
  • cloudprofilesync/imageupdater.go
  • cloudprofilesync/imageupdater_test.go

Copilot AI left a comment

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.

Pull request overview

This PR addresses Gardener API rejections during CloudProfile synchronization by preventing invalid (non-SemVer) legacy OCI tags from being written into spec.machineImages[].versions[].version, while still allowing images to be processed via a validated/normalized CleanVersion when available (GEP-33 transition behavior).

Changes:

  • Enhance filterImages() to validate legacy tags and clean versions independently, normalize tolerantly-parseable clean versions, and drop entries where both are invalid.
  • Skip writing legacy spec.machineImages[].versions entries when the raw OCI tag is not valid SemVer, avoiding API server rejection.
  • Extend Ginkgo tests to cover filtering/dual-write scenarios, including invalid legacy tags with valid clean versions.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cloudprofilesync/imageupdater.go Validates legacy tag vs clean version separately; normalizes clean versions; skips writing invalid legacy SemVer into CloudProfile spec.
cloudprofilesync/imageupdater_test.go Adds/extends tests for image filtering and legacy/clean version write behavior (flag ON/OFF).
go.mod Pins Go version with patch level.
.golangci.yaml Adjusts gomoddirectives Go version regex to allow patch versions.
.typos.toml Adds an ignore-regex for a specific identifier (ANDed).
.gitignore Ignores .claude directory.
LICENSES/CC0-1.0.txt Removes CC0 license text file.
.claude/settings.local.json Removes local Claude settings file from the repo.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cloudprofilesync/imageupdater.go
Comment thread cloudprofilesync/imageupdater_test.go
anton-paulovich and others added 4 commits June 16, 2026 11:27
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

Merging this branch will increase overall coverage

Impacted Packages Coverage Δ 🤖
github.com/cobaltcore-dev/cloud-profile-sync/cloudprofilesync 91.02% (+2.48%) 👍

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/cobaltcore-dev/cloud-profile-sync/cloudprofilesync/imageupdater.go 94.64% (+9.86%) 56 (+10) 53 (+14) 3 (-4) 👍

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files

  • github.com/cobaltcore-dev/cloud-profile-sync/cloudprofilesync/imageupdater_test.go

@anton-paulovich anton-paulovich merged commit 3e3c797 into master Jun 16, 2026
7 checks passed
@anton-paulovich anton-paulovich deleted the fix/gep-33-feature-flag branch June 16, 2026 10:08
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.

3 participants