Rename --oci-strict-version to --oci-normalize-version

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

@ -37,7 +37,7 @@ type Push struct {
caFile string caFile string
insecureSkipTLSVerify bool insecureSkipTLSVerify bool
plainHTTP bool plainHTTP bool
ociStrictVersion bool ociNormalizeVersion bool
out io.Writer out io.Writer
} }
@ -74,11 +74,11 @@ func WithPlainHTTP(plainHTTP bool) PushOpt {
} }
} }
// WithOCIStrictVersion configures whether the OCI tag is derived from the // WithOCINormalizeVersion configures whether the OCI tag is derived from the
// parsed/sanitized semver representation of the chart version. // canonical semver representation of the chart version.
func WithOCIStrictVersion(ociStrictVersion bool) PushOpt { func WithOCINormalizeVersion(ociNormalizeVersion bool) PushOpt {
return func(p *Push) { return func(p *Push) {
p.ociStrictVersion = ociStrictVersion p.ociNormalizeVersion = ociNormalizeVersion
} }
} }
@ -109,7 +109,7 @@ func (p *Push) Run(chartRef string, remote string) (string, error) {
pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile), pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile),
pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSVerify), pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSVerify),
pusher.WithPlainHTTP(p.plainHTTP), pusher.WithPlainHTTP(p.plainHTTP),
pusher.WithOCIStrictVersion(p.ociStrictVersion), pusher.WithOCINormalizeVersion(p.ociNormalizeVersion),
}, },
} }

@ -40,7 +40,7 @@ type registryPushOptions struct {
caFile string caFile string
insecureSkipTLSVerify bool insecureSkipTLSVerify bool
plainHTTP bool plainHTTP bool
ociStrictVersion bool ociNormalizeVersion bool
password string password string
username string username string
} }
@ -85,7 +85,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile), action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile),
action.WithInsecureSkipTLSVerify(o.insecureSkipTLSVerify), action.WithInsecureSkipTLSVerify(o.insecureSkipTLSVerify),
action.WithPlainHTTP(o.plainHTTP), action.WithPlainHTTP(o.plainHTTP),
action.WithOCIStrictVersion(o.ociStrictVersion), action.WithOCINormalizeVersion(o.ociNormalizeVersion),
action.WithPushOptWriter(out)) action.WithPushOptWriter(out))
client.Settings = settings client.Settings = settings
output, err := client.Run(chartRef, remote) output, err := client.Run(chartRef, remote)
@ -103,7 +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.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.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.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.BoolVar(&o.ociNormalizeVersion, "oci-normalize-version", false, "push using the canonical semver form of the chart version as the OCI tag (e.g. v1.2.3 is pushed as 1.2.3)")
f.StringVar(&o.username, "username", "", "chart repository username where to locate the requested chart") 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") f.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart")

