Skip to content

Commit

Permalink
Merge pull request #204 from brevdev/ssh-proxy-support
Browse files Browse the repository at this point in the history
ssh proxy support
  • Loading branch information
tmonty12 authored Oct 17, 2024
2 parents e5fa269 + 1dc9057 commit 9501426
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 47 deletions.
18 changes: 18 additions & 0 deletions pkg/cmd/refresh/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package refresh

import (
"fmt"
"io"
"io/fs"
"sync"

"github.com/brevdev/brev-cli/pkg/cmdcontext"
"github.com/brevdev/brev-cli/pkg/entity"
breverrors "github.com/brevdev/brev-cli/pkg/errors"
"github.com/brevdev/brev-cli/pkg/ssh"
"github.com/brevdev/brev-cli/pkg/store"
"github.com/brevdev/brev-cli/pkg/terminal"

"github.com/spf13/cobra"
Expand All @@ -19,6 +22,10 @@ type RefreshStore interface {
ssh.SSHConfigurerV2Store
GetCurrentUser() (*entity.User, error)
GetCurrentUserKeys() (*entity.UserKeys, error)
Chmod(string, fs.FileMode) error
MkdirAll(string, fs.FileMode) error
GetBrevCloudflaredBinaryPath() (string, error)
Create(string) (io.WriteCloser, error)
}

func NewCmdRefresh(t *terminal.Terminal, store RefreshStore) *cobra.Command {
Expand Down Expand Up @@ -51,6 +58,12 @@ func NewCmdRefresh(t *terminal.Terminal, store RefreshStore) *cobra.Command {
}

func RunRefresh(store RefreshStore) error {
cl := GetCloudflare(store)
err := cl.DownloadCloudflaredBinaryIfItDNE()
if err != nil {
return breverrors.WrapAndTrace(err)
}

cu, err := GetConfigUpdater(store)
if err != nil {
return breverrors.WrapAndTrace(err)
Expand Down Expand Up @@ -105,3 +118,8 @@ func GetConfigUpdater(store RefreshStore) (*ssh.ConfigUpdater, error) {

return cu, nil
}

func GetCloudflare(refreshStore RefreshStore) store.Cloudflared {
cl := store.NewCloudflare(refreshStore)
return cl
}
54 changes: 28 additions & 26 deletions pkg/entity/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,32 +284,34 @@ type WorkspaceGroup struct {
} // @Name WorkspaceGroup

type Workspace struct {
ID string `json:"id"`
Name string `json:"name"`
WorkspaceGroupID string `json:"workspaceGroupId"`
OrganizationID string `json:"organizationId"`
WorkspaceClassID string `json:"workspaceClassId"` // WorkspaceClassID is resources, like "2x8"
InstanceType string `json:"instanceType,omitempty"`
CreatedByUserID string `json:"createdByUserId"`
DNS string `json:"dns"`
Status string `json:"status"`
Password string `json:"password"`
GitRepo string `json:"gitRepo"`
Version string `json:"version"`
WorkspaceTemplate WorkspaceTemplate `json:"workspaceTemplate"`
NetworkID string `json:"networkId"`
StartupScriptPath string `json:"startupScriptPath"`
ReposV0 ReposV0 `json:"repos"`
ExecsV0 ExecsV0 `json:"execs"`
ReposV1 *ReposV1 `json:"reposV1"`
ExecsV1 *ExecsV1 `json:"execsV1"`
IDEConfig IDEConfig `json:"ideConfig"`
SSHPort int `json:"sshPort"`
SSHUser string `json:"sshUser"`
HostSSHPort int `json:"hostSshPort"`
HostSSHUser string `json:"hostSshUser"`
VerbBuildStatus VerbBuildStatus `json:"verbBuildStatus"`
VerbYaml string `json:"verbYaml"`
ID string `json:"id"`
Name string `json:"name"`
WorkspaceGroupID string `json:"workspaceGroupId"`
OrganizationID string `json:"organizationId"`
WorkspaceClassID string `json:"workspaceClassId"` // WorkspaceClassID is resources, like "2x8"
InstanceType string `json:"instanceType,omitempty"`
CreatedByUserID string `json:"createdByUserId"`
DNS string `json:"dns"`
Status string `json:"status"`
Password string `json:"password"`
GitRepo string `json:"gitRepo"`
Version string `json:"version"`
WorkspaceTemplate WorkspaceTemplate `json:"workspaceTemplate"`
NetworkID string `json:"networkId"`
StartupScriptPath string `json:"startupScriptPath"`
ReposV0 ReposV0 `json:"repos"`
ExecsV0 ExecsV0 `json:"execs"`
ReposV1 *ReposV1 `json:"reposV1"`
ExecsV1 *ExecsV1 `json:"execsV1"`
IDEConfig IDEConfig `json:"ideConfig"`
SSHPort int `json:"sshPort"`
SSHUser string `json:"sshUser"`
SSHProxyHostname string `json:"sshProxyHostname"`
HostSSHPort int `json:"hostSshPort"`
HostSSHUser string `json:"hostSshUser"`
HostSSHProxyHostname string `json:"hostSshProxyHostname"`
VerbBuildStatus VerbBuildStatus `json:"verbBuildStatus"`
VerbYaml string `json:"verbYaml"`
// PrimaryApplicationId string `json:"primaryApplicationId,omitempty"`
// LastOnlineAt string `json:"lastOnlineAt,omitempty"`
// CreatedAt string `json:"createdAt,omitempty"`
Expand Down
6 changes: 6 additions & 0 deletions pkg/files/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ func GetBrevSSHConfigPath(home string) string {
return brevSSHConfigPath
}

func GetBrevCloudflaredBinaryPath(home string) string {
path := GetBrevHome(home)
brevCloudflaredBinaryPath := filepath.Join(path, "cloudflared")
return brevCloudflaredBinaryPath
}

func GetOnboardingStepPath(home string) string {
path := GetBrevHome(home)
brevOnboardingFilePath := filepath.Join(path, "onboarding_step.json")
Expand Down
89 changes: 72 additions & 17 deletions pkg/ssh/sshconfigurer.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ type SSHConfigurerV2Store interface {
GetWSLHostBrevSSHConfigPath() (string, error)
GetWSLUserSSHConfig() (string, error)
WriteWSLUserSSHConfig(config string) error
GetBrevCloudflaredBinaryPath() (string, error)
}

var _ Config = SSHConfigurerV2{}
Expand Down Expand Up @@ -170,7 +171,12 @@ func (s SSHConfigurerV2) CreateWSLConfig(workspaces []entity.Workspace) (string,

pkpath := files.GetSSHPrivateKeyPath(homedir)

sshConfig, err := makeNewSSHConfig(toWindowsPath(configPath), workspaces, toWindowsPath(pkpath))
cloudflaredBinaryPath, err := s.store.GetBrevCloudflaredBinaryPath()
if err != nil {
return "", breverrors.WrapAndTrace(err)
}

sshConfig, err := makeNewSSHConfig(toWindowsPath(configPath), workspaces, toWindowsPath(pkpath), toWindowsPath(cloudflaredBinaryPath))
if err != nil {
return "", breverrors.WrapAndTrace(err)
}
Expand All @@ -188,18 +194,23 @@ func (s SSHConfigurerV2) CreateNewSSHConfig(workspaces []entity.Workspace) (stri
return "", breverrors.WrapAndTrace(err)
}

sshConfig, err := makeNewSSHConfig(configPath, workspaces, pkPath)
cloudflaredBinaryPath, err := s.store.GetBrevCloudflaredBinaryPath()
if err != nil {
return "", breverrors.WrapAndTrace(err)
}

sshConfig, err := makeNewSSHConfig(configPath, workspaces, pkPath, cloudflaredBinaryPath)
if err != nil {
return "", breverrors.WrapAndTrace(err)
}
return sshConfig, nil
}

func makeNewSSHConfig(configPath string, workspaces []entity.Workspace, pkpath string) (string, error) {
func makeNewSSHConfig(configPath string, workspaces []entity.Workspace, pkpath string, cloudflaredBinaryPath string) (string, error) {
sshConfig := fmt.Sprintf("# included in %s\n", configPath)
for _, w := range workspaces {

entry, err := makeSSHConfigEntryV2(w, pkpath)
entry, err := makeSSHConfigEntryV2(w, pkpath, cloudflaredBinaryPath)
if err != nil {
return "", breverrors.WrapAndTrace(err)
}
Expand Down Expand Up @@ -270,7 +281,7 @@ func tmplAndValToString(tmpl *template.Template, val interface{}) (string, error
return buf.String(), nil
}

func makeSSHConfigEntryV2(workspace entity.Workspace, privateKeyPath string) (string, error) {
func makeSSHConfigEntryV2(workspace entity.Workspace, privateKeyPath string, cloudflaredBinaryPath string) (string, error) { //nolint:funlen // ok
alias := string(workspace.GetLocalIdentifier())
privateKeyPath = "\"" + privateKeyPath + "\""
if workspace.IsLegacy() {
Expand All @@ -291,10 +302,13 @@ func makeSSHConfigEntryV2(workspace entity.Workspace, privateKeyPath string) (st
return "", breverrors.WrapAndTrace(err)
}
return val, nil
} else {
hostname := workspace.GetHostname()
}

var sshVal string
user := workspace.GetSSHUser()
hostname := workspace.GetHostname()
if workspace.SSHProxyHostname == "" {
port := workspace.GetSSHPort()
user := workspace.GetSSHUser()
entry := SSHConfigEntryV2{
Alias: alias,
IdentityFile: privateKeyPath,
Expand All @@ -307,34 +321,71 @@ func makeSSHConfigEntryV2(workspace entity.Workspace, privateKeyPath string) (st
if err != nil {
return "", breverrors.WrapAndTrace(err)
}
sshVal, err := tmplAndValToString(tmpl, entry)
sshVal, err = tmplAndValToString(tmpl, entry)
if err != nil {
return "", breverrors.WrapAndTrace(err)
}
} else {
proxyCommand := makeCloudflareSSHProxyCommand(cloudflaredBinaryPath, workspace.SSHProxyHostname)
entry := SSHConfigEntryV2{
Alias: alias,
IdentityFile: privateKeyPath,
User: user,
ProxyCommand: proxyCommand,
Dir: workspace.GetProjectFolderPath(),
}
tmpl, err := template.New(alias).Parse(SSHConfigEntryTemplateV2)
if err != nil {
return "", breverrors.WrapAndTrace(err)
}
sshVal, err = tmplAndValToString(tmpl, entry)
if err != nil {
return "", breverrors.WrapAndTrace(err)
}
}

port = workspace.GetHostSSHPort()
alias = fmt.Sprintf("%s-host", alias)
var hostSSHVal string
if workspace.HostSSHProxyHostname == "" {
port := workspace.GetHostSSHPort()
user = workspace.GetHostSSHUser()
alias = fmt.Sprintf("%s-host", alias)
entry = SSHConfigEntryV2{
entry := SSHConfigEntryV2{
Alias: alias,
IdentityFile: privateKeyPath,
User: user,
Dir: workspace.GetProjectFolderPath(),
HostName: hostname,
Port: port,
}
tmpl, err = template.New(alias).Parse(SSHConfigEntryTemplateV3)
tmpl, err := template.New(alias).Parse(SSHConfigEntryTemplateV3)
if err != nil {
return "", breverrors.WrapAndTrace(err)
}
hostSSHVal, err := tmplAndValToString(tmpl, entry)
hostSSHVal, err = tmplAndValToString(tmpl, entry)
if err != nil {
return "", breverrors.WrapAndTrace(err)
}
} else {
proxyCommand := makeCloudflareSSHProxyCommand(cloudflaredBinaryPath, workspace.HostSSHProxyHostname)
entry := SSHConfigEntryV2{
Alias: alias,
IdentityFile: privateKeyPath,
User: user,
ProxyCommand: proxyCommand,
Dir: workspace.GetProjectFolderPath(),
}
tmpl, err := template.New(alias).Parse(SSHConfigEntryTemplateV2)
if err != nil {
return "", breverrors.WrapAndTrace(err)
}
hostSSHVal, err = tmplAndValToString(tmpl, entry)
if err != nil {
return "", breverrors.WrapAndTrace(err)
}

val := fmt.Sprintf("%s%s", sshVal, hostSSHVal)
return val, nil
}

val := fmt.Sprintf("%s%s", sshVal, hostSSHVal)
return val, nil
}

// func makeSSHConfigEntryV2(workspace entity.Workspace, privateKeyPath string) (string, error) {
Expand Down Expand Up @@ -392,6 +443,10 @@ func makeSSHConfigEntryV2(workspace entity.Workspace, privateKeyPath string) (st
// }
// }

func makeCloudflareSSHProxyCommand(cloudflaredBinaryPath string, hostname string) string {
return fmt.Sprintf("%s access ssh --hostname %s", cloudflaredBinaryPath, hostname)
}

func makeProxyCommand(workspaceID string) string {
huproxyExec := "brev proxy"
return fmt.Sprintf("%s %s", huproxyExec, workspaceID)
Expand Down
62 changes: 58 additions & 4 deletions pkg/ssh/sshconfigurer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ func (d DummySSHConfigurerV2Store) WriteWSLUserSSHConfig(_ string) error {
return nil
}

func (d DummySSHConfigurerV2Store) GetBrevCloudflaredBinaryPath() (string, error) {
return "", nil
}

func TestCreateNewSSHConfig(t *testing.T) {
c := NewSSHConfigurerV2(DummySSHConfigurerV2Store{})
cStr, err := c.CreateNewSSHConfig(somePlainWorkspaces)
Expand Down Expand Up @@ -262,9 +266,10 @@ blaksdf;asdf;

func Test_makeSSHConfigEntryV2(t *testing.T) { //nolint:funlen // test
type args struct {
workspace entity.Workspace
privateKeyPath string
runRemoteCMD bool
workspace entity.Workspace
privateKeyPath string
cloudflaredBinaryPath string
runRemoteCMD bool
}
tests := []struct {
name string
Expand Down Expand Up @@ -486,12 +491,61 @@ Host testName2-host
ForwardAgent yes
RequestTTY yes
`,
},
{
name: "test default ssh proxy",
args: args{
workspace: entity.Workspace{
ID: "test-id-2",
Name: "testName2",
WorkspaceGroupID: "test-id-2",
OrganizationID: "oi",
WorkspaceClassID: "wci",
CreatedByUserID: "cui",
DNS: "test2-dns-org.brev.sh",
Status: entity.Running,
Password: "sdfal",
GitRepo: "gitrepo",
SSHProxyHostname: "test-verb-proxy.com",
HostSSHProxyHostname: "test-host-proxy.com",
},
privateKeyPath: "/my/priv/key.pem",
cloudflaredBinaryPath: "/Users/tmontfort/.brev/cloudflared",
runRemoteCMD: true,
},
want: `Host testName2
IdentityFile "/my/priv/key.pem"
User ubuntu
ProxyCommand /Users/tmontfort/.brev/cloudflared access ssh --hostname test-verb-proxy.com
ServerAliveInterval 30
UserKnownHostsFile /dev/null
IdentitiesOnly yes
StrictHostKeyChecking no
PasswordAuthentication no
AddKeysToAgent yes
ForwardAgent yes
RequestTTY yes
Host testName2-host
IdentityFile "/my/priv/key.pem"
User ubuntu
ProxyCommand /Users/tmontfort/.brev/cloudflared access ssh --hostname test-host-proxy.com
ServerAliveInterval 30
UserKnownHostsFile /dev/null
IdentitiesOnly yes
StrictHostKeyChecking no
PasswordAuthentication no
AddKeysToAgent yes
ForwardAgent yes
RequestTTY yes
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := makeSSHConfigEntryV2(tt.args.workspace, tt.args.privateKeyPath)
got, err := makeSSHConfigEntryV2(tt.args.workspace, tt.args.privateKeyPath, tt.args.cloudflaredBinaryPath)
if (err != nil) != tt.wantErr {
t.Errorf("makeSSHConfigEntryV2() error = %v, wantErr %v", err, tt.wantErr)
return
Expand Down
Loading

0 comments on commit 9501426

Please sign in to comment.