From 1bfb972fafb78b83e6cd3d4699af5756bc7d8046 Mon Sep 17 00:00:00 2001 From: waterWang <672684719@qq.com> Date: Fri, 28 Aug 2026 17:57:25 +0800 Subject: [PATCH] fix(resolver): report error when no OCI tag satisfies the constraint For OCI dependencies the 'found' flag was initialized to true and never reset, so when no tag in the registry satisfied the version constraint (e.g. a range like '1.x.x' or '>=1.1.10' with no matching tag), the resolver silently wrote the raw constraint string into Chart.lock instead of raising the 'can't get a valid version for N subchart(s)' error. The non-OCI branch already resets found to false; this change makes the OCI branch consistent with it. Adds regression tests covering (1) no matching tag -> error, (2) matching tag still resolves to the highest version, and (3) an explicit version still resolves without hitting the registry. Signed-off-by: waterWang <672684719@qq.com> --- internal/resolver/oci_constraint_test.go | 130 +++++++++++++++++++++++ internal/resolver/resolver.go | 6 ++ 2 files changed, 136 insertions(+) create mode 100644 internal/resolver/oci_constraint_test.go diff --git a/internal/resolver/oci_constraint_test.go b/internal/resolver/oci_constraint_test.go new file mode 100644 index 000000000..2a795f001 --- /dev/null +++ b/internal/resolver/oci_constraint_test.go @@ -0,0 +1,130 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resolver + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/registry" +) + +// fakeOCIRegistry is a minimal OCI registry stub that serves the tag list +// endpoint (GET /v2//tags/list) with the given tags. +type fakeOCIRegistry struct { + tags map[string][]string +} + +func newFakeOCIRegistry(tags map[string][]string) *fakeOCIRegistry { + return &fakeOCIRegistry{tags: tags} +} + +func (f *fakeOCIRegistry) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := strings.TrimSuffix(r.URL.Path, "/") + // Only the tag list endpoint is needed by the resolver. + if strings.HasSuffix(path, "/tags/list") { + repo := strings.TrimPrefix(path, "/v2/") + repo = strings.TrimSuffix(repo, "/tags/list") + tags := f.tags[repo] + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "name": repo, + "tags": tags, + }) + return + } + w.WriteHeader(http.StatusNotFound) +} + +// TestResolveOCIConstraintNoMatch reports an error when no OCI tag satisfies +// the version constraint, instead of silently writing the raw constraint +// string into the lock file. +func TestResolveOCIConstraintNoMatch(t *testing.T) { + server := httptest.NewServer(newFakeOCIRegistry(map[string][]string{ + "my-registry/my-subchart": {"1.1.9"}, + })) + defer server.Close() + + host := strings.TrimPrefix(server.URL, "http://") + registryClient, err := registry.NewClient(registry.ClientOptPlainHTTP()) + require.NoError(t, err) + + r := New("testdata/chartpath", "testdata/repository", registryClient) + + reqs := []*chart.Dependency{ + {Name: "my-subchart", Repository: fmt.Sprintf("oci://%s/my-registry", host), Version: ">=1.1.10"}, + } + + _, err = r.Resolve(reqs, map[string]string{"my-subchart": "my-registry"}) + require.Error(t, err, "expected an error when no OCI tag satisfies the constraint") + require.Contains(t, err.Error(), "can't get a valid version") +} + +// TestResolveOCIConstraintMatch still resolves when a tag satisfies the +// constraint (guard against over-eager found=false). +func TestResolveOCIConstraintMatch(t *testing.T) { + server := httptest.NewServer(newFakeOCIRegistry(map[string][]string{ + "my-registry/my-subchart": {"1.1.9", "1.1.17"}, + })) + defer server.Close() + + host := strings.TrimPrefix(server.URL, "http://") + registryClient, err := registry.NewClient(registry.ClientOptPlainHTTP()) + require.NoError(t, err) + + r := New("testdata/chartpath", "testdata/repository", registryClient) + + reqs := []*chart.Dependency{ + {Name: "my-subchart", Repository: fmt.Sprintf("oci://%s/my-registry", host), Version: ">=1.1.10"}, + } + + lock, err := r.Resolve(reqs, map[string]string{"my-subchart": "my-registry"}) + require.NoError(t, err) + require.Len(t, lock.Dependencies, 1) + require.Equal(t, "1.1.17", lock.Dependencies[0].Version) +} + +// TestResolveOCIExplicitVersion pins an explicit (non-range) version without +// hitting the registry: the found=false change must not break the explicit +// version short-circuit. +func TestResolveOCIExplicitVersion(t *testing.T) { + server := httptest.NewServer(newFakeOCIRegistry(map[string][]string{ + "my-registry/my-subchart": {"1.1.9", "1.1.17"}, + })) + defer server.Close() + + host := strings.TrimPrefix(server.URL, "http://") + registryClient, err := registry.NewClient(registry.ClientOptPlainHTTP()) + require.NoError(t, err) + + r := New("testdata/chartpath", "testdata/repository", registryClient) + + reqs := []*chart.Dependency{ + {Name: "my-subchart", Repository: fmt.Sprintf("oci://%s/my-registry", host), Version: "1.1.17"}, + } + + lock, err := r.Resolve(reqs, map[string]string{"my-subchart": "my-registry"}) + require.NoError(t, err) + require.Len(t, lock.Dependencies, 1) + require.Equal(t, "1.1.17", lock.Dependencies[0].Version) +} diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index 5f0c5b148..e2c002eb8 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -133,6 +133,12 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string } found = false } else { + // OCI dependency: like the non-OCI branch, only a version that + // satisfies the constraint should count as found. Otherwise a + // missing/unmatched tag set would silently write the raw + // constraint (e.g. "1.x.x") into Chart.lock instead of raising + // the "can't get a valid version" error below. + found = false version = d.Version // Check to see if an explicit version has been provided