Drop --version flag and support the <plugin>@<version> pattern

Signed-off-by: MrJack <36191829+biagiopietro@users.noreply.github.com>
pull/31615/head
MrJack 7 months ago
parent a030a7d538
commit a726dd8c00

@ -21,7 +21,9 @@ import (
"io" "io"
"log/slog" "log/slog"
"path/filepath" "path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v4/internal/plugin" "helm.sh/helm/v4/internal/plugin"
@ -29,17 +31,29 @@ import (
) )
type pluginUpdateOptions struct { type pluginUpdateOptions struct {
names []string plugins map[string]string
version string
} }
const pluginUpdateDesc = `Update one or more Helm plugins.
An exact version can be supplied per-plugin using the @version syntax:
helm plugin update myplugin@1.2.3 otherplugin@2.0.0
helm plugin update myplugin@v1.0.0
If no version is given for a plugin it is updated to the latest version:
helm plugin update myplugin otherplugin
`
func newPluginUpdateCmd(out io.Writer) *cobra.Command { func newPluginUpdateCmd(out io.Writer) *cobra.Command {
o := &pluginUpdateOptions{} o := &pluginUpdateOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "update <plugin>...", Use: "update <plugin[@version]>...",
Aliases: []string{"up"}, Aliases: []string{"up"},
Short: "update one or more Helm plugins", Short: "update one or more Helm plugins",
Long: pluginUpdateDesc,
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp
}, },
@ -50,7 +64,6 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command {
return o.run(out) return o.run(out)
}, },
} }
cmd.Flags().StringVar(&o.version, "version", "", "specify a version constraint. If this is not specified, the plugin is updated to the latest version")
return cmd return cmd
} }
@ -58,21 +71,39 @@ func (o *pluginUpdateOptions) complete(args []string) error {
if len(args) == 0 { if len(args) == 0 {
return errors.New("please provide plugin name to update") return errors.New("please provide plugin name to update")
} }
o.names = args
o.plugins = make(map[string]string, len(args))
for _, arg := range args {
name, version := parsePluginVersion(arg)
if name == "" {
return fmt.Errorf("invalid plugin reference %q: plugin name must not be empty", arg)
}
if _, exists := o.plugins[name]; exists {
return fmt.Errorf("plugin %q specified more than once", name)
}
if version != "" {
if _, err := semver.NewVersion(version); err != nil {
return fmt.Errorf("invalid version %q for plugin %q: must be an exact version (e.g. 1.2.3 or v1.2.3)", version, name)
}
}
o.plugins[name] = version
}
return nil return nil
} }
func (o *pluginUpdateOptions) run(out io.Writer) error { func (o *pluginUpdateOptions) run(out io.Writer) error {
slog.Debug("loading installed plugins", "path", settings.PluginsDirectory) slog.Debug("loading installed plugins", "path", settings.PluginsDirectory)
plugins, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc) installed, err := plugin.LoadAllDir(settings.PluginsDirectory, plugin.LogIgnorePluginLoadErrorFilterFunc)
if err != nil { if err != nil {
return err return err
} }
var errorPlugins []error var errorPlugins []error
for _, name := range o.names { for name, version := range o.plugins {
if found := findPlugin(plugins, name); found != nil { if found := findPlugin(installed, name); found != nil {
if err := updatePlugin(found, o.version); err != nil { if err := updatePlugin(found, version); err != nil {
errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%v)", name, err)) errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%v)", name, err))
} else { } else {
fmt.Fprintf(out, "Updated plugin: %s\n", name) fmt.Fprintf(out, "Updated plugin: %s\n", name)
@ -87,6 +118,13 @@ func (o *pluginUpdateOptions) run(out io.Writer) error {
return nil return nil
} }
func parsePluginVersion(arg string) (name, version string) {
if i := strings.LastIndex(arg, "@"); i >= 0 {
return arg[:i], arg[i+1:]
}
return arg, ""
}
func updatePlugin(p plugin.Plugin, version string) error { func updatePlugin(p plugin.Plugin, version string) error {
exactLocation, err := filepath.EvalSymlinks(p.Dir()) exactLocation, err := filepath.EvalSymlinks(p.Dir())
if err != nil { if err != nil {

@ -0,0 +1,154 @@
/*
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 cmd
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParsePluginVersion(t *testing.T) {
tests := []struct {
arg string
wantName string
wantVersion string
}{
{"myplugin", "myplugin", ""},
{"myplugin@1.2.3", "myplugin", "1.2.3"},
{"myplugin@v1.2.3", "myplugin", "v1.2.3"},
{"myplugin@", "myplugin", ""},
{"@version", "", "version"},
{"", "", ""},
// LastIndex ensures the last @ is used as delimiter
{"weird@name@1.0", "weird@name", "1.0"},
}
for _, tt := range tests {
t.Run(tt.arg, func(t *testing.T) {
name, version := parsePluginVersion(tt.arg)
assert.Equal(t, tt.wantName, name)
assert.Equal(t, tt.wantVersion, version)
})
}
}
func TestPluginUpdateComplete(t *testing.T) {
tests := []struct {
name string
args []string
wantPlugins map[string]string
wantErr string
}{
{
name: "no args",
args: []string{},
wantErr: "please provide plugin name to update",
},
{
name: "single plugin no version",
args: []string{"myplugin"},
wantPlugins: map[string]string{"myplugin": ""},
},
{
name: "single plugin with inline version",
args: []string{"myplugin@1.2.3"},
wantPlugins: map[string]string{"myplugin": "1.2.3"},
},
{
name: "multiple plugins no versions",
args: []string{"plugin-a", "plugin-b"},
wantPlugins: map[string]string{"plugin-a": "", "plugin-b": ""},
},
{
name: "multiple plugins with inline versions",
args: []string{"plugin-a@1.0.0", "plugin-b@2.0.0"},
wantPlugins: map[string]string{"plugin-a": "1.0.0", "plugin-b": "2.0.0"},
},
{
name: "multiple plugins each with different exact versions",
args: []string{"plugin-a@v1.2.3", "plugin-b@2.0.0", "plugin-c@3.0.0"},
wantPlugins: map[string]string{"plugin-a": "v1.2.3", "plugin-b": "2.0.0", "plugin-c": "3.0.0"},
},
{
name: "multiple plugins mixed versions",
args: []string{"plugin-a@1.0.0", "plugin-b"},
wantPlugins: map[string]string{"plugin-a": "1.0.0", "plugin-b": ""},
},
{
name: "multiple plugins mixed with latest in the middle",
args: []string{"plugin-a@1.0.0", "plugin-b", "plugin-c@3.0.0"},
wantPlugins: map[string]string{"plugin-a": "1.0.0", "plugin-b": "", "plugin-c": "3.0.0"},
},
{
name: "duplicate plugin name errors",
args: []string{"myplugin@1.0.0", "myplugin@2.0.0"},
wantErr: `plugin "myplugin" specified more than once`,
},
{
name: "empty plugin name errors",
args: []string{"@1.0.0"},
wantErr: `invalid plugin reference "@1.0.0": plugin name must not be empty`,
},
{
name: "tilde range version rejected",
args: []string{"myplugin@~1.2"},
wantErr: `invalid version "~1.2" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`,
},
{
name: "caret range version rejected",
args: []string{"myplugin@^1.2.3"},
wantErr: `invalid version "^1.2.3" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`,
},
{
name: "gte constraint rejected",
args: []string{"myplugin@>=1.0.0"},
wantErr: `invalid version ">=1.0.0" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`,
},
{
name: "wildcard version rejected",
args: []string{"myplugin@1.x"},
wantErr: `invalid version "1.x" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`,
},
{
name: "range constraint rejected",
args: []string{"myplugin@>=1.0.0, <2.0.0"},
wantErr: `invalid version ">=1.0.0, <2.0.0" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`,
},
{
name: "garbage version rejected",
args: []string{"myplugin@notaversion"},
wantErr: `invalid version "notaversion" for plugin "myplugin": must be an exact version (e.g. 1.2.3 or v1.2.3)`,
},
{
name: "range rejected among multiple plugins",
args: []string{"plugin-a@1.0.0", "plugin-b@~2.0"},
wantErr: `invalid version "~2.0" for plugin "plugin-b": must be an exact version (e.g. 1.2.3 or v1.2.3)`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &pluginUpdateOptions{}
err := o.complete(tt.args)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantPlugins, o.plugins)
})
}
}
Loading…
Cancel
Save