From 7bd7eca52840ce7aab665297d9a354b4bd8781fd Mon Sep 17 00:00:00 2001 From: Noah Campbell Date: Thu, 18 Sep 2025 12:56:12 -0500 Subject: [PATCH] does not auto update for package managers (#1850) --- cmd/preflight/cli/root.go | 2 +- cmd/troubleshoot/cli/root.go | 2 +- pkg/updater/pkgmgr/homebrew.go | 74 ++++++++++++++++++++++++++++++++++ pkg/updater/pkgmgr/krew.go | 69 +++++++++++++++++++++++++++++++ pkg/updater/pkgmgr/pkgmgr.go | 11 +++++ pkg/updater/updater.go | 71 ++++++++++++++++++++++++++++++++ 6 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 pkg/updater/pkgmgr/homebrew.go create mode 100644 pkg/updater/pkgmgr/krew.go create mode 100644 pkg/updater/pkgmgr/pkgmgr.go diff --git a/cmd/preflight/cli/root.go b/cmd/preflight/cli/root.go index a6b919f0..bf97d3f4 100644 --- a/cmd/preflight/cli/root.go +++ b/cmd/preflight/cli/root.go @@ -53,7 +53,7 @@ that a cluster meets the requirements to run an application.`, _ = updater.CheckAndUpdate(cmd.Context(), updater.Options{ BinaryName: "preflight", CurrentPath: exe, - Printf: func(f string, a ...interface{}) { klog.V(1).Infof(f, a...) }, + Printf: func(f string, a ...interface{}) { fmt.Fprintf(os.Stderr, f, a...) }, }) } } diff --git a/cmd/troubleshoot/cli/root.go b/cmd/troubleshoot/cli/root.go index 478fb68f..52dc7cc8 100644 --- a/cmd/troubleshoot/cli/root.go +++ b/cmd/troubleshoot/cli/root.go @@ -56,7 +56,7 @@ If no arguments are provided, specs are automatically loaded from the cluster by _ = updater.CheckAndUpdate(cmd.Context(), updater.Options{ BinaryName: "support-bundle", CurrentPath: exe, - Printf: func(f string, a ...interface{}) { klog.V(1).Infof(f, a...) }, + Printf: func(f string, a ...interface{}) { fmt.Fprintf(os.Stderr, f, a...) }, }) } } diff --git a/pkg/updater/pkgmgr/homebrew.go b/pkg/updater/pkgmgr/homebrew.go new file mode 100644 index 00000000..057b6529 --- /dev/null +++ b/pkg/updater/pkgmgr/homebrew.go @@ -0,0 +1,74 @@ +package pkgmgr + +import ( + "encoding/json" + "fmt" + "os/exec" +) + +// HomebrewPackageManager detects if a binary was installed via Homebrew +type HomebrewPackageManager struct{ + formula string +} + +var _ PackageManager = (*HomebrewPackageManager)(nil) + +type homebrewInfoOutput struct { + Installed []struct { + Version string `json:"version"` + InstalledOn bool `json:"installed_on_request"` + LinkedKeg string `json:"linked_keg"` + } `json:"installed"` +} + +// NewHomebrewPackageManager creates a new Homebrew package manager detector +func NewHomebrewPackageManager(formula string) PackageManager { + return &HomebrewPackageManager{ + formula: formula, + } +} + +// Name returns the human-readable name of the package manager +func (h *HomebrewPackageManager) Name() string { + return "Homebrew" +} + +// IsInstalled checks if the formula is installed via Homebrew +func (h *HomebrewPackageManager) IsInstalled() (bool, error) { + // First check if brew command exists + brewPath, err := exec.LookPath("brew") + if err != nil { + // No brew command found, definitely not installed via brew + return false, nil + } + + // Check if the formula is installed + out, err := exec.Command(brewPath, "info", h.formula, "--json").Output() + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + if exitError.ExitCode() == 1 { + // brew info with an invalid (not installed) package name returns an error + return false, nil + } + } + return false, err + } + + var info []homebrewInfoOutput + if err := json.Unmarshal(out, &info); err != nil { + return false, err + } + + if len(info) == 0 { + return false, nil + } + + // Check if the formula has any installed versions + return len(info[0].Installed) > 0, nil +} + +// UpgradeCommand returns the command to upgrade the package +func (h *HomebrewPackageManager) UpgradeCommand() string { + return fmt.Sprintf("brew upgrade %s", h.formula) +} + diff --git a/pkg/updater/pkgmgr/krew.go b/pkg/updater/pkgmgr/krew.go new file mode 100644 index 00000000..b96660b9 --- /dev/null +++ b/pkg/updater/pkgmgr/krew.go @@ -0,0 +1,69 @@ +package pkgmgr + +import ( + "fmt" + "os/exec" + "strings" +) + +// KrewPackageManager detects if a binary was installed via kubectl krew +type KrewPackageManager struct{ + pluginName string +} + +var _ PackageManager = (*KrewPackageManager)(nil) + +// NewKrewPackageManager creates a new Krew package manager detector +func NewKrewPackageManager(pluginName string) PackageManager { + return &KrewPackageManager{ + pluginName: pluginName, + } +} + +// Name returns the human-readable name of the package manager +func (k *KrewPackageManager) Name() string { + return "kubectl krew" +} + +// IsInstalled checks if the plugin is installed via krew +func (k *KrewPackageManager) IsInstalled() (bool, error) { + // First check if kubectl krew command exists + _, err := exec.LookPath("kubectl") + if err != nil { + return false, nil + } + + // Check if krew plugin is available + out, err := exec.Command("kubectl", "krew", "version").Output() + if err != nil { + // krew not installed + return false, nil + } + + if !strings.Contains(string(out), "krew") { + return false, nil + } + + // Check if the plugin is installed by listing installed plugins + listOut, err := exec.Command("kubectl", "krew", "list").Output() + if err != nil { + return false, err + } + + // Check if our plugin is in the installed list + installedPlugins := strings.Split(string(listOut), "\n") + for _, line := range installedPlugins { + // Lines are in format: "PLUGIN VERSION" + if strings.HasPrefix(strings.TrimSpace(line), k.pluginName+" ") || strings.TrimSpace(line) == k.pluginName { + return true, nil + } + } + + return false, nil +} + +// UpgradeCommand returns the command to upgrade the plugin +func (k *KrewPackageManager) UpgradeCommand() string { + return fmt.Sprintf("kubectl krew upgrade %s", k.pluginName) +} + diff --git a/pkg/updater/pkgmgr/pkgmgr.go b/pkg/updater/pkgmgr/pkgmgr.go new file mode 100644 index 00000000..1273baee --- /dev/null +++ b/pkg/updater/pkgmgr/pkgmgr.go @@ -0,0 +1,11 @@ +package pkgmgr + +// PackageManager represents an external package manager that can manage the binary +type PackageManager interface { + // IsInstalled returns true if the package/formula is installed via this package manager + IsInstalled() (bool, error) + // UpgradeCommand returns the command the user should run to upgrade + UpgradeCommand() string + // Name returns the human-readable name of the package manager + Name() string +} \ No newline at end of file diff --git a/pkg/updater/updater.go b/pkg/updater/updater.go index e11b6b19..a7dc75b4 100644 --- a/pkg/updater/updater.go +++ b/pkg/updater/updater.go @@ -17,6 +17,7 @@ import ( "time" hv "github.com/hashicorp/go-version" + "github.com/replicatedhq/troubleshoot/pkg/updater/pkgmgr" "github.com/replicatedhq/troubleshoot/pkg/version" ) @@ -47,6 +48,8 @@ func (o *Options) client() *http.Client { // CheckAndUpdate checks GitHub releases for a newer version and, if newer, downloads // the corresponding tar.gz asset, extracts the binary, and atomically replaces CurrentPath. +// If the binary was installed via a package manager (brew, krew), it will display the +// appropriate upgrade command instead of performing the update. func CheckAndUpdate(ctx context.Context, o Options) error { if o.Skip { return nil @@ -80,6 +83,15 @@ func CheckAndUpdate(ctx context.Context, o Options) error { return nil } + // Check if installed via package manager - only show message if newer version exists + if pkgMgr := detectPackageManager(o.BinaryName); pkgMgr != nil { + if o.Printf != nil { + o.Printf("A newer version (%s) is available. Please run: %s\n", + latest, pkgMgr.UpgradeCommand()) + } + return nil + } + if o.Printf != nil { o.Printf("Updating %s from %s to %s...\n", o.BinaryName, current, latest) } @@ -259,3 +271,62 @@ func sanityCheckBinary(path string) error { _ = hex.EncodeToString(h.Sum(nil)) return nil } + +// detectPackageManager checks if the binary was installed via a known package manager +func detectPackageManager(binaryName string) pkgmgr.PackageManager { + // Map binary names to their package manager formula/plugin names + formulaName := getHomebrewFormulaName(binaryName) + pluginName := getKrewPluginName(binaryName) + + // List of package managers to check + packageManagers := []pkgmgr.PackageManager{ + pkgmgr.NewHomebrewPackageManager(formulaName), + pkgmgr.NewKrewPackageManager(pluginName), + } + + for _, pm := range packageManagers { + installed, err := pm.IsInstalled() + if err != nil { + // Continue checking other package managers if one fails + continue + } + if installed { + return pm + } + } + + // No package manager detected + return nil +} + +// getHomebrewFormulaName maps binary names to Homebrew formula names +func getHomebrewFormulaName(binaryName string) string { + formulaMap := map[string]string{ + "preflight": "preflight", + "support-bundle": "support-bundle", + "troubleshoot": "troubleshoot", + } + + if formula, exists := formulaMap[binaryName]; exists { + return formula + } + + // Default to the binary name if not in the map + return binaryName +} + +// getKrewPluginName maps binary names to krew plugin names +func getKrewPluginName(binaryName string) string { + pluginMap := map[string]string{ + "preflight": "preflight", + "support-bundle": "support-bundle", + "troubleshoot": "troubleshoot", + } + + if plugin, exists := pluginMap[binaryName]; exists { + return plugin + } + + // Default to the binary name + return binaryName +}