Skip to content

Commit 737c4c6

Browse files
Merge branch 'main' of https://github.com/Azure/AppConfiguration-JavaScriptProvider into merge-main-to-preview
2 parents 663deb6 + 5227eb5 commit 737c4c6

File tree

11 files changed

+288
-59
lines changed

11 files changed

+288
-59
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@azure/app-configuration-provider",
3-
"version": "2.0.2",
3+
"version": "2.1.0",
44
"description": "The JavaScript configuration provider for Azure App Configuration",
55
"main": "dist/index.js",
66
"module": "./dist-esm/index.js",

src/AzureAppConfigurationImpl.ts

Lines changed: 134 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { AppConfigurationClient, ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions, featureFlagPrefix, isFeatureFlag } from "@azure/app-configuration";
4+
import {
5+
AppConfigurationClient,
6+
ConfigurationSetting,
7+
ConfigurationSettingId,
8+
GetConfigurationSettingOptions,
9+
GetConfigurationSettingResponse,
10+
ListConfigurationSettingsOptions,
11+
featureFlagPrefix,
12+
isFeatureFlag,
13+
isSecretReference,
14+
GetSnapshotOptions,
15+
GetSnapshotResponse,
16+
KnownSnapshotComposition
17+
} from "@azure/app-configuration";
518
import { isRestError } from "@azure/core-rest-pipeline";
619
import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./AzureAppConfiguration.js";
720
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
@@ -37,7 +50,14 @@ import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } fro
3750
import { parseContentType, isJsonContentType, isFeatureFlagContentType, isSecretReferenceContentType } from "./common/contentType.js";
3851
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
3952
import { RefreshTimer } from "./refresh/RefreshTimer.js";
40-
import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
53+
import {
54+
RequestTracingOptions,
55+
getConfigurationSettingWithTrace,
56+
listConfigurationSettingsWithTrace,
57+
getSnapshotWithTrace,
58+
listConfigurationSettingsForSnapshotWithTrace,
59+
requestTracingEnabled
60+
} from "./requestTracing/utils.js";
4161
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
4262
import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js";
4363
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
@@ -91,6 +111,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
91111
#ffRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
92112
#ffRefreshTimer: RefreshTimer;
93113

114+
// Key Vault references
115+
#resolveSecretsInParallel: boolean = false;
116+
94117
/**
95118
* Selectors of key-values obtained from @see AzureAppConfigurationOptions.selectors
96119
*/
@@ -171,6 +194,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
171194
}
172195
}
173196

197+
if (options?.keyVaultOptions?.parallelSecretResolutionEnabled) {
198+
this.#resolveSecretsInParallel = options.keyVaultOptions.parallelSecretResolutionEnabled;
199+
}
200+
174201
this.#adapters.push(new AzureKeyVaultKeyValueAdapter(options?.keyVaultOptions));
175202
this.#adapters.push(new JsonKeyValueAdapter());
176203
}
@@ -454,26 +481,49 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
454481
);
455482

