Skip to content

Commit c0d611c

Browse files
Merge branch 'zhiyuanliang/select-snapshot' of https://github.com/Azure/AppConfiguration-JavaScriptProvider into zhiyuanliang/tag-filter
2 parents ca3c113 + 381d833 commit c0d611c

File tree

5 files changed

+90
-6
lines changed

5 files changed

+90
-6
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ListConfigurationSettingsOptions,
1111
featureFlagPrefix,
1212
isFeatureFlag,
13+
isSecretReference,
1314
GetSnapshotOptions,
1415
GetSnapshotResponse,
1516
KnownSnapshotComposition
@@ -104,6 +105,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
104105
#ffRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
105106
#ffRefreshTimer: RefreshTimer;
106107

108+
// Key Vault references
109+
#resolveSecretsInParallel: boolean = false;
110+
107111
/**
108112
* Selectors of key-values obtained from @see AzureAppConfigurationOptions.selectors
109113
*/
@@ -184,6 +188,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
184188
}
185189
}
186190

191+
if (options?.keyVaultOptions?.parallelSecretResolutionEnabled) {
192+
this.#resolveSecretsInParallel = options.keyVaultOptions.parallelSecretResolutionEnabled;
193+
}
194+
187195
this.#adapters.push(new AzureKeyVaultKeyValueAdapter(options?.keyVaultOptions));
188196
this.#adapters.push(new JsonKeyValueAdapter());
189197
}
@@ -529,7 +537,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
529537
*/
530538
async #loadSelectedAndWatchedKeyValues() {
531539
const keyValues: [key: string, value: unknown][] = [];
532-
const loadedSettings = await this.#loadConfigurationSettings();
540+
const loadedSettings: ConfigurationSetting[] = await this.#loadConfigurationSettings();
533541
if (this.#refreshEnabled && !this.#watchAll) {
534542
await this.#updateWatchedKeyValuesEtag(loadedSettings);
535543
}
@@ -539,11 +547,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
539547
this.#aiConfigurationTracing.reset();
540548
}
541549

542-
// adapt configuration settings to key-values
550+
const secretResolutionPromises: Promise<void>[] = [];
543551
for (const setting of loadedSettings) {
552+
if (this.#resolveSecretsInParallel && isSecretReference(setting)) {
553+
// secret references are resolved asynchronously to improve performance
554+
const secretResolutionPromise = this.#processKeyValue(setting)
555+
.then(([key, value]) => {
556+
keyValues.push([key, value]);
557+
});
558+
secretResolutionPromises.push(secretResolutionPromise);
559+
continue;
560+
}
561+
// adapt configuration settings to key-values
544562
const [key, value] = await this.#processKeyValue(setting);
545563
keyValues.push([key, value]);
546564
}
565+
if (secretResolutionPromises.length > 0) {
566+
// wait for all secret resolution promises to be resolved
567+
await Promise.all(secretResolutionPromises);
568+
}
547569

548570
this.#clearLoadedKeyValues(); // clear existing key-values in case of configuration setting deletion
549571
for (const [k, v] of keyValues) {
@@ -588,7 +610,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
588610
*/
589611
async #loadFeatureFlags() {
590612
const loadFeatureFlag = true;
591-
const featureFlagSettings = await this.#loadConfigurationSettings(loadFeatureFlag);
613+
const featureFlagSettings: ConfigurationSetting[] = await this.#loadConfigurationSettings(loadFeatureFlag);
592614

593615
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
594616
// Reset old feature flag tracing in order to track the information present in the current response from server.
@@ -959,7 +981,9 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
959981
return [{ keyFilter: `${featureFlagPrefix}${KeyFilter.Any}`, labelFilter: LabelFilter.Null }];
960982
}
961983
selectors.forEach(selector => {
962-
selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`;
984+
if (selector.keyFilter) {
985+
selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`;
986+
}
963987
});
964988
return getValidSettingSelectors(selectors);
965989
}

src/keyvault/KeyVaultOptions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,12 @@ export interface KeyVaultOptions {
3232
* @returns The secret value.
3333
*/
3434
secretResolver?: (keyVaultReference: URL) => string | Promise<string>;
35+
36+
/**
37+
* Specifies whether to resolve the secret value in parallel.
38+
*
39+
* @remarks
40+
* If not specified, the default value is false.
41+
*/
42+
parallelSecretResolutionEnabled?: boolean;
3543
}

