Skip to content

Commit b7e54e5

Browse files
evankandersonmatheuscscp
authored andcommitted
Support Helm semver encoding in OCI repositories
Signed-off-by: Evan Anderson <evan.k.anderson@gmail.com> Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
1 parent 89be261 commit b7e54e5

4 files changed

Lines changed: 70 additions & 13 deletions

File tree

internal/controller/ocirepository_controller.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/google/go-containerregistry/pkg/v1/remote"
4141
"github.com/notaryproject/notation-go/verifier/trustpolicy"
4242
"github.com/sigstore/cosign/v3/pkg/cosign"
43+
"helm.sh/helm/v4/pkg/registry"
4344
corev1 "k8s.io/api/core/v1"
4445
"k8s.io/apimachinery/pkg/runtime"
4546
"k8s.io/apimachinery/pkg/types"
@@ -871,7 +872,7 @@ func (r *OCIRepositoryReconciler) getArtifactRef(obj *sourcev1.OCIRepository, op
871872
}
872873

873874
if obj.Spec.Reference.SemVer != "" {
874-
return r.getTagBySemver(repo, obj.Spec.Reference.SemVer, filterTags(obj.Spec.Reference.SemverFilter), options)
875+
return r.getTagBySemver(repo, obj.Spec.Reference.SemVer, filterTags(obj.Spec.Reference.SemverFilter), obj.GetLayerMediaType(), options)
875876
}
876877

877878
if obj.Spec.Reference.Tag != "" {
@@ -884,7 +885,7 @@ func (r *OCIRepositoryReconciler) getArtifactRef(obj *sourcev1.OCIRepository, op
884885

885886
// getTagBySemver call the remote container registry, fetches all the tags from the repository,
886887
// and returns the latest tag according to the semver expression.
887-
func (r *OCIRepositoryReconciler) getTagBySemver(repo name.Repository, exp string, filter filterFunc, options []remote.Option) (name.Reference, error) {
888+
func (r *OCIRepositoryReconciler) getTagBySemver(repo name.Repository, exp string, filter filterFunc, mediaType string, options []remote.Option) (name.Reference, error) {
888889
tags, err := remote.List(repo, options...)
889890
if err != nil {
890891
return nil, err
@@ -901,8 +902,9 @@ func (r *OCIRepositoryReconciler) getTagBySemver(repo name.Repository, exp strin
901902
}
902903

903904
var matchingVersions []*semver.Version
904-
for _, t := range validTags {
905-
v, err := version.ParseVersion(t)
905+
for _, ociTag := range validTags {
906+
semVerTag := convertOCIToSemVerTag(ociTag, mediaType)
907+
v, err := version.ParseVersion(semVerTag)
906908
if err != nil {
907909
continue
908910
}
@@ -916,8 +918,42 @@ func (r *OCIRepositoryReconciler) getTagBySemver(repo name.Repository, exp strin
916918
return nil, fmt.Errorf("no match found for semver: %s", exp)
917919
}
918920

921+
// Find the latest SemVer.
919922
sort.Sort(sort.Reverse(semver.Collection(matchingVersions)))
920-
return repo.Tag(matchingVersions[0].Original()), nil
923+
semVerTag := matchingVersions[0].Original()
924+
925+
// Convert the latest SemVer to an OCI tag and return the reference.
926+
ociTag := convertSemVerToOCITag(semVerTag, mediaType)
927+
return repo.Tag(ociTag), nil
928+
}
929+
930+
// convertSemVerToOCITag converts a SemVer tag to an OCI tag
931+
// according to rules defined by the media type.
932+
//
933+
// For OCI Helm charts, the conversion is mapping `+` to `_`,
934+
// because `+` is not permitted in OCI tags, while `_` is not
935+
// permitted in SemVer. Each character not being permitted in
936+
// one of the two sides establishes a perfect bijection between,
937+
// which then makes the mapping implemented by Helm (and honored
938+
// here) completely safe.
939+
func convertSemVerToOCITag(semVer, mediaType string) string {
940+
if mediaType == registry.ChartLayerMediaType {
941+
return strings.ReplaceAll(semVer, "+", "_")
942+
}
943+
return semVer
944+
}
945+
946+
// convertOCIToSemVerTag converts an OCI tag to a SemVer tag
947+
// according to rules defined by the media type.
948+
//
949+
// For OCI Helm charts, the conversion is mapping `_` to `+`,
950+
// see the comment above on convertSemVerToOCITag for the
951+
// mapping in the opposite direction and rationale.
952+
func convertOCIToSemVerTag(ociTag, mediaType string) string {
953+
if mediaType == registry.ChartLayerMediaType {
954+
return strings.ReplaceAll(ociTag, "_", "+")
955+
}
956+
return ociTag
921957
}
922958

923959
// keychain generates the credential keychain based on the resource

internal/controller/ocirepository_controller_test.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"github.com/notaryproject/notation-go/signer"
4444
"github.com/notaryproject/notation-go/verifier/trustpolicy"
4545
. "github.com/onsi/gomega"
46+
"github.com/onsi/gomega/types"
4647
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
4748
coptions "github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
4849
"github.com/sigstore/cosign/v3/cmd/cosign/cli/sign"
@@ -2892,44 +2893,61 @@ func TestOCIRepository_getArtifactRef(t *testing.T) {
28922893
"6.1.5",
28932894
"6.1.6-rc.1",
28942895
"6.1.6",
2896+
"6.2.1_ref.1234567", // Version 6.2.1+ref.1234567, encoded as a tag
2897+
"6.2.1", // Version 6.2.1, same precedence as 6.2.1, per semver rule 10
28952898
)
28962899
g.Expect(err).ToNot(HaveOccurred())
28972900

28982901
tests := []struct {
28992902
name string
29002903
url string
29012904
reference *sourcev1.OCIRepositoryRef
2905+
selector *sourcev1.OCILayerSelector
29022906
wantErr bool
2903-
want string
2907+
want types.GomegaMatcher
29042908
}{
29052909
{
29062910
name: "valid url with no reference",
29072911
url: "oci://ghcr.io/stefanprodan/charts",
2908-
want: "ghcr.io/stefanprodan/charts:latest",
2912+
want: Equal("ghcr.io/stefanprodan/charts:latest"),
29092913
},
29102914
{
29112915
name: "valid url with tag reference",
29122916
url: "oci://ghcr.io/stefanprodan/charts",
29132917
reference: &sourcev1.OCIRepositoryRef{
29142918
Tag: "6.1.6",
29152919
},
2916-
want: "ghcr.io/stefanprodan/charts:6.1.6",
2920+
want: Equal("ghcr.io/stefanprodan/charts:6.1.6"),
29172921
},
29182922
{
29192923
name: "valid url with digest reference",
29202924
url: "oci://ghcr.io/stefanprodan/charts",
29212925
reference: &sourcev1.OCIRepositoryRef{
29222926
Digest: imgs["6.1.6"].digest.String(),
29232927
},
2924-
want: "ghcr.io/stefanprodan/charts@" + imgs["6.1.6"].digest.String(),
2928+
want: Equal("ghcr.io/stefanprodan/charts@" + imgs["6.1.6"].digest.String()),
29252929
},
29262930
{
29272931
name: "valid url with semver reference",
29282932
url: fmt.Sprintf("oci://%s/podinfo", server.registryHost),
29292933
reference: &sourcev1.OCIRepositoryRef{
2930-
SemVer: ">= 6.1.6",
2934+
SemVer: "~6.1.x",
2935+
},
2936+
want: Equal(server.registryHost + "/podinfo:6.1.6"),
2937+
},
2938+
{
2939+
name: "valid url with semver reference and build identifier",
2940+
url: fmt.Sprintf("oci://%s/podinfo", server.registryHost),
2941+
reference: &sourcev1.OCIRepositoryRef{
2942+
SemVer: ">= 6.2.0",
29312943
},
2932-
want: server.registryHost + "/podinfo:6.1.6",
2944+
selector: &sourcev1.OCILayerSelector{
2945+
MediaType: "application/vnd.cncf.helm.chart.content.v1.tar+gzip",
2946+
},
2947+
// Build info does not have a defined sort order in SemVer, so these
2948+
// two are equivalently new.
2949+
want: Or(Equal(server.registryHost+"/podinfo:6.2.1_ref.1234567"),
2950+
Equal(server.registryHost+"/podinfo:6.2.1")),
29332951
},
29342952
{
29352953
name: "invalid url without oci prefix",
@@ -2943,7 +2961,7 @@ func TestOCIRepository_getArtifactRef(t *testing.T) {
29432961
SemVer: ">= 6.1.x-0",
29442962
SemverFilter: ".*-rc.*",
29452963
},
2946-
want: server.registryHost + "/podinfo:6.1.6-rc.1",
2964+
want: Equal(server.registryHost + "/podinfo:6.1.6-rc.1"),
29472965
},
29482966
{
29492967
name: "valid url with semver filter and unexisting version",
@@ -2984,6 +3002,9 @@ func TestOCIRepository_getArtifactRef(t *testing.T) {
29843002
if tt.reference != nil {
29853003
obj.Spec.Reference = tt.reference
29863004
}
3005+
if tt.selector != nil {
3006+
obj.Spec.LayerSelector = tt.selector
3007+
}
29873008

29883009
opts := makeRemoteOptions(ctx, makeTransport(true), authn.DefaultKeychain, nil)
29893010
got, err := r.getArtifactRef(obj, opts)
@@ -2992,7 +3013,7 @@ func TestOCIRepository_getArtifactRef(t *testing.T) {
29923013
return
29933014
}
29943015
g.Expect(err).ToNot(HaveOccurred())
2995-
g.Expect(got.String()).To(Equal(tt.want))
3016+
g.Expect(got.String()).To(tt.want)
29963017
})
29973018
}
29983019
}
14.5 KB
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)