Path: blob/main/components/supervisor/pkg/ports/ports_test.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"io"9"net"10"sync"11"testing"12"time"1314"github.com/gitpod-io/gitpod/common-go/log"15gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"16"github.com/gitpod-io/gitpod/supervisor/api"17"github.com/google/go-cmp/cmp"18"github.com/google/go-cmp/cmp/cmpopts"19"github.com/sirupsen/logrus"20"golang.org/x/sync/errgroup"21)2223func TestPortsUpdateState(t *testing.T) {24type ExposureExpectation []ExposedPort25type UpdateExpectation [][]*api.PortsStatus26type ConfigChange struct {27instance []*gitpod.PortsItems28}29type Change struct {30Config *ConfigChange31Served []ServedPort32Exposed []ExposedPort33Tunneled []PortTunnelState34ConfigErr error35ServedErr error36ExposedErr error37TunneledErr error38}39tests := []struct {40Desc string41InternalPorts []uint3242Changes []Change43ExpectedExposure ExposureExpectation44ExpectedUpdates UpdateExpectation45}{46{47Desc: "basic locally served",48Changes: []Change{49{Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 8080, true}}},50{Exposed: []ExposedPort{{LocalPort: 8080, URL: "foobar"}}},51{Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 8080, true}, {net.IPv4zero, 60000, false}}},52{Served: []ServedPort{{net.IPv4zero, 60000, false}}},53{Served: []ServedPort{}},54},55ExpectedExposure: []ExposedPort{56{LocalPort: 8080},57{LocalPort: 60000},58},59ExpectedUpdates: UpdateExpectation{60{},61[]*api.PortsStatus{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify_private}},62[]*api.PortsStatus{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{OnExposed: api.OnPortExposedAction_notify_private, Visibility: api.PortVisibility_private, Url: "foobar"}}},63[]*api.PortsStatus{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{OnExposed: api.OnPortExposedAction_notify_private, Visibility: api.PortVisibility_private, Url: "foobar"}}, {LocalPort: 60000, Served: true}},64[]*api.PortsStatus{{LocalPort: 8080, Served: false, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{OnExposed: api.OnPortExposedAction_notify_private, Visibility: api.PortVisibility_private, Url: "foobar"}}, {LocalPort: 60000, Served: true}},65[]*api.PortsStatus{{LocalPort: 8080, Served: false, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{OnExposed: api.OnPortExposedAction_notify_private, Visibility: api.PortVisibility_private, Url: "foobar"}}},66},67},68{69Desc: "basic globally served",70Changes: []Change{71{Served: []ServedPort{{net.IPv4zero, 8080, false}}},72{Served: []ServedPort{}},73},74ExpectedExposure: []ExposedPort{75{LocalPort: 8080},76},77ExpectedUpdates: UpdateExpectation{78{},79[]*api.PortsStatus{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify_private}},80{},81},82},83{84Desc: "basic port publically exposed",85Changes: []Change{86{Served: []ServedPort{{Port: 8080}}},87{Exposed: []ExposedPort{{LocalPort: 8080, Public: true, URL: "foobar"}}},88{Exposed: []ExposedPort{{LocalPort: 8080, Public: false, URL: "foobar"}}},89},90ExpectedExposure: ExposureExpectation{91{LocalPort: 8080},92},93ExpectedUpdates: UpdateExpectation{94{},95[]*api.PortsStatus{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify_private}},96[]*api.PortsStatus{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_public, Url: "foobar", OnExposed: api.OnPortExposedAction_notify_private}}},97[]*api.PortsStatus{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, Url: "foobar", OnExposed: api.OnPortExposedAction_notify_private}}},98},99},100{101Desc: "internal ports served",102InternalPorts: []uint32{8080},103Changes: []Change{104{Served: []ServedPort{}},105{Served: []ServedPort{{net.IPv4zero, 8080, false}}},106},107ExpectedExposure: ExposureExpectation(nil),108ExpectedUpdates: UpdateExpectation{{}},109},110{111Desc: "serving port from the configured port range",112Changes: []Change{113{Config: &ConfigChange{114instance: []*gitpod.PortsItems{{115OnOpen: "open-browser",116Port: "4000-5000",117}},118}},119{Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 4040, true}}},120{Exposed: []ExposedPort{{LocalPort: 4040, Public: true, URL: "4040-foobar"}}},121{Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 4040, true}, {net.IPv4zero, 60000, false}}},122},123ExpectedExposure: []ExposedPort{124{LocalPort: 4040},125{LocalPort: 60000},126},127ExpectedUpdates: UpdateExpectation{128{},129{},130[]*api.PortsStatus{{LocalPort: 4040, Served: true, OnOpen: api.PortsStatus_open_browser}},131[]*api.PortsStatus{{LocalPort: 4040, Served: true, OnOpen: api.PortsStatus_open_browser, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_public, Url: "4040-foobar", OnExposed: api.OnPortExposedAction_open_browser}}},132[]*api.PortsStatus{133{LocalPort: 4040, Served: true, OnOpen: api.PortsStatus_open_browser, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_public, Url: "4040-foobar", OnExposed: api.OnPortExposedAction_open_browser}},134{LocalPort: 60000, Served: true},135},136},137},138{139Desc: "auto expose configured ports",140Changes: []Change{141{142Config: &ConfigChange{instance: []*gitpod.PortsItems{143{Port: 8080, Visibility: "private"},144}},145},146{147Exposed: []ExposedPort{{LocalPort: 8080, Public: false, URL: "foobar"}},148},149{150Exposed: []ExposedPort{{LocalPort: 8080, Public: true, URL: "foobar"}},151},152{153Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 8080, true}},154},155{156Exposed: []ExposedPort{{LocalPort: 8080, Public: true, URL: "foobar"}},157},158{159Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 8080, true}},160},161{162Served: []ServedPort{},163},164{165Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 8080, false}},166},167},168ExpectedExposure: []ExposedPort{169{LocalPort: 8080, Public: false},170},171ExpectedUpdates: UpdateExpectation{172{},173[]*api.PortsStatus{{LocalPort: 8080, OnOpen: api.PortsStatus_notify}},174[]*api.PortsStatus{{LocalPort: 8080, OnOpen: api.PortsStatus_notify, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify, Url: "foobar"}}},175[]*api.PortsStatus{{LocalPort: 8080, OnOpen: api.PortsStatus_notify, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_public, OnExposed: api.OnPortExposedAction_notify, Url: "foobar"}}},176[]*api.PortsStatus{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_public, OnExposed: api.OnPortExposedAction_notify, Url: "foobar"}}},177[]*api.PortsStatus{{LocalPort: 8080, OnOpen: api.PortsStatus_notify, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_public, OnExposed: api.OnPortExposedAction_notify, Url: "foobar"}}},178[]*api.PortsStatus{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_public, OnExposed: api.OnPortExposedAction_notify, Url: "foobar"}}},179},180},181{182Desc: "starting multiple proxies for the same served event",183Changes: []Change{184{185Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 8080, true}, {net.IPv4zero, 3000, true}},186},187},188ExpectedExposure: []ExposedPort{189{LocalPort: 8080},190{LocalPort: 3000},191},192ExpectedUpdates: UpdateExpectation{193{},194{195{LocalPort: 3000, Served: true, OnOpen: api.PortsStatus_notify_private},196{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify_private},197},198},199},200{201Desc: "served between auto exposing configured and exposed update",202Changes: []Change{203{204Config: &ConfigChange{instance: []*gitpod.PortsItems{205{Port: 8080, Visibility: "private"},206}},207},208{209Served: []ServedPort{{net.IPv4zero, 8080, false}},210},211{212Exposed: []ExposedPort{{LocalPort: 8080, Public: false, URL: "foobar"}},213},214},215ExpectedExposure: []ExposedPort{216{LocalPort: 8080},217},218ExpectedUpdates: UpdateExpectation{219{},220{{LocalPort: 8080, OnOpen: api.PortsStatus_notify}},221{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify}},222{{LocalPort: 8080, Served: true, OnOpen: api.PortsStatus_notify, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify, Url: "foobar"}}},223},224},225{226Desc: "the same port served locally and then globally too, prefer globally (exposed in between)",227Changes: []Change{228{229Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 5900, true}},230},231{232Exposed: []ExposedPort{{LocalPort: 5900, URL: "foobar"}},233},234{235Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 5900, true}, {net.IPv4zero, 5900, false}},236},237{238Exposed: []ExposedPort{{LocalPort: 5900, URL: "foobar"}},239},240},241ExpectedExposure: []ExposedPort{242{LocalPort: 5900},243},244ExpectedUpdates: UpdateExpectation{245{},246{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private}},247{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify_private, Url: "foobar"}}},248},249},250{251Desc: "the same port served locally and then globally too, prefer globally (exposed after)",252Changes: []Change{253{254Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 5900, true}},255},256{257Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 5900, true}, {net.IPv4zero, 5900, false}},258},259{260Exposed: []ExposedPort{{LocalPort: 5900, URL: "foobar"}},261},262{263Exposed: []ExposedPort{{LocalPort: 5900, URL: "foobar"}},264},265},266ExpectedExposure: []ExposedPort{267{LocalPort: 5900},268},269ExpectedUpdates: UpdateExpectation{270{},271{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private}},272{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify_private, Url: "foobar"}}},273},274},275{276Desc: "the same port served globally and then locally too, prefer globally (exposed in between)",277Changes: []Change{278{279Served: []ServedPort{{net.IPv4zero, 5900, false}},280},281{282Exposed: []ExposedPort{{LocalPort: 5900, URL: "foobar"}},283},284{285Served: []ServedPort{{net.IPv4zero, 5900, false}, {net.IPv4(127, 0, 0, 1), 5900, true}},286},287},288ExpectedExposure: []ExposedPort{289{LocalPort: 5900},290},291ExpectedUpdates: UpdateExpectation{292{},293{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private}},294{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify_private, Url: "foobar"}}},295},296},297{298Desc: "the same port served globally and then locally too, prefer globally (exposed after)",299Changes: []Change{300{301Served: []ServedPort{{net.IPv4zero, 5900, false}},302},303{304Served: []ServedPort{{net.IPv4zero, 5900, false}, {net.IPv4(127, 0, 0, 1), 5900, true}},305},306{307Exposed: []ExposedPort{{LocalPort: 5900, URL: "foobar"}},308},309},310ExpectedExposure: []ExposedPort{311{LocalPort: 5900},312},313ExpectedUpdates: UpdateExpectation{314{},315{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private}},316{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify_private, Url: "foobar"}}},317},318},319{320Desc: "the same port served locally on ip4 and then locally on ip6 too, prefer first (exposed in between)",321Changes: []Change{322{323Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 5900, true}},324},325{326Exposed: []ExposedPort{{LocalPort: 5900, URL: "foobar"}},327},328{329Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 5900, true}, {net.IPv6zero, 5900, true}},330},331},332ExpectedExposure: []ExposedPort{333{LocalPort: 5900},334},335ExpectedUpdates: UpdateExpectation{336{},337{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private}},338{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify_private, Url: "foobar"}}},339},340},341{342Desc: "the same port served locally on ip4 and then locally on ip6 too, prefer first (exposed after)",343Changes: []Change{344{345Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 5900, true}},346},347{348Served: []ServedPort{{net.IPv4(127, 0, 0, 1), 5900, true}, {net.IPv6zero, 5900, true}},349},350{351Exposed: []ExposedPort{{LocalPort: 5900, URL: "foobar"}},352},353},354ExpectedExposure: []ExposedPort{355{LocalPort: 5900},356},357ExpectedUpdates: UpdateExpectation{358{},359{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private}},360{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify_private, Url: "foobar"}}},361},362},363{364Desc: "the same port served locally on ip4 and then globally on ip6 too, prefer first (exposed in between)",365Changes: []Change{366{367Served: []ServedPort{{net.IPv4zero, 5900, false}},368},369{370Exposed: []ExposedPort{{LocalPort: 5900, URL: "foobar"}},371},372{373Served: []ServedPort{{net.IPv4zero, 5900, false}, {net.IPv6zero, 5900, false}},374},375},376ExpectedExposure: []ExposedPort{377{LocalPort: 5900},378},379ExpectedUpdates: UpdateExpectation{380{},381{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private}},382{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify_private, Url: "foobar"}}},383},384},385{386Desc: "the same port served locally on ip4 and then globally on ip6 too, prefer first (exposed after)",387Changes: []Change{388{389Served: []ServedPort{{net.IPv4zero, 5900, false}},390},391{392Served: []ServedPort{{net.IPv4zero, 5900, false}, {net.IPv6zero, 5900, false}},393},394{395Exposed: []ExposedPort{{LocalPort: 5900, URL: "foobar"}},396},397},398ExpectedExposure: []ExposedPort{399{LocalPort: 5900},400},401ExpectedUpdates: UpdateExpectation{402{},403{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private}},404{{LocalPort: 5900, Served: true, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify_private, Url: "foobar"}}},405},406},407{408Desc: "port status has description set as soon as the port gets exposed, if there was a description configured",409Changes: []Change{410{411Config: &ConfigChange{instance: []*gitpod.PortsItems{412{Port: 8080, Visibility: "private", Description: "Development server"},413}},414},415{416Served: []ServedPort{{net.IPv4zero, 8080, false}},417},418{419Exposed: []ExposedPort{{LocalPort: 8080, Public: false, URL: "foobar"}},420},421},422ExpectedExposure: []ExposedPort{423{LocalPort: 8080},424},425ExpectedUpdates: UpdateExpectation{426{},427{{LocalPort: 8080, Description: "Development server", OnOpen: api.PortsStatus_notify}},428{{LocalPort: 8080, Description: "Development server", Served: true, OnOpen: api.PortsStatus_notify}},429{{LocalPort: 8080, Description: "Development server", Served: true, OnOpen: api.PortsStatus_notify, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify, Url: "foobar"}}},430},431},432{433Desc: "port status has the name attribute set as soon as the port gets exposed, if there was a name configured in Gitpod's Workspace",434Changes: []Change{435{436Config: &ConfigChange{instance: []*gitpod.PortsItems{437{Port: 3000, Visibility: "private", Name: "react"},438}},439},440{441Served: []ServedPort{{net.IPv4zero, 3000, false}},442},443{444Exposed: []ExposedPort{{LocalPort: 3000, Public: false, URL: "foobar"}},445},446},447ExpectedExposure: []ExposedPort{448{LocalPort: 3000},449},450ExpectedUpdates: UpdateExpectation{451{},452{{LocalPort: 3000, Name: "react", OnOpen: api.PortsStatus_notify}},453{{LocalPort: 3000, Name: "react", Served: true, OnOpen: api.PortsStatus_notify}},454{{LocalPort: 3000, Name: "react", Served: true, OnOpen: api.PortsStatus_notify, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify, Url: "foobar"}}},455},456},457{458Desc: "change configed ports order",459Changes: []Change{460{461Config: &ConfigChange{instance: []*gitpod.PortsItems{462{Port: 3001, Visibility: "private", Name: "react"},463{Port: 3000, Visibility: "private", Name: "react"},464}},465},466{467Config: &ConfigChange{instance: []*gitpod.PortsItems{468{Port: "5000-5999", Visibility: "private", Name: "react"},469{Port: 3001, Visibility: "private", Name: "react"},470{Port: 3000, Visibility: "private", Name: "react"},471}},472},473{474Served: []ServedPort{{net.IPv4zero, 5002, false}},475},476{477Served: []ServedPort{{net.IPv4zero, 5002, false}, {net.IPv4zero, 5001, false}},478},479{480Config: &ConfigChange{instance: []*gitpod.PortsItems{481{Port: 3000, Visibility: "private", Name: "react"},482{Port: 3001, Visibility: "private", Name: "react"},483}},484},485{486Served: []ServedPort{{net.IPv4zero, 5001, false}, {net.IPv4zero, 3000, false}},487},488{489Exposed: []ExposedPort{{LocalPort: 3000, Public: false, URL: "foobar"}},490},491},492ExpectedExposure: []ExposedPort{493{LocalPort: 5002},494{LocalPort: 5001},495{LocalPort: 3000},496{LocalPort: 3001},497},498ExpectedUpdates: UpdateExpectation{499{},500{501{LocalPort: 3001, Name: "react", OnOpen: api.PortsStatus_notify},502{LocalPort: 3000, Name: "react", OnOpen: api.PortsStatus_notify},503},504{505{LocalPort: 3001, Name: "react", OnOpen: api.PortsStatus_notify},506{LocalPort: 3000, Name: "react", OnOpen: api.PortsStatus_notify},507},508{509{LocalPort: 5002, Name: "react", Served: true, OnOpen: api.PortsStatus_notify},510{LocalPort: 3001, Name: "react", OnOpen: api.PortsStatus_notify},511{LocalPort: 3000, Name: "react", OnOpen: api.PortsStatus_notify},512},513{514{LocalPort: 5001, Name: "react", Served: true, OnOpen: api.PortsStatus_notify},515{LocalPort: 5002, Name: "react", Served: true, OnOpen: api.PortsStatus_notify},516{LocalPort: 3001, Name: "react", OnOpen: api.PortsStatus_notify},517{LocalPort: 3000, Name: "react", OnOpen: api.PortsStatus_notify},518},519{520{LocalPort: 3000, Name: "react", OnOpen: api.PortsStatus_notify},521{LocalPort: 3001, Name: "react", OnOpen: api.PortsStatus_notify},522{LocalPort: 5001, Served: true, OnOpen: api.PortsStatus_notify_private},523{LocalPort: 5002, Served: true, OnOpen: api.PortsStatus_notify_private},524},525{526{LocalPort: 3000, Name: "react", Served: true, OnOpen: api.PortsStatus_notify},527{LocalPort: 3001, Name: "react", OnOpen: api.PortsStatus_notify},528{LocalPort: 5001, Served: true, OnOpen: api.PortsStatus_notify_private},529},530{531{LocalPort: 3000, Name: "react", Served: true, OnOpen: api.PortsStatus_notify, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify, Url: "foobar"}},532{LocalPort: 3001, Name: "react", OnOpen: api.PortsStatus_notify},533{LocalPort: 5001, Served: true, OnOpen: api.PortsStatus_notify_private},534},535},536},537{538Desc: "change configed ports order with ranged covered not ranged",539Changes: []Change{540{541Config: &ConfigChange{542instance: []*gitpod.PortsItems{543{Port: 3001, Visibility: "private", Name: "react"},544{Port: 3000, Visibility: "private", Name: "react"},545},546},547},548{549Config: &ConfigChange{550instance: []*gitpod.PortsItems{551{Port: 3003, Visibility: "private", Name: "react"},552{Port: 3001, Visibility: "private", Name: "react"},553{Port: "3001-3005", Visibility: "private", Name: "react"},554{Port: 3000, Visibility: "private", Name: "react"},555},556},557},558{559Served: []ServedPort{{net.IPv4zero, 3000, false}},560},561{562Served: []ServedPort{{net.IPv4zero, 3000, false}, {net.IPv4zero, 3001, false}, {net.IPv4zero, 3002, false}},563},564{565Config: &ConfigChange{566instance: []*gitpod.PortsItems{567{Port: 3003, Visibility: "private", Name: "react"},568{Port: 3000, Visibility: "private", Name: "react"},569},570},571},572{573Config: &ConfigChange{574instance: []*gitpod.PortsItems{575{Port: "3001-3005", Visibility: "private", Name: "react"},576{Port: 3003, Visibility: "private", Name: "react"},577{Port: 3000, Visibility: "private", Name: "react"},578},579},580},581},582ExpectedExposure: []ExposedPort{583{LocalPort: 3000},584{LocalPort: 3001},585{LocalPort: 3002},586{LocalPort: 3003},587},588ExpectedUpdates: UpdateExpectation{589{},590{591{LocalPort: 3001, Name: "react", OnOpen: api.PortsStatus_notify},592{LocalPort: 3000, Name: "react", OnOpen: api.PortsStatus_notify},593},594{595{LocalPort: 3003, Name: "react", OnOpen: api.PortsStatus_notify},596{LocalPort: 3001, Name: "react", OnOpen: api.PortsStatus_notify},597{LocalPort: 3000, Name: "react", OnOpen: api.PortsStatus_notify},598},599{600{LocalPort: 3003, Name: "react", OnOpen: api.PortsStatus_notify},601{LocalPort: 3001, Name: "react", OnOpen: api.PortsStatus_notify},602{LocalPort: 3000, Served: true, Name: "react", OnOpen: api.PortsStatus_notify},603},604{605{LocalPort: 3003, Name: "react", OnOpen: api.PortsStatus_notify},606{LocalPort: 3001, Served: true, Name: "react", OnOpen: api.PortsStatus_notify},607{LocalPort: 3002, Served: true, Name: "react", OnOpen: api.PortsStatus_notify},608{LocalPort: 3000, Served: true, Name: "react", OnOpen: api.PortsStatus_notify},609},610{611{LocalPort: 3003, Name: "react", OnOpen: api.PortsStatus_notify},612{LocalPort: 3000, Served: true, Name: "react", OnOpen: api.PortsStatus_notify},613{LocalPort: 3001, Served: true, OnOpen: api.PortsStatus_notify_private},614{LocalPort: 3002, Served: true, OnOpen: api.PortsStatus_notify_private},615},616{617{LocalPort: 3001, Name: "react", Served: true, OnOpen: api.PortsStatus_notify},618{LocalPort: 3002, Name: "react", Served: true, OnOpen: api.PortsStatus_notify},619{LocalPort: 3003, Name: "react", OnOpen: api.PortsStatus_notify},620{LocalPort: 3000, Served: true, Name: "react", OnOpen: api.PortsStatus_notify},621},622},623},624{625// Please make sure this test pass for code browser resolveExternalPort626// see also https://github.com/gitpod-io/openvscode-server/blob/5ab7644a8bbf37d28e23212bc6f1529cafd8bf7b/extensions/gitpod-web/src/extension.ts#L310-L339627Desc: "expose port without served, port should be responded for use case of openvscode-server",628Changes: []Change{629{630Exposed: []ExposedPort{{LocalPort: 3000, Public: false, URL: "foobar"}},631},632},633// this will not exposed because test manager didn't implement it properly634// ExpectedExposure: []ExposedPort{635// {LocalPort: 3000},636// },637ExpectedUpdates: UpdateExpectation{638{},639{640{LocalPort: 3000, OnOpen: api.PortsStatus_notify_private, Exposed: &api.ExposedPortInfo{Visibility: api.PortVisibility_private, OnExposed: api.OnPortExposedAction_notify_private, Url: "foobar"}},641},642},643},644}645646log.Log.Logger.SetLevel(logrus.FatalLevel)647648for _, test := range tests {649t.Run(test.Desc, func(t *testing.T) {650var (651exposed = &testExposedPorts{652Changes: make(chan []ExposedPort),653Error: make(chan error, 1),654}655served = &testServedPorts{656Changes: make(chan []ServedPort),657Error: make(chan error, 1),658}659config = &testConfigService{660Changes: make(chan *Configs),661Error: make(chan error, 1),662}663tunneled = &testTunneledPorts{664Changes: make(chan []PortTunnelState),665Error: make(chan error, 1),666}667668pm = NewManager(exposed, served, config, tunneled, test.InternalPorts...)669updts [][]*api.PortsStatus670)671pm.proxyStarter = func(port uint32) (io.Closer, error) {672return io.NopCloser(nil), nil673}674675ctx, cancel := context.WithCancel(context.Background())676defer cancel()677var wg sync.WaitGroup678wg.Add(3)679go pm.Run(ctx, &wg)680sub, err := pm.Subscribe()681if err != nil {682t.Fatal(err)683}684go func() {685defer wg.Done()686defer sub.Close(true)687688for up := range sub.Updates() {689updts = append(updts, up)690}691}()692go func() {693defer wg.Done()694defer close(config.Error)695defer close(config.Changes)696defer close(served.Error)697defer close(served.Changes)698defer close(exposed.Error)699defer close(exposed.Changes)700defer close(tunneled.Error)701defer close(tunneled.Changes)702703for _, c := range test.Changes {704if c.Config != nil {705change := &Configs{}706portConfigs, rangeConfigs := parseInstanceConfigs(c.Config.instance)707change.instancePortConfigs = portConfigs708change.instanceRangeConfigs = rangeConfigs709config.Changes <- change710} else if c.ConfigErr != nil {711config.Error <- c.ConfigErr712} else if c.Served != nil {713served.Changes <- c.Served714} else if c.ServedErr != nil {715served.Error <- c.ServedErr716} else if c.Exposed != nil {717exposed.Changes <- c.Exposed718} else if c.ExposedErr != nil {719exposed.Error <- c.ExposedErr720} else if c.Tunneled != nil {721tunneled.Changes <- c.Tunneled722} else if c.TunneledErr != nil {723tunneled.Error <- c.TunneledErr724}725}726}()727728wg.Wait()729730var (731sortExposed = cmpopts.SortSlices(func(x, y ExposedPort) bool { return x.LocalPort < y.LocalPort })732ignoreUnexported = cmpopts.IgnoreUnexported(733api.PortsStatus{},734api.ExposedPortInfo{},735)736)737if diff := cmp.Diff(test.ExpectedExposure, ExposureExpectation(exposed.Exposures), sortExposed, ignoreUnexported); diff != "" {738t.Errorf("unexpected exposures (-want +got):\n%s", diff)739}740741if diff := cmp.Diff(test.ExpectedUpdates, UpdateExpectation(updts), ignoreUnexported); diff != "" {742t.Errorf("unexpected updates (-want +got):\n%s", diff)743}744})745}746}747748type testTunneledPorts struct {749Changes chan []PortTunnelState750Error chan error751}752753func (tep *testTunneledPorts) Observe(ctx context.Context) (<-chan []PortTunnelState, <-chan error) {754return tep.Changes, tep.Error755}756func (tep *testTunneledPorts) Tunnel(ctx context.Context, options *TunnelOptions, descs ...*PortTunnelDescription) ([]uint32, error) {757return nil, nil758}759func (tep *testTunneledPorts) CloseTunnel(ctx context.Context, localPorts ...uint32) ([]uint32, error) {760return nil, nil761}762func (tep *testTunneledPorts) EstablishTunnel(ctx context.Context, clientID string, localPort uint32, targetPort uint32) (net.Conn, error) {763return nil, nil764}765766type testConfigService struct {767Changes chan *Configs768Error chan error769}770771func (tep *testConfigService) Observe(ctx context.Context) (<-chan *Configs, <-chan error) {772return tep.Changes, tep.Error773}774775type testExposedPorts struct {776Changes chan []ExposedPort777Error chan error778779Exposures []ExposedPort780mu sync.Mutex781}782783func (tep *testExposedPorts) Observe(ctx context.Context) (<-chan []ExposedPort, <-chan error) {784return tep.Changes, tep.Error785}786787func (tep *testExposedPorts) Run(ctx context.Context) {788}789790func (tep *testExposedPorts) Expose(ctx context.Context, local uint32, public bool, protocol string) <-chan error {791tep.mu.Lock()792defer tep.mu.Unlock()793794tep.Exposures = append(tep.Exposures, ExposedPort{795LocalPort: local,796Public: public,797})798return nil799}800801type testServedPorts struct {802Changes chan []ServedPort803Error chan error804}805806func (tps *testServedPorts) Observe(ctx context.Context) (<-chan []ServedPort, <-chan error) {807return tps.Changes, tps.Error808}809810// testing for deadlocks between subscribing and processing events811func TestPortsConcurrentSubscribe(t *testing.T) {812var (813subscribes = 100814subscribing = make(chan struct{})815exposed = &testExposedPorts{816Changes: make(chan []ExposedPort),817Error: make(chan error, 1),818}819served = &testServedPorts{820Changes: make(chan []ServedPort),821Error: make(chan error, 1),822}823config = &testConfigService{824Changes: make(chan *Configs),825Error: make(chan error, 1),826}827tunneled = &testTunneledPorts{828Changes: make(chan []PortTunnelState),829Error: make(chan error, 1),830}831pm = NewManager(exposed, served, config, tunneled)832)833pm.proxyStarter = func(local uint32) (io.Closer, error) {834return io.NopCloser(nil), nil835}836837ctx, cancel := context.WithCancel(context.Background())838defer cancel()839var wg sync.WaitGroup840wg.Add(2)841go pm.Run(ctx, &wg)842go func() {843defer wg.Done()844defer close(config.Error)845defer close(config.Changes)846defer close(served.Error)847defer close(served.Changes)848defer close(exposed.Error)849defer close(exposed.Changes)850defer close(tunneled.Error)851defer close(tunneled.Changes)852853var j uint32854for {855856select {857case <-time.After(50 * time.Millisecond):858served.Changes <- []ServedPort{{Port: j}}859j++860case <-subscribing:861return862}863}864}()865866for i := 0; i < maxSubscriptions; i++ {867eg, _ := errgroup.WithContext(context.Background())868eg.Go(func() error {869for j := 0; j < subscribes; j++ {870sub, err := pm.Subscribe()871if err != nil {872return err873}874// status875select {876case <-sub.Updates():877// update878case <-sub.Updates():879}880sub.Close(true)881}882return nil883})884err := eg.Wait()885if err != nil {886t.Fatal(err)887}888time.Sleep(50 * time.Millisecond)889}890close(subscribing)891892wg.Wait()893}894895func TestManager_getStatus(t *testing.T) {896type portState struct {897port uint32898notServed bool899}900type fields struct {901orderInYaml []any902state []portState903}904tests := []struct {905name string906fields fields907want []uint32908}{909{910name: "happy path",911fields: fields{912// The port number (e.g. 1337) or range (e.g. 3000-3999) to expose.913orderInYaml: []any{1002, 1000, "3000-3999", 1001},914state: []portState{{port: 1000}, {port: 1001}, {port: 1002}, {port: 3003}, {port: 3001}, {port: 3002}, {port: 4002}, {port: 4000}, {port: 5000}, {port: 5005}},915},916want: []uint32{1002, 1000, 3001, 3002, 3003, 1001, 4000, 4002, 5000, 5005},917},918{919name: "order for ranged ports and inside ranged order by number ASC",920fields: fields{921orderInYaml: []any{1002, "3000-3999", 1009, "4000-4999"},922state: []portState{{port: 5000}, {port: 1000}, {port: 1009}, {port: 4000}, {port: 4001}, {port: 3000}, {port: 3009}},923},924want: []uint32{3000, 3009, 1009, 4000, 4001, 1000, 5000},925},926{927name: "served ports order by number ASC",928fields: fields{929orderInYaml: []any{},930state: []portState{{port: 4000}, {port: 4003}, {port: 4007}, {port: 4001}, {port: 4006}},931},932want: []uint32{4000, 4001, 4003, 4006, 4007},933},934{935// Please make sure this test pass for code browser resolveExternalPort936// see also https://github.com/gitpod-io/openvscode-server/blob/5ab7644a8bbf37d28e23212bc6f1529cafd8bf7b/extensions/gitpod-web/src/extension.ts#L310-L339937name: "expose not served ports should respond their status",938fields: fields{939orderInYaml: []any{},940state: []portState{{port: 4000, notServed: true}},941},942want: []uint32{4000},943},944// It will not works because we do not `Run` ports Manger945// As ports Manger will autoExpose those ports (but not ranged port) in yaml946// and they will exists in state947// {948// name: "not ignore ports that not served but exists in yaml",949// fields: fields{950// orderInYaml: []any{1002, 1000, 1001},951// state: []uint32{},952// },953// want: []uint32{1002, 1000, 1001},954// },955}956for _, tt := range tests {957t.Run(tt.name, func(t *testing.T) {958state := make(map[uint32]*managedPort)959for _, s := range tt.fields.state {960state[s.port] = &managedPort{961Served: !s.notServed,962LocalhostPort: s.port,963TunneledTargetPort: s.port,964TunneledClients: map[string]uint32{},965}966}967portsItems := []*gitpod.PortsItems{}968for _, port := range tt.fields.orderInYaml {969portsItems = append(portsItems, &gitpod.PortsItems{Port: port})970}971portsConfig, rangeConfig := parseInstanceConfigs(portsItems)972pm := &Manager{973configs: &Configs{974instancePortConfigs: portsConfig,975instanceRangeConfigs: rangeConfig,976},977state: state,978}979got := pm.getStatus()980if len(got) != len(tt.want) {981t.Errorf("Manager.getStatus() length = %v, want %v", len(got), len(tt.want))982}983gotPorts := []uint32{}984for _, g := range got {985gotPorts = append(gotPorts, g.LocalPort)986}987if diff := cmp.Diff(gotPorts, tt.want); diff != "" {988t.Errorf("unexpected exposures (-want +got):\n%s", diff)989}990})991}992}993994995