11// Copyright (c) Microsoft Corporation.
22// Licensed under the MIT license.
33
4- import { AppConfigurationClient , ConfigurationSetting , ConfigurationSettingId , GetConfigurationSettingOptions , GetConfigurationSettingResponse , ListConfigurationSettingsOptions , featureFlagPrefix , isFeatureFlag , isSecretReference } 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" ;
@@ -30,7 +43,14 @@ import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } fro
3043import { parseContentType , isJsonContentType , isFeatureFlagContentType , isSecretReferenceContentType } from "./common/contentType.js" ;
3144import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js" ;
3245import { RefreshTimer } from "./refresh/RefreshTimer.js" ;
33- import { RequestTracingOptions , getConfigurationSettingWithTrace , listConfigurationSettingsWithTrace , requestTracingEnabled } from "./requestTracing/utils.js" ;
46+ import {
47+ RequestTracingOptions ,
48+ getConfigurationSettingWithTrace ,
49+ listConfigurationSettingsWithTrace ,
50+ getSnapshotWithTrace ,
51+ listConfigurationSettingsForSnapshotWithTrace ,
52+ requestTracingEnabled
53+ } from "./requestTracing/utils.js" ;
3454import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js" ;
3555import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js" ;
3656import { KeyFilter , LabelFilter , SettingSelector } from "./types.js" ;
@@ -41,9 +61,6 @@ import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError
4161const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000 ; // 5 seconds
4262
4363type PagedSettingSelector = SettingSelector & {
44- /**
45- * Key: page eTag, Value: feature flag configurations
46- */
4764 pageEtags ?: string [ ] ;
4865} ;
4966
@@ -468,26 +485,49 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
468485 ) ;
469486
470487 for ( const selector of selectorsToUpdate ) {
471- const listOptions : ListConfigurationSettingsOptions = {
472- keyFilter : selector . keyFilter ,
473- labelFilter : selector . labelFilter
474- } ;
475-
476- const pageEtags : string [ ] = [ ] ;
477- const pageIterator = listConfigurationSettingsWithTrace (
478- this . #requestTraceOptions,
479- client ,
480- listOptions
481- ) . byPage ( ) ;
482- for await ( const page of pageIterator ) {
483- pageEtags . push ( page . etag ?? "" ) ;
484- for ( const setting of page . items ) {
485- if ( loadFeatureFlag === isFeatureFlag ( setting ) ) {
486- loadedSettings . push ( setting ) ;
488+ if ( selector . snapshotName === undefined ) {
489+ const listOptions : ListConfigurationSettingsOptions = {
490+ keyFilter : selector . keyFilter ,
491+ labelFilter : selector . labelFilter
492+ } ;
493+ const pageEtags : string [ ] = [ ] ;
494+ const pageIterator = listConfigurationSettingsWithTrace (
495+ this . #requestTraceOptions,
496+ client ,
497+ listOptions
498+ ) . byPage ( ) ;
499+
500+ for await ( const page of pageIterator ) {
501+ pageEtags . push ( page . etag ?? "" ) ;
502+ for ( const setting of page . items ) {
503+ if ( loadFeatureFlag === isFeatureFlag ( setting ) ) {
504+ loadedSettings . push ( setting ) ;
505+ }
506+ }
507+ }
508+ selector . pageEtags = pageEtags ;
509+ } else { // snapshot selector
510+ const snapshot = await this . #getSnapshot( selector . snapshotName ) ;
511+ if ( snapshot === undefined ) {
512+ throw new InvalidOperationError ( `Could not find snapshot with name ${ selector . snapshotName } .` ) ;
513+ }
514+ if ( snapshot . compositionType != KnownSnapshotComposition . Key ) {
515+ throw new InvalidOperationError ( `Composition type for the selected snapshot with name ${ selector . snapshotName } must be 'key'.` ) ;
516+ }
517+ const pageIterator = listConfigurationSettingsForSnapshotWithTrace (
518+ this . #requestTraceOptions,
519+ client ,
520+ selector . snapshotName
521+ ) . byPage ( ) ;
522+
523+ for await ( const page of pageIterator ) {
524+ for ( const setting of page . items ) {
525+ if ( loadFeatureFlag === isFeatureFlag ( setting ) ) {
526+ loadedSettings . push ( setting ) ;
527+ }
487528 }
488529 }
489530 }
490- selector . pageEtags = pageEtags ;
491531 }
492532
493533 if ( loadFeatureFlag ) {
@@ -678,6 +718,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
678718 async #checkConfigurationSettingsChange( selectors : PagedSettingSelector [ ] ) : Promise < boolean > {
679719 const funcToExecute = async ( client ) => {
680720 for ( const selector of selectors ) {
721+ if ( selector . snapshotName ) { // skip snapshot selector
722+ continue ;
723+ }
681724 const listOptions : ListConfigurationSettingsOptions = {
682725 keyFilter : selector . keyFilter ,
683726 labelFilter : selector . labelFilter ,
@@ -729,6 +772,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
729772 return response ;
730773 }
731774
775+ async #getSnapshot( snapshotName : string , customOptions ?: GetSnapshotOptions ) : Promise < GetSnapshotResponse | undefined > {
776+ const funcToExecute = async ( client ) => {
777+ return getSnapshotWithTrace (
778+ this . #requestTraceOptions,
779+ client ,
780+ snapshotName ,
781+ customOptions
782+ ) ;
783+ } ;
784+
785+ let response : GetSnapshotResponse | undefined ;
786+ try {
787+ response = await this . #executeWithFailoverPolicy( funcToExecute ) ;
788+ } catch ( error ) {
789+ if ( isRestError ( error ) && error . statusCode === 404 ) {
790+ response = undefined ;
791+ } else {
792+ throw error ;
793+ }
794+ }
795+ return response ;
796+ }
797+
732798 // Only operations related to Azure App Configuration should be executed with failover policy.
733799 async #executeWithFailoverPolicy( funcToExecute : ( client : AppConfigurationClient ) => Promise < any > ) : Promise < any > {
734800 let clientWrappers = await this . #clientManager. getClients ( ) ;
@@ -893,11 +959,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
893959 }
894960}
895961
896- function getValidSelectors ( selectors : SettingSelector [ ] ) : SettingSelector [ ] {
897- // below code deduplicates selectors by keyFilter and labelFilter , the latter selector wins
962+ function getValidSettingSelectors ( selectors : SettingSelector [ ] ) : SettingSelector [ ] {
963+ // below code deduplicates selectors, the latter selector wins
898964 const uniqueSelectors : SettingSelector [ ] = [ ] ;
899965 for ( const selector of selectors ) {
900- const existingSelectorIndex = uniqueSelectors . findIndex ( s => s . keyFilter === selector . keyFilter && s . labelFilter === selector . labelFilter ) ;
966+ const existingSelectorIndex = uniqueSelectors . findIndex ( s => s . keyFilter === selector . keyFilter && s . labelFilter === selector . labelFilter && s . snapshotName === selector . snapshotName ) ;
901967 if ( existingSelectorIndex >= 0 ) {
902968 uniqueSelectors . splice ( existingSelectorIndex , 1 ) ;
903969 }
@@ -906,14 +972,20 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
906972
907973 return uniqueSelectors . map ( selectorCandidate => {
908974 const selector = { ...selectorCandidate } ;
909- if ( ! selector . keyFilter ) {
910- throw new ArgumentError ( "Key filter cannot be null or empty." ) ;
911- }
912- if ( ! selector . labelFilter ) {
913- selector . labelFilter = LabelFilter . Null ;
914- }
915- if ( selector . labelFilter . includes ( "*" ) || selector . labelFilter . includes ( "," ) ) {
916- throw new ArgumentError ( "The characters '*' and ',' are not supported in label filters." ) ;
975+ if ( selector . snapshotName ) {
976+ if ( selector . keyFilter || selector . labelFilter ) {
977+ throw new ArgumentError ( "Key or label filter should not be used for a snapshot." ) ;
978+ }
979+ } else {
980+ if ( ! selector . keyFilter ) {
981+ throw new ArgumentError ( "Key filter cannot be null or empty." ) ;
982+ }
983+ if ( ! selector . labelFilter ) {
984+ selector . labelFilter = LabelFilter . Null ;
985+ }
986+ if ( selector . labelFilter . includes ( "*" ) || selector . labelFilter . includes ( "," ) ) {
987+ throw new ArgumentError ( "The characters '*' and ',' are not supported in label filters." ) ;
988+ }
917989 }
918990 return selector ;
919991 } ) ;
@@ -924,7 +996,7 @@ function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelect
924996 // Default selector: key: *, label: \0
925997 return [ { keyFilter : KeyFilter . Any , labelFilter : LabelFilter . Null } ] ;
926998 }
927- return getValidSelectors ( selectors ) ;
999+ return getValidSettingSelectors ( selectors ) ;
9281000}
9291001
9301002function getValidFeatureFlagSelectors ( selectors ?: SettingSelector [ ] ) : SettingSelector [ ] {
@@ -933,7 +1005,9 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
9331005 return [ { keyFilter : `${ featureFlagPrefix } ${ KeyFilter . Any } ` , labelFilter : LabelFilter . Null } ] ;
9341006 }
9351007 selectors . forEach ( selector => {
936- selector . keyFilter = `${ featureFlagPrefix } ${ selector . keyFilter } ` ;
1008+ if ( selector . keyFilter ) {
1009+ selector . keyFilter = `${ featureFlagPrefix } ${ selector . keyFilter } ` ;
1010+ }
9371011 } ) ;
938- return getValidSelectors ( selectors ) ;
1012+ return getValidSettingSelectors ( selectors ) ;
9391013}
0 commit comments