Latest and greatest
This commit is contained in:
commit
4a0eaa3da5
6 changed files with 956 additions and 0 deletions
403
api/api.go
Normal file
403
api/api.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
147
cmd/ghcs/create.go
Normal file
147
cmd/ghcs/create.go
Normal file
|
|
@ -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
|
||||
}
|
||||
55
cmd/ghcs/delete.go
Normal file
55
cmd/ghcs/delete.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
60
cmd/ghcs/list.go
Normal file
60
cmd/ghcs/list.go
Normal file
|
|
@ -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
|
||||
}
|
||||
29
cmd/ghcs/main.go
Normal file
29
cmd/ghcs/main.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
262
cmd/ghcs/ssh.go
Normal file
262
cmd/ghcs/ssh.go
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue