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>
pull/32589/head
waterWang 1 month ago
parent d62bee21c2
commit 1bfb972faf

@ -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/<repository>/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)
}

@ -133,6 +133,12 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
} }
found = false found = false
} else { } 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 version = d.Version
// Check to see if an explicit version has been provided // Check to see if an explicit version has been provided

Loading…
Cancel
Save