Skip to content

Commit 79ab02c

Browse files
Merge pull request #66 from Azure/copilot/merge-release-v1-6-0
Merge release/v1.6.0 to main
2 parents b3170f6 + f04d633 commit 79ab02c

8 files changed

Lines changed: 461 additions & 26 deletions

File tree

azureappconfiguration/azureappconfiguration.go

Lines changed: 108 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,21 @@ func (azappcfg *AzureAppConfiguration) loadKeyValues(ctx context.Context, settin
424428
azappcfg.tracingOptions.UseAIConfiguration = useAIConfiguration
425429
azappcfg.tracingOptions.UseAIChatCompletionConfiguration = useAIChatCompletionConfiguration
426430

431+
if len(snapshotRefs) > 0 {
432+
var loadSnapshot snapshotSettingsLoader
433+
if client, ok := settingsClient.(*selectorSettingsClient); ok {
434+
loadSnapshot = func(ctx context.Context, snapshotName string) ([]azappconfig.Setting, error) {
435+
return loadSnapshotSettings(ctx, client.client, snapshotName)
436+
}
437+
}
438+
439+
if loadSnapshot != nil {
440+
if err := azappcfg.loadSettingsFromSnapshotRefs(ctx, loadSnapshot, snapshotRefs, kvSettings, keyVaultRefs); err != nil {
441+
return err
442+
}
443+
}
444+
}
445+
427446
secrets, err := azappcfg.loadKeyVaultSecrets(ctx, keyVaultRefs)
428447
if err != nil {
429448
return fmt.Errorf("failed to load Key Vault secrets: %w", err)
@@ -437,6 +456,78 @@ func (azappcfg *AzureAppConfiguration) loadKeyValues(ctx context.Context, settin
437456
return nil
438457
}
439458

459+
func (azappcfg *AzureAppConfiguration) loadSettingsFromSnapshotRefs(ctx context.Context, loadSnapshot snapshotSettingsLoader, snapshotRefs map[string]string, kvSettings map[string]any, keyVaultRefs map[string]string) error {
460+
var useAIConfiguration, useAIChatCompletionConfiguration bool
461+
for key, snapshotRef := range snapshotRefs {
462+
// Parse the snapshot reference
463+
snapshotName, err := parseSnapshotReference(snapshotRef)
464+
if err != nil {
465+
return fmt.Errorf("invalid format for Snapshot reference setting %s: %w", key, err)
466+
}
467+
468+
// Load the snapshot settings
469+
settingsFromSnapshot, err := loadSnapshot(ctx, snapshotName)
470+
if err != nil {
471+
return fmt.Errorf("failed to load snapshot settings: key=%s, error=%w", key, err)
472+
}
473+
474+
for _, setting := range settingsFromSnapshot {
475+
if setting.Key == nil {
476+
continue
477+
}
478+
479+
trimmedKey := azappcfg.trimPrefix(*setting.Key)
480+
if len(trimmedKey) == 0 {
481+
log.Printf("Key of the setting '%s' is trimmed to the empty string, just ignore it", *setting.Key)
482+
continue
483+
}
484+
485+
if setting.ContentType == nil || setting.Value == nil {
486+
kvSettings[trimmedKey] = setting.Value
487+
continue
488+
}
489+
490+
contentType := strings.TrimSpace(strings.ToLower(*setting.ContentType))
491+
if contentType == featureFlagContentType {
492+
continue
493+
}
494+
495+
if contentType == secretReferenceContentType {
496+
keyVaultRefs[trimmedKey] = *setting.Value
497+
continue
498+
}
499+
500+
// Handle JSON content types (similar to regular key-value loading)
501+
if isJsonContentType(setting.ContentType) {
502+
var v any
503+
if err := json.Unmarshal([]byte(*setting.Value), &v); err != nil {
504+
// If the value is not valid JSON, try to remove comments and parse again
505+
if err := json.Unmarshal(jsonc.StripComments([]byte(*setting.Value)), &v); err != nil {
506+
// If still invalid, log the error and treat it as a plain string
507+
log.Printf("Failed to unmarshal JSON value from snapshot: key=%s, error=%s", *setting.Key, err.Error())
508+
kvSettings[trimmedKey] = setting.Value
509+
continue
510+
}
511+
}
512+
kvSettings[trimmedKey] = v
513+
if isAIConfigurationContentType(setting.ContentType) {
514+
useAIConfiguration = true
515+
}
516+
if isAIChatCompletionContentType(setting.ContentType) {
517+
useAIChatCompletionConfiguration = true
518+
}
519+
} else {
520+
kvSettings[trimmedKey] = setting.Value
521+
}
522+
}
523+
}
524+
525+
azappcfg.tracingOptions.UseAIConfiguration = useAIConfiguration
526+
azappcfg.tracingOptions.UseAIChatCompletionConfiguration = useAIChatCompletionConfiguration
527+
528+
return nil
529+
}
530+
440531
func (azappcfg *AzureAppConfiguration) loadKeyVaultSecrets(ctx context.Context, keyVaultRefs map[string]string) (map[string]any, error) {
441532
secrets := make(map[string]any)
442533
if len(keyVaultRefs) == 0 {
@@ -1019,3 +1110,20 @@ func isFailoverable(err error) bool {
10191110

10201111
return false
10211112
}
1113+
1114+
// "{\"snapshot_name\":\"referenced-snapshot\"}"
1115+
func parseSnapshotReference(ref string) (string, error) {
1116+
var snapshotRef struct {
1117+
SnapshotName string `json:"snapshot_name"`
1118+
}
1119+
1120+
if err := json.Unmarshal([]byte(ref), &snapshotRef); err != nil {
1121+
return "", fmt.Errorf("failed to parse snapshot reference: %w", err)
1122+
}
1123+
1124+
if snapshotRef.SnapshotName == "" {
1125+
return "", fmt.Errorf("snapshot_name is empty in snapshot reference")
1126+
}
1127+
1128+
return snapshotRef.SnapshotName, nil
1129+
}

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/internal/tracing/tracing_test.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,70 @@ func TestCreateCorrelationContextHeader(t *testing.T) {
145145
assert.Contains(t, features, AIChatCompletionConfigurationTag)
146146
})
147147

148+
t.Run("with snapshot reference", func(t *testing.T) {
149+
ctx := context.Background()
150+
options := Options{
151+
UseSnapshotReference: true,
152+
}
153+
154+
header := CreateCorrelationContextHeader(ctx, options)
155+
156+
corrContext := header.Get(CorrelationContextHeader)
157+
assert.Contains(t, corrContext, FeaturesKey+"="+SnapshotReferenceTag)
158+
})
159+
160+
t.Run("with snapshot reference not set", func(t *testing.T) {
161+
ctx := context.Background()
162+
options := Options{
163+
UseSnapshotReference: false,
164+
}
165+
166+
header := CreateCorrelationContextHeader(ctx, options)
167+
168+
corrContext := header.Get(CorrelationContextHeader)
169+
assert.NotContains(t, corrContext, SnapshotReferenceTag)
170+
})
171+
172+
t.Run("with snapshot reference and other features", func(t *testing.T) {
173+
ctx := context.Background()
174+
options := Options{
175+
UseAIConfiguration: true,
176+
UseSnapshotReference: true,
177+
}
178+
179+
header := CreateCorrelationContextHeader(ctx, options)
180+
181+
corrContext := header.Get(CorrelationContextHeader)
182+
assert.Contains(t, corrContext, FeaturesKey+"=")
183+
184+
// Extract the Features part
185+
parts := strings.Split(corrContext, DelimiterComma)
186+
var featuresPart string
187+
for _, part := range parts {
188+
if strings.HasPrefix(part, FeaturesKey+"=") {
189+
featuresPart = part
190+
break
191+
}
192+
}
193+
194+
// Check both tags are in the features part
195+
assert.Contains(t, featuresPart, AIConfigurationTag)
196+
assert.Contains(t, featuresPart, SnapshotReferenceTag)
197+
198+
// Check the delimiter is correct
199+
features := strings.Split(strings.TrimPrefix(featuresPart, FeaturesKey+"="), DelimiterPlus)
200+
assert.Len(t, features, 2)
201+
assert.Contains(t, features, AIConfigurationTag)
202+
assert.Contains(t, features, SnapshotReferenceTag)
203+
})
204+
148205
t.Run("with all options", func(t *testing.T) {
149206
options := Options{
150207
Host: HostTypeAzureFunction,
151208
KeyVaultConfigured: true,
152209
UseAIConfiguration: true,
153210
UseAIChatCompletionConfiguration: true,
211+
UseSnapshotReference: true,
154212
}
155213

156214
header := CreateCorrelationContextHeader(context.Background(), options)
@@ -172,9 +230,10 @@ func TestCreateCorrelationContextHeader(t *testing.T) {
172230
}
173231
}
174232

175-
// Check both AI tags are in the features part
233+
// Check all feature tags are in the features part
176234
assert.Contains(t, featuresPart, AIConfigurationTag)
177235
assert.Contains(t, featuresPart, AIChatCompletionConfigurationTag)
236+
assert.Contains(t, featuresPart, SnapshotReferenceTag)
178237

179238
// Verify the header format
180239
assert.Equal(t, 4, strings.Count(corrContext, DelimiterComma)+1, "Should have 4 parts")

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: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ type eTagsClient interface {
5050
checkIfETagChanged(ctx context.Context) (bool, error)
5151
}
5252

53+
// snapshotSettingsLoader is a function type that loads settings from a snapshot by name.
54+
type snapshotSettingsLoader func(ctx context.Context, snapshotName string) ([]azappconfig.Setting, error)
55+
5356
type refreshClient struct {
5457
loader settingsClient
5558
monitor eTagsClient
@@ -86,24 +89,11 @@ func (s *selectorSettingsClient) getSettings(ctx context.Context) (*settingsResp
8689

8790
pageETags[filter.comparableKey()] = eTags
8891
} else {
89-
snapshot, err := s.client.GetSnapshot(ctx, filter.SnapshotName, nil)
92+
snapshotSettings, err := loadSnapshotSettings(ctx, s.client, filter.SnapshotName)
9093
if err != nil {
9194
return nil, err
9295
}
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-
}
96+
settings = append(settings, snapshotSettings...)
10797
}
10898
}
10999

@@ -211,3 +201,31 @@ func (c *pageETagsClient) checkIfETagChanged(ctx context.Context) (bool, error)
211201

212202
return false, nil
213203
}
204+
205+
func loadSnapshotSettings(ctx context.Context, client *azappconfig.Client, snapshotName string) ([]azappconfig.Setting, error) {
206+
settings := make([]azappconfig.Setting, 0)
207+
snapshot, err := client.GetSnapshot(ctx, snapshotName, nil)
208+
if err != nil {
209+
var respErr *azcore.ResponseError
210+
if errors.As(err, &respErr) && respErr.StatusCode == 404 {
211+
return settings, nil // treat non-existing snapshot as empty
212+
}
213+
return nil, err
214+
}
215+
216+
if snapshot.CompositionType == nil || *snapshot.CompositionType != azappconfig.CompositionTypeKey {
217+
return nil, fmt.Errorf("composition type for the selected snapshot '%s' must be 'key'", snapshotName)
218+
}
219+
220+
pager := client.NewListSettingsForSnapshotPager(snapshotName, nil)
221+
for pager.More() {
222+
page, err := pager.NextPage(ctx)
223+
if err != nil {
224+
return nil, err
225+
} else if page.Settings != nil {
226+
settings = append(settings, page.Settings...)
227+
}
228+
}
229+
230+
return settings, nil
231+
}

0 commit comments

Comments
 (0)