diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md index c1b61f43125d..ece0633d8fec 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 1.5.3-preview.1 (Unreleased) + +### Features Added +- Added secondaryLedgerUri parameter for failover support in ConfidentialLedgerClientOptions. + ## 1.4.1-beta.3 (Unreleased) ### Features Added diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj index 6c9e88527448..742088155259 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Azure.Security.ConfidentialLedger.csproj @@ -2,7 +2,7 @@ Client SDK for the Azure Confidential Ledger service Azure Confidential Ledger - 1.4.1-beta.3 + 1.5.3-preview.1 1.3.0 Azure ConfidentialLedger diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClient.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClient.cs index 3971677f1d25..5118210fbbf4 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClient.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClient.cs @@ -81,6 +81,29 @@ internal ConfidentialLedgerClient(Uri ledgerEndpoint, TokenCredential credential new ConfidentialLedgerResponseClassifier()); _ledgerEndpoint = ledgerEndpoint; _apiVersion = actualOptions.Version; + + // Set up secondary endpoint if configured + _secondaryLedgerEndpoint = actualOptions.SecondaryLedgerEndpoint; + if (_secondaryLedgerEndpoint != null) + { + // Get certificate for secondary endpoint if needed + X509Certificate2 secondaryServiceCert = identityServiceCert ?? GetIdentityServerTlsCert(_secondaryLedgerEndpoint, certificateClientOptions ?? new ConfidentialLedgerCertificateClientOptions(), ledgerOptions: ledgerOptions).Cert; + + var secondaryTransportOptions = GetIdentityServerTlsCertAndTrust(secondaryServiceCert, ledgerOptions?.VerifyConnection ?? true); + if (clientCertificate != null) + { + secondaryTransportOptions.ClientCertificates.Add(clientCertificate); + } + + _secondaryPipeline = HttpPipelineBuilder.Build( + actualOptions, + Array.Empty(), + _tokenCredential == null ? + Array.Empty() : + new HttpPipelinePolicy[] { new BearerTokenAuthenticationPolicy(_tokenCredential, AuthorizationScopes) }, + secondaryTransportOptions, + new ConfidentialLedgerResponseClassifier()); + } } internal class ConfidentialLedgerResponseClassifier : ResponseClassifier @@ -154,7 +177,7 @@ public virtual Operation PostLedgerEntry( /// } /// /// - /// if the method should wait to return until the long-running operation has completed on the service; if it should return after starting the operation. For more information on long-running operations, please see Azure.Core Long-Running Operation samples. + /// if the method should wait to return until the long-running operation hascompleted on the service; if it should return after starting the operation. For more information on long-running operations, please see Azure.Core Long-Running Operation samples. /// The content to send as the body of the request. /// The collection id. /// The tags. diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClientOptions.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClientOptions.cs index 14c08ceb10e0..e0962fd663ef 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClientOptions.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/ConfidentialLedgerClientOptions.cs @@ -25,6 +25,12 @@ public partial class ConfidentialLedgerClientOptions : ClientOptions /// public bool VerifyConnection { get; set; } + /// + /// The secondary ledger endpoint URL for failover scenarios. + /// + /// + public Uri SecondaryLedgerEndpoint { get; set; } + /// The version of the service to use. public enum ServiceVersion { diff --git a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs index 1cc7f7def116..2487e64ce957 100644 --- a/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs +++ b/sdk/confidentialledger/Azure.Security.ConfidentialLedger/src/Generated/ConfidentialLedgerClient.cs @@ -23,6 +23,10 @@ public partial class ConfidentialLedgerClient private readonly Uri _ledgerEndpoint; private readonly string _apiVersion; + // Secondary endpoint support for failover + private readonly Uri _secondaryLedgerEndpoint; + private readonly HttpPipeline _secondaryPipeline; + /// The ClientDiagnostics is used to provide tracing support for the client library. internal ClientDiagnostics ClientDiagnostics { get; } @@ -252,8 +256,21 @@ public virtual async Task GetLedgerEntryAsync(string transactionId, st scope.Start(); try { - using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context); - return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + try + { + Console.WriteLine("trying primary endpoint"); + using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + } + catch (Exception ex) when ((ex is RequestFailedException || ex is System.Collections.Generic.KeyNotFoundException) && _secondaryLedgerEndpoint != null) + { + Console.WriteLine("primary endpoint failed: " + ex.Message); + Console.WriteLine("trying secondary endpoint"); + // If primary fails and secondary is available, try secondary + using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context, useSecondaryEndpoint: true); + var pipeline = _secondaryPipeline ?? _pipeline; + return await pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + } } catch (Exception e) { @@ -288,8 +305,20 @@ public virtual Response GetLedgerEntry(string transactionId, string collectionId scope.Start(); try { - using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context); - return _pipeline.ProcessMessage(message, context); + try + { + Console.WriteLine("trying primary endpoint"); + using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context); + return _pipeline.ProcessMessage(message, context); + } + catch (Exception ex) when ((ex is RequestFailedException || ex is System.Collections.Generic.KeyNotFoundException) && _secondaryLedgerEndpoint != null) + { + Console.WriteLine("trying secondary endpoint"); + // If primary fails and secondary is available, try secondary + using HttpMessage message = CreateGetLedgerEntryRequest(transactionId, collectionId, context, useSecondaryEndpoint: true); + var pipeline = _secondaryPipeline ?? _pipeline; + return pipeline.ProcessMessage(message, context); + } } catch (Exception e) { @@ -459,8 +488,20 @@ public virtual async Task GetCurrentLedgerEntryAsync(string collection scope.Start(); try { - using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context); - return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + try + { + Console.WriteLine("trying primary endpoint"); + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context); + return await _pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + } + catch (Exception ex) when ((ex is RequestFailedException || ex is System.Collections.Generic.KeyNotFoundException) && _secondaryLedgerEndpoint != null) + { + Console.WriteLine("trying secondary endpoint"); + // If primary fails and secondary is available, try secondary + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context, useSecondaryEndpoint: true); + var pipeline = _secondaryPipeline ?? _pipeline; + return await pipeline.ProcessMessageAsync(message, context).ConfigureAwait(false); + } } catch (Exception e) { @@ -490,8 +531,20 @@ public virtual Response GetCurrentLedgerEntry(string collectionId = null, Reques scope.Start(); try { - using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context); - return _pipeline.ProcessMessage(message, context); + try + { + Console.WriteLine("trying primary endpoint"); + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context); + return _pipeline.ProcessMessage(message, context); + } + catch (Exception ex) when ((ex is RequestFailedException || ex is System.Collections.Generic.KeyNotFoundException) && _secondaryLedgerEndpoint != null) + { + Console.WriteLine("trying secondary endpoint"); + // If primary fails and secondary is available, try secondary + using HttpMessage message = CreateGetCurrentLedgerEntryRequest(collectionId, context, useSecondaryEndpoint: true); + var pipeline = _secondaryPipeline ?? _pipeline; + return pipeline.ProcessMessage(message, context); + } } catch (Exception e) { @@ -2168,13 +2221,16 @@ internal HttpMessage CreateCreateLedgerEntryRequest(RequestContent content, stri return message; } - internal HttpMessage CreateGetLedgerEntryRequest(string transactionId, string collectionId, RequestContext context) + internal HttpMessage CreateGetLedgerEntryRequest(string transactionId, string collectionId, RequestContext context, bool useSecondaryEndpoint = false) { - var message = _pipeline.CreateMessage(context, ResponseClassifier200); + var endpoint = useSecondaryEndpoint && _secondaryLedgerEndpoint != null ? _secondaryLedgerEndpoint : _ledgerEndpoint; + var pipeline = useSecondaryEndpoint && _secondaryPipeline != null ? _secondaryPipeline : _pipeline; + + var message = pipeline.CreateMessage(context, ResponseClassifier200); var request = message.Request; request.Method = RequestMethod.Get; var uri = new RawRequestUriBuilder(); - uri.Reset(_ledgerEndpoint); + uri.Reset(endpoint); uri.AppendPath("/app/transactions/", false); uri.AppendPath(transactionId, true); uri.AppendQuery("api-version", _apiVersion, true); @@ -2219,13 +2275,16 @@ internal HttpMessage CreateGetTransactionStatusRequest(string transactionId, Req return message; } - internal HttpMessage CreateGetCurrentLedgerEntryRequest(string collectionId, RequestContext context) + internal HttpMessage CreateGetCurrentLedgerEntryRequest(string collectionId, RequestContext context, bool useSecondaryEndpoint = false) { - var message = _pipeline.CreateMessage(context, ResponseClassifier200); + var endpoint = useSecondaryEndpoint && _secondaryLedgerEndpoint != null ? _secondaryLedgerEndpoint : _ledgerEndpoint; + var pipeline = useSecondaryEndpoint && _secondaryPipeline != null ? _secondaryPipeline : _pipeline; + + var message = pipeline.CreateMessage(context, ResponseClassifier200); var request = message.Request; request.Method = RequestMethod.Get; var uri = new RawRequestUriBuilder(); - uri.Reset(_ledgerEndpoint); + uri.Reset(endpoint); uri.AppendPath("/app/transactions/current", false); uri.AppendQuery("api-version", _apiVersion, true); if (collectionId != null)