test/featureFlag.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as chai from "chai";
55
import * as chaiAsPromised from "chai-as-promised";
66
import { featureFlagContentType } from "@azure/app-configuration";
77
import { load } from "./exportedApi.js";
8-
import { MAX_TIME_OUT, createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper.js";
8+
import { MAX_TIME_OUT, mockAppConfigurationClientGetSnapshot, mockAppConfigurationClientListConfigurationSettingsForSnapshot, createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper.js";
99
chai.use(chaiAsPromised);
1010
const expect = chai.expect;
1111

@@ -337,4 +337,25 @@ describe("feature flags", function () {
337337
expect(featureFlag.telemetry.metadata.ETag).equals("ETag");
338338
expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/kv/.appconfig.featureflag/Telemetry_2?label=Test`);
339339
});
340+
341+
it("should load feature flags from snapshot", async () => {
342+
const snapshotName = "Test";
343+
mockAppConfigurationClientGetSnapshot(snapshotName, {compositionType: "key"});
344+
mockAppConfigurationClientListConfigurationSettingsForSnapshot(snapshotName, [[createMockedFeatureFlag("TestFeature", { enabled: true })]]);
345+
const connectionString = createMockedConnectionString();
346+
const settings = await load(connectionString, {
347+
featureFlagOptions: {
348+
enabled: true,
349+
selectors: [ { snapshotName: snapshotName } ]
350+
}
351+
});
352+
expect(settings).not.undefined;
353+
expect(settings.get("feature_management")).not.undefined;
354+
const featureFlags = settings.get<any>("feature_management").feature_flags;
355+
expect((featureFlags as []).length).equals(1);
356+
const featureFlag = featureFlags[0];
357+
expect(featureFlag.id).equals("TestFeature");
358+
expect(featureFlag.enabled).equals(true);
359+
restoreMocks();
360+
});
340361
});

test/keyvault.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,16 @@ describe("key vault reference", function () {
127127
expect(settings.get("TestKey")).eq("SecretValue");
128128
expect(settings.get("TestKey2")).eq("SecretValue2");
129129
});
130+
131+
it("should resolve key vault reference in parallel", async () => {
132+
const settings = await load(createMockedConnectionString(), {
133+
keyVaultOptions: {
134+
credential: createMockedTokenCredential(),
135+
parallelSecretResolutionEnabled: true
136+
}
137+
});
138+
expect(settings).not.undefined;
139+
expect(settings.get("TestKey")).eq("SecretValue");
140+
expect(settings.get("TestKeyFixedVersion")).eq("OldSecretValue");
141+
});
130142
});

test/load.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,25 @@ describe("load", function () {
136136
return expect(load("invalid-endpoint-url", credential)).eventually.rejectedWith("Invalid URL");
137137
});
138138

139+
it("should throw error given invalid selector", async () => {
140+
const connectionString = createMockedConnectionString();
141+
return expect(load(connectionString, {
142+
selectors: [{
143+
labelFilter: "\0"
144+
}]
145+
})).eventually.rejectedWith("Key filter cannot be null or empty.");
146+
});
147+
148+
it("should throw error given invalid snapshot selector", async () => {
149+
const connectionString = createMockedConnectionString();
150+
return expect(load(connectionString, {
151+
selectors: [{
152+
snapshotName: "Test",
153+
labelFilter: "\0"
154+
}]
155+
})).eventually.rejectedWith("Key or label filter should not be used for a snapshot.");
156+
});
157+
139158
it("should not include feature flags directly in the settings", async () => {
140159
const connectionString = createMockedConnectionString();
141160
const settings = await load(connectionString);
@@ -470,7 +489,7 @@ describe("load", function () {
470489
}).to.throw("Invalid separator '%'. Supported values: '.', ',', ';', '-', '_', '__', '/', ':'.");
471490
});
472491

473-
it("should load from snapshot", async () => {
492+
it("should load key values from snapshot", async () => {
474493
const snapshotName = "Test";
475494
mockAppConfigurationClientGetSnapshot(snapshotName, {compositionType: "key"});
476495
mockAppConfigurationClientListConfigurationSettingsForSnapshot(snapshotName, [[{key: "TestKey", value: "TestValue"}].map(createMockedKeyValue)]);

0 commit comments

Comments
 (0)