Skip to content

Commit 12b0216

Browse files
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-JavaScriptProvider into zhiyuanliang/enforce-api-version-for-cdn
2 parents e457605 + 0fb509a commit 12b0216

File tree

11 files changed

+300
-70
lines changed

11 files changed

+300
-70
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: 145 additions & 45 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";
@@ -99,6 +119,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
99119
#ffRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
100120
#ffRefreshTimer: RefreshTimer;
101121

122+
// Key Vault references
123+
#resolveSecretsInParallel: boolean = false;
124+
102125
/**
103126
* Selectors of key-values obtained from @see AzureAppConfigurationOptions.selectors
104127
*/
@@ -184,6 +207,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
184207
}
185208
}
186209

210+
if (options?.keyVaultOptions?.parallelSecretResolutionEnabled) {
211+
this.#resolveSecretsInParallel = options.keyVaultOptions.parallelSecretResolutionEnabled;
212+
}
213+
187214
this.#adapters.push(new AzureKeyVaultKeyValueAdapter(options?.keyVaultOptions));
188215
this.#adapters.push(new JsonKeyValueAdapter());
189216
}
@@ -494,38 +521,63 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
494521
);
495522

496523
for (const selector of selectorsToUpdate) {
497-
let listOptions: ListConfigurationSettingsOptions = {
498-
keyFilter: selector.keyFilter,
499-
labelFilter: selector.labelFilter
500-
};
501-
502-
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
503-
if (this.#isCdnUsed) {
504-
listOptions = {
505-
...listOptions,
506-
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selectorCollection.cdnCacheBreakString ?? "" }}
524+
if (selector.snapshotName === undefined) {
525+
let listOptions: ListConfigurationSettingsOptions = {
526+
keyFilter: selector.keyFilter,
527+
labelFilter: selector.labelFilter
507528
};
508-
}
509529

510-
const pageEtags: string[] = [];
511-
const pageIterator = listConfigurationSettingsWithTrace(
512-
this.#requestTraceOptions,
513-
client,
514-
listOptions
515-
).byPage();
516-
for await (const page of pageIterator) {
517-
pageEtags.push(page.etag ?? ""); // pageEtags is string[]
518-
for (const setting of page.items) {
519-
if (loadFeatureFlag === isFeatureFlag(setting)) {
520-
loadedSettings.push(setting);
530+
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
531+
if (this.#isCdnUsed) {
532+
listOptions = {
533+
...listOptions,
534+
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selectorCollection.cdnCacheBreakString ?? "" }}
535+
};
536+
}
537+
538+
const pageEtags: string[] = [];
539+
const pageIterator = listConfigurationSettingsWithTrace(
540+
this.#requestTraceOptions,
541+
client,
542+
listOptions
543+
).byPage();
544+
545+
for await (const page of pageIterator) {
546+
pageEtags.push(page.etag ?? "");
547+
for (const setting of page.items) {
548+
if (loadFeatureFlag === isFeatureFlag(setting)) {
549+
loadedSettings.push(setting);
550+
}
521551
}
522552
}
523-
}
524553

525-
if (pageEtags.length === 0) {
526-
console.warn(`No page is found in the response of listing key-value selector: key=${selector.keyFilter} and label=${selector.labelFilter}.`);
554+
if (pageEtags.length === 0) {
555+
console.warn(`No page is found in the response of listing key-value selector: key=${selector.keyFilter} and label=${selector.labelFilter}.`);
556+
}
557+
558+
selector.pageEtags = pageEtags;
559+
} else { // snapshot selector
560+
const snapshot = await this.#getSnapshot(selector.snapshotName);
561+
if (snapshot === undefined) {
562+
throw new InvalidOperationError(`Could not find snapshot with name ${selector.snapshotName}.`);
563+
}
564+
if (snapshot.compositionType != KnownSnapshotComposition.Key) {
565+
throw new InvalidOperationError(`Composition type for the selected snapshot with name ${selector.snapshotName} must be 'key'.`);
566+
}
567+
const pageIterator = listConfigurationSettingsForSnapshotWithTrace(
568+
this.#requestTraceOptions,
569+
client,
570+
selector.snapshotName
571+
).byPage();
572+
573+
for await (const page of pageIterator) {
574+
for (const setting of page.items) {
575+
if (loadFeatureFlag === isFeatureFlag(setting)) {
576+
loadedSettings.push(setting);
577+
}
578+
}
579+
}
527580
}
528-
selector.pageEtags = pageEtags;
529581
}
530582

531583
selectorCollection.selectors = selectorsToUpdate;
@@ -540,7 +592,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
540592
*/
541593
async #loadSelectedAndWatchedKeyValues() {
542594
const keyValues: [key: string, value: unknown][] = [];
543-
const loadedSettings = await this.#loadConfigurationSettings();
595+
const loadedSettings: ConfigurationSetting[] = await this.#loadConfigurationSettings();
544596
if (this.#refreshEnabled && !this.#watchAll) {
545597
await this.#updateWatchedKeyValuesEtag(loadedSettings);
546598
}
@@ -550,11 +602,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
550602
this.#aiConfigurationTracing.reset();
551603
}
552604

