MrJack 2 weeks ago committed by GitHub
commit cb604a80cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -155,8 +155,8 @@ func NewForSource(source, version string) (installer Installer, err error) {
}
// FindSource determines the correct Installer for the given source.
func FindSource(location string) (Installer, error) {
installer, err := existingVCSRepo(location)
func FindSource(location, version string) (Installer, error) {
installer, err := existingVCSRepo(location, version)
if err != nil && err.Error() == "Cannot detect VCS" {
slog.Warn(
"cannot get information about plugin source",

@ -38,14 +38,15 @@ type VCSInstaller struct {
base
}
func existingVCSRepo(location string) (Installer, error) {
func existingVCSRepo(location string, version string) (Installer, error) {
repo, err := vcs.NewRepo("", location)
if err != nil {
return nil, err
}
i := &VCSInstaller{
Repo: repo,
base: newBase(repo.Remote()),
Repo: repo,
Version: version,
base: newBase(repo.Remote()),
}
return i, nil
}
@ -104,6 +105,17 @@ func (i *VCSInstaller) Update() error {
if err := i.Repo.Update(); err != nil {
return err
}
ref, err := i.solveVersion(i.Repo)
if err != nil {
return err
}
if ref != "" {
if err := i.setVersion(i.Repo, ref); err != nil {
return err
}
}
if !isPlugin(i.Repo.LocalPath()) {
return ErrMissingMetadata
}

@ -48,6 +48,7 @@ func (r *testRepo) UpdateVersion(version string) error {
r.current = version
return r.err
}
func (r *testRepo) IsDirty() bool { return false }
func TestVCSInstaller(t *testing.T) {
ensure.HelmHome(t)
@ -96,7 +97,7 @@ func TestVCSInstaller(t *testing.T) {
}
// Testing FindSource method, expect error because plugin code is not a cloned repository
if _, err := FindSource(i.Path()); err == nil {
if _, err := FindSource(i.Path(), ""); err == nil {
t.Fatal("expected error for inability to find plugin source, got none")
} else if err.Error() != "cannot get information about plugin source" {
t.Fatalf("expected error for inability to find plugin source, got (%v)", err)
@ -158,7 +159,7 @@ func TestVCSInstallerUpdate(t *testing.T) {
}
// Test FindSource method for positive result
pluginInfo, err := FindSource(i.Path())
pluginInfo, err := FindSource(i.Path(), "")
if err != nil {
t.Fatal(err)
}
@ -186,3 +187,61 @@ func TestVCSInstallerUpdate(t *testing.T) {
t.Fatalf("expected error for plugin modified, got (%v)", err)
}
}
func TestVCSInstallerUpdateWithVersion(t *testing.T) {
ensure.HelmHome(t)
if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
}
source := "https://github.com/adamreese/helm-env"
testRepoPath, _ := filepath.Abs("../testdata/plugdir/good/echo-v1")
repo := &testRepo{
local: testRepoPath,
remote: source,
tags: []string{"0.1.0", "0.1.1", "0.2.0"},
}
// First install without version
i, err := NewForSource(source, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
vcsInstaller, ok := i.(*VCSInstaller)
if !ok {
t.Fatal("expected a VCSInstaller")
}
vcsInstaller.Repo = repo
if err := Install(i); err != nil {
t.Fatal(err)
}
// Now test update with specific version
vcsInstaller.Version = "0.1.1"
if err := Update(vcsInstaller); err != nil {
t.Fatal(err)
}
if repo.current != "0.1.1" {
t.Fatalf("expected version '0.1.1', got %q", repo.current)
}
// Test update with different version
vcsInstaller.Version = "0.2.0"
if err := Update(vcsInstaller); err != nil {
t.Fatal(err)
}
if repo.current != "0.2.0" {
t.Fatalf("expected version '0.2.0', got %q", repo.current)
}
// Test update with non-existent version
vcsInstaller.Version = "0.3.0"
if err := Update(vcsInstaller); err == nil {
t.Fatal("expected error for version does not exist, got none")
} else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", "0.3.0", source) {
t.Fatalf("expected error for version does not exist, got (%v)", err)
}
}

@ -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,18 +31,39 @@ import (
)
type pluginUpdateOptions struct {
names []string
plugins map[string]string
}
const pluginUpdateDesc = `Update one or more Helm plugins.
An exact semver version can be pinned per-plugin using the @version syntax.
Only exact versions (e.g. 1.2.3) are accepted; the "v" prefix and semver range
constraints (e.g. ~1.2, ^1.0.0, >=1.0.0) are not supported for updates.
This ensures a deterministic, reproducible update to a known version:
helm plugin update myplugin@1.2.3 otherplugin@2.0.0
helm plugin update myplugin@1.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
ignoredNames := make([]string, len(args))
for i, arg := range args {
name, _ := parsePluginVersion(arg)
ignoredNames[i] = name
}
return compListPlugins(toComplete, ignoredNames), cobra.ShellCompDirectiveNoFileComp
},
PreRunE: func(_ *cobra.Command, args []string) error {
return o.complete(args)
@ -56,21 +79,43 @@ 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.StrictNewVersion(version); err != nil {
errMsg := fmt.Sprintf("invalid version %q for plugin %q: must be an exact semver version (e.g. 1.2.3)", version, name)
if strings.HasPrefix(version, "v") {
errMsg += `; the "v" prefix is not allowed`
}
return fmt.Errorf("%s", errMsg)
}
}
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); 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)
@ -85,7 +130,14 @@ func (o *pluginUpdateOptions) run(out io.Writer) error {
return nil
}
func updatePlugin(p plugin.Plugin) error {
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 {
return err
@ -95,7 +147,7 @@ func updatePlugin(p plugin.Plugin) error {
return err
}
i, err := installer.FindSource(absExactLocation)
i, err := installer.FindSource(absExactLocation, version)
if err != nil {
return err
}

@ -0,0 +1,159 @@
/*
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@1.2.3", "plugin-b@2.0.0", "plugin-c@3.0.0"},
wantPlugins: map[string]string{"plugin-a": "1.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: "v-prefixed version rejected",
args: []string{"myplugin@v1.2.3"},
wantErr: `invalid version "v1.2.3" for plugin "myplugin": must be an exact semver version (e.g. 1.2.3); the "v" prefix is not allowed`,
},
{
name: "tilde range version rejected",
args: []string{"myplugin@~1.2"},
wantErr: `invalid version "~1.2" for plugin "myplugin": must be an exact semver version (e.g. 1.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 semver version (e.g. 1.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 semver version (e.g. 1.2.3)`,
},
{
name: "wildcard version rejected",
args: []string{"myplugin@1.x"},
wantErr: `invalid version "1.x" for plugin "myplugin": must be an exact semver version (e.g. 1.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 semver version (e.g. 1.2.3)`,
},
{
name: "garbage version rejected",
args: []string{"myplugin@notaversion"},
wantErr: `invalid version "notaversion" for plugin "myplugin": must be an exact semver version (e.g. 1.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 semver version (e.g. 1.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