// 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" "encoding/base64" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "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"` } 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) } defer resp.Body.Close() 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, jsonErrorResponse(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 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) } 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) } defer resp.Body.Close() 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, jsonErrorResponse(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 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"` RelayEndpoint string `json:"relayEndpoint"` RelaySAS string `json:"relaySas"` } 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, ) 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) } defer resp.Body.Close() 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, jsonErrorResponse(b) } var response struct { Codespaces []*Codespace `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, ownerLogin, codespaceName string) (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/"+ownerLogin+"/codespaces/"+codespaceName+"/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) } 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 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) } defer resp.Body.Close() 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, jsonErrorResponse(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) 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 typically a numeric code (not an error message, nor JSON). if len(b) > 100 { b = append(b[:97], "..."...) } if resp.StatusCode == http.StatusServiceUnavailable && strings.TrimSpace(string(b)) == "7" { // HTTP 503 with error code 7 (EnvironmentNotShutdown) is benign. // Ignore it. } else { return fmt.Errorf("failed to start codespace: %s", b) } } 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) } 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) } return response.Current, nil } 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) ([]*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) } 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) } defer resp.Body.Close() 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, 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 } 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, branch, location string) (*Codespace, error) { requestBody, err := json.Marshal(createCodespaceRequest{repository.ID, branch, location, sku}) 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) } defer resp.Body.Close() 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, jsonErrorResponse(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) } 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 jsonErrorResponse(b) } 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) } defer resp.Body.Close() 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, jsonErrorResponse(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") }