Add --oci-strict-version flag to push

Signed-off-by: Daniel Fox <danudey@me.com>
pull/32349/head
Daniel Fox 3 weeks ago
parent 68977ec0b5
commit f3cc94268b
No known key found for this signature in database

@ -37,6 +37,7 @@ type Push struct {
caFile string
insecureSkipTLSVerify bool
plainHTTP bool
ociStrictVersion bool
out io.Writer
}
@ -73,6 +74,14 @@ func WithPlainHTTP(plainHTTP bool) PushOpt {
}
}
// WithOCIStrictVersion configures whether the OCI tag is derived from the
// parsed/sanitized semver representation of the chart version.
func WithOCIStrictVersion(ociStrictVersion bool) PushOpt {
return func(p *Push) {
p.ociStrictVersion = ociStrictVersion
}
}
// WithPushOptWriter sets the registryOut field on the push configuration object.
func WithPushOptWriter(out io.Writer) PushOpt {
return func(p *Push) {
@ -100,6 +109,7 @@ func (p *Push) Run(chartRef string, remote string) (string, error) {
pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile),
pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSVerify),
pusher.WithPlainHTTP(p.plainHTTP),
pusher.WithOCIStrictVersion(p.ociStrictVersion),
},
}

@ -40,6 +40,7 @@ type registryPushOptions struct {
caFile string
insecureSkipTLSVerify bool
plainHTTP bool
ociStrictVersion bool
password string
username string
}
@ -84,6 +85,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile),
action.WithInsecureSkipTLSVerify(o.insecureSkipTLSVerify),
action.WithPlainHTTP(o.plainHTTP),
action.WithOCIStrictVersion(o.ociStrictVersion),
action.WithPushOptWriter(out))
client.Settings = settings
output, err := client.Run(chartRef, remote)
@ -101,6 +103,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart upload")
f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload")
f.BoolVar(&o.ociStrictVersion, "oci-strict-version", false, "derive the OCI tag from the parsed/sanitized semver representation of the chart version")
f.StringVar(&o.username, "username", "", "chart repository username where to locate the requested chart")
f.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart")

