Merge pull request #4446 from cli/jg/get-codespace-api

codespace: switch `API.GetCodespace` to new API endpoint
This commit is contained in:
Jose Garcia 2021-10-06 09:41:01 -04:00 committed by GitHub
commit 52e16dfe09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 68 additions and 240 deletions

View file

@ -151,6 +151,7 @@ type Codespace struct {
Name string `json:"name"`
CreatedAt string `json:"created_at"`
LastUsedAt string `json:"last_used_at"`
State string `json:"state"`
Branch string `json:"branch"`
RepositoryName string `json:"repository_name"`
RepositoryNWO string `json:"repository_nwo"`
@ -158,6 +159,8 @@ type Codespace struct {
Environment CodespaceEnvironment `json:"environment"`
}
const CodespaceStateProvisioned = "provisioned"
type CodespaceEnvironment struct {
State string `json:"state"`
Connection CodespaceEnvironmentConnection `json:"connection"`
@ -220,77 +223,28 @@ func (a *API) ListCodespaces(ctx context.Context) ([]*Codespace, error) {
return response.Codespaces, nil
}
// getCodespaceTokenRequest is the request body for the get codespace token endpoint.
type getCodespaceTokenRequest struct {
MintRepositoryToken bool `json:"mint_repository_token"`
}
type getCodespaceTokenResponse struct {
RepositoryToken string `json:"repository_token"`
}
// ErrNotProvisioned is returned by GetCodespacesToken to indicate that the
// creation of a codespace is not yet complete and that the caller should try again.
var ErrNotProvisioned = errors.New("codespace not provisioned")
// GetCodespaceToken returns a codespace token for the user.
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: %w", err)
}
req, err := http.NewRequest(
http.MethodPost,
a.githubAPI+"/vscs_internal/user/"+ownerLogin+"/codespaces/"+codespaceName+"/token",
bytes.NewBuffer(reqBody),
)
if err != nil {
return "", fmt.Errorf("error creating request: %w", err)
}
a.setHeaders(req)
resp, err := a.do(ctx, req, "/vscs_internal/user/*/codespaces/*/token")
if err != nil {
return "", fmt.Errorf("error making request: %w", err)
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading response body: %w", err)
}
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusUnprocessableEntity {
return "", ErrNotProvisioned
}
return "", jsonErrorResponse(b)
}
var response getCodespaceTokenResponse
if err := json.Unmarshal(b, &response); err != nil {
return "", fmt.Errorf("error unmarshaling response: %w", err)
}
return response.RepositoryToken, nil
}
// GetCodespace returns a codespace for the user.
func (a *API) GetCodespace(ctx context.Context, token, owner, codespace string) (*Codespace, error) {
// GetCodespace returns the user codespace based on the provided name.
// If the codespace is not found, an error is returned.
// If includeConnection is true, it will return the connection information for the codespace.
func (a *API) GetCodespace(ctx context.Context, codespaceName string, includeConnection bool) (*Codespace, error) {
req, err := http.NewRequest(
http.MethodGet,
a.githubAPI+"/vscs_internal/user/"+owner+"/codespaces/"+codespace,
a.githubAPI+"/user/codespaces/"+codespaceName,
nil,
)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
// TODO: use a.setHeaders()
req.Header.Set("Authorization", "Bearer "+token)
resp, err := a.do(ctx, req, "/vscs_internal/user/*/codespaces/*")
if includeConnection {
q := req.URL.Query()
q.Add("internal", "true")
q.Add("refresh", "true")
req.URL.RawQuery = q.Encode()
}
a.setHeaders(req)
resp, err := a.do(ctx, req, "/user/codespaces/*")
if err != nil {
return nil, fmt.Errorf("error making request: %w", err)
}
@ -437,7 +391,6 @@ func (a *API) GetCodespacesMachines(ctx context.Context, repoID int, branch, loc
// CreateCodespaceParams are the required parameters for provisioning a Codespace.
type CreateCodespaceParams struct {
User string
RepositoryID int
Branch, Machine, Location string
}
@ -464,21 +417,16 @@ func (a *API) CreateCodespace(ctx context.Context, params *CreateCodespaceParams
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
token, err := a.GetCodespaceToken(ctx, params.User, codespace.Name)
if err != nil {
if err == ErrNotProvisioned {
// Do nothing. We expect this to fail until the codespace is provisioned
continue
}
return nil, fmt.Errorf("failed to get codespace token: %w", err)
}
codespace, err = a.GetCodespace(ctx, token, params.User, codespace.Name)
codespace, err = a.GetCodespace(ctx, codespace.Name, false)
if err != nil {
return nil, fmt.Errorf("failed to get codespace: %w", err)
}
// we continue to poll until the codespace shows as provisioned
if codespace.State != CodespaceStateProvisioned {
continue
}
return codespace, nil
}
}

View file

@ -24,14 +24,13 @@ func connectionReady(codespace *api.Codespace) bool {
}
type apiClient interface {
GetCodespace(ctx context.Context, token, user, name string) (*api.Codespace, error)
GetCodespaceToken(ctx context.Context, user, codespace string) (string, error)
GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
StartCodespace(ctx context.Context, name string) error
}
// ConnectToLiveshare waits for a Codespace to become running,
// and connects to it using a Live Share session.
func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, userLogin, token string, codespace *api.Codespace) (*liveshare.Session, error) {
func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, codespace *api.Codespace) (*liveshare.Session, error) {
var startedCodespace bool
if codespace.Environment.State != api.CodespaceEnvironmentStateAvailable {
startedCodespace = true
@ -55,7 +54,7 @@ func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, us
}
var err error
codespace, err = apiClient.GetCodespace(ctx, token, userLogin, codespace.Name)
codespace, err = apiClient.GetCodespace(ctx, codespace.Name, true)
if err != nil {
return nil, fmt.Errorf("error getting codespace: %w", err)
}

View file

@ -36,13 +36,8 @@ type PostCreateState struct {
// PollPostCreateStates watches for state changes in a codespace,
// and calls the supplied poller for each batch of state changes.
// It runs until it encounters an error, including cancellation of the context.
func PollPostCreateStates(ctx context.Context, log logger, apiClient apiClient, user *api.User, codespace *api.Codespace, poller func([]PostCreateState)) (err error) {
token, err := apiClient.GetCodespaceToken(ctx, user.Login, codespace.Name)
if err != nil {
return fmt.Errorf("getting codespace token: %w", err)
}
session, err := ConnectToLiveshare(ctx, log, apiClient, user.Login, token, codespace)
func PollPostCreateStates(ctx context.Context, log logger, apiClient apiClient, codespace *api.Codespace, poller func([]PostCreateState)) (err error) {
session, err := ConnectToLiveshare(ctx, log, apiClient, codespace)
if err != nil {
return fmt.Errorf("connect to Live Share: %w", err)
}

View file

@ -33,8 +33,7 @@ func NewApp(logger *output.Logger, apiClient apiClient) *App {
//go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient
type apiClient interface {
GetUser(ctx context.Context) (*api.User, error)
GetCodespaceToken(ctx context.Context, user, name string) (string, error)
GetCodespace(ctx context.Context, token, user, name string) (*api.Codespace, error)
GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
ListCodespaces(ctx context.Context) ([]*api.Codespace, error)
DeleteCodespace(ctx context.Context, name string) error
StartCodespace(ctx context.Context, name string) error
@ -96,35 +95,24 @@ func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (
}
// getOrChooseCodespace prompts the user to choose a codespace if the codespaceName is empty.
// It then fetches the codespace token and the codespace record.
func getOrChooseCodespace(ctx context.Context, apiClient apiClient, user *api.User, codespaceName string) (codespace *api.Codespace, token string, err error) {
// It then fetches the codespace record with full connection details.
func getOrChooseCodespace(ctx context.Context, apiClient apiClient, codespaceName string) (codespace *api.Codespace, err error) {
if codespaceName == "" {
codespace, err = chooseCodespace(ctx, apiClient)
if err != nil {
if err == errNoCodespaces {
return nil, "", err
return nil, err
}
return nil, "", fmt.Errorf("choosing codespace: %w", err)
}
codespaceName = codespace.Name
token, err = apiClient.GetCodespaceToken(ctx, user.Login, codespaceName)
if err != nil {
return nil, "", fmt.Errorf("getting codespace token: %w", err)
return nil, fmt.Errorf("choosing codespace: %w", err)
}
} else {
token, err = apiClient.GetCodespaceToken(ctx, user.Login, codespaceName)
codespace, err = apiClient.GetCodespace(ctx, codespaceName, true)
if err != nil {
return nil, "", fmt.Errorf("getting codespace token for given codespace: %w", err)
}
codespace, err = apiClient.GetCodespace(ctx, token, user.Login, codespaceName)
if err != nil {
return nil, "", fmt.Errorf("getting full codespace details: %w", err)
return nil, fmt.Errorf("getting full codespace details: %w", err)
}
}
return codespace, token, nil
return codespace, nil
}
func safeClose(closer io.Closer, err *error) {

View file

@ -81,7 +81,6 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
a.logger.Print("Creating your codespace...")
codespace, err := a.apiClient.CreateCodespace(ctx, &api.CreateCodespaceParams{
User: userResult.User.Login,
RepositoryID: repository.ID,
Branch: branch,
Machine: machine,
@ -157,7 +156,7 @@ func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, us
}
}
err := codespaces.PollPostCreateStates(ctx, log, apiClient, user, codespace, poller)
err := codespaces.PollPostCreateStates(ctx, log, apiClient, codespace, poller)
if err != nil {
if errors.Is(err, context.Canceled) && breakNextState {
return nil // we cancelled the context to stop polling, we can ignore the error

View file

@ -58,12 +58,7 @@ func newDeleteCmd(app *App) *cobra.Command {
return deleteCmd
}
func (a *App) Delete(ctx context.Context, opts deleteOptions) error {
user, err := a.apiClient.GetUser(ctx)
if err != nil {
return fmt.Errorf("error getting user: %w", err)
}
func (a *App) Delete(ctx context.Context, opts deleteOptions) (err error) {
var codespaces []*api.Codespace
nameFilter := opts.codespaceName
if nameFilter == "" {
@ -80,12 +75,7 @@ func (a *App) Delete(ctx context.Context, opts deleteOptions) error {
nameFilter = c.Name
}
} else {
token, err := a.apiClient.GetCodespaceToken(ctx, user.Login, nameFilter)
if err != nil {
return fmt.Errorf("error getting codespace token: %w", err)
}
codespace, err := a.apiClient.GetCodespace(ctx, token, user.Login, nameFilter)
codespace, err := a.apiClient.GetCodespace(ctx, nameFilter, false)
if err != nil {
return fmt.Errorf("error fetching codespace information: %w", err)
}

View file

@ -168,19 +168,7 @@ func TestDelete(t *testing.T) {
return tt.codespaces, nil
}
} else {
apiMock.GetCodespaceTokenFunc = func(_ context.Context, userLogin, name string) (string, error) {
if userLogin != user.Login {
return "", fmt.Errorf("unexpected user %q", userLogin)
}
return "CS_TOKEN", nil
}
apiMock.GetCodespaceFunc = func(_ context.Context, token, userLogin, name string) (*api.Codespace, error) {
if userLogin != user.Login {
return nil, fmt.Errorf("unexpected user %q", userLogin)
}
if token != "CS_TOKEN" {
return nil, fmt.Errorf("unexpected token %q", token)
}
apiMock.GetCodespaceFunc = func(_ context.Context, name string, includeConnection bool) (*api.Codespace, error) {
return tt.codespaces[0], nil
}
}
@ -206,9 +194,6 @@ func TestDelete(t *testing.T) {
if (err != nil) != tt.wantErr {
t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr)
}
if n := len(apiMock.GetUserCalls()); n != 1 {
t.Errorf("GetUser invoked %d times, expected %d", n, 1)
}
var gotDeleted []string
for _, delArgs := range apiMock.DeleteCodespaceCalls() {
gotDeleted = append(gotDeleted, delArgs.Name)

View file

@ -46,12 +46,12 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err
authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login)
}()
codespace, token, err := getOrChooseCodespace(ctx, a.apiClient, user, codespaceName)
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
if err != nil {
return fmt.Errorf("get or choose codespace: %w", err)
}
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, user.Login, token, codespace)
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, codespace)
if err != nil {
return fmt.Errorf("connecting to Live Share: %w", err)
}

View file

@ -25,7 +25,7 @@ import (
// DeleteCodespaceFunc: func(ctx context.Context, name string) error {
// panic("mock out the DeleteCodespace method")
// },
// GetCodespaceFunc: func(ctx context.Context, token string, user string, name string) (*api.Codespace, error) {
// GetCodespaceFunc: func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
// panic("mock out the GetCodespace method")
// },
// GetCodespaceRegionLocationFunc: func(ctx context.Context) (string, error) {
@ -34,9 +34,6 @@ import (
// GetCodespaceRepositoryContentsFunc: func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) {
// panic("mock out the GetCodespaceRepositoryContents method")
// },
// GetCodespaceTokenFunc: func(ctx context.Context, user string, name string) (string, error) {
// panic("mock out the GetCodespaceToken method")
// },
// GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch string, location string) ([]*api.Machine, error) {
// panic("mock out the GetCodespacesMachines method")
// },
@ -69,7 +66,7 @@ type apiClientMock struct {
DeleteCodespaceFunc func(ctx context.Context, name string) error
// GetCodespaceFunc mocks the GetCodespace method.
GetCodespaceFunc func(ctx context.Context, token string, user string, name string) (*api.Codespace, error)
GetCodespaceFunc func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
// GetCodespaceRegionLocationFunc mocks the GetCodespaceRegionLocation method.
GetCodespaceRegionLocationFunc func(ctx context.Context) (string, error)
@ -77,9 +74,6 @@ type apiClientMock struct {
// GetCodespaceRepositoryContentsFunc mocks the GetCodespaceRepositoryContents method.
GetCodespaceRepositoryContentsFunc func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error)
// GetCodespaceTokenFunc mocks the GetCodespaceToken method.
GetCodespaceTokenFunc func(ctx context.Context, user string, name string) (string, error)
// GetCodespacesMachinesFunc mocks the GetCodespacesMachines method.
GetCodespacesMachinesFunc func(ctx context.Context, repoID int, branch string, location string) ([]*api.Machine, error)
@ -122,12 +116,10 @@ type apiClientMock struct {
GetCodespace []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Token is the token argument value.
Token string
// User is the user argument value.
User string
// Name is the name argument value.
Name string
// IncludeConnection is the includeConnection argument value.
IncludeConnection bool
}
// GetCodespaceRegionLocation holds details about calls to the GetCodespaceRegionLocation method.
GetCodespaceRegionLocation []struct {
@ -143,15 +135,6 @@ type apiClientMock struct {
// Path is the path argument value.
Path string
}
// GetCodespaceToken holds details about calls to the GetCodespaceToken method.
GetCodespaceToken []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// User is the user argument value.
User string
// Name is the name argument value.
Name string
}
// GetCodespacesMachines holds details about calls to the GetCodespacesMachines method.
GetCodespacesMachines []struct {
// Ctx is the ctx argument value.
@ -194,7 +177,6 @@ type apiClientMock struct {
lockGetCodespace sync.RWMutex
lockGetCodespaceRegionLocation sync.RWMutex
lockGetCodespaceRepositoryContents sync.RWMutex
lockGetCodespaceToken sync.RWMutex
lockGetCodespacesMachines sync.RWMutex
lockGetRepository sync.RWMutex
lockGetUser sync.RWMutex
@ -308,41 +290,37 @@ func (mock *apiClientMock) DeleteCodespaceCalls() []struct {
}
// GetCodespace calls GetCodespaceFunc.
func (mock *apiClientMock) GetCodespace(ctx context.Context, token string, user string, name string) (*api.Codespace, error) {
func (mock *apiClientMock) GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
if mock.GetCodespaceFunc == nil {
panic("apiClientMock.GetCodespaceFunc: method is nil but apiClient.GetCodespace was just called")
}
callInfo := struct {
Ctx context.Context
Token string
User string
Name string
Ctx context.Context
Name string
IncludeConnection bool
}{
Ctx: ctx,
Token: token,
User: user,
Name: name,
Ctx: ctx,
Name: name,
IncludeConnection: includeConnection,
}
mock.lockGetCodespace.Lock()
mock.calls.GetCodespace = append(mock.calls.GetCodespace, callInfo)
mock.lockGetCodespace.Unlock()
return mock.GetCodespaceFunc(ctx, token, user, name)
return mock.GetCodespaceFunc(ctx, name, includeConnection)
}
// GetCodespaceCalls gets all the calls that were made to GetCodespace.
// Check the length with:
// len(mockedapiClient.GetCodespaceCalls())
func (mock *apiClientMock) GetCodespaceCalls() []struct {
Ctx context.Context
Token string
User string
Name string
Ctx context.Context
Name string
IncludeConnection bool
} {
var calls []struct {
Ctx context.Context
Token string
User string
Name string
Ctx context.Context
Name string
IncludeConnection bool
}
mock.lockGetCodespace.RLock()
calls = mock.calls.GetCodespace
@ -420,45 +398,6 @@ func (mock *apiClientMock) GetCodespaceRepositoryContentsCalls() []struct {
return calls
}
// GetCodespaceToken calls GetCodespaceTokenFunc.
func (mock *apiClientMock) GetCodespaceToken(ctx context.Context, user string, name string) (string, error) {
if mock.GetCodespaceTokenFunc == nil {
panic("apiClientMock.GetCodespaceTokenFunc: method is nil but apiClient.GetCodespaceToken was just called")
}
callInfo := struct {
Ctx context.Context
User string
Name string
}{
Ctx: ctx,
User: user,
Name: name,
}
mock.lockGetCodespaceToken.Lock()
mock.calls.GetCodespaceToken = append(mock.calls.GetCodespaceToken, callInfo)
mock.lockGetCodespaceToken.Unlock()
return mock.GetCodespaceTokenFunc(ctx, user, name)
}
// GetCodespaceTokenCalls gets all the calls that were made to GetCodespaceToken.
// Check the length with:
// len(mockedapiClient.GetCodespaceTokenCalls())
func (mock *apiClientMock) GetCodespaceTokenCalls() []struct {
Ctx context.Context
User string
Name string
} {
var calls []struct {
Ctx context.Context
User string
Name string
}
mock.lockGetCodespaceToken.RLock()
calls = mock.calls.GetCodespaceToken
mock.lockGetCodespaceToken.RUnlock()
return calls
}
// GetCodespacesMachines calls GetCodespacesMachinesFunc.
func (mock *apiClientMock) GetCodespacesMachines(ctx context.Context, repoID int, branch string, location string) ([]*api.Machine, error) {
if mock.GetCodespacesMachinesFunc == nil {

View file

@ -49,12 +49,7 @@ func newPortsCmd(app *App) *cobra.Command {
// ListPorts lists known ports in a codespace.
func (a *App) ListPorts(ctx context.Context, codespaceName string, asJSON bool) (err error) {
user, err := a.apiClient.GetUser(ctx)
if err != nil {
return fmt.Errorf("error getting user: %w", err)
}
codespace, token, err := getOrChooseCodespace(ctx, a.apiClient, user, codespaceName)
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
if err != nil {
// TODO(josebalius): remove special handling of this error here and it other places
if err == errNoCodespaces {
@ -65,7 +60,7 @@ func (a *App) ListPorts(ctx context.Context, codespaceName string, asJSON bool)
devContainerCh := getDevContainer(ctx, a.apiClient, codespace)
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, user.Login, token, codespace)
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, codespace)
if err != nil {
return fmt.Errorf("error connecting to Live Share: %w", err)
}
@ -191,12 +186,7 @@ func newPortsPrivateCmd(app *App) *cobra.Command {
}
func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName, sourcePort string, public bool) (err error) {
user, err := a.apiClient.GetUser(ctx)
if err != nil {
return fmt.Errorf("error getting user: %w", err)
}
codespace, token, err := getOrChooseCodespace(ctx, a.apiClient, user, codespaceName)
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
if err != nil {
if err == errNoCodespaces {
return err
@ -204,7 +194,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName, sourcePor
return fmt.Errorf("error getting codespace: %w", err)
}
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, user.Login, token, codespace)
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, codespace)
if err != nil {
return fmt.Errorf("error connecting to Live Share: %w", err)
}
@ -255,12 +245,7 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st
return fmt.Errorf("get port pairs: %w", err)
}
user, err := a.apiClient.GetUser(ctx)
if err != nil {
return fmt.Errorf("error getting user: %w", err)
}
codespace, token, err := getOrChooseCodespace(ctx, a.apiClient, user, codespaceName)
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
if err != nil {
if err == errNoCodespaces {
return err
@ -268,7 +253,7 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st
return fmt.Errorf("error getting codespace: %w", err)
}
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, user.Login, token, codespace)
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, codespace)
if err != nil {
return fmt.Errorf("error connecting to Live Share: %w", err)
}

View file

@ -45,12 +45,12 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, sshProfile, codespaceNa
authkeys <- checkAuthorizedKeys(ctx, a.apiClient, user.Login)
}()
codespace, token, err := getOrChooseCodespace(ctx, a.apiClient, user, codespaceName)
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
if err != nil {
return fmt.Errorf("get or choose codespace: %w", err)
}
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, user.Login, token, codespace)
session, err := codespaces.ConnectToLiveshare(ctx, a.logger, a.apiClient, codespace)
if err != nil {
return fmt.Errorf("error connecting to Live Share: %w", err)
}