Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
93 changes: 68 additions & 25 deletions internal/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,14 @@ func getVirtualMachine(client openssh.Client) (vmHost string, region string) {

func syncFiles(username, hostname, projectDir string) error {

projectName := projectDirName(projectDir)
debug.Log("Will sync files to directory: ~/code/%s", projectName)
relProjectPathInVM, err := relativeProjectPathInVM(projectDir)
if err != nil {
return err
}
absPathInVM := absoluteProjectPathInVM(username, relProjectPathInVM)
debug.Log("absPathInVM: %s", absPathInVM)

err := copyConfigFileToVM(hostname, username, projectDir, projectName)
err = copyConfigFileToVM(hostname, username, projectDir, absPathInVM)
if err != nil {
return err
}
Expand All @@ -184,7 +188,9 @@ func syncFiles(username, hostname, projectDir string) error {
// TODO: instead of id, have the server return the machine's name and use that
// here to. It'll make things easier to debug.
machineID, _, _ := strings.Cut(hostname, ".")
mutagenSessionName := mutagen.SanitizeSessionName(fmt.Sprintf("devbox-%s-%s", projectName, machineID))
mutagenSessionName := mutagen.SanitizeSessionName(fmt.Sprintf("devbox-%s-%s", machineID,
hyphenatePath(relProjectPathInVM)))

_, err = mutagen.Sync(&mutagen.SessionSpec{
// If multiple projects can sync to the same machine, we need the name to also include
// the project's id.
Expand All @@ -195,7 +201,7 @@ func syncFiles(username, hostname, projectDir string) error {
// the projects files. If we pick a pre-existing directories with other files, those
// files will be synced back to the local directory (due to two-way-sync) and pollute
// the user's local project
BetaPath: projectPathInVM(projectName),
BetaPath: absPathInVM,
EnvVars: env,
Ignore: mutagen.SessionIgnore{
VCS: true,
Expand All @@ -210,20 +216,21 @@ func syncFiles(username, hostname, projectDir string) error {
time.Sleep(1 * time.Second)

// In a background routine, update the sync status in the cloud VM
go updateSyncStatus(mutagenSessionName, username, hostname, projectName)
go updateSyncStatus(mutagenSessionName, username, hostname, relProjectPathInVM)
return nil
}

// updateSyncStatus updates the starship prompt.
//
// wait for the mutagen session's status to change to "watching", and update the remote VM
// when the initial project sync completes and then exit.
func updateSyncStatus(mutagenSessionName, username, hostname, projectName string) {
func updateSyncStatus(mutagenSessionName, username, hostname, relProjectPathInVM string) {

status := "disconnected"

// Ensure the destination directory exists
destServer := fmt.Sprintf("%s@%s", username, hostname)
destDir := fmt.Sprintf("/home/%s/.config/devbox/starship/%s", username, projectName)
destDir := fmt.Sprintf("/home/%s/.config/devbox/starship/%s", username, hyphenatePath(filepath.Base(relProjectPathInVM)))
remoteCmd := fmt.Sprintf("mkdir -p %s", destDir)
cmd := exec.Command("ssh", destServer, remoteCmd)
err := cmd.Run()
Expand Down Expand Up @@ -274,11 +281,11 @@ func getSyncStatus(mutagenSessionName string) (string, error) {
return sessions[0].Status, nil
}

func copyConfigFileToVM(hostname, username, projectDir, projectName string) error {
func copyConfigFileToVM(hostname, username, projectDir, pathInVM string) error {

// Ensure the devbox-project's directory exists in the VM
destServer := fmt.Sprintf("%s@%s", username, hostname)
cmd := exec.Command("ssh", destServer, "--", "mkdir", "-p", projectPathInVM(projectName))
cmd := exec.Command("ssh", destServer, "--", "mkdir", "-p", pathInVM)
err := cmd.Run()
debug.Log("ssh mkdir command: %s with error: %s", cmd, err)
if err != nil {
Expand All @@ -287,36 +294,68 @@ func copyConfigFileToVM(hostname, username, projectDir, projectName string) erro

// Copy the config file to the devbox-project directory in the VM
configFilePath := filepath.Join(projectDir, "devbox.json")
destPath := fmt.Sprintf("%s:%s", destServer, projectPathInVM(projectName))
destPath := fmt.Sprintf("%s:%s", destServer, pathInVM)
cmd = exec.Command("scp", configFilePath, destPath)
err = cmd.Run()
debug.Log("scp devbox.json command: %s with error: %s", cmd, err)
return errors.WithStack(err)
}

func projectPathInVM(projectName string) string {
return fmt.Sprintf("~/code/%s/", projectName)
}

func shell(username, hostname, projectDir string) error {
projectPath, err := relativeProjectPathInVM(projectDir)
if err != nil {
return err
}

client := &openssh.Client{
Username: username,
Addr: hostname,
ProjectDirName: projectDirName(projectDir),
Username: username,
Addr: hostname,
PathInVM: absoluteProjectPathInVM(username, projectPath),
}
return client.Shell()
}

const defaultProjectDirName = "devbox_project"

// relativeProjectPathInVM refers to the project path relative to the user's
// home-directory within the VM.
//
// Ideally, we'd pass in devbox.Devbox struct and call ProjectDir but it
// makes it hard to wrap this in a test
func projectDirName(projectDir string) string {
name := filepath.Base(projectDir)
if name == "/" || name == "." {
return defaultProjectDirName
func relativeProjectPathInVM(projectDir string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", errors.WithStack(err)
}

// get absProjectDir to expand "." and so on
absProjectDir, err := filepath.Abs(projectDir)
if err != nil {
return "", errors.WithStack(err)
}
return name
projectDir = filepath.Clean(absProjectDir)

if !strings.HasPrefix(projectDir, home) {
projectDir, err = filepath.Abs(projectDir)
if err != nil {
return "", errors.WithStack(err)
}
return filepath.Join(outsideHomedirDirectory, projectDir), nil
}

relativeProjectDir, err := filepath.Rel(home, projectDir)
if err != nil {
return "", errors.WithStack(err)
}
return relativeProjectDir, nil
}

const outsideHomedirDirectory = "outside-homedir-code"

func absoluteProjectPathInVM(sshUser, relativeProjectPath string) string {
vmHomeDir := fmt.Sprintf("/home/%s", sshUser)
if strings.HasPrefix(relativeProjectPath, outsideHomedirDirectory) {
return fmt.Sprintf("%s/%s", vmHomeDir, relativeProjectPath)
}
return fmt.Sprintf("%s/%s/", vmHomeDir, relativeProjectPath)
}

func parseVMEnvVar() (username string, vmHostname string) {
Expand Down Expand Up @@ -385,3 +424,7 @@ func vmHostnameFromSSHControlPath() string {
// empty string means that aren't any active VM connections
return ""
}

func hyphenatePath(path string) string {
return strings.ReplaceAll(path, "/", "-")
}
35 changes: 27 additions & 8 deletions internal/cloud/cloud_test.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
package cloud

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestProjectDirName(t *testing.T) {
assertion := assert.New(t)

homeDir, err := os.UserHomeDir()
assertion.NoError(err)

workingDir, err := os.Getwd()
assertion.NoError(err)

relWorkingDir, err := filepath.Rel(homeDir, workingDir)
assertion.NoError(err)

testCases := []struct {
projectDir string
dirName string
dirPath string
}{
{"/", defaultProjectDirName},
{".", defaultProjectDirName},
{"/foo", "foo"},
{"foo/bar", "bar"},
{"foo/bar/", "bar"},
{"foo/bar///", "bar"},
// inside homedir
{".", relWorkingDir},
{filepath.Join(homeDir, "foo"), "foo"},
{filepath.Join(homeDir, "foo/bar"), "foo/bar"},

// non-home-dir
{"/", filepath.Join(outsideHomedirDirectory, "/")},
{"/foo", filepath.Join(outsideHomedirDirectory, "/foo")},
{"/foo/bar", filepath.Join(outsideHomedirDirectory, "/foo/bar")},
{"/foo/bar/", filepath.Join(outsideHomedirDirectory, "/foo/bar")},
{"/foo/bar///", filepath.Join(outsideHomedirDirectory, "/foo/bar")},
}

for _, testCase := range testCases {
t.Run(testCase.projectDir, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(testCase.dirName, projectDirName(testCase.projectDir))
path, err := relativeProjectPathInVM(testCase.projectDir)
assert.NoError(err)
assert.Equal(testCase.dirPath, path)
})
}
}
8 changes: 4 additions & 4 deletions internal/cloud/openssh/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ import (
)

type Client struct {
Username string
Addr string
ProjectDirName string
Username string
Addr string
PathInVM string
}

func (c *Client) Shell() error {
cmd := c.cmd("-t")
remoteCmd := fmt.Sprintf(`bash -l -c "start_devbox_shell.sh \"%s\""`, c.ProjectDirName)
remoteCmd := fmt.Sprintf(`bash -l -c "start_devbox_shell.sh \"%s\""`, c.PathInVM)
cmd.Args = append(cmd.Args, remoteCmd)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
Expand Down