456483
for (const selector of selectorsToUpdate) {
457-
const listOptions: ListConfigurationSettingsOptions = {
458-
keyFilter: selector.keyFilter,
459-
labelFilter: selector.labelFilter
460-
};
461-
462-
const pageEtags: string[] = [];
463-
const pageIterator = listConfigurationSettingsWithTrace(
464-
this.#requestTraceOptions,
465-
client,
466-
listOptions
467-
).byPage();
468-
for await (const page of pageIterator) {
469-
pageEtags.push(page.etag ?? "");
470-
for (const setting of page.items) {
471-
if (loadFeatureFlag === isFeatureFlag(setting)) {
472-
loadedSettings.push(setting);
484+
if (selector.snapshotName === undefined) {
485+
const listOptions: ListConfigurationSettingsOptions = {
486+
keyFilter: selector.keyFilter,
487+
labelFilter: selector.labelFilter
488+
};
489+
const pageEtags: string[] = [];
490+
const pageIterator = listConfigurationSettingsWithTrace(
491+
this.#requestTraceOptions,
492+
client,
493+
listOptions
494+
).byPage();
495+
496+
for await (const page of pageIterator) {
497+
pageEtags.push(page.etag ?? "");
498+
for (const setting of page.items) {
499+
if (loadFeatureFlag === isFeatureFlag(setting)) {
500+
loadedSettings.push(setting);
501+
}
502+
}
503+
}
504+
selector.pageEtags = pageEtags;
505+
} else { // snapshot selector
506+
const snapshot = await this.#getSnapshot(selector.snapshotName);
507+
if (snapshot === undefined) {
508+
throw new InvalidOperationError(`Could not find snapshot with name ${selector.snapshotName}.`);
509+
}
510+
if (snapshot.compositionType != KnownSnapshotComposition.Key) {
511+
throw new InvalidOperationError(`Composition type for the selected snapshot with name ${selector.snapshotName} must be 'key'.`);
512+
}
513+
const pageIterator = listConfigurationSettingsForSnapshotWithTrace(
514+
this.#requestTraceOptions,
515+
client,
516+
selector.snapshotName
517+
).byPage();
518+
519+
for await (const page of pageIterator) {
520+
for (const setting of page.items) {
521+
if (loadFeatureFlag === isFeatureFlag(setting)) {
522+
loadedSettings.push(setting);
523+
}
473524
}
474525
}
475526
}
476-
selector.pageEtags = pageEtags;
477527
}
478528

479529
if (loadFeatureFlag) {
@@ -492,7 +542,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
492542
*/
493543
async #loadSelectedAndWatchedKeyValues() {
494544
const keyValues: [key: string, value: unknown][] = [];
495-
const loadedSettings = await this.#loadConfigurationSettings();
545+
const loadedSettings: ConfigurationSetting[] = await this.#loadConfigurationSettings();
496546
if (this.#refreshEnabled && !this.#watchAll) {
497547
await this.#updateWatchedKeyValuesEtag(loadedSettings);
498548
}
@@ -502,11 +552,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
502552
this.#aiConfigurationTracing.reset();
503553
}
504554

505-
// adapt configuration settings to key-values
555+
const secretResolutionPromises: Promise<void>[] = [];
506556
for (const setting of loadedSettings) {
557+
if (this.#resolveSecretsInParallel && isSecretReference(setting)) {
558+
// secret references are resolved asynchronously to improve performance
559+
const secretResolutionPromise = this.#processKeyValue(setting)
560+
.then(([key, value]) => {
561+
keyValues.push([key, value]);
562+
});
563+
secretResolutionPromises.push(secretResolutionPromise);
564+
continue;
565+
}
566+
// adapt configuration settings to key-values
507567
const [key, value] = await this.#processKeyValue(setting);
508568
keyValues.push([key, value]);
509569
}
570+
if (secretResolutionPromises.length > 0) {
571+
// wait for all secret resolution promises to be resolved
572+
await Promise.all(secretResolutionPromises);
573+
}
510574

511575
this.#clearLoadedKeyValues(); // clear existing key-values in case of configuration setting deletion
512576
for (const [k, v] of keyValues) {
@@ -551,7 +615,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
551615
*/
552616
async #loadFeatureFlags() {
553617
const loadFeatureFlag = true;
554-
const featureFlagSettings = await this.#loadConfigurationSettings(loadFeatureFlag);
618+
const featureFlagSettings: ConfigurationSetting[] = await this.#loadConfigurationSettings(loadFeatureFlag);
555619

556620
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
557621
// Reset old feature flag tracing in order to track the information present in the current response from server.
@@ -631,6 +695,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
631695
async #checkConfigurationSettingsChange(selectors: PagedSettingSelector[]): Promise<boolean> {
632696
const funcToExecute = async (client) => {
633697
for (const selector of selectors) {
698+
if (selector.snapshotName) { // skip snapshot selector
699+
continue;
700+
}
634701
const listOptions: ListConfigurationSettingsOptions = {
635702
keyFilter: selector.keyFilter,
636703
labelFilter: selector.labelFilter,
@@ -682,6 +749,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
682749
return response;
683750
}
684751

752+
async #getSnapshot(snapshotName: string, customOptions?: GetSnapshotOptions): Promise<GetSnapshotResponse | undefined> {
753+
const funcToExecute = async (client) => {
754+
return getSnapshotWithTrace(
755+
this.#requestTraceOptions,
756+
client,
757+
snapshotName,
758+
customOptions
759+
);
760+
};
761+
762+
let response: GetSnapshotResponse | undefined;
763+
try {
764+
response = await this.#executeWithFailoverPolicy(funcToExecute);
765+
} catch (error) {
766+
if (isRestError(error) && error.statusCode === 404) {
767+
response = undefined;
768+
} else {
769+
throw error;
770+
}
771+
}
772+
return response;
773+
}
774+
685775
// Only operations related to Azure App Configuration should be executed with failover policy.
686776
async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise<any>): Promise<any> {
687777
let clientWrappers = await this.#clientManager.getClients();
@@ -940,11 +1030,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
9401030
}
9411031
}
9421032

943-
function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
944-
// below code deduplicates selectors by keyFilter and labelFilter, the latter selector wins
1033+
function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] {
1034+
// below code deduplicates selectors, the latter selector wins
9451035
const uniqueSelectors: SettingSelector[] = [];
9461036
for (const selector of selectors) {
947-
const existingSelectorIndex = uniqueSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter);
1037+
const existingSelectorIndex = uniqueSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter && s.snapshotName === selector.snapshotName);
9481038
if (existingSelectorIndex >= 0) {
9491039
uniqueSelectors.splice(existingSelectorIndex, 1);
9501040
}
@@ -953,14 +1043,20 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
9531043

9541044
return uniqueSelectors.map(selectorCandidate => {
9551045
const selector = { ...selectorCandidate };
956-
if (!selector.keyFilter) {
957-
throw new ArgumentError("Key filter cannot be null or empty.");
958-
}
959-
if (!selector.labelFilter) {
960-
selector.labelFilter = LabelFilter.Null;
961-
}
962-
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
963-
throw new ArgumentError("The characters '*' and ',' are not supported in label filters.");
1046+
if (selector.snapshotName) {
1047+
if (selector.keyFilter || selector.labelFilter) {
1048+
throw new ArgumentError("Key or label filter should not be used for a snapshot.");
1049+
}
1050+
} else {
1051+
if (!selector.keyFilter) {
1052+
throw new ArgumentError("Key filter cannot be null or empty.");
1053+
}
1054+
if (!selector.labelFilter) {
1055+
selector.labelFilter = LabelFilter.Null;
1056+
}
1057+
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
1058+
throw new ArgumentError("The characters '*' and ',' are not supported in label filters.");
1059+
}
9641060
}
9651061
return selector;
9661062
});
@@ -971,7 +1067,7 @@ function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelect
9711067
// Default selector: key: *, label: \0
9721068
return [{ keyFilter: KeyFilter.Any, labelFilter: LabelFilter.Null }];
9731069
}
974-
return getValidSelectors(selectors);
1070+
return getValidSettingSelectors(selectors);
9751071
}
9761072