@ -87,27 +87,24 @@ func (pusher *OCIPusher) push(chartRef, href string) error {
pushOpts = append(pushOpts, registry.PushOptProvData(provBytes)) pushOpts = append(pushOpts, registry.PushOptProvData(provBytes))
} }
// Resolve the version used as the base of the OCI tag. With strict // Build the OCI reference for the chart. When --oci-normalize-version is
// versioning enabled this is the sanitized semver form of the chart // set (pusher.opts.ociNormalizeVersion) the tag is the canonical semver
// version; otherwise it is the raw chart version. The registry client // form of the chart version; otherwise it is the raw chart version. This
// still applies its usual tag transformations (e.g. replacing plus (+) // flag is a separate concept from the registry client's own "strict mode"
// signs with underscores) afterwards. // (relaxed below).
version, err := resolveOCITagVersion(meta.Metadata.Version, pusher.opts.ociStrictVersion) ref, relaxStrictMode, err := buildOCIReference(href, meta.Metadata.Name, meta.Metadata.Version, pusher.opts.ociNormalizeVersion)
if err != nil { if err != nil {
return err return err
} }
// The sanitized version may differ from the raw chart version, so relax // The registry client's strict mode asserts that the tag equals the raw
// the registry client's strict-mode assertion (which requires the tag to // chart version. Once the version has been canonicalized the tag no longer
// equal the raw chart version) when it does. // matches, so that assertion must be disabled. (This is unrelated to the
if version != meta.Metadata.Version { // --oci-normalize-version flag despite the similar "strict" wording.)
if relaxStrictMode {
pushOpts = append(pushOpts, registry.PushOptStrictMode(false)) pushOpts = append(pushOpts, registry.PushOptStrictMode(false))
} }
ref := fmt.Sprintf("%s:%s",
path.Join(strings.TrimPrefix(href, registry.OCIScheme+"://"), meta.Metadata.Name),
version)
// The time the chart was "created" is semantically the time the chart archive file was last written(modified) // The time the chart was "created" is semantically the time the chart archive file was last written(modified)
chartArchiveFileCreatedTime := stat.ModTime() chartArchiveFileCreatedTime := stat.ModTime()
pushOpts = append(pushOpts, registry.PushOptCreationTime(chartArchiveFileCreatedTime.Format(time.RFC3339))) pushOpts = append(pushOpts, registry.PushOptCreationTime(chartArchiveFileCreatedTime.Format(time.RFC3339)))
@ -116,18 +113,46 @@ func (pusher *OCIPusher) push(chartRef, href string) error {
return err return err
} }
// buildOCIReference constructs the OCI reference used to push a chart and
// reports whether the registry client's strict mode must be relaxed for it.
//
// When ociNormalizeVersion is true the tag is the canonical semver form of the
// chart version (e.g. "v1.2.3" -> "1.2.3"); otherwise the raw chart version is
// used unchanged. The registry client still applies its usual tag
// transformations afterwards (e.g. replacing plus (+) signs with underscores).
//
// relaxStrictMode is true when the resulting tag differs from the raw chart
// version. The registry client's strict mode (registry.PushOptStrictMode)
// asserts the tag equals the raw chart version, so it must be disabled once the
// version has been canonicalized. This is separate from the option that drives
// ociNormalizeVersion despite the shared "strict" terminology.
func buildOCIReference(href, chartName, rawVersion string, ociNormalizeVersion bool) (ref string, relaxStrictMode bool, err error) {
version, err := resolveOCITagVersion(rawVersion, ociNormalizeVersion)
if err != nil {
return "", false, err
}
ref = fmt.Sprintf("%s:%s",
path.Join(strings.TrimPrefix(href, registry.OCIScheme+"://"), chartName),
version)
return ref, version != rawVersion, nil
}
// resolveOCITagVersion returns the version string to use as the base of the OCI // 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 // tag for a chart. When ociNormalizeVersion is false the raw chart version is
// returned unchanged. When it is true the version is parsed with semver and its // 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 // canonical string representation is returned, so that a canonical semver tag is
// produced regardless of how the version was written in Chart.yaml. // produced regardless of how the version was written in Chart.yaml.
func resolveOCITagVersion(rawVersion string, ociStrictVersion bool) (string, error) { func resolveOCITagVersion(rawVersion string, ociNormalizeVersion bool) (string, error) {
if !ociStrictVersion { if !ociNormalizeVersion {
return rawVersion, nil return rawVersion, nil
} }
parsedVersion, err := semver.NewVersion(rawVersion) parsedVersion, err := semver.NewVersion(rawVersion)
if err != nil { if err != nil {
// Defensive: the chart loader validates the version as semver before a
// push reaches this point, so a valid chart should never hit this path.
return "", fmt.Errorf("failed to parse chart version %q as semver: %w", rawVersion, err) return "", fmt.Errorf("failed to parse chart version %q as semver: %w", rawVersion, err)
} }

@ -393,8 +393,8 @@ func TestOCIPusher_Push_ChartOperations(t *testing.T) {
} }
} }
func TestWithOCIStrictVersion(t *testing.T) { func TestWithOCINormalizeVersion(t *testing.T) {
p, err := NewOCIPusher(WithOCIStrictVersion(true)) p, err := NewOCIPusher(WithOCINormalizeVersion(true))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -404,8 +404,8 @@ func TestWithOCIStrictVersion(t *testing.T) {
t.Fatal("Expected NewOCIPusher to produce an *OCIPusher") t.Fatal("Expected NewOCIPusher to produce an *OCIPusher")
} }
if !op.opts.ociStrictVersion { if !op.opts.ociNormalizeVersion {
t.Error("Expected WithOCIStrictVersion(true) to set ociStrictVersion") t.Error("Expected WithOCINormalizeVersion(true) to set ociNormalizeVersion")
} }
// Defaults to false when the option is not supplied. // Defaults to false when the option is not supplied.
@ -413,8 +413,8 @@ func TestWithOCIStrictVersion(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if p.(*OCIPusher).opts.ociStrictVersion { if p.(*OCIPusher).opts.ociNormalizeVersion {
t.Error("Expected ociStrictVersion to default to false") t.Error("Expected ociNormalizeVersion to default to false")
} }
} }
@ -422,57 +422,57 @@ func TestResolveOCITagVersion(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
rawVersion string rawVersion string
ociStrictVersion bool ociNormalizeVersion bool
want string want string
expectError bool expectError bool
}{ }{
{ {
name: "strict disabled returns raw version unchanged", name: "strict disabled returns raw version unchanged",
rawVersion: "v1.2.3", rawVersion: "v1.2.3",
ociStrictVersion: false, ociNormalizeVersion: false,
want: "v1.2.3", want: "v1.2.3",
}, },
{ {
name: "strict disabled does not validate version", name: "strict disabled does not validate version",
rawVersion: "not-a-semver", rawVersion: "not-a-semver",
ociStrictVersion: false, ociNormalizeVersion: false,
want: "not-a-semver", want: "not-a-semver",
}, },
{ {
name: "strict strips leading v", name: "strict strips leading v",
rawVersion: "v1.2.3", rawVersion: "v1.2.3",
ociStrictVersion: true, ociNormalizeVersion: true,
want: "1.2.3", want: "1.2.3",
}, },
{ {
name: "strict leaves canonical version unchanged", name: "strict leaves canonical version unchanged",
rawVersion: "1.2.3", rawVersion: "1.2.3",
ociStrictVersion: true, ociNormalizeVersion: true,
want: "1.2.3", want: "1.2.3",
}, },
{ {
name: "strict preserves prerelease", name: "strict preserves prerelease",
rawVersion: "1.2.3-alpha.1", rawVersion: "1.2.3-alpha.1",
ociStrictVersion: true, ociNormalizeVersion: true,
want: "1.2.3-alpha.1", want: "1.2.3-alpha.1",
}, },
{ {
name: "strict preserves build metadata (sanitized to underscore later)", name: "strict preserves build metadata (sanitized to underscore later)",
rawVersion: "1.2.3+build.5", rawVersion: "1.2.3+build.5",
ociStrictVersion: true, ociNormalizeVersion: true,
want: "1.2.3+build.5", want: "1.2.3+build.5",
}, },
{ {
name: "strict errors on non-semver version", name: "strict errors on non-semver version",
rawVersion: "not-a-semver", rawVersion: "not-a-semver",
ociStrictVersion: true, ociNormalizeVersion: true,
expectError: true, expectError: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := resolveOCITagVersion(tt.rawVersion, tt.ociStrictVersion) got, err := resolveOCITagVersion(tt.rawVersion, tt.ociNormalizeVersion)
if tt.expectError { if tt.expectError {
if err == nil { if err == nil {
t.Fatalf("Expected error for version %q but got none", tt.rawVersion) t.Fatalf("Expected error for version %q but got none", tt.rawVersion)
@ -483,7 +483,104 @@ func TestResolveOCITagVersion(t *testing.T) {
t.Fatalf("Unexpected error: %v", err) t.Fatalf("Unexpected error: %v", err)
} }
if got != tt.want { if got != tt.want {
t.Errorf("resolveOCITagVersion(%q, %t) = %q, want %q", tt.rawVersion, tt.ociStrictVersion, got, tt.want) t.Errorf("resolveOCITagVersion(%q, %t) = %q, want %q", tt.rawVersion, tt.ociNormalizeVersion, got, tt.want)
}
})
}
}
func TestBuildOCIReference(t *testing.T) {
tests := []struct {
name string
href string
chartName string
rawVersion string
ociNormalizeVersion bool
wantRef string
wantRelax bool
expectError bool
}{
{
name: "normalize disabled uses raw version and keeps strict mode",
href: "oci://localhost:5000/charts",
chartName: "mychart",
rawVersion: "v1.2.3",
ociNormalizeVersion: false,
wantRef: "localhost:5000/charts/mychart:v1.2.3",
wantRelax: false,
},
{
name: "normalize canonicalizes v-prefixed version and relaxes strict mode",
href: "oci://localhost:5000/charts",
chartName: "mychart",
rawVersion: "v1.2.3",
ociNormalizeVersion: true,
wantRef: "localhost:5000/charts/mychart:1.2.3",
wantRelax: true,
},
{
name: "normalize leaves canonical version and keeps strict mode",
href: "oci://localhost:5000/charts",
chartName: "mychart",
rawVersion: "1.2.3",
ociNormalizeVersion: true,
wantRef: "localhost:5000/charts/mychart:1.2.3",
wantRelax: false,
},
{
name: "normalize completes short version and relaxes strict mode",
href: "oci://localhost:5000/charts",
chartName: "mychart",
rawVersion: "1.2",
ociNormalizeVersion: true,
wantRef: "localhost:5000/charts/mychart:1.2.0",
wantRelax: true,
},
{
name: "normalize with build metadata keeps strict mode",
href: "oci://localhost:5000/charts",
chartName: "mychart",
rawVersion: "1.2.3+build.5",
ociNormalizeVersion: true,
wantRef: "localhost:5000/charts/mychart:1.2.3+build.5",
wantRelax: false,
},
{
name: "oci scheme prefix is trimmed from href",
href: "oci://registry.example.com/team/charts",
chartName: "mychart",
rawVersion: "1.2.3",
ociNormalizeVersion: false,
wantRef: "registry.example.com/team/charts/mychart:1.2.3",
wantRelax: false,
},
{
name: "normalize errors on non-semver version",
href: "oci://localhost:5000/charts",
chartName: "mychart",
rawVersion: "not-a-semver",
ociNormalizeVersion: true,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ref, relax, err := buildOCIReference(tt.href, tt.chartName, tt.rawVersion, tt.ociNormalizeVersion)
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 ref != tt.wantRef {
t.Errorf("buildOCIReference ref = %q, want %q", ref, tt.wantRef)
}
if relax != tt.wantRelax {
t.Errorf("buildOCIReference relaxStrictMode = %t, want %t", relax, tt.wantRelax)
} }
}) })
} }

