Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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