Merge pull request #11205 from cli/wm/bump-dev-tunnels

Update microsoft dev-tunnels to v0.1.13
This commit is contained in:
William Martin 2025-07-03 12:08:40 +02:00 committed by GitHub
commit 329dfa8ee7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 305 additions and 47 deletions

2
go.mod
View file

@ -39,7 +39,7 @@ require (
github.com/mattn/go-colorable v0.1.14
github.com/mattn/go-isatty v0.0.20
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/microsoft/dev-tunnels v0.0.25
github.com/microsoft/dev-tunnels v0.1.13
github.com/muhammadmuzzammil1998/jsonc v1.0.0
github.com/opentracing/opentracing-go v1.2.0
github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb

4
go.sum
View file

@ -370,8 +370,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/microsoft/dev-tunnels v0.0.25 h1:UlMKUI+2O8cSu4RlB52ioSyn1LthYSVkJA+CSTsdKoA=
github.com/microsoft/dev-tunnels v0.0.25/go.mod h1:frU++12T/oqxckXkDpTuYa427ncguEOodSPZcGCCrzQ=
github.com/microsoft/dev-tunnels v0.1.13 h1:bp1qqCvP/5iLol1Vz0c/lM2sexG7Gd8fRGcGv58vZdE=
github.com/microsoft/dev-tunnels v0.1.13/go.mod h1:Jvr6RlyjUXomM6KsDmIQbq+hhKd5mWrBcv3MEsa78dc=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=

View file

@ -132,7 +132,9 @@ func getTunnelManager(tunnelProperties api.TunnelProperties, httpClient *http.Cl
}
// Create the tunnel manager
tunnelManager, err = tunnels.NewManager(userAgent, nil, url, httpClient)
// This api version seems to be the only acceptable api version: https://github.com/microsoft/dev-tunnels/blob/bf96ae5a128041d1a23f81d53a47e9e6c26fdc8d/go/tunnels/manager.go#L66
apiVersion := "2023-09-27-preview"
tunnelManager, err = tunnels.NewManager(userAgent, nil, url, httpClient, apiVersion)
if err != nil {
return nil, fmt.Errorf("error creating tunnel manager: %w", err)
}

View file

@ -14,6 +14,7 @@ import (
"net/http/httptest"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"time"
@ -25,7 +26,28 @@ import (
"golang.org/x/crypto/ssh"
)
func NewMockHttpClient() (*http.Client, error) {
type mockClientOpts struct {
ports map[int]tunnels.TunnelPort // Port number to protocol
}
type mockClientOpt func(*mockClientOpts)
// WithSpecificPorts allows you to specify a map of ports to TunnelPorts that will be returned by the mock HTTP client.
// Note that this does not take a copy of the map, so you should not modify the map after passing it to this function.
func WithSpecificPorts(ports map[int]tunnels.TunnelPort) mockClientOpt {
return func(opts *mockClientOpts) {
opts.ports = ports
}
}
func NewMockHttpClient(opts ...mockClientOpt) (*http.Client, error) {
mockClientOpts := &mockClientOpts{}
for _, opt := range opts {
opt(mockClientOpts)
}
specifiedPorts := mockClientOpts.ports
accessToken := "tunnel access-token"
relayServer, err := newMockrelayServer(withAccessToken(accessToken))
if err != nil {
@ -35,7 +57,7 @@ func NewMockHttpClient() (*http.Client, error) {
hostURL := strings.Replace(relayServer.URL(), "http://", "ws://", 1)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var response []byte
if r.URL.Path == "/api/v1/tunnels/tunnel-id" {
if r.URL.Path == "/tunnels/tunnel-id" {
tunnel := &tunnels.Tunnel{
AccessTokens: map[tunnels.TunnelAccessScope]string{
tunnels.TunnelAccessScopeConnect: accessToken,
@ -54,54 +76,141 @@ func NewMockHttpClient() (*http.Client, error) {
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}
} else if strings.HasPrefix(r.URL.Path, "/api/v1/tunnels/tunnel-id/ports") {
// Use regex to check if the path ends with a number
match, err := regexp.MatchString(`\/\d+$`, r.URL.Path)
if err != nil {
log.Fatalf("regexp.MatchString returned an error: %v", err)
}
// If the path ends with a number, it's a request for a specific port
if match || r.Method == http.MethodPost {
_, _ = w.Write(response)
return
} else if strings.HasPrefix(r.URL.Path, "/tunnels/tunnel-id/ports") {
// Use regex to capture the port number from the end of the path
re := regexp.MustCompile(`\/(\d+)$`)
matches := re.FindStringSubmatch(r.URL.Path)
targetingSpecificPort := len(matches) > 0
if targetingSpecificPort {
if r.Method == http.MethodDelete {
w.WriteHeader(http.StatusOK)
return
}
tunnelPort := &tunnels.TunnelPort{
if r.Method == http.MethodGet {
// If no ports were configured, then we assume that every request for a port is valid.
if specifiedPorts == nil {
response, err := json.Marshal(tunnels.TunnelPort{
AccessControl: &tunnels.TunnelAccessControl{
Entries: []tunnels.TunnelAccessControlEntry{},
},
})
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}
_, _ = w.Write(response)
return
} else {
// Otherwise we'll fetch the port from our configured ports and include the protocol in the response.
port, err := strconv.Atoi(matches[1])
if err != nil {
log.Fatalf("strconv.Atoi returned an error: %v", err)
}
tunnelPort, ok := specifiedPorts[port]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
response, err := json.Marshal(tunnelPort)
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}
_, _ = w.Write(response)
return
}
}
// Else this is an unexpected request, fall through to 404 at the bottom
}
// If it's a PUT request, we assume it's for creating a new port so we'll do some validation
// and then return a stub.
if r.Method == http.MethodPut {
// If a port was already configured with this number, and the protocol has changed, return a 400 Bad Request.
if specifiedPorts != nil {
port, err := strconv.Atoi(matches[1])
if err != nil {
log.Fatalf("strconv.Atoi returned an error: %v", err)
}
var portRequest tunnels.TunnelPort
if err := json.NewDecoder(r.Body).Decode(&portRequest); err != nil {
log.Fatalf("json.NewDecoder returned an error: %v", err)
}
tunnelPort, ok := specifiedPorts[port]
if ok {
if tunnelPort.Protocol != portRequest.Protocol {
w.WriteHeader(http.StatusBadRequest)
return
}
}
// Create or update the new port entry.
specifiedPorts[port] = portRequest
}
response, err := json.Marshal(tunnels.TunnelPort{
AccessControl: &tunnels.TunnelAccessControl{
Entries: []tunnels.TunnelAccessControlEntry{},
},
}
})
// Convert the tunnel to JSON and write it to the response
response, err = json.Marshal(*tunnelPort)
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}
} else {
// If the path doesn't end with a number and we aren't making a POST request, return an array of ports
tunnelPorts := []tunnels.TunnelPort{
{
AccessControl: &tunnels.TunnelAccessControl{
Entries: []tunnels.TunnelAccessControlEntry{},
},
},
}
response, err = json.Marshal(tunnelPorts)
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}
_, _ = w.Write(response)
return
}
// Finally, if it's not targeting a specific port or a POST request, we return a list of ports, either
// totally stubbed, or whatever was configured in the mock client options.
if specifiedPorts == nil {
response, err := json.Marshal(tunnels.TunnelPortListResponse{
Value: []tunnels.TunnelPort{
{
AccessControl: &tunnels.TunnelAccessControl{
Entries: []tunnels.TunnelAccessControlEntry{},
},
},
},
})
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}
_, _ = w.Write(response)
return
} else {
var ports []tunnels.TunnelPort
for _, tunnelPort := range specifiedPorts {
ports = append(ports, tunnelPort)
}
response, err := json.Marshal(tunnels.TunnelPortListResponse{
Value: ports,
})
if err != nil {
log.Fatalf("json.Marshal returned an error: %v", err)
}
_, _ = w.Write(response)
return
}
} else {
w.WriteHeader(http.StatusNotFound)
return
}
// Write the response
_, _ = w.Write(response)
}))
url, err := url.Parse(mockServer.URL)

View file

@ -12,9 +12,9 @@ import (
)
const (
githubSubjectId = "1"
InternalPortTag = "InternalPort"
UserForwardedPortTag = "UserForwardedPort"
githubSubjectId = "1"
InternalPortLabel = "InternalPort"
UserForwardedPortLabel = "UserForwardedPort"
)
const (
@ -108,7 +108,26 @@ func (fwd *CodespacesPortForwarder) ForwardPort(ctx context.Context, opts Forwar
return fmt.Errorf("error converting port: %w", err)
}
tunnelPort := tunnels.NewTunnelPort(port, "", "", tunnels.TunnelProtocolHttp)
// In v0.0.25 of dev-tunnels, the dev-tunnel manager `CreateTunnelPort` would "accept" requests that
// change the port protocol but they would not result in any actual change. This has changed, resulting in
// an error `Invalid arguments. The tunnel port protocol cannot be changed.`. It's not clear why the previous
// behaviour existed, whether it was truly the API version, or whether the `If-Not-Match` header being set inside
// `CreateTunnelPort` avoided the server accepting the request to change the protocol and that has since regressed.
//
// In any case, now we check whether a port exists with the given port number, if it does, we use the existing protocol.
// If it doesn't exist, we default to HTTP, which was the previous behaviour for all ports.
protocol := tunnels.TunnelProtocolHttp
existingPort, err := fwd.connection.TunnelManager.GetTunnelPort(ctx, fwd.connection.Tunnel, opts.Port, fwd.connection.Options)
if err != nil && !strings.Contains(err.Error(), "404") {
return fmt.Errorf("error checking whether tunnel port already exists: %v", err)
}
if existingPort != nil {
protocol = tunnels.TunnelProtocol(existingPort.Protocol)
}
tunnelPort := tunnels.NewTunnelPort(port, "", "", protocol)
// If no visibility is provided, Dev Tunnels will use the default (private)
if opts.Visibility != "" {
@ -136,9 +155,9 @@ func (fwd *CodespacesPortForwarder) ForwardPort(ctx context.Context, opts Forwar
// Tag the port as internal or user forwarded so we know if it needs to be shown in the UI
if opts.Internal {
tunnelPort.Tags = []string{InternalPortTag}
tunnelPort.Labels = []string{InternalPortLabel}
} else {
tunnelPort.Tags = []string{UserForwardedPortTag}
tunnelPort.Labels = []string{UserForwardedPortLabel}
}
// Create the tunnel port
@ -362,8 +381,8 @@ func visibilityToAccessControlEntries(visibility string) []tunnels.TunnelAccessC
// IsInternalPort returns true if the port is internal.
func IsInternalPort(port *tunnels.TunnelPort) bool {
for _, tag := range port.Tags {
if strings.EqualFold(tag, InternalPortTag) {
for _, label := range port.Labels {
if strings.EqualFold(label, InternalPortLabel) {
return true
}
}

View file

@ -105,10 +105,10 @@ func TestAccessControlEntriesToVisibility(t *testing.T) {
func TestIsInternalPort(t *testing.T) {
internalPort := &tunnels.TunnelPort{
Tags: []string{"InternalPort"},
Labels: []string{"InternalPort"},
}
userForwardedPort := &tunnels.TunnelPort{
Tags: []string{"UserForwardedPort"},
Labels: []string{"UserForwardedPort"},
}
tests := []struct {
@ -137,3 +137,131 @@ func TestIsInternalPort(t *testing.T) {
})
}
}
func TestForwardPortDefaultsToHTTPProtocol(t *testing.T) {
codespace := &api.Codespace{
Name: "codespace-name",
State: api.CodespaceStateAvailable,
Connection: api.CodespaceConnection{
TunnelProperties: api.TunnelProperties{
ConnectAccessToken: "tunnel access-token",
ManagePortsAccessToken: "manage-ports-token",
ServiceUri: "http://global.rel.tunnels.api.visualstudio.com/",
TunnelId: "tunnel-id",
ClusterId: "usw2",
Domain: "domain.com",
},
},
RuntimeConstraints: api.RuntimeConstraints{
AllowedPortPrivacySettings: []string{"public", "private"},
},
}
// Given there are no forwarded ports.
tunnelPorts := map[int]tunnels.TunnelPort{}
httpClient, err := connection.NewMockHttpClient(
connection.WithSpecificPorts(tunnelPorts),
)
if err != nil {
t.Fatalf("NewMockHttpClient returned an error: %v", err)
}
connection, err := connection.NewCodespaceConnection(t.Context(), codespace, httpClient)
if err != nil {
t.Fatalf("NewCodespaceConnection returned an error: %v", err)
}
fwd, err := NewPortForwarder(t.Context(), connection)
if err != nil {
t.Fatalf("NewPortForwarder returned an error: %v", err)
}
// When we forward a port without an existing one to use for a protocol, it should default to HTTP.
if err := fwd.ForwardPort(t.Context(), ForwardPortOpts{
Port: 1337,
}); err != nil {
t.Fatalf("ForwardPort returned an error: %v", err)
}
ports, err := fwd.ListPorts(t.Context())
if err != nil {
t.Fatalf("ListPorts returned an error: %v", err)
}
if len(ports) != 1 {
t.Fatalf("expected 1 port, got %d", len(ports))
}
if ports[0].Protocol != string(tunnels.TunnelProtocolHttp) {
t.Fatalf("expected port protocol to be http, got %s", ports[0].Protocol)
}
}
func TestForwardPortRespectsProtocolOfExistingTunneledPorts(t *testing.T) {
codespace := &api.Codespace{
Name: "codespace-name",
State: api.CodespaceStateAvailable,
Connection: api.CodespaceConnection{
TunnelProperties: api.TunnelProperties{
ConnectAccessToken: "tunnel access-token",
ManagePortsAccessToken: "manage-ports-token",
ServiceUri: "http://global.rel.tunnels.api.visualstudio.com/",
TunnelId: "tunnel-id",
ClusterId: "usw2",
Domain: "domain.com",
},
},
RuntimeConstraints: api.RuntimeConstraints{
AllowedPortPrivacySettings: []string{"public", "private"},
},
}
// Given we already have a port forwarded with an HTTPS protocol.
tunnelPorts := map[int]tunnels.TunnelPort{
1337: {
Protocol: string(tunnels.TunnelProtocolHttps),
AccessControl: &tunnels.TunnelAccessControl{
Entries: []tunnels.TunnelAccessControlEntry{},
},
},
}
httpClient, err := connection.NewMockHttpClient(
connection.WithSpecificPorts(tunnelPorts),
)
if err != nil {
t.Fatalf("NewMockHttpClient returned an error: %v", err)
}
connection, err := connection.NewCodespaceConnection(t.Context(), codespace, httpClient)
if err != nil {
t.Fatalf("NewCodespaceConnection returned an error: %v", err)
}
fwd, err := NewPortForwarder(t.Context(), connection)
if err != nil {
t.Fatalf("NewPortForwarder returned an error: %v", err)
}
// When we forward a port, it would typically default to HTTP, to which the mock server would respond with a 400,
// but it should respect the existing port's protocol and forward it as HTTPS.
if err := fwd.ForwardPort(t.Context(), ForwardPortOpts{
Port: 1337,
}); err != nil {
t.Fatalf("ForwardPort returned an error: %v", err)
}
ports, err := fwd.ListPorts(t.Context())
if err != nil {
t.Fatalf("ListPorts returned an error: %v", err)
}
if len(ports) != 1 {
t.Fatalf("expected 1 port, got %d", len(ports))
}
if ports[0].Protocol != string(tunnels.TunnelProtocolHttps) {
t.Fatalf("expected port protocol to be https, got %s", ports[0].Protocol)
}
}

View file

@ -106,7 +106,7 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/mattn/go-runewidth](https://pkg.go.dev/github.com/mattn/go-runewidth) ([MIT](https://github.com/mattn/go-runewidth/blob/v0.0.16/LICENSE))
- [github.com/mgutz/ansi](https://pkg.go.dev/github.com/mgutz/ansi) ([MIT](https://github.com/mgutz/ansi/blob/d51e80ef957d/LICENSE))
- [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.0.25/LICENSE))
- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.1.13/LICENSE))
- [github.com/mitchellh/copystructure](https://pkg.go.dev/github.com/mitchellh/copystructure) ([MIT](https://github.com/mitchellh/copystructure/blob/v1.2.0/LICENSE))
- [github.com/mitchellh/go-homedir](https://pkg.go.dev/github.com/mitchellh/go-homedir) ([MIT](https://github.com/mitchellh/go-homedir/blob/v1.1.0/LICENSE))
- [github.com/mitchellh/hashstructure/v2](https://pkg.go.dev/github.com/mitchellh/hashstructure/v2) ([MIT](https://github.com/mitchellh/hashstructure/blob/v2.0.2/LICENSE))

View file

@ -106,7 +106,7 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/mattn/go-runewidth](https://pkg.go.dev/github.com/mattn/go-runewidth) ([MIT](https://github.com/mattn/go-runewidth/blob/v0.0.16/LICENSE))
- [github.com/mgutz/ansi](https://pkg.go.dev/github.com/mgutz/ansi) ([MIT](https://github.com/mgutz/ansi/blob/d51e80ef957d/LICENSE))
- [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.0.25/LICENSE))
- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.1.13/LICENSE))
- [github.com/mitchellh/copystructure](https://pkg.go.dev/github.com/mitchellh/copystructure) ([MIT](https://github.com/mitchellh/copystructure/blob/v1.2.0/LICENSE))
- [github.com/mitchellh/go-homedir](https://pkg.go.dev/github.com/mitchellh/go-homedir) ([MIT](https://github.com/mitchellh/go-homedir/blob/v1.1.0/LICENSE))
- [github.com/mitchellh/hashstructure/v2](https://pkg.go.dev/github.com/mitchellh/hashstructure/v2) ([MIT](https://github.com/mitchellh/hashstructure/blob/v2.0.2/LICENSE))

View file

@ -109,7 +109,7 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/mattn/go-runewidth](https://pkg.go.dev/github.com/mattn/go-runewidth) ([MIT](https://github.com/mattn/go-runewidth/blob/v0.0.16/LICENSE))
- [github.com/mgutz/ansi](https://pkg.go.dev/github.com/mgutz/ansi) ([MIT](https://github.com/mgutz/ansi/blob/d51e80ef957d/LICENSE))
- [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.0.25/LICENSE))
- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.1.13/LICENSE))
- [github.com/mitchellh/copystructure](https://pkg.go.dev/github.com/mitchellh/copystructure) ([MIT](https://github.com/mitchellh/copystructure/blob/v1.2.0/LICENSE))
- [github.com/mitchellh/go-homedir](https://pkg.go.dev/github.com/mitchellh/go-homedir) ([MIT](https://github.com/mitchellh/go-homedir/blob/v1.1.0/LICENSE))
- [github.com/mitchellh/hashstructure/v2](https://pkg.go.dev/github.com/mitchellh/hashstructure/v2) ([MIT](https://github.com/mitchellh/hashstructure/blob/v2.0.2/LICENSE))