Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ssh proxy support #204

Merged
merged 6 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading