From cdf3a41679f2906ea527a77c5ec36ccdf14de154 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Mon, 29 Aug 2022 18:37:14 +0700 Subject: [PATCH] Update Akka.Hosting extension methods to support DefaultAzureCredential (#237) * Update Akka.Hosting extension methods to support DefaultAzureCredential (cherry picked from commit b37c9dba61b678e2bb98bc4cad394696c4e098a8) * Make setup classes sealed * Fix missing HOCON settings * Revert const back to public Co-authored-by: Aaron Stannard --- .../AzurePersistenceExtensions.cs | 445 +++++++++++++++--- .../AzurePersistenceHostingSanityCheck.cs | 2 + .../AzureTableStorageJournalSettings.cs | 10 +- .../Journal/AzureTableStorageJournalSetup.cs | 12 +- .../AzureTableStorageReadJournalSetup.cs | 2 +- .../Snapshot/AzureBlobSnapshotSetup.cs | 28 +- .../AzureBlobSnapshotStoreSettings.cs | 10 +- 7 files changed, 423 insertions(+), 86 deletions(-) diff --git a/src/Akka.Persistence.Azure.Hosting/AzurePersistenceExtensions.cs b/src/Akka.Persistence.Azure.Hosting/AzurePersistenceExtensions.cs index 72590e9..821459e 100644 --- a/src/Akka.Persistence.Azure.Hosting/AzurePersistenceExtensions.cs +++ b/src/Akka.Persistence.Azure.Hosting/AzurePersistenceExtensions.cs @@ -6,10 +6,13 @@ using System; using Akka.Actor; -using Akka.Configuration; using Akka.Hosting; -using Akka.Persistence.Azure.Query; +using Akka.Persistence.Azure.Journal; +using Akka.Persistence.Azure.Snapshot; using Akka.Persistence.Hosting; +using Azure.Data.Tables; +using Azure.Identity; +using Azure.Storage.Blobs; namespace Akka.Persistence.Azure.Hosting { @@ -21,62 +24,315 @@ public static class AzurePersistenceExtensions public const string DefaultTableName = "AkkaPersistenceDefaultTable"; public const string DefaultBlobContainerName = "akka-persistence-default-container"; - private static string ToHocon(bool b) + /// + /// Add an AzureTableStorage journal as the default Akka.Persistence + /// implementations for a given . + /// + /// + /// The builder instance being configured. + /// + /// + /// A referencing the Azure Storage Table service. + /// This is likely to be similar to "https://{account_name}.table.core.windows.net". + /// + /// + /// The used to sign API requests. + /// + /// + /// Optional client options that define the transport pipeline policies for authentication, + /// retries, etc., that are applied to every request. + /// + /// + /// Automatically create the Table Storage table and Storage blob if no existing table is found + /// + /// + /// The Azure table we'll be connecting to. + /// + /// + /// A delegate that can be used to configure an instance + /// to set up event adapters. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithAzureTableJournal(this AkkaConfigurationBuilder builder, + Uri serviceUri, + DefaultAzureCredential defaultAzureCredential, + TableClientOptions tableClientOptions = null, + bool autoInitialize = true, + string tableName = DefaultTableName, + Action eventAdapterConfigurator = null) { - return b ? "on" : "off"; + if (serviceUri is null) + throw new ArgumentNullException(nameof(serviceUri)); + + if (defaultAzureCredential is null) + throw new ArgumentNullException(nameof(defaultAzureCredential)); + + var setup = new AzureTableStorageJournalSetup + { + ServiceUri = serviceUri, + DefaultAzureCredential = defaultAzureCredential, + TableClientOptions = tableClientOptions, + AutoInitialize = autoInitialize, + TableName = tableName + }; + return WithAzureTableJournal(builder, setup, eventAdapterConfigurator); } - + /// + /// Add an AzureTableStorage journal as the default Akka.Persistence + /// implementations for a given . + /// + /// + /// The builder instance being configured. + /// + /// + /// The connection string for connecting to Windows Azure table storage. + /// + /// + /// Automatically create the Table Storage table and Storage blob if no existing table is found + /// + /// + /// The Azure table we'll be connecting to. + /// + /// + /// A delegate that can be used to configure an instance + /// to set up event adapters. + /// + /// + /// The same instance originally passed in. + /// public static AkkaConfigurationBuilder WithAzureTableJournal(this AkkaConfigurationBuilder builder, - string connectionString, bool autoInitialize = true, string tableName = DefaultTableName, Action configurator = null) + string connectionString, + bool autoInitialize = true, + string tableName = DefaultTableName, + Action eventAdapterConfigurator = null) { - Config journalConfiguration = @$" - akka.persistence {{ - journal {{ - plugin = ""akka.persistence.journal.azure-table"" - azure-table {{ - class = ""Akka.Persistence.Azure.Journal.AzureTableStorageJournal, Akka.Persistence.Azure"" - connection-string = ""{connectionString}"" - # the name of the Windows Azure Table used to persist journal events - table-name = ""{tableName}"" - auto-initialize = {ToHocon(autoInitialize)} - }} - }} - }}"; - - var finalConfig = journalConfiguration; - builder.AddHocon(finalConfig, HoconAddMode.Prepend); + if (string.IsNullOrWhiteSpace(connectionString)) + throw new ArgumentNullException(nameof(connectionString)); + + var setup = new AzureTableStorageJournalSetup + { + ConnectionString = connectionString, + AutoInitialize = autoInitialize, + TableName = tableName + }; + return WithAzureTableJournal(builder, setup, eventAdapterConfigurator); + } + + /// + /// Add an AzureTableStorage journal as the default Akka.Persistence + /// implementations for a given . + /// + /// + /// The builder instance being configured. + /// + /// + /// A delegate that can be used to configure an instance + /// to set up the AzureTableStorage journal. + /// + /// + /// A delegate that can be used to configure an instance + /// to set up event adapters. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithAzureTableJournal( + this AkkaConfigurationBuilder builder, + Action configure, + Action eventAdapterConfigurator = null) + { + if (configure is null) + throw new ArgumentNullException(nameof(configure)); + + var setup = new AzureTableStorageJournalSetup(); + configure(setup); + return WithAzureTableJournal(builder, setup, eventAdapterConfigurator); + } + + /// + /// Add an AzureTableStorage journal as the default Akka.Persistence + /// implementations for a given . + /// + /// + /// The builder instance being configured. + /// + /// + /// An instance that will be used to set up + /// the AzureTableStorage journal. + /// + /// + /// A delegate that can be used to configure an instance + /// to set up event adapters. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithAzureTableJournal( + this AkkaConfigurationBuilder builder, + AzureTableStorageJournalSetup setup, + Action eventAdapterConfigurator = null) + { + if (setup is null) + throw new ArgumentNullException(nameof(setup)); + + builder.AddHocon("akka.persistence.journal.plugin = \"akka.persistence.journal.azure-table\"", HoconAddMode.Prepend); + builder.AddSetup(setup); // PUSH DEFAULT CONFIG TO END builder.AddHocon(AzurePersistence.DefaultConfig, HoconAddMode.Append); - if (configurator != null) // configure event adapters + if (eventAdapterConfigurator != null) // configure event adapters { - builder.WithJournal("azure-table", configurator); + builder.WithJournal("azure-table", eventAdapterConfigurator); } return builder; } - public static AkkaConfigurationBuilder WithAzureBlobsSnapshotStore(this AkkaConfigurationBuilder builder, - string connectionString, bool autoInitialize = true, string containerName = DefaultBlobContainerName) + /// + /// Add an AzureBlobStorage snapshot-store as the default Akka.Persistence + /// implementations for a given . + /// + /// + /// The builder instance being configured. + /// + /// + /// A referencing the Azure Storage blob service. + /// This is likely to be similar to "https://{account_name}.blob.core.windows.net". + /// + /// + /// The used to sign API requests. + /// + /// + /// Optional client options that define the transport pipeline policies for authentication, + /// retries, etc., that are applied to every request. + /// + /// + /// Automatically create the Table Storage table and Storage blob if no existing table is found + /// + /// + /// The table of the container we'll be using to serialize these blobs. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithAzureBlobsSnapshotStore( + this AkkaConfigurationBuilder builder, + Uri serviceUri, + DefaultAzureCredential defaultAzureCredential, + BlobClientOptions blobClientOptions, + bool autoInitialize = true, + string containerName = DefaultBlobContainerName) { - Config journalConfiguration = @$" - akka.persistence {{ - snapshot-store {{ - plugin = ""akka.persistence.snapshot-store.azure-blob-store"" - azure-blob-store {{ - class = ""Akka.Persistence.Azure.Snapshot.AzureBlobSnapshotStore, Akka.Persistence.Azure"" - connection-string = ""{connectionString}"" - # the name of the Windows Azure Table used to persist journal events - container-name = ""{containerName}"" - auto-initialize = {ToHocon(autoInitialize)} - }} - }} - }}"; - - var finalConfig = journalConfiguration; - builder.AddHocon(finalConfig, HoconAddMode.Prepend); + if (serviceUri is null) + throw new ArgumentNullException(nameof(serviceUri)); + + if (defaultAzureCredential is null) + throw new ArgumentNullException(nameof(defaultAzureCredential)); + + var setup = new AzureBlobSnapshotSetup + { + ServiceUri = serviceUri, + DefaultAzureCredential = defaultAzureCredential, + BlobClientOptions = blobClientOptions, + AutoInitialize = autoInitialize, + ContainerName = containerName + }; + + return WithAzureBlobsSnapshotStore(builder, setup); + } + + /// + /// Add an AzureBlobStorage snapshot-store as the default Akka.Persistence + /// implementations for a given . + /// + /// + /// The builder instance being configured. + /// + /// + /// The connection string for connecting to Windows Azure table storage. + /// + /// + /// Automatically create the Table Storage table and Storage blob if no existing table is found + /// + /// + /// The table of the container we'll be using to serialize these blobs. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithAzureBlobsSnapshotStore( + this AkkaConfigurationBuilder builder, + string connectionString, + bool autoInitialize = true, + string containerName = DefaultBlobContainerName) + { + if (string.IsNullOrWhiteSpace(connectionString)) + throw new ArgumentNullException(nameof(connectionString)); + + var setup = new AzureBlobSnapshotSetup + { + ConnectionString = connectionString, + AutoInitialize = autoInitialize, + ContainerName = containerName + }; + + return WithAzureBlobsSnapshotStore(builder, setup); + } + + /// + /// Add an AzureBlobStorage snapshot-store as the default Akka.Persistence + /// implementations for a given . + /// + /// + /// The builder instance being configured. + /// + /// + /// A delegate that can be used to configure an instance + /// to set up the AzureBlobStorage snapshot-store. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithAzureBlobsSnapshotStore( + this AkkaConfigurationBuilder builder, + Action configure) + { + if (configure is null) + throw new ArgumentNullException(nameof(configure)); + + var setup = new AzureBlobSnapshotSetup(); + configure(setup); + + return WithAzureBlobsSnapshotStore(builder, setup); + } + + /// + /// Add an AzureBlobStorage snapshot-store as the default Akka.Persistence + /// implementations for a given . + /// + /// + /// The builder instance being configured. + /// + /// + /// An instance that will be used to set up + /// the AzureBlobStorage snapshot-store. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithAzureBlobsSnapshotStore( + this AkkaConfigurationBuilder builder, + AzureBlobSnapshotSetup setup) + { + if (setup is null) + throw new ArgumentNullException(nameof(setup)); + + builder.AddHocon("akka.persistence.snapshot-store.plugin = \"akka.persistence.snapshot-store.azure-blob-store\"", HoconAddMode.Prepend); + builder.AddSetup(setup); // PUSH DEFAULT CONFIG TO END builder.AddHocon(AzurePersistence.DefaultConfig, HoconAddMode.Append); @@ -85,24 +341,103 @@ class = ""Akka.Persistence.Azure.Snapshot.AzureBlobSnapshotStore, Akka.Persisten } /// - /// Adds both AzureTableStorage journal and AzureBlobStorage snapshot-store as the default Akka.Persistence - /// implementations for a given . + /// Adds both AzureTableStorage journal and AzureBlobStorage snapshot-store as the default Akka.Persistence + /// implementations for a given . /// - /// - /// - /// - /// - /// - /// - /// - public static AkkaConfigurationBuilder WithAzurePersistence(this AkkaConfigurationBuilder builder, - string connectionString, bool autoInitialize = true, string containerName = DefaultBlobContainerName, - string tableName = DefaultTableName, Action configurator = null) + /// + /// The builder instance being configured. + /// + /// + /// The connection string for connecting to Windows Azure table storage. + /// + /// + /// Automatically create the Table Storage table and Storage blob if no existing table is found + /// + /// + /// The table of the container we'll be using to serialize these blobs. + /// + /// + /// The Azure table we'll be connecting to. + /// + /// + /// A delegate that can be used to configure an instance + /// to set up event adapters. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithAzurePersistence( + this AkkaConfigurationBuilder builder, + string connectionString, + bool autoInitialize = true, + string containerName = DefaultBlobContainerName, + string tableName = DefaultTableName, + Action eventAdapterConfigurator = null) { - builder.WithAzureTableJournal(connectionString, autoInitialize, tableName, configurator); + builder.WithAzureTableJournal(connectionString, autoInitialize, tableName, eventAdapterConfigurator); builder.WithAzureBlobsSnapshotStore(connectionString, autoInitialize, containerName); return builder; } + + /// + /// Adds both AzureTableStorage journal and AzureBlobStorage snapshot-store as the default Akka.Persistence + /// implementations for a given . + /// + /// + /// The builder instance being configured. + /// + /// + /// A referencing the Azure Storage blob service. + /// This is likely to be similar to "https://{account_name}.blob.core.windows.net". + /// + /// + /// A referencing the Azure Storage Table service. + /// This is likely to be similar to "https://{account_name}.table.core.windows.net". + /// + /// + /// The used to sign API requests. + /// + /// + /// Optional client options that define the transport pipeline policies for authentication, + /// retries, etc., that are applied to every request. + /// + /// + /// Optional client options that define the transport pipeline policies for authentication, + /// retries, etc., that are applied to every request. + /// + /// + /// Automatically create the Table Storage table and Storage blob if no existing table is found + /// + /// + /// The table of the container we'll be using to serialize these blobs. + /// + /// + /// The Azure table we'll be connecting to. + /// + /// + /// A delegate that can be used to configure an instance + /// to set up event adapters. + /// + /// + /// The same instance originally passed in. + /// + public static AkkaConfigurationBuilder WithAzurePersistence( + this AkkaConfigurationBuilder builder, + Uri blobStorageServiceUri, + Uri tableStorageServiceUri, + DefaultAzureCredential defaultAzureCredential, + BlobClientOptions blobClientOptions = null, + TableClientOptions tableClientOptions = null, + bool autoInitialize = true, + string containerName = DefaultBlobContainerName, + string tableName = DefaultTableName, + Action eventAdapterConfigurator = null) + { + builder.WithAzureTableJournal(tableStorageServiceUri, defaultAzureCredential, tableClientOptions, autoInitialize, tableName, eventAdapterConfigurator); + builder.WithAzureBlobsSnapshotStore(blobStorageServiceUri, defaultAzureCredential, blobClientOptions, autoInitialize, containerName); + + return builder; + } } } diff --git a/src/Akka.Persistence.Azure.Tests/Hosting/AzurePersistenceHostingSanityCheck.cs b/src/Akka.Persistence.Azure.Tests/Hosting/AzurePersistenceHostingSanityCheck.cs index 7ed2e8e..3c455d6 100644 --- a/src/Akka.Persistence.Azure.Tests/Hosting/AzurePersistenceHostingSanityCheck.cs +++ b/src/Akka.Persistence.Azure.Tests/Hosting/AzurePersistenceHostingSanityCheck.cs @@ -5,6 +5,7 @@ using Akka.Event; using Akka.Hosting; using Akka.Persistence.Azure.Hosting; +using Akka.Persistence.Azure.Tests.Helper; using Akka.TestKit.Xunit2.Internals; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; @@ -20,6 +21,7 @@ public class AzurePersistenceHostingSanityCheck public static async Task StartHost(Action testSetup) { var conn = Environment.GetEnvironmentVariable("AZURE_CONNECTION_STR") ?? "UseDevelopmentStorage=true"; + await DbUtils.CleanupCloudTable(conn); var host = new HostBuilder() .ConfigureServices(collection => { diff --git a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs index 20c67dd..4af08ef 100644 --- a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs +++ b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs @@ -116,19 +116,19 @@ public AzureTableStorageJournalSettings( public bool AutoInitialize { get; } /// - /// A referencing the blob service. - /// This is likely to be similar to "https://{account_name}.table.core.windows.net". + /// A referencing the Azure Table Storage service. + /// This is likely to be similar to "https://{account_name}.table.core.windows.net". /// public Uri ServiceUri { get; } /// - /// The used to sign API requests. + /// The used to sign API requests. /// public DefaultAzureCredential DefaultAzureCredential { get; } /// - /// Optional client options that define the transport pipeline policies for authentication, - /// retries, etc., that are applied to every request. + /// Optional client options that define the transport pipeline policies for authentication, + /// retries, etc., that are applied to every request. /// public TableClientOptions TableClientOptions { get; } diff --git a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSetup.cs b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSetup.cs index 760f4ff..2f85881 100644 --- a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSetup.cs +++ b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSetup.cs @@ -11,7 +11,7 @@ namespace Akka.Persistence.Azure.Journal { - public class AzureTableStorageJournalSetup : Setup + public sealed class AzureTableStorageJournalSetup : Setup { /// /// The connection string for connecting to Windows Azure table storage. @@ -51,19 +51,19 @@ public class AzureTableStorageJournalSetup : Setup public bool? AutoInitialize { get; set; } /// - /// A referencing the blob service. - /// This is likely to be similar to "https://{account_name}.table.core.windows.net". + /// A referencing the Azure Storage Table service. + /// This is likely to be similar to "https://{account_name}.table.core.windows.net". /// public Uri ServiceUri { get; set; } /// - /// The used to sign API requests. + /// The used to sign API requests. /// public DefaultAzureCredential DefaultAzureCredential { get; set; } /// - /// Optional client options that define the transport pipeline policies for authentication, - /// retries, etc., that are applied to every request. + /// Optional client options that define the transport pipeline policies for authentication, + /// retries, etc., that are applied to every request. /// public TableClientOptions TableClientOptions { get; set; } diff --git a/src/Akka.Persistence.Azure/Query/AzureTableStorageReadJournalSetup.cs b/src/Akka.Persistence.Azure/Query/AzureTableStorageReadJournalSetup.cs index 8096d97..2ab550e 100644 --- a/src/Akka.Persistence.Azure/Query/AzureTableStorageReadJournalSetup.cs +++ b/src/Akka.Persistence.Azure/Query/AzureTableStorageReadJournalSetup.cs @@ -9,7 +9,7 @@ namespace Akka.Persistence.Azure.Query { - public class AzureTableStorageReadJournalSetup: Setup + public sealed class AzureTableStorageReadJournalSetup: Setup { /// /// How many events to fetch in one query (replay) and keep buffered until they diff --git a/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotSetup.cs b/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotSetup.cs index f9f66ac..d9d18b3 100644 --- a/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotSetup.cs +++ b/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotSetup.cs @@ -13,24 +13,24 @@ namespace Akka.Persistence.Azure.Snapshot { /// - /// Setup class for . - /// Any populated properties will override its respective HOCON setting. + /// Setup class for . + /// Any populated properties will override its respective HOCON setting. /// - public class AzureBlobSnapshotSetup : Setup + public sealed class AzureBlobSnapshotSetup : Setup { /// - /// Create a new + /// Create a new /// /// - /// A referencing the blob service. - /// This is likely to be similar to "https://{account_name}.blob.core.windows.net". + /// A referencing the blob service. + /// This is likely to be similar to "https://{account_name}.blob.core.windows.net". /// /// - /// The used to sign requests. + /// The used to sign requests. /// /// - /// Optional client options that define the transport pipeline policies for authentication, - /// retries, etc., that are applied to every request. + /// Optional client options that define the transport pipeline policies for authentication, + /// retries, etc., that are applied to every request. /// /// A new instance public static AzureBlobSnapshotSetup Create( @@ -87,19 +87,19 @@ public static AzureBlobSnapshotSetup Create( public PublicAccessType? ContainerPublicAccessType { get; set; } /// - /// A referencing the blob service. - /// This is likely to be similar to "https://{account_name}.blob.core.windows.net". + /// A referencing the blob service. + /// This is likely to be similar to "https://{account_name}.blob.core.windows.net". /// public Uri ServiceUri { get; set; } /// - /// The used to sign API requests. + /// The used to sign API requests. /// public DefaultAzureCredential DefaultAzureCredential { get; set; } /// - /// Optional client options that define the transport pipeline policies for authentication, - /// retries, etc., that are applied to every request. + /// Optional client options that define the transport pipeline policies for authentication, + /// retries, etc., that are applied to every request. /// public BlobClientOptions BlobClientOptions { get; set; } diff --git a/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs b/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs index 50c0fcd..11d1c5f 100644 --- a/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs +++ b/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs @@ -129,19 +129,19 @@ public AzureBlobSnapshotStoreSettings( public PublicAccessType ContainerPublicAccessType { get; } /// - /// A referencing the blob service. - /// This is likely to be similar to "https://{account_name}.blob.core.windows.net". + /// A referencing the blob service. + /// This is likely to be similar to "https://{account_name}.blob.core.windows.net". /// public Uri ServiceUri { get; } /// - /// The used to sign API requests. + /// The used to sign API requests. /// public DefaultAzureCredential DefaultAzureCredential { get; } /// - /// Optional client options that define the transport pipeline policies for authentication, - /// retries, etc., that are applied to every request. + /// Optional client options that define the transport pipeline policies for authentication, + /// retries, etc., that are applied to every request. /// public BlobClientOptions BlobClientOptions { get; }