ghcs ports v1
This commit is contained in:
parent
349d3f382e
commit
3c42ab8f7a
3 changed files with 282 additions and 2 deletions
49
api/api.go
49
api/api.go
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -105,7 +106,7 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error
|
|||
|
||||
type Codespaces []*Codespace
|
||||
|
||||
func (c Codespaces) SortByRecent() {
|
||||
func (c Codespaces) SortByCreatedAt() {
|
||||
sort.Slice(c, func(i, j int) bool {
|
||||
return c[i].CreatedAt > c[j].CreatedAt
|
||||
})
|
||||
|
|
@ -397,6 +398,52 @@ func (a *API) DeleteCodespace(ctx context.Context, user *User, token, codespaceN
|
|||
return nil
|
||||
}
|
||||
|
||||
type getCodespaceRepositoryContentsResponse struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (a *API) GetCodespaceRepositoryContents(ctx context.Context, codespace *Codespace, path string) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, githubAPI+"/repos/"+codespace.RepositoryNWO+"/contents/"+path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating request: %v", err)
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("ref", codespace.Branch)
|
||||
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)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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 getCodespaceRepositoryContentsResponse
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling response: %v", err)
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(response.Content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding content: %v", err)
|
||||
}
|
||||
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
func (a *API) setHeaders(req *http.Request) {
|
||||
req.Header.Set("Authorization", "Bearer "+a.token)
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
|
|
|
|||
233
cmd/ghcs/ports.go
Normal file
233
cmd/ghcs/ports.go
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/github/ghcs/api"
|
||||
"github.com/github/go-liveshare"
|
||||
"github.com/muhammadmuzzammil1998/jsonc"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewPortsCmd() *cobra.Command {
|
||||
portsCmd := &cobra.Command{
|
||||
Use: "ports",
|
||||
Short: "ports",
|
||||
Long: "ports",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return Ports()
|
||||
},
|
||||
}
|
||||
|
||||
return portsCmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(NewPortsCmd())
|
||||
}
|
||||
|
||||
func Ports() 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.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)
|
||||
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),
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
fmt.Println("Loading ports...")
|
||||
ports, err := getPorts(ctx, liveShareClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting ports: %v", err)
|
||||
}
|
||||
|
||||
devContainerResult := <-devContainerCh
|
||||
if devContainerResult.Err != nil {
|
||||
fmt.Println("Failed to get port names: %v", devContainerResult.Err.Error())
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Label", "Source Port", "Destination 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 {
|
||||
portName = attributes.Label
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
table.Render()
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func getPorts(ctx context.Context, liveShareClient *liveshare.Client) (liveshare.Ports, error) {
|
||||
server, err := liveShareClient.NewServer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating server: %v", err)
|
||||
}
|
||||
|
||||
ports, err := server.GetSharedServers(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting shared servers: %v", err)
|
||||
}
|
||||
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
type devContainerResult struct {
|
||||
DevContainer *devContainer
|
||||
Err error
|
||||
}
|
||||
|
||||
type devContainer struct {
|
||||
PortAttributes map[string]portAttribute `json:"portsAttributes"`
|
||||
}
|
||||
|
||||
type portAttribute struct {
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
func getDevContainer(ctx context.Context, apiClient *api.API, codespace *api.Codespace) <-chan devContainerResult {
|
||||
ch := make(chan devContainerResult)
|
||||
go func() {
|
||||
contents, err := apiClient.GetCodespaceRepositoryContents(ctx, codespace, ".devcontainer/devcontainer.json")
|
||||
if err != nil {
|
||||
ch <- devContainerResult{nil, fmt.Errorf("error getting content: %v", err)}
|
||||
return
|
||||
}
|
||||
|
||||
if contents == nil {
|
||||
ch <- devContainerResult{nil, nil}
|
||||
return
|
||||
}
|
||||
|
||||
convertedJSON := jsonc.ToJSON(contents)
|
||||
if !jsonc.Valid(convertedJSON) {
|
||||
ch <- devContainerResult{nil, errors.New("failed to convert json to standard json")}
|
||||
return
|
||||
}
|
||||
|
||||
var container devContainer
|
||||
if err := json.Unmarshal(convertedJSON, &container); err != nil {
|
||||
ch <- devContainerResult{nil, fmt.Errorf("error unmarshaling: %v", err)}
|
||||
return
|
||||
}
|
||||
|
||||
ch <- devContainerResult{&container, nil}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ func SSH(sshProfile string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
codespaces.SortByRecent()
|
||||
codespaces.SortByCreatedAt()
|
||||
|
||||
codespacesByName := make(map[string]*api.Codespace)
|
||||
codespacesNames := make([]string, 0, len(codespaces))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue