Forward codespace ports over Dev Tunnels · hoffm/github-cli@e059f32 · GitHub
Skip to content

Commit e059f32

Browse files
committed
Forward codespace ports over Dev Tunnels
1 parent 48b0d53 commit e059f32

13 files changed

Lines changed: 1266 additions & 298 deletions

File tree

go.mod

Lines changed: 2 additions & 0 deletions

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
117117
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
118118
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
119119
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
120+
github.com/microsoft/dev-tunnels v0.0.21 h1:p4QP7C5ZOyP9bGbmanRjPxUMckfi9Z41Gl+KY4C11w0=
121+
github.com/microsoft/dev-tunnels v0.0.21/go.mod h1:frU++12T/oqxckXkDpTuYa427ncguEOodSPZcGCCrzQ=
120122
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
121123
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
122124
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
@@ -139,6 +141,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
139141
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
140142
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
141143
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
144+
github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ=
145+
github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4=
142146
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
143147
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
144148
github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 h1:kdEGVAV4sO46DPtb8k793jiecUEhaX9ixoIBt41HEGU=

internal/codespaces/api/api.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ type Codespace struct {
201201
GitStatus CodespaceGitStatus `json:"git_status"`
202202
Connection CodespaceConnection `json:"connection"`
203203
Machine CodespaceMachine `json:"machine"`
204+
RuntimeConstraints RuntimeConstraints `json:"runtime_constraints"`
204205
VSCSTarget string `json:"vscs_target"`
205206
PendingOperation bool `json:"pending_operation"`
206207
PendingOperationDisabledReason string `json:"pending_operation_disabled_reason"`
@@ -246,11 +247,25 @@ const (
246247
)
247248

248249
type CodespaceConnection struct {
249-
SessionID string `json:"sessionId"`
250-
SessionToken string `json:"sessionToken"`
251-
RelayEndpoint string `json:"relayEndpoint"`
252-
RelaySAS string `json:"relaySas"`
253-
HostPublicKeys []string `json:"hostPublicKeys"`
250+
SessionID string `json:"sessionId"`
251+
SessionToken string `json:"sessionToken"`
252+
RelayEndpoint string `json:"relayEndpoint"`
253+
RelaySAS string `json:"relaySas"`
254+
HostPublicKeys []string `json:"hostPublicKeys"`
255+
TunnelProperties TunnelProperties `json:"tunnelProperties"`
256+
}
257+
258+
type TunnelProperties struct {
259+
ConnectAccessToken string `json:"connectAccessToken"`
260+
ManagePortsAccessToken string `json:"managePortsAccessToken"`
261+
ServiceUri string `json:"serviceUri"`
262+
TunnelId string `json:"tunnelId"`
263+
ClusterId string `json:"clusterId"`
264+
Domain string `json:"domain"`
265+
}
266+
267+
type RuntimeConstraints struct {
268+
AllowedPortPrivacySettings []string `json:"allowed_port_privacy_settings"`
254269
}
255270

256271
// ListCodespaceFields is the list of exportable fields for a codespace when using the `gh cs list` command.
@@ -1162,3 +1177,13 @@ func (a *API) withRetry(f func() (*http.Response, error)) (*http.Response, error
11621177
return nil, fmt.Errorf("received response with status code %d", resp.StatusCode)
11631178
}, backoff.WithMaxRetries(bo, 3))
11641179
}
1180+
1181+
// HTTPClient returns the HTTP client used to make requests to the API.
1182+
func (a *API) HTTPClient() (*http.Client, error) {
1183+
httpClient, err := a.client()
1184+
if err != nil {
1185+
return nil, err
1186+
}
1187+
1188+
return httpClient, nil
1189+
}

internal/codespaces/codespaces.go

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,42 @@ import (
55
"errors"
66
"fmt"
77
"net"
8+
"net/http"
89
"time"
910

1011
"github.com/cenkalti/backoff/v4"
1112
"github.com/cli/cli/v2/internal/codespaces/api"
13+
"github.com/cli/cli/v2/internal/codespaces/connection"
1214
"github.com/cli/cli/v2/pkg/liveshare"
1315
)
1416

15-
func connectionReady(codespace *api.Codespace) bool {
17+
func connectionReady(codespace *api.Codespace, usingDevTunnels bool) bool {
18+
// If the codespace is not available, it is not ready
19+
if codespace.State != api.CodespaceStateAvailable {
20+
return false
21+
}
22+
23+
// If using Dev Tunnels, we need to check that we have all of the required tunnel properties
24+
if usingDevTunnels {
25+
return codespace.Connection.TunnelProperties.ConnectAccessToken != "" &&
26+
codespace.Connection.TunnelProperties.ManagePortsAccessToken != "" &&
27+
codespace.Connection.TunnelProperties.ServiceUri != "" &&
28+
codespace.Connection.TunnelProperties.TunnelId != "" &&
29+
codespace.Connection.TunnelProperties.ClusterId != "" &&
30+
codespace.Connection.TunnelProperties.Domain != ""
31+
}
32+
33+
// If not using Dev Tunnels, we need to check that we have all of the required Live Share properties
1634
return codespace.Connection.SessionID != "" &&
1735
codespace.Connection.SessionToken != "" &&
1836
codespace.Connection.RelayEndpoint != "" &&
19-
codespace.Connection.RelaySAS != "" &&
20-
codespace.State == api.CodespaceStateAvailable
37+
codespace.Connection.RelaySAS != ""
2138
}
2239

2340
type apiClient interface {
2441
GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
2542
StartCodespace(ctx context.Context, name string) error
43+
HTTPClient() (*http.Client, error)
2644
}
2745

2846
type progressIndicator interface {
@@ -43,9 +61,48 @@ func (e *TimeoutError) Error() string {
4361
return e.message
4462
}
4563

46-
// ConnectToLiveshare waits for a Codespace to become running,
47-
// and connects to it using a Live Share session.
64+
// GetCodespaceConnection waits until a codespace is able
65+
// to be connected to and initializes a connection to it.
66+
func GetCodespaceConnection(ctx context.Context, progress progressIndicator, apiClient apiClient, codespace *api.Codespace) (*connection.CodespaceConnection, error) {
67+
codespace, err := waitUntilCodespaceConnectionReady(ctx, progress, apiClient, codespace, true)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
progress.StartProgressIndicatorWithLabel("Connecting to codespace")
73+
defer progress.StopProgressIndicator()
74+
75+
httpClient, err := apiClient.HTTPClient()
76+
if err != nil {
77+
return nil, fmt.Errorf("error getting http client: %w", err)
78+
}
79+
80+
return connection.NewCodespaceConnection(ctx, codespace, httpClient)
81+
}
82+
83+
// ConnectToLiveshare waits until a codespace is able to be
84+
// connected to and connects to it using a Live Share session.
4885
func ConnectToLiveshare(ctx context.Context, progress progressIndicator, sessionLogger logger, apiClient apiClient, codespace *api.Codespace) (*liveshare.Session, error) {
86+
codespace, err := waitUntilCodespaceConnectionReady(ctx, progress, apiClient, codespace, false)
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
progress.StartProgressIndicatorWithLabel("Connecting to codespace")
92+
defer progress.StopProgressIndicator()
93+
94+
return liveshare.Connect(ctx, liveshare.Options{
95+
SessionID: codespace.Connection.SessionID,
96+
SessionToken: codespace.Connection.SessionToken,
97+
RelaySAS: codespace.Connection.RelaySAS,
98+
RelayEndpoint: codespace.Connection.RelayEndpoint,
99+
HostPublicKeys: codespace.Connection.HostPublicKeys,
100+
Logger: sessionLogger,
101+
})
102+
}
103+
104+
// waitUntilCodespaceConnectionReady waits for a Codespace to be running and is able to be connected to.
105+
func waitUntilCodespaceConnectionReady(ctx context.Context, progress progressIndicator, apiClient apiClient, codespace *api.Codespace, usingDevTunnels bool) (*api.Codespace, error) {
49106
if codespace.State != api.CodespaceStateAvailable {
50107
progress.StartProgressIndicatorWithLabel("Starting codespace")
51108
defer progress.StopProgressIndicator()
@@ -54,7 +111,7 @@ func ConnectToLiveshare(ctx context.Context, progress progressIndicator, session
54111
}
55112
}
56113

57-
if !connectionReady(codespace) {
114+
if !connectionReady(codespace, usingDevTunnels) {
58115
expBackoff := backoff.NewExponentialBackOff()
59116
expBackoff.Multiplier = 1.1
60117
expBackoff.MaxInterval = 10 * time.Second
@@ -67,7 +124,7 @@ func ConnectToLiveshare(ctx context.Context, progress progressIndicator, session
67124
return backoff.Permanent(fmt.Errorf("error getting codespace: %w", err))
68125
}
69126

70-
if connectionReady(codespace) {
127+
if connectionReady(codespace, usingDevTunnels) {
71128
return nil
72129
}
73130

@@ -83,17 +140,7 @@ func ConnectToLiveshare(ctx context.Context, progress progressIndicator, session
83140
}
84141
}
85142

86-
progress.StartProgressIndicatorWithLabel("Connecting to codespace")
87-
defer progress.StopProgressIndicator()
88-
89-
return liveshare.Connect(ctx, liveshare.Options{
90-
SessionID: codespace.Connection.SessionID,
91-
SessionToken: codespace.Connection.SessionToken,
92-
RelaySAS: codespace.Connection.RelaySAS,
93-
RelayEndpoint: codespace.Connection.RelayEndpoint,
94-
HostPublicKeys: codespace.Connection.HostPublicKeys,
95-
Logger: sessionLogger,
96-
})
143+
return codespace, nil
97144
}
98145

99146
// ListenTCP starts a localhost tcp listener on 127.0.0.1 (unless allInterfaces is true) and returns the listener and bound port
Lines changed: 116 additions & 0 deletions

0 commit comments

Comments
 (0)