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

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

@ -21,7 +21,9 @@ import (
"io"
"log/slog"
"path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/spf13/cobra"
"helm.sh/helm/v4/internal/plugin"
@ -29,17 +31,29 @@ import (
)
type pluginUpdateOptions struct {
names []string
version string
plugins map[string]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 {
o := &pluginUpdateOptions{}
cmd := &cobra.Command{
Use: "update <plugin>...",
Use: "update <plugin[@version]>...",
Aliases: []string{"up"},
Short: "update one or more Helm plugins",
Long: pluginUpdateDesc,
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp
},
@ -50,7 +64,6 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command {
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
}
@ -58,21 +71,39 @@ func (o *pluginUpdateOptions) complete(args []string) error {
if len(args) == 0 {
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
}
func (o *pluginUpdateOptions) run(out io.Writer) error {
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 {
return err
}
var errorPlugins []error
for _, name := range o.names {
if found := findPlugin(plugins, name); found != nil {
if err := updatePlugin(found, o.version); err != nil {
for name, version := range o.plugins {
if found := findPlugin(installed, name); found != nil {
if err := updatePlugin(found, version); err != nil {
errorPlugins = append(errorPlugins, fmt.Errorf("failed to update plugin %s, got error (%v)", name, err))
} else {
fmt.Fprintf(out, "Updated plugin: %s\n", name)
@ -87,6 +118,13 @@ func (o *pluginUpdateOptions) run(out io.Writer) error {
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 {
exactLocation, err := filepath.EvalSymlinks(p.Dir())
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