forked from cli/cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcodespaces.go
More file actions
149 lines (122 loc) · 4.48 KB
/
codespaces.go
File metadata and controls
149 lines (122 loc) · 4.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package codespaces
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/internal/codespaces/connection"
)
// codespaceStatePollingBackoff is the delay between state polls while waiting for codespaces to become
// available. It's only exposed so that it can be shortened for testing, otherwise it should not be changed
var codespaceStatePollingBackoff backoff.BackOff = backoff.NewExponentialBackOff(
backoff.WithInitialInterval(1*time.Second),
backoff.WithMultiplier(1.02),
backoff.WithMaxInterval(10*time.Second),
backoff.WithMaxElapsedTime(5*time.Minute),
)
func connectionReady(codespace *api.Codespace) bool {
// If the codespace is not available, it is not ready
if codespace.State != api.CodespaceStateAvailable {
return false
}
return codespace.Connection.TunnelProperties.ConnectAccessToken != "" &&
codespace.Connection.TunnelProperties.ManagePortsAccessToken != "" &&
codespace.Connection.TunnelProperties.ServiceUri != "" &&
codespace.Connection.TunnelProperties.TunnelId != "" &&
codespace.Connection.TunnelProperties.ClusterId != "" &&
codespace.Connection.TunnelProperties.Domain != ""
}
type apiClient interface {
GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
StartCodespace(ctx context.Context, name string) error
HTTPClient() (*http.Client, error)
}
type progressIndicator interface {
StartProgressIndicatorWithLabel(s string)
StopProgressIndicator()
}
type TimeoutError struct {
message string
}
func (e *TimeoutError) Error() string {
return e.message
}
// GetCodespaceConnection waits until a codespace is able
// to be connected to and initializes a connection to it.
func GetCodespaceConnection(ctx context.Context, progress progressIndicator, apiClient apiClient, codespace *api.Codespace) (*connection.CodespaceConnection, error) {
codespace, err := waitUntilCodespaceConnectionReady(ctx, progress, apiClient, codespace)
if err != nil {
return nil, err
}
progress.StartProgressIndicatorWithLabel("Connecting to codespace")
defer progress.StopProgressIndicator()
httpClient, err := apiClient.HTTPClient()
if err != nil {
return nil, fmt.Errorf("error getting http client: %w", err)
}
return connection.NewCodespaceConnection(ctx, codespace, httpClient)
}
// waitUntilCodespaceConnectionReady waits for a Codespace to be running and is able to be connected to.
func waitUntilCodespaceConnectionReady(ctx context.Context, progress progressIndicator, apiClient apiClient, codespace *api.Codespace) (*api.Codespace, error) {
if connectionReady(codespace) {
return codespace, nil
}
progress.StartProgressIndicatorWithLabel("Waiting for codespace to become ready")
defer progress.StopProgressIndicator()
lastState := ""
firstRetry := true
err := backoff.Retry(func() error {
var err error
if firstRetry {
firstRetry = false
} else {
codespace, err = apiClient.GetCodespace(ctx, codespace.Name, true)
if err != nil {
return backoff.Permanent(fmt.Errorf("error getting codespace: %w", err))
}
}
if connectionReady(codespace) {
return nil
}
// Only react to changes in the state (so that we don't try to start the codespace twice)
if codespace.State != lastState {
if codespace.State == api.CodespaceStateShutdown {
err = apiClient.StartCodespace(ctx, codespace.Name)
if err != nil {
return backoff.Permanent(fmt.Errorf("error starting codespace: %w", err))
}
}
}
lastState = codespace.State
return &TimeoutError{message: "codespace not ready yet"}
}, backoff.WithContext(codespaceStatePollingBackoff, ctx))
if err != nil {
var timeoutErr *TimeoutError
if errors.As(err, &timeoutErr) {
return nil, errors.New("timed out while waiting for the codespace to start")
}
return nil, err
}
return codespace, nil
}
// ListenTCP starts a localhost tcp listener on 127.0.0.1 (unless allInterfaces is true) and returns the listener and bound port
func ListenTCP(port int, allInterfaces bool) (*net.TCPListener, int, error) {
host := "127.0.0.1"
if allInterfaces {
host = ""
}
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return nil, 0, fmt.Errorf("failed to build tcp address: %w", err)
}
listener, err := net.ListenTCP("tcp", addr)
if err != nil {
return nil, 0, fmt.Errorf("failed to listen to local port over tcp: %w", err)
}
port = listener.Addr().(*net.TCPAddr).Port
return listener, port, nil
}