diff --git a/cmd/ghcs/logs.go b/cmd/ghcs/logs.go index 6f93ee3b9..31af10dda 100644 --- a/cmd/ghcs/logs.go +++ b/cmd/ghcs/logs.go @@ -57,15 +57,11 @@ func Logs(tail bool, codespaceName string) error { return fmt.Errorf("connecting to liveshare: %v", err) } - result, remoteSSHServerPort, sshUser, _, err := codespaces.StartSSHServer(ctx, lsclient) + remoteSSHServerPort, sshUser, err := codespaces.StartSSHServer(ctx, lsclient) if err != nil { return fmt.Errorf("error getting ssh server details: %v", err) } - if !result { - return fmt.Errorf("error starting ssh: %v", err) - } - tunnelPort, connClosed, err := codespaces.MakeSSHTunnel(ctx, lsclient, 0, remoteSSHServerPort) if err != nil { return fmt.Errorf("make ssh tunnel: %v", err) diff --git a/cmd/ghcs/ssh.go b/cmd/ghcs/ssh.go index f3e621824..e3e51e08e 100644 --- a/cmd/ghcs/ssh.go +++ b/cmd/ghcs/ssh.go @@ -1,13 +1,17 @@ package main import ( + "bufio" "context" "fmt" "os" + "strings" + "time" "github.com/github/ghcs/api" "github.com/github/ghcs/cmd/ghcs/output" "github.com/github/ghcs/internal/codespaces" + "github.com/github/go-liveshare" "github.com/spf13/cobra" ) @@ -55,15 +59,29 @@ func SSH(sshProfile, codespaceName string, sshServerPort int) error { return fmt.Errorf("error connecting to liveshare: %v", err) } - result, remoteSSHServerPort, sshUser, _, err := codespaces.StartSSHServer(ctx, lsclient) + remoteSSHServerPort, sshUser, err := codespaces.StartSSHServer(ctx, lsclient) if err != nil { return fmt.Errorf("error getting ssh server details: %v", err) } - if !result { - return fmt.Errorf("error starting ssh: %v", err) + terminal, err := liveshare.NewTerminal(lsclient) + if err != nil { + return fmt.Errorf("error creating liveshare terminal: %v", err) } + log.Print("Preparing SSH...") + if sshProfile == "" { + containerID, err := getContainerID(ctx, log, terminal) + if err != nil { + return fmt.Errorf("error getting container id: %v", err) + } + + if err := setupEnv(ctx, log, terminal, containerID, codespace.RepositoryName, sshUser); err != nil { + return fmt.Errorf("error creating ssh server: %v", err) + } + } + log.Print("\n") + tunnelPort, tunnelClosed, err := codespaces.MakeSSHTunnel(ctx, lsclient, sshServerPort, remoteSSHServerPort) if err != nil { return fmt.Errorf("make ssh tunnel: %v", err) @@ -91,3 +109,58 @@ func SSH(sshProfile, codespaceName string, sshServerPort int) error { return nil } + +func getContainerID(ctx context.Context, logger *output.Logger, terminal *liveshare.Terminal) (string, error) { + logger.Print(".") + + cmd := terminal.NewCommand( + "/", + "/usr/bin/docker ps -aq --filter label=Type=codespaces --filter status=running", + ) + + stream, err := cmd.Run(ctx) + if err != nil { + return "", fmt.Errorf("error running command: %v", err) + } + + logger.Print(".") + scanner := bufio.NewScanner(stream) + scanner.Scan() + + logger.Print(".") + containerID := scanner.Text() + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("error scanning stream: %v", err) + } + + logger.Print(".") + if err := stream.Close(); err != nil { + return "", fmt.Errorf("error closing stream: %v", err) + } + + return containerID, nil +} + +func setupEnv(ctx context.Context, logger *output.Logger, terminal *liveshare.Terminal, containerID, repositoryName, containerUser string) error { + setupBashProfileCmd := fmt.Sprintf(`echo "cd /workspaces/%v; export $(cat /workspaces/.codespaces/shared/.env | xargs); exec /bin/zsh;" > /home/%v/.bash_profile`, repositoryName, containerUser) + + logger.Print(".") + compositeCommand := []string{setupBashProfileCmd} + cmd := terminal.NewCommand( + "/", + fmt.Sprintf("/usr/bin/docker exec -t %s /bin/bash -c '"+strings.Join(compositeCommand, "; ")+"'", containerID), + ) + stream, err := cmd.Run(ctx) + if err != nil { + return fmt.Errorf("error running command: %v", err) + } + + logger.Print(".") + if err := stream.Close(); err != nil { + return fmt.Errorf("error closing stream: %v", err) + } + + time.Sleep(1 * time.Second) + + return nil +} diff --git a/internal/codespaces/codespaces.go b/internal/codespaces/codespaces.go index 3db66b427..8b18f7d7d 100644 --- a/internal/codespaces/codespaces.go +++ b/internal/codespaces/codespaces.go @@ -118,27 +118,30 @@ func ConnectToLiveshare(ctx context.Context, log logger, apiClient *api.API, tok return lsclient, nil } -func StartSSHServer(ctx context.Context, client *liveshare.Client) (result bool, serverPort int, user string, message string, err error) { +// StartSSHServer starts and installs the SSH server in the codespace +// returns the remote port where it is running, the user to use to login +// or an error if something failed. +func StartSSHServer(ctx context.Context, client *liveshare.Client) (serverPort int, user string, err error) { sshServer, err := liveshare.NewSSHServer(client) if err != nil { - return false, 0, "", "", fmt.Errorf("error creating live share: %v", err) + return 0, "", fmt.Errorf("error creating live share: %v", err) } sshServerStartResult, err := sshServer.StartRemoteServer(ctx) if err != nil { - return false, 0, "", "", fmt.Errorf("error creating live share: %v", err) + return 0, "", fmt.Errorf("error starting live share: %v", err) } if !sshServerStartResult.Result { - return false, 0, "", sshServerStartResult.Message, nil + return 0, "", errors.New(sshServerStartResult.Message) } portInt, err := strconv.Atoi(sshServerStartResult.ServerPort) if err != nil { - return false, 0, "", "", fmt.Errorf("error parsing port: %v", err) + return 0, "", fmt.Errorf("error parsing port: %v", err) } - return sshServerStartResult.Result, portInt, sshServerStartResult.User, sshServerStartResult.Message, err + return portInt, sshServerStartResult.User, nil } func GetOrChooseCodespace(ctx context.Context, apiClient *api.API, user *api.User, codespaceName string) (codespace *api.Codespace, token string, err error) { diff --git a/internal/codespaces/ssh.go b/internal/codespaces/ssh.go index cf6118704..cd944e114 100644 --- a/internal/codespaces/ssh.go +++ b/internal/codespaces/ssh.go @@ -14,7 +14,10 @@ import ( "github.com/github/go-liveshare" ) -func MakeSSHTunnel(ctx context.Context, lsclient *liveshare.Client, serverPort int, remoteSSHPort int) (int, <-chan error, error) { +// MakeSSHTunnel This function initializes the liveshare tunnel +// Creates the tunnel from a local port to a remote port. +// Returns the local port that was used, the channel and the error if any. +func MakeSSHTunnel(ctx context.Context, lsclient *liveshare.Client, localSSHPort int, remoteSSHPort int) (int, <-chan error, error) { tunnelClosed := make(chan error) server, err := liveshare.NewServer(lsclient) @@ -24,11 +27,10 @@ func MakeSSHTunnel(ctx context.Context, lsclient *liveshare.Client, serverPort i rand.Seed(time.Now().Unix()) port := rand.Intn(9999-2000) + 2000 // improve this obviously - if serverPort != 0 { - port = serverPort + if localSSHPort != 0 { + port = localSSHPort } - // TODO(josebalius): This port won't always be 2222 if err := server.StartSharing(ctx, "sshd", remoteSSHPort); err != nil { return 0, nil, fmt.Errorf("sharing sshd port: %v", err) }