Skip to content

Commit cbf9ad1

Browse files
authored
Adding support for "secretless" storage (updating host to Storage SDK 12.x)
1 parent 624b37d commit cbf9ad1

File tree

39 files changed

+12490
-9715
lines changed

39 files changed

+12490
-9715
lines changed

azure-pipelines.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ jobs:
349349
**\WebJobs.Script.Tests.Integration.csproj
350350
env:
351351
AzureWebJobsStorage: $(Storage)
352+
AzureWebJobsSecondaryStorage: $(SecondaryStorage)
352353
ConnectionStrings__CosmosDB: $(CosmosDB)
353354
AzureWebJobsEventHubSender: $(EventHub)
354355
AzureWebJobsEventHubReceiver: $(EventHub)
@@ -405,11 +406,13 @@ jobs:
405406
targetType: 'inline'
406407
script: |
407408
Write-Host "##vso[task.setvariable variable=AzureWebJobsStorage]$env:AzureWebJobsStorageSecretMap"
409+
Write-Host "##vso[task.setvariable variable=AzureWebJobsSeconaryStorage]$env:AzureWebJobsSecondaryStorageSecretMap"
408410
Write-Host "##vso[task.setvariable variable=ConnectionStrings__CosmosDB]$env:CosmosDbSecretMap"
409411
Write-Host "##vso[task.setvariable variable=AzureWebJobsEventHubSender]$env:AzureWebJobsEventHubSenderSecretMap"
410412
Write-Host "##vso[task.setvariable variable=AzureWebJobsEventHubReceiver]$env:AzureWebJobsEventHubReceiverSecretMap"
411413
env:
412414
AzureWebJobsStorageSecretMap: $(Storage)
415+
AzureWebJobsSecondaryStorageSecretMap: $(SecondaryStorage)
413416
CosmosDbSecretMap: $(CosmosDb)
414417
AzureWebJobsEventHubSenderSecretMap: $(EventHub)
415418
AzureWebJobsEventHubReceiverSecretMap: $(EventHub)

release_notes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@
1111

