Skip to content

Commit 52e16df

Browse files
authored
Merge pull request cli#4446 from cli/jg/get-codespace-api
codespace: switch `API.GetCodespace` to new API endpoint
2 parents 616d6c2 + b2ff4c3 commit 52e16df

File tree

11 files changed

+67
-239
lines changed

11 files changed

+67
-239
lines changed

internal/codespaces/api/api.go

Lines changed: 22 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,16 @@ type Codespace struct {
151151
Name string `json:"name"`
152152
CreatedAt string `json:"created_at"`
153153
LastUsedAt string `json:"last_used_at"`
154+
State string `json:"state"`
154155
Branch string `json:"branch"`
155156
RepositoryName string `json:"repository_name"`
156157
RepositoryNWO string `json:"repository_nwo"`
157158
OwnerLogin string `json:"owner_login"`
158159
Environment CodespaceEnvironment `json:"environment"`
159160
}
160161

162+
const CodespaceStateProvisioned = "provisioned"
163+
161164
type CodespaceEnvironment struct {
162165
State string `json:"state"`
163166
Connection CodespaceEnvironmentConnection `json:"connection"`
@@ -220,77 +223,28 @@ func (a *API) ListCodespaces(ctx context.Context) ([]*Codespace, error) {
220223
return response.Codespaces, nil
221224
}
222225

223-
// getCodespaceTokenRequest is the request body for the get codespace token endpoint.
224-
type getCodespaceTokenRequest struct {
225-
MintRepositoryToken bool `json:"mint_repository_token"`
226-
}
227-
228-
type getCodespaceTokenResponse struct {
229-
RepositoryToken string `json:"repository_token"`
230-
}
231-
232-
// ErrNotProvisioned is returned by GetCodespacesToken to indicate that the
233-
// creation of a codespace is not yet complete and that the caller should try again.
234-
var ErrNotProvisioned = errors.New("codespace not provisioned")
235-
236-
// GetCodespaceToken returns a codespace token for the user.
237-
func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName string) (string, error) {
238-
reqBody, err := json.Marshal(getCodespaceTokenRequest{true})
239-
if err != nil {
240-
return "", fmt.Errorf("error preparing request body: %w", err)
241-
}
242-
243-
req, err := http.NewRequest(
244-
http.MethodPost,
245-
a.githubAPI+"/vscs_internal/user/"+ownerLogin+"/codespaces/"+codespaceName+"/token",
246-
bytes.NewBuffer(reqBody),
247-
)
248-
if err != nil {
249-
return "", fmt.Errorf("error creating request: %w", err)
250-
}
251-
252-
a.setHeaders(req)
253-
resp, err := a.do(ctx, req, "/vscs_internal/user/*/codespaces/*/token")
254-
if err != nil {
255-
return "", fmt.Errorf("error making request: %w", err)
256-
}
257-
defer resp.Body.Close()
258-
259-
b, err := ioutil.ReadAll(resp.Body)
260-
if err != nil {
261-
return "", fmt.Errorf("error reading response body: %w", err)
262-
}
263-
264-
if resp.StatusCode != http.StatusOK {
265-
if resp.StatusCode == http.StatusUnprocessableEntity {
266-
return "", ErrNotProvisioned
267-
}
268-
269-
return "", jsonErrorResponse(b)
270-
}
271-
272-
var response getCodespaceTokenResponse
273-
if err := json.Unmarshal(b, &response); err != nil {
274-
return "", fmt.Errorf("error unmarshaling response: %w", err)
275-
}
276-
277-
return response.RepositoryToken, nil
278-
}
279-
280-
// GetCodespace returns a codespace for the user.
281-
func (a *API) GetCodespace(ctx context.Context, token, owner, codespace string) (*Codespace, error) {
226+
// GetCodespace returns the user codespace based on the provided name.
227+
// If the codespace is not found, an error is returned.
228+
// If includeConnection is true, it will return the connection information for the codespace.
229+
func (a *API) GetCodespace(ctx context.Context, codespaceName string, includeConnection bool) (*Codespace, error) {
282230
req, err := http.NewRequest(
283231
http.MethodGet,
284-
a.githubAPI+"/vscs_internal/user/"+owner+"/codespaces/"+codespace,
232+
a.githubAPI+"/user/codespaces/"+codespaceName,
285233
nil,
286234
)
287235
if err != nil {
288236
return nil, fmt.Errorf("error creating request: %w", err)
289237
}
290238

291-
// TODO: use a.setHeaders()
292-
req.Header.Set("Authorization", "Bearer "+token)
293-
resp, err := a.do(ctx, req, "/vscs_internal/user/*/codespaces/*")
239+
if includeConnection {
240+
q := req.URL.Query()
241+
q.Add("internal", "true")
242+
q.Add("refresh", "true")
243+
req.URL.RawQuery = q.Encode()
244+
}
245+
246+
a.setHeaders(req)
247+
resp, err := a.do(ctx, req, "/user/codespaces/*")
294248
if err != nil {
295249
return nil, fmt.Errorf("error making request: %w", err)
296250
}
@@ -437,7 +391,6 @@ func (a *API) GetCodespacesMachines(ctx context.Context, repoID int, branch, loc
437391

438392
// CreateCodespaceParams are the required parameters for provisioning a Codespace.
439393
type CreateCodespaceParams struct {
440-
User string
441394
RepositoryID int
442395
Branch, Machine, Location string
443396
}
@@ -464,19 +417,14 @@ func (a *API) CreateCodespace(ctx context.Context, params *CreateCodespaceParams
464417
case <-ctx.Done():
465418
return nil, ctx.Err()
466419
case <-ticker.C:
467-
token, err := a.GetCodespaceToken(ctx, params.User, codespace.Name)
420+
codespace, err = a.GetCodespace(ctx, codespace.Name, false)
468421
if err != nil {
469-
if err == ErrNotProvisioned {
470-
// Do nothing. We expect this to fail until the codespace is provisioned
471-
continue
472-
}
473-
474-
return nil, fmt.Errorf("failed to get codespace token: %w", err)
422+
return nil, fmt.Errorf("failed to get codespace: %w", err)
475423
}
476424

477-
codespace, err = a.GetCodespace(ctx, token, params.User, codespace.Name)
478-
if err != nil {
479-
return nil, fmt.Errorf("failed to get codespace: %w", err)
425+
// we continue to poll until the codespace shows as provisioned
426+
if codespace.State != CodespaceStateProvisioned {
427+
continue
480428
}
481429

482430
return codespace, nil

internal/codespaces/codespaces.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,13 @@ func connectionReady(codespace *api.Codespace) bool {
2424
}
2525

2626
type apiClient interface {
27-
GetCodespace(ctx context.Context, token, user, name string) (*api.Codespace, error)
28-
GetCodespaceToken(ctx context.Context, user, codespace string) (string, error)
27+
GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
2928
StartCodespace(ctx context.Context, name string) error
3029
}
3130

3231
// ConnectToLiveshare waits for a Codespace to become running,
3332
// and connects to it using a Live Share session.
34-
func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, userLogin, token string, codespace *api.Codespace) (*liveshare.Session, error) {
33+
func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, codespace *api.Codespace) (*liveshare.Session, error) {
3534
var startedCodespace bool
3635
if codespace.Environment.State != api.CodespaceEnvironmentStateAvailable {
3736
startedCodespace = true
@@ -55,7 +54,7 @@ func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, us
5554
}
5655

5756
var err error
58-
codespace, err = apiClient.GetCodespace(ctx, token, userLogin, codespace.Name)
57+
codespace, err = apiClient.GetCodespace(ctx, codespace.Name, true)
5958
if err != nil {
6059
return nil, fmt.Errorf("error getting codespace: %w", err)
6160
}

internal/codespaces/states.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,8 @@ type PostCreateState struct {
3636
// PollPostCreateStates watches for state changes in a codespace,
3737
// and calls the supplied poller for each batch of state changes.
3838
// It runs until it encounters an error, including cancellation of the context.
39-
func PollPostCreateStates(ctx context.Context, log logger, apiClient apiClient, user *api.User, codespace *api.Codespace, poller func([]PostCreateState)) (err error) {
40-
token, err := apiClient.GetCodespaceToken(ctx, user.Login, codespace.Name)
41-
if err != nil {
42-
return fmt.Errorf("getting codespace token: %w", err)
43-
}
44-
45-
session, err := ConnectToLiveshare(ctx, log, apiClient, user.Login, token, codespace)
39+
func PollPostCreateStates(ctx context.Context, log logger, apiClient apiClient, codespace *api.Codespace, poller func([]PostCreateState)) (err error) {
40+
session, err := ConnectToLiveshare(ctx, log, apiClient, codespace)
4641
if err != nil {
4742
return fmt.Errorf("connect to Live Share: %w", err)
4843
}

pkg/cmd/codespace/common.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ func NewApp(logger *output.Logger, apiClient apiClient) *App {
3333
//go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient
3434
type apiClient interface {
3535
GetUser(ctx context.Context) (*api.User, error)
36-
GetCodespaceToken(ctx context.Context, user, name string) (string, error)
37-
GetCodespace(ctx context.Context, token, user, name string) (*api.Codespace, error)
36+
GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
3837
ListCodespaces(ctx context.Context) ([]*api.Codespace, error)
3938
DeleteCodespace(ctx context.Context, name string) error
4039
StartCodespace(ctx context.Context, name string) error
@@ -96,35 +95,24 @@ func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (
9695
}
9796

9897
// getOrChooseCodespace prompts the user to choose a codespace if the codespaceName is empty.
99-
// It then fetches the codespace token and the codespace record.
100-
func getOrChooseCodespace(ctx context.Context, apiClient apiClient, user *api.User, codespaceName string) (codespace *api.Codespace, token string, err error) {
98+
// It then fetches the codespace record with full connection details.
99+
func getOrChooseCodespace(ctx context.Context, apiClient apiClient, codespaceName string) (codespace *api.Codespace, err error) {
101100
if codespaceName == "" {
102101
codespace, err = chooseCodespace(ctx, apiClient)
103102
if err != nil {
104103
if err == errNoCodespaces {
105-
return nil, "", err
104+
return nil, err
106105
}
107-
return nil, "", fmt.Errorf("choosing codespace: %w", err)
108-
}
109-
codespaceName = codespace.Name
110-
111-
token, err = apiClient.GetCodespaceToken(ctx, user.Login, codespaceName)
112-
if err != nil {
113-
return nil, "", fmt.Errorf("getting codespace token: %w", err)
106+
return nil, fmt.Errorf("choosing codespace: %w", err)
114107
}
115108
} else {
116-
token, err = apiClient.GetCodespaceToken(ctx, user.Login, codespaceName)
109+
codespace, err = apiClient.GetCodespace(ctx, codespaceName, true)
117110
if err != nil {
118-
return nil, "", fmt.Errorf("getting codespace token for given codespace: %w", err)
119-
}
120-
121-
codespace, err = apiClient.GetCodespace(ctx, token, user.Login, codespaceName)
122-
if err != nil {
123-
return nil, "", fmt.Errorf("getting full codespace details: %w", err)
111+
return nil, fmt.Errorf("getting full codespace details: %w", err)
124112
}
125113
}
126114

127-
return codespace, token, nil
115+
return codespace, nil
128116
}
129117

130118
func safeClose(closer io.Closer, err *error) {

pkg/cmd/codespace/create.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
8181

8282
a.logger.Print("Creating your codespace...")
8383
codespace, err := a.apiClient.CreateCodespace(ctx, &api.CreateCodespaceParams{
84-
User: userResult.User.Login,
8584
RepositoryID: repository.ID,
8685
Branch: branch,
8786
Machine: machine,
@@ -157,7 +156,7 @@ func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, us
157156
}
158157
}
159158

160-
err := codespaces.PollPostCreateStates(ctx, log, apiClient, user, codespace, poller)
159+
err := codespaces.PollPostCreateStates(ctx, log, apiClient, codespace, poller)
161160
if err != nil {
162161
if errors.Is(err, context.Canceled) && breakNextState {
163162
return nil // we cancelled the context to stop polling, we can ignore the error

pkg/cmd/codespace/delete.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,7 @@ func newDeleteCmd(app *App) *cobra.Command {
5858
return deleteCmd
5959
}
6060

61-
func (a *App) Delete(ctx context.Context, opts deleteOptions) error {
62-
user, err := a.apiClient.GetUser(ctx)
63-
if err != nil {
64-
return fmt.Errorf("error getting user: %w", err)
65-
}
66-
61+
func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
6762
var codespaces []*api.Codespace
6863
nameFilter := opts.codespaceName
6964
if nameFilter == "" {
@@ -80,12 +75,7 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) error {
8075
nameFilter = c.Name
8176
}
8277
} else {
83-
token, err := a.apiClient.GetCodespaceToken(ctx, user.Login, nameFilter)
84-
if err != nil {
85-
return fmt.Errorf("error getting codespace token: %w", err)
86-
}
87-
88-
codespace, err := a.apiClient.GetCodespace(ctx, token, user.Login, nameFilter)
78+
codespace, err := a.apiClient.GetCodespace(ctx, nameFilter, false)
8979
if err != nil {
9080
return fmt.Errorf("error fetching codespace information: %w", err)
9181
}

pkg/cmd/codespace/delete_test.go

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -168,19 +168,7 @@ func TestDelete(t *testing.T) {
168168
return tt.codespaces, nil
169169
}
170170
} else {
171-
apiMock.GetCodespaceTokenFunc = func(_ context.Context, userLogin, name string) (string, error) {
172-
if userLogin != user.Login {
173-
return "", fmt.Errorf("unexpected user %q", userLogin)
174-
}
175-
return "CS_TOKEN", nil
176-
}
177-
apiMock.GetCodespaceFunc = func(_ context.Context, token, userLogin, name string) (*api.Codespace, error) {
178-
if userLogin != user.Login {
179-
return nil, fmt.Errorf("unexpected user %q", userLogin)
180-
}
181-
if token != "CS_TOKEN" {
182-
return nil, fmt.Errorf("unexpected token %q", token)
183-
}
171+
apiMock.GetCodespaceFunc = func(_ context.Context, name string, includeConnection bool) (*api.Codespace, error) {
184172
return tt.codespaces[0], nil
185173
}
186174
}
@@ -206,9 +194,6 @@ func TestDelete(t *testing.T) {
206194
if (err != nil) != tt.wantErr {
207195
t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr)
208196
}
209-
if n := len(apiMock.GetUserCalls()); n != 1 {
210-
t.Errorf("GetUser invoked %d times, expected %d", n, 1)
211-
}
212197
var gotDeleted []string
213198
for _, delArgs := range apiMock.DeleteCodespaceCalls() {
214199
gotDeleted = append(gotDeleted, delArgs.Name)

pkg/cmd/codespace/logs.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err
4646
authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login)
4747
}()
4848

49-
codespace, token, err := getOrChooseCodespace(ctx, a.apiClient, user, codespaceName)
49+
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
5050
if err != nil {
5151
return fmt.Errorf("get or choose codespace: %w", err)
5252
}
5353

54-
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, user.Login, token, codespace)
54+
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, codespace)
5555
if err != nil {
5656
return fmt.Errorf("connecting to Live Share: %w", err)
5757
}

0 commit comments

Comments
 (0)