Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support standard Azure Function configuration options for managed identity #433

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
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
Loading