From 7f5bb43befd07de561c74a5a0e8fea36ed52b270 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Wed, 1 Oct 2025 15:36:20 +0200 Subject: [PATCH] Add --version cmd flag to push cmd to validate chart version Signed-off-by: Benoit Tigeot --- pkg/action/push.go | 12 ++++++++++++ pkg/cmd/push.go | 15 +++++++++++++-- pkg/pusher/ocipusher.go | 5 +++++ pkg/pusher/ocipusher_test.go | 27 +++++++++++++++++++++++++++ pkg/pusher/pusher.go | 9 +++++++++ 5 files changed, 66 insertions(+), 2 deletions(-) diff --git a/pkg/action/push.go b/pkg/action/push.go index 35472415c..52a33ca52 100644 --- a/pkg/action/push.go +++ b/pkg/action/push.go @@ -38,6 +38,7 @@ type Push struct { insecureSkipTLSverify bool plainHTTP bool out io.Writer + expectedVersion string } // PushOpt is a type of function that sets options for a push action. @@ -80,6 +81,13 @@ func WithPushOptWriter(out io.Writer) PushOpt { } } +// WithExpectedVersion configures an expected chart version that must match Chart.yaml. +func WithExpectedVersion(version string) PushOpt { + return func(p *Push) { + p.expectedVersion = version + } +} + // NewPushWithOpts creates a new push, with configuration options. func NewPushWithOpts(opts ...PushOpt) *Push { p := &Push{} @@ -103,6 +111,10 @@ func (p *Push) Run(chartRef string, remote string) (string, error) { }, } + if p.expectedVersion != "" { + c.Options = append(c.Options, pusher.WithExpectedVersion(p.expectedVersion)) + } + if registry.IsOCI(remote) { // Don't use the default registry client if tls options are set. c.Options = append(c.Options, pusher.WithRegistryClient(p.cfg.RegistryClient)) diff --git a/pkg/cmd/push.go b/pkg/cmd/push.go index 94d322b9d..372194e53 100644 --- a/pkg/cmd/push.go +++ b/pkg/cmd/push.go @@ -30,8 +30,16 @@ import ( const pushDesc = ` Upload a chart to a registry. -If the chart has an associated provenance file, -it will also be uploaded. +If the chart has an associated provenance file, it will also be uploaded. + +You can optionally specify --version or oci://...:version as a safety check. When provided, +it must match the version from Chart.yaml; otherwise the command will fail. + +Examples: + + $ helm push mychart-0.1.0.tgz oci://my-registry.io/helm/charts + $ helm push mychart-0.1.0.tgz oci://my-registry.io/helm/charts --version 0.1.0 + $ helm push mychart-0.1.0.tgz oci://my-registry.io/helm/charts:0.1.0 ` type registryPushOptions struct { @@ -42,6 +50,7 @@ type registryPushOptions struct { plainHTTP bool password string username string + version string } func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { @@ -84,6 +93,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.WithExpectedVersion(o.version), action.WithPushOptWriter(out)) client.Settings = settings output, err := client.Run(chartRef, remote) @@ -103,6 +113,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload") 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.version, "version", "", "specify the exact chart version to push. If this is not specified, the version from Chart.yaml is used") return cmd } diff --git a/pkg/pusher/ocipusher.go b/pkg/pusher/ocipusher.go index 1575164e1..18dbb3986 100644 --- a/pkg/pusher/ocipusher.go +++ b/pkg/pusher/ocipusher.go @@ -59,6 +59,11 @@ func (pusher *OCIPusher) push(chartRef, href string) error { return err } + // If an expected version is specified via --version, enforce it matches Chart.yaml + if pusher.opts.expectedVersion != "" && pusher.opts.expectedVersion != meta.Metadata.Version { + return fmt.Errorf("specified --version %q does not match chart version %q", pusher.opts.expectedVersion, meta.Metadata.Version) + } + ref, err := registry.BuildPushRef(href, meta.Metadata.Name, meta.Metadata.Version) if err != nil { return err diff --git a/pkg/pusher/ocipusher_test.go b/pkg/pusher/ocipusher_test.go index 5ab8edae1..90f7b78ab 100644 --- a/pkg/pusher/ocipusher_test.go +++ b/pkg/pusher/ocipusher_test.go @@ -455,3 +455,30 @@ func TestOCIPusher_Push_MultipleOptions(t *testing.T) { t.Error("Expected insecureSkipTLSverify option to be applied") } } + +func TestOCIPusher_Push_ExpectedVersionMismatch(t *testing.T) { + chartPath := "../../pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tgz" + + // Skip test if chart file doesn't exist + if _, err := os.Stat(chartPath); err != nil { + t.Skipf("Test chart %s not found, skipping test", chartPath) + } + + p, err := NewOCIPusher() + if err != nil { + t.Fatal(err) + } + + // Provide an expected version that does not match the chart's version + err = p.Push(chartPath, "oci://localhost:5000/test", + WithExpectedVersion("0.2.0"), + ) + + if err == nil { + t.Fatal("Expected error when --version does not match chart version") + } + + if !strings.Contains(err.Error(), "specified --version") { + t.Errorf("Expected error to mention version mismatch check, got %q", err.Error()) + } +} diff --git a/pkg/pusher/pusher.go b/pkg/pusher/pusher.go index e3c767be9..837a53452 100644 --- a/pkg/pusher/pusher.go +++ b/pkg/pusher/pusher.go @@ -34,6 +34,7 @@ type options struct { caFile string insecureSkipTLSverify bool plainHTTP bool + expectedVersion string } // Option allows specifying various settings configurable by the user for overriding the defaults @@ -69,6 +70,14 @@ func WithPlainHTTP(plainHTTP bool) Option { } } +// WithExpectedVersion sets a specific chart version to check against the chart metadata. +// If the chart's version differs, the push will fail. +func WithExpectedVersion(version string) Option { + return func(opts *options) { + opts.expectedVersion = version + } +} + // Pusher is an interface to support upload to the specified URL. type Pusher interface { // Push file content by url string