Skip to content

Commit

Permalink
Add CreateOrUpdate tunnels and ports and forced updates and creates w…
Browse files Browse the repository at this point in the history
…ith headers (#336)

* Revert "Revert "add create or update methods""

This reverts commit df18548.

* fix ts build issues

* fix cs builds

* go version

* Revert "Add forceCreate and forceUpdate query params (#335)"

This reverts commit 9d1f67c.

* use header for c# sdk

* update create and update

* add port headers

* add headers to ts

* java and go updates

* add precondition exception

* use const

* pr fixes

* update port setting

* fix build issues
  • Loading branch information
jfullerton44 authored Oct 27, 2023
1 parent 3597976 commit 0e65ace
Show file tree
Hide file tree
Showing 11 changed files with 664 additions and 74 deletions.
43 changes: 43 additions & 0 deletions cs/src/Management/ITunnelManagementClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,26 @@ Task<Tunnel> UpdateTunnelAsync(
TunnelRequestOptions? options = null,
CancellationToken cancellation = default);

/// <summary>
/// Updates a tunnel or creates it if it does not exist.
/// </summary>
/// <param name="tunnel">Tunnel object including all required properties.</param>
/// <param name="options">Request options.</param>
/// <param name="cancellation">Cancellation token.</param>
/// <returns>The created tunnel object.</returns>
/// <remarks>
/// Ports may be created at the same time as creating the tunnel by supplying
/// items in the <see cref="Tunnel.Ports" /> array.
/// </remarks>
/// <exception cref="UnauthorizedAccessException">The client access token was missing,
/// invalid, or unauthorized.</exception>
/// <exception cref="ArgumentException">A required property was missing, or a property
/// value was invalid.</exception>
Task<Tunnel> CreateOrUpdateTunnelAsync(
Tunnel tunnel,
TunnelRequestOptions? options = null,
CancellationToken cancellation = default);

/// <summary>
/// Deletes a tunnel.
/// </summary>
Expand Down Expand Up @@ -276,6 +296,29 @@ Task<TunnelPort> UpdateTunnelPortAsync(
TunnelRequestOptions? options = null,
CancellationToken cancellation = default);

/// <summary>
/// Updates a port or creates it if it does not exist.
/// </summary>
/// <param name="tunnel">Tunnel object including at least either a tunnel name
/// (globally unique, if configured) or tunnel ID and cluster ID.</param>
/// <param name="tunnelPort">Tunnel port object including all required properties.</param>
/// <param name="options">Request options.</param>
/// <param name="cancellation">Cancellation token.</param>
/// <returns>The created tunnel port object.</returns>
/// <exception cref="UnauthorizedAccessException">The client access token was missing,
/// invalid, or unauthorized.</exception>
/// <exception cref="InvalidOperationException">The tunnel ID or name was not found,
/// or a port with the specified port number already exists. (The inner
/// <see cref="HttpRequestException" /> status code may distinguish between these cases.)
/// </exception>
/// <exception cref="ArgumentException">A required property was missing, or a property
/// value was invalid.</exception>
Task<TunnelPort> CreateOrUpdateTunnelPortAsync(
Tunnel tunnel,
TunnelPort tunnelPort,
TunnelRequestOptions? options = null,
CancellationToken cancellation = default);

/// <summary>
/// Deletes a tunnel port.
/// </summary>
Expand Down
148 changes: 127 additions & 21 deletions cs/src/Management/TunnelManagementClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class TunnelManagementClient : ITunnelManagementClient
private const string TunnelAuthenticationScheme = "Tunnel";
private const string RequestIdHeaderName = "VsSaaS-Request-Id";
private const string CheckAvailableSubPath = ":checkNameAvailability";
private const int CreateNameRetries = 3;

private static readonly string[] ManageAccessTokenScope =
new[] { TunnelAccessScopes.Manage };
Expand Down Expand Up @@ -676,6 +677,7 @@ private string UserLimitsPath

case HttpStatusCode.NotFound:
case HttpStatusCode.Conflict:
case HttpStatusCode.PreconditionFailed:
case HttpStatusCode.TooManyRequests:
throw new InvalidOperationException(errorMessage, hrex);

Expand Down Expand Up @@ -924,14 +926,68 @@ public async Task<Tunnel> CreateTunnelAsync(
CancellationToken cancellation)
{
Requires.NotNull(tunnel, nameof(tunnel));
options ??= new TunnelRequestOptions();
options.AdditionalHeaders ??= new List<KeyValuePair<string,string>>();
options.AdditionalHeaders = options.AdditionalHeaders.Append(new KeyValuePair<string, string>("If-None-Match", "*"));
var tunnelId = tunnel.TunnelId;
var idGenerated = string.IsNullOrEmpty(tunnelId);
if (idGenerated)
{
tunnel.TunnelId = IdGeneration.GenerateTunnelId();
}
for (int retries = 0; retries <= CreateNameRetries; retries++)
{
try
{
var result = await this.SendTunnelRequestAsync<Tunnel, Tunnel>(
HttpMethod.Put,
tunnel,
ManageAccessTokenScope,
path: null,
query: GetApiQuery(),
options,
ConvertTunnelForRequest(tunnel),
cancellation,
true);
PreserveAccessTokens(tunnel, result);
return result!;
}
catch (UnauthorizedAccessException) when (idGenerated && retries < CreateNameRetries) // The tunnel ID was already taken.
{
tunnel.TunnelId = IdGeneration.GenerateTunnelId();
}
}

// This code is unreachable, but the compiler still requires it.
var result2 = await this.SendTunnelRequestAsync<Tunnel, Tunnel>(
HttpMethod.Put,
tunnel,
ManageAccessTokenScope,
path: null,
query: GetApiQuery(),
options,
ConvertTunnelForRequest(tunnel),
cancellation,
true);
PreserveAccessTokens(tunnel, result2);
return result2!;
}

/// <inheritdoc />
public async Task<Tunnel> CreateOrUpdateTunnelAsync(
Tunnel tunnel,
TunnelRequestOptions? options,
CancellationToken cancellation)
{
Requires.NotNull(tunnel, nameof(tunnel));

var tunnelId = tunnel.TunnelId;
var idGenerated = string.IsNullOrEmpty(tunnelId);
if (idGenerated)
{
tunnel.TunnelId = IdGeneration.GenerateTunnelId();
}
for (int retries = 0; retries <= 3; retries++)
for (int retries = 0; retries <= CreateNameRetries; retries++)
{
try
{
Expand All @@ -940,7 +996,7 @@ public async Task<Tunnel> CreateTunnelAsync(
tunnel,
ManageAccessTokenScope,
path: null,
query: GetApiQuery() + "&forceCreate=true",
query: GetApiQuery(),
options,
ConvertTunnelForRequest(tunnel),
cancellation,
Expand Down Expand Up @@ -975,12 +1031,15 @@ public async Task<Tunnel> UpdateTunnelAsync(
TunnelRequestOptions? options,
CancellationToken cancellation)
{
options ??= new TunnelRequestOptions();
options.AdditionalHeaders ??= new List<KeyValuePair<string, string>>();
options.AdditionalHeaders = options.AdditionalHeaders.Append(new KeyValuePair<string, string>("If-Match", "*"));
var result = await this.SendTunnelRequestAsync<Tunnel, Tunnel>(
HttpMethod.Put,
tunnel,
ManageAccessTokenScope,
path: null,
query: GetApiQuery() + "&forceUpdate=true",
query: GetApiQuery(),
options,
ConvertTunnelForRequest(tunnel),
cancellation);
Expand Down Expand Up @@ -1118,27 +1177,29 @@ public async Task<TunnelPort> CreateTunnelPortAsync(
{
Requires.NotNull(tunnelPort, nameof(tunnelPort));
var path = $"{PortsApiSubPath}/{tunnelPort.PortNumber}";
options ??= new TunnelRequestOptions();
options.AdditionalHeaders ??= new List<KeyValuePair<string,string>>();
options.AdditionalHeaders = options.AdditionalHeaders.Append(new KeyValuePair<string, string>("If-None-Match", "*"));

var result = (await this.SendTunnelRequestAsync<TunnelPort, TunnelPort>(
HttpMethod.Put,
tunnel,
ManagePortsAccessTokenScopes,
path,
query: GetApiQuery() + "&forceCreate=true",
query: GetApiQuery(),
options,
ConvertTunnelPortForRequest(tunnel, tunnelPort),
cancellation))!;
PreserveAccessTokens(tunnelPort, result);

if (tunnel.Ports != null)
{
// Also add the port to the local tunnel object.
tunnel.Ports = tunnel.Ports
.Where((p) => p.PortNumber != tunnelPort.PortNumber)
.Append(result)
.OrderBy((p) => p.PortNumber)
.ToArray();
}
tunnel.Ports ??= new TunnelPort[1];

// Also add the port to the local tunnel object.
tunnel.Ports = tunnel.Ports
.Where((p) => p.PortNumber != tunnelPort.PortNumber)
.Append(result)
.OrderBy((p) => p.PortNumber)
.ToArray();

return result;
}
Expand All @@ -1151,6 +1212,9 @@ public async Task<TunnelPort> UpdateTunnelPortAsync(
CancellationToken cancellation)
{
Requires.NotNull(tunnelPort, nameof(tunnelPort));
options ??= new TunnelRequestOptions();
options.AdditionalHeaders ??= new List<KeyValuePair<string,string>>();
options.AdditionalHeaders = options.AdditionalHeaders.Append(new KeyValuePair<string, string>("If-Match", "*"));

if (tunnelPort.ClusterId != null && tunnel.ClusterId != null &&
tunnelPort.ClusterId != tunnel.ClusterId)
Expand All @@ -1166,22 +1230,64 @@ public async Task<TunnelPort> UpdateTunnelPortAsync(
tunnel,
ManagePortsAccessTokenScopes,
path,
query: GetApiQuery() + "&forceUpdate=true",
query: GetApiQuery(),
options,
ConvertTunnelPortForRequest(tunnel, tunnelPort),
cancellation))!;
PreserveAccessTokens(tunnelPort, result);

if (tunnel.Ports != null)
tunnel.Ports ??= new TunnelPort[1];

// Also add the port to the local tunnel object.
tunnel.Ports = tunnel.Ports
.Where((p) => p.PortNumber != tunnelPort.PortNumber)
.Append(result)
.OrderBy((p) => p.PortNumber)
.ToArray();


return result;
}

/// <inheritdoc />
public async Task<TunnelPort> CreateOrUpdateTunnelPortAsync(
Tunnel tunnel,
TunnelPort tunnelPort,
TunnelRequestOptions? options,
CancellationToken cancellation)
{
Requires.NotNull(tunnelPort, nameof(tunnelPort));

if (tunnelPort.ClusterId != null && tunnel.ClusterId != null &&
tunnelPort.ClusterId != tunnel.ClusterId)
{
// Also update the port in the local tunnel object.
tunnel.Ports = tunnel.Ports
.Where((p) => p.PortNumber != tunnelPort.PortNumber)
.Append(result)
.OrderBy((p) => p.PortNumber)
.ToArray();
throw new ArgumentException(
"Tunnel port cluster ID is not consistent.", nameof(tunnelPort));
}

var portNumber = tunnelPort.PortNumber;
var path = $"{PortsApiSubPath}/{portNumber}";
var result = (await this.SendTunnelRequestAsync<TunnelPort, TunnelPort>(
HttpMethod.Put,
tunnel,
ManagePortsAccessTokenScopes,
path,
query: GetApiQuery(),
options,
ConvertTunnelPortForRequest(tunnel, tunnelPort),
cancellation))!;
PreserveAccessTokens(tunnelPort, result);

tunnel.Ports ??= new TunnelPort[1];

// Also add the port to the local tunnel object.
tunnel.Ports = tunnel.Ports
.Where((p) => p.PortNumber != tunnelPort.PortNumber)
.Append(result)
.OrderBy((p) => p.PortNumber)
.ToArray();


return result;
}

Expand Down
43 changes: 43 additions & 0 deletions cs/test/TunnelsSDK.Test/Mocks/MockTunnelManagementClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,47 @@ public Task<NamedRateStatus[]> ListUserLimitsAsync(CancellationToken cancellatio
{
throw new NotImplementedException();
}

public async Task<Tunnel> CreateOrUpdateTunnelAsync(Tunnel tunnel, TunnelRequestOptions options = null, CancellationToken cancellation = default)
{
if ((await GetTunnelAsync(tunnel, options, cancellation)) != null)
{
foreach (var t in Tunnels)
{
if (t.ClusterId == tunnel.ClusterId && t.TunnelId == tunnel.TunnelId)
{
if (tunnel.Name != null)
{
t.Name = tunnel.Name;
}

if (tunnel.Options != null)
{
t.Options = tunnel.Options;
}

if (tunnel.AccessControl != null)
{
t.AccessControl = tunnel.AccessControl;
}
}
}

IssueMockTokens(tunnel, options);

return tunnel;
}

tunnel.TunnelId = "tunnel" + (++this.idCounter);
tunnel.ClusterId = "localhost";
Tunnels.Add(tunnel);

IssueMockTokens(tunnel, options);
return tunnel;
}

public Task<TunnelPort> CreateOrUpdateTunnelPortAsync(Tunnel tunnel, TunnelPort tunnelPort, TunnelRequestOptions options = null, CancellationToken cancellation = default)
{
throw new NotImplementedException();
}
}
Loading

0 comments on commit 0e65ace

Please sign in to comment.