-
Notifications
You must be signed in to change notification settings - Fork 12
Fix: Handle OCI artifact versions with '+' correctly #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,9 +20,12 @@ import ( | |
| ) | ||
|
|
||
| // Returns whether this url should be handled by the Blob handler | ||
| // This is complicated because Blob is indicated by the trailing path, not the leading path. | ||
| // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-a-layer | ||
| // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-a-layer | ||
| // This is complicated because Blob is indicated by the trailing path, not the l | ||
| eading path. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Line is not commented, resulting in an identifier at package scope and causing a syntax error that breaks compilation. |
||
| // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulli | ||
| ng-a-layer | ||
| // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushi | ||
| ng-a-layer | ||
| func IsBlob(req *http.Request) bool { | ||
| elem := strings.Split(req.URL.Path, "/") | ||
| elem = elem[1:] | ||
|
|
@@ -71,3 +74,58 @@ func IsV2(req *http.Request) bool { | |
| } | ||
| return elems[len(elems)-1] == "v2" | ||
| } | ||
|
|
||
| // SemVerReplace replaces all underscores in a given string with plus signs. | ||
| // This function is primarily intended for use with semantic version strings | ||
| // where underscores might have been used in place of plus signs (e.g., for | ||
| // build metadata or pre-release identifiers). Standard Semantic Versioning | ||
| // (semver.org) specifies plus signs for build metadata (e.g., "1.0.0+20130313144700") | ||
| // and hyphens for pre-release identifiers (e.g., "1.0.0-alpha"). | ||
| // | ||
| // Purpose: | ||
| // The main purpose of this function is to normalize version strings by converting | ||
| // any underscores to plus signs. This can be particularly useful when dealing with | ||
| // version strings from systems or sources that use underscores due to constraints | ||
| // (e.g., where '+' is a special character) or by convention for information that | ||
| // semantically aligns with build metadata. | ||
| // | ||
| // When to use it: | ||
| // Use this function when you encounter version strings like "v1.2.3_build456" or | ||
| // "2.0.0_rc_1" and need to transform them into a format like "v1.2.3+build456" or | ||
| // "2.0.0+rc+1". This transformation is often a preparatory step before parsing | ||
| // the string with a semantic versioning library that strictly expects '+' for | ||
| // build metadata, or when aiming for a consistent display format for version information. | ||
| // | ||
| // Transformation Examples: | ||
| // - Input: "1.0.0_alpha" | ||
| // Output: "1.0.0+alpha" | ||
| // - Input: "v2.1.3_beta_build123" (handles multiple underscores) | ||
| // Output: "v2.1.3+beta+build123" | ||
| // - Input: "1.2.3" (string with no underscores) | ||
| // Output: "1.2.3" (string remains unchanged) | ||
| // - Input: "" (empty string) | ||
| // Output: "" (empty string remains unchanged) | ||
| // | ||
| // Semver Validation: | ||
| // This function does NOT perform validation of the overall semantic version string structure. | ||
| // For example, it does not check if the version string conforms to the MAJOR.MINOR.PATCH | ||
| // numerical format or other specific semver rules. Its sole responsibility is to | ||
| // replace every occurrence of the underscore character '_' with a plus sign '+'. | ||
| // For comprehensive semver parsing and validation, it is recommended to use a | ||
| // dedicated semver library on the string after this transformation, if necessary. | ||
| // | ||
| // Edge Cases Handled: | ||
| // - Multiple underscores: All occurrences of underscores are replaced. | ||
| // For instance, "1.0.0_alpha_snapshot" becomes "1.0.0+alpha+snapshot". | ||
| // - Empty string: If an empty string is provided, an empty string is returned. | ||
| // - String without underscores: If the string does not contain any underscores, | ||
| // it is returned as is. | ||
| func SemVerReplace(semver string) string { | ||
| // strings.ReplaceAll is efficient and handles edge cases gracefully: | ||
| // - If `semver` is an empty string, it returns an empty string. | ||
| // - If `semver` does not contain "_", it returns `semver` unchanged. | ||
| // - It replaces all occurrences of "_" with "+". | ||
| // Therefore, the original conditional check (if semver != "" && strings.Contains(semver, "_")) | ||
| // is not strictly necessary for correctness. | ||
| return strings.ReplaceAll(semver, "_", "+") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package helper | ||
|
|
||
| import ( | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestSemVerReplace(t *testing.T) { | ||
| testCases := []struct { | ||
| name string | ||
| input string | ||
| expected string | ||
| }{ | ||
| { | ||
| name: "empty string", | ||
| input: "", | ||
| expected: "", | ||
| }, | ||
| { | ||
| name: "no underscores", | ||
| input: "1.0.0", | ||
| expected: "1.0.0", | ||
| }, | ||
| { | ||
| name: "one underscore", | ||
| input: "1.0.0_alpha", | ||
| expected: "1.0.0+alpha", | ||
| }, | ||
| { | ||
| name: "multiple underscores", | ||
| input: "1.0.0_alpha_build123", | ||
| expected: "1.0.0+alpha+build123", | ||
| }, | ||
| { | ||
| name: "leading and trailing underscores", | ||
| input: "_1.0.0_", | ||
| expected: "+1.0.0+", | ||
| }, | ||
| { | ||
| name: "only underscores", | ||
| input: "___", | ||
| expected: "+++", | ||
| }, | ||
| { | ||
| name: "mixed case and underscores", | ||
| input: "v1.2.3_Rc1_candidate", | ||
| expected: "v1.2.3+Rc1+candidate", | ||
| }, | ||
| { | ||
| name: "no underscores with v prefix", | ||
| input: "v2.3.4", | ||
| expected: "v2.3.4", | ||
| }, | ||
| { | ||
| name: "already has plus (should not change)", | ||
| input: "1.0.0+beta", | ||
| expected: "1.0.0+beta", | ||
| }, | ||
| { | ||
| name: "mixed plus and underscore", | ||
| input: "1.0.0_alpha+beta_rc1", | ||
| expected: "1.0.0+alpha+beta+rc1", | ||
| }, | ||
| } | ||
|
|
||
| for _, tc := range testCases { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The loop variable tc is captured by reference inside the sub-test closure. If t.Parallel is added later, sub-tests could run concurrently and observe the mutated tc value, leading to flaky tests. Create a new local variable (tc := tc) before calling t.Run to capture the current iteration value. |
||
| t.Run(tc.name, func(t *testing.T) { | ||
| actual := SemVerReplace(tc.input) | ||
| if actual != tc.expected { | ||
| t.Errorf("SemVerReplace(%q) = %q; want %q", tc.input, actual, tc.expected) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ import ( | |||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||
| "github.com/container-registry/helm-charts-oci-proxy/internal/blobs/handler" | ||||||||||||||||||||||||||||||||||||||
| "github.com/container-registry/helm-charts-oci-proxy/internal/errors" | ||||||||||||||||||||||||||||||||||||||
| "github.com/container-registry/helm-charts-oci-proxy/internal/helper" | ||||||||||||||||||||||||||||||||||||||
| "github.com/opencontainers/go-digest" | ||||||||||||||||||||||||||||||||||||||
| ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||||||||||||||||||||||||||||||||||||||
| "helm.sh/helm/v3/pkg/chart" | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -47,10 +48,19 @@ func (m *Manifests) prepareChart(ctx context.Context, repo string, reference str | |||||||||||||||||||||||||||||||||||||
| m.log.Printf("searching index for %s with reference %s\n", chart, reference) | ||||||||||||||||||||||||||||||||||||||
| chartVer, err := index.Get(chart, reference) | ||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||
| originalReference := reference // Store Original Reference | ||||||||||||||||||||||||||||||||||||||
| if m.config.Debug { | ||||||||||||||||||||||||||||||||||||||
| m.log.Printf("Chart lookup for '%s' with reference '%s' failed. Attempting again after converting underscores to plus signs in reference.", chart, originalReference) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| reference = helper.SemVerReplace(originalReference) // Use originalReference for conversion | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| reference = helper.SemVerReplace(originalReference) // Use originalReference for conversion | |
| reference = helper.SemVerReplace(originalReference) // Centralized underscore-to-plus conversion |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
originalReference is undefined here because it was declared inside the previous if-block and is out of scope, causing a compilation error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix potential undefined variable in error message
The variable originalReference might be undefined if the initial lookup doesn't fail but the retry fails. Ensure it's always defined before the error message.
if err != nil {
+ originalReference := reference // Ensure originalReference is always defined
return &errors.RegError{
Status: http.StatusNotFound,
Code: "NOT FOUND",
Message: fmt.Sprintf("Chart: %s version: %s not found. Attempted lookup with original reference and after converting underscores to plus signs. Last error: %v", chart, originalReference, err),
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return &errors.RegError{ | |
| Status: http.StatusNotFound, | |
| Code: "NOT FOUND", | |
| Message: fmt.Sprintf("Chart: %s version: %s not found: %v", chart, reference, err), | |
| Message: fmt.Sprintf("Chart: %s version: %s not found. Attempted lookup with original reference and after converting underscores to plus signs. Last error: %v", chart, originalReference, err), | |
| } | |
| if err != nil { | |
| // Ensure originalReference is always defined before using it in the error message | |
| originalReference := reference | |
| return &errors.RegError{ | |
| Status: http.StatusNotFound, | |
| Code: "NOT FOUND", | |
| Message: fmt.Sprintf( | |
| "Chart: %s version: %s not found. Attempted lookup with original reference and after converting underscores to plus signs. Last error: %v", | |
| chart, originalReference, err, | |
| ), | |
| } | |
| } |
🧰 Tools
🪛 golangci-lint (1.64.8)
63-63: undefined: originalReference
(typecheck)
🤖 Prompt for AI Agents
In internal/manifest/charts.go around lines 60 to 64, the variable
originalReference used in the error message may be undefined if the initial
lookup succeeds but the retry fails. To fix this, ensure originalReference is
always assigned a value before this error return statement, either by
initializing it earlier or by capturing it from the first lookup attempt, so it
is defined regardless of which lookup fails.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -160,11 +160,19 @@ func (m *Manifests) Handle(resp http.ResponseWriter, req *http.Request) error { | |
|
|
||
| ma, ok = c[target] | ||
| if !ok { | ||
| // we failed | ||
| return &errors.RegError{ | ||
| Status: http.StatusNotFound, | ||
| Code: "NOT FOUND", | ||
| Message: fmt.Sprintf("Chart prepare's result not found: %v, %v", repo, target), | ||
| originalTarget := target // Store original target | ||
| if m.config.Debug { | ||
| m.log.Printf("GET: Chart lookup failed for repo %s with target %s after prepareChart. Attempting again after converting underscores to plus signs.", repo, originalTarget) | ||
| } | ||
| target = helper.SemVerReplace(originalTarget) // Use originalTarget for conversion | ||
| ma, ok = c[target] // Attempt lookup with the new target | ||
| if !ok { | ||
| // we failed again | ||
| return &errors.RegError{ | ||
| Status: http.StatusNotFound, | ||
| Code: "NOT FOUND", | ||
| Message: fmt.Sprintf("GET: Chart prepare's result not found for repo %s. Tried target '%s' and after underscore conversion '%s'.", repo, originalTarget, target), | ||
| } | ||
| } | ||
|
Comment on lines
+163
to
176
|
||
| } | ||
| } | ||
|
|
@@ -192,17 +200,28 @@ func (m *Manifests) Handle(resp http.ResponseWriter, req *http.Request) error { | |
| } | ||
| ma, ok := m.manifests[repo][target] | ||
| if !ok { | ||
| // First lookup failed, try preparing chart (which might involve its own SemVerReplace) | ||
| err := m.prepareChart(req.Context(), repo, target) | ||
| if err != nil { | ||
| return err | ||
| return err // Error from prepareChart | ||
| } | ||
| // After prepareChart, try lookup again with the potentially modified target from prepareChart | ||
| // and then with SemVerReplace if that also fails. | ||
| ma, ok = m.manifests[repo][target] | ||
| if !ok { | ||
| // we failed | ||
| return &errors.RegError{ | ||
| Status: http.StatusNotFound, | ||
| Code: "NOT FOUND", | ||
| Message: "Chart prepare error", | ||
| originalTarget := target // Store target before SemVerReplace | ||
| if m.config.Debug { | ||
| m.log.Printf("HEAD: Manifest not found for repo %s with target %s even after prepareChart. Attempting again after converting underscores to plus signs.", repo, originalTarget) | ||
| } | ||
| target = helper.SemVerReplace(originalTarget) // Convert underscores | ||
| ma, ok = m.manifests[repo][target] // Final attempt | ||
| if !ok { | ||
| // All attempts failed | ||
| return &errors.RegError{ | ||
| Status: http.StatusNotFound, | ||
| Code: "NOT FOUND", | ||
| Message: fmt.Sprintf("HEAD: Chart manifest not found for repo %s. Tried target '%s' and after underscore conversion '%s'.", repo, originalTarget, target), | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix line wrapping issue in comment
There's a syntax error in the comment where "leading" is split across two lines. This breaks the code and causes compilation errors that affect other files.
📝 Committable suggestion
🧰 Tools
🪛 golangci-lint (1.64.8)
24-24: expected declaration, found eading
(typecheck)
🤖 Prompt for AI Agents