Skip to content

Commit

Permalink
Merge pull request #8179 from dmgardiner25/poll-codespace-permissions
Browse files Browse the repository at this point in the history
Poll permission acceptance endpoint on codespace creation
  • Loading branch information
dmgardiner25 committed Oct 16, 2023
2 parents 7d558ed + 135c56a commit bc0f63b
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 30 deletions.
40 changes: 40 additions & 0 deletions internal/codespaces/api/api.go
Expand Up @@ -639,6 +639,46 @@ func (a *API) GetCodespacesMachines(ctx context.Context, repoID int, branch, loc
return response.Machines, nil
}

// GetCodespacesPermissionsCheck returns a bool indicating whether the user has accepted permissions for the given repo and devcontainer path.
func (a *API) GetCodespacesPermissionsCheck(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) (bool, error) {
reqURL := fmt.Sprintf("%s/repositories/%d/codespaces/permissions_check", a.githubAPI, repoID)
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
return false, fmt.Errorf("error creating request: %w", err)
}

q := req.URL.Query()
q.Add("location", location)
q.Add("ref", branch)
q.Add("devcontainer_path", devcontainerPath)
req.URL.RawQuery = q.Encode()

a.setHeaders(req)
resp, err := a.do(ctx, req, "/repositories/*/codespaces/permissions_check")
if err != nil {
return false, fmt.Errorf("error making request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return false, api.HandleHTTPError(resp)
}

b, err := io.ReadAll(resp.Body)
if err != nil {
return false, fmt.Errorf("error reading response body: %w", err)
}

var response struct {
Accepted bool `json:"accepted"`
}
if err := json.Unmarshal(b, &response); err != nil {
return false, fmt.Errorf("error unmarshalling response: %w", err)
}

return response.Accepted, nil
}

// RepoSearchParameters are the optional parameters for searching for repositories.
type RepoSearchParameters struct {
// The maximum number of repos to return. At most 100 repos are returned even if this value is greater than 100.
Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/codespace/common.go
Expand Up @@ -76,7 +76,8 @@ type apiClient interface {
CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error)
EditCodespace(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error)
GetRepository(ctx context.Context, nwo string) (*api.Repository, error)
GetCodespacesMachines(ctx context.Context, repoID int, branch, location string, devcontainerPath string) ([]*api.Machine, error)
GetCodespacesMachines(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*api.Machine, error)
GetCodespacesPermissionsCheck(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) (bool, error)
GetCodespaceRepositoryContents(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error)
ListDevContainers(ctx context.Context, repoID int, branch string, limit int) (devcontainers []api.DevContainerEntry, err error)
GetCodespaceRepoSuggestions(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error)
Expand Down
50 changes: 43 additions & 7 deletions pkg/cmd/codespace/create.go
Expand Up @@ -19,6 +19,11 @@ const (
DEVCONTAINER_PROMPT_DEFAULT = "Default Codespaces configuration"
)

const (
permissionsPollingInterval = 5 * time.Second
permissionsPollingTimeout = 1 * time.Minute
)

var (
DEFAULT_DEVCONTAINER_DEFINITIONS = []string{".devcontainer.json", ".devcontainer/devcontainer.json"}
)
Expand Down Expand Up @@ -307,7 +312,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
return fmt.Errorf("error creating codespace: %w", err)
}

codespace, err = a.handleAdditionalPermissions(ctx, createParams, aerr.AllowPermissionsURL)
codespace, err = a.handleAdditionalPermissions(ctx, createParams, aerr.AllowPermissionsURL, userInputs.Location)
if err != nil {
// this error could be a cmdutil.SilentError (in the case that the user opened the browser) so we don't want to wrap it
return err
Expand All @@ -331,7 +336,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
return nil
}

func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api.CreateCodespaceParams, allowPermissionsURL string) (*api.Codespace, error) {
func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api.CreateCodespaceParams, allowPermissionsURL string, location string) (*api.Codespace, error) {
var (
isInteractive = a.io.CanPrompt()
cs = a.io.ColorScheme()
Expand Down Expand Up @@ -372,21 +377,52 @@ func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api

// if the user chose to continue in the browser, open the URL
if answers.Accept == choices[0] {
fmt.Fprintln(a.io.ErrOut, "Please re-run the create request after accepting permissions in the browser.")
if err := a.browser.Browse(allowPermissionsURL); err != nil {
return nil, fmt.Errorf("error opening browser: %w", err)
}
// browser opened successfully but we do not know if they accepted the permissions
// so we must exit and wait for the user to attempt the create again
return nil, cmdutil.SilentError
}

// Poll until the user has accepted the permissions or timeout
err := a.RunWithProgress("Waiting for permissions to be accepted in the browser", func() (err error) {
ctx, cancel := context.WithTimeout(ctx, permissionsPollingTimeout)
defer cancel()

done := make(chan error, 1)
go func() {
for {
accepted, err := a.apiClient.GetCodespacesPermissionsCheck(ctx, createParams.RepositoryID, createParams.Branch, location, createParams.DevContainerPath)
if err != nil {
done <- err
return
}

if accepted {
done <- nil
return
}

// Wait before polling again
time.Sleep(permissionsPollingInterval)
}
}()

select {
case err := <-done:
return err
case <-ctx.Done():
return fmt.Errorf("timed out waiting for permissions to be accepted in the browser")
}
})
if err != nil {
return nil, fmt.Errorf("error polling for permissions: %w", err)
}

// if the user chose to create the codespace without the permissions,
// we can continue with the create opting out of the additional permissions
createParams.PermissionsOptOut = true

var codespace *api.Codespace
err := a.RunWithProgress("Creating codespace", func() (err error) {
err = a.RunWithProgress("Creating codespace", func() (err error) {
codespace, err = a.apiClient.CreateCodespace(ctx, createParams)
return
})
Expand Down
112 changes: 90 additions & 22 deletions pkg/cmd/codespace/mock_api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit bc0f63b

Please sign in to comment.