|
1 | 1 | package shell |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
4 | 5 | "fmt" |
5 | 6 | "os" |
6 | 7 | "os/exec" |
| 8 | + "strings" |
7 | 9 | "time" |
8 | 10 |
|
| 11 | + nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1" |
| 12 | + "connectrpc.com/connect" |
| 13 | + |
9 | 14 | "github.com/brevdev/brev-cli/pkg/analytics" |
10 | 15 | "github.com/brevdev/brev-cli/pkg/cmd/completions" |
11 | 16 | "github.com/brevdev/brev-cli/pkg/cmd/hello" |
12 | 17 | "github.com/brevdev/brev-cli/pkg/cmd/refresh" |
| 18 | + "github.com/brevdev/brev-cli/pkg/cmd/register" |
13 | 19 | "github.com/brevdev/brev-cli/pkg/cmd/util" |
| 20 | + "github.com/brevdev/brev-cli/pkg/config" |
14 | 21 | "github.com/brevdev/brev-cli/pkg/entity" |
15 | 22 | breverrors "github.com/brevdev/brev-cli/pkg/errors" |
16 | 23 | "github.com/brevdev/brev-cli/pkg/store" |
@@ -44,6 +51,7 @@ type ShellStore interface { |
44 | 51 | refresh.RefreshStore |
45 | 52 | GetOrganizations(options *store.GetOrganizationsOptions) ([]entity.Organization, error) |
46 | 53 | GetWorkspaces(organizationID string, options *store.GetWorkspacesOptions) ([]entity.Workspace, error) |
| 54 | + GetAccessToken() (string, error) |
47 | 55 | } |
48 | 56 |
|
49 | 57 | func NewCmdShell(t *terminal.Terminal, store ShellStore, noLoginStartStore ShellStore) *cobra.Command { |
@@ -78,7 +86,12 @@ func runShellCommand(t *terminal.Terminal, sstore ShellStore, workspaceNameOrID |
78 | 86 | s := t.NewSpinner() |
79 | 87 | workspace, err := util.GetUserWorkspaceByNameOrIDErr(sstore, workspaceNameOrID) |
80 | 88 | 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) |
82 | 95 | } |
83 | 96 |
|
84 | 97 | if workspace.Status == "STOPPED" { // we start the env for the user |
@@ -144,6 +157,87 @@ func runShellCommand(t *terminal.Terminal, sstore ShellStore, workspaceNameOrID |
144 | 157 | return nil |
145 | 158 | } |
146 | 159 |
|
| 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 | + |
147 | 241 | func runSSH(sshAlias string) error { |
148 | 242 | sshAgentEval := "eval $(ssh-agent -s)" |
149 | 243 | cmd := fmt.Sprintf("%s && ssh %s", sshAgentEval, sshAlias) |
|
0 commit comments