Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/supervisor/pkg/ports/exposed-ports.go
2500 views
1
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package ports
6
7
import (
8
"context"
9
"fmt"
10
"net/url"
11
"time"
12
13
backoff "github.com/cenkalti/backoff/v4"
14
"github.com/gitpod-io/gitpod/common-go/log"
15
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
16
"github.com/gitpod-io/gitpod/supervisor/pkg/serverapi"
17
)
18
19
// ExposedPort represents an exposed pprt
20
type ExposedPort struct {
21
LocalPort uint32
22
URL string
23
Public bool
24
Protocol string
25
}
26
27
// ExposedPortsInterface provides access to port exposure
28
type ExposedPortsInterface interface {
29
// Observe starts observing the exposed ports until the context is canceled.
30
// The list of exposed ports is always the complete picture, i.e. if a single port changes,
31
// the whole list is returned.
32
// When the observer stops operating (because the context as canceled or an irrecoverable
33
// error occured), the observer will close both channels.
34
Observe(ctx context.Context) (<-chan []ExposedPort, <-chan error)
35
36
// Run starts listening to expose port requests.
37
Run(ctx context.Context)
38
39
// Expose exposes a port to the internet. Upon successful execution any Observer will be updated.
40
Expose(ctx context.Context, port uint32, public bool, protocol string) <-chan error
41
}
42
43
// NoopExposedPorts implements ExposedPortsInterface but does nothing
44
type NoopExposedPorts struct{}
45
46
// Observe starts observing the exposed ports until the context is canceled.
47
func (*NoopExposedPorts) Observe(ctx context.Context) (<-chan []ExposedPort, <-chan error) {
48
return make(<-chan []ExposedPort), make(<-chan error)
49
}
50
51
// Run starts listening to expose port requests.
52
func (*NoopExposedPorts) Run(ctx context.Context) {}
53
54
// Expose exposes a port to the internet. Upon successful execution any Observer will be updated.
55
func (*NoopExposedPorts) Expose(ctx context.Context, local uint32, public bool, protocol string) <-chan error {
56
done := make(chan error)
57
close(done)
58
return done
59
}
60
61
// GitpodExposedPorts uses a connection to the Gitpod server to implement
62
// the ExposedPortsInterface.
63
type GitpodExposedPorts struct {
64
WorkspaceID string
65
InstanceID string
66
WorkspaceUrl string
67
gitpodService serverapi.APIInterface
68
69
localExposedPort []uint32
70
localExposedNotice chan struct{}
71
lastServerExposed []*gitpod.WorkspaceInstancePort
72
73
requests chan *exposePortRequest
74
}
75
76
type exposePortRequest struct {
77
port *gitpod.WorkspaceInstancePort
78
ctx context.Context
79
done chan error
80
}
81
82
// NewGitpodExposedPorts creates a new instance of GitpodExposedPorts
83
func NewGitpodExposedPorts(workspaceID string, instanceID string, workspaceUrl string, gitpodService serverapi.APIInterface) *GitpodExposedPorts {
84
return &GitpodExposedPorts{
85
WorkspaceID: workspaceID,
86
InstanceID: instanceID,
87
WorkspaceUrl: workspaceUrl,
88
gitpodService: gitpodService,
89
90
// allow clients to submit 3000 expose requests without blocking
91
requests: make(chan *exposePortRequest, 3000),
92
localExposedNotice: make(chan struct{}, 3000),
93
}
94
}
95
96
func (g *GitpodExposedPorts) getPortUrl(port uint32) string {
97
u, err := url.Parse(g.WorkspaceUrl)
98
if err != nil {
99
return ""
100
}
101
u.Host = fmt.Sprintf("%d-%s", port, u.Host)
102
return u.String()
103
}
104
105
func (g *GitpodExposedPorts) getPortProtocol(protocol string) string {
106
switch protocol {
107
case gitpod.PortProtocolHTTP, gitpod.PortProtocolHTTPS:
108
return protocol
109
default:
110
return gitpod.PortProtocolHTTP
111
}
112
}
113
114
func (g *GitpodExposedPorts) existInLocalExposed(port uint32) bool {
115
for _, p := range g.localExposedPort {
116
if p == port {
117
return true
118
}
119
}
120
return false
121
}
122
123
// Observe starts observing the exposed ports until the context is canceled.
124
func (g *GitpodExposedPorts) Observe(ctx context.Context) (<-chan []ExposedPort, <-chan error) {
125
var (
126
reschan = make(chan []ExposedPort)
127
errchan = make(chan error, 1)
128
)
129
130
go func() {
131
defer close(reschan)
132
defer close(errchan)
133
134
updates, err := g.gitpodService.WorkspaceUpdates(ctx)
135
if err != nil {
136
errchan <- err
137
return
138
}
139
mixin := func(localExposedPort []uint32, serverExposePort []*gitpod.WorkspaceInstancePort) []ExposedPort {
140
res := make(map[uint32]ExposedPort)
141
for _, port := range g.localExposedPort {
142
res[port] = ExposedPort{
143
LocalPort: port,
144
Public: false,
145
URL: g.getPortUrl(port),
146
Protocol: gitpod.PortProtocolHTTP,
147
}
148
}
149
150
for _, p := range serverExposePort {
151
res[uint32(p.Port)] = ExposedPort{
152
LocalPort: uint32(p.Port),
153
Public: p.Visibility == "public",
154
URL: g.getPortUrl(uint32(p.Port)),
155
Protocol: g.getPortProtocol(p.Protocol),
156
}
157
}
158
exposedPort := make([]ExposedPort, 0, len(res))
159
for _, p := range res {
160
exposedPort = append(exposedPort, p)
161
}
162
return exposedPort
163
}
164
for {
165
select {
166
case u := <-updates:
167
if u == nil {
168
return
169
}
170
g.lastServerExposed = u.Status.ExposedPorts
171
172
res := mixin(g.localExposedPort, g.lastServerExposed)
173
reschan <- res
174
case <-g.localExposedNotice:
175
res := mixin(g.localExposedPort, g.lastServerExposed)
176
reschan <- res
177
case <-ctx.Done():
178
return
179
}
180
}
181
}()
182
183
return reschan, errchan
184
}
185
186
// Listen starts listening to expose port requests
187
func (g *GitpodExposedPorts) Run(ctx context.Context) {
188
// process multiple parallel requests but process one by one to avoid server/ws-manager rate limitting
189
// if it does not help then we try to expose the same port again with the exponential backoff.
190
for {
191
select {
192
case <-ctx.Done():
193
return
194
case req := <-g.requests:
195
g.doExpose(req)
196
}
197
}
198
}
199
200
func (g *GitpodExposedPorts) doExpose(req *exposePortRequest) {
201
var err error
202
defer func() {
203
if err != nil {
204
req.done <- err
205
}
206
close(req.done)
207
}()
208
exp := &backoff.ExponentialBackOff{
209
InitialInterval: 2 * time.Second,
210
RandomizationFactor: 0.5,
211
Multiplier: 1.5,
212
MaxInterval: 30 * time.Second,
213
MaxElapsedTime: 0,
214
Stop: backoff.Stop,
215
Clock: backoff.SystemClock,
216
}
217
exp.Reset()
218
attempt := 0
219
for {
220
_, err = g.gitpodService.OpenPort(req.ctx, req.port)
221
if err == nil || req.ctx.Err() != nil || attempt == 5 {
222
return
223
}
224
delay := exp.NextBackOff()
225
log.WithError(err).
226
WithField("port", req.port).
227
WithField("attempt", attempt).
228
WithField("delay", delay.String()).
229
Error("failed to expose port, trying again...")
230
select {
231
case <-req.ctx.Done():
232
err = req.ctx.Err()
233
return
234
case <-time.After(delay):
235
attempt++
236
}
237
}
238
}
239
240
// Expose exposes a port to the internet. Upon successful execution any Observer will be updated.
241
func (g *GitpodExposedPorts) Expose(ctx context.Context, local uint32, public bool, protocol string) <-chan error {
242
if protocol != gitpod.PortProtocolHTTPS && protocol != gitpod.PortProtocolHTTP {
243
protocol = gitpod.PortProtocolHTTP
244
}
245
if !public && protocol != gitpod.PortProtocolHTTPS {
246
if !g.existInLocalExposed(local) {
247
g.localExposedPort = append(g.localExposedPort, local)
248
g.localExposedNotice <- struct{}{}
249
}
250
c := make(chan error)
251
close(c)
252
return c
253
}
254
visibility := gitpod.PortVisibilityPrivate
255
if public {
256
visibility = gitpod.PortVisibilityPublic
257
}
258
req := &exposePortRequest{
259
port: &gitpod.WorkspaceInstancePort{
260
Port: float64(local),
261
Visibility: visibility,
262
Protocol: protocol,
263
},
264
ctx: ctx,
265
done: make(chan error),
266
}
267
g.requests <- req
268
return req.done
269
}
270
271