Skip to content

Commit

Permalink
Support standard Azure Function configuration options for managed ide…
Browse files Browse the repository at this point in the history
…ntity (#433)

* Support configuration of managed identity via Azure Functions configuration section, and replace most  Azure Storage SDK V11 references with the V12 equivalents.

* remove dependency on Microsoft.Azure.Storage.Blob
  • Loading branch information
sebastianburckhardt authored Jan 14, 2025
1 parent 4d7c0a0 commit d7e3dab
Show file tree
Hide file tree
Showing 34 changed files with 636 additions and 503 deletions.
23 changes: 11 additions & 12 deletions src/DurableTask.Netherite.AzureFunctions/BlobLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ namespace DurableTask.Netherite.AzureFunctions
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.Blob;
using Microsoft.Extensions.Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Specialized;

/// <summary>
/// A simple utility class for writing text to an append blob in Azure Storage, using a periodic timer.
Expand All @@ -20,7 +19,7 @@ namespace DurableTask.Netherite.AzureFunctions
class BlobLogger
{
readonly DateTime starttime;
readonly Task<CloudAppendBlob> blob;
readonly Task<AppendBlobClient> blob;
readonly object flushLock = new object();
readonly object lineLock = new object();
readonly ConcurrentQueue<MemoryStream> writebackQueue;
Expand All @@ -36,14 +35,14 @@ public BlobLogger(ConnectionInfo storageConnection, string hubName, string worke
this.starttime = DateTime.UtcNow;

this.blob = GetBlobAsync();
async Task<CloudAppendBlob> GetBlobAsync()
async Task<AppendBlobClient> GetBlobAsync()
{
CloudStorageAccount storageAccount = await storageConnection.GetAzureStorageV11AccountAsync();
CloudBlobClient client = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = client.GetContainerReference("logs");
container.CreateIfNotExists();
var blob = container.GetAppendBlobReference($"{hubName}.{workerId}.{this.starttime:o}.log");
await blob.CreateOrReplaceAsync();
BlobServiceClient blobServiceClient = storageConnection.GetAzureStorageV12BlobServiceClient(new Azure.Storage.Blobs.BlobClientOptions());
BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient("logs");
await containerClient.CreateIfNotExistsAsync();
var blob = containerClient.GetAppendBlobClient($"{hubName}.{workerId}.{this.starttime:o}.log");
await blob.DeleteIfExistsAsync();
await blob.CreateIfNotExistsAsync();
return blob;
}

Expand Down Expand Up @@ -95,7 +94,7 @@ public void Flush(object ignored)
{
// save to storage
toSave.Seek(0, SeekOrigin.Begin);
this.blob.GetAwaiter().GetResult().AppendFromStream(toSave);
this.blob.GetAwaiter().GetResult().AppendBlock(toSave);
toSave.Dispose();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
namespace DurableTask.Netherite.AzureFunctions
{
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Azure.Identity;
using Azure.Messaging.EventHubs;
using DurableTask.Netherite;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration;

/// <summary>
/// Resolves connections using an AzureComponentFactory and configuration sections.
/// </summary>
public class ConfigurationSectionBasedConnectionNameResolver : DurableTask.Netherite.ConnectionResolver
{
readonly AzureComponentFactory componentFactory;
readonly IConfiguration configuration;

public ConfigurationSectionBasedConnectionNameResolver(AzureComponentFactory componentFactory, IConfiguration configuration)
{
this.componentFactory = componentFactory;
this.configuration = configuration;
}

public override void ResolveLayerConfiguration(string connectionName, out StorageChoices storageChoice, out TransportChoices transportChoice)
{
if (TransportConnectionString.IsPseudoConnectionString(connectionName))
{
TransportConnectionString.Parse(connectionName, out storageChoice, out transportChoice);
}
else
{
IConfigurationSection connectionSection = this.configuration.GetWebJobsConnectionStringSection(connectionName);

if (TransportConnectionString.IsPseudoConnectionString(connectionSection.Value))
{
TransportConnectionString.Parse(connectionSection.Value, out storageChoice, out transportChoice);
}
else
{
// the default settings are Faster and EventHubs
storageChoice = StorageChoices.Faster;
transportChoice = TransportChoices.EventHubs;
}
}
}

public override ConnectionInfo ResolveConnectionInfo(string taskHub, string connectionName, ResourceType resourceType)
{
switch (resourceType)
{
case ResourceType.BlobStorage:
case ResourceType.TableStorage:
case ResourceType.PageBlobStorage:
return this.ResolveStorageAccountConnection(connectionName, resourceType);

case ResourceType.EventHubsNamespace:
return this.ResolveEventHubsConnection(connectionName);

default:
throw new NotSupportedException("unknown resource type");
}
}

public ConnectionInfo ResolveStorageAccountConnection(string connectionName, ResourceType resourceType)
{
IConfigurationSection connectionSection = this.configuration.GetWebJobsConnectionStringSection(connectionName);

if (!string.IsNullOrWhiteSpace(connectionSection.Value))
{
// It's a connection string
return ConnectionInfo.FromStorageConnectionString(connectionSection.Value, resourceType);
}

// parse some of the relevant fields in the configuration section
StorageAccountOptions accountOptions = connectionSection.Get<StorageAccountOptions>();

var tokenCredential = this.componentFactory.CreateTokenCredential(connectionSection);

return ConnectionInfo.FromTokenCredentialAndHost(tokenCredential, accountOptions.GetHost(resourceType), resourceType);
}

class StorageAccountOptions
{
public string AccountName { get; set; }

public Uri BlobServiceUri { get; set; }

public Uri TableServiceUri { get; set; }

public string GetHost(ResourceType resourceType)
{
switch (resourceType)
{
case ResourceType.BlobStorage:
case ResourceType.PageBlobStorage:
return this.BlobServiceUri?.Host ?? $"{this.AccountName}.blob.core.windows.net";

case ResourceType.TableStorage:
return this.TableServiceUri?.Host ?? $"{this.AccountName}.table.core.windows.net";

default:
throw new NotSupportedException("unknown resource type");
}
}
}

public ConnectionInfo ResolveEventHubsConnection(string connectionName)
{
IConfigurationSection connectionSection = this.configuration.GetWebJobsConnectionStringSection(connectionName);
if (!connectionSection.Exists())
{
// A common mistake is for developers to set their `connection` to a full connection string rather
// than an informational name. We handle this case specifically, to be helpful, and to avoid leaking secrets in error messages.
try
{
var properties = EventHubsConnectionStringProperties.Parse(connectionName);

// we parsed without exception, so it's a connection string.
// We now throw a descriptive and secret-free exception.

throw new NetheriteConfigurationException($"a full event hubs connection string was incorrectly used instead of a connection setting name");
}
catch (FormatException)
{
}

// Not found
throw new NetheriteConfigurationException($"EventHub account connection string with name '{connectionName}' does not exist in the settings. " +
$"Make sure that it is a defined App Setting.");
}

if (!string.IsNullOrWhiteSpace(connectionSection.Value))
{
// It's a connection string
return ConnectionInfo.FromEventHubsConnectionString(connectionSection.Value);
}

var fullyQualifiedNamespace = connectionSection["fullyQualifiedNamespace"];
if (string.IsNullOrWhiteSpace(fullyQualifiedNamespace))
{
// We could not find the necessary parameter
throw new NetheriteConfigurationException($"Configuration for event hubs connection should have a 'fullyQualifiedNamespace' property or be a string representing a connection string.");
}

var tokenCredential = this.componentFactory.CreateTokenCredential(connectionSection);

return ConnectionInfo.FromTokenCredentialAndHost(tokenCredential, fullyQualifiedNamespace, ResourceType.EventHubsNamespace);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ namespace DurableTask.Netherite.AzureFunctions
using System;
using System.IO;
using System.Threading;
using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.Blob;
using Microsoft.Extensions.Logging;

class LoggerFactoryWrapper : ILoggerFactory
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ NetheriteOrchestrationServiceSettings GetNetheriteOrchestrationServiceSettings(s

if (!string.IsNullOrEmpty(connectionName))
{
if (this.connectionResolver is NameResolverBasedConnectionNameResolver)
if (this.connectionResolver is ConfigurationSectionBasedConnectionNameResolver)
{
// the application does not define a custom connection resolver.
// We split the connection name into two connection names, one for storage and one for event hubs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public void Configure(IWebJobsBuilder builder)
// We use the UnambiguousNetheriteProviderFactory class instead of the base NetheriteProviderFactory class
// to avoid ambiguous constructor errors during DI. More details for this workaround can be found in the UnambiguousNetheriteProviderFactory class.
builder.Services.AddSingleton<IDurabilityProviderFactory, UnambiguousNetheriteProviderFactory>();
builder.Services.TryAddSingleton<ConnectionResolver, NameResolverBasedConnectionNameResolver>();
builder.Services.TryAddSingleton<ConnectionResolver, ConfigurationSectionBasedConnectionNameResolver>();
#else
builder.Services.AddSingleton<IDurabilityProviderFactory, NetheriteProviderPseudoFactory>();
#endif
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace DurableTask.Netherite.AzureFunctions
{
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.Configuration;

static class WebJobsConfigurationExtensions
{
const string WebJobsConfigurationSectionName = "AzureWebJobs";

public static IConfigurationSection GetWebJobsConnectionStringSection(this IConfiguration configuration, string connectionStringName)
{
// first try prefixing
string prefixedConnectionStringName = GetPrefixedConnectionStringName(connectionStringName);
IConfigurationSection section = GetConnectionStringOrSetting(configuration, prefixedConnectionStringName);

if (!section.Exists())
{
// next try a direct unprefixed lookup
section = GetConnectionStringOrSetting(configuration, connectionStringName);
}

return section;
}

public static string GetPrefixedConnectionStringName(string connectionStringName)
{
return WebJobsConfigurationSectionName + connectionStringName;
}

/// <summary>
/// Looks for a connection string by first checking the ConfigurationStrings section, and then the root.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="connectionName">The connection string key.</param>
/// <returns></returns>
public static IConfigurationSection GetConnectionStringOrSetting(this IConfiguration configuration, string connectionName)
{
var connectionStringSection = configuration?.GetSection("ConnectionStrings").GetSection(connectionName);

if (connectionStringSection.Exists())
{
return connectionStringSection;
}
return configuration?.GetSection(connectionName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace DurableTask.Netherite
using System.Collections.Generic;
using System.Data;
using System.Text;
using Microsoft.Azure.Storage;
using Microsoft.Extensions.Logging.Abstractions;


Expand Down
Loading

0 comments on commit d7e3dab

Please sign in to comment.