Skip to content

Commit bcf710a

Browse files
committed
wiring open, refresh, and copy. Using refresh to insert external node info
1 parent 82edaa7 commit bcf710a

8 files changed

Lines changed: 323 additions & 85 deletions

File tree

pkg/cmd/copy/copy.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"strings"
99
"time"
1010

11+
nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
12+
1113
"github.com/brevdev/brev-cli/pkg/cmd/cmderrors"
1214
"github.com/brevdev/brev-cli/pkg/cmd/completions"
1315
"github.com/brevdev/brev-cli/pkg/cmd/refresh"
@@ -77,7 +79,12 @@ func runCopyCommand(t *terminal.Terminal, cstore CopyStore, source, dest string,
7779

7880
workspace, err := prepareWorkspace(t, cstore, workspaceNameOrID)
7981
if err != nil {
80-
return breverrors.WrapAndTrace(err)
82+
// Workspace not found — try external nodes.
83+
node, nodeErr := util.FindExternalNode(cstore, workspaceNameOrID)
84+
if nodeErr != nil || node == nil {
85+
return breverrors.WrapAndTrace(err) // return original workspace error
86+
}
87+
return copyExternalNode(t, cstore, node, localPath, remotePath, isUpload)
8188
}
8289

8390
sshName, err := setupSSHConnection(t, cstore, workspace, host)
@@ -287,6 +294,28 @@ func startWorkspaceIfStopped(t *terminal.Terminal, s *spinner.Spinner, tstore Co
287294
return nil
288295
}
289296

297+
func copyExternalNode(t *terminal.Terminal, cstore CopyStore, node *nodev1.ExternalNode, localPath, remotePath string, isUpload bool) error {
298+
info, err := util.ResolveExternalNodeSSH(cstore, node)
299+
if err != nil {
300+
return breverrors.WrapAndTrace(err)
301+
}
302+
alias := info.SSHAlias()
303+
304+
// Ensure SSH config is up to date so the alias resolves.
305+
refreshRes := refresh.RunRefreshAsync(cstore)
306+
if err := refreshRes.Await(); err != nil {
307+
return breverrors.WrapAndTrace(err)
308+
}
309+
310+
s := t.NewSpinner()
311+
err = waitForSSHToBeAvailable(alias, s)
312+
if err != nil {
313+
return breverrors.WrapAndTrace(err)
314+
}
315+
316+
return runSCP(t, alias, localPath, remotePath, isUpload)
317+
}
318+
290319
func pollUntil(s *spinner.Spinner, wsid string, state string, copyStore CopyStore, waitMsg string) error {
291320
isReady := false
292321
s.Suffix = waitMsg

pkg/cmd/open/open.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"strings"
1111
"time"
1212

13+
nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
14+
1315
"github.com/alessio/shellescape"
1416
"github.com/brevdev/brev-cli/pkg/analytics"
1517
"github.com/brevdev/brev-cli/pkg/cmd/cmderrors"
@@ -283,7 +285,16 @@ func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, s
283285
res := refresh.RunRefreshAsync(tstore)
284286
workspace, err := util.GetUserWorkspaceByNameOrIDErr(tstore, wsIDOrName)
285287
if err != nil {
286-
return breverrors.WrapAndTrace(err)
288+
// Workspace not found — try external nodes.
289+
node, nodeErr := util.FindExternalNode(tstore, wsIDOrName)
290+
if nodeErr != nil || node == nil {
291+
return breverrors.WrapAndTrace(err) // return original workspace error
292+
}
293+
// Await refresh so SSH config entries are written for the node.
294+
if awaitErr := res.Await(); awaitErr != nil {
295+
return breverrors.WrapAndTrace(awaitErr)
296+
}
297+
return openExternalNode(t, tstore, node, directory, editorType)
287298
}
288299
if workspace.Status == "STOPPED" { // we start the env for the user
289300
err = startWorkspaceIfStopped(t, tstore, wsIDOrName, workspace)
@@ -356,6 +367,36 @@ func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, s
356367
return nil
357368
}
358369

370+
func openExternalNode(t *terminal.Terminal, tstore OpenStore, node *nodev1.ExternalNode, directory string, editorType string) error {
371+
info, err := util.ResolveExternalNodeSSH(tstore, node)
372+
if err != nil {
373+
return breverrors.WrapAndTrace(err)
374+
}
375+
alias := info.SSHAlias()
376+
path := info.HomePath()
377+
if directory != "" {
378+
path = directory
379+
}
380+
381+
_ = hello.SetHasRunOpen(true)
382+
383+
s := t.NewSpinner()
384+
s.Start()
385+
s.Suffix = " checking if your node is ready..."
386+
err = waitForSSHToBeAvailable(t, s, alias)
387+
if err != nil {
388+
return breverrors.WrapAndTrace(err)
389+
}
390+
391+
editorName := getEditorName(editorType)
392+
s.Suffix = fmt.Sprintf(" Node is ready. Opening %s", editorName)
393+
time.Sleep(250 * time.Millisecond)
394+
s.Stop()
395+
t.Vprintf("\n")
396+
397+
return openEditorByType(t, editorType, alias, path, tstore)
398+
}
399+
359400
func pushOpenAnalytics(tstore OpenStore, workspace *entity.Workspace) error {
360401
userID := ""
361402
user, err := tstore.GetCurrentUser()

pkg/cmd/refresh/refresh.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
package refresh
33

44
import (
5+
"context"
56
"fmt"
67
"io"
78
"io/fs"
9+
"log"
810
"sync"
911

12+
nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
13+
"connectrpc.com/connect"
14+
15+
"github.com/brevdev/brev-cli/pkg/cmd/register"
1016
"github.com/brevdev/brev-cli/pkg/cmdcontext"
17+
"github.com/brevdev/brev-cli/pkg/config"
1118
"github.com/brevdev/brev-cli/pkg/entity"
1219
breverrors "github.com/brevdev/brev-cli/pkg/errors"
1320
"github.com/brevdev/brev-cli/pkg/ssh"
@@ -22,6 +29,8 @@ type RefreshStore interface {
2229
ssh.SSHConfigurerV2Store
2330
GetCurrentUser() (*entity.User, error)
2431
GetCurrentUserKeys() (*entity.UserKeys, error)
32+
GetActiveOrganizationOrDefault() (*entity.Organization, error)
33+
GetAccessToken() (string, error)
2534
Chmod(string, fs.FileMode) error
2635
MkdirAll(string, fs.FileMode) error
2736
GetBrevCloudflaredBinaryPath() (string, error)
@@ -151,10 +160,70 @@ func GetConfigUpdater(store RefreshStore) (*ssh.ConfigUpdater, error) {
151160
}
152161

153162
cu := ssh.NewConfigUpdater(store, configs, keys.PrivateKey)
163+
cu.ExternalNodes = getExternalNodeSSHEntries(store)
154164

155165
return cu, nil
156166
}
157167

168+
// getExternalNodeSSHEntries fetches external nodes and resolves their SSH details.
169+
// This is best-effort: if anything fails, it returns nil so workspace SSH config is unaffected.
170+
func getExternalNodeSSHEntries(store RefreshStore) []ssh.ExternalNodeSSHEntry {
171+
org, err := store.GetActiveOrganizationOrDefault()
172+
if err != nil {
173+
log.Printf("external nodes: skipping (no org): %v", err)
174+
return nil
175+
}
176+
177+
user, err := store.GetCurrentUser()
178+
if err != nil {
179+
log.Printf("external nodes: skipping (no user): %v", err)
180+
return nil
181+
}
182+
183+
client := register.NewNodeServiceClient(store, config.GlobalConfig.GetBrevPublicAPIURL())
184+
resp, err := client.ListNodes(context.Background(), connect.NewRequest(&nodev1.ListNodesRequest{
185+
OrganizationId: org.ID,
186+
}))
187+
if err != nil {
188+
log.Printf("external nodes: skipping (list failed): %v", err)
189+
return nil
190+
}
191+
192+
var entries []ssh.ExternalNodeSSHEntry
193+
for _, node := range resp.Msg.GetItems() {
194+
var linuxUser string
195+
for _, access := range node.GetSshAccess() {
196+
if access.GetUserId() == user.ID {
197+
linuxUser = access.GetLinuxUser()
198+
break
199+
}
200+
}
201+
if linuxUser == "" {
202+
continue
203+
}
204+
205+
var sshPort *nodev1.Port
206+
for _, p := range node.GetPorts() {
207+
if p.GetProtocol() == nodev1.PortProtocol_PORT_PROTOCOL_SSH {
208+
sshPort = p
209+
break
210+
}
211+
}
212+
if sshPort == nil || sshPort.GetHostname() == "" {
213+
continue
214+
}
215+
216+
entries = append(entries, ssh.ExternalNodeSSHEntry{
217+
Alias: ssh.SanitizeNodeName(node.GetName()),
218+
Hostname: sshPort.GetHostname(),
219+
Port: sshPort.GetPortNumber(),
220+
User: linuxUser,
221+
})
222+
}
223+
224+
return entries
225+
}
226+
158227
func GetCloudflare(refreshStore RefreshStore) store.Cloudflared {
159228
cl := store.NewCloudflare(refreshStore)
160229
return cl

pkg/cmd/revokessh/revokessh.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99

1010
nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
1111
"connectrpc.com/connect"
12-
1312
"github.com/brevdev/brev-cli/pkg/cmd/register"
1413
"github.com/brevdev/brev-cli/pkg/config"
1514
breverrors "github.com/brevdev/brev-cli/pkg/errors"
@@ -125,13 +124,7 @@ func runRevokeSSH(ctx context.Context, t *terminal.Terminal, s RevokeSSHStore, d
125124
}
126125
t.Vprint("")
127126

128-
// Remove the key from authorized_keys first.
129-
if err := deps.removeKeyLine(osUser, selectedKey.Line); err != nil {
130-
return fmt.Errorf("failed to remove key from authorized_keys: %w", err)
131-
}
132-
t.Vprint(" Brev public key removed from authorized_keys.")
133-
134-
// If we know the user ID, also revoke server-side.
127+
// revoke server-side, this will remove the key on the box, this allows this cmd to happen from anywhere
135128
if selectedKey.UserID != "" {
136129
client := deps.nodeClients.NewNodeClient(s, config.GlobalConfig.GetBrevPublicAPIURL())
137130
_, err := client.RevokeNodeSSHAccess(ctx, connect.NewRequest(&nodev1.RevokeNodeSSHAccessRequest{
@@ -143,10 +136,10 @@ func runRevokeSSH(ctx context.Context, t *terminal.Terminal, s RevokeSSHStore, d
143136
t.Vprintf(" %s\n", t.Yellow(fmt.Sprintf("Warning: server-side revocation failed: %v", err)))
144137
t.Vprint(" The key was removed locally but the server may still show access.")
145138
}
146-
} else {
147-
t.Vprint(" Key was old-format (no user ID); skipping server-side revocation.")
148139
}
149140

141+
t.Vprint(" Brev public key removed from authorized_keys.")
142+
150143
t.Vprint("")
151144
t.Vprint(t.Green("SSH key revoked."))
152145
return nil

pkg/cmd/shell/shell.go

Lines changed: 4 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,18 @@
11
package shell
22

33
import (
4-
"context"
54
"fmt"
65
"os"
76
"os/exec"
8-
"strings"
97
"time"
108

119
nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
12-
"connectrpc.com/connect"
1310

1411
"github.com/brevdev/brev-cli/pkg/analytics"
1512
"github.com/brevdev/brev-cli/pkg/cmd/completions"
1613
"github.com/brevdev/brev-cli/pkg/cmd/hello"
1714
"github.com/brevdev/brev-cli/pkg/cmd/refresh"
18-
"github.com/brevdev/brev-cli/pkg/cmd/register"
1915
"github.com/brevdev/brev-cli/pkg/cmd/util"
20-
"github.com/brevdev/brev-cli/pkg/config"
2116
"github.com/brevdev/brev-cli/pkg/entity"
2217
breverrors "github.com/brevdev/brev-cli/pkg/errors"
2318
"github.com/brevdev/brev-cli/pkg/store"
@@ -51,7 +46,6 @@ type ShellStore interface {
5146
refresh.RefreshStore
5247
GetOrganizations(options *store.GetOrganizationsOptions) ([]entity.Organization, error)
5348
GetWorkspaces(organizationID string, options *store.GetWorkspacesOptions) ([]entity.Workspace, error)
54-
GetAccessToken() (string, error)
5549
}
5650

5751
func NewCmdShell(t *terminal.Terminal, store ShellStore, noLoginStartStore ShellStore) *cobra.Command {
@@ -87,7 +81,7 @@ func runShellCommand(t *terminal.Terminal, sstore ShellStore, workspaceNameOrID
8781
workspace, err := util.GetUserWorkspaceByNameOrIDErr(sstore, workspaceNameOrID)
8882
if err != nil {
8983
// Workspace not found — try external nodes.
90-
node, nodeErr := findExternalNode(sstore, workspaceNameOrID)
84+
node, nodeErr := util.FindExternalNode(sstore, workspaceNameOrID)
9185
if nodeErr != nil || node == nil {
9286
return breverrors.WrapAndTrace(err) // return original workspace error
9387
}
@@ -157,64 +151,14 @@ func runShellCommand(t *terminal.Terminal, sstore ShellStore, workspaceNameOrID
157151
return nil
158152
}
159153

160-
func findExternalNode(sstore ShellStore, name string) (*nodev1.ExternalNode, error) {
161-
org, err := sstore.GetActiveOrganizationOrDefault()
162-
if err != nil {
163-
return nil, breverrors.WrapAndTrace(err)
164-
}
165-
client := register.NewNodeServiceClient(sstore, config.GlobalConfig.GetBrevPublicAPIURL())
166-
resp, err := client.ListNodes(context.Background(), connect.NewRequest(&nodev1.ListNodesRequest{
167-
OrganizationId: org.ID,
168-
}))
169-
if err != nil {
170-
return nil, breverrors.WrapAndTrace(err)
171-
}
172-
for _, node := range resp.Msg.GetItems() {
173-
if strings.EqualFold(node.GetName(), name) {
174-
return node, nil
175-
}
176-
}
177-
return nil, nil
178-
}
179-
180154
func shellIntoExternalNode(t *terminal.Terminal, sstore ShellStore, node *nodev1.ExternalNode) error {
181-
user, err := sstore.GetCurrentUser()
155+
info, err := util.ResolveExternalNodeSSH(sstore, node)
182156
if err != nil {
183157
return breverrors.WrapAndTrace(err)
184158
}
185159

186-
var linuxUser string
187-
for _, access := range node.GetSshAccess() {
188-
if access.GetUserId() == user.ID {
189-
linuxUser = access.GetLinuxUser()
190-
break
191-
}
192-
}
193-
if linuxUser == "" {
194-
return breverrors.New(fmt.Sprintf("you don't have SSH access to node %q — try running: brev grant-ssh", node.GetName()))
195-
}
196-
197-
var sshPort *nodev1.Port
198-
for _, p := range node.GetPorts() {
199-
if p.GetProtocol() == nodev1.PortProtocol_PORT_PROTOCOL_SSH {
200-
sshPort = p
201-
break
202-
}
203-
}
204-
if sshPort == nil {
205-
return breverrors.New(fmt.Sprintf("no SSH port configured for node %q", node.GetName()))
206-
}
207-
208-
hostname := sshPort.GetHostname()
209-
if hostname == "" {
210-
return breverrors.New(fmt.Sprintf("SSH port has no hostname for node %q", node.GetName()))
211-
}
212-
port := sshPort.GetPortNumber()
213-
214-
target := fmt.Sprintf("%s@%s", linuxUser, hostname)
215-
t.Vprintf("Connecting to external node %q as %s on port %d...\n", node.GetName(), linuxUser, port)
216-
217-
return runSSHWithPort(target, port)
160+
t.Vprintf("Connecting to external node %q as %s on port %d...\n", node.GetName(), info.LinuxUser, info.Port)
161+
return runSSHWithPort(info.SSHTarget(), info.Port)
218162
}
219163

220164
func runSSHWithPort(target string, port int32) error {

0 commit comments

Comments
 (0)