@ -34,7 +34,7 @@ type options struct {
caFile string caFile string
insecureSkipTLSVerify bool insecureSkipTLSVerify bool
plainHTTP bool plainHTTP bool
ociStrictVersion bool ociNormalizeVersion bool
} }
// Option allows specifying various settings configurable by the user for overriding the defaults // Option allows specifying various settings configurable by the user for overriding the defaults
@ -70,12 +70,15 @@ func WithPlainHTTP(plainHTTP bool) Option {
} }
} }
// WithOCIStrictVersion determines whether the OCI tag is derived from the // WithOCINormalizeVersion determines whether the OCI tag is derived from the
// parsed/sanitized semver representation of the chart version rather than the // canonical semver representation of the chart version rather than the raw
// raw version string. // version string. Note this is unrelated to the registry client's "strict
func WithOCIStrictVersion(ociStrictVersion bool) Option { // mode" (registry.PushOptStrictMode); enabling it may in fact require that
// strict mode to be relaxed, since the canonical tag can differ from the raw
// chart version.
func WithOCINormalizeVersion(ociNormalizeVersion bool) Option {
return func(opts *options) { return func(opts *options) {
opts.ociStrictVersion = ociStrictVersion opts.ociNormalizeVersion = ociNormalizeVersion
} }
} }

Loading…
Cancel
Save