X Tutup
Skip to content

Commit 1232dba

Browse files
committed
Merge remote-tracking branch 'origin' into raffo/delete-codespaces
2 parents 32d3a38 + 3e26a15 commit 1232dba

File tree

4 files changed

+100
-26
lines changed

4 files changed

+100
-26
lines changed

cmd/ghcs/create.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,15 @@ func create(opts *createOptions) error {
8181
return errors.New("there are no available machine types for this repository")
8282
}
8383

84-
log.Println("Creating your codespace...")
85-
86-
codespace, err := apiClient.CreateCodespace(ctx, userResult.User, repository, machine, branch, locationResult.Location)
84+
log.Print("Creating your codespace...")
85+
codespace, err := apiClient.CreateCodespace(ctx, log, &api.CreateCodespaceParams{
86+
User: userResult.User.Login,
87+
RepositoryID: repository.ID,
88+
Branch: branch,
89+
Machine: machine,
90+
Location: locationResult.Location,
91+
})
92+
log.Print("\n")
8793
if err != nil {
8894
return fmt.Errorf("error creating codespace: %w", err)
8995
}
@@ -172,7 +178,7 @@ type getUserResult struct {
172178

173179
// getUser fetches the user record associated with the GITHUB_TOKEN
174180
func getUser(ctx context.Context, apiClient *api.API) <-chan getUserResult {
175-
ch := make(chan getUserResult)
181+
ch := make(chan getUserResult, 1)
176182
go func() {
177183
user, err := apiClient.GetUser(ctx)
178184
ch <- getUserResult{user, err}
@@ -187,7 +193,7 @@ type locationResult struct {
187193

188194
// getLocation fetches the closest Codespace datacenter region/location to the user.
189195
func getLocation(ctx context.Context, apiClient *api.API) <-chan locationResult {
190-
ch := make(chan locationResult)
196+
ch := make(chan locationResult, 1)
191197
go func() {
192198
location, err := apiClient.GetCodespaceRegionLocation(ctx)
193199
ch <- locationResult{location, err}

cmd/ghcs/ports.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ type portAttribute struct {
123123
}
124124

125125
func getDevContainer(ctx context.Context, apiClient *api.API, codespace *api.Codespace) <-chan devContainerResult {
126-
ch := make(chan devContainerResult)
126+
ch := make(chan devContainerResult, 1)
127127
go func() {
128128
contents, err := apiClient.GetCodespaceRepositoryContents(ctx, codespace, ".devcontainer/devcontainer.json")
129129
if err != nil {

internal/api/api.go

Lines changed: 80 additions & 5 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
)
@@ -214,6 +215,10 @@ type getCodespaceTokenResponse struct {
214215
RepositoryToken string `json:"repository_token"`
215216
}
216217

218+
// ErrNotProvisioned is returned by GetCodespacesToken to indicate that the
219+
// creation of a codespace is not yet complete and that the caller should try again.
220+
var ErrNotProvisioned = errors.New("codespace not provisioned")
221+
217222
func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName string) (string, error) {
218223
reqBody, err := json.Marshal(getCodespaceTokenRequest{true})
219224
if err != nil {
@@ -242,6 +247,10 @@ func (a *API) GetCodespaceToken(ctx context.Context, ownerLogin, codespaceName s
242247
}
243248

244249
if resp.StatusCode != http.StatusOK {
250+
if resp.StatusCode == http.StatusUnprocessableEntity {
251+
return "", ErrNotProvisioned
252+
}
253+
245254
return "", jsonErrorResponse(b)
246255
}
247256

@@ -401,20 +410,83 @@ func (a *API) GetCodespacesSKUs(ctx context.Context, user *User, repository *Rep
401410
return response.SKUs, nil
402411
}
403412

404-
type createCodespaceRequest struct {
413+
// CreateCodespaceParams are the required parameters for provisioning a Codespace.
414+
type CreateCodespaceParams struct {
415+
User string
416+
RepositoryID int
417+
Branch, Machine, Location string
418+
}
419+
420+
type logger interface {
421+
Print(v ...interface{}) (int, error)
422+
Println(v ...interface{}) (int, error)
423+
}
424+
425+
// CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it
426+
// fails to create.
427+
func (a *API) CreateCodespace(ctx context.Context, log logger, params *CreateCodespaceParams) (*Codespace, error) {
428+
codespace, err := a.startCreate(
429+
ctx, params.User, params.RepositoryID, params.Machine, params.Branch, params.Location,
430+
)
431+
if err != errProvisioningInProgress {
432+
return codespace, err
433+
}
434+
435+
// errProvisioningInProgress indicates that codespace creation did not complete
436+
// within the GitHub API RPC time limit (10s), so it continues asynchronously.
437+
// We must poll the server to discover the outcome.
438+
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
439+
defer cancel()
440+
441+
ticker := time.NewTicker(1 * time.Second)
442+
defer ticker.Stop()
443+
444+
for {
445+
select {
446+
case <-ctx.Done():
447+
return nil, ctx.Err()
448+
case <-ticker.C:
449+
log.Print(".")
450+
token, err := a.GetCodespaceToken(ctx, params.User, codespace.Name)
451+
if err != nil {
452+
if err == ErrNotProvisioned {
453+
// Do nothing. We expect this to fail until the codespace is provisioned
454+
continue
455+
}
456+
457+
return nil, fmt.Errorf("failed to get codespace token: %w", err)
458+
}
459+
460+
codespace, err = a.GetCodespace(ctx, token, params.User, codespace.Name)
461+
if err != nil {
462+
return nil, fmt.Errorf("failed to get codespace: %w", err)
463+
}
464+
465+
return codespace, nil
466+
}
467+
}
468+
}
469+
470+
type startCreateRequest struct {
405471
RepositoryID int `json:"repository_id"`
406472
Ref string `json:"ref"`
407473
Location string `json:"location"`
408474
SkuName string `json:"sku_name"`
409475
}
410476

411-
func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repository, sku, branch, location string) (*Codespace, error) {
412-
requestBody, err := json.Marshal(createCodespaceRequest{repository.ID, branch, location, sku})
477+
var errProvisioningInProgress = errors.New("provisioning in progress")
478+
479+
// startCreate starts the creation of a codespace.
480+
// It may return success or an error, or errProvisioningInProgress indicating that the operation
481+
// did not complete before the GitHub API's time limit for RPCs (10s), in which case the caller
482+
// must poll the server to learn the outcome.
483+
func (a *API) startCreate(ctx context.Context, user string, repository int, sku, branch, location string) (*Codespace, error) {
484+
requestBody, err := json.Marshal(startCreateRequest{repository, branch, location, sku})
413485
if err != nil {
414486
return nil, fmt.Errorf("error marshaling request: %w", err)
415487
}
416488

417-
req, err := http.NewRequest(http.MethodPost, a.githubAPI+"/vscs_internal/user/"+user.Login+"/codespaces", bytes.NewBuffer(requestBody))
489+
req, err := http.NewRequest(http.MethodPost, a.githubAPI+"/vscs_internal/user/"+user+"/codespaces", bytes.NewBuffer(requestBody))
418490
if err != nil {
419491
return nil, fmt.Errorf("error creating request: %w", err)
420492
}
@@ -431,8 +503,11 @@ func (a *API) CreateCodespace(ctx context.Context, user *User, repository *Repos
431503
return nil, fmt.Errorf("error reading response body: %w", err)
432504
}
433505

434-
if resp.StatusCode > http.StatusAccepted {
506+
switch {
507+
case resp.StatusCode > http.StatusAccepted:
435508
return nil, jsonErrorResponse(b)
509+
case resp.StatusCode == http.StatusAccepted:
510+
return nil, errProvisioningInProgress // RPC finished before result of creation known
436511
}
437512

438513
var response Codespace

internal/codespaces/codespaces.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ func connectionReady(codespace *api.Codespace) bool {
2323
codespace.Environment.State == api.CodespaceEnvironmentStateAvailable
2424
}
2525

26-
// ConnectToLiveshare creates a Live Share client and joins the Live Share session.
27-
// It will start the Codespace if it is not already running, it will time out after 60 seconds if fails to start.
26+
// ConnectToLiveshare waits for a Codespace to become running,
27+
// and connects to it using a Live Share session.
2828
func ConnectToLiveshare(ctx context.Context, log logger, apiClient *api.API, userLogin, token string, codespace *api.Codespace) (*liveshare.Session, error) {
2929
var startedCodespace bool
3030
if codespace.Environment.State != api.CodespaceEnvironmentStateAvailable {
@@ -61,17 +61,10 @@ func ConnectToLiveshare(ctx context.Context, log logger, apiClient *api.API, use
6161

6262
log.Println("Connecting to your codespace...")
6363

64-
lsclient, err := liveshare.NewClient(
65-
liveshare.WithConnection(liveshare.Connection{
66-
SessionID: codespace.Environment.Connection.SessionID,
67-
SessionToken: codespace.Environment.Connection.SessionToken,
68-
RelaySAS: codespace.Environment.Connection.RelaySAS,
69-
RelayEndpoint: codespace.Environment.Connection.RelayEndpoint,
70-
}),
71-
)
72-
if err != nil {
73-
return nil, fmt.Errorf("error creating Live Share client: %w", err)
74-
}
75-
76-
return lsclient.JoinWorkspace(ctx)
64+
return liveshare.Connect(ctx, liveshare.Options{
65+
SessionID: codespace.Environment.Connection.SessionID,
66+
SessionToken: codespace.Environment.Connection.SessionToken,
67+
RelaySAS: codespace.Environment.Connection.RelaySAS,
68+
RelayEndpoint: codespace.Environment.Connection.RelayEndpoint,
69+
})
7770
}

0 commit comments

Comments
 (0)
X Tutup