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" ;
@@ -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
10531151function 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}
0 commit comments