Path: blob/main/components/supervisor/pkg/ports/exposed-ports.go
2500 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package ports56import (7"context"8"fmt"9"net/url"10"time"1112backoff "github.com/cenkalti/backoff/v4"13"github.com/gitpod-io/gitpod/common-go/log"14gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"15"github.com/gitpod-io/gitpod/supervisor/pkg/serverapi"16)1718// ExposedPort represents an exposed pprt19type ExposedPort struct {20LocalPort uint3221URL string22Public bool23Protocol string24}2526// ExposedPortsInterface provides access to port exposure27type ExposedPortsInterface interface {28// Observe starts observing the exposed ports until the context is canceled.29// The list of exposed ports is always the complete picture, i.e. if a single port changes,30// the whole list is returned.31// When the observer stops operating (because the context as canceled or an irrecoverable32// error occured), the observer will close both channels.33Observe(ctx context.Context) (<-chan []ExposedPort, <-chan error)3435// Run starts listening to expose port requests.36Run(ctx context.Context)3738// Expose exposes a port to the internet. Upon successful execution any Observer will be updated.39Expose(ctx context.Context, port uint32, public bool, protocol string) <-chan error40}4142// NoopExposedPorts implements ExposedPortsInterface but does nothing43type NoopExposedPorts struct{}4445// Observe starts observing the exposed ports until the context is canceled.46func (*NoopExposedPorts) Observe(ctx context.Context) (<-chan []ExposedPort, <-chan error) {47return make(<-chan []ExposedPort), make(<-chan error)48}4950// Run starts listening to expose port requests.51func (*NoopExposedPorts) Run(ctx context.Context) {}5253// Expose exposes a port to the internet. Upon successful execution any Observer will be updated.54func (*NoopExposedPorts) Expose(ctx context.Context, local uint32, public bool, protocol string) <-chan error {55done := make(chan error)56close(done)57return done58}5960// GitpodExposedPorts uses a connection to the Gitpod server to implement61// the ExposedPortsInterface.62type GitpodExposedPorts struct {63WorkspaceID string64InstanceID string65WorkspaceUrl string66gitpodService serverapi.APIInterface6768localExposedPort []uint3269localExposedNotice chan struct{}70lastServerExposed []*gitpod.WorkspaceInstancePort7172requests chan *exposePortRequest73}7475type exposePortRequest struct {76port *gitpod.WorkspaceInstancePort77ctx context.Context78done chan error79}8081// NewGitpodExposedPorts creates a new instance of GitpodExposedPorts82func NewGitpodExposedPorts(workspaceID string, instanceID string, workspaceUrl string, gitpodService serverapi.APIInterface) *GitpodExposedPorts {83return &GitpodExposedPorts{84WorkspaceID: workspaceID,85InstanceID: instanceID,86WorkspaceUrl: workspaceUrl,87gitpodService: gitpodService,8889// allow clients to submit 3000 expose requests without blocking90requests: make(chan *exposePortRequest, 3000),91localExposedNotice: make(chan struct{}, 3000),92}93}9495func (g *GitpodExposedPorts) getPortUrl(port uint32) string {96u, err := url.Parse(g.WorkspaceUrl)97if err != nil {98return ""99}100u.Host = fmt.Sprintf("%d-%s", port, u.Host)101return u.String()102}103104func (g *GitpodExposedPorts) getPortProtocol(protocol string) string {105switch protocol {106case gitpod.PortProtocolHTTP, gitpod.PortProtocolHTTPS:107return protocol108default:109return gitpod.PortProtocolHTTP110}111}112113func (g *GitpodExposedPorts) existInLocalExposed(port uint32) bool {114for _, p := range g.localExposedPort {115if p == port {116return true117}118}119return false120}121122// Observe starts observing the exposed ports until the context is canceled.123func (g *GitpodExposedPorts) Observe(ctx context.Context) (<-chan []ExposedPort, <-chan error) {124var (125reschan = make(chan []ExposedPort)126errchan = make(chan error, 1)127)128129go func() {130defer close(reschan)131defer close(errchan)132133updates, err := g.gitpodService.WorkspaceUpdates(ctx)134if err != nil {135errchan <- err136return137}138mixin := func(localExposedPort []uint32, serverExposePort []*gitpod.WorkspaceInstancePort) []ExposedPort {139res := make(map[uint32]ExposedPort)140for _, port := range g.localExposedPort {141res[port] = ExposedPort{142LocalPort: port,143Public: false,144URL: g.getPortUrl(port),145Protocol: gitpod.PortProtocolHTTP,146}147}148149for _, p := range serverExposePort {150res[uint32(p.Port)] = ExposedPort{151LocalPort: uint32(p.Port),152Public: p.Visibility == "public",153URL: g.getPortUrl(uint32(p.Port)),154Protocol: g.getPortProtocol(p.Protocol),155}156}157exposedPort := make([]ExposedPort, 0, len(res))158for _, p := range res {159exposedPort = append(exposedPort, p)160}161return exposedPort162}163for {164select {165case u := <-updates:166if u == nil {167return168}169g.lastServerExposed = u.Status.ExposedPorts170171res := mixin(g.localExposedPort, g.lastServerExposed)172reschan <- res173case <-g.localExposedNotice:174res := mixin(g.localExposedPort, g.lastServerExposed)175reschan <- res176case <-ctx.Done():177return178}179}180}()181182return reschan, errchan183}184185// Listen starts listening to expose port requests186func (g *GitpodExposedPorts) Run(ctx context.Context) {187// process multiple parallel requests but process one by one to avoid server/ws-manager rate limitting188// if it does not help then we try to expose the same port again with the exponential backoff.189for {190select {191case <-ctx.Done():192return193case req := <-g.requests:194g.doExpose(req)195}196}197}198199func (g *GitpodExposedPorts) doExpose(req *exposePortRequest) {200var err error201defer func() {202if err != nil {203req.done <- err204}205close(req.done)206}()207exp := &backoff.ExponentialBackOff{208InitialInterval: 2 * time.Second,209RandomizationFactor: 0.5,210Multiplier: 1.5,211MaxInterval: 30 * time.Second,212MaxElapsedTime: 0,213Stop: backoff.Stop,214Clock: backoff.SystemClock,215}216exp.Reset()217attempt := 0218for {219_, err = g.gitpodService.OpenPort(req.ctx, req.port)220if err == nil || req.ctx.Err() != nil || attempt == 5 {221return222}223delay := exp.NextBackOff()224log.WithError(err).225WithField("port", req.port).226WithField("attempt", attempt).227WithField("delay", delay.String()).228Error("failed to expose port, trying again...")229select {230case <-req.ctx.Done():231err = req.ctx.Err()232return233case <-time.After(delay):234attempt++235}236}237}238239// Expose exposes a port to the internet. Upon successful execution any Observer will be updated.240func (g *GitpodExposedPorts) Expose(ctx context.Context, local uint32, public bool, protocol string) <-chan error {241if protocol != gitpod.PortProtocolHTTPS && protocol != gitpod.PortProtocolHTTP {242protocol = gitpod.PortProtocolHTTP243}244if !public && protocol != gitpod.PortProtocolHTTPS {245if !g.existInLocalExposed(local) {246g.localExposedPort = append(g.localExposedPort, local)247g.localExposedNotice <- struct{}{}248}249c := make(chan error)250close(c)251return c252}253visibility := gitpod.PortVisibilityPrivate254if public {255visibility = gitpod.PortVisibilityPublic256}257req := &exposePortRequest{258port: &gitpod.WorkspaceInstancePort{259Port: float64(local),260Visibility: visibility,261Protocol: protocol,262},263ctx: ctx,264done: make(chan error),265}266g.requests <- req267return req.done268}269270271