Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import (
"github.com/brevdev/brev-cli/pkg/cmd/tasks"
"github.com/brevdev/brev-cli/pkg/cmd/test"
"github.com/brevdev/brev-cli/pkg/cmd/updatemodel"
"github.com/brevdev/brev-cli/pkg/cmd/upgrade"
"github.com/brevdev/brev-cli/pkg/cmd/version"
"github.com/brevdev/brev-cli/pkg/cmd/workspacegroups"
"github.com/brevdev/brev-cli/pkg/cmd/writeconnectionevent"
Expand Down Expand Up @@ -345,6 +346,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
cmd.AddCommand(refresh.NewCmdRefresh(t, loginCmdStore))
cmd.AddCommand(register.NewCmdRegister(t, externalNodeCmdStore))
cmd.AddCommand(deregister.NewCmdDeregister(t, externalNodeCmdStore))
cmd.AddCommand(upgrade.NewCmdUpgrade(t, noLoginCmdStore))
cmd.AddCommand(enablessh.NewCmdEnableSSH(t, externalNodeCmdStore))
cmd.AddCommand(grantssh.NewCmdGrantSSH(t, externalNodeCmdStore))
cmd.AddCommand(revokessh.NewCmdRevokeSSH(t, externalNodeCmdStore))
Expand Down
7 changes: 7 additions & 0 deletions pkg/cmd/deregister/deregister.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/brevdev/brev-cli/pkg/config"
"github.com/brevdev/brev-cli/pkg/entity"
"github.com/brevdev/brev-cli/pkg/externalnode"
"github.com/brevdev/brev-cli/pkg/sudo"
"github.com/brevdev/brev-cli/pkg/terminal"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -46,6 +47,7 @@ func (brevSSHKeyRemover) RemoveBrevKeys(u *user.User) ([]string, error) {
type deregisterDeps struct {
platform externalnode.PlatformChecker
prompter terminal.Selector
confirmer terminal.Confirmer
netbird register.NetBirdManager
nodeClients externalnode.NodeClientFactory
registrationStore register.RegistrationStore
Expand All @@ -56,6 +58,7 @@ func defaultDeregisterDeps() deregisterDeps {
return deregisterDeps{
platform: register.LinuxPlatform{},
prompter: register.TerminalPrompter{},
confirmer: register.TerminalPrompter{},
netbird: register.Netbird{},
nodeClients: register.DefaultNodeClientFactory{},
registrationStore: register.NewFileRegistrationStore(),
Expand Down Expand Up @@ -93,6 +96,10 @@ func runDeregister(ctx context.Context, t *terminal.Terminal, s DeregisterStore,
return fmt.Errorf("brev deregister is only supported on Linux")
}

if err := sudo.Gate(t, deps.confirmer, "Device deregistration"); err != nil {
return fmt.Errorf("sudo issue: %w", err)
}

reg, err := deps.registrationStore.Load()
if err != nil {
return breverrors.WrapAndTrace(err)
Expand Down
5 changes: 5 additions & 0 deletions pkg/cmd/deregister/deregister_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ func (m mockSelector) Select(label string, items []string) string {
return m.fn(label, items)
}

type mockConfirmer struct{ confirm bool }

func (m mockConfirmer) ConfirmYesNo(_ string) bool { return m.confirm }

type mockNetBirdManager struct {
called bool
err error
Expand Down Expand Up @@ -131,6 +135,7 @@ func testDeregisterDeps(t *testing.T, svc *fakeNodeService, regStore register.Re
}
return ""
}},
confirmer: mockConfirmer{confirm: true},
netbird: &mockNetBirdManager{},
nodeClients: mockNodeClientFactory{serverURL: server.URL},
registrationStore: regStore,
Expand Down
12 changes: 1 addition & 11 deletions pkg/cmd/open/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, s
return handlePathError(tstore, workspace, errMsg)
}
if strings.Contains(err.Error(), `tmux: command not found`) {
errMsg := "tmux not found on remote instance. This will be installed automatically."
errMsg := "tmux not found on remote instance. Please install it and try again."
return handlePathError(tstore, workspace, errMsg)
}
return breverrors.WrapAndTrace(err)
Expand Down Expand Up @@ -809,16 +809,6 @@ func ensureTmuxInstalled(sshAlias string) error {
checkCmd := fmt.Sprintf("ssh %s 'which tmux >/dev/null 2>&1'", sshAlias)
checkExec := exec.Command("bash", "-c", checkCmd) // #nosec G204
err := checkExec.Run()
if err == nil {
return nil
}

installCmd := fmt.Sprintf("ssh %s 'sudo apt-get update && sudo apt-get install -y tmux'", sshAlias)
installExec := exec.Command("bash", "-c", installCmd) // #nosec G204
installExec.Stderr = os.Stderr
installExec.Stdout = os.Stdout

err = installExec.Run()
if err != nil {
return breverrors.WrapAndTrace(err)
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/cmd/register/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
breverrors "github.com/brevdev/brev-cli/pkg/errors"
"github.com/brevdev/brev-cli/pkg/externalnode"
"github.com/brevdev/brev-cli/pkg/names"
"github.com/brevdev/brev-cli/pkg/sudo"
"github.com/brevdev/brev-cli/pkg/terminal"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -107,6 +108,10 @@ func runRegister(ctx context.Context, t *terminal.Terminal, s RegisterStore, nam
return breverrors.New("brev register is only supported on Linux")
}

if err := sudo.Gate(t, deps.prompter, "Device registration"); err != nil {
return fmt.Errorf("sudo issue: %w", err)
}

alreadyRegistered, err := deps.registrationStore.Exists()
if err != nil {
return breverrors.WrapAndTrace(err)
Expand Down
14 changes: 9 additions & 5 deletions pkg/cmd/register/register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,14 +246,18 @@ func Test_runRegister_UserCancels(t *testing.T) {

term := terminal.New()
err := runRegister(context.Background(), term, store, "my-spark", "", deps)
if err != nil {
t.Fatalf("expected nil error on cancel, got: %v", err)
// sudo.Gate returns a wrapped error when the user declines
if err == nil {
t.Fatal("expected error when user declines sudo gate")
}
if !strings.Contains(err.Error(), "canceled by user") {
t.Errorf("expected 'canceled by user' error, got: %v", err)
}

// Registration should not exist
exists, err := regStore.Exists()
if err != nil {
t.Fatalf("Exists error: %v", err)
exists, eerr := regStore.Exists()
if eerr != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing err to eerr seems unnecessary here

t.Fatalf("Exists error: %v", eerr)
}
if exists {
t.Error("registration should not exist after cancel")
Expand Down
32 changes: 32 additions & 0 deletions pkg/cmd/upgrade/detector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package upgrade

import "os/exec"

// InstallMethod represents how brev was installed on the system.
type InstallMethod int

const (
// InstallMethodDirect means brev was installed via direct binary download.
InstallMethodDirect InstallMethod = iota

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about iota

// InstallMethodBrew means brev was installed via Homebrew.
InstallMethodBrew
)

// Detector determines how brev was installed.
type Detector interface {
Detect() InstallMethod
}

// SystemDetector checks the actual system for install method.
type SystemDetector struct{}

// Detect checks whether brev was installed via Homebrew or direct download.
func (SystemDetector) Detect() InstallMethod {
if _, err := exec.LookPath("brew"); err != nil {
return InstallMethodDirect
}
if exec.Command("brew", "list", "brev").Run() == nil { //nolint:gosec // intentional brew probe
return InstallMethodBrew
}
return InstallMethodDirect
}
100 changes: 100 additions & 0 deletions pkg/cmd/upgrade/runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package upgrade

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"

"github.com/brevdev/brev-cli/pkg/terminal"
)

// Upgrader executes the actual upgrade process.
type Upgrader interface {
UpgradeViaBrew(t *terminal.Terminal) error
UpgradeViaInstallScript(t *terminal.Terminal) error
}

// SystemUpgrader executes upgrade commands on the real system.
type SystemUpgrader struct{}

// UpgradeViaBrew runs "brew upgrade brev" with output connected to the terminal.
func (SystemUpgrader) UpgradeViaBrew(t *terminal.Terminal) error {
t.Vprint("Running: brew upgrade brev")
t.Vprint("")

cmd := exec.Command("brew", "upgrade", "brev")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("brew upgrade failed: %w", err)
}
return nil
}

// UpgradeViaInstallScript downloads and installs the latest brev binary from GitHub.
func (SystemUpgrader) UpgradeViaInstallScript(t *terminal.Terminal) error {
osName := runtime.GOOS
arch := runtime.GOARCH

t.Vprintf("Detected platform: %s/%s\n", osName, arch)
t.Vprint("")

// Fetch latest release download URL
t.Vprint("Fetching latest release...")
pattern := fmt.Sprintf("browser_download_url.*%s.*%s", osName, arch)
curlCmd := fmt.Sprintf(
`curl -s https://api.github.com/repos/brevdev/brev-cli/releases/latest | grep "%s" | cut -d '"' -f 4`,
pattern,
)
out, err := exec.Command("bash", "-c", curlCmd).Output() //nolint:gosec // constructing URL pattern from known values
if err != nil {
return fmt.Errorf("failed to fetch latest release: %w", err)
}
downloadURL := string(out)
if downloadURL == "" {
return fmt.Errorf("could not find release for %s/%s", osName, arch)
}
// Trim trailing newline
downloadURL = downloadURL[:len(downloadURL)-1]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this newline guaranteed to be there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewrote to use script instead


// Create temp directory
tmpDir, err := os.MkdirTemp("", "brev-upgrade-*")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the *?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apparently this controls where the random string goes. By default it gets appended, removing.

if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)

// Download
t.Vprintf("Downloading %s...\n", downloadURL)
tarPath := filepath.Join(tmpDir, "brev.tar.gz")
dlCmd := exec.Command("curl", "-sL", downloadURL, "-o", tarPath)
dlCmd.Stderr = os.Stderr
if err := dlCmd.Run(); err != nil {
return fmt.Errorf("download failed: %w", err)
}

// Extract
extractCmd := exec.Command("tar", "-xzf", tarPath, "-C", tmpDir)
if err := extractCmd.Run(); err != nil {
return fmt.Errorf("extraction failed: %w", err)
}

// Install with sudo
brevPath := filepath.Join(tmpDir, "brev")
t.Vprint("Installing to /usr/local/bin/brev...")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could throw /usr/local/bin/brev into a const

mvCmd := exec.Command("sudo", "mv", brevPath, "/usr/local/bin/brev")
mvCmd.Stdout = os.Stdout
mvCmd.Stderr = os.Stderr
if err := mvCmd.Run(); err != nil {
return fmt.Errorf("failed to install binary: %w", err)
}

chmodCmd := exec.Command("sudo", "chmod", "+x", "/usr/local/bin/brev")
if err := chmodCmd.Run(); err != nil {
return fmt.Errorf("failed to set permissions: %w", err)
}

return nil
}
122 changes: 122 additions & 0 deletions pkg/cmd/upgrade/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Package upgrade provides the brev upgrade command.
package upgrade

import (
"fmt"

"github.com/brevdev/brev-cli/pkg/cmd/register"
"github.com/brevdev/brev-cli/pkg/cmd/version"
"github.com/brevdev/brev-cli/pkg/store"
"github.com/brevdev/brev-cli/pkg/sudo"
"github.com/brevdev/brev-cli/pkg/terminal"

"github.com/spf13/cobra"
)

// VersionStore fetches the latest release metadata from GitHub.
type VersionStore interface {
GetLatestReleaseMetadata() (*store.GithubReleaseMetadata, error)
}

type upgradeDeps struct {
detector Detector
upgrader Upgrader
confirmer terminal.Confirmer
}

func defaultUpgradeDeps() upgradeDeps {
return upgradeDeps{
detector: SystemDetector{},
upgrader: SystemUpgrader{},
confirmer: register.TerminalPrompter{},
}
}

var (
upgradeLong = "Upgrade brev to the latest version."
upgradeExample = " brev upgrade"
)

// NewCmdUpgrade creates the brev upgrade command.
func NewCmdUpgrade(t *terminal.Terminal, versionStore VersionStore) *cobra.Command {
cmd := &cobra.Command{
Annotations: map[string]string{"configuration": ""},
Use: "upgrade",
DisableFlagsInUseLine: true,
Short: "Upgrade brev to the latest version",
Long: upgradeLong,
Example: upgradeExample,
RunE: func(cmd *cobra.Command, args []string) error {
return runUpgrade(t, versionStore, defaultUpgradeDeps())
},
}
return cmd
}

func runUpgrade(t *terminal.Terminal, vs VersionStore, deps upgradeDeps) error {
t.Vprint("")
t.Vprintf("Current version: %s\n", version.Version)

release, err := vs.GetLatestReleaseMetadata()
if err != nil {
return fmt.Errorf("failed to check latest version: %w", err)
}

if release.TagName == version.Version {
t.Vprint(t.Green("Already up to date."))
return nil
}

t.Vprintf("New version available: %s\n", release.TagName)
t.Vprint("")

method := deps.detector.Detect()

switch method {
case InstallMethodBrew:
return upgradeViaBrew(t, deps)
case InstallMethodDirect:
return upgradeViaDirect(t, deps)
default:
return fmt.Errorf("unknown install method")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also install the latest skill?

}

func upgradeViaBrew(t *terminal.Terminal, deps upgradeDeps) error {
t.Vprint("Detected install method: Homebrew")
t.Vprint("This will run: brew upgrade brev")
t.Vprint("")

if !deps.confirmer.ConfirmYesNo("Proceed with upgrade?") {
t.Vprint("Upgrade canceled.")
return nil
}

t.Vprint("")
if err := deps.upgrader.UpgradeViaBrew(t); err != nil {
return fmt.Errorf("brew upgrade: %w", err)
}

t.Vprint("")
t.Vprint(t.Green("Upgrade complete."))
return nil
}

func upgradeViaDirect(t *terminal.Terminal, deps upgradeDeps) error {
t.Vprint("Detected install method: direct binary install")
t.Vprint("This will download the latest release and install it to /usr/local/bin/brev")
t.Vprint("")

if err := sudo.Gate(t, deps.confirmer, "Upgrade"); err != nil {
return fmt.Errorf("sudo issue: %w", err)
}

t.Vprint("")
if err := deps.upgrader.UpgradeViaInstallScript(t); err != nil {
return fmt.Errorf("direct upgrade: %w", err)
}

t.Vprint("")
t.Vprint(t.Green("Upgrade complete."))
return nil
}
Loading
Loading