diff --git a/api/api.go b/api/api.go index b9d4213a0..faa71d253 100644 --- a/api/api.go +++ b/api/api.go @@ -274,11 +274,16 @@ func (a *API) StartCodespace(ctx context.Context, token string, codespace *Codes } if resp.StatusCode != http.StatusOK { - // Error response is numeric code and/or string message, not JSON. + // Error response is typically a numeric code (not an error message, nor JSON). if len(b) > 100 { b = append(b[:97], "..."...) } - return fmt.Errorf("failed to start Codespace: %s", b) + if resp.StatusCode == http.StatusServiceUnavailable && strings.TrimSpace(string(b)) == "7" { + // HTTP 503 with error code 7 (EnvironmentNotShutdown) is benign. + // Ignore it. + } else { + return fmt.Errorf("failed to start Codespace: %s", b) + } } return nil diff --git a/cmd/ghcs/logs.go b/cmd/ghcs/logs.go index 4c840e77c..afe7e1843 100644 --- a/cmd/ghcs/logs.go +++ b/cmd/ghcs/logs.go @@ -57,7 +57,12 @@ func logs(tail bool, codespaceName string) error { return fmt.Errorf("connecting to Live Share: %v", err) } - tunnelPort, connClosed, err := codespaces.MakeSSHTunnel(ctx, lsclient, 0) + remoteSSHServerPort, sshUser, err := codespaces.StartSSHServer(ctx, lsclient, log) + if err != nil { + return fmt.Errorf("error getting ssh server details: %v", err) + } + + tunnelPort, connClosed, err := codespaces.MakeSSHTunnel(ctx, lsclient, 0, remoteSSHServerPort) if err != nil { return fmt.Errorf("make ssh tunnel: %v", err) } @@ -67,7 +72,7 @@ func logs(tail bool, codespaceName string) error { cmdType = "tail -f" } - dst := fmt.Sprintf("%s@localhost", getSSHUser(codespace)) + dst := fmt.Sprintf("%s@localhost", sshUser) stdout, err := codespaces.RunCommand( ctx, tunnelPort, dst, fmt.Sprintf("%v /workspaces/.codespaces/.persistedshare/creation.log", cmdType), ) diff --git a/cmd/ghcs/ssh.go b/cmd/ghcs/ssh.go index 7fecf0d1f..f276c796b 100644 --- a/cmd/ghcs/ssh.go +++ b/cmd/ghcs/ssh.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "strings" - "time" "github.com/github/ghcs/api" "github.com/github/ghcs/cmd/ghcs/output" @@ -59,6 +58,11 @@ func ssh(sshProfile, codespaceName string, sshServerPort int) error { return fmt.Errorf("error connecting to Live Share: %v", err) } + remoteSSHServerPort, sshUser, err := codespaces.StartSSHServer(ctx, lsclient, log) + if err != nil { + return fmt.Errorf("error getting ssh server details: %v", err) + } + terminal, err := liveshare.NewTerminal(lsclient) if err != nil { return fmt.Errorf("error creating Live Share terminal: %v", err) @@ -71,20 +75,20 @@ func ssh(sshProfile, codespaceName string, sshServerPort int) error { return fmt.Errorf("error getting container id: %v", err) } - if err := setupSSH(ctx, log, terminal, containerID, codespace.RepositoryName); err != nil { + 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) + tunnelPort, tunnelClosed, err := codespaces.MakeSSHTunnel(ctx, lsclient, sshServerPort, remoteSSHServerPort) if err != nil { return fmt.Errorf("make ssh tunnel: %v", err) } connectDestination := sshProfile if connectDestination == "" { - connectDestination = fmt.Sprintf("%s@localhost", getSSHUser(codespace)) + connectDestination = fmt.Sprintf("%s@localhost", sshUser) } usingCustomPort := tunnelPort == sshServerPort @@ -136,8 +140,8 @@ func getContainerID(ctx context.Context, logger *output.Logger, terminal *livesh return containerID, nil } -func setupSSH(ctx context.Context, logger *output.Logger, terminal *liveshare.Terminal, containerID, repositoryName string) error { - setupBashProfileCmd := fmt.Sprintf(`echo "cd /workspaces/%v; export $(cat /workspaces/.codespaces/shared/.env | xargs); exec /bin/zsh;" > /home/codespace/.bash_profile`, repositoryName) +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} @@ -155,14 +159,5 @@ func setupSSH(ctx context.Context, logger *output.Logger, terminal *liveshare.Te return fmt.Errorf("error closing stream: %v", err) } - time.Sleep(1 * time.Second) - return nil } - -func getSSHUser(codespace *api.Codespace) string { - if codespace.RepositoryNWO == "github/github" { - return "root" - } - return "codespace" -} diff --git a/internal/codespaces/ssh.go b/internal/codespaces/ssh.go index ba55efae5..7b1baf445 100644 --- a/internal/codespaces/ssh.go +++ b/internal/codespaces/ssh.go @@ -2,6 +2,7 @@ package codespaces import ( "context" + "errors" "fmt" "io" "math/rand" @@ -14,7 +15,7 @@ import ( "github.com/github/go-liveshare" ) -func MakeSSHTunnel(ctx context.Context, lsclient *liveshare.Client, serverPort int) (int, <-chan error, error) { +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,12 +25,11 @@ 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", 2222); err != nil { + if err := server.StartSharing(ctx, "sshd", remoteSSHPort); err != nil { return 0, nil, fmt.Errorf("sharing sshd port: %v", err) } @@ -45,6 +45,33 @@ func MakeSSHTunnel(ctx context.Context, lsclient *liveshare.Client, serverPort i return port, tunnelClosed, nil } +// StartSSHServer installs (if necessary) and starts the SSH in the codespace. +// It returns the remote port where it is running, the user to log in with, or an error if something failed. +func StartSSHServer(ctx context.Context, client *liveshare.Client, log logger) (serverPort int, user string, err error) { + log.Println("Fetching SSH details...") + + sshServer, err := liveshare.NewSSHServer(client) + if err != nil { + return 0, "", fmt.Errorf("error creating live share: %v", err) + } + + sshServerStartResult, err := sshServer.StartRemoteServer(ctx) + if err != nil { + return 0, "", fmt.Errorf("error starting live share: %v", err) + } + + if !sshServerStartResult.Result { + return 0, "", errors.New(sshServerStartResult.Message) + } + + portInt, err := strconv.Atoi(sshServerStartResult.ServerPort) + if err != nil { + return 0, "", fmt.Errorf("error parsing port: %v", err) + } + + return portInt, sshServerStartResult.User, nil +} + func makeSSHArgs(port int, dst, cmd string) ([]string, []string) { connArgs := []string{"-p", strconv.Itoa(port), "-o", "NoHostAuthenticationForLocalhost=yes"} cmdArgs := append([]string{dst, "-X", "-Y", "-C"}, connArgs...) // X11, X11Trust, Compression diff --git a/internal/codespaces/states.go b/internal/codespaces/states.go index b6d6937a8..76d8791e5 100644 --- a/internal/codespaces/states.go +++ b/internal/codespaces/states.go @@ -45,7 +45,12 @@ func PollPostCreateStates(ctx context.Context, log logger, apiClient *api.API, u return fmt.Errorf("connect to Live Share: %v", err) } - tunnelPort, connClosed, err := MakeSSHTunnel(ctx, lsclient, 0) + remoteSSHServerPort, sshUser, err := StartSSHServer(ctx, lsclient, log) + if err != nil { + return fmt.Errorf("error getting ssh server details: %v", err) + } + + tunnelPort, connClosed, err := MakeSSHTunnel(ctx, lsclient, 0, remoteSSHServerPort) if err != nil { return fmt.Errorf("make ssh tunnel: %v", err) } @@ -60,7 +65,7 @@ func PollPostCreateStates(ctx context.Context, log logger, apiClient *api.API, u case err := <-connClosed: return fmt.Errorf("connection closed: %v", err) case <-t.C: - states, err := getPostCreateOutput(ctx, tunnelPort, codespace) + states, err := getPostCreateOutput(ctx, tunnelPort, codespace, sshUser) if err != nil { return fmt.Errorf("get post create output: %v", err) } @@ -70,9 +75,9 @@ func PollPostCreateStates(ctx context.Context, log logger, apiClient *api.API, u } } -func getPostCreateOutput(ctx context.Context, tunnelPort int, codespace *api.Codespace) ([]PostCreateState, error) { +func getPostCreateOutput(ctx context.Context, tunnelPort int, codespace *api.Codespace, user string) ([]PostCreateState, error) { stdout, err := RunCommand( - ctx, tunnelPort, sshDestination(codespace), + ctx, tunnelPort, fmt.Sprintf("%s@localhost", user), "cat /workspaces/.codespaces/shared/postCreateOutput.json", ) if err != nil { @@ -94,12 +99,3 @@ func getPostCreateOutput(ctx context.Context, tunnelPort int, codespace *api.Cod return output.Steps, nil } - -// TODO(josebalius): this won't be needed soon -func sshDestination(codespace *api.Codespace) string { - user := "codespace" - if codespace.RepositoryNWO == "github/github" { - user = "root" - } - return user + "@localhost" -}