Merge remote-tracking branch 'origin/main' into edgonmsft/codespaces-ssh-rpc
This commit is contained in:
commit
ee36a005b1
11 changed files with 378 additions and 145 deletions
98
api/api.go
98
api/api.go
|
|
@ -1,5 +1,12 @@
|
|||
// TODO(adonovan): rename to package codespaces, and codespaces.Client.
|
||||
package api
|
||||
|
||||
// For descriptions of service interfaces, see:
|
||||
// - https://online.visualstudio.com/api/swagger (for visualstudio.com)
|
||||
// - https://docs.github.com/en/rest/reference/repos (for api.github.com)
|
||||
// - https://github.com/github/github/blob/master/app/api/codespaces.rb (for vscs_internal)
|
||||
// TODO(adonovan): replace the last link with a public doc URL when available.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
|
@ -9,7 +16,6 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -29,10 +35,6 @@ 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 {
|
||||
|
|
@ -44,6 +46,7 @@ func (a *API) GetUser(ctx context.Context) (*User, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
|
@ -51,7 +54,7 @@ func (a *API) GetUser(ctx context.Context) (*User, error) {
|
|||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, a.errorResponse(b)
|
||||
return nil, jsonErrorResponse(b)
|
||||
}
|
||||
|
||||
var response User
|
||||
|
|
@ -62,8 +65,10 @@ func (a *API) GetUser(ctx context.Context) (*User, error) {
|
|||
return &response, nil
|
||||
}
|
||||
|
||||
func (a *API) errorResponse(b []byte) error {
|
||||
var response errResponse
|
||||
func jsonErrorResponse(b []byte) error {
|
||||
var response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return fmt.Errorf("error unmarshaling error response: %v", err)
|
||||
}
|
||||
|
|
@ -86,6 +91,7 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
|
@ -93,7 +99,7 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error
|
|||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, a.errorResponse(b)
|
||||
return nil, jsonErrorResponse(b)
|
||||
}
|
||||
|
||||
var response Repository
|
||||
|
|
@ -104,14 +110,6 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error
|
|||
return &response, nil
|
||||
}
|
||||
|
||||
type Codespaces []*Codespace
|
||||
|
||||
func (c Codespaces) SortByCreatedAt() {
|
||||
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"`
|
||||
|
|
@ -139,7 +137,7 @@ type CodespaceEnvironmentConnection struct {
|
|||
RelaySAS string `json:"relaySas"`
|
||||
}
|
||||
|
||||
func (a *API) ListCodespaces(ctx context.Context, user *User) (Codespaces, error) {
|
||||
func (a *API) ListCodespaces(ctx context.Context, user *User) ([]*Codespace, error) {
|
||||
req, err := http.NewRequest(
|
||||
http.MethodGet, githubAPI+"/vscs_internal/user/"+user.Login+"/codespaces", nil,
|
||||
)
|
||||
|
|
@ -152,6 +150,7 @@ func (a *API) ListCodespaces(ctx context.Context, user *User) (Codespaces, error
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
|
@ -159,12 +158,12 @@ func (a *API) ListCodespaces(ctx context.Context, user *User) (Codespaces, error
|
|||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, a.errorResponse(b)
|
||||
return nil, jsonErrorResponse(b)
|
||||
}
|
||||
|
||||
response := struct {
|
||||
Codespaces Codespaces `json:"codespaces"`
|
||||
}{}
|
||||
var response struct {
|
||||
Codespaces []*Codespace `json:"codespaces"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %v", err)
|
||||
}
|
||||
|
|
@ -199,6 +198,7 @@ func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName s
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
|
@ -206,7 +206,7 @@ func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName s
|
|||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", a.errorResponse(b)
|
||||
return "", jsonErrorResponse(b)
|
||||
}
|
||||
|
||||
var response getCodespaceTokenResponse
|
||||
|
|
@ -232,6 +232,7 @@ func (a *API) GetCodespace(ctx context.Context, token, owner, codespace string)
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
|
@ -239,7 +240,7 @@ func (a *API) GetCodespace(ctx context.Context, token, owner, codespace string)
|
|||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, a.errorResponse(b)
|
||||
return nil, jsonErrorResponse(b)
|
||||
}
|
||||
|
||||
var response Codespace
|
||||
|
|
@ -261,10 +262,24 @@ func (a *API) StartCodespace(ctx context.Context, token string, codespace *Codes
|
|||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
_, err = a.client.Do(req)
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading response body: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// Error response is numeric code and/or string message, not JSON.
|
||||
if len(b) > 100 {
|
||||
b = append(b[:97], "..."...)
|
||||
}
|
||||
return fmt.Errorf("failed to start codespace: %s", b)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -283,12 +298,17 @@ func (a *API) GetCodespaceRegionLocation(ctx context.Context) (string, error) {
|
|||
if err != nil {
|
||||
return "", fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading response body: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", jsonErrorResponse(b)
|
||||
}
|
||||
|
||||
var response getCodespaceRegionLocationResponse
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return "", fmt.Errorf("error unmarshaling response: %v", err)
|
||||
|
|
@ -297,14 +317,12 @@ func (a *API) GetCodespaceRegionLocation(ctx context.Context) (string, error) {
|
|||
return response.Current, nil
|
||||
}
|
||||
|
||||
type Skus []*Sku
|
||||
|
||||
type Sku struct {
|
||||
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) {
|
||||
func (a *API) GetCodespacesSKUs(ctx context.Context, user *User, repository *Repository, location string) ([]*SKU, 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)
|
||||
|
|
@ -320,20 +338,25 @@ func (a *API) GetCodespacesSkus(ctx context.Context, user *User, repository *Rep
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
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 resp.StatusCode != http.StatusOK {
|
||||
return nil, jsonErrorResponse(b)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
SKUs []*SKU `json:"skus"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %v", err)
|
||||
}
|
||||
|
||||
return response.Skus, nil
|
||||
return response.SKUs, nil
|
||||
}
|
||||
|
||||
type createCodespaceRequest struct {
|
||||
|
|
@ -359,6 +382,7 @@ func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repos
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
|
|
@ -366,7 +390,7 @@ func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repos
|
|||
}
|
||||
|
||||
if resp.StatusCode > http.StatusAccepted {
|
||||
return nil, a.errorResponse(b)
|
||||
return nil, jsonErrorResponse(b)
|
||||
}
|
||||
|
||||
var response Codespace
|
||||
|
|
@ -388,13 +412,14 @@ func (a *API) DeleteCodespace(ctx context.Context, user *User, token, codespaceN
|
|||
if err != nil {
|
||||
return fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
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 jsonErrorResponse(b)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -419,6 +444,7 @@ func (a *API) GetCodespaceRepositoryContents(ctx context.Context, codespace *Cod
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
|
|
@ -430,7 +456,7 @@ func (a *API) GetCodespaceRepositoryContents(ctx context.Context, codespace *Cod
|
|||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, a.errorResponse(b)
|
||||
return nil, jsonErrorResponse(b)
|
||||
}
|
||||
|
||||
var response getCodespaceRepositoryContentsResponse
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCodeCmd() *cobra.Command {
|
||||
func newCodeCmd() *cobra.Command {
|
||||
useInsiders := false
|
||||
|
||||
codeCmd := &cobra.Command{
|
||||
|
|
@ -24,7 +24,7 @@ func NewCodeCmd() *cobra.Command {
|
|||
if len(args) > 0 {
|
||||
codespaceName = args[0]
|
||||
}
|
||||
return Code(codespaceName, useInsiders)
|
||||
return code(codespaceName, useInsiders)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -34,10 +34,10 @@ func NewCodeCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(NewCodeCmd())
|
||||
rootCmd.AddCommand(newCodeCmd())
|
||||
}
|
||||
|
||||
func Code(codespaceName string, useInsiders bool) error {
|
||||
func code(codespaceName string, useInsiders bool) error {
|
||||
apiClient := api.New(os.Getenv("GITHUB_TOKEN"))
|
||||
ctx := context.Background()
|
||||
|
||||
|
|
@ -57,8 +57,9 @@ func Code(codespaceName string, useInsiders bool) error {
|
|||
codespaceName = codespace.Name
|
||||
}
|
||||
|
||||
if err := open.Run(vscodeProtocolURL(codespaceName, useInsiders)); err != nil {
|
||||
return fmt.Errorf("error opening vscode URL")
|
||||
url := vscodeProtocolURL(codespaceName, useInsiders)
|
||||
if err := open.Run(url); err != nil {
|
||||
return fmt.Errorf("error opening vscode URL %s: %s. (Is VSCode installed?)", url, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -11,24 +11,33 @@ import (
|
|||
"github.com/fatih/camelcase"
|
||||
"github.com/github/ghcs/api"
|
||||
"github.com/github/ghcs/cmd/ghcs/output"
|
||||
"github.com/github/ghcs/internal/codespaces"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var repo, branch, machine string
|
||||
type createOptions struct {
|
||||
repo string
|
||||
branch string
|
||||
machine string
|
||||
showStatus bool
|
||||
}
|
||||
|
||||
func newCreateCmd() *cobra.Command {
|
||||
opts := &createOptions{}
|
||||
|
||||
createCmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a Codespace",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return Create()
|
||||
return create(opts)
|
||||
},
|
||||
}
|
||||
|
||||
createCmd.Flags().StringVarP(&repo, "repo", "r", "", "repository name with owner: user/repo")
|
||||
createCmd.Flags().StringVarP(&branch, "branch", "b", "", "repository branch")
|
||||
createCmd.Flags().StringVarP(&machine, "machine", "m", "", "hardware specifications for the VM")
|
||||
createCmd.Flags().StringVarP(&opts.repo, "repo", "r", "", "repository name with owner: user/repo")
|
||||
createCmd.Flags().StringVarP(&opts.branch, "branch", "b", "", "repository branch")
|
||||
createCmd.Flags().StringVarP(&opts.machine, "machine", "m", "", "hardware specifications for the VM")
|
||||
createCmd.Flags().BoolVarP(&opts.showStatus, "status", "s", false, "show status of post-create command and dotfiles")
|
||||
|
||||
return createCmd
|
||||
}
|
||||
|
|
@ -37,18 +46,18 @@ func init() {
|
|||
rootCmd.AddCommand(newCreateCmd())
|
||||
}
|
||||
|
||||
func Create() error {
|
||||
func create(opts *createOptions) error {
|
||||
ctx := context.Background()
|
||||
apiClient := api.New(os.Getenv("GITHUB_TOKEN"))
|
||||
locationCh := getLocation(ctx, apiClient)
|
||||
userCh := getUser(ctx, apiClient)
|
||||
log := output.NewLogger(os.Stdout, os.Stderr, false)
|
||||
|
||||
repo, err := getRepoName()
|
||||
repo, err := getRepoName(opts.repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting repository name: %v", err)
|
||||
}
|
||||
branch, err := getBranchName()
|
||||
branch, err := getBranchName(opts.branch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting branch name: %v", err)
|
||||
}
|
||||
|
|
@ -68,7 +77,7 @@ func Create() error {
|
|||
return fmt.Errorf("error getting codespace user: %v", userResult.Err)
|
||||
}
|
||||
|
||||
machine, err := getMachineName(ctx, userResult.User, repository, locationResult.Location, apiClient)
|
||||
machine, err := getMachineName(ctx, opts.machine, userResult.User, repository, locationResult.Location, apiClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting machine type: %v", err)
|
||||
}
|
||||
|
|
@ -83,6 +92,12 @@ func Create() error {
|
|||
return fmt.Errorf("error creating codespace: %v", err)
|
||||
}
|
||||
|
||||
if opts.showStatus {
|
||||
if err := showStatus(ctx, log, apiClient, userResult.User, codespace); err != nil {
|
||||
return fmt.Errorf("show status: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Codespace created: ")
|
||||
|
||||
fmt.Fprintln(os.Stdout, codespace.Name)
|
||||
|
|
@ -90,11 +105,67 @@ func Create() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func showStatus(ctx context.Context, log *output.Logger, apiClient *api.API, user *api.User, codespace *api.Codespace) error {
|
||||
var lastState codespaces.PostCreateState
|
||||
var breakNextState bool
|
||||
|
||||
finishedStates := make(map[string]bool)
|
||||
ctx, stopPolling := context.WithCancel(ctx)
|
||||
|
||||
poller := func(states []codespaces.PostCreateState) {
|
||||
var inProgress bool
|
||||
for _, state := range states {
|
||||
if _, found := finishedStates[state.Name]; found {
|
||||
continue // skip this state as we've processed it already
|
||||
}
|
||||
|
||||
if state.Name != lastState.Name {
|
||||
log.Print(state.Name)
|
||||
|
||||
if state.Status == codespaces.PostCreateStateRunning {
|
||||
inProgress = true
|
||||
lastState = state
|
||||
log.Print("...")
|
||||
break
|
||||
}
|
||||
|
||||
finishedStates[state.Name] = true
|
||||
log.Println("..." + state.Status)
|
||||
} else {
|
||||
if state.Status == codespaces.PostCreateStateRunning {
|
||||
inProgress = true
|
||||
log.Print(".")
|
||||
break
|
||||
}
|
||||
|
||||
finishedStates[state.Name] = true
|
||||
log.Println(state.Status)
|
||||
lastState = codespaces.PostCreateState{} // reset the value
|
||||
}
|
||||
}
|
||||
|
||||
if !inProgress {
|
||||
if breakNextState {
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
breakNextState = true
|
||||
}
|
||||
}
|
||||
|
||||
if err := codespaces.PollPostCreateStates(ctx, log, apiClient, user, codespace, poller); err != nil {
|
||||
return fmt.Errorf("failed to poll state changes from codespace: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type getUserResult struct {
|
||||
User *api.User
|
||||
Err error
|
||||
}
|
||||
|
||||
// getUser fetches the user record associated with the GITHUB_TOKEN
|
||||
func getUser(ctx context.Context, apiClient *api.API) <-chan getUserResult {
|
||||
ch := make(chan getUserResult)
|
||||
go func() {
|
||||
|
|
@ -109,6 +180,7 @@ type locationResult struct {
|
|||
Err error
|
||||
}
|
||||
|
||||
// getLocation fetches the closest Codespace datacenter region/location to the user.
|
||||
func getLocation(ctx context.Context, apiClient *api.API) <-chan locationResult {
|
||||
ch := make(chan locationResult)
|
||||
go func() {
|
||||
|
|
@ -118,7 +190,8 @@ func getLocation(ctx context.Context, apiClient *api.API) <-chan locationResult
|
|||
return ch
|
||||
}
|
||||
|
||||
func getRepoName() (string, error) {
|
||||
// getRepoName prompts the user for the name of the repository, or returns the repository if non-empty.
|
||||
func getRepoName(repo string) (string, error) {
|
||||
if repo != "" {
|
||||
return repo, nil
|
||||
}
|
||||
|
|
@ -126,15 +199,16 @@ func getRepoName() (string, error) {
|
|||
repoSurvey := []*survey.Question{
|
||||
{
|
||||
Name: "repository",
|
||||
Prompt: &survey.Input{Message: "Repository"},
|
||||
Prompt: &survey.Input{Message: "Repository:"},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
}
|
||||
err := survey.Ask(repoSurvey, &repo)
|
||||
err := ask(repoSurvey, &repo)
|
||||
return repo, err
|
||||
}
|
||||
|
||||
func getBranchName() (string, error) {
|
||||
// getBranchName prompts the user for the name of the branch, or returns the branch if non-empty.
|
||||
func getBranchName(branch string) (string, error) {
|
||||
if branch != "" {
|
||||
return branch, nil
|
||||
}
|
||||
|
|
@ -142,18 +216,19 @@ func getBranchName() (string, error) {
|
|||
branchSurvey := []*survey.Question{
|
||||
{
|
||||
Name: "branch",
|
||||
Prompt: &survey.Input{Message: "Branch"},
|
||||
Prompt: &survey.Input{Message: "Branch:"},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
}
|
||||
err := survey.Ask(branchSurvey, &branch)
|
||||
err := ask(branchSurvey, &branch)
|
||||
return branch, err
|
||||
}
|
||||
|
||||
func getMachineName(ctx context.Context, user *api.User, repo *api.Repository, location string, apiClient *api.API) (string, error) {
|
||||
skus, err := apiClient.GetCodespacesSkus(ctx, user, repo, location)
|
||||
// getMachineName prompts the user to select the machine type, or validates the machine if non-empty.
|
||||
func getMachineName(ctx context.Context, machine string, user *api.User, repo *api.Repository, location string, apiClient *api.API) (string, error) {
|
||||
skus, err := apiClient.GetCodespacesSKUs(ctx, user, repo, location)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting codespace skus: %v", err)
|
||||
return "", fmt.Errorf("error getting codespace SKUs: %v", err)
|
||||
}
|
||||
|
||||
// if user supplied a machine type, it must be valid
|
||||
|
|
@ -165,18 +240,18 @@ func getMachineName(ctx context.Context, user *api.User, repo *api.Repository, l
|
|||
}
|
||||
}
|
||||
|
||||
availableSkus := make([]string, len(skus))
|
||||
availableSKUs := make([]string, len(skus))
|
||||
for i := 0; i < len(skus); i++ {
|
||||
availableSkus[i] = skus[i].Name
|
||||
availableSKUs[i] = skus[i].Name
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("there are is no such machine for the repository: %s\nAvailable machines: %v", machine, availableSkus)
|
||||
return "", fmt.Errorf("there is no such machine for the repository: %s\nAvailable machines: %v", machine, availableSKUs)
|
||||
} else if len(skus) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
skuNames := make([]string, 0, len(skus))
|
||||
skuByName := make(map[string]*api.Sku)
|
||||
skuByName := make(map[string]*api.SKU)
|
||||
for _, sku := range skus {
|
||||
nameParts := camelcase.Split(sku.Name)
|
||||
machineName := strings.Title(strings.ToLower(nameParts[0]))
|
||||
|
|
@ -197,8 +272,8 @@ func getMachineName(ctx context.Context, user *api.User, repo *api.Repository, l
|
|||
},
|
||||
}
|
||||
|
||||
skuAnswers := struct{ SKU string }{}
|
||||
if err := survey.Ask(skuSurvey, &skuAnswers); err != nil {
|
||||
var skuAnswers struct{ SKU string }
|
||||
if err := ask(skuSurvey, &skuAnswers); err != nil {
|
||||
return "", fmt.Errorf("error getting SKU: %v", err)
|
||||
}
|
||||
|
||||
|
|
@ -207,3 +282,8 @@ func getMachineName(ctx context.Context, user *api.User, repo *api.Repository, l
|
|||
|
||||
return machine, nil
|
||||
}
|
||||
|
||||
// ask asks survery questions using standard options.
|
||||
func ask(qs []*survey.Question, response interface{}) error {
|
||||
return survey.Ask(qs, response, survey.WithShowCursor(true))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewDeleteCmd() *cobra.Command {
|
||||
func newDeleteCmd() *cobra.Command {
|
||||
deleteCmd := &cobra.Command{
|
||||
Use: "delete [<codespace>]",
|
||||
Short: "Delete a Codespace",
|
||||
|
|
@ -22,7 +22,7 @@ func NewDeleteCmd() *cobra.Command {
|
|||
if len(args) > 0 {
|
||||
codespaceName = args[0]
|
||||
}
|
||||
return Delete(codespaceName)
|
||||
return delete_(codespaceName)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ func NewDeleteCmd() *cobra.Command {
|
|||
Short: "Delete all Codespaces for the current user",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return DeleteAll()
|
||||
return deleteAll()
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ func NewDeleteCmd() *cobra.Command {
|
|||
Short: "Delete all Codespaces for a repository",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return DeleteByRepo(args[0])
|
||||
return deleteByRepo(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -50,10 +50,10 @@ func NewDeleteCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(NewDeleteCmd())
|
||||
rootCmd.AddCommand(newDeleteCmd())
|
||||
}
|
||||
|
||||
func Delete(codespaceName string) error {
|
||||
func delete_(codespaceName string) error {
|
||||
apiClient := api.New(os.Getenv("GITHUB_TOKEN"))
|
||||
ctx := context.Background()
|
||||
log := output.NewLogger(os.Stdout, os.Stderr, false)
|
||||
|
|
@ -74,10 +74,10 @@ func Delete(codespaceName string) error {
|
|||
|
||||
log.Println("Codespace deleted.")
|
||||
|
||||
return List(&ListOptions{})
|
||||
return list(&listOptions{})
|
||||
}
|
||||
|
||||
func DeleteAll() error {
|
||||
func deleteAll() error {
|
||||
apiClient := api.New(os.Getenv("GITHUB_TOKEN"))
|
||||
ctx := context.Background()
|
||||
log := output.NewLogger(os.Stdout, os.Stderr, false)
|
||||
|
|
@ -105,10 +105,10 @@ func DeleteAll() error {
|
|||
log.Printf("Codespace deleted: %s\n", c.Name)
|
||||
}
|
||||
|
||||
return List(&ListOptions{})
|
||||
return list(&listOptions{})
|
||||
}
|
||||
|
||||
func DeleteByRepo(repo string) error {
|
||||
func deleteByRepo(repo string) error {
|
||||
apiClient := api.New(os.Getenv("GITHUB_TOKEN"))
|
||||
ctx := context.Background()
|
||||
log := output.NewLogger(os.Stdout, os.Stderr, false)
|
||||
|
|
@ -146,5 +146,5 @@ func DeleteByRepo(repo string) error {
|
|||
return fmt.Errorf("No codespace was found for repository: %s", repo)
|
||||
}
|
||||
|
||||
return List(&ListOptions{})
|
||||
return list(&listOptions{})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,32 +10,32 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ListOptions struct {
|
||||
AsJSON bool
|
||||
type listOptions struct {
|
||||
asJSON bool
|
||||
}
|
||||
|
||||
func NewListCmd() *cobra.Command {
|
||||
opts := &ListOptions{}
|
||||
func newListCmd() *cobra.Command {
|
||||
opts := &listOptions{}
|
||||
|
||||
listCmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List your Codespaces",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return List(opts)
|
||||
return list(opts)
|
||||
},
|
||||
}
|
||||
|
||||
listCmd.Flags().BoolVar(&opts.AsJSON, "json", false, "Output as JSON")
|
||||
listCmd.Flags().BoolVar(&opts.asJSON, "json", false, "Output as JSON")
|
||||
|
||||
return listCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(NewListCmd())
|
||||
rootCmd.AddCommand(newListCmd())
|
||||
}
|
||||
|
||||
func List(opts *ListOptions) error {
|
||||
func list(opts *listOptions) error {
|
||||
apiClient := api.New(os.Getenv("GITHUB_TOKEN"))
|
||||
ctx := context.Background()
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ func List(opts *ListOptions) error {
|
|||
return fmt.Errorf("error getting codespaces: %v", err)
|
||||
}
|
||||
|
||||
table := output.NewTable(os.Stdout, opts.AsJSON)
|
||||
table := output.NewTable(os.Stdout, opts.asJSON)
|
||||
table.SetHeader([]string{"Name", "Repository", "Branch", "State", "Created At"})
|
||||
for _, codespace := range codespaces {
|
||||
table.Append([]string{
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewLogsCmd() *cobra.Command {
|
||||
func newLogsCmd() *cobra.Command {
|
||||
var tail bool
|
||||
|
||||
logsCmd := &cobra.Command{
|
||||
|
|
@ -24,7 +24,7 @@ func NewLogsCmd() *cobra.Command {
|
|||
if len(args) > 0 {
|
||||
codespaceName = args[0]
|
||||
}
|
||||
return Logs(tail, codespaceName)
|
||||
return logs(tail, codespaceName)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -34,10 +34,10 @@ func NewLogsCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(NewLogsCmd())
|
||||
rootCmd.AddCommand(newLogsCmd())
|
||||
}
|
||||
|
||||
func Logs(tail bool, codespaceName string) error {
|
||||
func logs(tail bool, codespaceName string) error {
|
||||
apiClient := api.New(os.Getenv("GITHUB_TOKEN"))
|
||||
ctx := context.Background()
|
||||
log := output.NewLogger(os.Stdout, os.Stderr, false)
|
||||
|
|
@ -52,7 +52,7 @@ func Logs(tail bool, codespaceName string) error {
|
|||
return fmt.Errorf("get or choose codespace: %v", err)
|
||||
}
|
||||
|
||||
lsclient, err := codespaces.ConnectToLiveshare(ctx, log, apiClient, token, codespace)
|
||||
lsclient, err := codespaces.ConnectToLiveshare(ctx, log, apiClient, user.Login, token, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting to liveshare: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
var Version = "DEV"
|
||||
var version = "DEV"
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "ghcs",
|
||||
|
|
@ -24,7 +24,7 @@ var rootCmd = &cobra.Command{
|
|||
|
||||
Running commands requires the GITHUB_TOKEN environment variable to be set to a
|
||||
token to access the GitHub API with.`,
|
||||
Version: Version,
|
||||
Version: version,
|
||||
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if os.Getenv("GITHUB_TOKEN") == "" {
|
||||
|
|
@ -42,5 +42,5 @@ func explainError(w io.Writer, err error) {
|
|||
fmt.Fprintln(w, "Make sure to enable SSO for your organizations after creating the token.")
|
||||
return
|
||||
}
|
||||
// fmt.Fprintf(w, "%v\n", err)
|
||||
fmt.Fprintf(w, "%v\n", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,48 +19,54 @@ import (
|
|||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type PortsOptions struct {
|
||||
CodespaceName string
|
||||
AsJSON bool
|
||||
// portOptions represents the options accepted by the ports command.
|
||||
type portsOptions struct {
|
||||
// CodespaceName is the name of the codespace, optional.
|
||||
codespaceName string
|
||||
|
||||
// AsJSON dictates whether the command returns a json output or not, optional.
|
||||
asJSON bool
|
||||
}
|
||||
|
||||
func NewPortsCmd() *cobra.Command {
|
||||
opts := &PortsOptions{}
|
||||
// newPortsCmd returns a Cobra "ports" command that displays a table of available ports,
|
||||
// according to the specified flags.
|
||||
func newPortsCmd() *cobra.Command {
|
||||
opts := &portsOptions{}
|
||||
|
||||
portsCmd := &cobra.Command{
|
||||
Use: "ports",
|
||||
Short: "List ports in a Codespace",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return Ports(opts)
|
||||
return ports(opts)
|
||||
},
|
||||
}
|
||||
|
||||
portsCmd.Flags().StringVarP(&opts.CodespaceName, "codespace", "c", "", "The `name` of the Codespace to use")
|
||||
portsCmd.Flags().BoolVar(&opts.AsJSON, "json", false, "Output as JSON")
|
||||
portsCmd.Flags().StringVarP(&opts.codespaceName, "codespace", "c", "", "The `name` of the Codespace to use")
|
||||
portsCmd.Flags().BoolVar(&opts.asJSON, "json", false, "Output as JSON")
|
||||
|
||||
portsCmd.AddCommand(NewPortsPublicCmd())
|
||||
portsCmd.AddCommand(NewPortsPrivateCmd())
|
||||
portsCmd.AddCommand(NewPortsForwardCmd())
|
||||
portsCmd.AddCommand(newPortsPublicCmd())
|
||||
portsCmd.AddCommand(newPortsPrivateCmd())
|
||||
portsCmd.AddCommand(newPortsForwardCmd())
|
||||
|
||||
return portsCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(NewPortsCmd())
|
||||
rootCmd.AddCommand(newPortsCmd())
|
||||
}
|
||||
|
||||
func Ports(opts *PortsOptions) error {
|
||||
func ports(opts *portsOptions) error {
|
||||
apiClient := api.New(os.Getenv("GITHUB_TOKEN"))
|
||||
ctx := context.Background()
|
||||
log := output.NewLogger(os.Stdout, os.Stderr, opts.AsJSON)
|
||||
log := output.NewLogger(os.Stdout, os.Stderr, opts.asJSON)
|
||||
|
||||
user, err := apiClient.GetUser(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting user: %v", err)
|
||||
}
|
||||
|
||||
codespace, token, err := codespaces.GetOrChooseCodespace(ctx, apiClient, user, opts.CodespaceName)
|
||||
codespace, token, err := codespaces.GetOrChooseCodespace(ctx, apiClient, user, opts.codespaceName)
|
||||
if err != nil {
|
||||
if err == codespaces.ErrNoCodespaces {
|
||||
return err
|
||||
|
|
@ -70,7 +76,7 @@ func Ports(opts *PortsOptions) error {
|
|||
|
||||
devContainerCh := getDevContainer(ctx, apiClient, codespace)
|
||||
|
||||
liveShareClient, err := codespaces.ConnectToLiveshare(ctx, log, apiClient, token, codespace)
|
||||
liveShareClient, err := codespaces.ConnectToLiveshare(ctx, log, apiClient, user.Login, token, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to liveshare: %v", err)
|
||||
}
|
||||
|
|
@ -82,17 +88,18 @@ func Ports(opts *PortsOptions) error {
|
|||
}
|
||||
|
||||
devContainerResult := <-devContainerCh
|
||||
if devContainerResult.Err != nil {
|
||||
_, _ = log.Errorf("Failed to get port names: %v\n", devContainerResult.Err.Error())
|
||||
if devContainerResult.err != nil {
|
||||
// Warn about failure to read the devcontainer file. Not a ghcs command error.
|
||||
_, _ = log.Errorf("Failed to get port names: %v\n", devContainerResult.err.Error())
|
||||
}
|
||||
|
||||
table := output.NewTable(os.Stdout, opts.AsJSON)
|
||||
table.SetHeader([]string{"Label", "Source Port", "Destination Port", "Public", "Browse URL"})
|
||||
table := output.NewTable(os.Stdout, opts.asJSON)
|
||||
table.SetHeader([]string{"Label", "Port", "Public", "Browse URL"})
|
||||
for _, port := range ports {
|
||||
sourcePort := strconv.Itoa(port.SourcePort)
|
||||
var portName string
|
||||
if devContainerResult.DevContainer != nil {
|
||||
if attributes, ok := devContainerResult.DevContainer.PortAttributes[sourcePort]; ok {
|
||||
if devContainerResult.devContainer != nil {
|
||||
if attributes, ok := devContainerResult.devContainer.PortAttributes[sourcePort]; ok {
|
||||
portName = attributes.Label
|
||||
}
|
||||
}
|
||||
|
|
@ -100,7 +107,6 @@ func Ports(opts *PortsOptions) error {
|
|||
table.Append([]string{
|
||||
portName,
|
||||
sourcePort,
|
||||
strconv.Itoa(port.DestinationPort),
|
||||
strings.ToUpper(strconv.FormatBool(port.IsPublic)),
|
||||
fmt.Sprintf("https://%s-%s.githubpreview.dev/", codespace.Name, sourcePort),
|
||||
})
|
||||
|
|
@ -125,8 +131,8 @@ func getPorts(ctx context.Context, lsclient *liveshare.Client) (liveshare.Ports,
|
|||
}
|
||||
|
||||
type devContainerResult struct {
|
||||
DevContainer *devContainer
|
||||
Err error
|
||||
devContainer *devContainer
|
||||
err error
|
||||
}
|
||||
|
||||
type devContainer struct {
|
||||
|
|
@ -168,7 +174,8 @@ func getDevContainer(ctx context.Context, apiClient *api.API, codespace *api.Cod
|
|||
return ch
|
||||
}
|
||||
|
||||
func NewPortsPublicCmd() *cobra.Command {
|
||||
// newPortsPublicCmd returns a Cobra "ports public" subcommand, which makes a given port public.
|
||||
func newPortsPublicCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "public <codespace> <port>",
|
||||
Short: "Mark port as public",
|
||||
|
|
@ -180,7 +187,8 @@ func NewPortsPublicCmd() *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
func NewPortsPrivateCmd() *cobra.Command {
|
||||
// newPortsPrivateCmd returns a Cobra "ports private" subcommand, which makes a given port private.
|
||||
func newPortsPrivateCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "private <codespace> <port>",
|
||||
Short: "Mark port as private",
|
||||
|
|
@ -211,7 +219,7 @@ func updatePortVisibility(log *output.Logger, codespaceName, sourcePort string,
|
|||
return fmt.Errorf("error getting codespace: %v", err)
|
||||
}
|
||||
|
||||
lsclient, err := codespaces.ConnectToLiveshare(ctx, log, apiClient, token, codespace)
|
||||
lsclient, err := codespaces.ConnectToLiveshare(ctx, log, apiClient, user.Login, token, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to liveshare: %v", err)
|
||||
}
|
||||
|
|
@ -239,7 +247,9 @@ func updatePortVisibility(log *output.Logger, codespaceName, sourcePort string,
|
|||
return nil
|
||||
}
|
||||
|
||||
func NewPortsForwardCmd() *cobra.Command {
|
||||
// NewPortsForwardCmd returns a Cobra "ports forward" subcommand, which forwards a set of
|
||||
// port pairs from the codespace to localhost.
|
||||
func newPortsForwardCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "forward <codespace> <source-port>:<destination-port>",
|
||||
Short: "Forward ports",
|
||||
|
|
@ -275,7 +285,7 @@ func forwardPorts(log *output.Logger, codespaceName string, ports []string) erro
|
|||
return fmt.Errorf("error getting codespace: %v", err)
|
||||
}
|
||||
|
||||
lsclient, err := codespaces.ConnectToLiveshare(ctx, log, apiClient, token, codespace)
|
||||
lsclient, err := codespaces.ConnectToLiveshare(ctx, log, apiClient, user.Login, token, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to liveshare: %v", err)
|
||||
}
|
||||
|
|
@ -289,14 +299,14 @@ func forwardPorts(log *output.Logger, codespaceName string, ports []string) erro
|
|||
for _, portPair := range portPairs {
|
||||
pp := portPair
|
||||
|
||||
srcstr := strconv.Itoa(portPair.Src)
|
||||
if err := server.StartSharing(gctx, "share-"+srcstr, pp.Src); err != nil {
|
||||
srcstr := strconv.Itoa(portPair.src)
|
||||
if err := server.StartSharing(gctx, "share-"+srcstr, pp.src); err != nil {
|
||||
return fmt.Errorf("start sharing port: %v", err)
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
log.Println("Forwarding port: " + srcstr + " ==> " + strconv.Itoa(pp.Dst))
|
||||
portForwarder := liveshare.NewPortForwarder(lsclient, server, pp.Dst)
|
||||
log.Println("Forwarding port: " + srcstr + " ==> " + strconv.Itoa(pp.dst))
|
||||
portForwarder := liveshare.NewPortForwarder(lsclient, server, pp.dst)
|
||||
if err := portForwarder.Start(gctx); err != nil {
|
||||
return fmt.Errorf("error forwarding port: %v", err)
|
||||
}
|
||||
|
|
@ -313,16 +323,17 @@ func forwardPorts(log *output.Logger, codespaceName string, ports []string) erro
|
|||
}
|
||||
|
||||
type portPair struct {
|
||||
Src, Dst int
|
||||
src, dst int
|
||||
}
|
||||
|
||||
// getPortPairs parses a list of strings of form "%d:%d" into pairs of numbers.
|
||||
func getPortPairs(ports []string) ([]portPair, error) {
|
||||
pp := make([]portPair, 0, len(ports))
|
||||
|
||||
for _, portString := range ports {
|
||||
parts := strings.Split(portString, ":")
|
||||
if len(parts) < 2 {
|
||||
return pp, fmt.Errorf("port pair: '%v' is not valid", portString)
|
||||
return nil, fmt.Errorf("port pair: '%v' is not valid", portString)
|
||||
}
|
||||
|
||||
srcp, err := strconv.Atoi(parts[0])
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewSSHCmd() *cobra.Command {
|
||||
func newSSHCmd() *cobra.Command {
|
||||
var sshProfile, codespaceName string
|
||||
var sshServerPort int
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ func NewSSHCmd() *cobra.Command {
|
|||
Short: "SSH into a Codespace",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return SSH(sshProfile, codespaceName, sshServerPort)
|
||||
return ssh(sshProfile, codespaceName, sshServerPort)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -36,10 +36,10 @@ func NewSSHCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(NewSSHCmd())
|
||||
rootCmd.AddCommand(newSSHCmd())
|
||||
}
|
||||
|
||||
func SSH(sshProfile, codespaceName string, sshServerPort int) error {
|
||||
func ssh(sshProfile, codespaceName string, sshServerPort int) error {
|
||||
apiClient := api.New(os.Getenv("GITHUB_TOKEN"))
|
||||
ctx := context.Background()
|
||||
log := output.NewLogger(os.Stdout, os.Stderr, false)
|
||||
|
|
@ -54,7 +54,7 @@ func SSH(sshProfile, codespaceName string, sshServerPort int) error {
|
|||
return fmt.Errorf("get or choose codespace: %v", err)
|
||||
}
|
||||
|
||||
lsclient, err := codespaces.ConnectToLiveshare(ctx, log, apiClient, token, codespace)
|
||||
lsclient, err := codespaces.ConnectToLiveshare(ctx, log, apiClient, user.Login, token, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error connecting to liveshare: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
|
|
@ -25,7 +26,9 @@ func ChooseCodespace(ctx context.Context, apiClient *api.API, user *api.User) (*
|
|||
return nil, ErrNoCodespaces
|
||||
}
|
||||
|
||||
codespaces.SortByCreatedAt()
|
||||
sort.Slice(codespaces, func(i, j int) bool {
|
||||
return codespaces[i].CreatedAt > codespaces[j].CreatedAt
|
||||
})
|
||||
|
||||
codespacesByName := make(map[string]*api.Codespace)
|
||||
codespacesNames := make([]string, 0, len(codespaces))
|
||||
|
|
@ -62,16 +65,25 @@ type logger interface {
|
|||
Println(v ...interface{}) (int, error)
|
||||
}
|
||||
|
||||
func ConnectToLiveshare(ctx context.Context, log logger, apiClient *api.API, token string, codespace *api.Codespace) (client *liveshare.Client, err error) {
|
||||
func connectionReady(codespace *api.Codespace) bool {
|
||||
return codespace.Environment.Connection.SessionID != "" &&
|
||||
codespace.Environment.Connection.SessionToken != "" &&
|
||||
codespace.Environment.Connection.RelayEndpoint != "" &&
|
||||
codespace.Environment.Connection.RelaySAS != "" &&
|
||||
codespace.Environment.State == api.CodespaceEnvironmentStateAvailable
|
||||
}
|
||||
|
||||
func ConnectToLiveshare(ctx context.Context, log logger, apiClient *api.API, userLogin, token string, codespace *api.Codespace) (client *liveshare.Client, err error) {
|
||||
var startedCodespace bool
|
||||
if codespace.Environment.State != api.CodespaceEnvironmentStateAvailable {
|
||||
log.Println("Starting your codespace...")
|
||||
startedCodespace = true
|
||||
log.Print("Starting your codespace...")
|
||||
if err := apiClient.StartCodespace(ctx, token, codespace); err != nil {
|
||||
return nil, fmt.Errorf("error starting codespace: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
retries := 0
|
||||
for codespace.Environment.Connection.SessionID == "" || codespace.Environment.State != api.CodespaceEnvironmentStateAvailable {
|
||||
for retries := 0; !connectionReady(codespace); retries++ {
|
||||
if retries > 1 {
|
||||
if retries%2 == 0 {
|
||||
log.Print(".")
|
||||
|
|
@ -84,16 +96,14 @@ func ConnectToLiveshare(ctx context.Context, log logger, apiClient *api.API, tok
|
|||
return nil, errors.New("timed out while waiting for the codespace to start")
|
||||
}
|
||||
|
||||
codespace, err = apiClient.GetCodespace(ctx, token, codespace.OwnerLogin, codespace.Name)
|
||||
codespace, err = apiClient.GetCodespace(ctx, token, userLogin, codespace.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting codespace: %v", err)
|
||||
}
|
||||
|
||||
retries += 1
|
||||
}
|
||||
|
||||
if retries >= 2 {
|
||||
log.Print("\n")
|
||||
if startedCodespace {
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
log.Println("Connecting to your codespace...")
|
||||
|
|
|
|||
105
internal/codespaces/states.go
Normal file
105
internal/codespaces/states.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package codespaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/ghcs/api"
|
||||
)
|
||||
|
||||
// PostCreateStateStatus is a string value representing the different statuses a state can have.
|
||||
type PostCreateStateStatus string
|
||||
|
||||
func (p PostCreateStateStatus) String() string {
|
||||
return strings.Title(string(p))
|
||||
}
|
||||
|
||||
const (
|
||||
PostCreateStateRunning PostCreateStateStatus = "running"
|
||||
PostCreateStateSuccess PostCreateStateStatus = "succeeded"
|
||||
PostCreateStateFailed PostCreateStateStatus = "failed"
|
||||
)
|
||||
|
||||
// PostCreateState is a combination of a state and status value that is captured
|
||||
// during codespace creation.
|
||||
type PostCreateState struct {
|
||||
Name string `json:"name"`
|
||||
Status PostCreateStateStatus `json:"status"`
|
||||
}
|
||||
|
||||
// PollPostCreateStates watches for state changes in a codespace,
|
||||
// and calls the supplied poller for each batch of state changes.
|
||||
// It runs until the context is cancelled or SSH tunnel is closed.
|
||||
func PollPostCreateStates(ctx context.Context, log logger, apiClient *api.API, user *api.User, codespace *api.Codespace, poller func([]PostCreateState)) error {
|
||||
token, err := apiClient.GetCodespaceToken(ctx, user.Login, codespace.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting codespace token: %v", err)
|
||||
}
|
||||
|
||||
lsclient, err := ConnectToLiveshare(ctx, log, apiClient, user.Login, token, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to liveshare: %v", err)
|
||||
}
|
||||
|
||||
tunnelPort, connClosed, err := MakeSSHTunnel(ctx, lsclient, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("make ssh tunnel: %v", err)
|
||||
}
|
||||
|
||||
t := time.NewTicker(1 * time.Second)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case err := <-connClosed:
|
||||
return fmt.Errorf("connection closed: %v", err)
|
||||
case <-t.C:
|
||||
states, err := getPostCreateOutput(ctx, tunnelPort, codespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get post create output: %v", err)
|
||||
}
|
||||
|
||||
poller(states)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPostCreateOutput(ctx context.Context, tunnelPort int, codespace *api.Codespace) ([]PostCreateState, error) {
|
||||
stdout, err := RunCommand(
|
||||
ctx, tunnelPort, sshDestination(codespace),
|
||||
"cat /workspaces/.codespaces/shared/postCreateOutput.json",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("run command: %v", err)
|
||||
}
|
||||
defer stdout.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(stdout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read output: %v", err)
|
||||
}
|
||||
|
||||
var output struct {
|
||||
Steps []PostCreateState `json:"steps"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &output); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal output: %v", err)
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue