From 798413848b0c8b211caf1fd96fa3bc5b74baef29 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Sat, 17 Jul 2021 20:32:47 -0400 Subject: [PATCH] Portfowarding private/public/forward now supported --- api/api.go | 4 +- cmd/ghcs/delete.go | 3 +- cmd/ghcs/ports.go | 246 ++++++++++++++++++++---------- cmd/ghcs/ssh.go | 96 ++---------- internal/codespaces/codespaces.go | 110 +++++++++++++ 5 files changed, 286 insertions(+), 173 deletions(-) create mode 100644 internal/codespaces/codespaces.go diff --git a/api/api.go b/api/api.go index 00ff6b056..8bf5e155d 100644 --- a/api/api.go +++ b/api/api.go @@ -177,7 +177,7 @@ type getCodespaceTokenResponse struct { RepositoryToken string `json:"repository_token"` } -func (a *API) GetCodespaceToken(ctx context.Context, codespace *Codespace) (string, error) { +func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName string) (string, error) { reqBody, err := json.Marshal(getCodespaceTokenRequest{true}) if err != nil { return "", fmt.Errorf("error preparing request body: %v", err) @@ -185,7 +185,7 @@ func (a *API) GetCodespaceToken(ctx context.Context, codespace *Codespace) (stri req, err := http.NewRequest( http.MethodPost, - githubAPI+"/vscs_internal/user/"+codespace.OwnerLogin+"/codespaces/"+codespace.Name+"/token", + githubAPI+"/vscs_internal/user/"+ownerLogin+"/codespaces/"+codespaceName+"/token", bytes.NewBuffer(reqBody), ) if err != nil { diff --git a/cmd/ghcs/delete.go b/cmd/ghcs/delete.go index e5cd34a94..a24374a4c 100644 --- a/cmd/ghcs/delete.go +++ b/cmd/ghcs/delete.go @@ -39,8 +39,7 @@ func Delete(codespaceName string) error { return fmt.Errorf("error getting user: %v", err) } - codespace := api.Codespace{OwnerLogin: user.Login, Name: codespaceName} - token, err := apiClient.GetCodespaceToken(ctx, &codespace) + token, err := apiClient.GetCodespaceToken(ctx, user.Login, codespaceName) if err != nil { return fmt.Errorf("error getting codespace token: %v", err) } diff --git a/cmd/ghcs/ports.go b/cmd/ghcs/ports.go index 9d58fd491..7766f230d 100644 --- a/cmd/ghcs/ports.go +++ b/cmd/ghcs/ports.go @@ -8,10 +8,9 @@ import ( "os" "strconv" "strings" - "time" - "github.com/AlecAivazis/survey/v2" "github.com/github/ghcs/api" + "github.com/github/ghcs/internal/codespaces" "github.com/github/go-liveshare" "github.com/muhammadmuzzammil1998/jsonc" "github.com/olekukonko/tablewriter" @@ -28,6 +27,9 @@ func NewPortsCmd() *cobra.Command { }, } + portsCmd.AddCommand(NewPortsPublicCmd()) + portsCmd.AddCommand(NewPortsPrivateCmd()) + portsCmd.AddCommand(NewPortsForwardCmd()) return portsCmd } @@ -44,98 +46,25 @@ func Ports() error { return fmt.Errorf("error getting user: %v", err) } - codespaces, err := apiClient.ListCodespaces(ctx, user) + codespace, err := codespaces.ChooseCodespace(ctx, apiClient, user) if err != nil { - return fmt.Errorf("error getting codespaces: %v", err) + if err == codespaces.ErrNoCodespaces { + fmt.Println(err.Error()) + return nil + } + return fmt.Errorf("error choosing codespace: %v", err) } - if len(codespaces) == 0 { - fmt.Println("You have no codespaces.") - return nil - } - - codespaces.SortByCreatedAt() - - 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) - } - - portsSurvey := []*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(portsSurvey, &answers); err != nil { - return fmt.Errorf("error getting answers: %v", err) - } - - codespace := codespacesByName[answers.Codespace] devContainerCh := getDevContainer(ctx, apiClient, codespace) - token, err := apiClient.GetCodespaceToken(ctx, codespace) + token, err := apiClient.GetCodespaceToken(ctx, user.Login, codespace.Name) 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 == 30 { - return errors.New("timed out while waiting for the codespace to start") - } - - 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), - ) + liveShareClient, err := codespaces.ConnectToLiveshare(ctx, apiClient, token, codespace) 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) + return fmt.Errorf("error connecting to liveshare: %v", err) } fmt.Println("Loading ports...") @@ -144,6 +73,11 @@ func Ports() error { return fmt.Errorf("error getting ports: %v", err) } + if len(ports) == 0 { + fmt.Println("This codespace has no open ports") + return nil + } + devContainerResult := <-devContainerCh if devContainerResult.Err != nil { fmt.Println("Failed to get port names: %v", devContainerResult.Err.Error()) @@ -231,3 +165,147 @@ func getDevContainer(ctx context.Context, apiClient *api.API, codespace *api.Cod }() return ch } + +func NewPortsPublicCmd() *cobra.Command { + return &cobra.Command{ + Use: "public", + Short: "public", + Long: "public", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return errors.New("[codespace_name] [source] port number are required.") + } + + return updatePortVisibility(args[0], args[1], true) + }, + } +} + +func NewPortsPrivateCmd() *cobra.Command { + return &cobra.Command{ + Use: "private", + Short: "private", + Long: "private", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return errors.New("[codespace_name] [source] port number are required.") + } + + return updatePortVisibility(args[0], args[1], false) + }, + } +} + +func updatePortVisibility(codespaceName, sourcePort string, public bool) error { + ctx := context.Background() + apiClient := api.New(os.Getenv("GITHUB_TOKEN")) + + user, err := apiClient.GetUser(ctx) + if err != nil { + return fmt.Errorf("error getting user: %v", err) + } + + token, err := apiClient.GetCodespaceToken(ctx, user.Login, codespaceName) + if err != nil { + return fmt.Errorf("error getting codespace token: %v", err) + } + + codespace, err := apiClient.GetCodespace(ctx, token, user.Login, codespaceName) + if err != nil { + return fmt.Errorf("error getting codespace: %v", err) + } + + liveShareClient, err := codespaces.ConnectToLiveshare(ctx, apiClient, token, codespace) + if err != nil { + return fmt.Errorf("error connecting to liveshare: %v", err) + } + + server, err := liveShareClient.NewServer() + if err != nil { + return fmt.Errorf("error creating server: %v", err) + } + + port, err := strconv.Atoi(sourcePort) + if err != nil { + return fmt.Errorf("error reading port number: %v", err) + } + + if err := server.UpdateSharedVisibility(ctx, port, public); err != nil { + return fmt.Errorf("error update port to public: %v", err) + } + + state := "PUBLIC" + if public == false { + state = "PRIVATE" + } + + fmt.Println(fmt.Sprintf("Port %s is now %s.", sourcePort, state)) + + return nil +} + +func NewPortsForwardCmd() *cobra.Command { + return &cobra.Command{ + Use: "forward", + Short: "forward", + Long: "forward", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 3 { + return errors.New("[codespace_name] [source] [dst] port number are required.") + } + return forwardPort(args[0], args[1], args[2]) + }, + } +} + +func forwardPort(codespaceName, sourcePort, destPort string) error { + ctx := context.Background() + apiClient := api.New(os.Getenv("GITHUB_TOKEN")) + + user, err := apiClient.GetUser(ctx) + if err != nil { + return fmt.Errorf("error getting user: %v", err) + } + + token, err := apiClient.GetCodespaceToken(ctx, user.Login, codespaceName) + if err != nil { + return fmt.Errorf("error getting codespace token: %v", err) + } + + codespace, err := apiClient.GetCodespace(ctx, token, user.Login, codespaceName) + if err != nil { + return fmt.Errorf("error getting codespace: %v", err) + } + + liveShareClient, err := codespaces.ConnectToLiveshare(ctx, apiClient, token, codespace) + if err != nil { + return fmt.Errorf("error connecting to liveshare: %v", err) + } + + server, err := liveShareClient.NewServer() + if err != nil { + return fmt.Errorf("error creating server: %v", err) + } + + sourcePortInt, err := strconv.Atoi(sourcePort) + if err != nil { + return fmt.Errorf("error reading source port: %v", err) + } + + dstPortInt, err := strconv.Atoi(destPort) + if err != nil { + return fmt.Errorf("error reading destination port: %v", err) + } + + if err := server.StartSharing(ctx, "share-"+sourcePort, sourcePortInt); err != nil { + return fmt.Errorf("error sharing source port: %v", err) + } + + fmt.Println("Forwarding port: " + sourcePort + " -> " + destPort) + portForwarder := liveshare.NewLocalPortForwarder(liveShareClient, server, dstPortInt) + if err := portForwarder.Start(ctx); err != nil { + return fmt.Errorf("error forwarding port: %v", err) + } + + return nil +} diff --git a/cmd/ghcs/ssh.go b/cmd/ghcs/ssh.go index 39019054f..50196dd07 100644 --- a/cmd/ghcs/ssh.go +++ b/cmd/ghcs/ssh.go @@ -3,7 +3,6 @@ package main import ( "bufio" "context" - "errors" "fmt" "math/rand" "os" @@ -12,8 +11,8 @@ import ( "strings" "time" - "github.com/AlecAivazis/survey/v2" "github.com/github/ghcs/api" + "github.com/github/ghcs/internal/codespaces" "github.com/github/go-liveshare" "github.com/spf13/cobra" ) @@ -48,97 +47,24 @@ func SSH(sshProfile string) error { return fmt.Errorf("error getting user: %v", err) } - codespaces, err := apiClient.ListCodespaces(ctx, user) + codespace, err := codespaces.ChooseCodespace(ctx, apiClient, user) if err != nil { - return fmt.Errorf("error getting codespaces: %v", err) + if err == codespaces.ErrNoCodespaces { + fmt.Println(err.Error()) + return nil + } + + return fmt.Errorf("error choosing codespace: %v", err) } - if len(codespaces) == 0 { - fmt.Println("You have no codespaces.") - return nil - } - - codespaces.SortByCreatedAt() - - 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) + token, err := apiClient.GetCodespaceToken(ctx, user.Login, codespace.Name) 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 == 30 { - return errors.New("timed out while waiting for the codespace to start") - } - - 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), - ) + liveShareClient, err := codespaces.ConnectToLiveshare(ctx, apiClient, token, codespace) 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) + return fmt.Errorf("error connecting to liveshare: %v", err) } terminal, err := liveShareClient.NewTerminal() diff --git a/internal/codespaces/codespaces.go b/internal/codespaces/codespaces.go new file mode 100644 index 000000000..be290fab1 --- /dev/null +++ b/internal/codespaces/codespaces.go @@ -0,0 +1,110 @@ +package codespaces + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/AlecAivazis/survey/v2" + "github.com/github/ghcs/api" + "github.com/github/go-liveshare" +) + +var ( + ErrNoCodespaces = errors.New("You have no codespaces.") +) + +func ChooseCodespace(ctx context.Context, apiClient *api.API, user *api.User) (*api.Codespace, error) { + codespaces, err := apiClient.ListCodespaces(ctx, user) + if err != nil { + return nil, fmt.Errorf("error getting codespaces: %v", err) + } + + if len(codespaces) == 0 { + return nil, ErrNoCodespaces + } + + codespaces.SortByCreatedAt() + + 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 nil, fmt.Errorf("error getting answers: %v", err) + } + + codespace := codespacesByName[answers.Codespace] + return codespace, nil +} + +func ConnectToLiveshare(ctx context.Context, apiClient *api.API, token string, codespace *api.Codespace) (client *liveshare.Client, err error) { + if codespace.Environment.State != api.CodespaceEnvironmentStateAvailable { + fmt.Println("Starting your codespace...") // TODO(josebalius): better way of notifying of events + 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 { + if retries > 1 { + if retries%2 == 0 { + fmt.Print(".") + } + + time.Sleep(1 * time.Second) + } + + if retries == 30 { + return nil, errors.New("timed out while waiting for the codespace to start") + } + + codespace, err = apiClient.GetCodespace(ctx, token, codespace.OwnerLogin, codespace.Name) + if err != nil { + return nil, 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 nil, fmt.Errorf("error creating live share: %v", err) + } + + liveShareClient := liveShare.NewClient() + if err := liveShareClient.Join(ctx); err != nil { + return nil, fmt.Errorf("error joining liveshare client: %v", err) + } + + return liveShareClient, nil +}