diff --git a/cmd/cmd.go b/cmd/cmd.go index af84635ea..49a4d479e 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -124,9 +124,9 @@ func NewCmdRoot(streams genericclioptions.IOStreams) *cobra.Command { // Checks if the version check should be run func shouldRunVersionCheck(skipVersionCheckFlag bool, commandName string) bool { - - // If either are true, then the version check should NOT run, hence negation - return !(skipVersionCheckFlag || canCommandSkipVersionCheck(commandName)) + return !skipVersionCheckFlag && + !canCommandSkipVersionCheck(commandName) && + !utils.IsManagedInstall() } func canCommandSkipVersionCheck(commandName string) bool { diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 1993590ac..133c95327 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -26,6 +26,17 @@ var upgradeCmd = &cobra.Command{ } func upgrade(cmd *cobra.Command, args []string) error { + if utils.IsManagedInstall() { + instruction, err := utils.UpgradeInstruction() + if err != nil { + return err + } + fmt.Fprintf(cmd.ErrOrStderr(), + "osdctl was installed via %s; self-upgrade is disabled.\nPlease upgrade using: %s\n", + utils.InstallMethod, instruction) + return nil + } + // rootName ensures that the upgrade will fail if we ever decide to rename osdctl // between releases :-) rootName := cmd.Root().Name() diff --git a/cmd/upgrade_test.go b/cmd/upgrade_test.go new file mode 100644 index 000000000..80c415037 --- /dev/null +++ b/cmd/upgrade_test.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "bytes" + "strings" + "testing" + + "github.com/openshift/osdctl/pkg/utils" +) + +func TestUpgradeRefusesWhenManaged(t *testing.T) { + tests := []struct { + name string + installMethod string + wantSubstring string + wantErr bool + }{ + {"copr", "copr", "dnf upgrade osdctl", false}, + {"homebrew", "homebrew", "brew upgrade osdctl", false}, + {"unknown", "unknown", "unknown install method", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := utils.InstallMethod + defer func() { utils.InstallMethod = original }() + utils.InstallMethod = tt.installMethod + + var buf bytes.Buffer + upgradeCmd.SetErr(&buf) + defer upgradeCmd.SetErr(nil) + + err := upgrade(upgradeCmd, nil) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), tt.wantSubstring) { + t.Errorf("error should contain %q, got: %s", tt.wantSubstring, err.Error()) + } + return + } + if err != nil { + t.Fatalf("expected nil error, got: %v", err) + } + output := buf.String() + if !strings.Contains(output, tt.installMethod) { + t.Errorf("output should mention %q, got: %s", tt.installMethod, output) + } + if !strings.Contains(output, tt.wantSubstring) { + t.Errorf("output should contain %q, got: %s", tt.wantSubstring, output) + } + }) + } +} diff --git a/cmd/version.go b/cmd/version.go index b23577375..824b71ce4 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -13,9 +13,10 @@ import ( // versionResponse is necessary for the JSON version response. It uses the three // variables that get set during the build. type versionResponse struct { - Commit string `json:"commit"` - Version string `json:"version"` - Latest string `json:"latest"` + Commit string `json:"commit"` + Version string `json:"version"` + Latest string `json:"latest"` + InstallMethod string `json:"install_method,omitempty"` } // versionCmd is the subcommand "osdctl version" for cobra. @@ -41,9 +42,10 @@ func version(cmd *cobra.Command, args []string) error { latest, _ := utils.GetLatestVersion() // let's ignore this error, just in case we have no internet access ver, err := json.MarshalIndent(&versionResponse{ - Commit: gitCommit, - Version: utils.Version, - Latest: strings.TrimPrefix(latest, "v"), + Commit: gitCommit, + Version: utils.Version, + Latest: strings.TrimPrefix(latest, "v"), + InstallMethod: utils.InstallMethod, }, "", " ") if err != nil { return err diff --git a/hack/copr.sh b/hack/copr.sh index 84c1e8bb8..2af9c29b8 100644 --- a/hack/copr.sh +++ b/hack/copr.sh @@ -83,7 +83,7 @@ Source: %{gosource} %if %{without bootstrap} %define gomodulesmode GO111MODULE=on %build -export GO_LDFLAGS='-X "github.com/openshift/osdctl/pkg/utils.Version="@version@"' +export GO_LDFLAGS='-X "github.com/openshift/osdctl/pkg/utils.Version=@version@" -X "github.com/openshift/osdctl/pkg/utils.InstallMethod=copr"' %gobuild -o %{gobuilddir}/bin/osdctl %{goipath} %endif diff --git a/hack/osdctl.spec b/hack/osdctl.spec index 44bb149db..df47e535d 100644 --- a/hack/osdctl.spec +++ b/hack/osdctl.spec @@ -56,7 +56,7 @@ Source: %{gosource} %if %{without bootstrap} %define gomodulesmode GO111MODULE=on %build -export GO_LDFLAGS='-X "github.com/openshift/osdctl/pkg/utils.Version="@version@"' +export GO_LDFLAGS='-X "github.com/openshift/osdctl/pkg/utils.Version=@version@" -X "github.com/openshift/osdctl/pkg/utils.InstallMethod=copr"' %gobuild -o %{gobuilddir}/bin/osdctl %{goipath} %endif diff --git a/pkg/utils/version.go b/pkg/utils/version.go index 5ce13b32f..120ec33c0 100644 --- a/pkg/utils/version.go +++ b/pkg/utils/version.go @@ -2,6 +2,7 @@ package utils import ( "encoding/json" + "fmt" "io" "net/http" "time" @@ -22,8 +23,35 @@ var ( // Will be set during build process via GoReleaser // See also: https://pkg.go.dev/cmd/link Version string + + // InstallMethod is set at build time via -X ldflags when osdctl is + // built by a package manager. Empty string (default) means the binary + // was built from source or via GoReleaser (GitHub releases). + // Known values: "copr", "homebrew". + InstallMethod string ) +// IsManagedInstall reports whether osdctl was installed via a package +// manager (e.g. COPR/RPM, Homebrew) rather than from a GitHub release. +func IsManagedInstall() bool { + return InstallMethod != "" +} + +// UpgradeInstruction returns a human-readable upgrade command for the +// current install method. +func UpgradeInstruction() (string, error) { + switch InstallMethod { + case "": + return "", nil + case "copr": + return "dnf upgrade osdctl", nil + case "homebrew": + return "brew upgrade osdctl", nil + default: + return "", fmt.Errorf("unknown install method: %q", InstallMethod) + } +} + // githubResponse is a necessary struct for the JSON unmarshalling that is happening // in the getLatestVersion(). type gitHubResponse struct { diff --git a/pkg/utils/version_test.go b/pkg/utils/version_test.go new file mode 100644 index 000000000..50c75a971 --- /dev/null +++ b/pkg/utils/version_test.go @@ -0,0 +1,54 @@ +package utils + +import "testing" + +func TestIsManagedInstall(t *testing.T) { + tests := []struct { + name string + installMethod string + want bool + }{ + {"empty (GitHub release)", "", false}, + {"copr", "copr", true}, + {"homebrew", "homebrew", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := InstallMethod + defer func() { InstallMethod = original }() + InstallMethod = tt.installMethod + if got := IsManagedInstall(); got != tt.want { + t.Errorf("IsManagedInstall() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUpgradeInstruction(t *testing.T) { + tests := []struct { + name string + installMethod string + want string + wantErr bool + }{ + {"copr", "copr", "dnf upgrade osdctl", false}, + {"homebrew", "homebrew", "brew upgrade osdctl", false}, + {"empty", "", "", false}, + {"unknown", "unknown", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + original := InstallMethod + defer func() { InstallMethod = original }() + InstallMethod = tt.installMethod + got, err := UpgradeInstruction() + if (err != nil) != tt.wantErr { + t.Errorf("UpgradeInstruction() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("UpgradeInstruction() = %q, want %q", got, tt.want) + } + }) + } +}