From cf811bb11f857c7af42c174e829ccf90c0f29ef3 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 8 Sep 2025 16:07:48 -0600 Subject: [PATCH] chore: registry utils clean up Signed-off-by: Terry Howe --- pkg/registry/{util.go => chart.go} | 82 ------------- pkg/registry/{util_test.go => chart_test.go} | 0 pkg/registry/reference.go | 6 + pkg/registry/tag.go | 59 +++++++++ pkg/registry/tag_test.go | 122 +++++++++++++++++++ 5 files changed, 187 insertions(+), 82 deletions(-) rename pkg/registry/{util.go => chart.go} (61%) rename pkg/registry/{util_test.go => chart_test.go} (100%) create mode 100644 pkg/registry/tag.go create mode 100644 pkg/registry/tag_test.go diff --git a/pkg/registry/util.go b/pkg/registry/chart.go similarity index 61% rename from pkg/registry/util.go rename to pkg/registry/chart.go index 6071c66c3..b00fc616d 100644 --- a/pkg/registry/util.go +++ b/pkg/registry/chart.go @@ -18,18 +18,12 @@ package registry // import "helm.sh/helm/v4/pkg/registry" import ( "bytes" - "fmt" - "io" - "net/http" - "slices" "strings" "time" - "helm.sh/helm/v4/internal/tlsutil" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" - "github.com/Masterminds/semver/v3" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -38,52 +32,6 @@ var immutableOciAnnotations = []string{ ocispec.AnnotationTitle, } -// IsOCI determines whether a URL is to be treated as an OCI URL -func IsOCI(url string) bool { - return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme)) -} - -// ContainsTag determines whether a tag is found in a provided list of tags -func ContainsTag(tags []string, tag string) bool { - return slices.Contains(tags, tag) -} - -func GetTagMatchingVersionOrConstraint(tags []string, versionString string) (string, error) { - var constraint *semver.Constraints - if versionString == "" { - // If string is empty, set wildcard constraint - constraint, _ = semver.NewConstraint("*") - } else { - // when customer inputs specific version, check whether there's an exact match first - for _, v := range tags { - if versionString == v { - return v, nil - } - } - - // Otherwise set constraint to the string given - var err error - constraint, err = semver.NewConstraint(versionString) - if err != nil { - return "", err - } - } - - // Otherwise try to find the first available version matching the string, - // in case it is a constraint - for _, v := range tags { - test, err := semver.NewVersion(v) - if err != nil { - continue - } - if constraint.Check(test) { - return v, nil - } - } - - return "", fmt.Errorf("could not locate a version matching provided version string %s", versionString) -} - // extractChartMeta is used to extract a chart metadata from a byte array func extractChartMeta(chartData []byte) (*chart.Metadata, error) { ch, err := loader.LoadArchive(bytes.NewReader(chartData)) @@ -93,35 +41,6 @@ func extractChartMeta(chartData []byte) (*chart.Metadata, error) { return ch.Metadata, nil } -// NewRegistryClientWithTLS is a helper function to create a new registry client with TLS enabled. -func NewRegistryClientWithTLS(out io.Writer, certFile, keyFile, caFile string, insecureSkipTLSverify bool, registryConfig string, debug bool) (*Client, error) { - tlsConf, err := tlsutil.NewTLSConfig( - tlsutil.WithInsecureSkipVerify(insecureSkipTLSverify), - tlsutil.WithCertKeyPairFiles(certFile, keyFile), - tlsutil.WithCAFile(caFile), - ) - if err != nil { - return nil, fmt.Errorf("can't create TLS config for client: %s", err) - } - // Create a new registry client - registryClient, err := NewClient( - ClientOptDebug(debug), - ClientOptEnableCache(true), - ClientOptWriter(out), - ClientOptCredentialsFile(registryConfig), - ClientOptHTTPClient(&http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConf, - Proxy: http.ProxyFromEnvironment, - }, - }), - ) - if err != nil { - return nil, err - } - return registryClient, nil -} - // generateOCIAnnotations will generate OCI annotations to include within the OCI manifest func generateOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string { @@ -202,5 +121,4 @@ func addToMap(inputMap map[string]string, newKey string, newValue string) map[st } return inputMap - } diff --git a/pkg/registry/util_test.go b/pkg/registry/chart_test.go similarity index 100% rename from pkg/registry/util_test.go rename to pkg/registry/chart_test.go diff --git a/pkg/registry/reference.go b/pkg/registry/reference.go index b5677761d..bd0974e69 100644 --- a/pkg/registry/reference.go +++ b/pkg/registry/reference.go @@ -17,6 +17,7 @@ limitations under the License. package registry import ( + "fmt" "strings" "oras.land/oras-go/v2/registry" @@ -76,3 +77,8 @@ func (r *reference) String() string { } return r.orasReference.String() } + +// IsOCI determines whether a URL is to be treated as an OCI URL +func IsOCI(url string) bool { + return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme)) +} diff --git a/pkg/registry/tag.go b/pkg/registry/tag.go new file mode 100644 index 000000000..701701d7b --- /dev/null +++ b/pkg/registry/tag.go @@ -0,0 +1,59 @@ +/* +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 registry // import "helm.sh/helm/v4/pkg/registry" + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" +) + +func GetTagMatchingVersionOrConstraint(tags []string, versionString string) (string, error) { + var constraint *semver.Constraints + if versionString == "" { + // If string is empty, set wildcard constraint + constraint, _ = semver.NewConstraint("*") + } else { + // when customer inputs specific version, check whether there's an exact match first + for _, v := range tags { + if versionString == v { + return v, nil + } + } + + // Otherwise set constraint to the string given + var err error + constraint, err = semver.NewConstraint(versionString) + if err != nil { + return "", err + } + } + + // Otherwise try to find the first available version matching the string, + // in case it is a constraint + for _, v := range tags { + test, err := semver.NewVersion(v) + if err != nil { + continue + } + if constraint.Check(test) { + return v, nil + } + } + + return "", fmt.Errorf("could not locate a version matching provided version string %s", versionString) +} diff --git a/pkg/registry/tag_test.go b/pkg/registry/tag_test.go new file mode 100644 index 000000000..09f0f12ea --- /dev/null +++ b/pkg/registry/tag_test.go @@ -0,0 +1,122 @@ +/* +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 registry + +import ( + "strings" + "testing" +) + +func TestGetTagMatchingVersionOrConstraint_ExactMatch(t *testing.T) { + tags := []string{"1.0.0", "1.2.3", "2.0.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, "1.2.3") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.2.3" { + t.Fatalf("expected exact match '1.2.3', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_EmptyVersionWildcard(t *testing.T) { + // Includes a non-semver tag which should be skipped + tags := []string{"latest", "0.9.0", "1.0.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Should pick the first valid semver tag in order, which is 0.9.0 + if got != "0.9.0" { + t.Fatalf("expected '0.9.0', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_ConstraintRange(t *testing.T) { + tags := []string{"0.5.0", "1.0.0", "1.1.0", "2.0.0"} + + // Caret range + got, err := GetTagMatchingVersionOrConstraint(tags, "^1.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.0.0" { // first match in order + t.Fatalf("expected '1.0.0', got %q", got) + } + + // Compound range + got, err = GetTagMatchingVersionOrConstraint(tags, ">=1.0.0 <2.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.0.0" { + t.Fatalf("expected '1.0.0', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_InvalidConstraint(t *testing.T) { + tags := []string{"1.0.0"} + _, err := GetTagMatchingVersionOrConstraint(tags, ">a1") + if err == nil { + t.Fatalf("expected error for invalid constraint") + } +} + +func TestGetTagMatchingVersionOrConstraint_NoMatches(t *testing.T) { + tags := []string{"0.1.0", "0.2.0"} + _, err := GetTagMatchingVersionOrConstraint(tags, ">=1.0.0") + if err == nil { + t.Fatalf("expected error when no tags match") + } + if !strings.Contains(err.Error(), ">=1.0.0") { + t.Fatalf("expected error to contain version string, got: %v", err) + } +} + +func TestGetTagMatchingVersionOrConstraint_SkipsNonSemverTags(t *testing.T) { + tags := []string{"alpha", "1.0.0", "beta", "1.1.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, ">=1.0.0 <2.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.0.0" { + t.Fatalf("expected '1.0.0', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_OrderMatters_FirstMatchReturned(t *testing.T) { + // Both 1.2.0 and 1.3.0 satisfy >=1.2.0 <2.0.0, but the function returns the first in input order + tags := []string{"1.3.0", "1.2.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, ">=1.2.0 <2.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.3.0" { + t.Fatalf("expected '1.3.0' (first satisfying tag), got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_ExactMatchHasPrecedence(t *testing.T) { + // Exact match should be returned even if another earlier tag would match the parsed constraint + tags := []string{"1.3.0", "1.2.3"} + got, err := GetTagMatchingVersionOrConstraint(tags, "1.2.3") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.2.3" { + t.Fatalf("expected exact match '1.2.3', got %q", got) + } +}