Skip to content

Commit 82edaa7

Browse files
committed
feat(BRE2-756): brev shell external node support
1 parent 8bdb6f9 commit 82edaa7

1 file changed

Lines changed: 95 additions & 1 deletion

File tree

pkg/cmd/shell/shell.go

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
package shell
22

33
import (
4+
"context"
45
"fmt"
56
"os"
67
"os/exec"
8+
"strings"
79
"time"
810

11+
nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
12+
"connectrpc.com/connect"
13+
914
"github.com/brevdev/brev-cli/pkg/analytics"
1015
"github.com/brevdev/brev-cli/pkg/cmd/completions"
1116
"github.com/brevdev/brev-cli/pkg/cmd/hello"
1217
"github.com/brevdev/brev-cli/pkg/cmd/refresh"
18+
"github.com/brevdev/brev-cli/pkg/cmd/register"
1319
"github.com/brevdev/brev-cli/pkg/cmd/util"
20+
"github.com/brevdev/brev-cli/pkg/config"
1421
"github.com/brevdev/brev-cli/pkg/entity"
1522
breverrors "github.com/brevdev/brev-cli/pkg/errors"
1623
"github.com/brevdev/brev-cli/pkg/store"
@@ -44,6 +51,7 @@ type ShellStore interface {
4451
refresh.RefreshStore
4552
GetOrganizations(options *store.GetOrganizationsOptions) ([]entity.Organization, error)
4653
GetWorkspaces(organizationID string, options *store.GetWorkspacesOptions) ([]entity.Workspace, error)
54+
GetAccessToken() (string, error)
4755
}
4856

4957
func NewCmdShell(t *terminal.Terminal, store ShellStore, noLoginStartStore ShellStore) *cobra.Command {
@@ -78,7 +86,12 @@ func runShellCommand(t *terminal.Terminal, sstore ShellStore, workspaceNameOrID
7886
s := t.NewSpinner()
7987
workspace, err := util.GetUserWorkspaceByNameOrIDErr(sstore, workspaceNameOrID)
8088
if err != nil {
81-
return breverrors.WrapAndTrace(err)
89+
// Workspace not found — try external nodes.
90+
node, nodeErr := findExternalNode(sstore, workspaceNameOrID)
91+
if nodeErr != nil || node == nil {
92+
return breverrors.WrapAndTrace(err) // return original workspace error
93+
}
94+
return shellIntoExternalNode(t, sstore, node)
8295
}
8396

8497
if workspace.Status == "STOPPED" { // we start the env for the user
@@ -144,6 +157,87 @@ func runShellCommand(t *terminal.Terminal, sstore ShellStore, workspaceNameOrID
144157
return nil
145158
}
146159

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+
180+
func shellIntoExternalNode(t *terminal.Terminal, sstore ShellStore, node *nodev1.ExternalNode) error {
181+
user, err := sstore.GetCurrentUser()
182+
if err != nil {
183+
return breverrors.WrapAndTrace(err)
184+
}
185+
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)
218+
}
219+
220+
func runSSHWithPort(target string, port int32) error {
221+
sshAgentEval := "eval $(ssh-agent -s)"
222+
cmd := fmt.Sprintf("%s && ssh -p %d %s", sshAgentEval, port, target)
223+
224+
sshCmd := exec.Command("bash", "-c", cmd) //nolint:gosec //cmd is constructed from API data
225+
sshCmd.Stderr = os.Stderr
226+
sshCmd.Stdout = os.Stdout
227+
sshCmd.Stdin = os.Stdin
228+
229+
err := hello.SetHasRunShell(true)
230+
if err != nil {
231+
return breverrors.WrapAndTrace(err)
232+
}
233+
234+
err = sshCmd.Run()
235+
if err != nil {
236+
return breverrors.WrapAndTrace(err)
237+
}
238+
return nil
239+
}
240+
147241
func runSSH(sshAlias string) error {
148242
sshAgentEval := "eval $(ssh-agent -s)"
149243
cmd := fmt.Sprintf("%s && ssh %s", sshAgentEval, sshAlias)

0 commit comments

Comments
 (0)