553-
// adapt configuration settings to key-values
605+
const secretResolutionPromises: Promise<void>[] = [];
554606
for (const setting of loadedSettings) {
607+
if (this.#resolveSecretsInParallel && isSecretReference(setting)) {
608+
// secret references are resolved asynchronously to improve performance
609+
const secretResolutionPromise = this.#processKeyValue(setting)
610+
.then(([key, value]) => {
611+
keyValues.push([key, value]);
612+
});
613+
secretResolutionPromises.push(secretResolutionPromise);
614+
continue;
615+
}
616+
// adapt configuration settings to key-values
555617
const [key, value] = await this.#processKeyValue(setting);
556618
keyValues.push([key, value]);
557619
}
620+
if (secretResolutionPromises.length > 0) {
621+
// wait for all secret resolution promises to be resolved
622+
await Promise.all(secretResolutionPromises);
623+
}
558624

559625
this.#clearLoadedKeyValues(); // clear existing key-values in case of configuration setting deletion
560626
for (const [k, v] of keyValues) {
@@ -598,7 +664,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
598664
*/
599665
async #loadFeatureFlags() {
600666
const loadFeatureFlag = true;
601-
const featureFlagSettings = await this.#loadConfigurationSettings(loadFeatureFlag);
667+
const featureFlagSettings: ConfigurationSetting[] = await this.#loadConfigurationSettings(loadFeatureFlag);
602668

603669
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
604670
// Reset old feature flag tracing in order to track the information present in the current response from server.
@@ -681,6 +747,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
681747
async #checkConfigurationSettingsChange(selectorCollection: SettingSelectorCollection): Promise<boolean> {
682748
const funcToExecute = async (client) => {
683749
for (const selector of selectorCollection.selectors) {
750+
if (selector.snapshotName) { // skip snapshot selector
751+
continue;
752+
}
684753
let listOptions: ListConfigurationSettingsOptions = {
685754
keyFilter: selector.keyFilter,
686755
labelFilter: selector.labelFilter
@@ -758,6 +827,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
758827
return response;
759828
}
760829

830+
async #getSnapshot(snapshotName: string, customOptions?: GetSnapshotOptions): Promise<GetSnapshotResponse | undefined> {
831+
const funcToExecute = async (client) => {
832+
return getSnapshotWithTrace(
833+
this.#requestTraceOptions,
834+
client,
835+
snapshotName,
836+
customOptions
837+
);
838+
};
839+
840+
let response: GetSnapshotResponse | undefined;
841+
try {
842+
response = await this.#executeWithFailoverPolicy(funcToExecute);
843+
} catch (error) {
844+
if (isRestError(error) && error.statusCode === 404) {
845+
response = undefined;
846+
} else {
847+
throw error;
848+
}
849+
}
850+
return response;
851+
}
852+
761853
// Only operations related to Azure App Configuration should be executed with failover policy.
762854
async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise<any>): Promise<any> {
763855
let clientWrappers = await this.#clientManager.getClients();
@@ -1016,11 +1108,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
10161108
}
10171109
}
10181110

1019-
function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
1020-
// below code deduplicates selectors by keyFilter and labelFilter, the latter selector wins
1111+
function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] {
1112+
// below code deduplicates selectors, the latter selector wins
10211113
const uniqueSelectors: SettingSelector[] = [];
10221114
for (const selector of selectors) {
1023-
const existingSelectorIndex = uniqueSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter);
1115+
const existingSelectorIndex = uniqueSelectors.findIndex(s => s.keyFilter === selector.keyFilter && s.labelFilter === selector.labelFilter && s.snapshotName === selector.snapshotName);
10241116
if (existingSelectorIndex >= 0) {
10251117
uniqueSelectors.splice(existingSelectorIndex, 1);
10261118
}
@@ -1029,14 +1121,20 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
10291121

10301122
return uniqueSelectors.map(selectorCandidate => {
10311123
const selector = { ...selectorCandidate };
1032-
if (!selector.keyFilter) {
1033-
throw new ArgumentError("Key filter cannot be null or empty.");
1034-
}
1035-
if (!selector.labelFilter) {
1036-
selector.labelFilter = LabelFilter.Null;
1037-
}
1038-
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
1039-
throw new ArgumentError("The characters '*' and ',' are not supported in label filters.");
1124+
if (selector.snapshotName) {
1125+
if (selector.keyFilter || selector.labelFilter) {
1126+
throw new ArgumentError("Key or label filter should not be used for a snapshot.");
1127+
}
1128+
} else {
1129+
if (!selector.keyFilter) {
1130+
throw new ArgumentError("Key filter cannot be null or empty.");
1131+
}
1132+
if (!selector.labelFilter) {
1133+
selector.labelFilter = LabelFilter.Null;
1134+
}
1135+
if (selector.labelFilter.includes("*") || selector.labelFilter.includes(",")) {
1136+
throw new ArgumentError("The characters '*' and ',' are not supported in label filters.");
1137+
}
10401138
}
10411139
return selector;
10421140
});
@@ -1047,7 +1145,7 @@ function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelect
10471145
// Default selector: key: *, label: \0
10481146
return [{ keyFilter: KeyFilter.Any, labelFilter: LabelFilter.Null }];
10491147
}
1050-
return getValidSelectors(selectors);
1148+
return getValidSettingSelectors(selectors);
10511149
}
10521150

10531151
function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSelector[] {
@@ -1056,7 +1154,9 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
10561154
return [{ keyFilter: `${featureFlagPrefix}${KeyFilter.Any}`, labelFilter: LabelFilter.Null }];
10571155
}
10581156
selectors.forEach(selector => {
1059-
selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`;
1157+
if (selector.keyFilter) {
1158+
selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`;
1159+
}
10601160
});
1061-
return getValidSelectors(selectors);
1161+
return getValidSettingSelectors(selectors);
10621162
}

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
}

0 commit comments

Comments
 (0)