Skip to content

Commit de87ff5

Browse files
committed
update
1 parent 2c4a286 commit de87ff5

3 files changed

Lines changed: 65 additions & 72 deletions

File tree

src/keyvault/keyVaultKeyValueAdapter.ts

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,58 +29,56 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {
2929
if (!this.#keyVaultOptions) {
3030
throw new ArgumentError(KeyVaultReferenceErrorMessages.KEY_VAULT_OPTIONS_UNDEFINED);
3131
}
32-
let secretIdentifier: KeyVaultSecretIdentifier;
33-
try {
34-
secretIdentifier = parseKeyVaultSecretIdentifier(
35-
parseSecretReference(setting).value.secretId
36-
);
37-
} catch (error) {
38-
throw new KeyVaultReferenceError(buildKeyVaultReferenceErrorMessage("Invalid Key Vault reference.", setting), { cause: error });
39-
}
40-
41-
try {
42-
const secretValue = await this.#keyVaultSecretProvider.getSecretValue(secretIdentifier);
43-
return [setting.key, secretValue];
44-
} catch (error) {
45-
if (isRestError(error) || error instanceof AuthenticationError) {
46-
throw new KeyVaultReferenceError(buildKeyVaultReferenceErrorMessage("Failed to resolve Key Vault reference.", setting, secretIdentifier.sourceId), { cause: error });
47-
}
48-
throw error;
49-
}
32+
// Secret references are parsed, validated and resolved during preload; here we only read the
33+
// cached value. Parsing is guaranteed to succeed because preload runs first.
34+
const secretIdentifier = parseKeyVaultSecretIdentifier(
35+
parseSecretReference(setting).value.secretId
36+
);
37+
const secretValue = this.#keyVaultSecretProvider.getSecretValue(secretIdentifier);
38+
return [setting.key, secretValue];
5039
}
5140

