Skip to content

Commit 16ab265

Browse files
committed
feat(BRE2-810): sudo gating. Remove tmux remote install
1 parent 9266638 commit 16ab265

12 files changed

Lines changed: 519 additions & 22 deletions

File tree

pkg/cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import (
5454
"github.com/brevdev/brev-cli/pkg/cmd/tasks"
5555
"github.com/brevdev/brev-cli/pkg/cmd/test"
5656
"github.com/brevdev/brev-cli/pkg/cmd/updatemodel"
57+
"github.com/brevdev/brev-cli/pkg/cmd/upgrade"
5758
"github.com/brevdev/brev-cli/pkg/cmd/version"
5859
"github.com/brevdev/brev-cli/pkg/cmd/workspacegroups"
5960
"github.com/brevdev/brev-cli/pkg/cmd/writeconnectionevent"
@@ -345,6 +346,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
345346
cmd.AddCommand(refresh.NewCmdRefresh(t, loginCmdStore))
346347
cmd.AddCommand(register.NewCmdRegister(t, externalNodeCmdStore))
347348
cmd.AddCommand(deregister.NewCmdDeregister(t, externalNodeCmdStore))
349+
cmd.AddCommand(upgrade.NewCmdUpgrade(t, noLoginCmdStore))
348350
cmd.AddCommand(enablessh.NewCmdEnableSSH(t, externalNodeCmdStore))
349351
cmd.AddCommand(grantssh.NewCmdGrantSSH(t, externalNodeCmdStore))
350352
cmd.AddCommand(revokessh.NewCmdRevokeSSH(t, externalNodeCmdStore))

pkg/cmd/deregister/deregister.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/brevdev/brev-cli/pkg/config"
1515
"github.com/brevdev/brev-cli/pkg/entity"
1616
"github.com/brevdev/brev-cli/pkg/externalnode"
17+
"github.com/brevdev/brev-cli/pkg/sudo"
1718
"github.com/brevdev/brev-cli/pkg/terminal"
1819

1920
"github.com/spf13/cobra"
@@ -46,6 +47,7 @@ func (brevSSHKeyRemover) RemoveBrevKeys(u *user.User) ([]string, error) {
4647
type deregisterDeps struct {
4748
platform externalnode.PlatformChecker
4849
prompter terminal.Selector
50+
confirmer terminal.Confirmer
4951
netbird register.NetBirdManager
5052
nodeClients externalnode.NodeClientFactory
5153
registrationStore register.RegistrationStore
@@ -56,6 +58,7 @@ func defaultDeregisterDeps() deregisterDeps {
5658
return deregisterDeps{
5759
platform: register.LinuxPlatform{},
5860
prompter: register.TerminalPrompter{},
61+
confirmer: register.TerminalPrompter{},
5962
netbird: register.Netbird{},
6063
nodeClients: register.DefaultNodeClientFactory{},
6164
registrationStore: register.NewFileRegistrationStore(),
@@ -93,6 +96,10 @@ func runDeregister(ctx context.Context, t *terminal.Terminal, s DeregisterStore,
9396
return fmt.Errorf("brev deregister is only supported on Linux")
9497
}
9598

99+
if err := sudo.Gate(t, deps.confirmer, "Device deregistration"); err != nil {
100+
return fmt.Errorf("sudo issue: %w", err)
101+
}
102+
96103
reg, err := deps.registrationStore.Load()
97104
if err != nil {
98105
return breverrors.WrapAndTrace(err)

pkg/cmd/deregister/deregister_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ func (m mockSelector) Select(label string, items []string) string {
8686
return m.fn(label, items)
8787
}
8888

89+
type mockConfirmer struct{ confirm bool }
90+
91+
func (m mockConfirmer) ConfirmYesNo(_ string) bool { return m.confirm }
92+
8993
type mockNetBirdManager struct {
9094
called bool
9195
err error
@@ -131,6 +135,7 @@ func testDeregisterDeps(t *testing.T, svc *fakeNodeService, regStore register.Re
131135
}
132136
return ""
133137
}},
138+
confirmer: mockConfirmer{confirm: true},
134139
netbird: &mockNetBirdManager{},
135140
nodeClients: mockNodeClientFactory{serverURL: server.URL},
136141
registrationStore: regStore,

pkg/cmd/open/open.go

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, s
356356
return handlePathError(tstore, workspace, errMsg)
357357
}
358358
if strings.Contains(err.Error(), `tmux: command not found`) {
359-
errMsg := "tmux not found on remote instance. This will be installed automatically."
359+
errMsg := "tmux not found on remote instance. Please install it and try again."
360360
return handlePathError(tstore, workspace, errMsg)
361361
}
362362
return breverrors.WrapAndTrace(err)
@@ -809,16 +809,6 @@ func ensureTmuxInstalled(sshAlias string) error {
809809
checkCmd := fmt.Sprintf("ssh %s 'which tmux >/dev/null 2>&1'", sshAlias)
810810
checkExec := exec.Command("bash", "-c", checkCmd) // #nosec G204
811811
err := checkExec.Run()
812-
if err == nil {
813-
return nil
814-
}
815-
816-
installCmd := fmt.Sprintf("ssh %s 'sudo apt-get update && sudo apt-get install -y tmux'", sshAlias)
817-
installExec := exec.Command("bash", "-c", installCmd) // #nosec G204
818-
installExec.Stderr = os.Stderr
819-
installExec.Stdout = os.Stdout
820-
821-
err = installExec.Run()
822812
if err != nil {
823813
return breverrors.WrapAndTrace(err)
824814
}

pkg/cmd/register/register.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
breverrors "github.com/brevdev/brev-cli/pkg/errors"
1818
"github.com/brevdev/brev-cli/pkg/externalnode"
1919
"github.com/brevdev/brev-cli/pkg/names"
20+
"github.com/brevdev/brev-cli/pkg/sudo"
2021
"github.com/brevdev/brev-cli/pkg/terminal"
2122

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

111+
if err := sudo.Gate(t, deps.prompter, "Device registration"); err != nil {
112+
return fmt.Errorf("sudo issue: %w", err)
113+
}
114+
110115
alreadyRegistered, err := deps.registrationStore.Exists()
111116
if err != nil {
112117
return breverrors.WrapAndTrace(err)

pkg/cmd/register/register_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,18 @@ func Test_runRegister_UserCancels(t *testing.T) {
246246

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

253257
// Registration should not exist
254-
exists, err := regStore.Exists()
255-
if err != nil {
256-
t.Fatalf("Exists error: %v", err)
258+
exists, eerr := regStore.Exists()
259+
if eerr != nil {
260+
t.Fatalf("Exists error: %v", eerr)
257261
}
258262
if exists {
259263
t.Error("registration should not exist after cancel")

pkg/cmd/upgrade/detector.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package upgrade
2+
3+
import "os/exec"
4+
5+
// InstallMethod represents how brev was installed on the system.
6+
type InstallMethod int
7+
8+
const (
9+
// InstallMethodDirect means brev was installed via direct binary download.
10+
InstallMethodDirect InstallMethod = iota
11+
// InstallMethodBrew means brev was installed via Homebrew.
12+
InstallMethodBrew
13+
)
14+
15+
// Detector determines how brev was installed.
16+
type Detector interface {
17+
Detect() InstallMethod
18+
}
19+
20+
// SystemDetector checks the actual system for install method.
21+
type SystemDetector struct{}
22+
23+
// Detect checks whether brev was installed via Homebrew or direct download.
24+
func (SystemDetector) Detect() InstallMethod {
25+
if _, err := exec.LookPath("brew"); err != nil {
26+
return InstallMethodDirect
27+
}
28+
if exec.Command("brew", "list", "brev").Run() == nil { //nolint:gosec // intentional brew probe
29+
return InstallMethodBrew
30+
}
31+
return InstallMethodDirect
32+
}

pkg/cmd/upgrade/runner.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package upgrade
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"runtime"
9+
10+
"github.com/brevdev/brev-cli/pkg/terminal"
11+
)
12+
13+
// Upgrader executes the actual upgrade process.
14+
type Upgrader interface {
15+
UpgradeViaBrew(t *terminal.Terminal) error
16+
UpgradeViaInstallScript(t *terminal.Terminal) error
17+
}
18+
19+
// SystemUpgrader executes upgrade commands on the real system.
20+
type SystemUpgrader struct{}
21+
22+
// UpgradeViaBrew runs "brew upgrade brev" with output connected to the terminal.
23+
func (SystemUpgrader) UpgradeViaBrew(t *terminal.Terminal) error {
24+
t.Vprint("Running: brew upgrade brev")
25+
t.Vprint("")
26+
27+
cmd := exec.Command("brew", "upgrade", "brev")
28+
cmd.Stdout = os.Stdout
29+
cmd.Stderr = os.Stderr
30+
if err := cmd.Run(); err != nil {
31+
return fmt.Errorf("brew upgrade failed: %w", err)
32+
}
33+
return nil
34+
}
35+
36+
// UpgradeViaInstallScript downloads and installs the latest brev binary from GitHub.
37+
func (SystemUpgrader) UpgradeViaInstallScript(t *terminal.Terminal) error {
38+
osName := runtime.GOOS
39+
arch := runtime.GOARCH
40+
41+
t.Vprintf("Detected platform: %s/%s\n", osName, arch)
42+
t.Vprint("")
43+
44+
// Fetch latest release download URL
45+
t.Vprint("Fetching latest release...")
46+
pattern := fmt.Sprintf("browser_download_url.*%s.*%s", osName, arch)
47+
curlCmd := fmt.Sprintf(
48+
`curl -s https://api.github.com/repos/brevdev/brev-cli/releases/latest | grep "%s" | cut -d '"' -f 4`,
49+
pattern,
50+
)
51+
out, err := exec.Command("bash", "-c", curlCmd).Output() //nolint:gosec // constructing URL pattern from known values
52+
if err != nil {
53+
return fmt.Errorf("failed to fetch latest release: %w", err)
54+
}
55+
downloadURL := string(out)
56+
if downloadURL == "" {
57+
return fmt.Errorf("could not find release for %s/%s", osName, arch)
58+
}
59+
// Trim trailing newline
60+
downloadURL = downloadURL[:len(downloadURL)-1]
61+
62+
// Create temp directory
63+
tmpDir, err := os.MkdirTemp("", "brev-upgrade-*")
64+
if err != nil {
65+
return fmt.Errorf("failed to create temp directory: %w", err)
66+
}
67+
defer os.RemoveAll(tmpDir)
68+
69+
// Download
70+
t.Vprintf("Downloading %s...\n", downloadURL)
71+
tarPath := filepath.Join(tmpDir, "brev.tar.gz")
72+
dlCmd := exec.Command("curl", "-sL", downloadURL, "-o", tarPath)
73+
dlCmd.Stderr = os.Stderr
74+
if err := dlCmd.Run(); err != nil {
75+
return fmt.Errorf("download failed: %w", err)
76+
}
77+
78+
// Extract
79+
extractCmd := exec.Command("tar", "-xzf", tarPath, "-C", tmpDir)
80+
if err := extractCmd.Run(); err != nil {
81+
return fmt.Errorf("extraction failed: %w", err)
82+
}
83+
84+
// Install with sudo
85+
brevPath := filepath.Join(tmpDir, "brev")
86+
t.Vprint("Installing to /usr/local/bin/brev...")
87+
mvCmd := exec.Command("sudo", "mv", brevPath, "/usr/local/bin/brev")
88+
mvCmd.Stdout = os.Stdout
89+
mvCmd.Stderr = os.Stderr
90+
if err := mvCmd.Run(); err != nil {
91+
return fmt.Errorf("failed to install binary: %w", err)
92+
}
93+
94+
chmodCmd := exec.Command("sudo", "chmod", "+x", "/usr/local/bin/brev")
95+
if err := chmodCmd.Run(); err != nil {
96+
return fmt.Errorf("failed to set permissions: %w", err)
97+
}
98+
99+
return nil
100+
}

pkg/cmd/upgrade/upgrade.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Package upgrade provides the brev upgrade command.
2+
package upgrade
3+
4+
import (
5+
"fmt"
6+
7+
"github.com/brevdev/brev-cli/pkg/cmd/register"
8+
"github.com/brevdev/brev-cli/pkg/cmd/version"
9+
"github.com/brevdev/brev-cli/pkg/store"
10+
"github.com/brevdev/brev-cli/pkg/sudo"
11+
"github.com/brevdev/brev-cli/pkg/terminal"
12+
13+
"github.com/spf13/cobra"
14+
)
15+
16+
// VersionStore fetches the latest release metadata from GitHub.
17+
type VersionStore interface {
18+
GetLatestReleaseMetadata() (*store.GithubReleaseMetadata, error)
19+
}
20+
21+
type upgradeDeps struct {
22+
detector Detector
23+
upgrader Upgrader
24+
confirmer terminal.Confirmer
25+
}
26+
27+
func defaultUpgradeDeps() upgradeDeps {
28+
return upgradeDeps{
29+
detector: SystemDetector{},
30+
upgrader: SystemUpgrader{},
31+
confirmer: register.TerminalPrompter{},
32+
}
33+
}
34+
35+
var (
36+
upgradeLong = "Upgrade brev to the latest version."
37+
upgradeExample = " brev upgrade"
38+
)
39+
40+
// NewCmdUpgrade creates the brev upgrade command.
41+
func NewCmdUpgrade(t *terminal.Terminal, versionStore VersionStore) *cobra.Command {
42+
cmd := &cobra.Command{
43+
Annotations: map[string]string{"configuration": ""},
44+
Use: "upgrade",
45+
DisableFlagsInUseLine: true,
46+
Short: "Upgrade brev to the latest version",
47+
Long: upgradeLong,
48+
Example: upgradeExample,
49+
RunE: func(cmd *cobra.Command, args []string) error {
50+
return runUpgrade(t, versionStore, defaultUpgradeDeps())
51+
},
52+
}
53+
return cmd
54+
}
55+
56+
func runUpgrade(t *terminal.Terminal, vs VersionStore, deps upgradeDeps) error {
57+
t.Vprint("")
58+
t.Vprintf("Current version: %s\n", version.Version)
59+
60+
release, err := vs.GetLatestReleaseMetadata()
61+
if err != nil {
62+
return fmt.Errorf("failed to check latest version: %w", err)
63+
}
64+
65+
if release.TagName == version.Version {
66+
t.Vprint(t.Green("Already up to date."))
67+
return nil
68+
}
69+
70+
t.Vprintf("New version available: %s\n", release.TagName)
71+
t.Vprint("")
72+
73+
method := deps.detector.Detect()
74+
75+
switch method {
76+
case InstallMethodBrew:
77+
return upgradeViaBrew(t, deps)
78+
case InstallMethodDirect:
79+
return upgradeViaDirect(t, deps)
80+
default:
81+
return fmt.Errorf("unknown install method")
82+
}
83+
}
84+
85+
func upgradeViaBrew(t *terminal.Terminal, deps upgradeDeps) error {
86+
t.Vprint("Detected install method: Homebrew")
87+
t.Vprint("This will run: brew upgrade brev")
88+
t.Vprint("")
89+
90+
if !deps.confirmer.ConfirmYesNo("Proceed with upgrade?") {
91+
t.Vprint("Upgrade canceled.")
92+
return nil
93+
}
94+
95+
t.Vprint("")
96+
if err := deps.upgrader.UpgradeViaBrew(t); err != nil {
97+
return fmt.Errorf("brew upgrade: %w", err)
98+
}
99+
100+
t.Vprint("")
101+
t.Vprint(t.Green("Upgrade complete."))
102+
return nil
103+
}
104+
105+
func upgradeViaDirect(t *terminal.Terminal, deps upgradeDeps) error {
106+
t.Vprint("Detected install method: direct binary install")
107+
t.Vprint("This will download the latest release and install it to /usr/local/bin/brev")
108+
t.Vprint("")
109+
110+
if err := sudo.Gate(t, deps.confirmer, "Upgrade"); err != nil {
111+
return fmt.Errorf("sudo issue: %w", err)
112+
}
113+
114+
t.Vprint("")
115+
if err := deps.upgrader.UpgradeViaInstallScript(t); err != nil {
116+
return fmt.Errorf("direct upgrade: %w", err)
117+
}
118+
119+
t.Vprint("")
120+
t.Vprint(t.Green("Upgrade complete."))
121+
return nil
122+
}

0 commit comments

Comments
 (0)