Skip to content

Commit 8f87af0

Browse files
committed
adding some ls capability, enable ssh, fixed rpc url
1 parent eee4c0b commit 8f87af0

11 files changed

Lines changed: 297 additions & 42 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ifdef env
2020
@echo 'export BREV_AUTH_URL="https://api.stg.ngc.nvidia.com"' >> brev
2121
@echo 'export BREV_AUTH_ISSUER_URL="https://stg.login.nvidia.com"' >> brev
2222
@echo 'export BREV_API_URL="https://bd.$(env).brev.nvidia.com"' >> brev
23+
@echo 'export BREV_PUBLIC_API_URL="https://api.$(env).brev.nvidia.com"' >> brev
2324
@echo 'export BREV_GRPC_URL="api.$(env).brev.nvidia.com:443"' >> brev
2425
@echo 'exec "$$(cd "$$(dirname "$$0")" && pwd)/brev-local" "$$@"' >> brev
2526
@chmod +x brev

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/brevdev/brev-cli
33
go 1.24.0
44

55
require (
6-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260226234124-59cddad562f0.2
7-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260226234124-59cddad562f0.1
6+
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260227212944-1f05724e97ab.2
7+
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260227212944-1f05724e97ab.1
88
connectrpc.com/connect v1.19.1
99
github.com/alessio/shellescape v1.4.1
1010
github.com/brevdev/parse v0.0.11

go.sum

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
1-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260226031750-e6fd8dbaf991.2 h1:8ZrXfJx6gzHTeBU2Lfn2jdpi8q8QJMXZPo8GlVEm6+A=
2-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260226031750-e6fd8dbaf991.2/go.mod h1:EGcIExX0SEtObIZr1l3pouENtdl2gsZtHjOYOfuB7ss=
3-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260226200709-e1ac2ea142d1.2 h1:7ld1AqzV9YsRWP5I4FvMUxDiq5fMCEEsNMECCcUJE/s=
4-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260226200709-e1ac2ea142d1.2/go.mod h1:ZqSAMH+RVqnfQsnUQ5OpJI7dUWx0UzPUWcceudIHWmI=
5-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260226234124-59cddad562f0.2 h1:B+GNU2e5fb54KUw11+kOUXNuzxWM40J2GiSmONL8VEA=
6-
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260226234124-59cddad562f0.2/go.mod h1:k1PtdOGpCm4AS2SszBDYyA2pbj9Y39TYRwdhlA17slw=
7-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260226031750-e6fd8dbaf991.1 h1:xkJkJcCnAq5WiEUevk7Kz3b+aFuK7aj64DyVUQM9ZQ0=
8-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260226031750-e6fd8dbaf991.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
9-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260226200709-e1ac2ea142d1.1 h1:pWlngsd33oF5xFhfTbxYProXMihFXmthzTAcgd3zXKg=
10-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260226200709-e1ac2ea142d1.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
11-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260226234124-59cddad562f0.1 h1:7+YIWe9KK1AJjpzFThk402ginqJ51bgtjTulw97a4fo=
12-
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260226234124-59cddad562f0.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
1+
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260227212944-1f05724e97ab.2 h1:R8G6M5utUEZaurs/9o/fhGMCp/ZHsaYjjArMFBsrrr8=
2+
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260227212944-1f05724e97ab.2/go.mod h1:Jooemm4ArTV81Co3zkto/PgOQtelQf5j3fgAGsv73T4=
3+
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260227212944-1f05724e97ab.1 h1:lOMJE1EzhYelGdR5FQEHUfUl60/3E69Y8dM4O1C8nMc=
4+
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260227212944-1f05724e97ab.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
135
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1 h1:6amhprQmCKJ4wgJ6ngkh32d9V+dQcOLUZ/SfHdOnYgo=
146
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1/go.mod h1:O+pnSHMru/naTMrm4tmpBoH3wz6PHa+R75HR7Mv8X2g=
157
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=

pkg/cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/brevdev/brev-cli/pkg/cmd/copy"
1313
"github.com/brevdev/brev-cli/pkg/cmd/delete"
1414
"github.com/brevdev/brev-cli/pkg/cmd/deregister"
15+
"github.com/brevdev/brev-cli/pkg/cmd/enablessh"
1516
"github.com/brevdev/brev-cli/pkg/cmd/envvars"
1617
"github.com/brevdev/brev-cli/pkg/cmd/exec"
1718
"github.com/brevdev/brev-cli/pkg/cmd/fu"
@@ -294,6 +295,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
294295
cmd.AddCommand(refresh.NewCmdRefresh(t, loginCmdStore))
295296
cmd.AddCommand(register.NewCmdRegister(t, loginCmdStore))
296297
cmd.AddCommand(deregister.NewCmdDeregister(t, loginCmdStore))
298+
cmd.AddCommand(enablessh.NewCmdEnableSSH(t, loginCmdStore))
297299
cmd.AddCommand(runtasks.NewCmdRunTasks(t, noLoginCmdStore))
298300
cmd.AddCommand(proxy.NewCmdProxy(t, noLoginCmdStore))
299301
cmd.AddCommand(healthcheck.NewCmdHealthcheck(t, noLoginCmdStore))

pkg/cmd/deregister/deregister.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,9 @@ func runDeregister(ctx context.Context, t *terminal.Terminal, s DeregisterStore,
121121

122122
t.Vprint("")
123123
t.Vprint(t.Yellow("Removing node from Brev..."))
124-
client := deps.newNodeClient(s, config.GlobalConfig.GetBrevAPIURl())
124+
client := deps.newNodeClient(s, config.GlobalConfig.GetBrevPublicAPIURL())
125125
if _, err := client.RemoveNode(ctx, connect.NewRequest(&nodev1.RemoveNodeRequest{
126126
ExternalNodeId: reg.ExternalNodeID,
127-
OrganizationId: reg.OrgID,
128127
})); err != nil {
129128
return fmt.Errorf("failed to deregister node: %w", err)
130129
}

pkg/cmd/deregister/deregister_test.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,10 @@ func Test_runDeregister_HappyPath(t *testing.T) {
113113
token: "tok",
114114
}
115115

116-
var gotNodeID, gotOrgID string
116+
var gotNodeID string
117117
svc := &fakeNodeService{
118118
removeNodeFn: func(req *nodev1.RemoveNodeRequest) (*nodev1.RemoveNodeResponse, error) {
119119
gotNodeID = req.GetExternalNodeId()
120-
gotOrgID = req.GetOrganizationId()
121120
return &nodev1.RemoveNodeResponse{}, nil
122121
},
123122
}
@@ -134,9 +133,6 @@ func Test_runDeregister_HappyPath(t *testing.T) {
134133
if gotNodeID != "unode_abc" {
135134
t.Errorf("expected node ID unode_abc, got %s", gotNodeID)
136135
}
137-
if gotOrgID != "org_123" {
138-
t.Errorf("expected org ID org_123, got %s", gotOrgID)
139-
}
140136

141137
// Registration should be deleted
142138
exists, err := regStore.Exists()

pkg/cmd/enablessh/enablessh.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Package enablessh provides the brev enableSSH command for enabling SSH access
2+
// to a registered external node.
3+
package enablessh
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"os"
9+
"os/exec"
10+
"os/user"
11+
"path/filepath"
12+
"runtime"
13+
"strings"
14+
15+
nodev1connect "buf.build/gen/go/brevdev/devplane/connectrpc/go/devplaneapi/v1/devplaneapiv1connect"
16+
nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
17+
"connectrpc.com/connect"
18+
19+
"github.com/brevdev/brev-cli/pkg/cmd/register"
20+
"github.com/brevdev/brev-cli/pkg/config"
21+
"github.com/brevdev/brev-cli/pkg/entity"
22+
breverrors "github.com/brevdev/brev-cli/pkg/errors"
23+
"github.com/brevdev/brev-cli/pkg/terminal"
24+
25+
"github.com/spf13/cobra"
26+
)
27+
28+
// EnableSSHStore defines the store methods needed by the enableSSH command.
29+
type EnableSSHStore interface {
30+
GetCurrentUser() (*entity.User, error)
31+
GetBrevHomePath() (string, error)
32+
GetAccessToken() (string, error)
33+
}
34+
35+
// enableSSHDeps bundles the side-effecting dependencies of runEnableSSH so they
36+
// can be replaced in tests.
37+
type enableSSHDeps struct {
38+
goos string
39+
newNodeClient func(provider register.TokenProvider, baseURL string) nodev1connect.ExternalNodeServiceClient
40+
registrationStore register.RegistrationStore
41+
}
42+
43+
func prodEnableSSHDeps(brevHome string) enableSSHDeps {
44+
return enableSSHDeps{
45+
goos: runtime.GOOS,
46+
newNodeClient: register.NewNodeServiceClient,
47+
registrationStore: register.NewFileRegistrationStore(brevHome),
48+
}
49+
}
50+
51+
func NewCmdEnableSSH(t *terminal.Terminal, store EnableSSHStore) *cobra.Command {
52+
cmd := &cobra.Command{
53+
Annotations: map[string]string{"configuration": ""},
54+
Use: "enableSSH",
55+
DisableFlagsInUseLine: true,
56+
Short: "Enable SSH access to this registered device",
57+
Long: "Enable SSH access to this registered device for the current Brev user.",
58+
Example: " brev enableSSH",
59+
RunE: func(cmd *cobra.Command, args []string) error {
60+
brevHome, err := store.GetBrevHomePath()
61+
if err != nil {
62+
return breverrors.WrapAndTrace(err)
63+
}
64+
return runEnableSSH(cmd.Context(), t, store, prodEnableSSHDeps(brevHome))
65+
},
66+
}
67+
68+
return cmd
69+
}
70+
71+
func runEnableSSH(ctx context.Context, t *terminal.Terminal, s EnableSSHStore, deps enableSSHDeps) error {
72+
if deps.goos != "linux" {
73+
return fmt.Errorf("brev enableSSH is only supported on Linux")
74+
}
75+
76+
registered, err := deps.registrationStore.Exists()
77+
if err != nil {
78+
return breverrors.WrapAndTrace(err)
79+
}
80+
if !registered {
81+
return fmt.Errorf("no registration found; this machine does not appear to be registered\nRun 'brev register' to register your device first")
82+
}
83+
84+
reg, err := deps.registrationStore.Load()
85+
if err != nil {
86+
return fmt.Errorf("failed to read registration file: %w", err)
87+
}
88+
89+
brevUser, err := s.GetCurrentUser()
90+
if err != nil {
91+
return breverrors.WrapAndTrace(err)
92+
}
93+
94+
return EnableSSH(ctx, t, deps.newNodeClient, s, reg, brevUser)
95+
}
96+
97+
// EnableSSH grants SSH access to the given node for the specified Brev user.
98+
// It is exported so that the register command can reuse it after registration.
99+
func EnableSSH(
100+
ctx context.Context,
101+
t *terminal.Terminal,
102+
newClient func(register.TokenProvider, string) nodev1connect.ExternalNodeServiceClient,
103+
tokenProvider register.TokenProvider,
104+
reg *register.DeviceRegistration,
105+
brevUser *entity.User,
106+
) error {
107+
u, err := user.Current()
108+
if err != nil {
109+
return fmt.Errorf("failed to determine current Linux user: %w", err)
110+
}
111+
linuxUser := u.Username
112+
113+
checkSSHDaemon(t)
114+
115+
t.Vprint("")
116+
t.Vprint(t.Green("Enabling SSH access on this device"))
117+
t.Vprint("")
118+
t.Vprintf(" Node: %s (%s)\n", reg.DisplayName, reg.ExternalNodeID)
119+
t.Vprintf(" Brev user: %s\n", brevUser.ID)
120+
t.Vprintf(" Linux user: %s\n", linuxUser)
121+
t.Vprint("")
122+
123+
client := newClient(tokenProvider, config.GlobalConfig.GetBrevPublicAPIURL())
124+
if _, err := client.GrantNodeSSHAccess(ctx, connect.NewRequest(&nodev1.GrantNodeSSHAccessRequest{
125+
ExternalNodeId: reg.ExternalNodeID,
126+
UserId: brevUser.ID,
127+
LinuxUser: linuxUser,
128+
OrganizationId: reg.OrgID,
129+
})); err != nil {
130+
return fmt.Errorf("failed to enable SSH access: %w", err)
131+
}
132+
133+
if brevUser.PublicKey != "" {
134+
if err := installAuthorizedKey(u, brevUser.PublicKey); err != nil {
135+
t.Vprintf(" %s\n", t.Yellow(fmt.Sprintf("Warning: failed to install SSH public key: %v", err)))
136+
} else {
137+
t.Vprint(" Brev public key added to authorized_keys.")
138+
}
139+
}
140+
141+
t.Vprint(t.Green(fmt.Sprintf("SSH access enabled. You can now SSH to this device via: brev shell %s", reg.DisplayName)))
142+
return nil
143+
}
144+
145+
// installAuthorizedKey appends the given public key to the user's
146+
// ~/.ssh/authorized_keys if it isn't already present.
147+
func installAuthorizedKey(u *user.User, pubKey string) error {
148+
pubKey = strings.TrimSpace(pubKey)
149+
if pubKey == "" {
150+
return nil
151+
}
152+
153+
sshDir := filepath.Join(u.HomeDir, ".ssh")
154+
if err := os.MkdirAll(sshDir, 0o700); err != nil {
155+
return fmt.Errorf("creating .ssh directory: %w", err)
156+
}
157+
158+
authKeysPath := filepath.Join(sshDir, "authorized_keys")
159+
160+
existing, err := os.ReadFile(authKeysPath) // #nosec G304
161+
if err != nil && !os.IsNotExist(err) {
162+
return fmt.Errorf("reading authorized_keys: %w", err)
163+
}
164+
165+
if strings.Contains(string(existing), pubKey) {
166+
return nil // already present
167+
}
168+
169+
// Ensure existing content ends with a newline before appending.
170+
content := string(existing)
171+
if len(content) > 0 && !strings.HasSuffix(content, "\n") {
172+
content += "\n"
173+
}
174+
content += pubKey + "\n"
175+
176+
if err := os.WriteFile(authKeysPath, []byte(content), 0o600); err != nil {
177+
return fmt.Errorf("writing authorized_keys: %w", err)
178+
}
179+
180+
return nil
181+
}
182+
183+
// checkSSHDaemon prints a warning if neither "ssh" nor "sshd" systemd services
184+
// appear to be active. It never returns an error — it is best-effort.
185+
func checkSSHDaemon(t *terminal.Terminal) {
186+
for _, svc := range []string{"ssh", "sshd"} {
187+
out, err := exec.Command("systemctl", "is-active", svc).Output() //nolint:gosec // fixed service names
188+
if err == nil && len(out) > 0 && string(out[:len(out)-1]) == "active" {
189+
return
190+
}
191+
}
192+
t.Vprintf(" %s\n", t.Yellow("Warning: SSH daemon does not appear to be running. SSH access may not work until sshd is started."))
193+
}

pkg/cmd/ls/ls.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ type NodeInfo struct {
670670
}
671671

672672
func (ls Ls) listNodes(org *entity.Organization) ([]*nodev1.ExternalNode, error) {
673-
client := register.NewNodeServiceClient(ls.lsStore, config.GlobalConfig.GetBrevAPIURl())
673+
client := register.NewNodeServiceClient(ls.lsStore, config.GlobalConfig.GetBrevPublicAPIURL())
674674
resp, err := client.ListNodes(context.Background(), connect.NewRequest(&nodev1.ListNodesRequest{
675675
OrganizationId: org.ID,
676676
}))
@@ -774,11 +774,8 @@ func displayNodesTablePlain(nodes []*nodev1.ExternalNode) {
774774
}
775775

776776
func nodeConnectionStatus(n *nodev1.ExternalNode) string {
777-
if ci := n.GetConnectivityInfo(); ci != nil && ci.HasConnected() {
778-
if ci.GetConnected() {
779-
return "CONNECTED"
780-
}
781-
return "DISCONNECTED"
777+
if ci := n.GetConnectivityInfo(); ci != nil && ci.GetRegistrationCommand() != "" {
778+
return "REGISTERED"
782779
}
783780
return "UNKNOWN"
784781
}

0 commit comments

Comments
 (0)