1212
**Release sprint:** Sprint 100
1313
[ [bugs](https://github.com/Azure/azure-functions-host/issues?q=is%3Aissue+milestone%3A%22Functions+Sprint+100%22+label%3Abug+is%3Aclosed) | [features](https://github.com/Azure/azure-functions-host/issues?q=is%3Aissue+milestone%3A%22Functions+Sprint+100%22+label%3Afeature+is%3Aclosed) ]
14-
- Update App Service Authentication/Authorization on Linux Consumption from 1.4.0 to 1.4.5. Release notes for this feature captured at https://github.com/Azure/app-service-announcements/issues. (#7205)
14+
- Update App Service Authentication/Authorization on Linux Consumption from 1.4.0 to 1.4.5. Release notes for this feature captured at https://github.com/Azure/app-service-announcements/issues. (#7205)
15+
- Add v12 Storage-dependent implementations to Functions Host to support Managed Identity integration

src/WebJobs.Script.WebHost/BreakingChangeAnalysis/BlobChangeAnalysisStateProvider.cs

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.IO;
56
using System.Threading;
67
using System.Threading.Tasks;
8+
using Azure;
9+
using Azure.Storage.Blobs;
710
using Microsoft.Azure.WebJobs.Host.Executors;
811
using Microsoft.Extensions.Configuration;
912
using Microsoft.Extensions.Logging;
10-
using Microsoft.WindowsAzure.Storage;
11-
using Microsoft.WindowsAzure.Storage.Blob;
1213

1314
namespace Microsoft.Azure.WebJobs.Script.ChangeAnalysis
1415
{
@@ -19,63 +20,84 @@ internal sealed class BlobChangeAnalysisStateProvider : IChangeAnalysisStateProv
1920
private readonly ILogger _logger;
2021
private readonly IConfiguration _configuration;
2122
private readonly IHostIdProvider _hostIdProvider;
23+
private readonly IAzureStorageProvider _azureStorageProvider;
2224

23-
public BlobChangeAnalysisStateProvider(IConfiguration configuration, IHostIdProvider hostIdProvider, ILogger<BlobChangeAnalysisStateProvider> logger)
25+
public BlobChangeAnalysisStateProvider(IConfiguration configuration, IHostIdProvider hostIdProvider, ILogger<BlobChangeAnalysisStateProvider> logger, IAzureStorageProvider azureStorageProvider)
2426
{
2527
_configuration = configuration;
2628
_hostIdProvider = hostIdProvider;
2729
_logger = logger;
30+
_azureStorageProvider = azureStorageProvider;
2831
}
2932

3033
public async Task<ChangeAnalysisState> GetCurrentAsync(CancellationToken cancellationToken)
3134
{
32-
string storageConnectionString = _configuration.GetWebJobsConnectionString(ConnectionStringNames.Storage);
33-
34-
if (string.IsNullOrEmpty(storageConnectionString) ||
35-
!CloudStorageAccount.TryParse(storageConnectionString, out CloudStorageAccount account))
35+
if (!_azureStorageProvider.TryGetBlobServiceClientFromConnection(out BlobServiceClient blobServiceClient, ConnectionStringNames.Storage))
3636
{
3737
throw new InvalidOperationException($"The {nameof(BlobChangeAnalysisStateProvider)} requires the default storage account '{ConnectionStringNames.Storage}', which is not defined.");
3838
}
3939

4040
string hostId = await _hostIdProvider.GetHostIdAsync(cancellationToken);
4141
string analysisBlobPath = $"changeanalysis/{hostId}/sentinel";
4242

43-
CloudBlobClient blobClient = account.CreateCloudBlobClient();
44-
var blobContainer = blobClient.GetContainerReference(ScriptConstants.AzureWebJobsHostsContainerName);
45-
4643
DateTimeOffset lastAnalysisTime = DateTimeOffset.MinValue;
47-
48-
var reference = blobContainer.GetBlockBlobReference(analysisBlobPath);
49-
bool blobExists = await reference.ExistsAsync(null, null, cancellationToken);
50-
if (blobExists)
44+
BlobClient blobClient = default;
45+
try
5146
{
52-
reference.Metadata.TryGetValue(AnalysisTimestampMetadataName, out string lastAnalysisMetadata);
47+
var blobContainerClient = blobServiceClient.GetBlobContainerClient(ScriptConstants.AzureWebJobsHostsContainerName);
48+
blobClient = blobContainerClient.GetBlobClient(analysisBlobPath);
49+
if (await blobClient.ExistsAsync(cancellationToken: cancellationToken))
50+
{
51+
var blobPropertiesResponse = await blobClient.GetPropertiesAsync(cancellationToken: cancellationToken);
52+
blobPropertiesResponse.Value.Metadata.TryGetValue(AnalysisTimestampMetadataName, out string lastAnalysisMetadata);
5353

54-
_logger.LogInformation("Last analysis flag value '{flag}'.", lastAnalysisMetadata ?? "(null)");
54+
_logger.LogInformation("Last analysis flag value '{flag}'.", lastAnalysisMetadata ?? "(null)");
5555

56-
if (!DateTimeOffset.TryParse(lastAnalysisMetadata, out lastAnalysisTime))
57-
{
58-
_logger.LogInformation("Unable to parse last analysis timestamp flag. Default (MinValue) will be used.");
56+
if (!DateTimeOffset.TryParse(lastAnalysisMetadata, out lastAnalysisTime))
57+
{
58+
_logger.LogInformation("Unable to parse last analysis timestamp flag. Default (MinValue) will be used.");
59+
}
5960
}
6061
}
62+
catch (RequestFailedException ex)
63+
{
64+
_logger.LogError(ex, "Unable to get current change analysis state.");
65+
}
6166

62-
return new ChangeAnalysisState(lastAnalysisTime, reference);
67+
return new ChangeAnalysisState(lastAnalysisTime, blobClient);
6368
}
6469

6570
public async Task SetTimestampAsync(DateTimeOffset timestamp, object handle, CancellationToken cancellationToken)
6671
{
67-
if (handle is CloudBlockBlob blob)
72+
if (handle is BlobClient blobClient)
6873
{
69-
if (!await blob.ExistsAsync())
74+
try
7075
{
71-
await blob.UploadTextAsync(string.Empty);
72-
}
76+
if (!await blobClient.ExistsAsync())
77+
{
78+
using (Stream stream = new MemoryStream())
79+
{
80+
await blobClient.UploadAsync(stream, cancellationToken: cancellationToken);
81+
}
82+
}
7383

74-
string timestampValue = timestamp.ToString("O");
75-
blob.Metadata[AnalysisTimestampMetadataName] = timestampValue;
76-
await blob.SetMetadataAsync(null, null, null, cancellationToken);
84+
string timestampValue = timestamp.ToString("O");
7785

78-
_logger.LogInformation("Analysis blob metadata updated with analysis timestamp '{timestamp}'.", timestampValue);
86+
var blobPropertiesResponse = await blobClient.GetPropertiesAsync(cancellationToken: cancellationToken);
87+
blobPropertiesResponse.Value.Metadata[AnalysisTimestampMetadataName] = timestampValue;
88+
89+
await blobClient.SetMetadataAsync(blobPropertiesResponse.Value.Metadata, cancellationToken: cancellationToken);
90+
91+
_logger.LogInformation("Analysis blob metadata updated with analysis timestamp '{timestamp}'.", timestampValue);
92+
}
93+
catch (RequestFailedException ex)
94+
{
95+
_logger.LogError(ex, "Unable to set change analysis state metadata.");
96+
}
97+
}
98+
else
99+
{
100+
_logger.LogError("Analysis blob SetTimestampAsync was given a null BlobClient");
79101
}
80102
}
81103
}

src/WebJobs.Script.WebHost/Management/FunctionsSyncManager.cs

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
using System.Text.RegularExpressions;
1212
using System.Threading;
1313
using System.Threading.Tasks;
14-
using Microsoft.Azure.Storage;
15-
using Microsoft.Azure.Storage.Blob;
14+
using Azure.Storage.Blobs;
1615
using Microsoft.Azure.WebJobs.Host.Executors;
1716
using Microsoft.Azure.WebJobs.Script.Description;
1817
using Microsoft.Azure.WebJobs.Script.Models;
@@ -60,10 +59,11 @@ public class FunctionsSyncManager : IFunctionsSyncManager, IDisposable
6059
private readonly HostNameProvider _hostNameProvider;
6160
private readonly IFunctionMetadataManager _functionMetadataManager;
6261
private readonly SemaphoreSlim _syncSemaphore = new SemaphoreSlim(1, 1);
62+
private readonly IAzureStorageProvider _azureStorageProvider;
6363

64-
private CloudBlockBlob _hashBlob;
64+
private BlobClient _hashBlobClient;
6565

66-
public FunctionsSyncManager(IConfiguration configuration, IHostIdProvider hostIdProvider, IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, ILogger<FunctionsSyncManager> logger, HttpClient httpClient, ISecretManagerProvider secretManagerProvider, IScriptWebHostEnvironment webHostEnvironment, IEnvironment environment, HostNameProvider hostNameProvider, IFunctionMetadataManager functionMetadataManager)
66+
public FunctionsSyncManager(IConfiguration configuration, IHostIdProvider hostIdProvider, IOptionsMonitor<ScriptApplicationHostOptions> applicationHostOptions, ILogger<FunctionsSyncManager> logger, HttpClient httpClient, ISecretManagerProvider secretManagerProvider, IScriptWebHostEnvironment webHostEnvironment, IEnvironment environment, HostNameProvider hostNameProvider, IFunctionMetadataManager functionMetadataManager, IAzureStorageProvider azureStorageProvider)
6767
{
6868
_applicationHostOptions = applicationHostOptions;
6969
_logger = logger;
@@ -75,6 +75,7 @@ public FunctionsSyncManager(IConfiguration configuration, IHostIdProvider hostId
7575
_environment = environment;
7676
_hostNameProvider = hostNameProvider;
7777
_functionMetadataManager = functionMetadataManager;
78+
_azureStorageProvider = azureStorageProvider;
7879
}
7980

8081
internal bool ArmCacheEnabled
@@ -104,8 +105,8 @@ public async Task<SyncTriggersResult> TrySyncTriggersAsync(bool isBackgroundSync
104105
{
105106
await _syncSemaphore.WaitAsync();
106107

107-
var hashBlob = await GetHashBlobAsync();
108-
if (isBackgroundSync && hashBlob == null)
108+
var hashBlobClient = await GetHashBlobAsync();
109+
if (isBackgroundSync && hashBlobClient == null)
109110
{
110111
// short circuit before doing any work in background sync
111112
// cases where we need to check/update hash but don't have
@@ -128,7 +129,7 @@ public async Task<SyncTriggersResult> TrySyncTriggersAsync(bool isBackgroundSync
128129
string newHash = null;
129130
if (isBackgroundSync)
130131
{
131-
newHash = await CheckHashAsync(hashBlob, payload.Content);
132+
newHash = await CheckHashAsync(hashBlobClient, payload.Content);
132133
shouldSyncTriggers = newHash != null;
133134
}
134135

@@ -137,7 +138,7 @@ public async Task<SyncTriggersResult> TrySyncTriggersAsync(bool isBackgroundSync
137138
var (success, error) = await SetTriggersAsync(payload.Content);
138139
if (success && newHash != null)
139140
{
140-
await UpdateHashAsync(hashBlob, newHash);
141+
await UpdateHashAsync(hashBlobClient, newHash);
141142
}
142143
result.Success = success;
143144
result.Error = error;
@@ -193,7 +194,7 @@ internal static bool IsSyncTriggersEnvironment(IScriptWebHostEnvironment webHost
193194
return true;
194195
}
195196

196-
internal async Task<string> CheckHashAsync(CloudBlockBlob hashBlob, string content)
197+
internal async Task<string> CheckHashAsync(BlobClient hashBlobClient, string content)
197198
{
198199
try
199200
{
@@ -210,9 +211,13 @@ internal async Task<string> CheckHashAsync(CloudBlockBlob hashBlob, string conte
210211

211212
// get the last hash value if present
212213
string lastHash = null;
213-
if (await hashBlob.ExistsAsync())
214+
if (await hashBlobClient.ExistsAsync())
214215
{
215-
lastHash = await hashBlob.DownloadTextAsync();
216+
var downloadResponse = await hashBlobClient.DownloadAsync();
217+
using (StreamReader reader = new StreamReader(downloadResponse.Value.Content))
218+
{
219+
lastHash = reader.ReadToEnd();
220+
}
216221
_logger.LogDebug($"SyncTriggers hash (Last='{lastHash}', Current='{currentHash}')");
217222
}
218223

@@ -234,13 +239,16 @@ internal async Task<string> CheckHashAsync(CloudBlockBlob hashBlob, string conte
234239
return null;
235240
}
236241

237-
internal async Task UpdateHashAsync(CloudBlockBlob hashBlob, string hash)
242+
internal async Task UpdateHashAsync(BlobClient hashBlobClient, string hash)
238243
{
239244
try
240245
{
241246
// hash value has changed or was not yet stored
242247
// update the last hash value in storage
243-
await hashBlob.UploadTextAsync(hash);
248+
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(hash)))
249+
{
250+
await hashBlobClient.UploadAsync(stream);
251+
}
244252
_logger.LogDebug($"SyncTriggers hash updated to '{hash}'");
245253
}
246254
catch (Exception ex)
@@ -250,23 +258,19 @@ internal async Task UpdateHashAsync(CloudBlockBlob hashBlob, string hash)
250258
}
251259
}
252260

253-
internal async Task<CloudBlockBlob> GetHashBlobAsync()
261+
internal async Task<BlobClient> GetHashBlobAsync()
254262
{
255-
if (_hashBlob == null)
263+
if (_hashBlobClient == null)
256264
{
257-
string storageConnectionString = _configuration.GetWebJobsConnectionString(ConnectionStringNames.Storage);
258-
CloudStorageAccount account = null;
259-
if (!string.IsNullOrEmpty(storageConnectionString) &&
260-
CloudStorageAccount.TryParse(storageConnectionString, out account))
265+
if (_azureStorageProvider.TryGetBlobServiceClientFromConnection(out BlobServiceClient blobClient, ConnectionStringNames.Storage))
261266
{
262267
string hostId = await _hostIdProvider.GetHostIdAsync(CancellationToken.None);
263-
CloudBlobClient blobClient = account.CreateCloudBlobClient();
264-
var blobContainer = blobClient.GetContainerReference(ScriptConstants.AzureWebJobsHostsContainerName);
268+
var blobContainerClient = blobClient.GetBlobContainerClient(ScriptConstants.AzureWebJobsHostsContainerName);
265269
string hashBlobPath = $"synctriggers/{hostId}/last";
266-
_hashBlob = blobContainer.GetBlockBlobReference(hashBlobPath);
270+
_hashBlobClient = blobContainerClient.GetBlobClient(hashBlobPath);
267271
}
268272
}
269-
return _hashBlob;
273+
return _hashBlobClient;
270274
}
271275

272276
public async Task<SyncTriggersPayload> GetSyncTriggersPayload()

src/WebJobs.Script.WebHost/Security/KeyManagement/BlobStorageSasSecretsRepository.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5-
using Microsoft.Azure.Storage.Blob;
5+
using Azure.Storage.Blobs;
66
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.Extensions;
77
using Microsoft.Extensions.Logging;
88

@@ -13,14 +13,14 @@ namespace Microsoft.Azure.WebJobs.Script.WebHost
1313
/// </summary>
1414
public sealed class BlobStorageSasSecretsRepository : BlobStorageSecretsRepository
1515
{
16-
public BlobStorageSasSecretsRepository(string secretSentinelDirectoryPath, string containerSasUri, string siteSlotName, ILogger logger, IEnvironment environment)
17-
: base(secretSentinelDirectoryPath, containerSasUri, siteSlotName, logger, environment)
16+
public BlobStorageSasSecretsRepository(string secretSentinelDirectoryPath, string containerSasUri, string siteSlotName, ILogger logger, IEnvironment environment, IAzureStorageProvider azureStorageProvider)
17+
: base(secretSentinelDirectoryPath, containerSasUri, siteSlotName, logger, environment, azureStorageProvider)
1818
{
1919
}
2020

21-
protected override CloudBlobContainer CreateBlobContainer(string containerSasUri)
21+
protected override BlobContainerClient CreateBlobContainerClient(string containerSasUri)
2222
{
23-
return new CloudBlobContainer(new Uri(containerSasUri));
23+
return new BlobContainerClient(new Uri(containerSasUri));
2424
}
2525

2626
protected override void LogErrorMessage(string operation)

0 commit comments

Comments
 (0)