diff --git a/internal/plugin/installer/http_installer.go b/internal/plugin/installer/http_installer.go index bb96314f4..154d31a59 100644 --- a/internal/plugin/installer/http_installer.go +++ b/internal/plugin/installer/http_installer.go @@ -146,6 +146,11 @@ func (i *HTTPInstaller) Update() error { return fmt.Errorf("method Update() not implemented for HttpInstaller") } +// GetLatestVersion fetches the latest version of the plugin.Add a comment on lines R158 to R160Add diff commentMarkdown input: edit mode selected.WritePreviewAdd a suggestionHeadingBoldItalicQuoteCodeLinkUnordered listNumbered listTask listMentionReferenceSaved repliesAdd FilesPaste, drop, or click to add filesCancelCommentStart a reviewReturn to code +func (i *HTTPInstaller) GetLatestVersion() (string, error) { + return "", fmt.Errorf("not supported") +} + // Path is overridden because we want to join on the plugin name not the file name func (i HTTPInstaller) Path() string { if i.Source == "" { diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index a6599c443..3a2aa8e45 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -50,6 +50,8 @@ type Installer interface { Path() string // Update updates a plugin. Update() error + // GetLatestVersion fetches the latest version of the plugin. + GetLatestVersion() (string, error) } // Verifier provides an interface for installers that support verification. diff --git a/internal/plugin/installer/local_installer.go b/internal/plugin/installer/local_installer.go index e02261d59..8bbdaf40e 100644 --- a/internal/plugin/installer/local_installer.go +++ b/internal/plugin/installer/local_installer.go @@ -164,6 +164,10 @@ func (i *LocalInstaller) Update() error { return nil } +func (i *LocalInstaller) GetLatestVersion() (string, error) { + return "", fmt.Errorf("not supported") +} + // Path is overridden to handle archive plugin names properly func (i *LocalInstaller) Path() string { if i.Source == "" { diff --git a/internal/plugin/installer/oci_installer.go b/internal/plugin/installer/oci_installer.go index afbb42ca5..323dd786c 100644 --- a/internal/plugin/installer/oci_installer.go +++ b/internal/plugin/installer/oci_installer.go @@ -197,6 +197,10 @@ func (i OCIInstaller) Path() string { return filepath.Join(i.settings.PluginsDirectory, i.PluginName) } +func (i *OCIInstaller) GetLatestVersion() (string, error) { + return "", fmt.Errorf("not supported") +} + // extractTarGz extracts a gzipped tar archive to a directory func extractTarGz(r io.Reader, targetDir string) error { gzr, err := gzip.NewReader(r) diff --git a/internal/plugin/installer/vcs_installer.go b/internal/plugin/installer/vcs_installer.go index 3601ec7a8..56eef6c70 100644 --- a/internal/plugin/installer/vcs_installer.go +++ b/internal/plugin/installer/vcs_installer.go @@ -110,6 +110,26 @@ func (i *VCSInstaller) Update() error { return nil } +func (i *VCSInstaller) GetLatestVersion() (string, error) { + _, err := i.Repo.RunFromDir("git", "fetch", "-q", "--tags") + if err != nil { + return "", fmt.Errorf("failed to fetch tags: %w", err) + } + + refs, err := i.Repo.Tags() + if err != nil { + return "", fmt.Errorf("failed to get tags: %w", err) + } + + semvers := getSemVers(refs) + if len(semvers) == 0 { + return "", fmt.Errorf("no valid semver tags found in repository") + } + + sort.Sort(sort.Reverse(semver.Collection(semvers))) + return semvers[0].Original(), nil +} + func (i *VCSInstaller) solveVersion(repo vcs.Repo) (string, error) { if i.Version == "" { return "", nil diff --git a/pkg/cmd/plugin_list.go b/pkg/cmd/plugin_list.go index 74e969e04..2a44b987b 100644 --- a/pkg/cmd/plugin_list.go +++ b/pkg/cmd/plugin_list.go @@ -24,12 +24,19 @@ import ( "github.com/gosuri/uitable" "github.com/spf13/cobra" + "helm.sh/helm/v4/internal/plugin/installer" "helm.sh/helm/v4/internal/plugin" "helm.sh/helm/v4/internal/plugin/schema" ) +type pluginListOptions struct { + showOutdated bool +} + func newPluginListCmd(out io.Writer) *cobra.Command { + o := &pluginListOptions{} + var pluginType string cmd := &cobra.Command{ Use: "list", @@ -51,7 +58,7 @@ func newPluginListCmd(out io.Writer) *cobra.Command { signingInfo := plugin.GetSigningInfoForPlugins(plugins) table := uitable.New() - table.AddRow("NAME", "VERSION", "TYPE", "APIVERSION", "PROVENANCE", "SOURCE") + table.AddRow("NAME", "VERSION", "LATEST", "TYPE", "APIVERSION", "PROVENANCE", "SOURCE") for _, p := range plugins { m := p.Metadata() sourceURL := m.SourceURL @@ -63,7 +70,15 @@ func newPluginListCmd(out io.Writer) *cobra.Command { if info, ok := signingInfo[m.Name]; ok { signedStatus = info.Status } - table.AddRow(m.Name, m.Version, m.Type, m.APIVersion, signedStatus, sourceURL) + + latest, err := getLatestVersion(p) + if err != nil { + latest = "unknown" + } + + if !o.showOutdated || (latest != "unknown" && latest != m.Version) { + table.AddRow(m.Name, m.Version, latest, m.Type, m.APIVersion, signedStatus, sourceURL) + } } fmt.Fprintln(out, table) return nil @@ -72,6 +87,7 @@ func newPluginListCmd(out io.Writer) *cobra.Command { f := cmd.Flags() f.StringVar(&pluginType, "type", "", "Plugin type") + f.BoolVar(&o.showOutdated, "outdated", false, "show only outdated plugins") return cmd } @@ -115,3 +131,22 @@ func compListPlugins(_ string, ignoredPluginNames []string) []string { } return pNames } + +// getLatestVersion returns the latest version of a plugin +func getLatestVersion(p plugin.Plugin) (string, error) { + exactLocation, err := filepath.EvalSymlinks(p.Dir()) + if err != nil { + return "", err + } + absExactLocation, err := filepath.Abs(exactLocation) + if err != nil { + return "", err + } + + i, err := installer.FindSource(absExactLocation) + if err != nil { + return "", err + } + + return i.GetLatestVersion() +}