9771073
function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSelector[] {
@@ -980,7 +1076,9 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
9801076
return [{ keyFilter: `${featureFlagPrefix}${KeyFilter.Any}`, labelFilter: LabelFilter.Null }];
9811077
}
9821078
selectors.forEach(selector => {
983-
selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`;
1079+
if (selector.keyFilter) {
1080+
selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`;
1081+
}
9841082
});
985-
return getValidSelectors(selectors);
1083+
return getValidSettingSelectors(selectors);
9861084
}

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
}

src/requestTracing/utils.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions } from "@azure/app-configuration";
4+
import { OperationOptions } from "@azure/core-client";
5+
import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions, GetSnapshotOptions, ListConfigurationSettingsForSnapshotOptions } from "@azure/app-configuration";
56
import { AzureAppConfigurationOptions } from "../AzureAppConfigurationOptions.js";
67
import { FeatureFlagTracingOptions } from "./FeatureFlagTracingOptions.js";
78
import { AIConfigurationTracingOptions } from "./AIConfigurationTracingOptions.js";
@@ -52,15 +53,7 @@ export function listConfigurationSettingsWithTrace(
5253
client: AppConfigurationClient,
5354
listOptions: ListConfigurationSettingsOptions
5455
) {
55-
const actualListOptions = { ...listOptions };
56-
if (requestTracingOptions.enabled) {
57-
actualListOptions.requestOptions = {
58-
customHeaders: {
59-
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions)
60-
}
61-
};
62-
}
63-
56+
const actualListOptions = applyRequestTracing(requestTracingOptions, listOptions);
6457
return client.listConfigurationSettings(actualListOptions);
6558
}
6659

@@ -70,20 +63,43 @@ export function getConfigurationSettingWithTrace(
7063
configurationSettingId: ConfigurationSettingId,
7164
getOptions?: GetConfigurationSettingOptions,
7265
) {
73-
const actualGetOptions = { ...getOptions };
66+
const actualGetOptions = applyRequestTracing(requestTracingOptions, getOptions);
67+
return client.getConfigurationSetting(configurationSettingId, actualGetOptions);
68+
}
69+
70+
export function getSnapshotWithTrace(
71+
requestTracingOptions: RequestTracingOptions,
72+
client: AppConfigurationClient,
73+
snapshotName: string,
74+
getOptions?: GetSnapshotOptions
75+
) {
76+
const actualGetOptions = applyRequestTracing(requestTracingOptions, getOptions);
77+
return client.getSnapshot(snapshotName, actualGetOptions);
78+
}
7479

80+
export function listConfigurationSettingsForSnapshotWithTrace(
81+
requestTracingOptions: RequestTracingOptions,
82+
client: AppConfigurationClient,
83+
snapshotName: string,
84+
listOptions?: ListConfigurationSettingsForSnapshotOptions
85+
) {
86+
const actualListOptions = applyRequestTracing(requestTracingOptions, listOptions);
87+
return client.listConfigurationSettingsForSnapshot(snapshotName, actualListOptions);
88+
}
89+
90+
function applyRequestTracing<T extends OperationOptions>(requestTracingOptions: RequestTracingOptions, operationOptions?: T) {
91+
const actualOptions = { ...operationOptions };
7592
if (requestTracingOptions.enabled) {
76-
actualGetOptions.requestOptions = {
93+
actualOptions.requestOptions = {
7794
customHeaders: {
7895
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions)
7996
}
8097
};
8198
}
82-
83-
return client.getConfigurationSetting(configurationSettingId, actualGetOptions);
99+
return actualOptions;
84100
}
85101

86-
export function createCorrelationContextHeader(requestTracingOptions: RequestTracingOptions): string {
102+
function createCorrelationContextHeader(requestTracingOptions: RequestTracingOptions): string {
87103
/*
88104
RequestType: 'Startup' during application starting up, 'Watch' after startup completed.
89105
Host: identify with defined envs
@@ -227,4 +243,3 @@ export function isWebWorker() {
227243

228244
return workerGlobalScopeDefined && importScriptsAsGlobalFunction && isNavigatorDefinedAsExpected;
229245
}
230-

0 commit comments

Comments
 (0)