@ -26,6 +26,8 @@ import (
"strings"
"time"
"github.com/Masterminds/semver/v3"
"helm.sh/helm/v4/internal/tlsutil"
"helm.sh/helm/v4/pkg/chart/v2/loader"
"helm.sh/helm/v4/pkg/registry"
@ -85,9 +87,26 @@ func (pusher *OCIPusher) push(chartRef, href string) error {
pushOpts = append(pushOpts, registry.PushOptProvData(provBytes))
}
// Resolve the version used as the base of the OCI tag. With strict
// versioning enabled this is the sanitized semver form of the chart
// version; otherwise it is the raw chart version. The registry client
// still applies its usual tag transformations (e.g. replacing plus (+)
// signs with underscores) afterwards.
version, err := resolveOCITagVersion(meta.Metadata.Version, pusher.opts.ociStrictVersion)
if err != nil {
return err
}
// The sanitized version may differ from the raw chart version, so relax
// the registry client's strict-mode assertion (which requires the tag to
// equal the raw chart version) when it does.
if version != meta.Metadata.Version {
pushOpts = append(pushOpts, registry.PushOptStrictMode(false))
}
ref := fmt.Sprintf("%s:%s",
path.Join(strings.TrimPrefix(href, registry.OCIScheme+"://"), meta.Metadata.Name),
meta.Metadata.Version)
version)
// The time the chart was "created" is semantically the time the chart archive file was last written(modified)
chartArchiveFileCreatedTime := stat.ModTime()
@ -97,6 +116,24 @@ func (pusher *OCIPusher) push(chartRef, href string) error {
return err
}
// resolveOCITagVersion returns the version string to use as the base of the OCI
// tag for a chart. When ociStrictVersion is false the raw chart version is
// returned unchanged. When it is true the version is parsed with semver and its
// sanitized string representation is returned, so that a canonical semver tag is
// produced regardless of how the version was written in Chart.yaml.
func resolveOCITagVersion(rawVersion string, ociStrictVersion bool) (string, error) {
if !ociStrictVersion {
return rawVersion, nil
}
parsedVersion, err := semver.NewVersion(rawVersion)
if err != nil {
return "", fmt.Errorf("failed to parse chart version %q as semver: %w", rawVersion, err)
}
return parsedVersion.String(), nil
}
// NewOCIPusher constructs a valid OCI client as a Pusher
func NewOCIPusher(ops ...Option) (Pusher, error) {
var client OCIPusher

@ -393,6 +393,102 @@ func TestOCIPusher_Push_ChartOperations(t *testing.T) {
}
}
func TestWithOCIStrictVersion(t *testing.T) {
p, err := NewOCIPusher(WithOCIStrictVersion(true))
if err != nil {
t.Fatal(err)
}
op, ok := p.(*OCIPusher)
if !ok {
t.Fatal("Expected NewOCIPusher to produce an *OCIPusher")
}
if !op.opts.ociStrictVersion {
t.Error("Expected WithOCIStrictVersion(true) to set ociStrictVersion")
}
// Defaults to false when the option is not supplied.
p, err = NewOCIPusher()
if err != nil {
t.Fatal(err)
}
if p.(*OCIPusher).opts.ociStrictVersion {
t.Error("Expected ociStrictVersion to default to false")
}
}
func TestResolveOCITagVersion(t *testing.T) {
tests := []struct {
name string
rawVersion string
ociStrictVersion bool
want string
expectError bool
}{
{
name: "strict disabled returns raw version unchanged",
rawVersion: "v1.2.3",
ociStrictVersion: false,
want: "v1.2.3",
},
{
name: "strict disabled does not validate version",
rawVersion: "not-a-semver",
ociStrictVersion: false,
want: "not-a-semver",
},
{
name: "strict strips leading v",
rawVersion: "v1.2.3",
ociStrictVersion: true,
want: "1.2.3",
},
{
name: "strict leaves canonical version unchanged",
rawVersion: "1.2.3",
ociStrictVersion: true,
want: "1.2.3",
},
{
name: "strict preserves prerelease",
rawVersion: "1.2.3-alpha.1",
ociStrictVersion: true,
want: "1.2.3-alpha.1",
},
{
name: "strict preserves build metadata (sanitized to underscore later)",
rawVersion: "1.2.3+build.5",
ociStrictVersion: true,
want: "1.2.3+build.5",
},
{
name: "strict errors on non-semver version",
rawVersion: "not-a-semver",
ociStrictVersion: true,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := resolveOCITagVersion(tt.rawVersion, tt.ociStrictVersion)
if tt.expectError {
if err == nil {
t.Fatalf("Expected error for version %q but got none", tt.rawVersion)
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("resolveOCITagVersion(%q, %t) = %q, want %q", tt.rawVersion, tt.ociStrictVersion, got, tt.want)
}
})
}
}
func TestOCIPusher_Push_MultipleOptions(t *testing.T) {
chartPath := "../../pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tgz"

@ -34,6 +34,7 @@ type options struct {
caFile string
insecureSkipTLSVerify bool
plainHTTP bool
ociStrictVersion bool
}
// Option allows specifying various settings configurable by the user for overriding the defaults
@ -69,6 +70,15 @@ func WithPlainHTTP(plainHTTP bool) Option {
}
}
// WithOCIStrictVersion determines whether the OCI tag is derived from the
// parsed/sanitized semver representation of the chart version rather than the
// raw version string.
func WithOCIStrictVersion(ociStrictVersion bool) Option {
return func(opts *options) {
opts.ociStrictVersion = ociStrictVersion
}
}
// Pusher is an interface to support upload to the specified URL.
type Pusher interface {
// Push file content by url string

Loading…
Cancel
Save