Skip to content

Commit e5ec5f1

Browse files
authored
feat(BRE2-810): sudo gating, brev upgrade (#320)
* feat(BRE2-810): sudo gating. Remove tmux remote install * review feedback * review feedback: install claude skill
1 parent ce5403b commit e5ec5f1

12 files changed

Lines changed: 568 additions & 19 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: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,12 @@ 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

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 = 0
11+
// InstallMethodBrew means brev was installed via Homebrew.
12+
InstallMethodBrew InstallMethod = 1
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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package upgrade
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
8+
"github.com/brevdev/brev-cli/pkg/terminal"
9+
)
10+
11+
const installScriptURL = "https://raw.githubusercontent.com/brevdev/brev-cli/main/bin/install-latest.sh"
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 runs the upstream install-latest.sh script via sudo.
37+
func (SystemUpgrader) UpgradeViaInstallScript(t *terminal.Terminal) error {
38+
t.Vprintf("Running: bash -c \"$(curl -fsSL %s)\"\n", installScriptURL)
39+
t.Vprint("")
40+
41+
cmd := exec.Command("bash", "-c", fmt.Sprintf(`curl -fsSL "%s" | bash`, installScriptURL)) //nolint:gosec // URL is a compile-time constant
42+
cmd.Stdout = os.Stdout
43+
cmd.Stderr = os.Stderr
44+
if err := cmd.Run(); err != nil {
45+
return fmt.Errorf("install script failed: %w", err)
46+
}
47+
return nil
48+
}

pkg/cmd/upgrade/upgrade.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Package upgrade provides the brev upgrade command.
2+
package upgrade
3+
4+
import (
5+
"fmt"
6+
"os"
7+
8+
"github.com/brevdev/brev-cli/pkg/cmd/agentskill"
9+
"github.com/brevdev/brev-cli/pkg/cmd/register"
10+
"github.com/brevdev/brev-cli/pkg/cmd/version"
11+
"github.com/brevdev/brev-cli/pkg/store"
12+
"github.com/brevdev/brev-cli/pkg/sudo"
13+
"github.com/brevdev/brev-cli/pkg/terminal"
14+
15+
"github.com/spf13/cobra"
16+
)
17+
18+
// VersionStore fetches the latest release metadata from GitHub.
19+
type VersionStore interface {
20+
GetLatestReleaseMetadata() (*store.GithubReleaseMetadata, error)
21+
}
22+
23+
// SkillInstaller updates agent skill files after a binary upgrade.
24+
type SkillInstaller interface {
25+
InstallSkill(t *terminal.Terminal) error
26+
}
27+
28+
// defaultSkillInstaller calls agentskill.InstallSkill using the real home directory.
29+
type defaultSkillInstaller struct{}
30+
31+
func (defaultSkillInstaller) InstallSkill(t *terminal.Terminal) error {
32+
homeDir, err := os.UserHomeDir()
33+
if err != nil {
34+
return fmt.Errorf("could not determine home directory: %w", err)
35+
}
36+
if err := agentskill.InstallSkill(t, homeDir, false); err != nil {
37+
return fmt.Errorf("install skill: %w", err)
38+
}
39+
return nil
40+
}
41+
42+
type upgradeDeps struct {
43+
detector Detector
44+
upgrader Upgrader
45+
confirmer terminal.Confirmer
46+
skillInstaller SkillInstaller
47+
}
48+
49+
func defaultUpgradeDeps() upgradeDeps {
50+
return upgradeDeps{
51+
detector: SystemDetector{},
52+
upgrader: SystemUpgrader{},
53+
confirmer: register.TerminalPrompter{},
54+
skillInstaller: defaultSkillInstaller{},
55+
}
56+
}
57+
58+
var (
59+
upgradeLong = "Upgrade brev to the latest version."
60+
upgradeExample = " brev upgrade"
61+
)
62+
63+
// NewCmdUpgrade creates the brev upgrade command.
64+
func NewCmdUpgrade(t *terminal.Terminal, versionStore VersionStore) *cobra.Command {
65+
cmd := &cobra.Command{
66+
Annotations: map[string]string{"configuration": ""},
67+
Use: "upgrade",
68+
DisableFlagsInUseLine: true,
69+
Short: "Upgrade brev to the latest version",
70+
Long: upgradeLong,
71+
Example: upgradeExample,
72+
RunE: func(cmd *cobra.Command, args []string) error {
73+
return runUpgrade(t, versionStore, defaultUpgradeDeps())
74+
},
75+
}
76+
return cmd
77+
}
78+
79+
func runUpgrade(t *terminal.Terminal, vs VersionStore, deps upgradeDeps) error {
80+
t.Vprint("")
81+
t.Vprintf("Current version: %s\n", version.Version)
82+
83+
release, err := vs.GetLatestReleaseMetadata()
84+
if err != nil {
85+
return fmt.Errorf("failed to check latest version: %w", err)
86+
}
87+
88+
if release.TagName == version.Version {
89+
t.Vprint(t.Green("Already up to date."))
90+
return nil
91+
}
92+
93+
t.Vprintf("New version available: %s\n", release.TagName)
94+
t.Vprint("")
95+
96+
method := deps.detector.Detect()
97+
98+
var (
99+
upgraded bool
100+
upgradeErr error
101+
)
102+
switch method {
103+
case InstallMethodBrew:
104+
upgraded, upgradeErr = upgradeViaBrew(t, deps)
105+
case InstallMethodDirect:
106+
upgraded, upgradeErr = upgradeViaDirect(t, deps)
107+
default:
108+
return fmt.Errorf("unknown install method")
109+
}
110+
111+
if upgradeErr != nil {
112+
return upgradeErr
113+
}
114+
115+
if upgraded {
116+
if err := deps.skillInstaller.InstallSkill(t); err != nil {
117+
t.Vprintf(" Warning: skill update failed: %v\n", err)
118+
t.Vprintf(" You can retry with: brev agent-skill install\n")
119+
}
120+
}
121+
122+
return nil
123+
}
124+
125+
func upgradeViaBrew(t *terminal.Terminal, deps upgradeDeps) (bool, error) {
126+
t.Vprint("Detected install method: Homebrew")
127+
t.Vprint("This will run: brew upgrade brev")
128+
t.Vprint("")
129+
130+
if !deps.confirmer.ConfirmYesNo("Proceed with upgrade?") {
131+
t.Vprint("Upgrade canceled.")
132+
return false, nil
133+
}
134+
135+
t.Vprint("")
136+
if err := deps.upgrader.UpgradeViaBrew(t); err != nil {
137+
return false, fmt.Errorf("brew upgrade: %w", err)
138+
}
139+
140+
t.Vprint("")
141+
t.Vprint(t.Green("Upgrade complete."))
142+
return true, nil
143+
}
144+
145+
func upgradeViaDirect(t *terminal.Terminal, deps upgradeDeps) (bool, error) {
146+
t.Vprint("Detected install method: direct binary install")
147+
t.Vprint("This will download the latest release and install it to /usr/local/bin/brev")
148+
t.Vprint("")
149+
150+
if err := sudo.Gate(t, deps.confirmer, "Upgrade"); err != nil {
151+
return false, fmt.Errorf("sudo issue: %w", err)
152+
}
153+
154+
t.Vprint("")
155+
if err := deps.upgrader.UpgradeViaInstallScript(t); err != nil {
156+
return false, fmt.Errorf("direct upgrade: %w", err)
157+
}
158+
159+
t.Vprint("")
160+
t.Vprint(t.Green("Upgrade complete."))
161+
return true, nil
162+
}

0 commit comments

Comments
 (0)