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" ;
518import { isRestError } from "@azure/core-rest-pipeline" ;
619import { AzureAppConfiguration , ConfigurationObjectConstructionOptions } from "./AzureAppConfiguration.js" ;
720import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js" ;
@@ -37,7 +50,14 @@ import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } fro
3750import { parseContentType , isJsonContentType , isFeatureFlagContentType , isSecretReferenceContentType } from "./common/contentType.js" ;
3851import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js" ;
3952import { 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" ;
4161import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js" ;
4262import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js" ;
4363import { 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
9771073function 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}
0 commit comments