commit 4a0eaa3da503e5117045ab4ea39ebaafae522c0b Author: Jose Garcia Date: Wed Jul 14 16:12:30 2021 -0400 Latest and greatest diff --git a/api/api.go b/api/api.go new file mode 100644 index 000000000..b3f7577ed --- /dev/null +++ b/api/api.go @@ -0,0 +1,403 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "sort" + "strconv" + "strings" +) + +const githubAPI = "https://api.github.com" + +type API struct { + token string + client *http.Client +} + +func New(token string) *API { + return &API{token, &http.Client{}} +} + +type User struct { + Login string `json:"login"` +} + +type errResponse struct { + Message string `json:"message"` +} + +func (a *API) GetUser(ctx context.Context) (*User, error) { + req, err := http.NewRequest(http.MethodGet, githubAPI+"/user", nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + a.setHeaders(req) + resp, err := a.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, a.errorResponse(b) + } + + var response User + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %v", err) + } + + return &response, nil +} + +func (a *API) errorResponse(b []byte) error { + var response errResponse + if err := json.Unmarshal(b, &response); err != nil { + return fmt.Errorf("error unmarshaling error response: %v", err) + } + + return errors.New(response.Message) +} + +type Repository struct { + ID int `json:"id"` +} + +func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error) { + req, err := http.NewRequest(http.MethodGet, githubAPI+"/repos/"+strings.ToLower(nwo), nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + a.setHeaders(req) + resp, err := a.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, a.errorResponse(b) + } + + var response Repository + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %v", err) + } + + return &response, nil +} + +type Codespaces []*Codespace + +func (c Codespaces) SortByRecent() { + sort.Slice(c, func(i, j int) bool { + return c[i].CreatedAt > c[j].CreatedAt + }) +} + +type Codespace struct { + Name string `json:"name"` + GUID string `json:"guid"` + CreatedAt string `json:"created_at"` + Branch string `json:"branch"` + RepositoryName string `json:"repository_name"` + RepositoryNWO string `json:"repository_nwo"` + OwnerLogin string `json:"owner_login"` + Environment CodespaceEnvironment `json:"environment"` +} + +type CodespaceEnvironment struct { + State string `json:"state"` + Connection CodespaceEnvironmentConnection `json:"connection"` +} + +const ( + CodespaceEnvironmentStateAvailable = "Available" +) + +type CodespaceEnvironmentConnection struct { + SessionID string `json:"sessionId"` + SessionToken string `json:"sessionToken"` +} + +func (a *API) ListCodespaces(ctx context.Context, user *User) (Codespaces, error) { + req, err := http.NewRequest( + http.MethodGet, githubAPI+"/vscs_internal/user/"+user.Login+"/codespaces", nil, + ) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + a.setHeaders(req) + resp, err := a.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, a.errorResponse(b) + } + + response := struct { + Codespaces Codespaces `json:"codespaces"` + }{} + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %v", err) + } + return response.Codespaces, nil +} + +type getCodespaceTokenRequest struct { + MintRepositoryToken bool `json:"mint_repository_token"` +} + +type getCodespaceTokenResponse struct { + RepositoryToken string `json:"repository_token"` +} + +func (a *API) GetCodespaceToken(ctx context.Context, codespace *Codespace) (string, error) { + reqBody, err := json.Marshal(getCodespaceTokenRequest{true}) + if err != nil { + return "", fmt.Errorf("error preparing request body: %v", err) + } + + req, err := http.NewRequest( + http.MethodPost, + githubAPI+"/vscs_internal/user/"+codespace.OwnerLogin+"/codespaces/"+codespace.Name+"/token", + bytes.NewBuffer(reqBody), + ) + if err != nil { + return "", fmt.Errorf("error creating request: %v", err) + } + + a.setHeaders(req) + resp, err := a.client.Do(req) + if err != nil { + return "", fmt.Errorf("error making request: %v", err) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("error reading response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return "", a.errorResponse(b) + } + + var response getCodespaceTokenResponse + if err := json.Unmarshal(b, &response); err != nil { + return "", fmt.Errorf("error unmarshaling response: %v", err) + } + + return response.RepositoryToken, nil +} + +func (a *API) GetCodespace(ctx context.Context, token, owner, codespace string) (*Codespace, error) { + req, err := http.NewRequest( + http.MethodGet, + githubAPI+"/vscs_internal/user/"+owner+"/codespaces/"+codespace, + nil, + ) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + req.Header.Set("Authorization", "Bearer "+token) + resp, err := a.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, a.errorResponse(b) + } + + var response Codespace + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %v", err) + } + + return &response, nil +} + +func (a *API) StartCodespace(ctx context.Context, token string, codespace *Codespace) error { + req, err := http.NewRequest( + http.MethodPost, + githubAPI+"/vscs_internal/proxy/environments/"+codespace.GUID+"/start", + nil, + ) + if err != nil { + return fmt.Errorf("error creating request: %v", err) + } + + req.Header.Set("Authorization", "Bearer "+token) + _, err = a.client.Do(req) + if err != nil { + return fmt.Errorf("error making request: %v", err) + } + + return nil +} + +type getCodespaceRegionLocationResponse struct { + Current string `json:"current"` +} + +func (a *API) GetCodespaceRegionLocation(ctx context.Context) (string, error) { + req, err := http.NewRequest(http.MethodGet, "https://online.visualstudio.com/api/v1/locations", nil) + if err != nil { + return "", fmt.Errorf("error creating request: %v", err) + } + + resp, err := a.client.Do(req) + if err != nil { + return "", fmt.Errorf("error making request: %v", err) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("error reading response body: %v", err) + } + + var response getCodespaceRegionLocationResponse + if err := json.Unmarshal(b, &response); err != nil { + return "", fmt.Errorf("error unmarshaling response: %v", err) + } + + return response.Current, nil +} + +type Skus []*Sku + +type Sku struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` +} + +func (a *API) GetCodespacesSkus(ctx context.Context, user *User, repository *Repository, location string) (Skus, error) { + req, err := http.NewRequest(http.MethodGet, githubAPI+"/vscs_internal/user/"+user.Login+"/skus", nil) + if err != nil { + return nil, fmt.Errorf("err creating request: %v", err) + } + + q := req.URL.Query() + q.Add("location", location) + q.Add("repository_id", strconv.Itoa(repository.ID)) + req.URL.RawQuery = q.Encode() + + a.setHeaders(req) + resp, err := a.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + + response := struct { + Skus Skus `json:"skus"` + }{} + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %v", err) + } + + return response.Skus, nil +} + +type createCodespaceRequest struct { + RepositoryID int `json:"repository_id"` + Ref string `json:"ref"` + Location string `json:"location"` + SkuName string `json:"sku_name"` +} + +func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repository, sku *Sku, branch, location string) (*Codespace, error) { + requestBody, err := json.Marshal(createCodespaceRequest{repository.ID, branch, location, sku.Name}) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %v", err) + } + + req, err := http.NewRequest(http.MethodPost, githubAPI+"/vscs_internal/user/"+user.Login+"/codespaces", bytes.NewBuffer(requestBody)) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + a.setHeaders(req) + resp, err := a.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %v", err) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %v", err) + } + + if resp.StatusCode > http.StatusAccepted { + return nil, a.errorResponse(b) + } + + var response Codespace + if err := json.Unmarshal(b, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling response: %v", err) + } + + return &response, nil +} + +func (a *API) DeleteCodespace(ctx context.Context, user *User, token, codespaceName string) error { + req, err := http.NewRequest(http.MethodDelete, githubAPI+"/vscs_internal/user/"+user.Login+"/codespaces/"+codespaceName, nil) + if err != nil { + return fmt.Errorf("error creating request: %v", err) + } + + req.Header.Set("Authorization", "Bearer "+token) + resp, err := a.client.Do(req) + if err != nil { + return fmt.Errorf("error making request: %v", err) + } + + if resp.StatusCode > http.StatusAccepted { + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response body: %v", err) + } + return a.errorResponse(b) + } + + return nil +} + +func (a *API) setHeaders(req *http.Request) { + req.Header.Set("Authorization", "Bearer "+a.token) + req.Header.Set("Accept", "application/vnd.github.v3+json") +} diff --git a/cmd/ghcs/create.go b/cmd/ghcs/create.go new file mode 100644 index 000000000..44bedb5f2 --- /dev/null +++ b/cmd/ghcs/create.go @@ -0,0 +1,147 @@ +package main + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/fatih/camelcase" + "github.com/github/ghcs/api" + "github.com/spf13/cobra" +) + +var createCmd = &cobra.Command{ + Use: "create", + Short: "Create", + Long: "Create", + RunE: func(cmd *cobra.Command, args []string) error { + return Create() + }, +} + +func init() { + rootCmd.AddCommand(createCmd) +} + +var createSurvey = []*survey.Question{ + { + Name: "repository", + Prompt: &survey.Input{Message: "Repository"}, + Validate: survey.Required, + }, + { + Name: "branch", + Prompt: &survey.Input{Message: "Branch"}, + Validate: survey.Required, + }, +} + +func Create() error { + ctx := context.Background() + apiClient := api.New(os.Getenv("GITHUB_TOKEN")) + locationCh := getLocation(ctx, apiClient) + userCh := getUser(ctx, apiClient) + + answers := struct { + Repository string + Branch string + }{} + + if err := survey.Ask(createSurvey, &answers); err != nil { + return fmt.Errorf("error getting answers: %v", err) + } + + repository, err := apiClient.GetRepository(ctx, answers.Repository) + if err != nil { + return fmt.Errorf("error getting repository: %v", err) + } + + locationResult := <-locationCh + if locationResult.Err != nil { + return fmt.Errorf("error getting codespace region location: %v", locationResult.Err) + } + + userResult := <-userCh + if userResult.Err != nil { + return fmt.Errorf("error getting codespace user: %v", userResult.Err) + } + + skus, err := apiClient.GetCodespacesSkus(ctx, userResult.User, repository, locationResult.Location) + if err != nil { + return fmt.Errorf("error getting codespace skus: %v", err) + } + + if len(skus) == 0 { + fmt.Println("There are no available machine types for this repository") + return nil + } + + skuNames := make([]string, 0, len(skus)) + skuByName := make(map[string]*api.Sku) + for _, sku := range skus { + nameParts := camelcase.Split(sku.Name) + machineName := strings.Title(strings.ToLower(nameParts[0])) + skuName := fmt.Sprintf("%s - %s", machineName, sku.DisplayName) + skuNames = append(skuNames, skuName) + skuByName[skuName] = sku + } + + skuSurvey := []*survey.Question{ + { + Name: "sku", + Prompt: &survey.Select{ + Message: "Choose Machine Type:", + Options: skuNames, + Default: skuNames[0], + }, + Validate: survey.Required, + }, + } + + skuAnswers := struct{ SKU string }{} + if err := survey.Ask(skuSurvey, &skuAnswers); err != nil { + return fmt.Errorf("error getting SKU: %v", err) + } + + sku := skuByName[skuAnswers.SKU] + fmt.Println("Creating your codespace...") + + codespace, err := apiClient.CreateCodespace(ctx, userResult.User, repository, sku, answers.Branch, locationResult.Location) + if err != nil { + return fmt.Errorf("error creating codespace: %v", err) + } + + fmt.Println("Codespace created: " + codespace.Name) + + return nil +} + +type getUserResult struct { + User *api.User + Err error +} + +func getUser(ctx context.Context, apiClient *api.API) <-chan getUserResult { + ch := make(chan getUserResult) + go func() { + user, err := apiClient.GetUser(ctx) + ch <- getUserResult{user, err} + }() + return ch +} + +type locationResult struct { + Location string + Err error +} + +func getLocation(ctx context.Context, apiClient *api.API) <-chan locationResult { + ch := make(chan locationResult) + go func() { + location, err := apiClient.GetCodespaceRegionLocation(ctx) + ch <- locationResult{location, err} + }() + return ch +} diff --git a/cmd/ghcs/delete.go b/cmd/ghcs/delete.go new file mode 100644 index 000000000..e5cd34a94 --- /dev/null +++ b/cmd/ghcs/delete.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/github/ghcs/api" + "github.com/spf13/cobra" +) + +func NewDeleteCmd() *cobra.Command { + deleteCmd := &cobra.Command{ + Use: "delete CODESPACE_NAME", + Short: "delete", + Long: "delete", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("A Codespace name is required.") + } + return Delete(args[0]) + }, + } + + return deleteCmd +} + +func init() { + rootCmd.AddCommand(NewDeleteCmd()) +} + +func Delete(codespaceName string) error { + apiClient := api.New(os.Getenv("GITHUB_TOKEN")) + ctx := context.Background() + + user, err := apiClient.GetUser(ctx) + if err != nil { + return fmt.Errorf("error getting user: %v", err) + } + + codespace := api.Codespace{OwnerLogin: user.Login, Name: codespaceName} + token, err := apiClient.GetCodespaceToken(ctx, &codespace) + if err != nil { + return fmt.Errorf("error getting codespace token: %v", err) + } + + if err := apiClient.DeleteCodespace(ctx, user, token, codespaceName); err != nil { + return fmt.Errorf("error deleting codespace: %v", err) + } + + fmt.Println("Codespace deleted.") + + return List() +} diff --git a/cmd/ghcs/list.go b/cmd/ghcs/list.go new file mode 100644 index 000000000..e02e6a1d2 --- /dev/null +++ b/cmd/ghcs/list.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/olekukonko/tablewriter" + + "github.com/github/ghcs/api" + "github.com/spf13/cobra" +) + +func NewListCmd() *cobra.Command { + listCmd := &cobra.Command{ + Use: "list", + Short: "list", + Long: "list", + RunE: func(cmd *cobra.Command, args []string) error { + return List() + }, + } + + return listCmd +} + +func init() { + rootCmd.AddCommand(NewListCmd()) +} + +func List() error { + apiClient := api.New(os.Getenv("GITHUB_TOKEN")) + ctx := context.Background() + + user, err := apiClient.GetUser(ctx) + if err != nil { + return fmt.Errorf("error getting user: %v", err) + } + + codespaces, err := apiClient.ListCodespaces(ctx, user) + if err != nil { + return fmt.Errorf("error getting codespaces: %v", err) + } + + if len(codespaces) == 0 { + fmt.Println("You have no codespaces.") + return nil + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Name", "Repository", "Branch", "State", "Created At"}) + for _, codespace := range codespaces { + table.Append([]string{ + codespace.Name, codespace.RepositoryNWO, codespace.Branch, codespace.Environment.State, codespace.CreatedAt, + }) + } + + table.Render() + return nil +} diff --git a/cmd/ghcs/main.go b/cmd/ghcs/main.go new file mode 100644 index 000000000..400f5324c --- /dev/null +++ b/cmd/ghcs/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// ghcs create +// ghcs connect +// ghcs delete +// ghcs list +func main() { + Execute() +} + +var rootCmd = &cobra.Command{ + Use: "ghcs", + Short: "Codespaces", + Long: "Codespaces", +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/ghcs/ssh.go b/cmd/ghcs/ssh.go new file mode 100644 index 000000000..56f22224f --- /dev/null +++ b/cmd/ghcs/ssh.go @@ -0,0 +1,262 @@ +package main + +import ( + "bufio" + "context" + "errors" + "fmt" + "log" + "math/rand" + "os" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/AlecAivazis/survey/v2" + "github.com/github/ghcs/api" + "github.com/github/go-liveshare" + "github.com/spf13/cobra" +) + +func NewSSHCmd() *cobra.Command { + var sshProfile string + + sshCmd := &cobra.Command{ + Use: "ssh", + Short: "ssh", + Long: "ssh", + RunE: func(cmd *cobra.Command, args []string) error { + return SSH(sshProfile) + }, + } + + sshCmd.Flags().StringVarP(&sshProfile, "profile", "", "", "SSH Profile") + + return sshCmd +} + +func init() { + rootCmd.AddCommand(NewSSHCmd()) +} + +func SSH(sshProfile string) error { + apiClient := api.New(os.Getenv("GITHUB_TOKEN")) + ctx := context.Background() + + user, err := apiClient.GetUser(ctx) + if err != nil { + return fmt.Errorf("error getting user: %v", err) + } + + codespaces, err := apiClient.ListCodespaces(ctx, user) + if err != nil { + return fmt.Errorf("error getting codespaces: %v", err) + } + + if len(codespaces) == 0 { + fmt.Println("You have no codespaces.") + return nil + } + + codespaces.SortByRecent() + + codespacesByName := make(map[string]*api.Codespace) + codespacesNames := make([]string, 0, len(codespaces)) + for _, codespace := range codespaces { + codespacesByName[codespace.Name] = codespace + codespacesNames = append(codespacesNames, codespace.Name) + } + + sshSurvey := []*survey.Question{ + { + Name: "codespace", + Prompt: &survey.Select{ + Message: "Choose Codespace:", + Options: codespacesNames, + Default: codespacesNames[0], + }, + Validate: survey.Required, + }, + } + + answers := struct { + Codespace string + }{} + if err := survey.Ask(sshSurvey, &answers); err != nil { + return fmt.Errorf("error getting answers: %v", err) + } + + codespace := codespacesByName[answers.Codespace] + + token, err := apiClient.GetCodespaceToken(ctx, codespace) + if err != nil { + return fmt.Errorf("error getting codespace token: %v", err) + } + + if codespace.Environment.State != api.CodespaceEnvironmentStateAvailable { + fmt.Println("Starting your codespace...") + if err := apiClient.StartCodespace(ctx, token, codespace); err != nil { + return fmt.Errorf("error starting codespace: %v", err) + } + } + + retries := 0 + for codespace.Environment.Connection.SessionID == "" || codespace.Environment.State != api.CodespaceEnvironmentStateAvailable { + if retries > 1 { + if retries%2 == 0 { + fmt.Print(".") + } + + time.Sleep(1 * time.Second) + } + + if retries == 10 { + return errors.New("Failed to start codespace") + } + + codespace, err = apiClient.GetCodespace(ctx, token, codespace.OwnerLogin, codespace.Name) + if err != nil { + return fmt.Errorf("error getting codespace: %v", err) + } + + retries += 1 + } + + if retries >= 2 { + fmt.Print("\n") + } + + fmt.Println("Connecting to your codespace...") + + liveShare, err := liveshare.New( + liveshare.WithWorkspaceID(codespace.Environment.Connection.SessionID), + liveshare.WithToken(codespace.Environment.Connection.SessionToken), + ) + if err != nil { + return fmt.Errorf("error creating live share: %v", err) + } + + liveShareClient := liveShare.NewClient() + if err := liveShareClient.Join(ctx); err != nil { + return fmt.Errorf("error joining liveshare client: %v", err) + } + + terminal, err := liveShareClient.NewTerminal() + if err != nil { + return fmt.Errorf("error creating liveshare terminal: %v", err) + } + + if sshProfile == "" { + containerID, err := getContainerID(ctx, terminal) + if err != nil { + return fmt.Errorf("error getting container id: %v", err) + } + + if err := setupSSH(ctx, terminal, containerID, codespace.RepositoryName); err != nil { + return fmt.Errorf("error creating ssh server: %v", err) + } + } + + server, err := liveShareClient.NewServer() + if err != nil { + return fmt.Errorf("error creating server: %v", err) + } + + rand.Seed(time.Now().Unix()) + port := rand.Intn(9999-2000) + 2000 // improve this obviously + if err := server.StartSharing(ctx, "sshd", 2222); err != nil { + return fmt.Errorf("error sharing sshd port: %v", err) + } + + portForwarder := liveshare.NewLocalPortForwarder(liveShareClient, server, port) + go func() { + if err := portForwarder.Start(ctx); err != nil { + panic(fmt.Errorf("error forwarding port: %v", err)) + } + }() + + if err := connect(ctx, port, sshProfile); err != nil { + return fmt.Errorf("error connecting via SSH: %v", err) + } + + return nil +} + +func connect(ctx context.Context, port int, sshProfile string) error { + var cmd *exec.Cmd + if sshProfile != "" { + cmd = exec.CommandContext(ctx, "ssh", sshProfile, "-p", strconv.Itoa(port), "-C") + } else { + cmd = exec.CommandContext(ctx, "ssh", "codespace@localhost", "-C", "-p", strconv.Itoa(port), "-o", "NoHostAuthenticationForLocalhost=yes") + } + + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return fmt.Errorf("error running ssh: %v", err) + } + + go func() { + if err := cmd.Wait(); err != nil { + log.Println(fmt.Errorf("error waiting for ssh to finish: %v", err)) + } + }() + + done := make(chan bool) + <-done + + return nil +} + +func getContainerID(ctx context.Context, terminal *liveshare.Terminal) (string, error) { + 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) + } + + scanner := bufio.NewScanner(stream) + scanner.Scan() + + containerID := scanner.Text() + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("error scanning stream: %v", err) + } + + if err := stream.Close(); err != nil { + return "", fmt.Errorf("error closing stream: %v", err) + } + + return containerID, nil +} + +func setupSSH(ctx context.Context, terminal *liveshare.Terminal, containerID, repositoryName string) error { + getUsernameCmd := "GITHUB_USERNAME=\"$(jq .CODESPACE_NAME /workspaces/.codespaces/shared/environment-variables.json -r | cut -f1 -d -)\"" + makeSSHDirCmd := "mkdir /home/codespace/.ssh" + getUserKeysCmd := "curl --silent --fail \"https://github.com/$(echo $GITHUB_USERNAME).keys\" > /home/codespace/.ssh/authorized_keys" + setupLoginDirCmd := fmt.Sprintf("echo \"cd /workspaces/%v\" > /home/codespace/.bash_profile", repositoryName) + + compositeCommand := []string{getUsernameCmd, makeSSHDirCmd, getUserKeysCmd, setupLoginDirCmd} + 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) + } + + if err := stream.Close(); err != nil { + return fmt.Errorf("error closing stream: %v", err) + } + + time.Sleep(1 * time.Second) + + return nil +}