From 49d7aac2d29dec9aba73b127f16f330d10886dad Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Wed, 1 Oct 2025 08:37:18 +0200 Subject: [PATCH] Add basic registry BuildPushRef to be used in ocipusher We want an easy way to check for chart version provided Signed-off-by: Benoit Tigeot --- pkg/registry/pushref.go | 65 +++++++++++++++++++++ pkg/registry/pushref_test.go | 106 +++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 pkg/registry/pushref.go create mode 100644 pkg/registry/pushref_test.go diff --git a/pkg/registry/pushref.go b/pkg/registry/pushref.go new file mode 100644 index 000000000..bd2b2e537 --- /dev/null +++ b/pkg/registry/pushref.go @@ -0,0 +1,65 @@ +/* +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 ( + "fmt" + "path" + "strings" +) + +func BuildPushRef(href, chartName, chartVersion string) (string, error) { + ref, err := newReference(href) + if err != nil { + return "", err + } + + if ref.Digest != "" { + return "", fmt.Errorf("cannot push to a reference with a digest: %q. Only tags are allowed", href) + } + + // Normalize chart version for tag comparison/build (registry tags cannot contain '+') + normalizedVersion := strings.ReplaceAll(chartVersion, "+", "_") + + // Determine final tag: + // - if href tag present, it must match normalized chart version + // - else use chart version + finalTag := normalizedVersion + if ref.Tag != "" { + if ref.Tag != normalizedVersion { + return "", fmt.Errorf("tag %q does not match provided chart version %q", ref.Tag, chartVersion) + } + finalTag = ref.Tag + } + + // Ensure repository ends with the chart name once (avoid duplication) + finalRepo := ref.Repository + if chartName != "" { + last := chartName + // Extract last segment of current repository path + if idx := strings.LastIndex(finalRepo, "/"); idx >= 0 { + last = finalRepo[idx+1:] + } else { + last = finalRepo + } + if last != chartName { + finalRepo = path.Join(finalRepo, chartName) + } + } + + return fmt.Sprintf("%s/%s:%s", ref.Registry, finalRepo, finalTag), nil +} diff --git a/pkg/registry/pushref_test.go b/pkg/registry/pushref_test.go new file mode 100644 index 000000000..cfdcdb387 --- /dev/null +++ b/pkg/registry/pushref_test.go @@ -0,0 +1,106 @@ +/* +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 ( + "reflect" + "testing" +) + +func TestBuildPushRef(t *testing.T) { + tests := []struct { + name string + registry string + chart string + version string + want string + wantErr bool + }{ + { + name: "simple case", + registry: "oci://my-registry.io/my-repo:1.0.0", + chart: "my-repo", + version: "1.0.0", + want: "my-registry.io/my-repo:1.0.0", + wantErr: false, + }, + { + name: "append chart name to repo", + registry: "oci://my-registry.io/ns", + chart: "my-repo", + version: "1.0.0", + want: "my-registry.io/ns/my-repo:1.0.0", + wantErr: false, + }, + { + name: "digest not allowed", + registry: "oci://my-registry.io/my-repo@sha256:abcdef1234567890", + chart: "my-repo", + version: "1.0.0", + want: "", + wantErr: true, + }, + { + name: "invalid registry", + registry: "invalid-registry", + chart: "my-repo", + version: "1.0.0", + want: "", + wantErr: true, + }, + { + name: "tag mismatch", + registry: "oci://my-registry.io/my-repo:2.0.0", + chart: "my-repo", + version: "1.0.0", + want: "", + wantErr: true, + }, + { + name: "plus to underscore normalization", + registry: "oci://my-registry.io/my-repo:1.0.0_abc", + chart: "my-repo", + version: "1.0.0+abc", + want: "my-registry.io/my-repo:1.0.0_abc", + wantErr: false, + }, + } + + for _, tt := range tests { + + if tt.wantErr { + t.Run(tt.name, func(t *testing.T) { + _, err := BuildPushRef(tt.registry, tt.chart, tt.version) + if err == nil { + t.Errorf("BuildPushRef() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } else { + t.Run(tt.name, func(t *testing.T) { + got, err := BuildPushRef(tt.registry, tt.chart, tt.version) + if err != nil { + t.Errorf("BuildPushRef() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("BuildPushRef() got = %v, want %v", got, tt.want) + } + }) + } + } +}