Update microsoft dev-tunnels to v0.1.13
https://github.com/microsoft/dev-tunnels/pull/322 introduced a few breaking changes: * Port Tags were renamed to Labels * Client construction must now provide an API version (of which there is only one) * The /api/v1 prefix was dropped from request paths * TunnelPortListResponses may now be paginated (but we don't support that) * Requests to create a port with a changed protocol began erroring
This commit is contained in:
parent
4c47bad604
commit
ad089d48d9
9 changed files with 305 additions and 47 deletions
2
go.mod
2
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue