Skip to content

Commit

Permalink
Properly create secondary endpoint Uri for Azurite endpoints (#17246)
Browse files Browse the repository at this point in the history
fixes #17215
  • Loading branch information
christothes authored Dec 1, 2020
1 parent 14bd8d7 commit 05d8343
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 13 deletions.
3 changes: 3 additions & 0 deletions sdk/tables/Azure.Data.Tables/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 3.0.0-beta.4 (Unreleased)

### Fixed

- Properly create secondary endpoint Uri for Azurite endpoints

## 3.0.0-beta.3 (2020-11-12)

Expand Down
47 changes: 44 additions & 3 deletions sdk/tables/Azure.Data.Tables/src/TableConnectionString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ string settingOrDefault(string key)

var matchesAutomaticEndpointsSpec = settings.TryGetSegmentValue(TableConstants.ConnectionStrings.AccountNameSetting, out var accountName) &&
settings.TryGetSegmentValue(TableConstants.ConnectionStrings.AccountKeySetting, out var accountKey) &&
(settings.TryGetSegmentValue(TableConstants.ConnectionStrings.TableEndpointSetting, out accountName) ||
(settings.TryGetSegmentValue(TableConstants.ConnectionStrings.TableEndpointSetting, out var primary) ||
settings.TryGetSegmentValue(TableConstants.ConnectionStrings.EndpointSuffixSetting, out var endpointSuffix));

if (matchesAutomaticEndpointsSpec || matchesExplicitEndpointsSpec)
Expand All @@ -212,7 +212,10 @@ static bool IsValidEndpointPair(string primary, string secondary) =>
{
if (!string.IsNullOrWhiteSpace(primary))
{
return (CreateUri(primary, sasToken), CreateUri(secondary, sasToken));
Uri primaryUri = CreateUri(primary, sasToken);
Uri secondaryUri = CreateUri(secondary, sasToken) ?? GetSecondaryUriFromPrimary(primaryUri, accountName);

return (primaryUri, secondaryUri);
}
else if (matchesAutomaticEndpointsSpec && factory != null)
{
Expand Down Expand Up @@ -266,6 +269,44 @@ static Uri CreateUri(string endpoint, string sasToken)
return false;
}

internal static Uri GetSecondaryUriFromPrimary(Uri primaryUri, string accountName = null)
{
var secondaryUriBuilder = new UriBuilder(primaryUri);

if (!string.IsNullOrEmpty(accountName))
{
// We've been provided the accountName, so just insert the '-secondary' suffix after it
var indexOfAccountName = secondaryUriBuilder.Host.IndexOf(accountName, StringComparison.OrdinalIgnoreCase);
secondaryUriBuilder.Host = secondaryUriBuilder.Host.Insert(indexOfAccountName + accountName.Length, TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix);
return secondaryUriBuilder.Uri;
}

var indexOfDot = secondaryUriBuilder.Host.IndexOf('.');
if (indexOfDot >= 0 && Uri.CheckHostName(primaryUri.Host) == UriHostNameType.Dns)
{
// This is a dns name such as contoso.core.windows.net
// Insert the '-secondary' suffix after the first part of the host name
secondaryUriBuilder.Host = secondaryUriBuilder.Host.Insert(indexOfDot, TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix);
}
else if (primaryUri.IsLoopback)
{
// This is most likely Azurite, which looks like this: https://127.0.0.1:10002/contoso/
// Insert the '-secondary' suffix after the 2nd segment (the first segment is '/')
var segments = primaryUri.Segments;
var accountNameSegmentLength = segments[1].Length;
var insertIndex = segments[1].EndsWith("/", StringComparison.OrdinalIgnoreCase) ? accountNameSegmentLength - 1 : accountNameSegmentLength;
segments[1] = segments[1].Insert(insertIndex, TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix);
secondaryUriBuilder.Path = string.Join(string.Empty, segments);
}
else
{
// this is not a valid host name
return default;
}

return secondaryUriBuilder.Uri;
}

/// <summary>
/// Returns a <see cref="TableConnectionString"/> with development storage credentials using the specified proxy Uri.
/// </summary>
Expand Down Expand Up @@ -403,7 +444,7 @@ private static (Uri, Uri) ConstructUris(
}

/// <summary>
/// Gets the default queue endpoint using the specified protocol and account name.
/// Gets the default table endpoint using the specified protocol and account name.
/// </summary>
/// <param name="scheme">The protocol to use.</param>
/// <param name="accountName">The name of the storage account.</param>
Expand Down
18 changes: 9 additions & 9 deletions sdk/tables/Azure.Data.Tables/src/TableServiceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class TableServiceClient
private readonly OdataMetadataFormat _format = OdataMetadataFormat.ApplicationJsonOdataMinimalmetadata;
private readonly string _version;
internal readonly bool _isPremiumEndpoint;
private readonly QueryOptions _defaultQueryOptions= new QueryOptions() { Format = OdataMetadataFormat.ApplicationJsonOdataMinimalmetadata};
private readonly QueryOptions _defaultQueryOptions = new QueryOptions() { Format = OdataMetadataFormat.ApplicationJsonOdataMinimalmetadata };

/// <summary>
/// Initializes a new instance of the <see cref="TableServiceClient"/> using the specified <see cref="Uri" /> containing a shared access signature (SAS)
Expand Down Expand Up @@ -124,8 +124,8 @@ public TableServiceClient(string connectionString, TableClientOptions options =
TableConnectionString connString = TableConnectionString.Parse(connectionString);

options ??= new TableClientOptions();
var endpointString = connString.TableStorageUri.PrimaryUri.ToString();
var secondaryEndpoint = connString.TableStorageUri.PrimaryUri?.ToString() ?? endpointString.Insert(endpointString.IndexOf('.'), "-secondary");
var endpointString = connString.TableStorageUri.PrimaryUri.AbsoluteUri;
var secondaryEndpoint = connString.TableStorageUri.SecondaryUri?.AbsoluteUri;

TableSharedKeyPipelinePolicy policy = connString.Credentials switch
{
Expand All @@ -147,8 +147,8 @@ internal TableServiceClient(Uri endpoint, TableSharedKeyPipelinePolicy policy, T
Argument.AssertNotNull(endpoint, nameof(endpoint));

options ??= new TableClientOptions();
var endpointString = endpoint.ToString();
var secondaryEndpoint = endpointString.Insert(endpointString.IndexOf('.'), "-secondary");
var endpointString = endpoint.AbsoluteUri;
string secondaryEndpoint = TableConnectionString.GetSecondaryUriFromPrimary(endpoint)?.AbsoluteUri;
HttpPipeline pipeline = HttpPipelineBuilder.Build(options, policy);

_version = options.VersionString;
Expand Down Expand Up @@ -314,7 +314,7 @@ public virtual Response<TableItem> CreateTable(string tableName, CancellationTok
scope.Start();
try
{
var response = _tableOperations.Create(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken);
var response = _tableOperations.Create(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken);
return Response.FromValue(response.Value as TableItem, response.GetRawResponse());
}
catch (Exception ex)
Expand All @@ -337,7 +337,7 @@ public virtual async Task<Response<TableItem>> CreateTableAsync(string tableName
scope.Start();
try
{
var response = await _tableOperations.CreateAsync(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
var response = await _tableOperations.CreateAsync(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
return Response.FromValue(response.Value as TableItem, response.GetRawResponse());
}
catch (Exception ex)
Expand All @@ -360,7 +360,7 @@ public virtual Response<TableItem> CreateTableIfNotExists(string tableName, Canc
scope.Start();
try
{
var response = _tableOperations.Create(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken);
var response = _tableOperations.Create(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken);
return Response.FromValue(response.Value as TableItem, response.GetRawResponse());
}
catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.Conflict)
Expand All @@ -387,7 +387,7 @@ public virtual async Task<Response<TableItem>> CreateTableIfNotExistsAsync(strin
scope.Start();
try
{
var response = await _tableOperations.CreateAsync(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
var response = await _tableOperations.CreateAsync(new TableProperties() { TableName = tableName }, null, queryOptions: _defaultQueryOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
return Response.FromValue(response.Value as TableItem, response.GetRawResponse());
}
catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.Conflict)
Expand Down
34 changes: 33 additions & 1 deletion sdk/tables/Azure.Data.Tables/tests/TableConnectionStringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Azure.Data.Tables;
using NUnit.Framework;

namespace Azure.Tables.Tests
{
public class TableConnectionStringTests
{
private const string AccountName = "accountName";
private const string AccountName = "accountname";
private const string SasToken = "sv=2019-12-12&ss=t&srt=s&sp=rwdlacu&se=2020-08-28T23:45:30Z&st=2020-08-26T15:45:30Z&spr=https&sig=mySig";
private const string Secret = "Kg==";
private readonly TableSharedKeyCredential _expectedCred = new TableSharedKeyCredential(AccountName, Secret);
Expand Down Expand Up @@ -47,6 +49,7 @@ public void ParsesStorage(string connString)
Assert.That(tcs.Credentials, Is.Not.Null);
Assert.That(GetCredString(tcs.Credentials), Is.EqualTo(GetExpectedHash(_expectedCred)), "The Credentials should have matched.");
Assert.That(tcs.TableStorageUri.PrimaryUri, Is.EqualTo(new Uri($"https://{AccountName}.table.core.windows.net/")), "The PrimaryUri should have matched.");
Assert.That(tcs.TableStorageUri.SecondaryUri, Is.EqualTo(new Uri($"https://{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}.table.core.windows.net/")), "The SecondaryUri should have matched.");
}

public static IEnumerable<object[]> ValidCosmosConnStrings()
Expand All @@ -66,6 +69,7 @@ public void ParsesCosmos(string connString)
Assert.That(tcs.Credentials, Is.Not.Null);
Assert.That(GetCredString(tcs.Credentials), Is.EqualTo(GetExpectedHash(_expectedCred)), "The Credentials should have matched.");
Assert.That(tcs.TableStorageUri.PrimaryUri, Is.EqualTo(new Uri($"https://{AccountName}.table.cosmos.azure.com:443/")), "The PrimaryUri should have matched.");
Assert.That(tcs.TableStorageUri.SecondaryUri, Is.EqualTo(new Uri($"https://{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}.table.cosmos.azure.com:443/")), "The SecondaryUri should have matched.");
}

/// <summary>
Expand All @@ -80,6 +84,7 @@ public void ParsesSaS()
Assert.That(tcs.Credentials, Is.Not.Null);
Assert.That(GetCredString(tcs.Credentials), Is.EqualTo(SasToken), "The Credentials should have matched.");
Assert.That(tcs.TableStorageUri.PrimaryUri, Is.EqualTo(new Uri($"https://{AccountName}.table.core.windows.net/?{SasToken}")), "The PrimaryUri should have matched.");
Assert.That(tcs.TableStorageUri.SecondaryUri, Is.EqualTo(new Uri($"https://{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}.table.core.windows.net/?{SasToken}")), "The SecondaryUri should have matched.");
}

public static IEnumerable<object[]> InvalidConnStrings()
Expand All @@ -101,6 +106,33 @@ public void ParseFailsWithInvalidConnString(string connString)
Assert.That(TableConnectionString.TryParse(connString, out TableConnectionString tcs), Is.False, "Parsing should not have been successful");
}

[Test]
public void GetSecondaryUriFromPrimaryCosmos()
{
Uri secondaryEndpoint = TableConnectionString.GetSecondaryUriFromPrimary(new Uri($"https://{AccountName}.table.cosmos.azure.com:443/"));

Assert.That(secondaryEndpoint, Is.Not.Null.Or.Empty, "Secondary endpoint should not be null or empty");
Assert.That(secondaryEndpoint.AbsoluteUri, Is.EqualTo(new Uri($"https://{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}.table.cosmos.azure.com:443/").AbsoluteUri));
}

[Test]
public void GetSecondaryUriFromPrimaryStorage()
{
Uri secondaryEndpoint = TableConnectionString.GetSecondaryUriFromPrimary(new Uri($"https://{AccountName}.table.core.windows.net/"));

Assert.That(secondaryEndpoint, Is.Not.Null.Or.Empty, "Secondary endpoint should not be null or empty");
Assert.That(secondaryEndpoint.AbsoluteUri, Is.EqualTo(new Uri($"https://{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}.table.core.windows.net/")));
}

[Test]
public void GetSecondaryUriFromPrimaryAzurite()
{
Uri secondaryEndpoint = TableConnectionString.GetSecondaryUriFromPrimary(new Uri($"https://127.0.0.1:10002/{AccountName}/"));

Assert.That(secondaryEndpoint, Is.Not.Null.Or.Empty, "Secondary endpoint should not be null or empty");
Assert.That(secondaryEndpoint.AbsoluteUri, Is.EqualTo(new Uri($"https://127.0.0.1:10002/{AccountName}{TableConstants.ConnectionStrings.SecondaryLocationAccountSuffix}/")));
}

private string GetExpectedHash(TableSharedKeyCredential cred) => cred.ComputeHMACSHA256("message");

private string GetCredString(object credential) => credential switch
Expand Down

0 comments on commit 05d8343

Please sign in to comment.