-
Notifications
You must be signed in to change notification settings - Fork 37
Expand file tree
/
Copy pathexternalnode.go
More file actions
165 lines (146 loc) · 5.37 KB
/
Copy pathexternalnode.go
File metadata and controls
165 lines (146 loc) · 5.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package util
import (
"context"
"fmt"
"strings"
nodev1 "buf.build/gen/go/brevdev/devplane/protocolbuffers/go/devplaneapi/v1"
"connectrpc.com/connect"
"github.com/brevdev/brev-cli/pkg/cmd/register"
"github.com/brevdev/brev-cli/pkg/config"
"github.com/brevdev/brev-cli/pkg/entity"
breverrors "github.com/brevdev/brev-cli/pkg/errors"
"github.com/brevdev/brev-cli/pkg/ssh"
)
// ExternalNodeStore is the minimal interface needed for external node lookup and SSH resolution.
type ExternalNodeStore interface {
GetActiveOrganizationOrDefault() (*entity.Organization, error)
GetAccessToken() (string, error)
GetCurrentUser() (*entity.User, error)
}
type WorkspaceOrNodeResolver interface {
GetWorkspaceByNameOrIDErrStore
ExternalNodeStore
}
// WorkspaceOrNode is returned by ResolveWorkspaceOrNode. Exactly one field is non-nil.
type WorkspaceOrNode struct {
Workspace *entity.Workspace
Node *nodev1.ExternalNode
}
// ResolveWorkspaceOrNode looks up a workspace first; if not found, falls back to external nodes.
// The store must satisfy both GetWorkspaceByNameOrIDErrStore and ExternalNodeStore.
func ResolveWorkspaceOrNode(store WorkspaceOrNodeResolver, nameOrID string,
) (*WorkspaceOrNode, error) {
workspace, wsErr := GetUserWorkspaceByNameOrIDErr(store, nameOrID)
if wsErr == nil {
return &WorkspaceOrNode{Workspace: workspace}, nil
}
node, nodeErr := FindExternalNode(store, nameOrID)
if nodeErr != nil || node == nil {
return nil, wsErr // return original workspace error
}
return &WorkspaceOrNode{Node: node}, nil
}
// ExternalNodeSSHInfo holds resolved SSH connection details for an external node.
type ExternalNodeSSHInfo struct {
Node *nodev1.ExternalNode
LinuxUser string
Hostname string
Port int32
}
// SSHTarget returns the "user@host" string for direct SSH.
func (info *ExternalNodeSSHInfo) SSHTarget() string {
return fmt.Sprintf("%s@%s", info.LinuxUser, info.Hostname)
}
// SSHAlias returns a sanitized node name suitable for use as an SSH config Host alias.
func (info *ExternalNodeSSHInfo) SSHAlias() string {
return ssh.SanitizeNodeName(info.Node.GetName())
}
// HomePath returns the home directory path for the linux user.
func (info *ExternalNodeSSHInfo) HomePath() string {
return fmt.Sprintf("/home/%s", info.LinuxUser)
}
// ResolveNodeSSHEntry is a pure data function that extracts the SSH config entry
// for a given user from a node. Returns nil if the user has no access or the node
// has no SSH port. This is the single source of truth for node→SSHEntry conversion,
// used by both ResolveExternalNodeSSH (for commands) and refresh (for SSH config generation).
func ResolveNodeSSHEntry(userID string, node *nodev1.ExternalNode) *ssh.ExternalNodeSSHEntry {
var linuxUser string
for _, access := range node.GetSshAccess() {
if access.GetUserId() == userID {
linuxUser = access.GetLinuxUser()
break
}
}
if linuxUser == "" {
return nil
}
var sshPort *nodev1.Port
for _, p := range node.GetPorts() {
if p.GetProtocol() == nodev1.PortProtocol_PORT_PROTOCOL_SSH {
sshPort = p
break
}
}
if sshPort == nil || sshPort.GetHostname() == "" {
return nil
}
return &ssh.ExternalNodeSSHEntry{
Alias: ssh.SanitizeNodeName(node.GetName()),
Hostname: sshPort.GetHostname(),
Port: sshPort.GetPortNumber(),
User: linuxUser,
}
}
// OpenPort calls the OpenPort RPC to open a port on an external node via netbird.
// This must be called before attempting to connect to a non-SSH port on a node.
func OpenPort(store ExternalNodeStore, nodeID string, portNumber int32, protocol nodev1.PortProtocol) (*nodev1.Port, error) {
client := register.NewNodeServiceClient(store, config.GlobalConfig.GetBrevPublicAPIURL())
resp, err := client.OpenPort(context.Background(), connect.NewRequest(&nodev1.OpenPortRequest{
ExternalNodeId: nodeID,
Protocol: protocol,
PortNumber: portNumber,
}))
if err != nil {
return nil, breverrors.WrapAndTrace(err)
}
return resp.Msg.GetPort(), nil
}
// FindExternalNode searches for an external node by name in the user's active organization.
// Returns (nil, nil) if no matching node is found.
func FindExternalNode(store ExternalNodeStore, name string) (*nodev1.ExternalNode, error) {
org, err := store.GetActiveOrganizationOrDefault()
if err != nil {
return nil, breverrors.WrapAndTrace(err)
}
client := register.NewNodeServiceClient(store, config.GlobalConfig.GetBrevPublicAPIURL())
resp, err := client.ListNodes(context.Background(), connect.NewRequest(&nodev1.ListNodesRequest{
OrganizationId: org.ID,
}))
if err != nil {
return nil, breverrors.WrapAndTrace(err)
}
for _, node := range resp.Msg.GetItems() {
if strings.EqualFold(node.GetName(), name) {
return node, nil
}
}
return nil, nil
}
// ResolveExternalNodeSSH resolves the SSH connection details for an external node
// by finding the current user's SSH access and the node's SSH port.
func ResolveExternalNodeSSH(store ExternalNodeStore, node *nodev1.ExternalNode) (*ExternalNodeSSHInfo, error) {
user, err := store.GetCurrentUser()
if err != nil {
return nil, breverrors.WrapAndTrace(err)
}
entry := ResolveNodeSSHEntry(user.ID, node)
if entry == nil {
return nil, breverrors.New(fmt.Sprintf("cannot resolve SSH for node %q — no access, no SSH port, or no hostname", node.GetName()))
}
return &ExternalNodeSSHInfo{
Node: node,
LinuxUser: entry.User,
Hostname: entry.Hostname,
Port: entry.Port,
}, nil
}