5241
async preload(settings: ConfigurationSetting[]): Promise<void> {
5342
if (!this.#keyVaultOptions) {
5443
return; // no-op when keyVaultOptions is not configured
5544
}
56-
const uniqueSecretIdentifiers = new Map<string, KeyVaultSecretIdentifier>();
45+
// Deduplicate references by secret identifier (sourceId)
46+
// ConfigurationSetting is for Key Vault reference error building.
47+
const uniqueSecrets = new Map<string, { secretIdentifier: KeyVaultSecretIdentifier; setting: ConfigurationSetting }>();
5748
for (const setting of settings) {
5849
if (!this.canProcess(setting)) {
5950
continue;
6051
}
52+
let secretIdentifier: KeyVaultSecretIdentifier;
6153
try {
62-
const secretIdentifier = parseKeyVaultSecretIdentifier(
54+
secretIdentifier = parseKeyVaultSecretIdentifier(
6355
parseSecretReference(setting).value.secretId
6456
);
65-
uniqueSecretIdentifiers.set(secretIdentifier.sourceId, secretIdentifier); // dedup by sourceId
66-
} catch {
67-
// Skip invalid references; processKeyValue re-parses and raises KeyVaultReferenceError with context.
57+
} catch (error) {
58+
throw new KeyVaultReferenceError(buildKeyVaultReferenceErrorMessage("Invalid Key Vault reference.", setting), { cause: error });
59+
}
60+
if (!uniqueSecrets.has(secretIdentifier.sourceId)) {
61+
uniqueSecrets.set(secretIdentifier.sourceId, { secretIdentifier, setting });
6862
}
6963
}
7064

71-
const loadSecret = async (secretIdentifier: KeyVaultSecretIdentifier) => {
65+
const loadSecret = async ({ secretIdentifier, setting }: { secretIdentifier: KeyVaultSecretIdentifier; setting: ConfigurationSetting }) => {
7266
try {
7367
await this.#keyVaultSecretProvider.loadSecretValue(secretIdentifier);
74-
} catch {
75-
// Leave uncached; getSecretValue re-fetches and surfaces the error during resolution.
68+
} catch (error) {
69+
if (isRestError(error) || error instanceof AuthenticationError) {
70+
throw new KeyVaultReferenceError(buildKeyVaultReferenceErrorMessage("Failed to resolve Key Vault reference.", setting, secretIdentifier.sourceId), { cause: error });
71+
}
72+
throw error;
7673
}
7774
};
7875

79-
if (this.#keyVaultOptions?.parallelSecretResolutionEnabled) {
80-
await Promise.all([...uniqueSecretIdentifiers.values()].map(loadSecret));
76+
const uniqueSecretEntries = [...uniqueSecrets.values()];
77+
if (this.#keyVaultOptions.parallelSecretResolutionEnabled) {
78+
await Promise.all(uniqueSecretEntries.map(loadSecret));
8179
} else {
82-
for (const secretIdentifier of uniqueSecretIdentifiers.values()) {
83-
await loadSecret(secretIdentifier);
80+
for (const entry of uniqueSecretEntries) {
81+
await loadSecret(entry);
8482
}
8583
}
8684
}

src/keyvault/keyVaultSecretProvider.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,9 @@ export class AzureKeyVaultSecretProvider {
4242
this.#cachedSecretValues.set(identifierKey, await this.#getSecretValueFromKeyVault(secretIdentifier));
4343
}
4444

45-
async getSecretValue(secretIdentifier: KeyVaultSecretIdentifier): Promise<unknown> {
46-
const identifierKey = secretIdentifier.sourceId;
47-
if (this.#cachedSecretValues.has(identifierKey)) {
48-
return this.#cachedSecretValues.get(identifierKey);
49-
}
50-
51-
// Fallback for secrets that preload skipped or failed to fetch. loadSecretValue populates the cache.
52-
await this.loadSecretValue(secretIdentifier);
53-
return this.#cachedSecretValues.get(identifierKey);
45+
// Reads a secret value that was fetched into the cache during preload. All network I/O happens in preload.
46+
getSecretValue(secretIdentifier: KeyVaultSecretIdentifier): unknown {
47+
return this.#cachedSecretValues.get(secretIdentifier.sourceId);
5448
}
5549

5650
clearCache(): void {

test/keyvault.test.ts

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -269,20 +269,21 @@ describe("key vault reference deduplication", function () {
269269
});
270270

271271
it("should re-fetch once per unique secret on a key-value change reload", async () => {
272-
const sentinelEtag = "sentinel-etag";
273-
const kvs = [
274-
...["TestKey1", "TestKey2", "TestKey3", "TestKey4", "TestKey5"].map((key) => createMockedKeyVaultReference(key, sameSecretUri)),
275-
createMockedKeyValue({ key: "sentinel", value: "v1", etag: sentinelEtag })
272+
const secretKeys = ["TestKey1", "TestKey2", "TestKey3", "TestKey4", "TestKey5"];
273+
// Mutable page so the sentinel can change without re-stubbing (which would tear down the fake clock).
274+
const kvPage = [
275+
...secretKeys.map((key) => createMockedKeyVaultReference(key, sameSecretUri)),
276+
createMockedKeyValue({ key: "sentinel", value: "v1", etag: "sentinel-etag-1" })
276277
];
277-
mockAppConfigurationClientListConfigurationSettings([kvs]);
278-
mockAppConfigurationClientGetConfigurationSetting(kvs);
278+
mockAppConfigurationClientListConfigurationSettings([kvPage]);
279+
mockAppConfigurationClientGetConfigurationSetting(kvPage);
279280

280281
const client = new SecretClient("https://fake-vault-name.vault.azure.net", createMockedTokenCredential());
282+
let secretValue = "SecretValue-initial";
281283
let callCount = 0;
282284
sinon.stub(client, "getSecret").callsFake(async () => {
283285
callCount++;
284-
await sleepInMs(100);
285-
return { value: `SecretValue-${callCount}` } as KeyVaultSecret;
286+
return { value: secretValue } as KeyVaultSecret;
286287
});
287288

288289
const settings = await load(createMockedConnectionString(), {
@@ -299,30 +300,26 @@ describe("key vault reference deduplication", function () {
299300
// Initial load resolves the duplicates with a single request.
300301
expect(callCount).eq(1);
301302

302-
// Wait past the min secret refresh interval so the key-value change reload re-fetches secrets.
303-
await sleepInMs(60_000 + 100);
303+
// Install the fake clock seeded with the current time so the refresh timers (created during load) stay consistent.
304+
const clock = sinon.useFakeTimers({ now: Date.now() });
305+
try {
306+
// Advance past the min secret refresh interval so the key-value change reload clears the cache.
307+
await clock.tickAsync(60_000 + 100);
304308

305-
// Trigger a key-value change reload by changing the sentinel.
306-
const updatedKvs = [
307-
...["TestKey1", "TestKey2", "TestKey3", "TestKey4", "TestKey5"].map((key) => createMockedKeyVaultReference(key, sameSecretUri)),
308-
createMockedKeyValue({ key: "sentinel", value: "v2", etag: "sentinel-etag-2" })
309-
];
310-
restoreMocks();
311-
mockAppConfigurationClientListConfigurationSettings([updatedKvs]);
312-
mockAppConfigurationClientGetConfigurationSetting(updatedKvs);
313-
let reloadCallCount = 0;
314-
sinon.stub(client, "getSecret").callsFake(async () => {
315-
reloadCallCount++;
316-
await sleepInMs(100);
317-
return { value: "SecretValue-reloaded" } as KeyVaultSecret;
318-
});
309+
// Change the watched sentinel (new etag) and the secret value in place to trigger a reload.
310+
kvPage[kvPage.length - 1] = createMockedKeyValue({ key: "sentinel", value: "v2", etag: "sentinel-etag-2" });
311+
secretValue = "SecretValue-reloaded";
312+
const callCountBeforeReload = callCount;
319313

320-
await settings.refresh();
314+
await settings.refresh();
321315

322-
// The key-value change reload clears the cache and re-fetches, but only once for the unique secret.
323-
expect(reloadCallCount).eq(1);
324-
for (const key of ["TestKey1", "TestKey2", "TestKey3", "TestKey4", "TestKey5"]) {
325-
expect(settings.get(key)).eq("SecretValue-reloaded");
316+
// The key-value change reload clears the cache and re-fetches, but only once for the unique secret.
317+
expect(callCount - callCountBeforeReload).eq(1);
318+
for (const key of secretKeys) {
319+
expect(settings.get(key)).eq("SecretValue-reloaded");
320+
}
321+
} finally {
322+
clock.restore();
326323
}
327324
});
328325

@@ -332,7 +329,6 @@ describe("key vault reference deduplication", function () {
332329
let callCount = 0;
333330
sinon.stub(client, "getSecret").callsFake(async () => {
334331
callCount++;
335-
await sleepInMs(100);
336332
return { value: `SecretValue-${callCount}` } as KeyVaultSecret;
337333
});
338334

@@ -347,9 +343,14 @@ describe("key vault reference deduplication", function () {
347343
expect(callCount).eq(1);
348344
expect(settings.get("TestKey1")).eq("SecretValue-1");
349345

350-
// After the secret refresh interval elapses, the refresh round re-fetches once.
351-
await sleepInMs(60_000 + 100);
352-
await settings.refresh();
346+
// Advance past the secret refresh interval using a fake clock so the refresh round re-fetches once.
347+
const clock = sinon.useFakeTimers({ now: Date.now() });
348+
try {
349+
await clock.tickAsync(60_000 + 100);
350+
await settings.refresh();
351+
} finally {
352+
clock.restore();
353+
}
353354
expect(callCount).eq(2);
354355
expect(settings.get("TestKey1")).eq("SecretValue-2");
355356
});

0 commit comments

Comments
 (0)