From 1c8fcc2dee38dc4550b3bc81947f39fc12d6a77c Mon Sep 17 00:00:00 2001 From: Jacob Fullerton Date: Wed, 11 Oct 2023 12:31:38 -0700 Subject: [PATCH] C#, TS, Go and Java changes to use new API version (#322) * update c# to use new api version * add IdGeneration * Update API version * first pass of typescript * update id gen * update references * update flatten * update ts contract gen * update create ports * update writer * update go and java * update name in typescript * auto gen ts and cs ids * id gen in java and go * remove old files * java and go retries * update retry * add endpoint query param * fix e2e test bugs * use old chars * update request objects * go list response * update ids * update exports * remove unused * update relay ids * update tags to labels * remove tag references * fix namespace export * generated contracts * update return * update words --- cs/src/Connections/TunnelRelayTunnelHost.cs | 4 +- cs/src/Contracts/Tunnel.cs | 14 +- cs/src/Contracts/TunnelConstraints.cs | 36 +-- cs/src/Contracts/TunnelListByRegion.cs | 2 +- cs/src/Contracts/TunnelListResponse.cs | 37 --- cs/src/Contracts/TunnelPort.cs | 14 +- cs/src/Contracts/TunnelPortListResponse.cs | 4 +- cs/src/Contracts/TunnelPortV2.cs | 154 ----------- cs/src/Contracts/TunnelV2.cs | 147 ----------- cs/src/Management/ITunnelManagementClient.cs | 23 +- cs/src/Management/IdGeneration.cs | 31 +++ cs/src/Management/ThreadSafeRandom.cs | 34 +++ cs/src/Management/TunnelExtensions.cs | 93 ------- cs/src/Management/TunnelManagementClient.cs | 240 ++++++------------ cs/src/Management/TunnelRequestOptions.cs | 26 +- .../Mocks/MockTunnelManagementClient.cs | 20 +- .../TunnelsSDK.Generator/TSContractWriter.cs | 4 +- go/tunnels/examples/example.go | 2 +- go/tunnels/id_generation.go | 24 ++ go/tunnels/manager.go | 114 ++++++--- go/tunnels/manager_test.go | 25 +- go/tunnels/request_options.go | 22 +- go/tunnels/tunnel.go | 4 +- go/tunnels/tunnel_constraints.go | 16 +- go/tunnels/tunnel_list_by_region.go | 2 +- go/tunnels/tunnel_list_response.go | 2 +- go/tunnels/tunnel_port.go | 4 +- go/tunnels/tunnel_port_list_response.go | 2 +- go/tunnels/tunnel_port_v2.go | 74 ------ go/tunnels/tunnel_v2.go | 71 ------ go/tunnels/tunnels.go | 9 +- .../microsoft/tunnels/contracts/Tunnel.java | 4 +- .../tunnels/contracts/TunnelConstraints.java | 16 +- .../contracts/TunnelConstraintsStatics.java | 2 +- .../tunnels/contracts/TunnelListByRegion.java | 2 +- .../tunnels/contracts/TunnelListResponse.java | 24 -- .../tunnels/contracts/TunnelPort.java | 4 +- .../contracts/TunnelPortListResponse.java | 2 +- .../tunnels/contracts/TunnelPortV2.java | 126 --------- .../microsoft/tunnels/contracts/TunnelV2.java | 119 --------- .../management/ITunnelManagementClient.java | 9 +- .../tunnels/management/IdGeneration.java | 20 ++ .../management/TunnelManagementClient.java | 132 ++++++---- .../management/TunnelRequestOptions.java | 24 +- .../tunnels/TunnelManagementClientTests.java | 2 +- .../com/microsoft/tunnels/TunnelTest.java | 3 +- ts/src/connections/tunnelRelayTunnelHost.ts | 5 +- ts/src/contracts/index.ts | 2 + ts/src/contracts/tunnel.ts | 4 +- ts/src/contracts/tunnelAccessControlEntry.ts | 2 +- ts/src/contracts/tunnelConstraints.ts | 18 +- ts/src/contracts/tunnelListByRegion.ts | 4 +- ts/src/contracts/tunnelListResponse.ts | 21 -- ts/src/contracts/tunnelPort.ts | 4 +- ts/src/contracts/tunnelPortListResponse.ts | 4 +- ts/src/contracts/tunnelPortV2.ts | 111 -------- ts/src/contracts/tunnelV2.ts | 105 -------- ts/src/management/idGeneration.ts | 16 ++ ts/src/management/tunnelManagementClient.ts | 10 +- .../management/tunnelManagementHttpClient.ts | 142 +++++++---- ts/src/management/tunnelRequestOptions.ts | 16 +- ts/test/tunnels-test/connection.ts | 1 + ts/test/tunnels-test/host.ts | 1 + .../mocks/mockTunnelManagementClient.ts | 18 +- ts/test/tunnels-test/tunnelManagementTests.ts | 10 +- 65 files changed, 659 insertions(+), 1578 deletions(-) delete mode 100644 cs/src/Contracts/TunnelListResponse.cs delete mode 100644 cs/src/Contracts/TunnelPortV2.cs delete mode 100644 cs/src/Contracts/TunnelV2.cs create mode 100644 cs/src/Management/IdGeneration.cs create mode 100644 cs/src/Management/ThreadSafeRandom.cs create mode 100644 go/tunnels/id_generation.go delete mode 100644 go/tunnels/tunnel_port_v2.go delete mode 100644 go/tunnels/tunnel_v2.go delete mode 100644 java/src/main/java/com/microsoft/tunnels/contracts/TunnelListResponse.java delete mode 100644 java/src/main/java/com/microsoft/tunnels/contracts/TunnelPortV2.java delete mode 100644 java/src/main/java/com/microsoft/tunnels/contracts/TunnelV2.java create mode 100644 java/src/main/java/com/microsoft/tunnels/management/IdGeneration.java delete mode 100644 ts/src/contracts/tunnelListResponse.ts delete mode 100644 ts/src/contracts/tunnelPortV2.ts delete mode 100644 ts/src/contracts/tunnelV2.ts create mode 100644 ts/src/management/idGeneration.ts diff --git a/cs/src/Connections/TunnelRelayTunnelHost.cs b/cs/src/Connections/TunnelRelayTunnelHost.cs index c3bf42ae..7d48711d 100644 --- a/cs/src/Connections/TunnelRelayTunnelHost.cs +++ b/cs/src/Connections/TunnelRelayTunnelHost.cs @@ -50,6 +50,7 @@ public class TunnelRelayTunnelHost : TunnelHost, IRelayClient private SshClientSession? hostSession; private Uri? relayUri; + private string endpointId { get { return hostId + "-relay"; } } /// /// Creates a new instance of a host that connects to a tunnel via a tunnel relay. @@ -92,7 +93,7 @@ public override async ValueTask DisposeAsync() if (Tunnel != null) { - tasks.Add(ManagementClient!.DeleteTunnelEndpointsAsync(Tunnel, this.hostId, TunnelConnectionMode.TunnelRelay)); + tasks.Add(ManagementClient!.DeleteTunnelEndpointsAsync(Tunnel, endpointId)); } foreach (RemotePortForwarder forwarder in RemoteForwarders.Values) @@ -123,6 +124,7 @@ protected override async Task CreateTunnelConnectorAsync(Cance var endpoint = new TunnelRelayTunnelEndpoint { HostId = this.hostId, + Id = this.endpointId, HostPublicKeys = hostPublicKeys, }; List>? additionalQueryParams = null; diff --git a/cs/src/Contracts/Tunnel.cs b/cs/src/Contracts/Tunnel.cs index c8ae79c3..014d5bda 100644 --- a/cs/src/Contracts/Tunnel.cs +++ b/cs/src/Contracts/Tunnel.cs @@ -37,8 +37,8 @@ public Tunnel() /// Gets or sets the generated ID of the tunnel, unique within the cluster. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [RegularExpression(OldTunnelIdPattern)] - [StringLength(OldTunnelIdLength, MinimumLength = OldTunnelIdLength)] + [RegularExpression(NewTunnelIdPattern)] + [StringLength(NewTunnelIdMaxLength, MinimumLength = NewTunnelIdMinLength)] public string? TunnelId { get; set; } /// @@ -61,13 +61,13 @@ public Tunnel() public string? Description { get; set; } /// - /// Gets or sets the tags of the tunnel. + /// Gets or sets the labels of the tunnel. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [MaxLength(MaxTags)] - [ArrayStringLength(TagMaxLength, MinimumLength = TagMinLength)] - [ArrayRegularExpression(TagPattern)] - public string[]? Tags { get; set; } + [MaxLength(MaxLabels)] + [ArrayStringLength(LabelMaxLength, MinimumLength = LabelMinLength)] + [ArrayRegularExpression(LabelPattern)] + public string[]? Labels { get; set; } /// /// Gets or sets the optional parent domain of the tunnel, if it is not using diff --git a/cs/src/Contracts/TunnelConstraints.cs b/cs/src/Contracts/TunnelConstraints.cs index 8c0c4d53..eaabdfe4 100644 --- a/cs/src/Contracts/TunnelConstraints.cs +++ b/cs/src/Contracts/TunnelConstraints.cs @@ -72,23 +72,23 @@ public static class TunnelConstraints /// /// Min length of a single tunnel or port tag. /// - /// - /// - public const int TagMinLength = 1; + /// + /// + public const int LabelMinLength = 1; /// /// Max length of a single tunnel or port tag. /// - /// - /// - public const int TagMaxLength = 50; + /// + /// + public const int LabelMaxLength = 50; /// - /// Maximum number of tags that can be applied to a tunnel or port. + /// Maximum number of labels that can be applied to a tunnel or port. /// - /// - /// - public const int MaxTags = 100; + /// + /// + public const int MaxLabels = 100; /// /// Min length of a tunnel domain. @@ -265,17 +265,17 @@ public static class TunnelConstraints public static Regex TunnelNameRegex { get; } = new Regex(TunnelNamePattern); /// - /// Regular expression that can match or validate tunnel or port tags. + /// Regular expression that can match or validate tunnel or port labels. /// - /// - public const string TagPattern = "[\\w-=]{1,50}"; + /// + public const string LabelPattern = "[\\w-=]{1,50}"; /// - /// Regular expression that can match or validate tunnel or port tags. + /// Regular expression that can match or validate tunnel or port labels. /// - /// - /// - public static Regex TagRegex { get; } = new Regex(TagPattern); + /// + /// + public static Regex LabelRegex { get; } = new Regex(LabelPattern); /// /// Regular expression that can match or validate tunnel domains. @@ -422,7 +422,7 @@ public static bool IsValidTag(string tag) return false; } - var m = TagRegex.Match(tag); + var m = LabelRegex.Match(tag); return m.Index == 0 && m.Length == tag.Length; } diff --git a/cs/src/Contracts/TunnelListByRegion.cs b/cs/src/Contracts/TunnelListByRegion.cs index 1f9a28d4..b00f6303 100644 --- a/cs/src/Contracts/TunnelListByRegion.cs +++ b/cs/src/Contracts/TunnelListByRegion.cs @@ -29,7 +29,7 @@ public class TunnelListByRegion /// List of tunnels. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public TunnelV2[]? Value { get; set; } + public Tunnel[]? Value { get; set; } /// /// Error detail if getting list of tunnels in the region failed. diff --git a/cs/src/Contracts/TunnelListResponse.cs b/cs/src/Contracts/TunnelListResponse.cs deleted file mode 100644 index c45dc79a..00000000 --- a/cs/src/Contracts/TunnelListResponse.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. -// - -using System; -using System.Text.Json.Serialization; - -namespace Microsoft.DevTunnels.Contracts; - -/// -/// Data contract for response of a list tunnel call. -/// -public class TunnelListResponse -{ - - /// - /// Initializes a new instance of the class. - /// - public TunnelListResponse() - { - Value = Array.Empty(); - } - - /// - /// List of tunnels - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public TunnelV2[] Value { get; set; } - - /// - /// Link to get next page of results - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? NextLink { get; set; } - -} diff --git a/cs/src/Contracts/TunnelPort.cs b/cs/src/Contracts/TunnelPort.cs index dd74572c..7073b19c 100644 --- a/cs/src/Contracts/TunnelPort.cs +++ b/cs/src/Contracts/TunnelPort.cs @@ -36,8 +36,8 @@ public TunnelPort() /// Gets or sets the generated ID of the tunnel, unique within the cluster. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [RegularExpression(OldTunnelIdPattern)] - [StringLength(OldTunnelIdLength, MinimumLength = OldTunnelIdLength)] + [RegularExpression(NewTunnelIdPattern)] + [StringLength(NewTunnelIdMaxLength, MinimumLength = NewTunnelIdMinLength)] public string? TunnelId { get; set; } /// @@ -64,13 +64,13 @@ public TunnelPort() public string? Description { get; set; } /// - /// Gets or sets the tags of the port. + /// Gets or sets the labels of the port. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [MaxLength(MaxTags)] - [ArrayStringLength(TagMaxLength, MinimumLength = TagMinLength)] - [ArrayRegularExpression(TagPattern)] - public string[]? Tags { get; set; } + [MaxLength(MaxLabels)] + [ArrayStringLength(LabelMaxLength, MinimumLength = LabelMinLength)] + [ArrayRegularExpression(LabelPattern)] + public string[]? Labels { get; set; } /// /// Gets or sets the protocol of the tunnel port. diff --git a/cs/src/Contracts/TunnelPortListResponse.cs b/cs/src/Contracts/TunnelPortListResponse.cs index e10f09d9..69f9f492 100644 --- a/cs/src/Contracts/TunnelPortListResponse.cs +++ b/cs/src/Contracts/TunnelPortListResponse.cs @@ -19,14 +19,14 @@ public class TunnelPortListResponse /// public TunnelPortListResponse() { - Value = Array.Empty(); + Value = Array.Empty(); } /// /// List of tunnels /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public TunnelPortV2[] Value { get; set; } + public TunnelPort[] Value { get; set; } /// /// Link to get next page of results diff --git a/cs/src/Contracts/TunnelPortV2.cs b/cs/src/Contracts/TunnelPortV2.cs deleted file mode 100644 index 12ba42a8..00000000 --- a/cs/src/Contracts/TunnelPortV2.cs +++ /dev/null @@ -1,154 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. -// - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; -using Microsoft.DevTunnels.Contracts.Validation; - -namespace Microsoft.DevTunnels.Contracts; - -using static TunnelConstraints; - -/// -/// Data contract for tunnel port objects managed through the tunnel service REST API. -/// -public class TunnelPortV2 -{ - /// - /// Initializes a new instance of the class. - /// - public TunnelPortV2() - { - } - - /// - /// Gets or sets the ID of the cluster the tunnel was created in. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [RegularExpression(ClusterIdPattern)] - [StringLength(ClusterIdMaxLength, MinimumLength = ClusterIdMinLength)] - public string? ClusterId { get; set; } - - /// - /// Gets or sets the generated ID of the tunnel, unique within the cluster. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [RegularExpression(NewTunnelIdPattern)] - [StringLength(NewTunnelIdMaxLength, MinimumLength = NewTunnelIdMinLength)] - public string? TunnelId { get; set; } - - /// - /// Gets or sets the IP port number of the tunnel port. - /// - public ushort PortNumber { get; set; } - - /// - /// Gets or sets the optional short name of the port. - /// - /// - /// The name must be unique among named ports of the same tunnel. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [RegularExpression(TunnelNamePattern)] - [StringLength(TunnelNameMaxLength)] - public string? Name { get; set; } - - /// - /// Gets or sets the optional description of the port. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [StringLength(DescriptionMaxLength)] - public string? Description { get; set; } - - /// - /// Gets or sets the tags of the port. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [MaxLength(MaxTags)] - [ArrayStringLength(TagMaxLength, MinimumLength = TagMinLength)] - [ArrayRegularExpression(TagPattern)] - public string[]? Labels { get; set; } - - /// - /// Gets or sets the protocol of the tunnel port. - /// - /// - /// Should be one of the string constants from . - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [StringLength(TunnelProtocol.MaxLength)] - public string? Protocol { get; set; } - - /// - /// Gets or sets a value indicating whether this port is a default port for the tunnel. - /// - /// - /// A client that connects to a tunnel (by ID or name) without specifying a port number will - /// connect to the default port for the tunnel, if a default is configured. Or if the tunnel - /// has only one port then the single port is the implicit default. - /// - /// Selection of a default port for a connection also depends on matching the connection to the - /// port , so it is possible to configure separate defaults for distinct - /// protocols like and . - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public bool IsDefault { get; set; } - - /// - /// Gets or sets a dictionary mapping from scopes to tunnel access tokens. - /// - /// - /// Unlike the tokens in , these tokens are restricted - /// to the individual port. - /// - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public IDictionary? AccessTokens { get; set; } - - /// - /// Gets or sets access control settings for the tunnel port. - /// - /// - /// See documentation for details about the - /// access control model. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public TunnelAccessControl? AccessControl { get; set; } - - /// - /// Gets or sets options for the tunnel port. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public TunnelOptions? Options { get; set; } - - /// - /// Gets or sets current connection status of the tunnel port. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public TunnelPortStatus? Status { get; set; } - - /// - /// Gets or sets the username for the ssh service user is trying to forward. - /// - /// - /// Should be provided if the is Ssh. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [StringLength(TunnelNameMaxLength)] - public string? SshUser { get; set; } - - /// - /// Gets or sets web forwarding URIs. - /// If set, it's a list of absolute URIs where the port can be accessed with web forwarding. - /// - public string[]? PortForwardingUris { get; set; } - - /// - /// Gets or sets inspection URI. - /// If set, it's an absolute URIs where the port's traffic can be inspected. - /// - public string? InspectionUri { get; set; } -} diff --git a/cs/src/Contracts/TunnelV2.cs b/cs/src/Contracts/TunnelV2.cs deleted file mode 100644 index 0fd4ae5b..00000000 --- a/cs/src/Contracts/TunnelV2.cs +++ /dev/null @@ -1,147 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. -// - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; -using Microsoft.DevTunnels.Contracts.Validation; - -namespace Microsoft.DevTunnels.Contracts; - -using static TunnelConstraints; - -/// -/// Data contract for tunnel objects managed through the tunnel service REST API. -/// -public class TunnelV2 -{ - /// - /// Initializes a new instance of the class. - /// - public TunnelV2() - { - } - - /// - /// Gets or sets the ID of the cluster the tunnel was created in. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [RegularExpression(ClusterIdPattern)] - [StringLength(ClusterIdMaxLength, MinimumLength = ClusterIdMinLength)] - public string? ClusterId { get; set; } - - /// - /// Gets or sets the generated ID of the tunnel, unique within the cluster. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [RegularExpression(NewTunnelIdPattern)] - [StringLength(NewTunnelIdMaxLength, MinimumLength = NewTunnelIdMinLength)] - public string? TunnelId { get; set; } - - /// - /// Gets or sets the optional short name (alias) of the tunnel. - /// - /// - /// The name must be globally unique within the parent domain, and must be a valid - /// subdomain. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [RegularExpression(TunnelNamePattern)] - [StringLength(TunnelNameMaxLength)] - public string? Name { get; set; } - - /// - /// Gets or sets the description of the tunnel. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [StringLength(DescriptionMaxLength)] - public string? Description { get; set; } - - /// - /// Gets or sets the tags of the tunnel. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [MaxLength(MaxTags)] - [ArrayStringLength(TagMaxLength, MinimumLength = TagMinLength)] - [ArrayRegularExpression(TagPattern)] - public string[]? Labels { get; set; } - - /// - /// Gets or sets the optional parent domain of the tunnel, if it is not using - /// the default parent domain. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [RegularExpression(TunnelDomainPattern)] - [StringLength(TunnelDomainMaxLength)] - public string? Domain { get; set; } - - /// - /// Gets or sets a dictionary mapping from scopes to tunnel access tokens. - /// - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public IDictionary? AccessTokens { get; set; } - - /// - /// Gets or sets access control settings for the tunnel. - /// - /// - /// See documentation for details about the - /// access control model. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public TunnelAccessControl? AccessControl { get; set; } - - /// - /// Gets or sets default options for the tunnel. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public TunnelOptions? Options { get; set; } - - /// - /// Gets or sets current connection status of the tunnel. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public TunnelStatus? Status { get; set; } - - /// - /// Gets or sets an array of endpoints where hosts are currently accepting - /// client connections to the tunnel. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public TunnelEndpoint[]? Endpoints { get; set; } - - /// - /// Gets or sets a list of ports in the tunnel. - /// - /// - /// This optional property enables getting info about all ports in a tunnel at the same time - /// as getting tunnel info, or creating one or more ports at the same time as creating a - /// tunnel. It is omitted when listing (multiple) tunnels, or when updating tunnel - /// properties. (For the latter, use APIs to create/update/delete individual ports instead.) - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [MaxLength(TunnelMaxPorts)] - public TunnelPortV2[]? Ports { get; set; } - - /// - /// Gets or sets the time in UTC of tunnel creation. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public DateTime? Created { get; set; } - - /// - /// Gets or the time the tunnel will be deleted if it is not used or updated. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public DateTime? Expiration { get; set; } - - /// - /// Gets or the custom amount of time the tunnel will be valid if it is not used or updated in seconds. - /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public uint? CustomExpiration { get; set; } -} diff --git a/cs/src/Management/ITunnelManagementClient.cs b/cs/src/Management/ITunnelManagementClient.cs index a24bcc97..76f1bd7e 100644 --- a/cs/src/Management/ITunnelManagementClient.cs +++ b/cs/src/Management/ITunnelManagementClient.cs @@ -29,7 +29,7 @@ public interface ITunnelManagementClient : IDisposable /// The client access token was missing, /// invalid, or unauthorized. /// - /// The list can be filtered by setting . + /// The list can be filtered by setting . /// Ports will not be included in the returned tunnels unless /// is set to true. /// @@ -41,10 +41,10 @@ Task ListTunnelsAsync( CancellationToken cancellation = default); /// - /// Search for all tunnels with matching tags. + /// Search for all tunnels with matching labels. /// - /// The tags that will be searched for - /// If a tunnel must have all tags that are being searched for. + /// The labels that will be searched for + /// If a tunnel must have all labels that are being searched for. /// A tunnel cluster ID, or null to list tunnels globally. /// Tunnel domain, or null for the default domain. /// Request options. @@ -52,10 +52,10 @@ Task ListTunnelsAsync( /// Array of tunnel objects. /// The client access token was missing, /// invalid, or unauthorized. - [Obsolete("Use ListTunnelsAsync() method with TunnelRequestOptions.Tags instead.")] + [Obsolete("Use ListTunnelsAsync() method with TunnelRequestOptions.Labels instead.")] Task SearchTunnelsAsync( - string[] tags, - bool requireAllTags, + string[] labels, + bool requireAllLabels, string? clusterId = null, string? domain = null, TunnelRequestOptions? options = null, @@ -170,9 +170,7 @@ Task UpdateTunnelEndpointAsync( /// /// Tunnel object including at least either a tunnel name /// (globally unique, if configured) or tunnel ID and cluster ID. - /// Required ID of the host for endpoint(s) to be deleted. - /// Optional connection mode for endpoint(s) to be deleted, - /// or null to delete endpoints for all connection modes. + /// Required ID of the endpoint to be deleted. /// Request options. /// Cancellation token. /// True if one or more endpoints were deleted, false if none were found. @@ -186,8 +184,7 @@ Task UpdateTunnelEndpointAsync( /// Task DeleteTunnelEndpointsAsync( Tunnel tunnel, - string hostId, - TunnelConnectionMode? connectionMode, + string Id, TunnelRequestOptions? options = null, CancellationToken cancellation = default); @@ -204,7 +201,7 @@ Task DeleteTunnelEndpointsAsync( /// The tunnel ID or name was not found. /// /// - /// The list can be filtered by setting . + /// The list can be filtered by setting . /// Task ListTunnelPortsAsync( Tunnel tunnel, diff --git a/cs/src/Management/IdGeneration.cs b/cs/src/Management/IdGeneration.cs new file mode 100644 index 00000000..97c31106 --- /dev/null +++ b/cs/src/Management/IdGeneration.cs @@ -0,0 +1,31 @@ +using System.Text; +using Microsoft.DevTunnels.Contracts; + +namespace Microsoft.DevTunnels.Management; + +/// +/// Static class that can be used for generating tunnelIds +/// +public static class IdGeneration +{ + private static string[] nouns = { "pond", "hill", "mountain", "field", "fog", "ant", "dog", "cat", "shoe", "plane", "chair", "book", "ocean", "lake", "river" , "horse" }; + private static string[] adjectives = { "fun", "happy", "interesting", "neat", "peaceful", "puzzled", "kind", "joyful", "new", "giant", "sneaky", "quick", "majestic", "jolly" , "fancy", "tidy", "swift", "silent", "amusing", "spiffy" }; + /// + /// Generate valid tunnelIds + /// + /// string tunnel id + public static string GenerateTunnelId() + { + var sb = new StringBuilder(); + sb.Append(adjectives[ThreadSafeRandom.Next(adjectives.Length)]); + sb.Append("-"); + sb.Append(nouns[ThreadSafeRandom.Next(nouns.Length)]); + sb.Append("-"); + + for (int i = 0; i < 7; i++) + { + sb.Append(TunnelConstraints.OldTunnelIdChars[ThreadSafeRandom.Next(TunnelConstraints.OldTunnelIdChars.Length)]); + } + return sb.ToString(); + } +} \ No newline at end of file diff --git a/cs/src/Management/ThreadSafeRandom.cs b/cs/src/Management/ThreadSafeRandom.cs new file mode 100644 index 00000000..4320b7db --- /dev/null +++ b/cs/src/Management/ThreadSafeRandom.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.DevTunnels.Management; + +internal static class ThreadSafeRandom +{ + [ThreadStatic] + private static Random? _local; + private static readonly Random Global = new(); + + private static Random Instance + { + get + { + if (_local is null) + { + int seed; + lock (Global) + { + seed = Global.Next(); + } + + _local = new Random(seed); + } + + return _local; + } + } + + public static int Next() => Instance.Next(); + public static int Next(int maxValue) => Instance.Next(maxValue); + +} \ No newline at end of file diff --git a/cs/src/Management/TunnelExtensions.cs b/cs/src/Management/TunnelExtensions.cs index 68f60c24..b2caa7b1 100644 --- a/cs/src/Management/TunnelExtensions.cs +++ b/cs/src/Management/TunnelExtensions.cs @@ -72,64 +72,6 @@ public static bool TryGetAccessToken(this Tunnel tunnel, string accessTokenScope return false; } - /// - /// Try to get an access token from for . - /// - /// - /// The tokens are searched in Tunnel.AccessTokens dictionary where each - /// key may be either a single scope or space-delimited list of scopes. - /// - /// The tunnel to get the access token from. - /// Access token scope to get the token for. - /// If non-null and non-empty token is found, the token value. null if not found. - /// - /// true if has non-null and non-empty an access token for ; - /// false if has no access token for or the token is null or empty. - /// - /// If or is null. - /// If is empty. - public static bool TryGetAccessToken(this TunnelV2 tunnel, string accessTokenScope, [NotNullWhen(true)] out string? accessToken) - { - Requires.NotNull(tunnel, nameof(tunnel)); - Requires.NotNullOrEmpty(accessTokenScope, nameof(accessTokenScope)); - - if (tunnel.AccessTokens?.Count > 0) - { - var scope = accessTokenScope.AsSpan(); - foreach (var (key, value) in tunnel.AccessTokens) - { - // Each key may be either a single scope or space-delimited list of scopes. - var index = 0; - while (index < key?.Length) - { - var spaceIndex = key.IndexOf(' ', index); - if (spaceIndex == -1) - { - spaceIndex = key.Length; - } - - if (spaceIndex - index == scope.Length && - key.AsSpan(index, scope.Length).SequenceEqual(scope)) - { - if (string.IsNullOrEmpty(value)) - { - accessToken = null; - return false; - } - - accessToken = value; - return true; - } - - index = spaceIndex + 1; - } - } - } - - accessToken = null; - return false; - } - /// /// Try to get a valid access token from for . /// If the token is found and looks like JWT, it's validated for expiration. @@ -164,39 +106,4 @@ public static bool TryGetValidAccessToken(this Tunnel tunnel, string accessToken return false; } - - /// - /// Try to get a valid access token from for . - /// If the token is found and looks like JWT, it's validated for expiration. - /// - /// - /// The tokens are searched in Tunnel.AccessTokens dictionary where each - /// key may be either a single scope or space-delimited list of scopes. - /// The method only validates token expiration. It doesn't validate if the token is not JWT. It doesn't validate JWT signature or claims. - /// - /// The tunnel to get the access token from. - /// Access token scope to get the token for. - /// If the token is found and it's valid, the token value. null if not found. - /// - /// true if has a valid token for ; - /// false if has no access token for or the token is null or empty. - /// - /// If or is null. - /// If is empty. - /// If the token for is expired. - public static bool TryGetValidAccessToken(this TunnelV2 tunnel, string accessTokenScope, [NotNullWhen(true)] out string? accessToken) - { - Requires.NotNull(tunnel, nameof(tunnel)); - Requires.NotNullOrEmpty(accessTokenScope, nameof(accessTokenScope)); - - accessToken = null; - if (tunnel.TryGetAccessToken(accessTokenScope, out var result)) - { - TunnelAccessTokenProperties.ValidateTokenExpiration(result); - accessToken = result; - return true; - } - - return false; - } } diff --git a/cs/src/Management/TunnelManagementClient.cs b/cs/src/Management/TunnelManagementClient.cs index 72a26df8..2894c6ee 100644 --- a/cs/src/Management/TunnelManagementClient.cs +++ b/cs/src/Management/TunnelManagementClient.cs @@ -41,7 +41,7 @@ public class TunnelManagementClient : ITunnelManagementClient private const string ClustersV1ApiPath = ApiV1Path + "/clusters"; private const string TunnelAuthenticationScheme = "Tunnel"; private const string RequestIdHeaderName = "VsSaaS-Request-Id"; - private const string CheckAvailableSubPath = "/checkNameAvailability"; + private const string CheckAvailableSubPath = ":checkNameAvailability"; private static readonly string[] ManageAccessTokenScope = new[] { TunnelAccessScopes.Manage }; @@ -66,7 +66,7 @@ public class TunnelManagementClient : ITunnelManagementClient /// public string[] TunnelsApiVersions = { - "2023-05-23-preview" + "2023-09-27-preview" }; @@ -341,6 +341,7 @@ private string UserLimitsPath /// Request options. /// Request body object. /// Cancellation token. + /// Whether the request is a create operation. /// The request body type. /// The expected result type. /// Result of the request. @@ -367,59 +368,11 @@ private string UserLimitsPath string? query, TunnelRequestOptions? options, TRequest? body, - CancellationToken cancellation) + CancellationToken cancellation, + bool isCreate = false) where TRequest : class { - var uri = BuildTunnelUri(tunnel, path, query, options); - var authHeader = await GetAuthenticationHeaderAsync(tunnel, accessTokenScopes, options); - return await SendRequestAsync( - method, uri, options, authHeader, body, cancellation); - } - - /// - /// Sends an HTTP request with body content to the tunnel management API, targeting a - /// specific tunnel. - /// - /// HTTP request method. - /// Tunnel that the request is targeting. - /// Required list of access scopes for tokens in - /// that could be used to - /// authorize the request. - /// Optional request sub-path relative to the tunnel. - /// Optional query string to append to the request. - /// Request options. - /// Request body object. - /// Cancellation token. - /// The request body type. - /// The expected result type. - /// Result of the request. - /// The request parameters were invalid. - /// The request was unauthorized or forbidden. - /// The WWW-Authenticate response header may be captured in the exception data. - /// The request would have caused a conflict - /// or exceeded a limit. - /// The request failed for some other - /// reason. - /// - /// This protected method enables subclasses to support additional tunnel management APIs. - /// Authentication will use one of the following, if available, in order of preference: - /// - on - /// - token provided by the user token callback - /// - token in that matches - /// one of the scopes in - /// - protected async Task SendTunnelRequestAsync( - HttpMethod method, - TunnelV2 tunnel, - string[] accessTokenScopes, - string? path, - string? query, - TunnelRequestOptions? options, - TRequest? body, - CancellationToken cancellation) - where TRequest : class - { - var uri = BuildTunnelUri(tunnel, path, query, options); + var uri = BuildTunnelUri(tunnel, path, query, options, isCreate); var authHeader = await GetAuthenticationHeaderAsync(tunnel, accessTokenScopes, options); return await SendRequestAsync( method, uri, options, authHeader, body, cancellation); @@ -804,53 +757,14 @@ private Uri BuildTunnelUri( Tunnel tunnel, string? path, string? query, - TunnelRequestOptions? options) - { - Requires.NotNull(tunnel, nameof(tunnel)); - - string tunnelPath; - var pathBase = TunnelsPath; - if (!string.IsNullOrEmpty(tunnel.ClusterId) && !string.IsNullOrEmpty(tunnel.TunnelId)) - { - tunnelPath = $"{pathBase}/{tunnel.TunnelId}"; - } - else - { - Requires.Argument( - !string.IsNullOrEmpty(tunnel.Name), - nameof(tunnel), - "Tunnel object must include either a name or tunnel ID and cluster ID."); - - if (string.IsNullOrEmpty(tunnel.Domain)) - { - - tunnelPath = $"{pathBase}/{tunnel.Name}"; - } - else - { - // Append the domain to the tunnel name. - tunnelPath = $"{pathBase}/{tunnel.Name}.{tunnel.Domain}"; - } - } - - return BuildUri( - tunnel.ClusterId, - tunnelPath + (!string.IsNullOrEmpty(path) ? path : string.Empty), - query, - options); - } - - private Uri BuildTunnelUri( - TunnelV2 tunnel, - string? path, - string? query, - TunnelRequestOptions? options) + TunnelRequestOptions? options, + bool isCreate = false) { Requires.NotNull(tunnel, nameof(tunnel)); string tunnelPath; var pathBase = TunnelsPath; - if (!string.IsNullOrEmpty(tunnel.ClusterId) && !string.IsNullOrEmpty(tunnel.TunnelId)) + if (!string.IsNullOrEmpty(tunnel.TunnelId) && (!string.IsNullOrEmpty(tunnel.ClusterId) || isCreate)) { tunnelPath = $"{pathBase}/{tunnel.TunnelId}"; } @@ -915,41 +829,6 @@ private Uri BuildTunnelUri( return authHeader; } - private async Task GetAuthenticationHeaderAsync( - TunnelV2? tunnel, - string[]? accessTokenScopes, - TunnelRequestOptions? options) - { - AuthenticationHeaderValue? authHeader = null; - - if (!string.IsNullOrEmpty(options?.AccessToken)) - { - TunnelAccessTokenProperties.ValidateTokenExpiration(options.AccessToken); - authHeader = new AuthenticationHeaderValue( - TunnelAuthenticationScheme, options.AccessToken); - } - - if (authHeader == null) - { - authHeader = await this.userTokenCallback(); - } - - if (authHeader == null && tunnel?.AccessTokens != null && accessTokenScopes != null) - { - foreach (var scope in accessTokenScopes) - { - if (tunnel.TryGetValidAccessToken(scope, out string? accessToken)) - { - authHeader = new AuthenticationHeaderValue( - TunnelAuthenticationScheme, accessToken); - break; - } - } - } - - return authHeader; - } - /// public async Task ListTunnelsAsync( string? clusterId, @@ -966,21 +845,26 @@ public async Task ListTunnelsAsync( ownedTunnelsOnly == true ? "ownedTunnelsOnly=true" : null, }; var query = string.Join("&", queryParams.Where((p) => p != null)); - var result = await this.SendRequestAsync( + var result = await this.SendRequestAsync( HttpMethod.Get, clusterId, TunnelsPath, query, options, cancellation); - return result!; + if (result?.Value != null) + { + return result.Value.Where(t => t.Value != null).SelectMany(t => t.Value!).ToArray(); + } + + return Array.Empty(); } /// - [Obsolete("Use ListTunnelsAsync() method with TunnelRequestOptions.Tags instead.")] + [Obsolete("Use ListTunnelsAsync() method with TunnelRequestOptions.Labels instead.")] public async Task SearchTunnelsAsync( - string[] tags, - bool requireAllTags, + string[] labels, + bool requireAllLabels, string? clusterId, string? domain, TunnelRequestOptions? options, @@ -990,8 +874,8 @@ public async Task SearchTunnelsAsync( { string.IsNullOrEmpty(clusterId) ? "global=true" : null, !string.IsNullOrEmpty(domain) ? $"domain={HttpUtility.UrlEncode(domain)}" : null, - $"tags={string.Join(",", tags.Select(HttpUtility.UrlEncode))}", - $"allTags={requireAllTags}", + $"labels={string.Join(",", labels.Select(HttpUtility.UrlEncode))}", + $"allLabels={requireAllLabels}", !string.IsNullOrEmpty(ApiVersion) ? GetApiQuery() : null, }; var query = string.Join("&", queryParams.Where((p) => p != null)); @@ -1032,22 +916,47 @@ public async Task CreateTunnelAsync( Requires.NotNull(tunnel, nameof(tunnel)); var tunnelId = tunnel.TunnelId; - if (tunnelId != null) + var idGenerated = string.IsNullOrEmpty(tunnelId); + if (idGenerated) { - throw new ArgumentException( - "An ID may not be specified when creating a tunnel.", nameof(tunnelId)); + tunnel.TunnelId = IdGeneration.GenerateTunnelId(); + } + for (int retries = 0; retries <= 3; retries++) + { + try + { + var result = await this.SendTunnelRequestAsync( + HttpMethod.Put, + tunnel, + ManageAccessTokenScope, + path: null, + query: GetApiQuery(), + options, + ConvertTunnelForRequest(tunnel), + cancellation, + true); + PreserveAccessTokens(tunnel, result); + return result!; + } + catch (UnauthorizedAccessException) when (idGenerated && retries < 3) // The tunnel ID was already taken. + { + tunnel.TunnelId = IdGeneration.GenerateTunnelId(); + } } - var result = await this.SendRequestAsync( - HttpMethod.Post, - tunnel.ClusterId, - TunnelsPath, - query: GetApiQuery(), - options, - ConvertTunnelForRequest(tunnel), - cancellation); - PreserveAccessTokens(tunnel, result); - return result!; + // This code is unreachable, but the compiler still requires it. + var result2 = await this.SendTunnelRequestAsync( + HttpMethod.Put, + tunnel, + ManageAccessTokenScope, + path: null, + query: GetApiQuery(), + options, + ConvertTunnelForRequest(tunnel), + cancellation, + true); + PreserveAccessTokens(tunnel, result2); + return result2!; } /// @@ -1096,17 +1005,20 @@ public async Task UpdateTunnelEndpointAsync( Requires.NotNull(endpoint, nameof(endpoint)); Requires.NotNullOrEmpty(endpoint.HostId!, nameof(TunnelEndpoint.HostId)); - var path = $"{EndpointsApiSubPath}/{endpoint.HostId}/{endpoint.ConnectionMode}"; + var path = $"{EndpointsApiSubPath}/{endpoint.Id}"; + var query = GetApiQuery(); + query += "&connectionMode=" + endpoint.ConnectionMode; var result = (await this.SendTunnelRequestAsync( HttpMethod.Put, tunnel, HostAccessTokenScope, path, - query: GetApiQuery(), + query: query, options, endpoint, cancellation))!; + if (tunnel.Endpoints != null) { // Also update the endpoint in the local tunnel object. @@ -1123,15 +1035,13 @@ public async Task UpdateTunnelEndpointAsync( /// public async Task DeleteTunnelEndpointsAsync( Tunnel tunnel, - string hostId, - TunnelConnectionMode? connectionMode, + string id, TunnelRequestOptions? options = null, CancellationToken cancellation = default) { - Requires.NotNullOrEmpty(hostId, nameof(hostId)); + Requires.NotNullOrEmpty(id, nameof(id)); - var path = connectionMode == null ? $"{EndpointsApiSubPath}/{hostId}" : - $"{EndpointsApiSubPath}/{hostId}/{connectionMode}"; + var path = $"{EndpointsApiSubPath}/{id}"; var result = await this.SendTunnelRequestAsync( HttpMethod.Delete, tunnel, @@ -1145,7 +1055,7 @@ public async Task DeleteTunnelEndpointsAsync( { // Also delete the endpoint in the local tunnel object. tunnel.Endpoints = tunnel.Endpoints - .Where((e) => e.HostId != hostId || e.ConnectionMode != connectionMode) + .Where((e) => e.Id != id) .ToArray(); } @@ -1158,7 +1068,7 @@ public async Task ListTunnelPortsAsync( TunnelRequestOptions? options, CancellationToken cancellation) { - var result = await this.SendTunnelRequestAsync( + var result = await this.SendTunnelRequestAsync( HttpMethod.Get, tunnel, ReadAccessTokenScopes, @@ -1166,7 +1076,7 @@ public async Task ListTunnelPortsAsync( query: GetApiQuery(), options, cancellation); - return result!; + return result!.Value!; } /// @@ -1196,12 +1106,13 @@ public async Task CreateTunnelPortAsync( CancellationToken cancellation) { Requires.NotNull(tunnelPort, nameof(tunnelPort)); + var path = $"{PortsApiSubPath}/{tunnelPort.PortNumber}"; var result = (await this.SendTunnelRequestAsync( - HttpMethod.Post, + HttpMethod.Put, tunnel, ManagePortsAccessTokenScopes, - PortsApiSubPath, + path, query: GetApiQuery(), options, ConvertTunnelPortForRequest(tunnel, tunnelPort), @@ -1299,10 +1210,11 @@ private Tunnel ConvertTunnelForRequest(Tunnel tunnel) { return new Tunnel { + TunnelId = tunnel.TunnelId, Name = tunnel.Name, Domain = tunnel.Domain, Description = tunnel.Description, - Tags = tunnel.Tags, + Labels = tunnel.Labels, CustomExpiration = tunnel.CustomExpiration, Options = tunnel.Options, AccessControl = tunnel.AccessControl == null ? null : new TunnelAccessControl( @@ -1339,7 +1251,7 @@ private TunnelPort ConvertTunnelPortForRequest(Tunnel tunnel, TunnelPort tunnelP Protocol = tunnelPort.Protocol, IsDefault = tunnelPort.IsDefault, Description = tunnelPort.Description, - Tags = tunnelPort.Tags, + Labels = tunnelPort.Labels, Options = tunnelPort.Options, AccessControl = tunnelPort.AccessControl == null ? null : new TunnelAccessControl( tunnelPort.AccessControl.Where((ace) => !ace.IsInherited)), diff --git a/cs/src/Management/TunnelRequestOptions.cs b/cs/src/Management/TunnelRequestOptions.cs index 4fec06fb..56acbb2b 100644 --- a/cs/src/Management/TunnelRequestOptions.cs +++ b/cs/src/Management/TunnelRequestOptions.cs @@ -89,23 +89,23 @@ public class TunnelRequestOptions public bool IncludeAccessControl { get; set; } /// - /// Gets or sets an optional list of tags to filter the requested tunnels or ports. + /// Gets or sets an optional list of labels to filter the requested tunnels or ports. /// /// - /// Requested tags are compared to the or - /// when calling + /// Requested labels are compared to the or + /// when calling /// or /// respectively. By default, an - /// item is included if ANY tag matches; set to match ALL - /// tags instead. + /// item is included if ANY tag matches; set to match ALL + /// labels instead. /// - public string[]? Tags { get; set; } + public string[]? Labels { get; set; } /// - /// Gets or sets a flag that indicates whether listed items must match all tags - /// specified in . If false, an item is included if any tag matches. + /// Gets or sets a flag that indicates whether listed items must match all labels + /// specified in . If false, an item is included if any tag matches. /// - public bool RequireAllTags { get; set; } + public bool RequireAllLabels { get; set; } /// /// Gets or sets an optional list of token scopes that are requested when retrieving @@ -163,12 +163,12 @@ protected internal virtual string ToQueryString() queryOptions["forceRename"] = TrueOption; } - if (Tags != null && Tags.Length > 0) + if (Labels != null && Labels.Length > 0) { - queryOptions["tags"] = Tags; - if (RequireAllTags) + queryOptions["labels"] = Labels; + if (RequireAllLabels) { - queryOptions["allTags"] = TrueOption; + queryOptions["allLabels"] = TrueOption; } } diff --git a/cs/test/TunnelsSDK.Test/Mocks/MockTunnelManagementClient.cs b/cs/test/TunnelsSDK.Test/Mocks/MockTunnelManagementClient.cs index 68087c67..7343e383 100644 --- a/cs/test/TunnelsSDK.Test/Mocks/MockTunnelManagementClient.cs +++ b/cs/test/TunnelsSDK.Test/Mocks/MockTunnelManagementClient.cs @@ -152,12 +152,11 @@ public Task UpdateTunnelEndpointAsync( public Task DeleteTunnelEndpointsAsync( Tunnel tunnel, - string hostId, - TunnelConnectionMode? connectionMode, + string Id, TunnelRequestOptions options = null, CancellationToken cancellation = default) { - Requires.NotNullOrEmpty(hostId, nameof(hostId)); + Requires.NotNullOrEmpty(Id, nameof(Id)); if (tunnel.Endpoints == null) { @@ -166,8 +165,7 @@ public Task DeleteTunnelEndpointsAsync( var initialLength = tunnel.Endpoints.Length; tunnel.Endpoints = tunnel.Endpoints - .Where((ep) => ep.HostId == hostId && - (connectionMode == null || ep.ConnectionMode == connectionMode)) + .Where((ep) => ep.Id != Id) .ToArray(); return Task.FromResult(tunnel.Endpoints.Length < initialLength); } @@ -238,22 +236,22 @@ public Task DeleteTunnelPortAsync( } public Task SearchTunnelsAsync( - string[] tags, - bool requireAllTags, + string[] labels, + bool requireAllLabels, string clusterId, string domain, TunnelRequestOptions options, CancellationToken cancellation) { IEnumerable tunnels; - if (!requireAllTags) + if (!requireAllLabels) { - tunnels = Tunnels.Where(tunnel => (tunnel.Tags != null) && (tunnel.Tags.Intersect(tags).Count() > 0)); + tunnels = Tunnels.Where(tunnel => (tunnel.Labels != null) && (tunnel.Labels.Intersect(labels).Count() > 0)); } else { - var numTags = tags.Length; - tunnels = Tunnels.Where(tunnel => (tunnel.Tags != null) && (tunnel.Tags.Intersect(tags).Count() == numTags)); + var numLabels = labels.Length; + tunnels = Tunnels.Where(tunnel => (tunnel.Labels != null) && (tunnel.Labels.Intersect(labels).Count() == numLabels)); } domain ??= string.Empty; diff --git a/cs/tools/TunnelsSDK.Generator/TSContractWriter.cs b/cs/tools/TunnelsSDK.Generator/TSContractWriter.cs index c67389d1..41b88cd7 100644 --- a/cs/tools/TunnelsSDK.Generator/TSContractWriter.cs +++ b/cs/tools/TunnelsSDK.Generator/TSContractWriter.cs @@ -81,7 +81,7 @@ private void WriteContractType( if (nestedTypes.Length > 0) { s.AppendLine(); - s.Append($"{indent}namespace {type.Name} {{"); + s.Append($"{indent}export namespace {type.Name} {{"); foreach (var nestedType in nestedTypes.Where( (t) => !ContractsGenerator.ExcludedContractTypes.Contains(t.Name))) @@ -204,7 +204,7 @@ private void WriteStaticClassContract( { s.Append(FormatDocComment(type.GetDocumentationCommentXml(), indent)); - s.Append($"namespace {type.Name} {{"); + s.Append($"export namespace {type.Name} {{"); foreach (var member in type.GetMembers()) { diff --git a/go/tunnels/examples/example.go b/go/tunnels/examples/example.go index b3759cc9..498c90af 100644 --- a/go/tunnels/examples/example.go +++ b/go/tunnels/examples/example.go @@ -44,7 +44,7 @@ func main() { } // create manager to get tunnel - managementClient, err := tunnels.NewManager(userAgent, getAccessToken, url, nil) + managementClient, err := tunnels.NewManager(userAgent, getAccessToken, url, nil, "2023-09-27-preview") if err != nil { fmt.Println(fmt.Errorf(err.Error())) return diff --git a/go/tunnels/id_generation.go b/go/tunnels/id_generation.go new file mode 100644 index 00000000..eb686713 --- /dev/null +++ b/go/tunnels/id_generation.go @@ -0,0 +1,24 @@ +package tunnels + +import ( + "math/rand" + "strings" + "time" +) + +var nouns = []string{ "pond", "hill", "mountain", "field", "fog", "ant", "dog", "cat", "shoe", "plane", "chair", "book", "ocean", "lake", "river" , "horse"} +var adjectives = []string{"fun", "happy", "interesting", "neat", "peaceful", "puzzled", "kind", "joyful", "new", "giant", "sneaky", "quick", "majestic", "jolly" , "fancy", "tidy", "swift", "silent", "amusing", "spiffy"} + +func generateTunnelId() string { + rand.Seed(time.Now().UnixNano()) + var sb strings.Builder + sb.WriteString(adjectives[rand.Intn(len(adjectives))]) + sb.WriteString("-") + sb.WriteString(nouns[rand.Intn(len(nouns))]) + sb.WriteString("-") + + for i := 0; i < 7; i++ { + sb.WriteByte(TunnelConstraintsOldTunnelIDChars[rand.Intn(len(TunnelConstraintsOldTunnelIDChars))]) + } + return sb.String() +} diff --git a/go/tunnels/manager.go b/go/tunnels/manager.go index d1ec1d35..c90c8db4 100644 --- a/go/tunnels/manager.go +++ b/go/tunnels/manager.go @@ -40,12 +40,11 @@ var DevServiceProperties = TunnelServiceProperties{ type tokenProviderfn func() string const ( - apiV1Path = "/api/v1" - tunnelsApiPath = apiV1Path + "/tunnels" - userLimitsApiPath = apiV1Path + "/userlimits" - subjectsApiPath = apiV1Path + "/subjects" - clustersApiPath = apiV1Path + "/clusters" - checkNameAvailabilityPath = "/checkNameAvailability" + tunnelsApiPath = "/tunnels" + userLimitsApiPath = "/userlimits" + subjectsApiPath = "/subjects" + clustersApiPath = "/clusters" + checkNameAvailabilityPath = ":checkNameAvailability" endpointsApiSubPath = "/endpoints" portsApiSubPath = "/ports" tunnelAuthenticationScheme = "Tunnel" @@ -63,6 +62,8 @@ var ( readAccessTokenScopes = []TunnelAccessScope{TunnelAccessScopeManage, TunnelAccessScopeManagePorts, TunnelAccessScopeHost, TunnelAccessScopeConnect} ) +var apiVersions = []string{"2023-09-27-preview"} + // UserAgent contains the name and version of the client. type UserAgent struct { Name string @@ -76,13 +77,17 @@ type Manager struct { uri *url.URL additionalHeaders map[string]string userAgents []UserAgent + apiVersion string } // Creates a new Manager used for interacting with the Tunnels APIs. // tokenProvider is an optional paramater containing a function that returns the access token to use for the request. // If no tunnelServiceUrl or httpClient is provided, the default values will be used. // Can return error if userAgent is empty or url is invalid. -func NewManager(userAgents []UserAgent, tp tokenProviderfn, tunnelServiceUrl *url.URL, httpHandler *http.Client) (*Manager, error) { +func NewManager(userAgents []UserAgent, tp tokenProviderfn, tunnelServiceUrl *url.URL, httpHandler *http.Client, apiVersion string) (*Manager, error) { + if !contains(apiVersions, apiVersion) { + return nil, fmt.Errorf("api version must be in %v", apiVersions) + } if len(userAgents) == 0 { return nil, fmt.Errorf("user agents cannot be empty") } @@ -111,7 +116,7 @@ func NewManager(userAgents []UserAgent, tp tokenProviderfn, tunnelServiceUrl *ur client = httpHandler } - return &Manager{tokenProvider: tp, httpClient: client, uri: tunnelServiceUrl, userAgents: userAgents}, nil + return &Manager{tokenProvider: tp, httpClient: client, uri: tunnelServiceUrl, userAgents: userAgents, apiVersion: apiVersion}, nil } // Lists tunnels owned by the authenticated user. @@ -131,11 +136,16 @@ func (m *Manager) ListTunnels( if err != nil { return nil, fmt.Errorf("error sending list tunnel request: %w", err) } - - err = json.Unmarshal(response, &ts) + var tunnelResponse *TunnelListByRegionResponse + err = json.Unmarshal(response, &tunnelResponse) if err != nil { return nil, fmt.Errorf("error parsing response json to tunnel: %w", err) } + for _, region := range tunnelResponse.Value { + for _, tunnel := range region.Value { + ts = append(ts, &tunnel) + } + } return ts, nil } @@ -144,7 +154,7 @@ func (m *Manager) ListTunnels( // If getting a tunnel by name the domain must be provided if the tunnel is not in the default domain. // Returns the requested tunnel or an error if the tunnel is not found. func (m *Manager) GetTunnel(ctx context.Context, tunnel *Tunnel, options *TunnelRequestOptions) (t *Tunnel, err error) { - url, err := m.buildTunnelSpecificUri(tunnel, "", options, "") + url, err := m.buildTunnelSpecificUri(tunnel, "", options, "", false) if err != nil { return nil, fmt.Errorf("error creating tunnel url: %w", err) } @@ -170,15 +180,27 @@ func (m *Manager) CreateTunnel(ctx context.Context, tunnel *Tunnel, options *Tun if tunnel == nil { return nil, fmt.Errorf("tunnel must be provided") } - if tunnel.TunnelID != "" { - return nil, fmt.Errorf("tunnelId cannot be set for creating a tunnel") + if tunnel.TunnelID == "" { + tunnel.TunnelID = generateTunnelId() + } + url, err := m.buildTunnelSpecificUri(tunnel, "", options, "", true) + if err != nil { + return nil, fmt.Errorf("error creating request url: %w", err) } - url := m.buildUri(tunnel.ClusterID, tunnelsApiPath, options, "") convertedTunnel, err := tunnel.requestObject() + convertedTunnel.TunnelID = tunnel.TunnelID if err != nil { return nil, fmt.Errorf("error converting tunnel for request: %w", err) } - response, err := m.sendTunnelRequest(ctx, tunnel, options, http.MethodPost, url, convertedTunnel, nil, manageAccessTokenScope, false) + var response []byte + + for i := 0; i < 3; i++ { + response, err = m.sendTunnelRequest(ctx, tunnel, options, http.MethodPut, url, convertedTunnel, nil, manageAccessTokenScope, false) + if err != nil { + convertedTunnel.TunnelID = generateTunnelId() + tunnel.TunnelID = convertedTunnel.TunnelID + } + } if err != nil { return nil, fmt.Errorf("error sending create tunnel request: %w", err) } @@ -199,7 +221,7 @@ func (m *Manager) UpdateTunnel(ctx context.Context, tunnel *Tunnel, updateFields return nil, fmt.Errorf("tunnel must be provided") } - url, err := m.buildTunnelSpecificUri(tunnel, "", options, "") + url, err := m.buildTunnelSpecificUri(tunnel, "", options, "", false) if err != nil { return nil, fmt.Errorf("error creating request url: %w", err) } @@ -225,7 +247,7 @@ func (m *Manager) UpdateTunnel(ctx context.Context, tunnel *Tunnel, updateFields // Deletes a tunnel. // Returns error if delete fails. func (m *Manager) DeleteTunnel(ctx context.Context, tunnel *Tunnel, options *TunnelRequestOptions) error { - url, err := m.buildTunnelSpecificUri(tunnel, "", options, "") + url, err := m.buildTunnelSpecificUri(tunnel, "", options, "", false) if err != nil { return fmt.Errorf("error creating tunnel url: %w", err) } @@ -248,7 +270,7 @@ func (m *Manager) UpdateTunnelEndpoint( if endpoint.HostID == "" { return nil, fmt.Errorf("endpoint hostId must be provided and must not be nil") } - url, err := m.buildTunnelSpecificUri(tunnel, fmt.Sprintf("%s/%s/%s", endpointsApiSubPath, endpoint.HostID, endpoint.ConnectionMode), options, "") + url, err := m.buildTunnelSpecificUri(tunnel, fmt.Sprintf("%s/%s", endpointsApiSubPath, endpoint.ID), options, fmt.Sprintf("connectionMode=%s", endpoint.ConnectionMode), false) if err != nil { return nil, fmt.Errorf("error creating tunnel url: %w", err) } @@ -279,16 +301,14 @@ func (m *Manager) UpdateTunnelEndpoint( // Deletes endpoints on a tunnel. // Returns error if the delete fails. func (m *Manager) DeleteTunnelEndpoints( - ctx context.Context, tunnel *Tunnel, hostID string, connectionMode TunnelConnectionMode, options *TunnelRequestOptions, + ctx context.Context, tunnel *Tunnel, Id string, options *TunnelRequestOptions, ) error { - if hostID == "" { - return fmt.Errorf("hostId must be provided and must not be nil") - } - path := fmt.Sprintf("%s/%s/%s", endpointsApiSubPath, hostID, connectionMode) - if connectionMode == "" { - path = fmt.Sprintf("%s/%s", endpointsApiSubPath, hostID) + if Id == "" { + return fmt.Errorf("Id must be provided and must not be nil") } - url, err := m.buildTunnelSpecificUri(tunnel, path, options, "") + path := fmt.Sprintf("%s/%s", endpointsApiSubPath, Id) + + url, err := m.buildTunnelSpecificUri(tunnel, path, options, "", false) if err != nil { return fmt.Errorf("error creating tunnel url: %w", err) } @@ -300,7 +320,7 @@ func (m *Manager) DeleteTunnelEndpoints( var newEndpoints []TunnelEndpoint for _, ep := range tunnel.Endpoints { - if ep.HostID != hostID || ep.ConnectionMode != connectionMode { + if ep.ID != Id { newEndpoints = append(newEndpoints, ep) } } @@ -313,7 +333,7 @@ func (m *Manager) DeleteTunnelEndpoints( func (m *Manager) ListTunnelPorts( ctx context.Context, tunnel *Tunnel, options *TunnelRequestOptions, ) (tp []*TunnelPort, err error) { - url, err := m.buildTunnelSpecificUri(tunnel, portsApiSubPath, options, "") + url, err := m.buildTunnelSpecificUri(tunnel, portsApiSubPath, options, "", false) if err != nil { return nil, fmt.Errorf("error creating tunnel url: %w", err) } @@ -323,18 +343,24 @@ func (m *Manager) ListTunnelPorts( return nil, fmt.Errorf("error sending list tunnel ports request: %w", err) } + var tunnelPortsResponse *TunnelPortListResponse // Read response into a tunnel port - err = json.Unmarshal(response, &tp) + err = json.Unmarshal(response, &tunnelPortsResponse) if err != nil { return nil, fmt.Errorf("error parsing response json to tunnel ports: %w", err) } + + for _, port := range tunnelPortsResponse.Value { + tp = append(tp, &port) + } + return tp, nil } func (m *Manager) GetTunnelPort( ctx context.Context, tunnel *Tunnel, port int, options *TunnelRequestOptions, ) (tp *TunnelPort, err error) { - url, err := m.buildTunnelSpecificUri(tunnel, fmt.Sprintf("%s/%d", portsApiSubPath, port), options, "") + url, err := m.buildTunnelSpecificUri(tunnel, fmt.Sprintf("%s/%d", portsApiSubPath, port), options, "", false) if err != nil { return nil, fmt.Errorf("error creating tunnel url: %w", err) } @@ -357,7 +383,8 @@ func (m *Manager) GetTunnelPort( func (m *Manager) CreateTunnelPort( ctx context.Context, tunnel *Tunnel, port *TunnelPort, options *TunnelRequestOptions, ) (tp *TunnelPort, err error) { - url, err := m.buildTunnelSpecificUri(tunnel, portsApiSubPath, options, "") + path := fmt.Sprintf("%s/%d", portsApiSubPath, port.PortNumber) + url, err := m.buildTunnelSpecificUri(tunnel, path, options, "", false) if err != nil { return nil, fmt.Errorf("error creating tunnel url: %w", err) } @@ -367,7 +394,7 @@ func (m *Manager) CreateTunnelPort( return nil, fmt.Errorf("error converting port for request: %w", err) } - response, err := m.sendTunnelRequest(ctx, tunnel, options, http.MethodPost, url, convertedPort, nil, managePortsAccessTokenScopes, true) + response, err := m.sendTunnelRequest(ctx, tunnel, options, http.MethodPut, url, convertedPort, nil, managePortsAccessTokenScopes, true) if err != nil { return nil, fmt.Errorf("error sending create tunnel port request: %w", err) } @@ -400,7 +427,7 @@ func (m *Manager) UpdateTunnelPort( return nil, fmt.Errorf("cluster ids do not match") } path := fmt.Sprintf("%s/%d", portsApiSubPath, port.PortNumber) - url, err := m.buildTunnelSpecificUri(tunnel, path, options, "") + url, err := m.buildTunnelSpecificUri(tunnel, path, options, "", false) if err != nil { return nil, fmt.Errorf("error creating tunnel url: %w", err) } @@ -441,7 +468,7 @@ func (m *Manager) DeleteTunnelPort( ) error { path := fmt.Sprintf("%s/%d", portsApiSubPath, port) - url, err := m.buildTunnelSpecificUri(tunnel, path, options, "") + url, err := m.buildTunnelSpecificUri(tunnel, path, options, "", false) if err != nil { return fmt.Errorf("error creating tunnel url: %w", err) } @@ -699,18 +726,24 @@ func (m *Manager) buildUri(clusterId string, path string, options *TunnelRequest } } } + + if query != "" { + query = fmt.Sprintf("%s&api-version=%s", query, m.apiVersion) + } else { + query = fmt.Sprintf("api-version=%s", m.apiVersion) + } baseAddress.Path = path baseAddress.RawQuery = query return baseAddress } -func (m *Manager) buildTunnelSpecificUri(tunnel *Tunnel, path string, options *TunnelRequestOptions, query string) (*url.URL, error) { +func (m *Manager) buildTunnelSpecificUri(tunnel *Tunnel, path string, options *TunnelRequestOptions, query string, isCreate bool) (*url.URL, error) { var tunnelPath string if tunnel == nil { return nil, fmt.Errorf("tunnel cannot be nil to make uri") } switch { - case tunnel.ClusterID != "" && tunnel.TunnelID != "": + case (tunnel.ClusterID != "" || isCreate) && tunnel.TunnelID != "": tunnelPath = fmt.Sprintf("%s/%s", tunnelsApiPath, tunnel.TunnelID) case tunnel.Name != "": tunnelPath = fmt.Sprintf("%s/%s", tunnelsApiPath, tunnel.Name) @@ -749,3 +782,12 @@ func partialMarshal(value interface{}, fields []string) ([]byte, error) { return json.Marshal(m) } + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/go/tunnels/manager_test.go b/go/tunnels/manager_test.go index 6b788746..f4dab118 100644 --- a/go/tunnels/manager_test.go +++ b/go/tunnels/manager_test.go @@ -41,7 +41,7 @@ func TestTunnelCreateDelete(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -80,7 +80,7 @@ func TestListTunnels(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -134,7 +134,7 @@ func TestGetAccessToken(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -195,7 +195,7 @@ func TestTunnelCreateUpdateDelete(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -246,7 +246,7 @@ func TestTunnelCreateUpdateTwiceDelete(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -310,7 +310,7 @@ func TestTunnelCreateGetDelete(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -361,7 +361,7 @@ func TestTunnelAddPort(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -425,7 +425,7 @@ func TestTunnelDeletePort(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -508,7 +508,7 @@ func TestTunnelUpdatePort(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -608,7 +608,7 @@ func TestTunnelListPorts(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -695,7 +695,7 @@ func TestTunnelEndpoints(t *testing.T) { t.Errorf(err.Error()) } - managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil) + managementClient, err := NewManager(userAgentManagerTest, getUserToken, url, nil, "2023-09-27-preview") if err != nil { t.Errorf(err.Error()) } @@ -719,6 +719,7 @@ func TestTunnelEndpoints(t *testing.T) { // Create and add endpoint endpoint := &TunnelEndpoint{ HostID: "test", + ID: "test", ConnectionMode: TunnelConnectionModeTunnelRelay, } @@ -744,7 +745,7 @@ func TestTunnelEndpoints(t *testing.T) { t.Errorf("endpoint was not successfully updated") } - err = managementClient.DeleteTunnelEndpoints(ctx, createdTunnel, "test", TunnelConnectionModeTunnelRelay, options) + err = managementClient.DeleteTunnelEndpoints(ctx, createdTunnel, "test", options) if err != nil { t.Errorf(err.Error()) return diff --git a/go/tunnels/request_options.go b/go/tunnels/request_options.go index 37d1792d..cdcf47fe 100644 --- a/go/tunnels/request_options.go +++ b/go/tunnels/request_options.go @@ -28,14 +28,14 @@ type TunnelRequestOptions struct { // Flag that requests tunnel access control details when listing or searching tunnels. IncludeAccessControl bool - // Optional list of tags to filter the requested tunnels or ports. - // By default, an item is included if ANY tag matches; set `requireAllTags` to match - // ALL tags instead. - Tags []string + // Optional list of labels to filter the requested tunnels or ports. + // By default, an item is included if ANY label matches; set `requireAllLabels` to match + // ALL labels instead. + Labels []string - // Flag that indicates whether listed items must match all tags specified in `tags`. + // Flag that indicates whether listed items must match all labels specified in `labels`. // If false, an item is included if any tag matches. - RequireAllTags bool + RequireAllLabels bool // List of token scopes that are requested when retrieving a tunnel or tunnel port object. TokenScopes TunnelAccessScopes @@ -66,13 +66,13 @@ func (options *TunnelRequestOptions) queryString() string { if options.ForceRename { queryOptions.Set("forceRename", "true") } - if options.Tags != nil { - for _, tag := range options.Tags { - queryOptions.Add("tags", string(tag)) + if options.Labels != nil { + for _, label := range options.Labels { + queryOptions.Add("labels", string(label)) } - if options.RequireAllTags { - queryOptions.Set("allTags", "true") + if options.RequireAllLabels { + queryOptions.Set("allLabels", "true") } } diff --git a/go/tunnels/tunnel.go b/go/tunnels/tunnel.go index ebc7717f..6e188dca 100644 --- a/go/tunnels/tunnel.go +++ b/go/tunnels/tunnel.go @@ -25,8 +25,8 @@ type Tunnel struct { // Gets or sets the description of the tunnel. Description string `json:"description,omitempty"` - // Gets or sets the tags of the tunnel. - Tags []string `json:"tags,omitempty"` + // Gets or sets the labels of the tunnel. + Labels []string `json:"labels,omitempty"` // Gets or sets the optional parent domain of the tunnel, if it is not using the default // parent domain. diff --git a/go/tunnels/tunnel_constraints.go b/go/tunnels/tunnel_constraints.go index 924d0cd3..2a50a02e 100644 --- a/go/tunnels/tunnel_constraints.go +++ b/go/tunnels/tunnel_constraints.go @@ -37,13 +37,13 @@ const ( TunnelConstraintsDescriptionMaxLength = 400 // Min length of a single tunnel or port tag. - TunnelConstraintsTagMinLength = 1 + TunnelConstraintsLabelMinLength = 1 // Max length of a single tunnel or port tag. - TunnelConstraintsTagMaxLength = 50 + TunnelConstraintsLabelMaxLength = 50 - // Maximum number of tags that can be applied to a tunnel or port. - TunnelConstraintsMaxTags = 100 + // Maximum number of labels that can be applied to a tunnel or port. + TunnelConstraintsMaxLabels = 100 // Min length of a tunnel domain. TunnelConstraintsTunnelDomainMinLength = 4 @@ -114,8 +114,8 @@ const ( // empty string because tunnels may be unnamed. TunnelConstraintsTunnelNamePattern = "([a-z0-9][a-z0-9-]{1,58}[a-z0-9])|(^$)" - // Regular expression that can match or validate tunnel or port tags. - TunnelConstraintsTagPattern = "[\\w-=]{1,50}" + // Regular expression that can match or validate tunnel or port labels. + TunnelConstraintsLabelPattern = "[\\w-=]{1,50}" // Regular expression that can match or validate tunnel domains. // @@ -169,8 +169,8 @@ var ( // empty string because tunnels may be unnamed. TunnelConstraintsTunnelNameRegex = regexp.MustCompile(TunnelConstraintsTunnelNamePattern) - // Regular expression that can match or validate tunnel or port tags. - TunnelConstraintsTagRegex = regexp.MustCompile(TunnelConstraintsTagPattern) + // Regular expression that can match or validate tunnel or port labels. + TunnelConstraintsLabelRegex = regexp.MustCompile(TunnelConstraintsLabelPattern) // Regular expression that can match or validate tunnel domains. // diff --git a/go/tunnels/tunnel_list_by_region.go b/go/tunnels/tunnel_list_by_region.go index 9dc818f8..a2db664a 100644 --- a/go/tunnels/tunnel_list_by_region.go +++ b/go/tunnels/tunnel_list_by_region.go @@ -13,7 +13,7 @@ type TunnelListByRegion struct { ClusterID string `json:"clusterId,omitempty"` // List of tunnels. - Value []TunnelV2 `json:"value,omitempty"` + Value []Tunnel `json:"value,omitempty"` // Error detail if getting list of tunnels in the region failed. Error *ErrorDetail `json:"error,omitempty"` diff --git a/go/tunnels/tunnel_list_response.go b/go/tunnels/tunnel_list_response.go index e07940c2..4dab71f4 100644 --- a/go/tunnels/tunnel_list_response.go +++ b/go/tunnels/tunnel_list_response.go @@ -7,7 +7,7 @@ package tunnels // Data contract for response of a list tunnel call. type TunnelListResponse struct { // List of tunnels - Value []TunnelV2 `json:"value,omitempty"` + Value []Tunnel `json:"value,omitempty"` // Link to get next page of results NextLink string `json:"nextLink,omitempty"` diff --git a/go/tunnels/tunnel_port.go b/go/tunnels/tunnel_port.go index 71d69da7..da25d172 100644 --- a/go/tunnels/tunnel_port.go +++ b/go/tunnels/tunnel_port.go @@ -23,8 +23,8 @@ type TunnelPort struct { // Gets or sets the optional description of the port. Description string `json:"description,omitempty"` - // Gets or sets the tags of the port. - Tags []string `json:"tags,omitempty"` + // Gets or sets the labels of the port. + Labels []string `json:"labels,omitempty"` // Gets or sets the protocol of the tunnel port. // diff --git a/go/tunnels/tunnel_port_list_response.go b/go/tunnels/tunnel_port_list_response.go index 6258a4ce..a0d5a37b 100644 --- a/go/tunnels/tunnel_port_list_response.go +++ b/go/tunnels/tunnel_port_list_response.go @@ -7,7 +7,7 @@ package tunnels // Data contract for response of a list tunnel ports call. type TunnelPortListResponse struct { // List of tunnels - Value []TunnelPortV2 `json:"value,omitempty"` + Value []TunnelPort `json:"value,omitempty"` // Link to get next page of results NextLink string `json:"nextLink,omitempty"` diff --git a/go/tunnels/tunnel_port_v2.go b/go/tunnels/tunnel_port_v2.go deleted file mode 100644 index b977fe45..00000000 --- a/go/tunnels/tunnel_port_v2.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// Generated from ../../../cs/src/Contracts/TunnelPortV2.cs - -package tunnels - -// Data contract for tunnel port objects managed through the tunnel service REST API. -type TunnelPortV2 struct { - // Gets or sets the ID of the cluster the tunnel was created in. - ClusterID string `json:"clusterId,omitempty"` - - // Gets or sets the generated ID of the tunnel, unique within the cluster. - TunnelID string `json:"tunnelId,omitempty"` - - // Gets or sets the IP port number of the tunnel port. - PortNumber uint16 `json:"portNumber"` - - // Gets or sets the optional short name of the port. - // - // The name must be unique among named ports of the same tunnel. - Name string `json:"name,omitempty"` - - // Gets or sets the optional description of the port. - Description string `json:"description,omitempty"` - - // Gets or sets the tags of the port. - Labels []string `json:"labels,omitempty"` - - // Gets or sets the protocol of the tunnel port. - // - // Should be one of the string constants from `TunnelProtocol`. - Protocol string `json:"protocol,omitempty"` - - // Gets or sets a value indicating whether this port is a default port for the tunnel. - // - // A client that connects to a tunnel (by ID or name) without specifying a port number - // will connect to the default port for the tunnel, if a default is configured. Or if the - // tunnel has only one port then the single port is the implicit default. - // - // Selection of a default port for a connection also depends on matching the connection - // to the port `TunnelPortV2.Protocol`, so it is possible to configure separate defaults - // for distinct protocols like `TunnelProtocol.Http` and `TunnelProtocol.Ssh`. - IsDefault bool `json:"isDefault,omitempty"` - - // Gets or sets a dictionary mapping from scopes to tunnel access tokens. - // - // Unlike the tokens in `Tunnel.AccessTokens`, these tokens are restricted to the - // individual port. - AccessTokens map[TunnelAccessScope]string `json:"accessTokens,omitempty"` - - // Gets or sets access control settings for the tunnel port. - // - // See `TunnelAccessControl` documentation for details about the access control model. - AccessControl *TunnelAccessControl `json:"accessControl,omitempty"` - - // Gets or sets options for the tunnel port. - Options *TunnelOptions `json:"options,omitempty"` - - // Gets or sets current connection status of the tunnel port. - Status *TunnelPortStatus `json:"status,omitempty"` - - // Gets or sets the username for the ssh service user is trying to forward. - // - // Should be provided if the `TunnelProtocol` is Ssh. - SshUser string `json:"sshUser,omitempty"` - - // Gets or sets web forwarding URIs. If set, it's a list of absolute URIs where the port - // can be accessed with web forwarding. - PortForwardingURIs []string `json:"portForwardingUris"` - - // Gets or sets inspection URI. If set, it's an absolute URIs where the port's traffic - // can be inspected. - InspectionURI string `json:"inspectionUri"` -} diff --git a/go/tunnels/tunnel_v2.go b/go/tunnels/tunnel_v2.go deleted file mode 100644 index cb3e21eb..00000000 --- a/go/tunnels/tunnel_v2.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// Generated from ../../../cs/src/Contracts/TunnelV2.cs - -package tunnels - -import ( - "time" -) - -// Data contract for tunnel objects managed through the tunnel service REST API. -type TunnelV2 struct { - // Gets or sets the ID of the cluster the tunnel was created in. - ClusterID string `json:"clusterId,omitempty"` - - // Gets or sets the generated ID of the tunnel, unique within the cluster. - TunnelID string `json:"tunnelId,omitempty"` - - // Gets or sets the optional short name (alias) of the tunnel. - // - // The name must be globally unique within the parent domain, and must be a valid - // subdomain. - Name string `json:"name,omitempty"` - - // Gets or sets the description of the tunnel. - Description string `json:"description,omitempty"` - - // Gets or sets the tags of the tunnel. - Labels []string `json:"labels,omitempty"` - - // Gets or sets the optional parent domain of the tunnel, if it is not using the default - // parent domain. - Domain string `json:"domain,omitempty"` - - // Gets or sets a dictionary mapping from scopes to tunnel access tokens. - AccessTokens map[TunnelAccessScope]string `json:"accessTokens,omitempty"` - - // Gets or sets access control settings for the tunnel. - // - // See `TunnelAccessControl` documentation for details about the access control model. - AccessControl *TunnelAccessControl `json:"accessControl,omitempty"` - - // Gets or sets default options for the tunnel. - Options *TunnelOptions `json:"options,omitempty"` - - // Gets or sets current connection status of the tunnel. - Status *TunnelStatus `json:"status,omitempty"` - - // Gets or sets an array of endpoints where hosts are currently accepting client - // connections to the tunnel. - Endpoints []TunnelEndpoint `json:"endpoints,omitempty"` - - // Gets or sets a list of ports in the tunnel. - // - // This optional property enables getting info about all ports in a tunnel at the same - // time as getting tunnel info, or creating one or more ports at the same time as - // creating a tunnel. It is omitted when listing (multiple) tunnels, or when updating - // tunnel properties. (For the latter, use APIs to create/update/delete individual ports - // instead.) - Ports []TunnelPortV2 `json:"ports,omitempty"` - - // Gets or sets the time in UTC of tunnel creation. - Created *time.Time `json:"created,omitempty"` - - // Gets or the time the tunnel will be deleted if it is not used or updated. - Expiration *time.Time `json:"expiration,omitempty"` - - // Gets or the custom amount of time the tunnel will be valid if it is not used or - // updated in seconds. - CustomExpiration uint32 `json:"customExpiration,omitempty"` -} diff --git a/go/tunnels/tunnels.go b/go/tunnels/tunnels.go index f6c0704e..61c72a1f 100644 --- a/go/tunnels/tunnels.go +++ b/go/tunnels/tunnels.go @@ -10,14 +10,15 @@ import ( "github.com/rodaine/table" ) -const PackageVersion = "0.0.25" +const PackageVersion = "0.1.0" func (tunnel *Tunnel) requestObject() (*Tunnel, error) { convertedTunnel := &Tunnel{ + TunnelID: tunnel.TunnelID, Name: tunnel.Name, Domain: tunnel.Domain, Description: tunnel.Description, - Tags: tunnel.Tags, + Labels: tunnel.Labels, Options: tunnel.Options, Endpoints: tunnel.Endpoints, CustomExpiration: tunnel.CustomExpiration, @@ -71,7 +72,7 @@ func (t *Tunnel) Table() table.Table { tbl.AddRow("TunnelId", t.TunnelID) tbl.AddRow("Name", t.Name) tbl.AddRow("Description", t.Description) - tbl.AddRow("Tags", fmt.Sprintf("%v", t.Tags)) + tbl.AddRow("Labels", fmt.Sprintf("%v", t.Labels)) if t.AccessControl != nil { tbl.AddRow("Access Control", fmt.Sprintf("%v", *t.AccessControl)) } @@ -132,7 +133,7 @@ func (tunnelPort *TunnelPort) requestObject(tunnel *Tunnel) (*TunnelPort, error) Protocol: tunnelPort.Protocol, IsDefault: tunnelPort.IsDefault, Description: tunnelPort.Description, - Tags: tunnelPort.Tags, + Labels: tunnelPort.Labels, SshUser: tunnelPort.SshUser, Options: tunnelPort.Options, } diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/Tunnel.java b/java/src/main/java/com/microsoft/tunnels/contracts/Tunnel.java index 887f0af1..f913704b 100644 --- a/java/src/main/java/com/microsoft/tunnels/contracts/Tunnel.java +++ b/java/src/main/java/com/microsoft/tunnels/contracts/Tunnel.java @@ -40,10 +40,10 @@ public class Tunnel { public String description; /** - * Gets or sets the tags of the tunnel. + * Gets or sets the labels of the tunnel. */ @Expose - public String[] tags; + public String[] labels; /** * Gets or sets the optional parent domain of the tunnel, if it is not using the diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelConstraints.java b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelConstraints.java index 5e1e3a3b..e04d5359 100644 --- a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelConstraints.java +++ b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelConstraints.java @@ -58,17 +58,17 @@ public class TunnelConstraints { /** * Min length of a single tunnel or port tag. */ - public static final int tagMinLength = 1; + public static final int labelMinLength = 1; /** * Max length of a single tunnel or port tag. */ - public static final int tagMaxLength = 50; + public static final int labelMaxLength = 50; /** - * Maximum number of tags that can be applied to a tunnel or port. + * Maximum number of labels that can be applied to a tunnel or port. */ - public static final int maxTags = 100; + public static final int maxLabels = 100; /** * Min length of a tunnel domain. @@ -211,14 +211,14 @@ public class TunnelConstraints { public static final Pattern tunnelNameRegex = java.util.regex.Pattern.compile(TunnelConstraints.tunnelNamePattern); /** - * Regular expression that can match or validate tunnel or port tags. + * Regular expression that can match or validate tunnel or port labels. */ - public static final String tagPattern = "[\\w-=]{1,50}"; + public static final String labelPattern = "[\\w-=]{1,50}"; /** - * Regular expression that can match or validate tunnel or port tags. + * Regular expression that can match or validate tunnel or port labels. */ - public static final Pattern tagRegex = java.util.regex.Pattern.compile(TunnelConstraints.tagPattern); + public static final Pattern labelRegex = java.util.regex.Pattern.compile(TunnelConstraints.labelPattern); /** * Regular expression that can match or validate tunnel domains. diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelConstraintsStatics.java b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelConstraintsStatics.java index 3753089e..7fbea5d9 100644 --- a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelConstraintsStatics.java +++ b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelConstraintsStatics.java @@ -58,7 +58,7 @@ static boolean isValidTag(String tag) { return false; } - var matcher = TunnelConstraints.tagRegex.matcher(tag); + var matcher = TunnelConstraints.labelRegex.matcher(tag); return matcher.find() && matcher.start() == 0 && matcher.end() == tag.length(); } diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelListByRegion.java b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelListByRegion.java index fbab79bd..eafd495b 100644 --- a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelListByRegion.java +++ b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelListByRegion.java @@ -26,7 +26,7 @@ public class TunnelListByRegion { * List of tunnels. */ @Expose - public TunnelV2[] value; + public Tunnel[] value; /** * Error detail if getting list of tunnels in the region failed. diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelListResponse.java b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelListResponse.java deleted file mode 100644 index 42886358..00000000 --- a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelListResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// Generated from ../../../../../../../../cs/src/Contracts/TunnelListResponse.cs - -package com.microsoft.tunnels.contracts; - -import com.google.gson.annotations.Expose; - -/** - * Data contract for response of a list tunnel call. - */ -public class TunnelListResponse { - /** - * List of tunnels - */ - @Expose - public TunnelV2[] value; - - /** - * Link to get next page of results - */ - @Expose - public String nextLink; -} diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPort.java b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPort.java index cb208c74..51f2a4e1 100644 --- a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPort.java +++ b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPort.java @@ -44,10 +44,10 @@ public class TunnelPort { public String description; /** - * Gets or sets the tags of the port. + * Gets or sets the labels of the port. */ @Expose - public String[] tags; + public String[] labels; /** * Gets or sets the protocol of the tunnel port. diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPortListResponse.java b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPortListResponse.java index 6ca5283a..d0a6ffb5 100644 --- a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPortListResponse.java +++ b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPortListResponse.java @@ -14,7 +14,7 @@ public class TunnelPortListResponse { * List of tunnels */ @Expose - public TunnelPortV2[] value; + public TunnelPort[] value; /** * Link to get next page of results diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPortV2.java b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPortV2.java deleted file mode 100644 index 1bc04225..00000000 --- a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelPortV2.java +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// Generated from ../../../../../../../../cs/src/Contracts/TunnelPortV2.cs - -package com.microsoft.tunnels.contracts; - -import com.google.gson.annotations.Expose; -import java.util.Map; - -/** - * Data contract for tunnel port objects managed through the tunnel service REST API. - */ -public class TunnelPortV2 { - /** - * Gets or sets the ID of the cluster the tunnel was created in. - */ - @Expose - public String clusterId; - - /** - * Gets or sets the generated ID of the tunnel, unique within the cluster. - */ - @Expose - public String tunnelId; - - /** - * Gets or sets the IP port number of the tunnel port. - */ - @Expose - public int portNumber; - - /** - * Gets or sets the optional short name of the port. - * - * The name must be unique among named ports of the same tunnel. - */ - @Expose - public String name; - - /** - * Gets or sets the optional description of the port. - */ - @Expose - public String description; - - /** - * Gets or sets the tags of the port. - */ - @Expose - public String[] labels; - - /** - * Gets or sets the protocol of the tunnel port. - * - * Should be one of the string constants from {@link TunnelProtocol}. - */ - @Expose - public String protocol; - - /** - * Gets or sets a value indicating whether this port is a default port for the tunnel. - * - * A client that connects to a tunnel (by ID or name) without specifying a port number - * will connect to the default port for the tunnel, if a default is configured. Or if - * the tunnel has only one port then the single port is the implicit default. - * - * Selection of a default port for a connection also depends on matching the - * connection to the port {@link TunnelPortV2#protocol}, so it is possible to - * configure separate defaults for distinct protocols like {@link TunnelProtocol#http} - * and {@link TunnelProtocol#ssh}. - */ - @Expose - public boolean isDefault; - - /** - * Gets or sets a dictionary mapping from scopes to tunnel access tokens. - * - * Unlike the tokens in {@link Tunnel#accessTokens}, these tokens are restricted to - * the individual port. - */ - @Expose - public Map accessTokens; - - /** - * Gets or sets access control settings for the tunnel port. - * - * See {@link TunnelAccessControl} documentation for details about the access control - * model. - */ - @Expose - public TunnelAccessControl accessControl; - - /** - * Gets or sets options for the tunnel port. - */ - @Expose - public TunnelOptions options; - - /** - * Gets or sets current connection status of the tunnel port. - */ - @Expose - public TunnelPortStatus status; - - /** - * Gets or sets the username for the ssh service user is trying to forward. - * - * Should be provided if the {@link TunnelProtocol} is Ssh. - */ - @Expose - public String sshUser; - - /** - * Gets or sets web forwarding URIs. If set, it's a list of absolute URIs where the - * port can be accessed with web forwarding. - */ - @Expose - public String[] portForwardingUris; - - /** - * Gets or sets inspection URI. If set, it's an absolute URIs where the port's traffic - * can be inspected. - */ - @Expose - public String inspectionUri; -} diff --git a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelV2.java b/java/src/main/java/com/microsoft/tunnels/contracts/TunnelV2.java deleted file mode 100644 index 9d5016a7..00000000 --- a/java/src/main/java/com/microsoft/tunnels/contracts/TunnelV2.java +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// Generated from ../../../../../../../../cs/src/Contracts/TunnelV2.cs - -package com.microsoft.tunnels.contracts; - -import com.google.gson.annotations.Expose; -import java.util.Date; -import java.util.Map; - -/** - * Data contract for tunnel objects managed through the tunnel service REST API. - */ -public class TunnelV2 { - /** - * Gets or sets the ID of the cluster the tunnel was created in. - */ - @Expose - public String clusterId; - - /** - * Gets or sets the generated ID of the tunnel, unique within the cluster. - */ - @Expose - public String tunnelId; - - /** - * Gets or sets the optional short name (alias) of the tunnel. - * - * The name must be globally unique within the parent domain, and must be a valid - * subdomain. - */ - @Expose - public String name; - - /** - * Gets or sets the description of the tunnel. - */ - @Expose - public String description; - - /** - * Gets or sets the tags of the tunnel. - */ - @Expose - public String[] labels; - - /** - * Gets or sets the optional parent domain of the tunnel, if it is not using the - * default parent domain. - */ - @Expose - public String domain; - - /** - * Gets or sets a dictionary mapping from scopes to tunnel access tokens. - */ - @Expose - public Map accessTokens; - - /** - * Gets or sets access control settings for the tunnel. - * - * See {@link TunnelAccessControl} documentation for details about the access control - * model. - */ - @Expose - public TunnelAccessControl accessControl; - - /** - * Gets or sets default options for the tunnel. - */ - @Expose - public TunnelOptions options; - - /** - * Gets or sets current connection status of the tunnel. - */ - @Expose - public TunnelStatus status; - - /** - * Gets or sets an array of endpoints where hosts are currently accepting client - * connections to the tunnel. - */ - @Expose - public TunnelEndpoint[] endpoints; - - /** - * Gets or sets a list of ports in the tunnel. - * - * This optional property enables getting info about all ports in a tunnel at the same - * time as getting tunnel info, or creating one or more ports at the same time as - * creating a tunnel. It is omitted when listing (multiple) tunnels, or when updating - * tunnel properties. (For the latter, use APIs to create/update/delete individual - * ports instead.) - */ - @Expose - public TunnelPortV2[] ports; - - /** - * Gets or sets the time in UTC of tunnel creation. - */ - @Expose - public Date created; - - /** - * Gets or the time the tunnel will be deleted if it is not used or updated. - */ - @Expose - public Date expiration; - - /** - * Gets or the custom amount of time the tunnel will be valid if it is not used or - * updated in seconds. - */ - @Expose - public int customExpiration; -} diff --git a/java/src/main/java/com/microsoft/tunnels/management/ITunnelManagementClient.java b/java/src/main/java/com/microsoft/tunnels/management/ITunnelManagementClient.java index 6becb08f..cfe72a88 100644 --- a/java/src/main/java/com/microsoft/tunnels/management/ITunnelManagementClient.java +++ b/java/src/main/java/com/microsoft/tunnels/management/ITunnelManagementClient.java @@ -111,19 +111,14 @@ public CompletableFuture updateTunnelEndpointAsync( * name * (globally unique, if configured) or tunnel ID and * cluster ID. - * @param hostId Required ID of the host for endpoint(s) to be + * @param id Required ID of the endpoint to be * deleted. - * @param tunnelConnectionMode Optional connection mode for endpoint(s) to be - * deleted, - * or null to delete endpoints for all connection - * modes. * @param options Request options. * @return True if one or more endpoints were deleted, false if none were found. */ public CompletableFuture deleteTunnelEndpointsAsync( Tunnel tunnel, - String hostId, - TunnelConnectionMode tunnelConnectionMode, + String id, TunnelRequestOptions options); /** diff --git a/java/src/main/java/com/microsoft/tunnels/management/IdGeneration.java b/java/src/main/java/com/microsoft/tunnels/management/IdGeneration.java new file mode 100644 index 00000000..be9fa9fe --- /dev/null +++ b/java/src/main/java/com/microsoft/tunnels/management/IdGeneration.java @@ -0,0 +1,20 @@ +package com.microsoft.tunnels.management; + +import com.microsoft.tunnels.contracts.TunnelConstraints; +import java.util.Random; + +public class IdGeneration { + private static String[] nouns = { "pond", "hill", "mountain", "field", "fog", "ant", "dog", "cat", "shoe", "plane", "chair", "book", "ocean", "lake", "river" , "horse" }; + private static String[] adjectives = {"fun", "happy", "interesting", "neat", "peaceful", "puzzled", "kind", "joyful", "new", "giant", "sneaky", "quick", "majestic", "jolly" , "fancy", "tidy", "swift", "silent", "amusing", "spiffy"}; + private static Random rand = new Random(); + + public static String generateTunnelId() { + String tunnelId = ""; + tunnelId += adjectives[rand.nextInt(adjectives.length)] + "-"; + tunnelId += nouns[rand.nextInt(nouns.length)] + "-"; + for (int i = 0; i < 7; i++) { + tunnelId += TunnelConstraints.oldTunnelIdChars.charAt(rand.nextInt(TunnelConstraints.oldTunnelIdChars.length())); + } + return tunnelId; + } +} diff --git a/java/src/main/java/com/microsoft/tunnels/management/TunnelManagementClient.java b/java/src/main/java/com/microsoft/tunnels/management/TunnelManagementClient.java index 1b85ef6c..98b5e237 100644 --- a/java/src/main/java/com/microsoft/tunnels/management/TunnelManagementClient.java +++ b/java/src/main/java/com/microsoft/tunnels/management/TunnelManagementClient.java @@ -50,15 +50,14 @@ public class TunnelManagementClient implements ITunnelManagementClient { // Api strings private static final String prodServiceUri = "https://global.rel.tunnels.api.visualstudio.com"; - private static final String apiV1Path = "/api/v1"; - private static final String tunnelsApiPath = apiV1Path + "/tunnels"; - private static final String userLimitsApiPath = apiV1Path + "/userlimits"; - private static final String subjectsApiPath = apiV1Path + "/subjects"; + private static final String tunnelsApiPath = "/tunnels"; + private static final String userLimitsApiPath = "/userlimits"; + private static final String subjectsApiPath = "/subjects"; private static final String endpointsApiSubPath = "/endpoints"; private static final String portsApiSubPath = "/ports"; - private String clustersApiPath = apiV1Path + "/clusters"; + private String clustersApiPath = "/clusters"; private static final String tunnelAuthenticationScheme = "Tunnel"; - private static final String checkTunnelNamePath = "/checkNameAvailability"; + private static final String checkTunnelNamePath = ":checkNameAvailability"; // Access Scopes private static final String[] ManageAccessTokenScope = { @@ -85,15 +84,21 @@ public class TunnelManagementClient implements ITunnelManagementClient { private final ProductHeaderValue[] userAgents; private final Supplier> userTokenCallback; private final String baseAddress; + private final String apiVersion; - public TunnelManagementClient(ProductHeaderValue[] userAgents) { - this(userAgents, null, null); + public static final String[] ApiVersions = { + "2023-09-27-preview" + }; + + public TunnelManagementClient(ProductHeaderValue[] userAgents, String apiVersion) { + this(userAgents, null, apiVersion); } public TunnelManagementClient( ProductHeaderValue[] userAgents, - Supplier> userTokenCallback) { - this(userAgents, userTokenCallback, null); + Supplier> userTokenCallback, + String apiVersion) { + this(userAgents, userTokenCallback, null, apiVersion); } /** @@ -109,15 +114,20 @@ public TunnelManagementClient( public TunnelManagementClient( ProductHeaderValue[] userAgents, Supplier> userTokenCallback, - String tunnelServiceUri) { + String tunnelServiceUri, + String apiVersion) { if (userAgents.length == 0) { throw new IllegalArgumentException("user agents cannot be empty"); } + if (!Arrays.asList(ApiVersions).contains(apiVersion)) { + throw new IllegalArgumentException("apiVersion must be one of: " + Arrays.toString(ApiVersions)); + } this.userAgents = userAgents; this.userTokenCallback = userTokenCallback != null ? userTokenCallback : () -> CompletableFuture.completedFuture(null); this.baseAddress = tunnelServiceUri != null ? tunnelServiceUri : prodServiceUri; this.httpClient = HttpClient.newHttpClient(); + this.apiVersion = apiVersion; } private CompletableFuture requestAsync( @@ -243,21 +253,21 @@ private T parseResponse(HttpResponse response, Type typeOfT) { return gson.fromJson(response.body(), typeOfT); } - private URI buildUri(Tunnel tunnel, TunnelRequestOptions options) { - return buildUri(tunnel, options, null, null); + private URI buildUri(Tunnel tunnel, TunnelRequestOptions options, boolean isTunnelCreate) { + return buildUri(tunnel, options, null, null, isTunnelCreate); } private URI buildUri(Tunnel tunnel, TunnelRequestOptions options, String path) { - return buildUri(tunnel, options, path, null); + return buildUri(tunnel, options, path, null, false); } - private URI buildUri(Tunnel tunnel, TunnelRequestOptions options, String path, String query) { + private URI buildUri(Tunnel tunnel, TunnelRequestOptions options, String path, String query, boolean isTunnelCreate) { if (tunnel == null) { throw new Error("Tunnel must be specified"); } String tunnelPath; - if (StringUtils.isNotBlank(tunnel.clusterId) && StringUtils.isNotBlank(tunnel.tunnelId)) { + if ((StringUtils.isNotBlank(tunnel.clusterId) || isTunnelCreate) && StringUtils.isNotBlank(tunnel.tunnelId)) { tunnelPath = tunnelsApiPath + "/" + tunnel.tunnelId; } else { if (tunnel.name == null) { @@ -310,6 +320,9 @@ private URI buildUri(String clusterId, queryString += StringUtils.isBlank(queryString) ? query : "&" + query; } + queryString += StringUtils.isBlank(queryString) ? "api-version="+this.apiVersion : "&api-version="+this.apiVersion; + + try { return new URI( baseAddress.getScheme(), @@ -346,7 +359,7 @@ public CompletableFuture> listTunnelsAsync( @Override public CompletableFuture getTunnelAsync(Tunnel tunnel, TunnelRequestOptions options) { - var requestUri = buildUri(tunnel, options, null, null); + var requestUri = buildUri(tunnel, options, null, null, false); final Type responseType = new TypeToken() { }.getType(); return requestAsync( @@ -361,28 +374,51 @@ public CompletableFuture getTunnelAsync(Tunnel tunnel, TunnelRequestOpti @Override public CompletableFuture createTunnelAsync(Tunnel tunnel, TunnelRequestOptions options) { - if (tunnel.tunnelId != null) { - throw new IllegalArgumentException("Tunnel ID may not be specified when creating a tunnel."); + var generatedId = tunnel.tunnelId == null; + if (generatedId) { + tunnel.tunnelId = IdGeneration.generateTunnelId(); } - var uri = buildUri(tunnel.clusterId, tunnelsApiPath, options, null); + var uri = buildUri(tunnel, options, true); final Type responseType = new TypeToken() { }.getType(); + for (int i = 0; i <= 3; i++){ + try { + return requestAsync( + tunnel, + options, + HttpMethod.PUT, + uri, + ManageAccessTokenScope, + convertTunnelForRequest(tunnel), + responseType); + } + catch (Exception e) { + if (generatedId) { + tunnel.tunnelId = IdGeneration.generateTunnelId();; + } + else{ + throw e; + } + } + } + return requestAsync( - tunnel, - options, - HttpMethod.POST, - uri, - ManageAccessTokenScope, - convertTunnelForRequest(tunnel), - responseType); + tunnel, + options, + HttpMethod.PUT, + uri, + ManageAccessTokenScope, + convertTunnelForRequest(tunnel), + responseType); } private Tunnel convertTunnelForRequest(Tunnel tunnel) { Tunnel converted = new Tunnel(); + converted.tunnelId = tunnel.tunnelId; converted.name = tunnel.name; converted.domain = tunnel.domain; converted.description = tunnel.description; - converted.tags = tunnel.tags; + converted.labels = tunnel.labels; converted.options = tunnel.options; converted.accessControl = tunnel.accessControl; converted.customExpiration = tunnel.customExpiration; @@ -408,7 +444,7 @@ private Tunnel convertTunnelForRequest(Tunnel tunnel) { @Override public CompletableFuture updateTunnelAsync(Tunnel tunnel, TunnelRequestOptions options) { - var uri = buildUri(tunnel, options); + var uri = buildUri(tunnel, options, true); final Type responseType = new TypeToken() { }.getType(); return requestAsync( @@ -427,7 +463,7 @@ public CompletableFuture deleteTunnelAsync(Tunnel tunnel) { @Override public CompletableFuture deleteTunnelAsync(Tunnel tunnel, TunnelRequestOptions options) { - var uri = buildUri(tunnel, options); + var uri = buildUri(tunnel, options, true); final Type responseType = new TypeToken() { }.getType(); return requestAsync( @@ -448,15 +484,17 @@ public CompletableFuture updateTunnelEndpointAsync( if (endpoint == null) { throw new IllegalArgumentException("Endpoint must not be null."); } - if (StringUtils.isBlank(endpoint.hostId)) { - throw new IllegalArgumentException("Endpoint hostId must not be null."); + if (StringUtils.isBlank(endpoint.id)) { + throw new IllegalArgumentException("Endpoint id must not be null."); } - var path = endpointsApiSubPath + "/" + endpoint.hostId + "/" + endpoint.connectionMode; + var path = endpointsApiSubPath + "/" + endpoint.id; var uri = buildUri( tunnel, options, - path); + path, + "connectionMode=" + endpoint.connectionMode.toString(), + false); final Type responseType = new TypeToken() { }.getType(); @@ -472,7 +510,7 @@ public CompletableFuture updateTunnelEndpointAsync( if (tunnel.endpoints != null) { var updatedEndpoints = new ArrayList(); for (TunnelEndpoint e : tunnel.endpoints) { - if (e.hostId != endpoint.hostId || e.connectionMode != endpoint.connectionMode) { + if (e.id != endpoint.id) { updatedEndpoints.add(e); } } @@ -486,16 +524,12 @@ public CompletableFuture updateTunnelEndpointAsync( @Override public CompletableFuture deleteTunnelEndpointsAsync( Tunnel tunnel, - String hostId, - TunnelConnectionMode connectionMode, + String id, TunnelRequestOptions options) { - if (hostId == null) { - throw new IllegalArgumentException("hostId must not be null"); - } - var path = endpointsApiSubPath + "/" + hostId; - if (connectionMode != null) { - path += "/" + connectionMode; + if (id == null) { + throw new IllegalArgumentException("id must not be null"); } + var path = endpointsApiSubPath + "/" + id; var uri = buildUri(tunnel, options, path); final Type responseType = new TypeToken() { @@ -512,7 +546,7 @@ public CompletableFuture deleteTunnelEndpointsAsync( if (tunnel.endpoints != null) { var updatedEndpoints = new ArrayList(); for (TunnelEndpoint e : tunnel.endpoints) { - if (e.hostId != hostId || e.connectionMode != connectionMode) { + if (e.id != id) { updatedEndpoints.add(e); } } @@ -569,13 +603,17 @@ public CompletableFuture createTunnelPortAsync( if (tunnelPort == null) { throw new IllegalArgumentException("Tunnel port must be specified"); } - var uri = buildUri(tunnel, options, portsApiSubPath); + var path = portsApiSubPath + "/" + tunnelPort.portNumber; + var uri = buildUri( + tunnel, + options, + path); final Type responseType = new TypeToken() { }.getType(); CompletableFuture result = requestAsync( tunnel, options, - HttpMethod.POST, + HttpMethod.PUT, uri, ManagePortsAccessTokenScopes, convertTunnelPortForRequest(tunnel, tunnelPort), @@ -615,7 +653,7 @@ private TunnelPort convertTunnelPortForRequest(Tunnel tunnel, TunnelPort tunnelP converted.protocol = tunnelPort.protocol; converted.isDefault = tunnelPort.isDefault; converted.description = tunnelPort.description; - converted.tags = tunnelPort.tags; + converted.labels = tunnelPort.labels; converted.sshUser = tunnelPort.sshUser; converted.options = tunnelPort.options; if (tunnelPort.accessControl != null && tunnelPort.accessControl.entries != null) { diff --git a/java/src/main/java/com/microsoft/tunnels/management/TunnelRequestOptions.java b/java/src/main/java/com/microsoft/tunnels/management/TunnelRequestOptions.java index ddb2b73d..e1b58ec0 100644 --- a/java/src/main/java/com/microsoft/tunnels/management/TunnelRequestOptions.java +++ b/java/src/main/java/com/microsoft/tunnels/management/TunnelRequestOptions.java @@ -79,20 +79,20 @@ public class TunnelRequestOptions { public boolean includeAccessControl; /** - * Gets or sets an optional list of tags to filter the requested tunnels or ports. + * Gets or sets an optional list of labels to filter the requested tunnels or ports. * - * Requested tags are compared to the `Tunnel.tags` or `TunnelPort.tags` when calling + * Requested labels are compared to the `Tunnel.labels` or `TunnelPort.labels` when calling * `TunnelManagementClient.listTunnels` or `TunnelManagementClient.listTunnelPorts` respectively. - * By default, an item is included if ANY tag matches; set `requireAllTags` to match - * ALL tags instead. + * By default, an item is included if ANY tag matches; set `requireAllLabels` to match + * ALL labels instead. */ - public Collection tags; + public Collection labels; /* - * Gets or sets a flag that indicates whether listed items must match all tags - * specified in `tags`. If false, an item is included if any tag matches. + * Gets or sets a flag that indicates whether listed items must match all labels + * specified in `labels`. If false, an item is included if any tag matches. */ - public boolean requireAllTags; + public boolean requireAllLabels; /** * Gets or sets an optional list of token scopes that are requested when retrieving a @@ -144,10 +144,10 @@ public String toQueryString() { queryOptions.put("forceRename", Arrays.asList("true")); } - if (this.tags != null) { - queryOptions.put("tags", this.tags); - if (this.requireAllTags) { - queryOptions.put("allTags", Arrays.asList("true")); + if (this.labels != null) { + queryOptions.put("labels", this.labels); + if (this.requireAllLabels) { + queryOptions.put("allLabels", Arrays.asList("true")); } } diff --git a/java/src/test/java/com/microsoft/tunnels/TunnelManagementClientTests.java b/java/src/test/java/com/microsoft/tunnels/TunnelManagementClientTests.java index c96fb516..4c4742bb 100644 --- a/java/src/test/java/com/microsoft/tunnels/TunnelManagementClientTests.java +++ b/java/src/test/java/com/microsoft/tunnels/TunnelManagementClientTests.java @@ -65,7 +65,7 @@ public void createTunnel() { assertNotNull(createdTunnel.options); assertNotNull(createdTunnel.ports); assertNotNull(createdTunnel.status); - assertNotNull(createdTunnel.tags); + assertNotNull(createdTunnel.labels); assertNotNull(createdTunnel.tunnelId); tunnelManagementClient.deleteTunnelAsync(createdTunnel).join(); diff --git a/java/src/test/java/com/microsoft/tunnels/TunnelTest.java b/java/src/test/java/com/microsoft/tunnels/TunnelTest.java index 50f2c056..60ea84c5 100644 --- a/java/src/test/java/com/microsoft/tunnels/TunnelTest.java +++ b/java/src/test/java/com/microsoft/tunnels/TunnelTest.java @@ -36,7 +36,8 @@ public abstract class TunnelTest { protected static TunnelManagementClient tunnelManagementClient = new TunnelManagementClient( new ProductHeaderValue[] { userAgent }, - userTokenCallback); + userTokenCallback, + "2023-09-27-preview"); protected static final Logger logger = LoggerFactory.getLogger(TunnelTest.class); diff --git a/ts/src/connections/tunnelRelayTunnelHost.ts b/ts/src/connections/tunnelRelayTunnelHost.ts index 32f39e33..a1444a2f 100644 --- a/ts/src/connections/tunnelRelayTunnelHost.ts +++ b/ts/src/connections/tunnelRelayTunnelHost.ts @@ -84,7 +84,7 @@ export class TunnelRelayTunnelHost extends tunnelRelaySessionClass( public constructor(managementClient: TunnelManagementClient, trace?: Trace) { super(managementClient, trace); this.hostId = MultiModeTunnelHost.hostId; - this.id = uuidv4(); + this.id = uuidv4() + "-relay"; } /** @@ -517,8 +517,7 @@ export class TunnelRelayTunnelHost extends tunnelRelaySessionClass( if (this.tunnel) { const promise = this.managementClient!.deleteTunnelEndpoints( this.tunnel, - this.hostId, - TunnelConnectionMode.TunnelRelay, + this.id, ); promises.push(promise); } diff --git a/ts/src/contracts/index.ts b/ts/src/contracts/index.ts index c8938b1e..41f709d7 100644 --- a/ts/src/contracts/index.ts +++ b/ts/src/contracts/index.ts @@ -19,3 +19,5 @@ export { TunnelRelayTunnelEndpoint } from './tunnelRelayTunnelEndpoint'; export { TunnelServiceProperties } from './tunnelServiceProperties'; export { TunnelStatus } from './tunnelStatus'; export { ClusterDetails } from './clusterDetails'; +export { TunnelListByRegionResponse } from './tunnelListByRegionResponse'; +export { TunnelPortListResponse } from './tunnelPortListResponse'; diff --git a/ts/src/contracts/tunnel.ts b/ts/src/contracts/tunnel.ts index 8e5edd46..d34f1c90 100644 --- a/ts/src/contracts/tunnel.ts +++ b/ts/src/contracts/tunnel.ts @@ -37,9 +37,9 @@ export interface Tunnel { description?: string; /** - * Gets or sets the tags of the tunnel. + * Gets or sets the labels of the tunnel. */ - tags?: string[]; + labels?: string[]; /** * Gets or sets the optional parent domain of the tunnel, if it is not using the diff --git a/ts/src/contracts/tunnelAccessControlEntry.ts b/ts/src/contracts/tunnelAccessControlEntry.ts index a05c9ad1..ea1bdd34 100644 --- a/ts/src/contracts/tunnelAccessControlEntry.ts +++ b/ts/src/contracts/tunnelAccessControlEntry.ts @@ -98,7 +98,7 @@ export interface TunnelAccessControlEntry { expiration?: Date; } -namespace TunnelAccessControlEntry { +export namespace TunnelAccessControlEntry { /** * Constants for well-known identity providers. */ diff --git a/ts/src/contracts/tunnelConstraints.ts b/ts/src/contracts/tunnelConstraints.ts index e41a70a9..da6ae0af 100644 --- a/ts/src/contracts/tunnelConstraints.ts +++ b/ts/src/contracts/tunnelConstraints.ts @@ -6,7 +6,7 @@ /** * Tunnel constraints. */ -namespace TunnelConstraints { +export namespace TunnelConstraints { /** * Min length of tunnel cluster ID. */ @@ -55,17 +55,17 @@ namespace TunnelConstraints { /** * Min length of a single tunnel or port tag. */ - export const tagMinLength: number = 1; + export const labelMinLength: number = 1; /** * Max length of a single tunnel or port tag. */ - export const tagMaxLength: number = 50; + export const labelMaxLength: number = 50; /** - * Maximum number of tags that can be applied to a tunnel or port. + * Maximum number of labels that can be applied to a tunnel or port. */ - export const maxTags: number = 100; + export const maxLabels: number = 100; /** * Min length of a tunnel domain. @@ -208,14 +208,14 @@ namespace TunnelConstraints { export const tunnelNameRegex: RegExp = new RegExp(TunnelConstraints.tunnelNamePattern); /** - * Regular expression that can match or validate tunnel or port tags. + * Regular expression that can match or validate tunnel or port labels. */ - export const tagPattern: string = '[\\w-=]{1,50}'; + export const labelPattern: string = '[\\w-=]{1,50}'; /** - * Regular expression that can match or validate tunnel or port tags. + * Regular expression that can match or validate tunnel or port labels. */ - export const tagRegex: RegExp = new RegExp(TunnelConstraints.tagPattern); + export const labelRegex: RegExp = new RegExp(TunnelConstraints.labelPattern); /** * Regular expression that can match or validate tunnel domains. diff --git a/ts/src/contracts/tunnelListByRegion.ts b/ts/src/contracts/tunnelListByRegion.ts index 027c981d..397155f9 100644 --- a/ts/src/contracts/tunnelListByRegion.ts +++ b/ts/src/contracts/tunnelListByRegion.ts @@ -4,7 +4,7 @@ /* eslint-disable */ import { ErrorDetail } from './errorDetail'; -import { TunnelV2 } from './tunnelV2'; +import { Tunnel } from './tunnel'; /** * Tunnel list by region. @@ -23,7 +23,7 @@ export interface TunnelListByRegion { /** * List of tunnels. */ - value?: TunnelV2[]; + value?: Tunnel[]; /** * Error detail if getting list of tunnels in the region failed. diff --git a/ts/src/contracts/tunnelListResponse.ts b/ts/src/contracts/tunnelListResponse.ts deleted file mode 100644 index 3da3ccf0..00000000 --- a/ts/src/contracts/tunnelListResponse.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// Generated from ../../../cs/src/Contracts/TunnelListResponse.cs -/* eslint-disable */ - -import { TunnelV2 } from './tunnelV2'; - -/** - * Data contract for response of a list tunnel call. - */ -export interface TunnelListResponse { - /** - * List of tunnels - */ - value: TunnelV2[]; - - /** - * Link to get next page of results - */ - nextLink?: string; -} diff --git a/ts/src/contracts/tunnelPort.ts b/ts/src/contracts/tunnelPort.ts index 9190b891..4c5080c2 100644 --- a/ts/src/contracts/tunnelPort.ts +++ b/ts/src/contracts/tunnelPort.ts @@ -39,9 +39,9 @@ export interface TunnelPort { description?: string; /** - * Gets or sets the tags of the port. + * Gets or sets the labels of the port. */ - tags?: string[]; + labels?: string[]; /** * Gets or sets the protocol of the tunnel port. diff --git a/ts/src/contracts/tunnelPortListResponse.ts b/ts/src/contracts/tunnelPortListResponse.ts index 206a49fd..ed62eccc 100644 --- a/ts/src/contracts/tunnelPortListResponse.ts +++ b/ts/src/contracts/tunnelPortListResponse.ts @@ -3,7 +3,7 @@ // Generated from ../../../cs/src/Contracts/TunnelPortListResponse.cs /* eslint-disable */ -import { TunnelPortV2 } from './tunnelPortV2'; +import { TunnelPort } from './tunnelPort'; /** * Data contract for response of a list tunnel ports call. @@ -12,7 +12,7 @@ export interface TunnelPortListResponse { /** * List of tunnels */ - value: TunnelPortV2[]; + value: TunnelPort[]; /** * Link to get next page of results diff --git a/ts/src/contracts/tunnelPortV2.ts b/ts/src/contracts/tunnelPortV2.ts deleted file mode 100644 index 6f4be8ab..00000000 --- a/ts/src/contracts/tunnelPortV2.ts +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// Generated from ../../../cs/src/Contracts/TunnelPortV2.cs -/* eslint-disable */ - -import { TunnelAccessControl } from './tunnelAccessControl'; -import { TunnelOptions } from './tunnelOptions'; -import { TunnelPortStatus } from './tunnelPortStatus'; - -/** - * Data contract for tunnel port objects managed through the tunnel service REST API. - */ -export interface TunnelPortV2 { - /** - * Gets or sets the ID of the cluster the tunnel was created in. - */ - clusterId?: string; - - /** - * Gets or sets the generated ID of the tunnel, unique within the cluster. - */ - tunnelId?: string; - - /** - * Gets or sets the IP port number of the tunnel port. - */ - portNumber: number; - - /** - * Gets or sets the optional short name of the port. - * - * The name must be unique among named ports of the same tunnel. - */ - name?: string; - - /** - * Gets or sets the optional description of the port. - */ - description?: string; - - /** - * Gets or sets the tags of the port. - */ - labels?: string[]; - - /** - * Gets or sets the protocol of the tunnel port. - * - * Should be one of the string constants from {@link TunnelProtocol}. - */ - protocol?: string; - - /** - * Gets or sets a value indicating whether this port is a default port for the tunnel. - * - * A client that connects to a tunnel (by ID or name) without specifying a port number - * will connect to the default port for the tunnel, if a default is configured. Or if - * the tunnel has only one port then the single port is the implicit default. - * - * Selection of a default port for a connection also depends on matching the - * connection to the port {@link TunnelPortV2.protocol}, so it is possible to - * configure separate defaults for distinct protocols like {@link TunnelProtocol.http} - * and {@link TunnelProtocol.ssh}. - */ - isDefault?: boolean; - - /** - * Gets or sets a dictionary mapping from scopes to tunnel access tokens. - * - * Unlike the tokens in {@link Tunnel.accessTokens}, these tokens are restricted to - * the individual port. - */ - accessTokens?: { [scope: string]: string }; - - /** - * Gets or sets access control settings for the tunnel port. - * - * See {@link TunnelAccessControl} documentation for details about the access control - * model. - */ - accessControl?: TunnelAccessControl; - - /** - * Gets or sets options for the tunnel port. - */ - options?: TunnelOptions; - - /** - * Gets or sets current connection status of the tunnel port. - */ - status?: TunnelPortStatus; - - /** - * Gets or sets the username for the ssh service user is trying to forward. - * - * Should be provided if the {@link TunnelProtocol} is Ssh. - */ - sshUser?: string; - - /** - * Gets or sets web forwarding URIs. If set, it's a list of absolute URIs where the - * port can be accessed with web forwarding. - */ - portForwardingUris?: string[]; - - /** - * Gets or sets inspection URI. If set, it's an absolute URIs where the port's traffic - * can be inspected. - */ - inspectionUri?: string; -} diff --git a/ts/src/contracts/tunnelV2.ts b/ts/src/contracts/tunnelV2.ts deleted file mode 100644 index 5c6dcf00..00000000 --- a/ts/src/contracts/tunnelV2.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// Generated from ../../../cs/src/Contracts/TunnelV2.cs -/* eslint-disable */ - -import { TunnelAccessControl } from './tunnelAccessControl'; -import { TunnelEndpoint } from './tunnelEndpoint'; -import { TunnelOptions } from './tunnelOptions'; -import { TunnelPortV2 } from './tunnelPortV2'; -import { TunnelStatus } from './tunnelStatus'; - -/** - * Data contract for tunnel objects managed through the tunnel service REST API. - */ -export interface TunnelV2 { - /** - * Gets or sets the ID of the cluster the tunnel was created in. - */ - clusterId?: string; - - /** - * Gets or sets the generated ID of the tunnel, unique within the cluster. - */ - tunnelId?: string; - - /** - * Gets or sets the optional short name (alias) of the tunnel. - * - * The name must be globally unique within the parent domain, and must be a valid - * subdomain. - */ - name?: string; - - /** - * Gets or sets the description of the tunnel. - */ - description?: string; - - /** - * Gets or sets the tags of the tunnel. - */ - labels?: string[]; - - /** - * Gets or sets the optional parent domain of the tunnel, if it is not using the - * default parent domain. - */ - domain?: string; - - /** - * Gets or sets a dictionary mapping from scopes to tunnel access tokens. - */ - accessTokens?: { [scope: string]: string }; - - /** - * Gets or sets access control settings for the tunnel. - * - * See {@link TunnelAccessControl} documentation for details about the access control - * model. - */ - accessControl?: TunnelAccessControl; - - /** - * Gets or sets default options for the tunnel. - */ - options?: TunnelOptions; - - /** - * Gets or sets current connection status of the tunnel. - */ - status?: TunnelStatus; - - /** - * Gets or sets an array of endpoints where hosts are currently accepting client - * connections to the tunnel. - */ - endpoints?: TunnelEndpoint[]; - - /** - * Gets or sets a list of ports in the tunnel. - * - * This optional property enables getting info about all ports in a tunnel at the same - * time as getting tunnel info, or creating one or more ports at the same time as - * creating a tunnel. It is omitted when listing (multiple) tunnels, or when updating - * tunnel properties. (For the latter, use APIs to create/update/delete individual - * ports instead.) - */ - ports?: TunnelPortV2[]; - - /** - * Gets or sets the time in UTC of tunnel creation. - */ - created?: Date; - - /** - * Gets or the time the tunnel will be deleted if it is not used or updated. - */ - expiration?: Date; - - /** - * Gets or the custom amount of time the tunnel will be valid if it is not used or - * updated in seconds. - */ - customExpiration?: number; -} diff --git a/ts/src/management/idGeneration.ts b/ts/src/management/idGeneration.ts new file mode 100644 index 00000000..f2cb11fd --- /dev/null +++ b/ts/src/management/idGeneration.ts @@ -0,0 +1,16 @@ +import { TunnelConstraints } from "../contracts/tunnelConstraints" + +export class IdGeneration { + private static nouns: string[] = [ "pond", "hill", "mountain", "field", "fog", "ant", "dog", "cat", "shoe", "plane", "chair", "book", "ocean", "lake", "river" , "horse"]; + private static adjectives: string[] = ["fun", "happy", "interesting", "neat", "peaceful", "puzzled", "kind", "joyful", "new", "giant", "sneaky", "quick", "majestic", "jolly" , "fancy", "tidy", "swift", "silent", "amusing", "spiffy"]; + + public static generateTunnelId(): string { + let tunnelId = ""; + tunnelId += this.adjectives[Math.floor(Math.random() * this.adjectives.length)] + "-"; + tunnelId += this.nouns[Math.floor(Math.random() * this.nouns.length)] + "-"; + for (let i = 0; i < 7; i++) { + tunnelId += TunnelConstraints.oldTunnelIdChars[Math.floor(Math.random() * (TunnelConstraints.oldTunnelIdChars.length))]; + } + return tunnelId; + } +} \ No newline at end of file diff --git a/ts/src/management/tunnelManagementClient.ts b/ts/src/management/tunnelManagementClient.ts index 0a0f647c..27263d72 100644 --- a/ts/src/management/tunnelManagementClient.ts +++ b/ts/src/management/tunnelManagementClient.ts @@ -25,7 +25,7 @@ export interface TunnelManagementClient { /** * Lists tunnels that are owned by the caller. * - * The list can be filtered by setting `TunnelRequestOptions.tags`. Ports will not be + * The list can be filtered by setting `TunnelRequestOptions.labels`. Ports will not be * included in the returned tunnels unless `TunnelRequestOptions.includePorts` is set to true. * * @param clusterId A tunnel cluster ID, or null to list tunnels globally. @@ -86,21 +86,19 @@ export interface TunnelManagementClient { /** * Deletes a tunnel endpoint. * @param tunnel - * @param hostId - * @param connectionMode + * @param id * @param options */ deleteTunnelEndpoints( tunnel: Tunnel, - hostId: string, - connectionMode?: TunnelConnectionMode, + id: string, options?: TunnelRequestOptions, ): Promise; /** * Lists ports on a tunnel. * - * The list can be filtered by setting `TunnelRequestOptions.tags`. + * The list can be filtered by setting `TunnelRequestOptions.labels`. * * @param tunnel Tunnel object including at least either a tunnel name (globally unique, * if configured) or tunnel ID and cluster ID. diff --git a/ts/src/management/tunnelManagementHttpClient.ts b/ts/src/management/tunnelManagementHttpClient.ts index 3c9ba6c6..b2f9cc00 100644 --- a/ts/src/management/tunnelManagementHttpClient.ts +++ b/ts/src/management/tunnelManagementHttpClient.ts @@ -12,6 +12,8 @@ import { TunnelServiceProperties, ClusterDetails, NamedRateStatus, + TunnelListByRegionResponse, + TunnelPortListResponse, } from '@microsoft/dev-tunnels-contracts'; import { ProductHeaderValue, @@ -24,17 +26,17 @@ import { tunnelSdkUserAgent } from './version'; import axios, { AxiosAdapter, AxiosError, AxiosRequestConfig, AxiosResponse, Method } from 'axios'; import * as https from 'https'; import { TunnelPlanTokenProperties } from './tunnelPlanTokenProperties'; +import { IdGeneration } from './idGeneration'; type NullableIfNotBoolean = T extends boolean ? T : T | null; -const apiV1Path = `/api/v1`; -const tunnelsApiPath = apiV1Path + '/tunnels'; -const limitsApiPath = apiV1Path + '/userlimits'; +const tunnelsApiPath = '/tunnels'; +const limitsApiPath = '/userlimits'; const endpointsApiSubPath = '/endpoints'; const portsApiSubPath = '/ports'; -const clustersApiPath = apiV1Path + '/clusters'; +const clustersApiPath = '/clusters'; const tunnelAuthentication = 'Authorization'; -const checkAvailablePath = '/checkAvailability'; +const checkAvailablePath = ':checkNameAvailability'; function comparePorts(a: TunnelPort, b: TunnelPort) { return (a.portNumber ?? Number.MAX_SAFE_INTEGER) - (b.portNumber ?? Number.MAX_SAFE_INTEGER); @@ -101,9 +103,11 @@ const readAccessTokenScopes = [ TunnelAccessScopes.Host, TunnelAccessScopes.Connect, ]; +const apiVersions = ["2023-09-27-preview"] export class TunnelManagementHttpClient implements TunnelManagementClient { public additionalRequestHeaders?: { [header: string]: string }; + public apiVersion: string; private readonly baseAddress: string; private readonly userTokenCallback: () => Promise; @@ -128,11 +132,17 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { */ public constructor( userAgents: (ProductHeaderValue | string)[] | ProductHeaderValue | string, + apiVersion: string, userTokenCallback?: () => Promise, tunnelServiceUri?: string, public readonly httpsAgent?: https.Agent, private readonly adapter?: AxiosAdapter ) { + if (apiVersions.indexOf(apiVersion) === -1) { + throw new TypeError(`Invalid API version: ${apiVersion}, must be one of ${apiVersions}`); + } + this.apiVersion = apiVersion; + if (!userAgents) { throw new TypeError('User agent must be provided.'); } @@ -202,15 +212,23 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { ): Promise { const queryParams = [clusterId ? null : 'global=true', domain ? `domain=${domain}` : null]; const query = queryParams.filter((p) => !!p).join('&'); - const results = (await this.sendRequest( + const results = (await this.sendRequest( 'GET', clusterId, tunnelsApiPath, query, options, ))!; - results.forEach(parseTunnelDates); - return results; + let tunnels = new Array(); + if (results.value) { + for (const region of results.value) { + if (region.value) { + tunnels = tunnels.concat(region.value); + } + } + } + tunnels.forEach(parseTunnelDates); + return tunnels; } public async getTunnel(tunnel: Tunnel, options?: TunnelRequestOptions): Promise { @@ -229,22 +247,49 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { public async createTunnel(tunnel: Tunnel, options?: TunnelRequestOptions): Promise { const tunnelId = tunnel.tunnelId; - if (tunnelId) { - throw new Error('An ID may not be specified when creating a tunnel.'); + const idGenerated = tunnelId === undefined || tunnelId === null || tunnelId === ''; + if (idGenerated) { + tunnel.tunnelId = IdGeneration.generateTunnelId(); + } + for (let i = 0;i<=3; i++){ + try { + const result = (await this.sendTunnelRequest( + 'PUT', + tunnel, + manageAccessTokenScope, + undefined, + undefined, + options, + this.convertTunnelForRequest(tunnel), + undefined, + true, + ))!; + preserveAccessTokens(tunnel, result); + parseTunnelDates(result); + return result; + } catch (error) { + if (idGenerated) { + // The tunnel ID was generated and there was a conflict. + // Try again with a new ID. + tunnel.tunnelId = IdGeneration.generateTunnelId(); + } else { + throw error; + } + } } - tunnel = this.convertTunnelForRequest(tunnel); - const result = (await this.sendRequest( - 'POST', - tunnel.clusterId, - tunnelsApiPath, + const result2 = (await this.sendTunnelRequest( + 'PUT', + tunnel, + manageAccessTokenScope, + undefined, undefined, options, - tunnel, + this.convertTunnelForRequest(tunnel), ))!; - preserveAccessTokens(tunnel, result); - parseTunnelDates(result); - return result; + preserveAccessTokens(tunnel, result2); + parseTunnelDates(result2); + return result2; } public async updateTunnel(tunnel: Tunnel, options?: TunnelRequestOptions): Promise { @@ -280,13 +325,16 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { endpoint: TunnelEndpoint, options?: TunnelRequestOptions, ): Promise { - const path = `${endpointsApiSubPath}/${endpoint.hostId}/${endpoint.connectionMode}`; + if (endpoint.id == null) { + throw new Error('Endpoint ID must be specified when updating an endpoint.'); + } + const path = `${endpointsApiSubPath}/${endpoint.id}`; const result = (await this.sendTunnelRequest( 'PUT', tunnel, hostAccessTokenScope, path, - undefined, + "connectionMode=" + endpoint.connectionMode, options, endpoint, ))!; @@ -307,14 +355,10 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { public async deleteTunnelEndpoints( tunnel: Tunnel, - hostId: string, - connectionMode?: TunnelConnectionMode, + id: string, options?: TunnelRequestOptions, ): Promise { - const path = - connectionMode == null - ? `${endpointsApiSubPath}/${hostId}` - : `${endpointsApiSubPath}/${hostId}/${connectionMode}`; + const path = `${endpointsApiSubPath}/${id}`; const result = await this.sendTunnelRequest( 'DELETE', tunnel, @@ -329,7 +373,7 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { if (result && tunnel.endpoints) { // Also delete the endpoint in the local tunnel object. tunnel.endpoints = tunnel.endpoints.filter( - (e) => e.hostId !== hostId || e.connectionMode !== connectionMode, + (e) => e.id !== id, ); } @@ -351,7 +395,7 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { tunnel: Tunnel, options?: TunnelRequestOptions, ): Promise { - const results = (await this.sendTunnelRequest( + const results = (await this.sendTunnelRequest( 'GET', tunnel, readAccessTokenScopes, @@ -359,8 +403,10 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { undefined, options, ))!; - results.forEach(parseTunnelPortDates); - return results; + if (results.value){ + results.value.forEach(parseTunnelPortDates); + } + return results.value; } public async getTunnelPort( @@ -387,11 +433,12 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { options?: TunnelRequestOptions, ): Promise { tunnelPort = this.convertTunnelPortForRequest(tunnel, tunnelPort); + const path = `${portsApiSubPath}/${tunnelPort.portNumber}`; const result = (await this.sendTunnelRequest( - 'POST', + 'PUT', tunnel, managePortsAccessTokenScopes, - portsApiSubPath, + path, undefined, options, tunnelPort, @@ -507,8 +554,9 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { options?: TunnelRequestOptions, body?: object, allowNotFound?: boolean, + isCreate: boolean = false ): Promise> { - const uri = await this.buildUriForTunnel(tunnel, path, query, options); + const uri = await this.buildUriForTunnel(tunnel, path, query, options, isCreate); const config = await this.getAxiosRequestConfig(tunnel, options, accessTokenScopes); const result = await this.request(method, uri, body, config, allowNotFound); return result; @@ -645,17 +693,15 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { path?: string, query?: string, options?: TunnelRequestOptions, + isCreate: boolean = false, ) { let tunnelPath = ''; - if (tunnel.clusterId && tunnel.tunnelId) { + if ((tunnel.clusterId || isCreate) && tunnel.tunnelId) { tunnelPath = `${tunnelsApiPath}/${tunnel.tunnelId}`; } else { - if (!tunnel.name) { - throw new Error( - 'Tunnel object must include either a name or tunnel ID and cluster ID.', - ); - } - tunnelPath = `${tunnelsApiPath}/${tunnel.name}`; + throw new Error( + 'Tunnel object must include a tunnel ID always and cluster ID for non creates.', + ); } if (options?.additionalQueryParameters) { @@ -735,10 +781,11 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { private convertTunnelForRequest(tunnel: Tunnel): Tunnel { const convertedTunnel: Tunnel = { + tunnelId: tunnel.tunnelId, name: tunnel.name, domain: tunnel.domain, description: tunnel.description, - tags: tunnel.tags, + labels: tunnel.labels, options: tunnel.options, customExpiration: tunnel.customExpiration, accessControl: !tunnel.accessControl @@ -765,7 +812,7 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { protocol: tunnelPort.protocol, isDefault: tunnelPort.isDefault, description: tunnelPort.description, - tags: tunnelPort.tags, + labels: tunnelPort.labels, sshUser: tunnelPort.sshUser, options: tunnelPort.options, accessControl: !tunnelPort.accessControl @@ -799,10 +846,10 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { queryOptions.forceRename = ['true']; } - if (options.tags) { - queryOptions.tags = options.tags; - if (options.requireAllTags) { - queryOptions.allTags = ['true']; + if (options.labels) { + queryOptions.labels = options.labels; + if (options.requireAllLabels) { + queryOptions.allLabels = ['true']; } } @@ -821,6 +868,7 @@ export class TunnelManagementHttpClient implements TunnelManagementClient { if (additionalQuery) { queryItems.push(additionalQuery); } + queryItems.push(`api-version=${this.apiVersion}`) const queryString = queryItems.join('&'); return queryString; diff --git a/ts/src/management/tunnelRequestOptions.ts b/ts/src/management/tunnelRequestOptions.ts index a91bd23c..17151fed 100644 --- a/ts/src/management/tunnelRequestOptions.ts +++ b/ts/src/management/tunnelRequestOptions.ts @@ -55,20 +55,20 @@ export interface TunnelRequestOptions { includeAccessControl?: boolean; /** - * Gets or sets an optional list of tags to filter the requested tunnels or ports. + * Gets or sets an optional list of labels to filter the requested tunnels or ports. * - * Requested tags are compared to the `Tunnel.tags` or `TunnelPort.tags` when calling + * Requested labels are compared to the `Tunnel.labels` or `TunnelPort.labels` when calling * `TunnelManagementClient.listTunnels` or `TunnelManagementClient.listTunnelPorts` - * respectively. By default, an item is included if ANY tag matches; set `requireAllTags` - * to match ALL tags instead. + * respectively. By default, an item is included if ANY tag matches; set `requireAllLabels` + * to match ALL labels instead. */ - tags?: string[]; + labels?: string[]; /* - * Gets or sets a flag that indicates whether listed items must match all tags - * specified in `tags`. If false, an item is included if any tag matches. + * Gets or sets a flag that indicates whether listed items must match all labels + * specified in `labels`. If false, an item is included if any tag matches. */ - requireAllTags?: boolean; + requireAllLabels?: boolean; /** * Gets or sets an optional list of token scopes that are requested when retrieving a diff --git a/ts/test/tunnels-test/connection.ts b/ts/test/tunnels-test/connection.ts index ce4d5fc1..6a34a591 100644 --- a/ts/test/tunnels-test/connection.ts +++ b/ts/test/tunnels-test/connection.ts @@ -58,6 +58,7 @@ async function connect(port: number, options: { [name: string]: string }) { async function startTunnelRelayConnection() { let tunnelManagementClient = new TunnelManagementHttpClient( userAgent, + "2023-09-27-preview", () => Promise.resolve('Bearer'), 'http://localhost:9900/'); const tunnel: Tunnel = { diff --git a/ts/test/tunnels-test/host.ts b/ts/test/tunnels-test/host.ts index 629d13d6..169e41a2 100644 --- a/ts/test/tunnels-test/host.ts +++ b/ts/test/tunnels-test/host.ts @@ -43,6 +43,7 @@ async function main() { async function startTunnelRelayHost() { let tunnelManagementClient = new TunnelManagementHttpClient( userAgent, + "2023-09-27-preview", () => Promise.resolve('Bearer'), 'http://localhost:9900/', //'https://ci.dev.tunnels.vsengsaas.visualstudio.com/', new https.Agent({ diff --git a/ts/test/tunnels-test/mocks/mockTunnelManagementClient.ts b/ts/test/tunnels-test/mocks/mockTunnelManagementClient.ts index 7e87c979..5185a2d7 100644 --- a/ts/test/tunnels-test/mocks/mockTunnelManagementClient.ts +++ b/ts/test/tunnels-test/mocks/mockTunnelManagementClient.ts @@ -25,14 +25,14 @@ export class MockTunnelManagementClient implements TunnelManagementClient { ): Promise { let tunnels = this.tunnels; - if (options?.tags) { - if (!options.requireAllTags) { + if (options?.labels) { + if (!options.requireAllLabels) { tunnels = this.tunnels.filter( - (tunnel) => tunnel.tags && options.tags!.some((t) => tunnel.tags!.includes(t)), + (tunnel) => tunnel.labels && options.labels!.some((t) => tunnel.labels!.includes(t)), ); } else { tunnels = this.tunnels.filter( - (tunnel) => tunnel.tags && options.tags!.every((t) => tunnel.tags!.includes(t)), + (tunnel) => tunnel.labels && options.labels!.every((t) => tunnel.labels!.includes(t)), ); } } @@ -144,12 +144,11 @@ export class MockTunnelManagementClient implements TunnelManagementClient { deleteTunnelEndpoints( tunnel: Tunnel, - hostId: string, - connectionMode?: TunnelConnectionMode, + id: string, options?: TunnelRequestOptions, ): Promise { - if (!hostId) { - throw new Error('Host ID cannot be empty'); + if (!id) { + throw new Error('ID cannot be empty'); } if (!tunnel.endpoints) { @@ -161,8 +160,7 @@ export class MockTunnelManagementClient implements TunnelManagementClient { let initialLength = tunnel.endpoints.length; tunnel.endpoints = tunnel.endpoints.filter( (ep) => - ep.hostId == hostId && - (connectionMode == null || ep.connectionMode == connectionMode), + ep.id == id, ); return Promise.resolve(tunnel.endpoints!.length < initialLength); } diff --git a/ts/test/tunnels-test/tunnelManagementTests.ts b/ts/test/tunnels-test/tunnelManagementTests.ts index 0f24683e..c6ea39bc 100644 --- a/ts/test/tunnels-test/tunnelManagementTests.ts +++ b/ts/test/tunnels-test/tunnelManagementTests.ts @@ -17,7 +17,7 @@ export class TunnelManagementTests { public constructor() { this.managementClient = new TunnelManagementHttpClient( - 'test/0.0.0', undefined, 'http://global.tunnels.test.api.visualstudio.com'); + 'test/0.0.0', "2023-09-27-preview", undefined, 'http://global.tunnels.test.api.visualstudio.com'); (this.managementClient).request = this.mockRequest.bind(this); } @@ -47,6 +47,7 @@ export class TunnelManagementTests { assert(this.lastRequest && this.lastRequest.uri); assert(this.lastRequest.uri.startsWith('http://' + testClusterId + '.')); assert(!this.lastRequest.uri.includes('global=true')); + assert(this.lastRequest.uri.includes('api-version=2023-09-27-preview')); } @test @@ -56,6 +57,7 @@ export class TunnelManagementTests { assert(this.lastRequest && this.lastRequest.uri); assert(this.lastRequest.uri.startsWith('http://global.')); assert(this.lastRequest.uri.includes('global=true')); + assert(this.lastRequest.uri.includes('api-version=2023-09-27-preview')); } @test @@ -65,6 +67,7 @@ export class TunnelManagementTests { assert(this.lastRequest && this.lastRequest.uri); assert(this.lastRequest.uri.startsWith('http://global.')); assert(this.lastRequest.uri.includes('includePorts=true&global=true')); + assert(this.lastRequest.uri.includes('api-version=2023-09-27-preview')); } @test @@ -73,7 +76,8 @@ export class TunnelManagementTests { await this.managementClient.listUserLimits(); assert(this.lastRequest && this.lastRequest.uri); assert.equal(this.lastRequest.method, 'GET'); - assert(this.lastRequest.uri.endsWith('/api/v1/userlimits')); + assert(this.lastRequest.uri.includes('/userlimits')); + assert(this.lastRequest.uri.includes('api-version=2023-09-27-preview')); } @test @@ -106,7 +110,7 @@ export class TunnelManagementTests { // Create a management client with a mock https agent and adapter const managementClient = new TunnelManagementHttpClient( - 'test/0.0.0', undefined, 'http://global.tunnels.test.api.visualstudio.com', httpsAgent, axiosAdapter); + 'test/0.0.0',"2023-09-27-preview", undefined, 'http://global.tunnels.test.api.visualstudio.com', httpsAgent, axiosAdapter); (managementClient).request = this.mockRequest.bind(this); this.nextResponse = [];