Skip to content

Commit 9a81330

Browse files
committed
Load from Azure Front Door support
1 parent a4d86d9 commit 9a81330

7 files changed

Lines changed: 181 additions & 35 deletions

File tree

azureappconfiguration/azureappconfiguration.go

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"sync/atomic"
2929
"time"
3030

31+
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/afd"
3132
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/jsonc"
3233
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/refresh"
3334
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/tracing"
@@ -51,12 +52,13 @@ type AzureAppConfiguration struct {
5152
trimPrefixes []string
5253
watchedSettings []WatchedSetting
5354
loadBalancingEnabled bool
55+
afdUsed bool
5456

5557
// Settings used for refresh scenarios
5658
sentinelETags map[WatchedSetting]*azcore.ETag
5759
watchAll bool
58-
kvETags map[comparableSelector][]*azcore.ETag
59-
ffETags map[comparableSelector][]*azcore.ETag
60+
kvWatchers map[comparableSelector][]settingWatcher
61+
ffWatchers map[comparableSelector][]settingWatcher
6062
keyVaultRefs map[string]string // unversioned Key Vault references
6163
kvRefreshTimer refresh.Condition
6264
secretRefreshTimer refresh.Condition
@@ -117,11 +119,18 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
117119
credential: options.KeyVaultOptions.Credential,
118120
}
119121

122+
if authentication.Credential != nil {
123+
if _, ok := authentication.Credential.(*afd.EmptyTokenCredential); ok {
124+
azappcfg.afdUsed = true
125+
azappcfg.tracingOptions.AfdUsed = true
126+
}
127+
}
128+
120129
if options.RefreshOptions.Enabled {
121130
azappcfg.kvRefreshTimer = refresh.NewTimer(options.RefreshOptions.Interval)
122131
azappcfg.watchedSettings = normalizedWatchedSettings(options.RefreshOptions.WatchedSettings)
123132
azappcfg.sentinelETags = make(map[WatchedSetting]*azcore.ETag)
124-
azappcfg.kvETags = make(map[comparableSelector][]*azcore.ETag)
133+
azappcfg.kvWatchers = make(map[comparableSelector][]settingWatcher)
125134
if len(options.RefreshOptions.WatchedSettings) == 0 {
126135
azappcfg.watchAll = true
127136
}
@@ -137,7 +146,7 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
137146
azappcfg.ffSelectors = getFeatureFlagSelectors(deduplicateSelectors(options.FeatureFlagOptions.Selectors))
138147
if options.FeatureFlagOptions.RefreshOptions.Enabled {
139148
azappcfg.ffRefreshTimer = refresh.NewTimer(options.FeatureFlagOptions.RefreshOptions.Interval)
140-
azappcfg.ffETags = make(map[comparableSelector][]*azcore.ETag)
149+
azappcfg.ffWatchers = make(map[comparableSelector][]settingWatcher)
141150
}
142151
}
143152

@@ -150,6 +159,41 @@ func Load(ctx context.Context, authentication AuthenticationOptions, options *Op
150159
return azappcfg, nil
151160
}
152161

162+
func (azappcfg *AzureAppConfiguration) LoadFromAzureFrontDoor(ctx context.Context, endpoint string, options *Options) (*AzureAppConfiguration, error) {
163+
if options == nil {
164+
options = &Options{}
165+
}
166+
167+
if options.ReplicaDiscoveryEnabled != nil && *options.ReplicaDiscoveryEnabled {
168+
return nil, errors.New("replica discovery is not supported when loading from Azure Front Door. For guidance on how to take advantage of geo-replication when Azure Front Door is used, visit https://aka.ms/appconfig/geo-replication-with-afd")
169+
}
170+
171+
if options.LoadBalancingEnabled {
172+
return nil, errors.New("load balancing is not supported when loading from Azure Front Door. For guidance on how to take advantage of geo-replication when Azure Front Door is used, visit https://aka.ms/appconfig/geo-replication-with-afd")
173+
}
174+
175+
if options.RefreshOptions.Enabled &&
176+
len(options.RefreshOptions.WatchedSettings) > 0 {
177+
return nil, errors.New("specifying watched settings is not supported when loading from Azure Front Door. If refresh is enabled, all loaded configuration settings will be watched automatically")
178+
}
179+
180+
disableReplicaDiscovery := false
181+
options.ReplicaDiscoveryEnabled = &disableReplicaDiscovery
182+
183+
if options.ClientOptions == nil {
184+
options.ClientOptions = &azappconfig.ClientOptions{}
185+
}
186+
187+
options.ClientOptions.PerRetryPolicies = append(options.ClientOptions.PerRetryPolicies, afd.NewAnonymousRequestPipelinePolicy(), afd.NewRemoveSyncTokenPipelinePolicy())
188+
189+
authOptions := AuthenticationOptions{
190+
Endpoint: endpoint,
191+
Credential: afd.NewEmptyTokenCredential(),
192+
}
193+
194+
return Load(ctx, authOptions, options)
195+
}
196+
153197
// Unmarshal parses the configuration and stores the result in the value pointed to v. It builds a hierarchical configuration structure based on key separators.
154198
// It supports converting values to appropriate target types.
155199
//
@@ -432,7 +476,7 @@ func (azappcfg *AzureAppConfiguration) loadKeyValues(ctx context.Context, settin
432476
maps.Copy(kvSettings, secrets)
433477
azappcfg.keyValues = kvSettings
434478
azappcfg.keyVaultRefs = getUnversionedKeyVaultRefs(keyVaultRefs)
435-
azappcfg.kvETags = settingsResponse.pageETags
479+
azappcfg.kvWatchers = settingsResponse.pageWatchers
436480

437481
return nil
438482
}
@@ -509,7 +553,7 @@ func (azappcfg *AzureAppConfiguration) loadFeatureFlags(ctx context.Context, set
509553
},
510554
}
511555

512-
azappcfg.ffETags = settingsResponse.pageETags
556+
azappcfg.ffWatchers = settingsResponse.pageWatchers
513557
azappcfg.featureFlags = ffSettings
514558

515559
return nil
@@ -857,10 +901,10 @@ func normalizedWatchedSettings(s []WatchedSetting) []WatchedSetting {
857901
func (azappcfg *AzureAppConfiguration) newKeyValueRefreshClient(client *azappconfig.Client) refreshClient {
858902
var monitor eTagsClient
859903
if azappcfg.watchAll {
860-
monitor = &pageETagsClient{
904+
monitor = &pageWatcherClient{
861905
client: client,
862906
tracingOptions: azappcfg.tracingOptions,
863-
pageETags: azappcfg.kvETags,
907+
pageWatchers: azappcfg.kvWatchers,
864908
}
865909
} else {
866910
monitor = &watchedSettingClient{
@@ -892,10 +936,10 @@ func (azappcfg *AzureAppConfiguration) newFeatureFlagRefreshClient(client *azapp
892936
client: client,
893937
tracingOptions: azappcfg.tracingOptions,
894938
},
895-
monitor: &pageETagsClient{
939+
monitor: &pageWatcherClient{
896940
client: client,
897941
tracingOptions: azappcfg.tracingOptions,
898-
pageETags: azappcfg.ffETags,
942+
pageWatchers: azappcfg.ffWatchers,
899943
},
900944
}
901945
}

azureappconfiguration/azureappconfiguration_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515

1616
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/fm"
1717
"github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration/internal/tracing"
18-
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
1918
"github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig/v2"
2019
"github.com/stretchr/testify/assert"
2120
"github.com/stretchr/testify/mock"
@@ -128,7 +127,7 @@ func TestLoadFeatureFlags_Success(t *testing.T) {
128127
{Key: toPtr(".appconfig.featureflag/Beta"), Value: &value1, ContentType: toPtr(featureFlagContentType)},
129128
{Key: toPtr(".appconfig.featureflag/Alpha"), Value: &value2, ContentType: toPtr(featureFlagContentType)},
130129
},
131-
pageETags: map[comparableSelector][]*azcore.ETag{},
130+
pageWatchers: map[comparableSelector][]settingWatcher{},
132131
}
133132

134133
mockClient.On("getSettings", ctx).Return(mockResponse, nil)
@@ -1545,7 +1544,7 @@ func TestLoadFeatureFlags_TracingUpdated(t *testing.T) {
15451544
ContentType: toPtr(featureFlagContentType),
15461545
},
15471546
},
1548-
pageETags: map[comparableSelector][]*azcore.ETag{},
1547+
pageWatchers: map[comparableSelector][]settingWatcher{},
15491548
}
15501549

15511550
mockClient.On("getSettings", ctx).Return(mockResponse, nil)
@@ -1639,7 +1638,7 @@ func TestLoadKeyValues_WithTagFilter(t *testing.T) {
16391638
},
16401639
},
16411640
},
1642-
pageETags: map[comparableSelector][]*azcore.ETag{},
1641+
pageWatchers: map[comparableSelector][]settingWatcher{},
16431642
}
16441643

16451644
mockClient.On("getSettings", ctx).Return(mockResponse, nil)
@@ -1695,7 +1694,7 @@ func TestLoadKeyValues_WithMultipleTagFilters(t *testing.T) {
16951694
},
16961695
},
16971696
},
1698-
pageETags: map[comparableSelector][]*azcore.ETag{},
1697+
pageWatchers: map[comparableSelector][]settingWatcher{},
16991698
}
17001699

17011700
mockClient.On("getSettings", ctx).Return(mockResponse, nil)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package afd
5+
6+
import (
7+
"context"
8+
"math"
9+
"time"
10+
11+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
12+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
13+
)
14+
15+
type EmptyTokenCredential struct{}
16+
17+
func (e *EmptyTokenCredential) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) {
18+
return azcore.AccessToken{
19+
Token: "",
20+
ExpiresOn: time.Unix(math.MaxInt64, 0),
21+
}, nil
22+
}
23+
24+
func NewEmptyTokenCredential() azcore.TokenCredential {
25+
return &EmptyTokenCredential{}
26+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package afd
5+
6+
import (
7+
"net/http"
8+
9+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
10+
)
11+
12+
type AnonymousRequestPipelinePolicy struct{}
13+
14+
type RemoveSyncTokenPipelinePolicy struct{}
15+
16+
func NewAnonymousRequestPipelinePolicy() *AnonymousRequestPipelinePolicy {
17+
return &AnonymousRequestPipelinePolicy{}
18+
}
19+
20+
func NewRemoveSyncTokenPipelinePolicy() *RemoveSyncTokenPipelinePolicy {
21+
return &RemoveSyncTokenPipelinePolicy{}
22+
}
23+
24+
func (p *AnonymousRequestPipelinePolicy) Do(req *policy.Request) (*http.Response, error) {
25+
if req.Raw().Header.Get("Authorization") != "" {
26+
req.Raw().Header.Del("Authorization")
27+
}
28+
29+
return req.Next()
30+
}
31+
32+
func (p *RemoveSyncTokenPipelinePolicy) Do(req *policy.Request) (*http.Response, error) {
33+
if req.Raw().Header.Get("Sync-Token") != "" {
34+
req.Raw().Header.Del("Sync-Token")
35+
}
36+
37+
return req.Next()
38+
}

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+
AFDUsedTag = "AFD"
4647

4748
// Feature flag usage tracing
4849
FMGoVerEnv = "MS_FEATURE_MANAGEMENT_GO_VERSION"
@@ -77,6 +78,7 @@ type Options struct {
7778
IsFailoverRequest bool
7879
ReplicaCount int
7980
IsLoadBalancingEnabled bool
81+
AfdUsed bool
8082
FeatureFlagTracing *FeatureFlagTracing
8183
FMVersion string
8284
}
@@ -143,6 +145,10 @@ func CreateCorrelationContextHeader(ctx context.Context, options Options) http.H
143145
features = append(features, LoadBalancingEnabledTag)
144146
}
145147

148+
if options.AfdUsed {
149+
features = append(features, AFDUsedTag)
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,

0 commit comments

Comments
 (0)