Skip to content

Commit d07e3d1

Browse files
committed
snapshot reference support
1 parent f3d48ce commit d07e3d1

5 files changed

Lines changed: 119 additions & 24 deletions

File tree

azureappconfiguration/azureappconfiguration.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ func (azappcfg *AzureAppConfiguration) loadKeyValues(ctx context.Context, settin
385385
var useAIConfiguration, useAIChatCompletionConfiguration bool
386386
kvSettings := make(map[string]any, len(settingsResponse.settings))
387387
keyVaultRefs := make(map[string]string)
388+
snapshotRefs := make(map[string]string)
388389
for trimmedKey, setting := range rawSettings {
389390
if setting.ContentType == nil || setting.Value == nil {
390391
kvSettings[trimmedKey] = setting.Value
@@ -396,6 +397,9 @@ func (azappcfg *AzureAppConfiguration) loadKeyValues(ctx context.Context, settin
396397
continue // ignore feature flag while getting key value settings
397398
case secretReferenceContentType:
398399
keyVaultRefs[trimmedKey] = *setting.Value
400+
case snapshotReferenceContentType:
401+
snapshotRefs[trimmedKey] = *setting.Value
402+
azappcfg.tracingOptions.UseSnapshotReference = true
399403
default:
400404
if isJsonContentType(setting.ContentType) {
401405
var v any
@@ -424,6 +428,10 @@ func (azappcfg *AzureAppConfiguration) loadKeyValues(ctx context.Context, settin
424428
azappcfg.tracingOptions.UseAIConfiguration = useAIConfiguration
425429
azappcfg.tracingOptions.UseAIChatCompletionConfiguration = useAIChatCompletionConfiguration
426430

431+
if err := azappcfg.loadSettingsFromSnapshotRefs(ctx, settingsClient, snapshotRefs, kvSettings, keyVaultRefs); err != nil {
432+
return err
433+
}
434+
427435
secrets, err := azappcfg.loadKeyVaultSecrets(ctx, keyVaultRefs)
428436
if err != nil {
429437
return fmt.Errorf("failed to load Key Vault secrets: %w", err)
@@ -437,6 +445,58 @@ func (azappcfg *AzureAppConfiguration) loadKeyValues(ctx context.Context, settin
437445
return nil
438446
}
439447

448+
func (azappcfg *AzureAppConfiguration) loadSettingsFromSnapshotRefs(ctx context.Context, settingsClient settingsClient, snapshotRefs map[string]string, kvSettings map[string]any, keyVaultRefs map[string]string) error {
449+
for key, snapshotRef := range snapshotRefs {
450+
// Parse the snapshot reference
451+
snapshotName, err := parseSnapshotReference(snapshotRef)
452+
if err != nil {
453+
return fmt.Errorf("invalid format for Snapshot reference setting %s: %w", key, err)
454+
}
455+
456+
if client, ok := settingsClient.(*selectorSettingsClient); ok {
457+
// Load the snapshot settings
458+
settingsFromSnapshot, err := loadSnapshotSettings(ctx, client.client, snapshotName)
459+
if err != nil {
460+
return fmt.Errorf("failed to load snapshot settings: key=%s, error=%s", key, err.Error())
461+
}
462+
463+
for _, setting := range settingsFromSnapshot {
464+
if setting.ContentType == nil || setting.Value == nil || setting.Key == nil {
465+
continue
466+
}
467+
468+
if *setting.ContentType == featureFlagContentType {
469+
continue
470+
}
471+
472+
if *setting.ContentType == secretReferenceContentType {
473+
keyVaultRefs[*setting.Key] = *setting.Value
474+
continue
475+
}
476+
477+
// Handle JSON content types (similar to regular key-value loading)
478+
if isJsonContentType(setting.ContentType) {
479+
var v any
480+
if err := json.Unmarshal([]byte(*setting.Value), &v); err != nil {
481+
// If the value is not valid JSON, try to remove comments and parse again
482+
if err := json.Unmarshal(jsonc.StripComments([]byte(*setting.Value)), &v); err != nil {
483+
// If still invalid, log the error and treat it as a plain string
484+
log.Printf("Failed to unmarshal JSON value from snapshot: key=%s, error=%s", *setting.Key, err.Error())
485+
kvSettings[*setting.Key] = setting.Value
486+
continue
487+
}
488+
}
489+
kvSettings[*setting.Key] = v
490+
} else {
491+
kvSettings[*setting.Key] = setting.Value
492+
}
493+
}
494+
}
495+
}
496+
497+
return nil
498+
}
499+
440500
func (azappcfg *AzureAppConfiguration) loadKeyVaultSecrets(ctx context.Context, keyVaultRefs map[string]string) (map[string]any, error) {
441501
secrets := make(map[string]any)
442502
if len(keyVaultRefs) == 0 {
@@ -1019,3 +1079,20 @@ func isFailoverable(err error) bool {
10191079

10201080
return false
10211081
}
1082+
1083+
// "{\"snapshot_name\":\"referenced-snapshot\"}"
1084+
func parseSnapshotReference(ref string) (string, error) {
1085+
var snapshotRef struct {
1086+
SnapshotName string `json:"snapshot_name"`
1087+
}
1088+
1089+
if err := json.Unmarshal([]byte(ref), &snapshotRef); err != nil {
1090+
return "", fmt.Errorf("failed to parse snapshot reference: %w", err)
1091+
}
1092+
1093+
if snapshotRef.SnapshotName == "" {
1094+
return "", fmt.Errorf("snapshot_name is empty in snapshot reference")
1095+
}
1096+
1097+
return snapshotRef.SnapshotName, nil
1098+
}

azureappconfiguration/constants.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ const (
1414

1515
// General configuration constants
1616
const (
17-
defaultLabel = "\x00"
18-
wildCard = "*"
19-
defaultSeparator = "."
20-
secretReferenceContentType string = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
21-
featureFlagContentType string = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"
22-
featureFlagKeyPrefix string = ".appconfig.featureflag/"
23-
featureManagementSectionKey string = "feature_management"
24-
featureFlagSectionKey string = "feature_flags"
17+
defaultLabel = "\x00"
18+
wildCard = "*"
19+
defaultSeparator = "."
20+
secretReferenceContentType string = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
21+
snapshotReferenceContentType string = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8"
22+
featureFlagContentType string = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"
23+
featureFlagKeyPrefix string = ".appconfig.featureflag/"
24+
featureManagementSectionKey string = "feature_management"
25+
featureFlagSectionKey string = "feature_flags"
2526
)
2627

2728
// Feature flag constants

azureappconfiguration/internal/tracing/tracing.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343
FailoverRequestTag = "Failover"
4444
ReplicaCountKey = "ReplicaCount"
4545
LoadBalancingEnabledTag = "LB"
46+
SnapshotReferenceTag = "SnapshotRef"
4647

4748
// Feature flag usage tracing
4849
FMGoVerEnv = "MS_FEATURE_MANAGEMENT_GO_VERSION"
@@ -74,6 +75,7 @@ type Options struct {
7475
KeyVaultRefreshConfigured bool
7576
UseAIConfiguration bool
7677
UseAIChatCompletionConfiguration bool
78+
UseSnapshotReference bool
7779
IsFailoverRequest bool
7880
ReplicaCount int
7981
IsLoadBalancingEnabled bool
@@ -143,6 +145,10 @@ func CreateCorrelationContextHeader(ctx context.Context, options Options) http.H
143145
features = append(features, LoadBalancingEnabledTag)
144146
}
145147

148+
if options.UseSnapshotReference {
149+
features = append(features, SnapshotReferenceTag)
150+
}
151+
146152
if len(features) > 0 {
147153
featureStr := FeaturesKey + "=" + strings.Join(features, DelimiterPlus)
148154
output = append(output, featureStr)

azureappconfiguration/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type Selector struct {
9191
// comparableKey returns a comparable representation of the Selector that can be used as a map key.
9292
// This method creates a deterministic string representation by sorting the TagFilter slice.
9393
func (s Selector) comparableKey() comparableSelector {
94-
cs := comparableSelector{
94+
cs := comparableSelector{
9595
KeyFilter: s.KeyFilter,
9696
LabelFilter: s.LabelFilter,
9797
SnapshotName: s.SnapshotName,

azureappconfiguration/settings_client.go

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -86,24 +86,11 @@ func (s *selectorSettingsClient) getSettings(ctx context.Context) (*settingsResp
8686

8787
pageETags[filter.comparableKey()] = eTags
8888
} else {
89-
snapshot, err := s.client.GetSnapshot(ctx, filter.SnapshotName, nil)
89+
snapshotSettings, err := loadSnapshotSettings(ctx, s.client, filter.SnapshotName)
9090
if err != nil {
9191
return nil, err
9292
}
93-
94-
if snapshot.CompositionType == nil || *snapshot.CompositionType != azappconfig.CompositionTypeKey {
95-
return nil, fmt.Errorf("composition type for the selected snapshot '%s' must be 'key'", filter.SnapshotName)
96-
}
97-
98-
pager := s.client.NewListSettingsForSnapshotPager(filter.SnapshotName, nil)
99-
for pager.More() {
100-
page, err := pager.NextPage(ctx)
101-
if err != nil {
102-
return nil, err
103-
} else if page.Settings != nil {
104-
settings = append(settings, page.Settings...)
105-
}
106-
}
93+
settings = append(settings, snapshotSettings...)
10794
}
10895
}
10996

@@ -211,3 +198,27 @@ func (c *pageETagsClient) checkIfETagChanged(ctx context.Context) (bool, error)
211198

212199
return false, nil
213200
}
201+
202+
func loadSnapshotSettings(ctx context.Context, client *azappconfig.Client, snapshotName string) ([]azappconfig.Setting, error) {
203+
settings := make([]azappconfig.Setting, 0)
204+
snapshot, err := client.GetSnapshot(ctx, snapshotName, nil)
205+
if err != nil {
206+
return nil, err
207+
}
208+
209+
if snapshot.CompositionType == nil || *snapshot.CompositionType != azappconfig.CompositionTypeKey {
210+
return nil, fmt.Errorf("composition type for the selected snapshot '%s' must be 'key'", snapshotName)
211+
}
212+
213+
pager := client.NewListSettingsForSnapshotPager(snapshotName, nil)
214+
for pager.More() {
215+
page, err := pager.NextPage(ctx)
216+
if err != nil {
217+
return nil, err
218+
} else if page.Settings != nil {
219+
settings = append(settings, page.Settings...)
220+
}
221+
}
222+
223+
return settings, nil
224+
}

0 commit comments

Comments
 (0)