Skip to content

Commit de09f9f

Browse files
authored
Updating secret repository tests (#10317)
1 parent 3ec65cf commit de09f9f

File tree

4 files changed

+102
-80
lines changed

4 files changed

+102
-80
lines changed

eng/ci/templates/official/jobs/run-non-e2e-tests.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@ jobs:
4343
keyVaultName: azure-functions-host-$(LeaseBlob)
4444
secretsFilter: '*'
4545

46+
- task: AzureCLI@2
47+
displayName: 'Setup Azure environment'
48+
inputs:
49+
azureSubscription: Azure-Functions-Host-CI-internal
50+
addSpnToEnvironment: true
51+
scriptType: bash
52+
scriptLocation: inlineScript
53+
inlineScript: |
54+
echo "##vso[task.setvariable variable=ARM_CLIENT_ID]$servicePrincipalId"
55+
echo "##vso[task.setvariable variable=ARM_ID_TOKEN]$idToken"
56+
echo "##vso[task.setvariable variable=ARM_TENANT_ID]$tenantId"
57+
58+
# This step ensures the azure context defined by the previous task is persisted
59+
# and available to subsequent steps/tasks.
60+
- bash: |
61+
az login --service-principal -u $(ARM_CLIENT_ID) --tenant $(ARM_TENANT_ID) --allow-no-subscriptions --federated-token $(ARM_ID_TOKEN)
62+
displayName: 'Login to Azure'
63+
4664
- task: DotNetCoreCLI@2
4765
displayName: Build Integration.csproj
4866
inputs:

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ public KeyVaultSecretsRepository(string secretsSentinelFilePath, string vaultUri
5757
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
5858
}
5959

60-
internal KeyVaultSecretsRepository(string secretsSentinelFilePath, string vaultUri, string clientId, string clientSecret, string tenantId, ILogger logger, IEnvironment environment, TokenCredential testEnvironmentTokenCredential) : this(secretsSentinelFilePath, vaultUri, clientId, clientSecret, tenantId, logger, environment)
60+
internal KeyVaultSecretsRepository(string secretsSentinelFilePath, string vaultUri, ILogger logger, IEnvironment environment, TokenCredential testEnvironmentTokenCredential)
61+
: this(secretsSentinelFilePath, vaultUri, null, null, null, logger, environment)
6162
{
6263
_tokenCredential = new Lazy<TokenCredential>(() =>
6364
{
64-
if (!TryCreateTokenCredential(clientId, clientSecret, tenantId, out TokenCredential credential))
65+
if (!TryCreateTokenCredential(null, null, null, out TokenCredential credential))
6566
{
6667
throw new InvalidOperationException("Failed to create token credential for KeyVaultSecretsRepository");
6768
}

src/WebJobs.Script/Utility.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,13 @@ await InvokeWithRetriesAsync(() =>
141141
}, maxRetries, retryInterval);
142142
}
143143

144-
internal static async Task InvokeWithRetriesAsync(Func<Task> action, int maxRetries, TimeSpan retryInterval)
144+
internal static async Task InvokeWithRetriesWhenAsync(Func<Task> action, int maxRetries, TimeSpan retryInterval, Func<Exception, bool> predicate)
145145
{
146+
if (predicate is null)
147+
{
148+
throw new ArgumentNullException(nameof(predicate));
149+
}
150+
146151
int attempt = 0;
147152
while (true)
148153
{
@@ -151,7 +156,7 @@ internal static async Task InvokeWithRetriesAsync(Func<Task> action, int maxRetr
151156
await action();
152157
return;
153158
}
154-
catch (Exception ex) when (!ex.IsFatal())
159+
catch (Exception ex) when (predicate(ex))
155160
{
156161
if (++attempt > maxRetries)
157162
{
@@ -162,6 +167,9 @@ internal static async Task InvokeWithRetriesAsync(Func<Task> action, int maxRetr
162167
}
163168
}
164169

170+
internal static Task InvokeWithRetriesAsync(Func<Task> action, int maxRetries, TimeSpan retryInterval)
171+
=> InvokeWithRetriesWhenAsync(action, maxRetries, retryInterval, (e) => !e.IsFatal());
172+
165173
/// <summary>
166174
/// Delays while the specified condition remains true.
167175
/// </summary>

test/WebJobs.Script.Tests.Integration/Host/SecretsRepositoryTests.cs

Lines changed: 71 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -93,49 +93,47 @@ public async Task Constructor_CreatesSecretPathIfNotExists(SecretsRepositoryType
9393
[InlineData(SecretsRepositoryType.KeyVault, ScriptSecretsType.Function)]
9494
public async Task ReadAsync_ReadsExpectedFile(SecretsRepositoryType repositoryType, ScriptSecretsType secretsType)
9595
{
96-
using (var directory = new TempDirectory())
96+
using var directory = new TempDirectory();
97+
98+
await _fixture.TestInitialize(repositoryType, directory.Path);
99+
ScriptSecrets testSecrets = null;
100+
if (secretsType == ScriptSecretsType.Host)
97101
{
98-
await _fixture.TestInitialize(repositoryType, directory.Path);
99-
ScriptSecrets testSecrets = null;
100-
if (secretsType == ScriptSecretsType.Host)
102+
testSecrets = new HostSecrets()
101103
{
102-
testSecrets = new HostSecrets()
103-
{
104-
MasterKey = new Key("master", "test"),
105-
FunctionKeys = new List<Key>() { new Key(KeyName, "test") },
106-
SystemKeys = new List<Key>() { new Key(KeyName, "test") }
107-
};
108-
}
109-
else
104+
MasterKey = new("master", "test"),
105+
FunctionKeys = [new(KeyName, "test")],
106+
SystemKeys = [new(KeyName, "test")]
107+
};
108+
}
109+
else
110+
{
111+
testSecrets = new FunctionSecrets()
110112
{
111-
testSecrets = new FunctionSecrets()
112-
{
113-
Keys = new List<Key>() { new Key(KeyName, "test") }
114-
};
115-
}
116-
string testFunctionName = secretsType == ScriptSecretsType.Host ? "host" : functionName;
113+
Keys = [new(KeyName, "test")]
114+
};
115+
}
116+
string testFunctionName = secretsType == ScriptSecretsType.Host ? "host" : functionName;
117117

118-
await _fixture.WriteSecret(testFunctionName, testSecrets);
118+
await _fixture.WriteSecret(testFunctionName, testSecrets);
119119

120-
var target = _fixture.GetNewSecretRepository();
120+
var target = _fixture.GetNewSecretRepository();
121121

122-
ScriptSecrets secretsContent = await target.ReadAsync(secretsType, testFunctionName);
123-
124-
if (secretsType == ScriptSecretsType.Host)
125-
{
126-
Assert.Equal((secretsContent as HostSecrets).MasterKey.Name, "master");
127-
Assert.Equal((secretsContent as HostSecrets).MasterKey.Value, "test");
128-
Assert.Equal((secretsContent as HostSecrets).FunctionKeys[0].Name, KeyName);
129-
Assert.Equal((secretsContent as HostSecrets).FunctionKeys[0].Value, "test");
130-
Assert.Equal((secretsContent as HostSecrets).SystemKeys[0].Name, KeyName);
131-
Assert.Equal((secretsContent as HostSecrets).SystemKeys[0].Value, "test");
132-
}
133-
else
134-
{
135-
Assert.Equal((secretsContent as FunctionSecrets).Keys[0].Name, KeyName);
136-
Assert.Equal((secretsContent as FunctionSecrets).Keys[0].Value, "test");
137-
}
122+
ScriptSecrets secretsContent = await target.ReadAsync(secretsType, testFunctionName);
138123

124+
if (secretsType == ScriptSecretsType.Host)
125+
{
126+
Assert.Equal((secretsContent as HostSecrets).MasterKey.Name, "master");
127+
Assert.Equal((secretsContent as HostSecrets).MasterKey.Value, "test");
128+
Assert.Equal((secretsContent as HostSecrets).FunctionKeys[0].Name, KeyName);
129+
Assert.Equal((secretsContent as HostSecrets).FunctionKeys[0].Value, "test");
130+
Assert.Equal((secretsContent as HostSecrets).SystemKeys[0].Name, KeyName);
131+
Assert.Equal((secretsContent as HostSecrets).SystemKeys[0].Value, "test");
132+
}
133+
else
134+
{
135+
Assert.Equal((secretsContent as FunctionSecrets).Keys[0].Name, KeyName);
136+
Assert.Equal((secretsContent as FunctionSecrets).Keys[0].Value, "test");
139137
}
140138
}
141139

@@ -526,6 +524,8 @@ async Task RunTest()
526524

527525
public class Fixture : IDisposable
528526
{
527+
private readonly DefaultAzureCredentialOptions _azureCredentialOptions;
528+
529529
public Fixture()
530530
{
531531
TestSiteName = "Test_test";
@@ -534,17 +534,15 @@ public Fixture()
534534
var configuration = TestHelpers.GetTestConfiguration();
535535
BlobConnectionString = configuration.GetWebJobsConnectionString(ConnectionStringNames.Storage);
536536
KeyVaultUri = configuration.GetWebJobsConnectionString(EnvironmentSettingNames.AzureWebJobsSecretStorageKeyVaultUri);
537-
KeyVaultClientId = configuration.GetWebJobsConnectionString(EnvironmentSettingNames.AzureWebJobsSecretStorageKeyVaultClientId);
538-
KeyVaultClientSecret = configuration.GetWebJobsConnectionString(EnvironmentSettingNames.AzureWebJobsSecretStorageKeyVaultClientSecret);
539-
KeyVaultTenantId = configuration.GetWebJobsConnectionString(EnvironmentSettingNames.AzureWebJobsSecretStorageKeyVaultTenantId);
540537

541-
if (KeyVaultTenantId is not null && KeyVaultClientId is not null &&
542-
KeyVaultClientSecret is not null && KeyVaultUri is not null)
538+
// Exclude managed identity for the test to avoid using the
539+
// credential setup in the test environment.
540+
_azureCredentialOptions = new DefaultAzureCredentialOptions
543541
{
544-
// These will fail later if required; but sometimes when testing locally you don't care about KeyVault
545-
var credential = new ClientSecretCredential(KeyVaultTenantId, KeyVaultClientId, KeyVaultClientSecret);
546-
SecretClient = new SecretClient(new Uri(KeyVaultUri), credential);
547-
}
542+
ExcludeManagedIdentityCredential = true,
543+
};
544+
545+
SecretClient = new SecretClient(new Uri(KeyVaultUri), new DefaultAzureCredential(_azureCredentialOptions));
548546

549547
AzureBlobStorageProvider = TestHelpers.GetAzureBlobStorageProvider(configuration);
550548
}
@@ -565,12 +563,6 @@ public Fixture()
565563

566564
public string KeyVaultUri { get; private set; }
567565

568-
public string KeyVaultClientId { get; private set; }
569-
570-
public string KeyVaultClientSecret { get; private set; }
571-
572-
public string KeyVaultTenantId { get; private set; }
573-
574566
public SecretsRepositoryType RepositoryType { get; private set; }
575567

576568
public ILoggerProvider LoggerProvider { get; private set; }
@@ -612,22 +604,14 @@ public async Task TestInitialize(SecretsRepositoryType repositoryType, string se
612604
public ISecretsRepository GetNewSecretRepository()
613605
{
614606
var logger = LoggerProvider.CreateLogger("Test");
615-
if (RepositoryType == SecretsRepositoryType.BlobStorage)
616-
{
617-
return new BlobStorageSecretsRepository(SecretsDirectory, ConnectionStringNames.Storage, TestSiteName, logger, Environment, AzureBlobStorageProvider);
618-
}
619-
else if (RepositoryType == SecretsRepositoryType.BlobStorageSas)
620-
{
621-
return new BlobStorageSasSecretsRepository(SecretsDirectory, BlobSasConnectionUri.ToString(), TestSiteName, logger, Environment, AzureBlobStorageProvider);
622-
}
623-
else if (RepositoryType == SecretsRepositoryType.FileSystem)
624-
{
625-
return new FileSystemSecretsRepository(SecretsDirectory, logger, Environment);
626-
}
627-
else
607+
608+
return RepositoryType switch
628609
{
629-
return new KeyVaultSecretsRepository(SecretsDirectory, KeyVaultUri, KeyVaultClientId, KeyVaultClientSecret, KeyVaultTenantId, logger, Environment, new WorkloadIdentityCredential());
630-
}
610+
SecretsRepositoryType.BlobStorage => new BlobStorageSecretsRepository(SecretsDirectory, ConnectionStringNames.Storage, TestSiteName, logger, Environment, AzureBlobStorageProvider),
611+
SecretsRepositoryType.BlobStorageSas => new BlobStorageSasSecretsRepository(SecretsDirectory, BlobSasConnectionUri.ToString(), TestSiteName, logger, Environment, AzureBlobStorageProvider),
612+
SecretsRepositoryType.FileSystem => new FileSystemSecretsRepository(SecretsDirectory, logger, Environment),
613+
_ => new KeyVaultSecretsRepository(SecretsDirectory, KeyVaultUri, logger, Environment, new DefaultAzureCredential(_azureCredentialOptions))
614+
};
631615
}
632616

633617
public void Dispose()
@@ -652,10 +636,8 @@ private string RelativeBlobPath(string functionNameOrHost)
652636

653637
private string SecretsFileOrSentinelPath(string functionNameOrHost)
654638
{
655-
string secretFilePath = null;
656639
string fileName = string.Format(CultureInfo.InvariantCulture, "{0}.json", functionNameOrHost);
657-
secretFilePath = Path.Combine(SecretsDirectory, fileName);
658-
return secretFilePath;
640+
return Path.Combine(SecretsDirectory, fileName);
659641
}
660642

661643
public async Task WriteSecret(string functionNameOrHost, ScriptSecrets scriptSecret)
@@ -703,7 +685,8 @@ private async Task WriteSecretsKeyVaultAndUpdateSectinelFile(string functionName
703685
Dictionary<string, string> dictionary = KeyVaultSecretsRepository.GetDictionaryFromScriptSecrets(secrets, functionNameOrHost);
704686
foreach (string key in dictionary.Keys)
705687
{
706-
await SecretClient.SetSecretAsync(key, dictionary[key]);
688+
await Utility.InvokeWithRetriesWhenAsync(() => SecretClient.SetSecretAsync(key, dictionary[key]),
689+
5, TimeSpan.FromSeconds(1), (e) => e is RequestFailedException rfex && rfex.Status == 409);
707690
}
708691
}
709692

@@ -818,16 +801,28 @@ private async Task ClearAllBlobSecrets()
818801
}
819802
}
820803

804+
private async Task DeleteKeyVaultSecret(SecretProperties secret)
805+
{
806+
var result = await SecretClient.StartDeleteSecretAsync(secret.Name);
807+
await result.WaitForCompletionAsync();
808+
809+
if (result.HasCompleted && secret.RecoveryLevel.Contains("purgeable", StringComparison.OrdinalIgnoreCase))
810+
{
811+
await SecretClient.PurgeDeletedSecretAsync(secret.Name);
812+
}
813+
}
814+
821815
private async Task ClearAllKeyVaultSecrets()
822816
{
823-
var secretsPages = KeyVaultSecretsRepository.GetKeyVaultSecretsPagesAsync(SecretClient).AsPages();
824-
await foreach (Page<SecretProperties> page in secretsPages)
817+
var pages = SecretClient.GetPropertiesOfSecretsAsync();
818+
819+
var secretOperations = new List<Task>();
820+
await foreach (var item in pages)
825821
{
826-
foreach (SecretProperties item in page.Values)
827-
{
828-
await SecretClient.StartDeleteSecretAsync(item.Name);
829-
}
822+
secretOperations.Add(DeleteKeyVaultSecret(item));
830823
}
824+
825+
await Task.WhenAll(secretOperations);
831826
}
832827
}
833828
}

0 commit comments

Comments
 (0)