X Tutup
Skip to content

Commit d2d2199

Browse files
committed
Move ProvisionCodespace to API client
- Make CreateCodespace private along with its errors
1 parent 8c5330d commit d2d2199

File tree

4 files changed

+86
-91
lines changed

4 files changed

+86
-91
lines changed

cmd/ghcs/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func create(opts *createOptions) error {
8383

8484
log.Println("Creating your codespace...")
8585

86-
codespace, err := codespaces.Provision(ctx, log, apiClient, &codespaces.ProvisionParams{
86+
codespace, err := apiClient.ProvisionCodespace(ctx, log, &api.ProvisionCodespaceParams{
8787
User: userResult.User,
8888
Repository: repository,
8989
Branch: branch,

internal/api/api.go

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"net/http"
3737
"strconv"
3838
"strings"
39+
"time"
3940

4041
"github.com/opentracing/opentracing-go"
4142
)
@@ -402,16 +403,91 @@ func (a *API) GetCodespacesSKUs(ctx context.Context, user *User, repository *Rep
402403
return response.SKUs, nil
403404
}
404405

406+
// ProvisionCodespaceParams are the required parameters for provisioning a Codespace.
407+
type ProvisionCodespaceParams struct {
408+
User *User
409+
Repository *Repository
410+
Branch, Machine, Location string
411+
}
412+
413+
type logger interface {
414+
Print(v ...interface{}) (int, error)
415+
Println(v ...interface{}) (int, error)
416+
}
417+
418+
// ProvisionCodespace creates a codespace with the given parameters and handles polling in the case
419+
// of initial creation failures.
420+
func (a *API) ProvisionCodespace(ctx context.Context, log logger, params *ProvisionCodespaceParams) (*Codespace, error) {
421+
codespace, err := a.createCodespace(
422+
ctx, params.User, params.Repository, params.Machine, params.Branch, params.Location,
423+
)
424+
if err != nil {
425+
// This error is returned by the API when the initial creation fails with a retryable error.
426+
// A retryable error means that GitHub will retry to re-create Codespace and clients should poll
427+
// the API and attempt to fetch the Codespace for the next two minutes.
428+
if err == errProvisioningInProgress {
429+
pollTimeout := 2 * time.Minute
430+
pollInterval := 1 * time.Second
431+
log.Print(".")
432+
codespace, err = pollForCodespace(ctx, a, log, pollTimeout, pollInterval, params.User.Login, codespace.Name)
433+
log.Print("\n")
434+
435+
if err != nil {
436+
return nil, fmt.Errorf("error creating codespace with async provisioning: %s: %w", codespace.Name, err)
437+
}
438+
}
439+
440+
return nil, err
441+
}
442+
443+
return codespace, nil
444+
}
445+
446+
type apiClient interface {
447+
GetCodespaceToken(ctx context.Context, userLogin, codespaceName string) (string, error)
448+
GetCodespace(ctx context.Context, token, userLogin, codespaceName string) (*Codespace, error)
449+
}
450+
451+
// pollForCodespace polls the Codespaces GET endpoint on a given interval for a specified duration.
452+
// If it succeeds at fetching the codespace, we consider the codespace provisioned.
453+
func pollForCodespace(ctx context.Context, client apiClient, log logger, duration, interval time.Duration, user, name string) (*Codespace, error) {
454+
ctx, cancel := context.WithTimeout(ctx, duration)
455+
defer cancel()
456+
457+
ticker := time.NewTicker(interval)
458+
defer ticker.Stop()
459+
460+
for {
461+
select {
462+
case <-ctx.Done():
463+
return nil, ctx.Err()
464+
case <-ticker.C:
465+
log.Print(".")
466+
token, err := client.GetCodespaceToken(ctx, user, name)
467+
if err != nil {
468+
if err == ErrNotProvisioned {
469+
// Do nothing. We expect this to fail until the codespace is provisioned
470+
continue
471+
}
472+
473+
return nil, fmt.Errorf("failed to get codespace token: %w", err)
474+
}
475+
476+
return client.GetCodespace(ctx, token, user, name)
477+
}
478+
}
479+
}
480+
405481
type createCodespaceRequest struct {
406482
RepositoryID int `json:"repository_id"`
407483
Ref string `json:"ref"`
408484
Location string `json:"location"`
409485
SkuName string `json:"sku_name"`
410486
}
411487

412-
var ErrProvisioningInProgress = errors.New("provisioning in progress")
488+
var errProvisioningInProgress = errors.New("provisioning in progress")
413489

414-
func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repository, sku, branch, location string) (*Codespace, error) {
490+
func (a *API) createCodespace(ctx context.Context, user *User, repository *Repository, sku, branch, location string) (*Codespace, error) {
415491
requestBody, err := json.Marshal(createCodespaceRequest{repository.ID, branch, location, sku})
416492
if err != nil {
417493
return nil, fmt.Errorf("error marshaling request: %w", err)
@@ -442,7 +518,7 @@ func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repos
442518
// being retried. For clients this means that they must implement a polling strategy
443519
// to check for the codespace existence for the next two minutes. We return an error
444520
// here so callers can detect and handle this condition.
445-
return nil, ErrProvisioningInProgress
521+
return nil, errProvisioningInProgress
446522
}
447523

448524
var response Codespace
Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package codespaces
1+
package api
22

33
import (
44
"context"
@@ -8,21 +8,11 @@ import (
88
"time"
99

1010
"github.com/github/ghcs/cmd/ghcs/output"
11-
"github.com/github/ghcs/internal/api"
1211
)
1312

1413
type mockAPIClient struct {
15-
createCodespace func(context.Context, *api.User, *api.Repository, string, string, string) (*api.Codespace, error)
1614
getCodespaceToken func(context.Context, string, string) (string, error)
17-
getCodespace func(context.Context, string, string, string) (*api.Codespace, error)
18-
}
19-
20-
func (m *mockAPIClient) CreateCodespace(ctx context.Context, user *api.User, repo *api.Repository, machine, branch, location string) (*api.Codespace, error) {
21-
if m.createCodespace == nil {
22-
return nil, errors.New("mock api client CreateCodespace not implemented")
23-
}
24-
25-
return m.createCodespace(ctx, user, repo, machine, branch, location)
15+
getCodespace func(context.Context, string, string, string) (*Codespace, error)
2616
}
2717

2818
func (m *mockAPIClient) GetCodespaceToken(ctx context.Context, userLogin, codespaceName string) (string, error) {
@@ -33,7 +23,7 @@ func (m *mockAPIClient) GetCodespaceToken(ctx context.Context, userLogin, codesp
3323
return m.getCodespaceToken(ctx, userLogin, codespaceName)
3424
}
3525

36-
func (m *mockAPIClient) GetCodespace(ctx context.Context, token, userLogin, codespaceName string) (*api.Codespace, error) {
26+
func (m *mockAPIClient) GetCodespace(ctx context.Context, token, userLogin, codespaceName string) (*Codespace, error) {
3727
if m.getCodespace == nil {
3828
return nil, errors.New("mock api client GetCodespace not implemented")
3929
}
@@ -43,8 +33,8 @@ func (m *mockAPIClient) GetCodespace(ctx context.Context, token, userLogin, code
4333

4434
func TestPollForCodespace(t *testing.T) {
4535
logger := output.NewLogger(nil, nil, false)
46-
user := &api.User{Login: "test"}
47-
tmpCodespace := &api.Codespace{Name: "tmp-codespace"}
36+
user := &User{Login: "test"}
37+
tmpCodespace := &Codespace{Name: "tmp-codespace"}
4838
codespaceToken := "codespace-token"
4939
ctx := context.Background()
5040

@@ -61,7 +51,7 @@ func TestPollForCodespace(t *testing.T) {
6151
}
6252
return codespaceToken, nil
6353
},
64-
getCodespace: func(ctx context.Context, token, userLogin, codespace string) (*api.Codespace, error) {
54+
getCodespace: func(ctx context.Context, token, userLogin, codespace string) (*Codespace, error) {
6555
if token != codespaceToken {
6656
return nil, fmt.Errorf("token does not match, got: %s, expected: %s", token, codespaceToken)
6757
}

internal/codespaces/codespaces.go

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -75,74 +75,3 @@ func ConnectToLiveshare(ctx context.Context, log logger, apiClient *api.API, use
7575

7676
return lsclient.JoinWorkspace(ctx)
7777
}
78-
79-
type apiClient interface {
80-
CreateCodespace(ctx context.Context, user *api.User, repo *api.Repository, machine, branch, location string) (*api.Codespace, error)
81-
GetCodespaceToken(ctx context.Context, userLogin, codespaceName string) (string, error)
82-
GetCodespace(ctx context.Context, token, userLogin, codespaceName string) (*api.Codespace, error)
83-
}
84-
85-
// ProvisionParams are the required parameters for provisioning a Codespace.
86-
type ProvisionParams struct {
87-
User *api.User
88-
Repository *api.Repository
89-
Branch, Machine, Location string
90-
}
91-
92-
// Provision creates a codespace with the given parameters and handles polling in the case
93-
// of initial creation failures.
94-
func Provision(ctx context.Context, log logger, client apiClient, params *ProvisionParams) (*api.Codespace, error) {
95-
codespace, err := client.CreateCodespace(
96-
ctx, params.User, params.Repository, params.Machine, params.Branch, params.Location,
97-
)
98-
if err != nil {
99-
// This error is returned by the API when the initial creation fails with a retryable error.
100-
// A retryable error means that GitHub will retry to re-create Codespace and clients should poll
101-
// the API and attempt to fetch the Codespace for the next two minutes.
102-
if err == api.ErrProvisioningInProgress {
103-
pollTimeout := 2 * time.Minute
104-
pollInterval := 1 * time.Second
105-
log.Print(".")
106-
codespace, err = pollForCodespace(ctx, client, log, pollTimeout, pollInterval, params.User.Login, codespace.Name)
107-
log.Print("\n")
108-
109-
if err != nil {
110-
return nil, fmt.Errorf("error creating codespace with async provisioning: %s: %w", codespace.Name, err)
111-
}
112-
}
113-
114-
return nil, err
115-
}
116-
117-
return codespace, nil
118-
}
119-
120-
// pollForCodespace polls the Codespaces GET endpoint on a given interval for a specified duration.
121-
// If it succeeds at fetching the codespace, we consider the codespace provisioned.
122-
func pollForCodespace(ctx context.Context, client apiClient, log logger, duration, interval time.Duration, user, name string) (*api.Codespace, error) {
123-
ctx, cancel := context.WithTimeout(ctx, duration)
124-
defer cancel()
125-
126-
ticker := time.NewTicker(interval)
127-
defer ticker.Stop()
128-
129-
for {
130-
select {
131-
case <-ctx.Done():
132-
return nil, ctx.Err()
133-
case <-ticker.C:
134-
log.Print(".")
135-
token, err := client.GetCodespaceToken(ctx, user, name)
136-
if err != nil {
137-
if err == api.ErrNotProvisioned {
138-
// Do nothing. We expect this to fail until the codespace is provisioned
139-
continue
140-
}
141-
142-
return nil, fmt.Errorf("failed to get codespace token: %w", err)
143-
}
144-
145-
return client.GetCodespace(ctx, token, user, name)
146-
}
147-
}
148-
}

0 commit comments

Comments
 (0)
X Tutup